> */ public array $amazonRows = []; /** @var array> */ public array $rows = []; public ?int $activeStabileId = null; public ?string $activeStabileLabel = null; /** @var array> */ public array $invoiceRows = []; /** @var array> */ public array $serialRows = []; public ?int $selectedProductId = null; /** @var array|null */ public ?array $detailCard = null; public static function canAccess(): bool { $user = Auth::user(); return $user instanceof User && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore', 'fornitore']); } public function mount(): void { [$fornitore] = $this->resolveOperatoreContext(allowAdminWithoutSupplier: true); if (! $fornitore instanceof Fornitore) { $this->missingAdminContext = true; return; } $this->fornitoreId = (int) $fornitore->id; $this->fornitoreLabel = $this->getFornitoreLabel($fornitore); $this->amazonConfigured = app(AmazonCreatorsApiService::class)->isConfigured(); $requestedTab = (string) request()->query('tab', 'panoramica'); if ($this->isValidTab($requestedTab)) { $this->activeTab = $requestedTab; } $this->search = trim((string) request()->query('q', '')); $this->selectedProductId = request()->integer('product'); $user = Auth::user(); if ($user instanceof User) { $stabile = StabileContext::getActiveStabile($user); $this->activeStabileId = $stabile ? (int) $stabile->id : null; $this->activeStabileLabel = $stabile ? trim(implode(' - ', array_filter([ trim((string) ($stabile->codice_stabile ?? '')), trim((string) ($stabile->denominazione ?? '')), ]))) : null; } $this->refreshRows(); if (($this->selectedProductId ?? 0) > 0) { $this->openProductDetail((int) $this->selectedProductId); } } public function updatedSearch(): void { $this->refreshRows(); } public function setActiveTab(string $tab): void { if ($this->isValidTab($tab)) { $this->activeTab = $tab; } } public function refreshRows(): void { if (! $this->fornitoreId) { $this->rows = []; $this->serialRows = []; return; } $fornitore = Fornitore::query()->find((int) $this->fornitoreId); $supplierIds = $fornitore instanceof Fornitore ? $fornitore->catalogScopeSupplierIds() : [(int) $this->fornitoreId]; $term = trim($this->search); $normalized = preg_replace('/[^A-Za-z0-9]+/', '', strtoupper($term)) ?? ''; $products = Product::query() ->with(['identifiers', 'offers']) ->withCount(['identifiers', 'offers', 'serials']) ->where('is_active', true) ->where(function ($query) use ($supplierIds): void { $query->whereNull('default_fornitore_id') ->orWhereIn('default_fornitore_id', $supplierIds) ->orWhereHas('offers', fn($offerQuery) => $offerQuery->whereIn('fornitore_id', $supplierIds)); }) ->when($term !== '', function ($query) use ($term, $normalized): void { $query->where(function ($builder) use ($term, $normalized): void { $builder->where('name', 'like', '%' . $term . '%') ->orWhere('brand', 'like', '%' . $term . '%') ->orWhere('model', 'like', '%' . $term . '%') ->orWhere('internal_code', 'like', '%' . $term . '%'); if ($normalized !== '') { $builder->orWhereHas('identifiers', function ($identifierQuery) use ($normalized): void { $identifierQuery->where('normalized_code', $normalized) ->orWhere('code_value', 'like', '%' . $normalized . '%'); }); } }); }) ->limit(120) ->get(); $this->rows = $products->map(function (Product $product) use ($supplierIds): array { $identifier = $product->identifiers ->first(fn(ProductIdentifier $item): bool => in_array((int) ($item->fornitore_id ?? 0), $supplierIds, true)) ?? $product->identifiers->first(); $offer = $product->offers ->first(fn(ProductOffer $item): bool => in_array((int) ($item->fornitore_id ?? 0), $supplierIds, true)) ?? $product->offers->first(); return [ 'id' => (int) $product->id, 'internal_code' => (string) ($product->internal_code ?? ''), 'label' => trim(implode(' ', array_filter([(string) $product->name, (string) ($product->brand ?? ''), (string) ($product->model ?? '')]))), 'barcode' => (string) ($identifier->code_value ?? ''), 'source' => (string) ($offer?->source_name ?: 'Catalogo interno'), 'price' => $offer?->price_amount !== null ? number_format((float) $offer->price_amount, 2, ',', '.') . ' ' . (string) ($offer->currency ?? 'EUR') : '-', 'external_url' => (string) ($offer?->referral_url ?: $offer?->external_url ?: $this->buildAmazonSearchUrl((string) $product->name)), 'serials_count' => (int) ($product->serials_count ?? 0), 'offers_count' => (int) ($product->offers_count ?? 0), 'identifiers_count' => (int) ($product->identifiers_count ?? 0), ]; })->all(); if (($this->selectedProductId ?? 0) > 0 && ! collect($this->rows)->contains(fn(array $row): bool => (int) ($row['id'] ?? 0) === (int) $this->selectedProductId)) { $this->detailCard = null; } $this->refreshInvoiceRows($term, $normalized); $this->refreshSerialRows($term, $normalized); } public function getFornitoreSchedaUrl(): ?string { if (! $this->fornitoreId) { return null; } return \App\Filament\Pages\Gescon\FornitoreScheda::getUrl(['record' => (int) $this->fornitoreId], panel: 'admin-filament'); } public function getSerialiUrl(): string { return SerialiCatalogo::getUrl([ 'fornitore' => (int) ($this->fornitoreId ?? 0), 'q' => trim($this->search), ], panel: 'admin-filament'); } public function getContabilitaFornitoriUrl(): string { return \App\Filament\Pages\Contabilita\FornitoriArchivio::getUrl(panel: 'admin-filament'); } public function scanFattureElettronicheCatalogo(): void { if (! $this->fornitoreId || ! $this->activeStabileId) { Notification::make() ->title('Contesto fornitore o stabile non disponibile') ->warning() ->send(); return; } $fornitore = Fornitore::query()->find((int) $this->fornitoreId); if (! $fornitore instanceof Fornitore) { Notification::make()->title('Fornitore non trovato')->danger()->send(); return; } $limit = max(1, min(5000, (int) $this->feExtractionLimit)); $stats = app(FornitoreProductCatalogService::class) ->extractFromFattureElettroniche($fornitore, (int) $this->activeStabileId, $limit); $this->refreshRows(); $this->activeTab = 'seriali'; Notification::make() ->title('Scansione FE completata') ->body( 'Fatture: ' . (int) ($stats['fatture'] ?? 0) . ' · righe: ' . (int) ($stats['lines'] ?? 0) . ' · nuovi prodotti: ' . (int) ($stats['products'] ?? 0) . ' · identificativi: ' . (int) ($stats['identifiers'] ?? 0) . ' · righe agganciate: ' . (int) ($stats['linked_rows'] ?? 0) . ' · saltate: ' . (int) ($stats['skipped'] ?? 0) ) ->success() ->send(); } public function searchAmazonCatalog(): void { if (! $this->amazonConfigured) { Notification::make() ->title('Amazon Creators API non configurata') ->body('Valorizza prima le variabili AMAZON_CREATORS_* in ambiente.') ->danger() ->send(); return; } $query = trim($this->amazonQuery); if ($query === '') { Notification::make()->title('Inserisci una query Amazon')->warning()->send(); return; } try { $this->amazonRows = app(AmazonCreatorsCatalogSyncService::class) ->searchCatalog($query, 8); } catch (RuntimeException $e) { $this->amazonRows = []; Notification::make()->title('Ricerca Amazon non riuscita')->body($e->getMessage())->danger()->send(); return; } if ($this->amazonRows === []) { Notification::make()->title('Nessun risultato Amazon')->warning()->send(); } } public function saveAmazonCandidate(string $asin): void { $asin = trim($asin); $row = collect($this->amazonRows)->first(fn(array $item): bool => (string) ($item['asin'] ?? '') === $asin); if (! is_array($row) || ! is_array($row['item_payload'] ?? null)) { Notification::make()->title('Risultato Amazon non disponibile')->danger()->send(); return; } $fornitore = $this->resolveCurrentFornitore(); if (! $fornitore instanceof Fornitore) { Notification::make()->title('Fornitore non trovato')->danger()->send(); return; } try { $result = app(AmazonCreatorsCatalogSyncService::class) ->importItemForSupplier($fornitore, $row['item_payload'], [ 'source' => 'amazon_catalog_search_ui', 'search_query' => trim($this->amazonQuery), ]); } catch (RuntimeException $e) { Notification::make()->title('Import Amazon non riuscito')->body($e->getMessage())->danger()->send(); return; } $product = $result['product'] ?? null; Notification::make() ->title(! empty($result['created']) ? 'Prodotto Amazon memorizzato' : 'Prodotto Amazon aggiornato') ->body($product instanceof Product ? 'Articolo salvato nel catalogo come candidato da verificare: ' . ((string) ($product->internal_code ?? '') !== '' ? $product->internal_code . ' · ' : '') . (string) ($product->name ?? '') : 'Import completato.') ->success() ->send(); $this->refreshRows(); $this->searchAmazonCatalog(); } public function saveAllAmazonCandidates(): void { if ($this->amazonRows === []) { Notification::make()->title('Nessun risultato Amazon da salvare')->warning()->send(); return; } $fornitore = $this->resolveCurrentFornitore(); if (! $fornitore instanceof Fornitore) { Notification::make()->title('Fornitore non trovato')->danger()->send(); return; } $result = app(AmazonCreatorsCatalogSyncService::class)->importSearchResultsForSupplier($fornitore, $this->amazonRows, [ 'source' => 'amazon_catalog_search_bulk_ui', 'search_query' => trim($this->amazonQuery), ]); $body = 'Nuovi: ' . (int) ($result['created'] ?? 0) . ' · aggiornati: ' . (int) ($result['updated'] ?? 0) . ' · saltati: ' . (int) ($result['skipped'] ?? 0); $errors = array_values(array_filter(array_map(static fn(mixed $value): string => trim((string) $value), (array) ($result['errors'] ?? [])))); if ($errors !== []) { $body .= ' · errori: ' . count($errors) . ' (' . Str::limit(implode(' | ', $errors), 180) . ')'; } Notification::make() ->title('Import Amazon completato') ->body($body) ->success() ->send(); $this->refreshRows(); $this->searchAmazonCatalog(); } public function getLavorazioniUrl(): string { return LavorazioniOperative::getUrl(['fornitore' => (int) $this->fornitoreId], panel: 'admin-filament'); } public function openProductDetail(int $productId): void { $scopeSupplierIds = [(int) $this->fornitoreId]; if ($this->fornitoreId) { $fornitore = Fornitore::query()->find((int) $this->fornitoreId); if ($fornitore instanceof Fornitore) { $scopeSupplierIds = $fornitore->catalogScopeSupplierIds(); } } $product = Product::query() ->with([ 'identifiers.fornitore' => fn($query) => $query->orderBy('ragione_sociale')->orderBy('nome')->orderBy('cognome'), 'identifiers' => fn($query) => $query->orderBy('code_type')->orderBy('code_value'), 'offers.fornitore' => fn($query) => $query->orderBy('ragione_sociale')->orderBy('nome')->orderBy('cognome'), 'offers' => fn($query) => $query->orderByDesc('last_seen_at')->orderByDesc('id'), 'serials' => fn($query) => $query->orderByDesc('purchase_date')->orderByDesc('id')->limit(8), ]) ->find($productId); if (! $product instanceof Product) { $this->selectedProductId = null; $this->detailCard = null; return; } $identifiers = $product->identifiers ->filter(fn(ProductIdentifier $identifier): bool => (int) ($identifier->fornitore_id ?? 0) === 0 || in_array((int) ($identifier->fornitore_id ?? 0), $scopeSupplierIds, true)) ->values(); $offers = $product->offers ->filter(fn(ProductOffer $offer): bool => (int) ($offer->fornitore_id ?? 0) === 0 || in_array((int) ($offer->fornitore_id ?? 0), $scopeSupplierIds, true)) ->values(); $linkedSuppliers = collect() ->merge($identifiers->pluck('fornitore')) ->merge($offers->pluck('fornitore')) ->filter(fn(mixed $supplier): bool => $supplier instanceof Fornitore) ->unique(fn(Fornitore $supplier): int => (int) $supplier->id) ->map(fn(Fornitore $supplier): array=> [ 'id' => (int) $supplier->id, 'label' => $this->getFornitoreLabel($supplier), ]) ->values() ->all(); $this->selectedProductId = (int) $product->id; $this->detailCard = [ 'id' => (int) $product->id, 'internal_code' => (string) ($product->internal_code ?? ''), 'name' => (string) ($product->name ?? ''), 'brand' => (string) ($product->brand ?? ''), 'model' => (string) ($product->model ?? ''), 'description' => (string) ($product->description ?? ''), 'track_serials' => (bool) ($product->track_serials ?? false), 'linked_suppliers' => $linkedSuppliers, 'scope_supplier_ids' => $scopeSupplierIds, 'identifiers' => $identifiers->map(fn(ProductIdentifier $identifier): array=> [ 'type' => (string) ($identifier->code_type ?? ''), 'role' => (string) ($identifier->code_role ?? ''), 'value' => (string) ($identifier->code_value ?? ''), 'supplier' => $identifier->fornitore instanceof Fornitore ? $this->getFornitoreLabel($identifier->fornitore) : '', ])->all(), 'offers' => $offers->map(fn(ProductOffer $offer): array=> [ 'source' => (string) ($offer->source_name ?? 'Catalogo interno'), 'title' => (string) ($offer->title ?? ''), 'price' => $offer->price_amount !== null ? number_format((float) $offer->price_amount, 2, ',', '.') . ' ' . (string) ($offer->currency ?? 'EUR') : '-', 'availability' => (string) ($offer->availability ?? ''), 'url' => (string) ($offer->referral_url ?: $offer->external_url ?: ''), 'supplier' => $offer->fornitore instanceof Fornitore ? $this->getFornitoreLabel($offer->fornitore) : '', ])->all(), 'serials' => $product->serials->map(fn(ProductSerial $serial): array=> [ 'serial' => (string) ($serial->serial_number ?? $serial->serial_number_2 ?? ''), 'invoice' => (string) ($serial->purchase_invoice_ref ?? ''), 'date' => $serial->purchase_date?->format('d/m/Y') ?: '', 'price' => $serial->purchase_price !== null ? number_format((float) $serial->purchase_price, 2, ',', '.') . ' ' . (string) ($serial->purchase_currency ?? 'EUR') : '-', ])->all(), ]; } public function getRubricaUrl(): string { return RubricaClienti::getUrl(['fornitore' => (int) $this->fornitoreId], panel: 'admin-filament'); } protected function buildAmazonSearchUrl(string $term): string { $query = ['k' => trim($term)]; $tag = trim((string) config('services.amazon.referral_tag', '')); if ($tag !== '') { $query['tag'] = $tag; } return 'https://www.amazon.it/s?' . http_build_query($query); } private function refreshInvoiceRows(string $term, string $normalized): void { $this->invoiceRows = []; if ( $term === '' || ! $this->activeStabileId || ! Schema::hasTable('contabilita_fatture_fornitori_righe') || ! Schema::hasTable('contabilita_fatture_fornitori') ) { return; } $righe = FatturaFornitoreRiga::query() ->with([ 'fattura:id,stabile_id,fornitore_id,fattura_elettronica_id,data_documento,numero_documento', 'fattura.fornitore:id,ragione_sociale,nome,cognome', 'product:id,internal_code,name', ]) ->whereHas('fattura', fn(Builder $query) => $query->where('stabile_id', (int) $this->activeStabileId)) ->where(function (Builder $query) use ($term, $normalized): void { $query->where('descrizione', 'like', '%' . $term . '%'); if ($normalized !== '') { $query->orWhereHas('product', function (Builder $productQuery) use ($term, $normalized): void { $productQuery->where('internal_code', 'like', '%' . $term . '%') ->orWhere('name', 'like', '%' . $term . '%') ->orWhereHas('identifiers', function (Builder $identifierQuery) use ($normalized): void { $identifierQuery->where('normalized_code', $normalized) ->orWhere('code_value', 'like', '%' . $normalized . '%'); }); }); } }) ->orderByDesc('id') ->limit(24) ->get(); $this->invoiceRows = $righe->map(function (FatturaFornitoreRiga $riga): array { $fattura = $riga->fattura; $fornitore = $fattura?->fornitore; $fornitoreLabel = trim((string) ($fornitore?->ragione_sociale ?? '')); if ($fornitoreLabel === '') { $fornitoreLabel = trim(implode(' ', array_filter([ trim((string) ($fornitore?->nome ?? '')), trim((string) ($fornitore?->cognome ?? '')), ]))); } return [ 'fattura_id' => (int) ($fattura?->id ?? 0), 'fattura_elettronica_id' => (int) ($fattura?->fattura_elettronica_id ?? 0), 'fornitore' => $fornitoreLabel !== '' ? $fornitoreLabel : 'Fornitore non associato', 'numero_documento' => trim((string) ($fattura?->numero_documento ?? '')), 'data_documento' => $fattura?->data_documento?->format('d/m/Y') ?: '', 'descrizione' => trim((string) ($riga->descrizione ?? '')), 'totale_euro' => is_numeric($riga->totale_euro) ? (float) $riga->totale_euro : null, 'product_id' => (int) ($riga->product_id ?? 0), 'product_label' => trim(implode(' · ', array_filter([ trim((string) ($riga->product?->internal_code ?? '')), trim((string) ($riga->product?->name ?? '')), ]))), 'fe_url' => ($fattura?->fattura_elettronica_id ?? null) ? FatturaElettronicaScheda::getUrl(['record' => (int) $fattura->fattura_elettronica_id], panel : 'admin-filament') : null, ]; })->all(); } private function refreshSerialRows(string $term, string $normalized): void { if (! $this->fornitoreId) { $this->serialRows = []; return; } $rows = ProductSerial::query() ->with(['product:id,internal_code,name,brand,model']) ->where('fornitore_id', (int) $this->fornitoreId) ->when($term !== '', function ($query) use ($term, $normalized): void { $query->where(function ($builder) use ($term, $normalized): void { $builder->where('serial_number', 'like', '%' . $term . '%') ->orWhere('serial_number_2', 'like', '%' . $term . '%') ->orWhere('product_model', 'like', '%' . $term . '%') ->orWhere('product_code', 'like', '%' . $term . '%') ->orWhere('purchase_invoice_ref', 'like', '%' . $term . '%'); if ($normalized !== '') { $builder->orWhereHas('product', function (Builder $productQuery) use ($term, $normalized): void { $productQuery->where('internal_code', 'like', '%' . $term . '%') ->orWhere('name', 'like', '%' . $term . '%') ->orWhereHas('identifiers', function (Builder $identifierQuery) use ($normalized): void { $identifierQuery->where('normalized_code', $normalized) ->orWhere('code_value', 'like', '%' . $normalized . '%'); }); }); } }); }) ->orderByDesc('purchase_date') ->orderByDesc('id') ->limit(40) ->get(); $this->serialRows = $rows->map(function (ProductSerial $serial): array { return [ 'id' => (int) $serial->id, 'serial_number' => (string) ($serial->serial_number ?? ''), 'serial_number_2' => (string) ($serial->serial_number_2 ?? ''), 'product_label' => trim(implode(' · ', array_filter([ trim((string) ($serial->product?->internal_code ?? '')), trim((string) ($serial->product?->name ?? '')), trim((string) ($serial->product_model ?? '')), ]))), 'purchase_invoice_ref' => (string) ($serial->purchase_invoice_ref ?? ''), 'purchase_date' => $serial->purchase_date?->format('d/m/Y') ?: '', 'purchase_price' => $serial->purchase_price !== null ? (float) $serial->purchase_price : null, 'purchase_currency' => (string) ($serial->purchase_currency ?? 'EUR'), 'source' => (string) ($serial->source ?? ''), ]; })->all(); } private function isValidTab(string $tab): bool { return in_array($tab, ['panoramica', 'catalogo', 'seriali', 'acquisti'], true); } private function resolveCurrentFornitore(): ?Fornitore { return $this->fornitoreId ? Fornitore::query()->find((int) $this->fornitoreId) : null; } /** @return array> */ public function getAmazonSetupChecklistProperty(): array { $items = [ ['key' => 'AMAZON_CREATORS_ENABLED', 'required' => true, 'value' => (bool) config('services.amazon.creators_enabled', false) ? 'true' : 'false'], ['key' => 'AMAZON_CREATORS_API_BASE_URL', 'required' => true, 'value' => (string) config('services.amazon.creators_api_base_url', '')], ['key' => 'AMAZON_CREATORS_TOKEN_URL', 'required' => true, 'value' => (string) config('services.amazon.creators_token_url', '')], ['key' => 'AMAZON_CREATORS_CREDENTIAL_ID', 'required' => true, 'value' => (string) config('services.amazon.creators_credential_id', '')], ['key' => 'AMAZON_CREATORS_CREDENTIAL_SECRET', 'required' => true, 'value' => (string) config('services.amazon.creators_credential_secret', '')], ['key' => 'AMAZON_CREATORS_SEARCH_PATH', 'required' => true, 'value' => (string) data_get(config('services.amazon.creators_paths', []), 'searchItems', '')], ['key' => 'AMAZON_CREATORS_GET_ITEMS_PATH', 'required' => true, 'value' => (string) data_get(config('services.amazon.creators_paths', []), 'getItems', '')], ['key' => 'AMAZON_MARKETPLACE', 'required' => false, 'value' => (string) config('services.amazon.marketplace', 'www.amazon.it')], ['key' => 'AMAZON_REFERRAL_TAG', 'required' => false, 'value' => (string) config('services.amazon.referral_tag', '')], ]; return array_map(static function (array $item): array { $rawValue = trim((string) ($item['value'] ?? '')); $isBoolean = ($item['key'] ?? '') === 'AMAZON_CREATORS_ENABLED'; $configured = $isBoolean ? $rawValue === 'true' : $rawValue !== ''; return [ 'key' => (string) ($item['key'] ?? ''), 'required' => (bool) ($item['required'] ?? false), 'configured' => $configured, 'display' => $rawValue !== '' ? Str::limit($rawValue, 54) : 'non valorizzata', ]; }, $items); } }