merge($this->searchIeLineTemplates($business, $listing)); $candidates = $candidates->merge($this->searchIeItemMasters($business, $listing)); $candidates = $candidates->merge($this->searchInstalledAssets($business, $listing)); $candidates = $candidates->merge($this->searchCmmsEquipment($business, $listing)); return DB::transaction(function () use ($business, $listing, $replaceSuggested, $candidates) { if ($replaceSuggested) { CatalogMatch::where('listing_id', $listing->id) ->where('status', 'suggested') ->delete(); } $saved = collect(); foreach ($candidates->sortByDesc('score')->take(15) as $row) { $match = CatalogMatch::updateOrCreate( [ 'listing_id' => $listing->id, 'catalog_type' => $row['catalog_type'], 'catalog_id' => $row['catalog_id'], ], [ 'business_id' => $business->id, 'catalog_label' => $row['label'], 'match_score' => $row['score'], 'match_reason' => $row['reason'], 'status' => 'suggested', ] ); $saved->push($match); } return $saved; }); } public function confirmMatch(Business $business, CatalogMatch $match, ?User $user = null): Listing { $match->loadMissing('listing'); $listing = $match->listing; if (! $listing || $listing->business_id !== $business->id) { throw new \RuntimeException('عرضه نامعتبر است.'); } return DB::transaction(function () use ($business, $match, $listing, $user) { $match->update([ 'status' => 'confirmed', 'confirmed_by' => $user?->id, 'confirmed_at' => now(), ]); $updates = match ($match->catalog_type) { 'ie_line_template' => ['ie_line_template_id' => $match->catalog_id], 'ie_item_master' => [], 'ie_installed_asset' => ['ie_installed_asset_id' => $match->catalog_id], 'maintenance_equipment' => ['maintenance_equipment_id' => $match->catalog_id], default => [], }; if ($match->catalog_type === 'ie_line_template' && Schema::hasTable('ie_line_revisions')) { $revision = DB::table('ie_line_revisions') ->where('business_id', $business->id) ->where('line_template_id', $match->catalog_id) ->orderByDesc('id') ->first(); if ($revision) { $updates['ie_line_revision_id'] = $revision->id; if (Schema::hasTable('ie_boms')) { $bom = DB::table('ie_boms') ->where('business_id', $business->id) ->where('line_revision_id', $revision->id) ->orderByDesc('id') ->first(); if ($bom) { $updates['ie_bom_id'] = $bom->id; } } } } if (! empty($updates)) { $listing->update($updates); } CatalogMatch::where('listing_id', $listing->id) ->where('id', '!=', $match->id) ->where('catalog_type', $match->catalog_type) ->where('status', 'suggested') ->update(['status' => 'rejected']); return $listing->fresh(['catalogMatches']); }); } public function searchCatalog(Business $business, string $term, ?string $type = null): array { $listing = new Listing([ 'business_id' => $business->id, 'title' => $term, 'manufacturer' => $term, 'model' => $term, ]); $all = collect(); if (! $type || $type === 'ie_line_template') { $all = $all->merge($this->searchIeLineTemplates($business, $listing, 30)); } if (! $type || $type === 'ie_item_master') { $all = $all->merge($this->searchIeItemMasters($business, $listing, 30)); } if (! $type || $type === 'maintenance_equipment') { $all = $all->merge($this->searchCmmsEquipment($business, $listing, 30)); } if (! $type || $type === 'ie_installed_asset') { $all = $all->merge($this->searchInstalledAssets($business, $listing, 30)); } return $all->sortByDesc('score')->take(25)->values()->all(); } protected function searchIeLineTemplates(Business $business, Listing $listing, int $limit = 10): Collection { if (! Schema::hasTable('ie_line_templates')) { return collect(); } $terms = $this->searchTerms($listing); $query = DB::table('ie_line_templates')->where('business_id', $business->id); $query->where(function ($q) use ($terms) { foreach ($terms as $term) { $q->orWhere('name', 'like', '%'.$term.'%') ->orWhere('code', 'like', '%'.$term.'%'); } }); return $query->limit($limit)->get()->map(function ($row) use ($terms) { return [ 'catalog_type' => 'ie_line_template', 'catalog_id' => $row->id, 'label' => $row->name.' ('.$row->code.')', 'score' => $this->scoreMatch($row->name.' '.$row->code, $terms), 'reason' => 'تطبیق نام/کد خط تولید در IE', ]; }); } protected function searchIeItemMasters(Business $business, Listing $listing, int $limit = 10): Collection { if (! Schema::hasTable('ie_item_masters')) { return collect(); } $terms = $this->searchTerms($listing); $query = DB::table('ie_item_masters')->where('business_id', $business->id); $query->where(function ($q) use ($terms, $listing) { foreach ($terms as $term) { $q->orWhere('name', 'like', '%'.$term.'%') ->orWhere('item_code', 'like', '%'.$term.'%'); } if ($listing->manufacturer) { $q->orWhere('manufacturer', 'like', '%'.$listing->manufacturer.'%'); } }); return $query->limit($limit)->get()->map(function ($row) use ($terms) { $label = $row->name; if (! empty($row->item_code)) { $label .= ' ['.$row->item_code.']'; } return [ 'catalog_type' => 'ie_item_master', 'catalog_id' => $row->id, 'label' => $label, 'score' => $this->scoreMatch($row->name.' '.($row->item_code ?? ''), $terms), 'reason' => 'تطبیق آیتم/قطعه/ماشین در بانک IE', ]; }); } protected function searchInstalledAssets(Business $business, Listing $listing, int $limit = 10): Collection { if (! Schema::hasTable('ie_customer_installed_assets')) { return collect(); } $terms = $this->searchTerms($listing); $query = DB::table('ie_customer_installed_assets')->where('business_id', $business->id); $query->where(function ($q) use ($terms) { foreach ($terms as $term) { $q->orWhere('asset_tag', 'like', '%'.$term.'%') ->orWhere('site_name', 'like', '%'.$term.'%'); } }); return $query->limit($limit)->get()->map(function ($row) use ($terms) { return [ 'catalog_type' => 'ie_installed_asset', 'catalog_id' => $row->id, 'label' => ($row->asset_tag ?? 'Asset').' — '.($row->site_name ?? ''), 'score' => $this->scoreMatch(($row->asset_tag ?? '').' '.($row->site_name ?? ''), $terms), 'reason' => 'تطبیق دارایی نصب‌شده مشتری در IE', ]; }); } protected function searchCmmsEquipment(Business $business, Listing $listing, int $limit = 10): Collection { if (! Schema::hasTable('maintenance_equipment')) { return collect(); } $terms = $this->searchTerms($listing); $query = DB::table('maintenance_equipment')->where('business_id', $business->id); $query->where(function ($q) use ($terms, $listing) { foreach ($terms as $term) { $q->orWhere('name', 'like', '%'.$term.'%') ->orWhere('code', 'like', '%'.$term.'%') ->orWhere('model', 'like', '%'.$term.'%') ->orWhere('serial_number', 'like', '%'.$term.'%'); } if ($listing->serial_number) { $q->orWhere('serial_number', $listing->serial_number); } }); return $query->limit($limit)->get()->map(function ($row) use ($terms, $listing) { $score = $this->scoreMatch($row->name.' '.($row->model ?? '').' '.($row->serial_number ?? ''), $terms); if ($listing->serial_number && $row->serial_number === $listing->serial_number) { $score = 100; } return [ 'catalog_type' => 'maintenance_equipment', 'catalog_id' => $row->id, 'label' => $row->name.' / '.($row->model ?? '—'), 'score' => $score, 'reason' => 'تطبیق تجهیز CMMS', ]; }); } protected function searchTerms(Listing $listing): array { $parts = array_filter([ $listing->title, $listing->manufacturer, $listing->model, $listing->serial_number, ]); $terms = []; foreach ($parts as $part) { foreach (preg_split('/\s+/', (string) $part) as $word) { $word = trim($word); if (strlen($word) >= 2) { $terms[] = $word; } } } return array_values(array_unique($terms)); } protected function scoreMatch(string $haystack, array $terms): float { if (empty($terms)) { return 0; } $haystack = mb_strtolower($haystack); $hits = 0; foreach ($terms as $term) { if (str_contains($haystack, mb_strtolower($term))) { $hits++; } } return round(min(100, ($hits / count($terms)) * 100), 2); } }