*/ public array $box = []; /** @var array> */ public array $anteprimaAde = []; /** @var array> */ public array $anteprimaFe = []; /** @var array> */ public array $anteprimaContabilita = []; /** @var array> */ public array $fattureAssociate = []; /** @var array> */ public array $raVersateRows = []; /** @var array> */ public array $catalogoProdottiRows = []; /** @var array> */ public array $tecnorepairRows = []; public string $tagsInput = ''; /** @var array */ public array $tagSuggestions = []; /** @var array> */ public array $dipendentiRows = []; public string $nuovoDipendenteNome = ''; public string $nuovoDipendenteCognome = ''; public string $nuovoDipendenteEmail = ''; public string $nuovoDipendenteTelefono = ''; public string $dipendenteRubricaSearch = ''; public ?int $dipendenteRubricaId = null; public string $sectionTab = 'profilo'; /** @var array> */ public array $dipendenteRubricaMatches = []; public ?string $lastGeneratedPassword = null; public function cleanDisplayValue(?string $value, string $fallback = '-'): string { $v = trim((string) $value); if ($v === '') { return $fallback; } $upper = strtoupper($v); if (in_array($upper, ['NULL', 'N/D', 'ND', 'N.A.', '-', '--', '[]', '{}'], true)) { return $fallback; } return $v; } public function mount(int | string $record): void { $user = Auth::user(); if (! $user instanceof User) { abort(403); } if (! $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore', 'fornitore'])) { abort(403); } if ($user->hasRole('fornitore') && ! $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore'])) { [$resolvedFornitore] = $this->resolveOperatoreContext((int) $record); $this->fornitore = $resolvedFornitore->loadMissing(['rubrica', 'amministratore']); } else { $this->fornitore = Fornitore::query()->with(['rubrica', 'amministratore'])->findOrFail((int) $record); // Tenant guard: impedisce accesso cross-amministratore via URL. if (! $user->hasAnyRole(['super-admin', 'admin'])) { $allowedAdminIds = []; $activeStabile = StabileContext::getActiveStabile($user); $activeAdminId = (int) ($activeStabile?->amministratore_id ?: 0); if ($activeAdminId > 0) { $allowedAdminIds[] = $activeAdminId; } $userAdminId = (int) ($user->amministratore?->id ?: 0); if ($userAdminId > 0) { $allowedAdminIds[] = $userAdminId; } $ownerAdminId = (int) ($user->amministratoreOwner?->id ?: 0); if ($ownerAdminId > 0) { $allowedAdminIds[] = $ownerAdminId; } // Collaboratori: includi anche gli amministratori degli stabili assegnati. try { $assigned = $user->stabiliAssegnati()->pluck('amministratore_id')->all(); foreach ($assigned as $aid) { $aid = (int) $aid; if ($aid > 0) { $allowedAdminIds[] = $aid; } } } catch (\Throwable) { // ignore } $allowedAdminIds = array_values(array_unique(array_filter($allowedAdminIds, fn($v) => (int) $v > 0))); $fornitoreAdminIds = array_values(array_unique(array_filter([ (int) ($this->fornitore->amministratore_id ?? 0), (int) ($this->fornitore->rubrica?->amministratore_id ?? 0), ], fn($v) => (int) $v > 0))); if ($fornitoreAdminIds === []) { if (! $user->hasAnyRole(['admin', 'amministratore'])) { abort(404); } } else { $hasIntersection = count(array_intersect($fornitoreAdminIds, $allowedAdminIds)) > 0; if (! $hasIntersection) { abort(404); } } } } $this->hydrateBoxData($user); $this->tagsInput = (string) ($this->fornitore->tags ?? ''); $this->loadTagSuggestions(); $this->refreshDipendentiRows(); $this->refreshCatalogRows(); $requestedTab = (string) request()->query('tab', 'profilo'); if ($this->isValidSectionTab($requestedTab)) { $this->sectionTab = $requestedTab; } } public function setSectionTab(string $tab): void { if ($this->isValidSectionTab($tab)) { $this->sectionTab = $tab; } } public function getProdottiHubUrl(): string { return ProdottiCatalogo::getUrl(['fornitore' => (int) $this->fornitore->id], panel: 'admin-filament'); } public function getSerialiHubUrl(): string { return SerialiCatalogo::getUrl(['fornitore' => (int) $this->fornitore->id], panel: 'admin-filament'); } public function getContabilitaFornitoriUrl(): string { return ContabilitaFornitoriArchivio::getUrl(panel: 'admin-filament'); } private function isValidSectionTab(string $tab): bool { return in_array($tab, ['profilo', 'accessi', 'assistenza', 'catalogo', 'contabilita'], true); } public function creaDipendenteFornitore(): void { $nome = trim($this->nuovoDipendenteNome); $email = mb_strtolower(trim($this->nuovoDipendenteEmail)); if ($nome === '' || $email === '') { Notification::make()->title('Nome ed email sono obbligatori')->warning()->send(); return; } $exists = FornitoreDipendente::query() ->where('fornitore_id', (int) $this->fornitore->id) ->whereRaw('LOWER(email) = ?', [$email]) ->exists(); if ($exists) { Notification::make()->title('Dipendente gia presente per questo fornitore')->warning()->send(); return; } FornitoreDipendente::query()->create([ 'fornitore_id' => (int) $this->fornitore->id, 'nome' => $nome, 'cognome' => trim($this->nuovoDipendenteCognome) ?: null, 'email' => $email, 'telefono' => trim($this->nuovoDipendenteTelefono) ?: null, 'attivo' => true, 'created_by_user_id' => Auth::id(), 'updated_by_user_id' => Auth::id(), ]); $this->nuovoDipendenteNome = ''; $this->nuovoDipendenteCognome = ''; $this->nuovoDipendenteEmail = ''; $this->nuovoDipendenteTelefono = ''; $this->refreshDipendentiRows(); Notification::make()->title('Dipendente aggiunto')->success()->send(); } public function updatedDipendenteRubricaSearch(): void { $this->searchDipendenteRubricaMatches(); } public function selezionaDipendenteRubrica(int $rubricaId): void { $rubrica = RubricaUniversale::query()->find($rubricaId); if (! $rubrica instanceof RubricaUniversale) { return; } $this->dipendenteRubricaId = (int) $rubrica->id; $this->dipendenteRubricaSearch = $this->formatRubricaLookupLabel($rubrica); $this->dipendenteRubricaMatches = [$this->mapRubricaLookupRow($rubrica)]; } public function resetDipendenteRubricaSelection(): void { $this->dipendenteRubricaId = null; $this->dipendenteRubricaSearch = ''; $this->dipendenteRubricaMatches = []; } public function collegaDipendenteDaRubrica(): void { $rubricaId = (int) ($this->dipendenteRubricaId ?? 0); if ($rubricaId <= 0) { Notification::make()->title('Seleziona un nominativo rubrica')->warning()->send(); return; } $rubrica = RubricaUniversale::query()->find($rubricaId); if (! $rubrica instanceof RubricaUniversale) { Notification::make()->title('Nominativo rubrica non trovato')->danger()->send(); return; } $email = mb_strtolower(trim((string) ($rubrica->email ?? ''))); $query = FornitoreDipendente::query() ->where('fornitore_id', (int) $this->fornitore->id) ->where(function ($builder) use ($rubricaId, $email, $rubrica): void { $builder->where('rubrica_id', $rubricaId); if ($email !== '') { $builder->orWhereRaw('LOWER(email) = ?', [$email]); } $builder->orWhere(function ($sub) use ($rubrica): void { $sub->where('nome', (string) ($rubrica->nome ?? '')) ->where('cognome', (string) ($rubrica->cognome ?? '')); }); }); $dipendente = $query->first(); if (! $dipendente instanceof FornitoreDipendente) { $dipendente = new FornitoreDipendente(); $dipendente->fornitore_id = (int) $this->fornitore->id; $dipendente->created_by_user_id = Auth::id(); } $dipendente->rubrica_id = $rubricaId; $dipendente->nome = (string) ($rubrica->nome ?: $rubrica->ragione_sociale ?: 'Dipendente'); $dipendente->cognome = trim((string) ($rubrica->cognome ?? '')) !== '' ? (string) $rubrica->cognome : null; $dipendente->email = $email !== '' ? $email : null; $dipendente->telefono = trim((string) ($rubrica->telefono_cellulare ?: $rubrica->telefono_ufficio ?: $rubrica->telefono_casa)) ?: null; $dipendente->attivo = true; $dipendente->updated_by_user_id = Auth::id(); $dipendente->save(); $this->resetDipendenteRubricaSelection(); $this->refreshDipendentiRows(); Notification::make()->title('Dipendente collegato dalla rubrica')->success()->send(); } public function abilitaAccessoDipendente(int $dipendenteId): void { $dipendente = FornitoreDipendente::query() ->where('fornitore_id', (int) $this->fornitore->id) ->find($dipendenteId); if (! $dipendente instanceof FornitoreDipendente) { Notification::make()->title('Dipendente non trovato')->danger()->send(); return; } $email = trim((string) ($dipendente->email ?? '')); if ($email === '') { Notification::make()->title('Email dipendente mancante')->warning()->send(); return; } $targetUser = User::query()->whereRaw('LOWER(email) = ?', [mb_strtolower($email)])->first(); $created = false; if (! $targetUser instanceof User) { $generatedPassword = Str::random(12); $targetUser = User::query()->create([ 'name' => trim((string) ($dipendente->nome_completo ?: 'Utente fornitore')), 'email' => $email, 'password' => Hash::make($generatedPassword), 'email_verified_at' => now(), 'is_active' => true, ]); $this->lastGeneratedPassword = $generatedPassword; $created = true; } $targetUser->assignRole('fornitore'); $dipendente->user_id = (int) $targetUser->id; $dipendente->attivo = true; $dipendente->updated_by_user_id = Auth::id(); $dipendente->save(); $this->refreshDipendentiRows(); Notification::make() ->title('Accesso dipendente abilitato') ->body($created ? 'Accesso creato. Password temporanea: ' . ($this->lastGeneratedPassword ?? '(non disponibile)') : 'Accesso collegato ad utente esistente.') ->success() ->send(); } public function resetPasswordDipendenteUser(int $userId): void { $dipendente = FornitoreDipendente::query() ->where('fornitore_id', (int) $this->fornitore->id) ->where('user_id', $userId) ->first(); if (! $dipendente instanceof FornitoreDipendente) { Notification::make()->title('Utente non collegato a questo fornitore')->danger()->send(); return; } $target = User::query()->find($userId); if (! $target instanceof User) { Notification::make()->title('Utente non trovato')->danger()->send(); return; } $newPassword = Str::random(12); $target->password = Hash::make($newPassword); $target->save(); $this->lastGeneratedPassword = $newPassword; Notification::make() ->title('Password dipendente resettata') ->body('Nuova password temporanea: ' . $newPassword) ->success() ->send(); } public function toggleDipendenteAttivo(int $dipendenteId): void { $dipendente = FornitoreDipendente::query() ->where('fornitore_id', (int) $this->fornitore->id) ->find($dipendenteId); if (! $dipendente instanceof FornitoreDipendente) { Notification::make()->title('Dipendente non trovato')->danger()->send(); return; } $dipendente->attivo = ! (bool) $dipendente->attivo; $dipendente->updated_by_user_id = Auth::id(); $dipendente->save(); $this->refreshDipendentiRows(); Notification::make()->title('Stato dipendente aggiornato')->success()->send(); } public function saveTags(): void { if (! Schema::hasColumn('fornitori', 'tags')) { Notification::make()->title('Colonna tags non presente')->warning()->send(); return; } $normalized = $this->normalizeTags($this->tagsInput); $this->fornitore->tags = implode(', ', $normalized); $this->fornitore->save(); $this->tagsInput = (string) ($this->fornitore->tags ?? ''); $this->loadTagSuggestions(); Notification::make()->title('Tag fornitore salvati')->success()->send(); } public function importLegacyTags(): void { if (! Schema::hasColumn('fornitori', 'tags')) { Notification::make()->title('Colonna tags non presente')->warning()->send(); return; } try { Artisan::call('fornitori:import-legacy-tags', [ '--fornitore-id' => [(int) $this->fornitore->id], ]); } catch (\Throwable $e) { Notification::make()->title('Import tag legacy fallito')->body($e->getMessage())->danger()->send(); return; } $this->fornitore->refresh(); $this->tagsInput = (string) ($this->fornitore->tags ?? ''); $this->loadTagSuggestions(); Notification::make()->title('Tag legacy importati')->success()->send(); } public function syncRubricaFromFornitore(): void { $payload = [ 'tipo_contatto' => 'persona_giuridica', 'categoria' => 'fornitore', 'stato' => 'attivo', 'ragione_sociale' => $this->fornitore->ragione_sociale ?: trim(($this->fornitore->nome ?? '') . ' ' . ($this->fornitore->cognome ?? '')), 'nome' => $this->fornitore->nome, 'cognome' => $this->fornitore->cognome, 'partita_iva' => $this->fornitore->partita_iva, 'codice_fiscale' => $this->fornitore->codice_fiscale, 'indirizzo' => $this->fornitore->indirizzo, 'civico' => $this->fornitore->civico, 'cap' => $this->fornitore->cap, 'citta' => $this->fornitore->citta, 'provincia' => $this->fornitore->provincia, 'nazione' => $this->fornitore->nazione, 'email' => $this->fornitore->email, 'pec' => $this->fornitore->pec, 'telefono_ufficio' => $this->fornitore->telefono, 'telefono_cellulare' => $this->fornitore->cellulare, 'sito_web' => $this->fornitore->sito_web, 'note' => $this->fornitore->note, 'data_ultima_modifica' => now()->toDateString(), ]; $rubrica = null; if ((int) ($this->fornitore->rubrica_id ?? 0) > 0) { $rubrica = RubricaUniversale::query()->find((int) $this->fornitore->rubrica_id); } if (! $rubrica instanceof RubricaUniversale) { $rubrica = RubricaUniversale::query()->create(array_merge($payload, [ 'data_inserimento' => now()->toDateString(), ])); } else { $rubrica->fill($payload); $rubrica->save(); } if ((int) ($this->fornitore->rubrica_id ?? 0) !== (int) $rubrica->id) { $this->fornitore->rubrica_id = (int) $rubrica->id; $this->fornitore->save(); } $this->fornitore->refresh(); Notification::make()->title('Rubrica aggiornata dal fornitore')->success()->send(); } private function loadTagSuggestions(): void { if (! Schema::hasColumn('fornitori', 'tags')) { $this->tagSuggestions = []; return; } $all = Fornitore::query() ->whereNotNull('tags') ->pluck('tags') ->all(); $pool = []; foreach ($all as $row) { foreach ($this->splitTags((string) $row) as $tag) { $pool[] = $tag; } } $pool = array_values(array_unique($pool)); sort($pool, SORT_NATURAL | SORT_FLAG_CASE); $this->tagSuggestions = array_slice($pool, 0, 300); } private function refreshDipendentiRows(): void { $this->dipendentiRows = FornitoreDipendente::query() ->with(['user:id,name,email', 'rubrica:id,nome,cognome,ragione_sociale']) ->where('fornitore_id', (int) $this->fornitore->id) ->orderByDesc('attivo') ->orderBy('nome') ->orderBy('cognome') ->get() ->map(function (FornitoreDipendente $d): array { $userId = (int) ($d->user_id ?? 0); return [ 'id' => (int) $d->id, 'nome' => (string) ($d->nome_completo ?: 'Dipendente #' . $d->id), 'email' => (string) ($d->email ?? ''), 'telefono' => (string) ($d->telefono ?? ''), 'attivo' => (bool) $d->attivo, 'rubrica_id' => (int) ($d->rubrica_id ?? 0), 'rubrica' => $d->rubrica ? $this->formatRubricaLookupLabel($d->rubrica) : '', 'user_id' => $userId, 'user_label' => $userId > 0 ? ((string) ($d->user?->name ?: ('Utente #' . $userId))): '-', ]; }) ->all(); } private function searchDipendenteRubricaMatches(): void { $this->dipendenteRubricaMatches = $this->searchRubricaLookupMatches( $this->dipendenteRubricaSearch, $this->dipendenteRubricaId, null, 12, (int) ($this->fornitore->amministratore_id ?? 0) ); } private function refreshCatalogRows(): void { $this->catalogoProdottiRows = []; $this->tecnorepairRows = []; $this->catalogoProdottiRows = app(ProductHubViewService::class) ->buildRows($this->fornitore, null, 20); if (Schema::hasTable('assistenza_tecnorepair_schede_legacy')) { $this->tecnorepairRows = $this->fornitore->tecnorepairSchede() ->withCount('allegati') ->orderByRaw("CASE status_bucket WHEN 'open' THEN 0 WHEN 'waiting' THEN 1 WHEN 'closed' THEN 2 ELSE 3 END") ->orderByDesc('date_received') ->limit(12) ->get(['id', 'legacy_numero_scheda', 'product_model', 'product_code', 'serial_number', 'status_bucket', 'status_label']) ->map(fn($scheda) => [ 'id' => (int) $scheda->id, 'legacy_numero' => (string) ($scheda->legacy_numero_scheda ?? ''), 'product_model' => (string) ($scheda->product_model ?? ''), 'product_code' => (string) ($scheda->product_code ?? ''), 'serial_number' => (string) ($scheda->serial_number ?? ''), 'status_bucket' => (string) ($scheda->status_bucket ?? ''), 'status_label' => (string) ($scheda->status_label ?? ''), 'allegati_count' => (int) ($scheda->allegati_count ?? 0), ]) ->all(); } } private function resolveFornitoreAdminCode(): ?string { $admin = $this->fornitore->amministratore; if (! $admin) { return null; } $code = trim((string) ($admin->codice_amministratore ?? '')); return $code !== '' ? $code : (string) $admin->id; } private function suggestWholesaleCsvPath(): string { $basePath = base_path('Miki-Bug-workspace/Fornitori/Listini da importare'); if (! is_dir($basePath)) { return ''; } $normalizedName = Str::of((string) ($this->fornitore->ragione_sociale ?? '')) ->ascii() ->lower() ->replaceMatches('/[^a-z0-9]+/', ' ') ->trim() ->value(); $candidates = glob($basePath . '/*/wholesale.csv') ?: []; foreach ($candidates as $candidate) { $segment = Str::of((string) basename(dirname($candidate))) ->ascii() ->lower() ->replaceMatches('/[^a-z0-9]+/', ' ') ->trim() ->value(); if ($segment !== '' && $normalizedName !== '' && Str::contains($normalizedName, $segment)) { return $candidate; } } return (string) ($candidates[0] ?? ''); } private function suggestWholesaleXlsxPath(): string { $basePath = base_path('Miki-Bug-workspace/Fornitori/Listini da importare'); if (! is_dir($basePath)) { return ''; } $normalizedName = Str::of((string) ($this->fornitore->ragione_sociale ?? '')) ->ascii() ->lower() ->replaceMatches('/[^a-z0-9]+/', ' ') ->trim() ->value(); $candidates = glob($basePath . '/*/*.xlsx') ?: []; foreach ($candidates as $candidate) { $segment = Str::of((string) basename(dirname($candidate))) ->ascii() ->lower() ->replaceMatches('/[^a-z0-9]+/', ' ') ->trim() ->value(); if ($segment !== '' && $normalizedName !== '' && (Str::contains($normalizedName, $segment) || Str::contains($segment, $normalizedName))) { return $candidate; } } return (string) ($candidates[0] ?? ''); } private function refreshOperationalBoxes(): void { $user = Auth::user(); if ($user instanceof User) { $this->hydrateBoxData($user); } $this->fornitore->refresh(); $this->refreshCatalogRows(); } /** * @return array */ private function normalizeTags(string $input): array { $tags = []; foreach ($this->splitTags($input) as $tag) { $normalized = $this->canonicalizeTag($tag); if ($normalized !== null) { $tags[] = $normalized; } } $tags = array_values(array_unique($tags)); sort($tags, SORT_NATURAL | SORT_FLAG_CASE); return $tags; } /** * @return array */ private function splitTags(string $value): array { $parts = preg_split('/[,;|\n\r\/]+/', $value) ?: []; return array_values(array_filter(array_map(function (string $part): string { $clean = trim($part); $clean = preg_replace('/\s+/', ' ', $clean) ?? ''; return $clean; }, $parts), fn(string $p): bool => $p !== '')); } private function canonicalizeTag(string $raw): ?string { $clean = trim(mb_strtolower($raw)); if ($clean === 'pc') { return 'pc'; } if ($clean === '' || mb_strlen($clean) < 3) { return null; } $map = [ 'informat' => 'informatica', 'assist' => 'assistenza', 'computer' => 'pc', 'apple' => 'apple', 'idr' => 'idraulico', 'idraul' => 'idraulico', 'elett' => 'elettricista', 'elettric' => 'elettricista', 'ascens' => 'ascensorista', 'puliz' => 'pulizie', 'giardin' => 'giardiniere', 'assicur' => 'assicurazione', 'manut' => 'manutenzione', ]; foreach ($map as $prefix => $canonical) { if (str_starts_with($clean, $prefix)) { return $canonical; } } return $clean; } private function hydrateBoxData(User $user): void { $this->fattureAssociate = []; $this->raVersateRows = []; $identityCandidates = $this->getSupplierIdentityCandidates(); $this->box = [ 'stabile_id' => null, 'fornitore_features' => [], 'fornitore_defaults' => [ 'voce_spesa_default_id' => null, 'conto_costo_default_id' => null, ], 'catalogo_modulo' => [ 'scope_code' => null, 'fe_products' => 0, 'csv_products' => 0, 'manual_products' => 0, 'internal_only' => 0, 'private_links' => 0, 'offers' => 0, 'amazon_links' => 0, ], 'prodotti' => [ 'count' => 0, 'serializzati' => 0, 'identifiers' => 0, 'with_media' => 0, ], 'tecnorepair' => [ 'schede' => 0, 'aperte' => 0, 'chiuse' => 0, 'seriali' => 0, ], 'ade' => ['count' => 0, 'lordo' => 0.0], 'fe' => ['count' => 0, 'totale' => 0.0, 'contabilizzate' => 0], 'contabilita' => [ 'aperte_count' => 0, 'aperte_netto' => 0.0, 'pagate_count' => 0, 'pagate_netto' => 0.0, ], 'da_pagare' => ['source' => null, 'importo' => 0.0], 'ra' => [ 'da_versare' => 0.0, 'versata' => 0.0, 'compensata' => 0.0, ], ]; $activeStabileId = StabileContext::resolveActiveStabileId($user); if (! $activeStabileId) { return; } $stabile = Stabile::query()->select(['id', 'amministratore_id'])->find($activeStabileId); $amministratoreId = (int) ($stabile?->amministratore_id ?: 0); if ($amministratoreId <= 0) { return; } $this->box['stabile_id'] = $activeStabileId; if (Schema::hasTable('products')) { $productsBase = Product::query()->where('default_fornitore_id', (int) $this->fornitore->id); $catalogProducts = (clone $productsBase)->get(['id', 'meta']); $this->box['prodotti']['count'] = (int) (clone $productsBase)->count(); $this->box['prodotti']['serializzati'] = (int) (clone $productsBase)->where('track_serials', true)->count(); $this->box['prodotti']['with_media'] = (int) (clone $productsBase)->whereHas('media')->count(); $this->box['prodotti']['identifiers'] = (int) (clone $productsBase)->withCount('identifiers')->get()->sum('identifiers_count'); $scopeCode = trim((string) ($this->fornitore->codice_univoco ?? '')); $this->box['catalogo_modulo']['scope_code'] = $scopeCode !== '' ? $scopeCode : ('FORN-' . str_pad((string) $this->fornitore->id, 6, '0', STR_PAD_LEFT)); foreach ($catalogProducts as $product) { $meta = is_array($product->meta ?? null) ? $product->meta : []; $source = (string) ($meta['source'] ?? 'manual'); $catalog = is_array($meta['catalog'] ?? null) ? $meta['catalog'] : []; $privateLinks = is_array($catalog['private_source_links'] ?? null) ? $catalog['private_source_links'] : []; if ($source === 'fattura_elettronica') { $this->box['catalogo_modulo']['fe_products']++; } elseif ($source === 'ncom_wholesale_csv') { $this->box['catalogo_modulo']['csv_products']++; } else { $this->box['catalogo_modulo']['manual_products']++; } if ((string) ($catalog['publication_mode'] ?? '') === 'internal_only') { $this->box['catalogo_modulo']['internal_only']++; } $this->box['catalogo_modulo']['private_links'] += count(array_filter($privateLinks, fn($value) => filled($value))); } if (Schema::hasTable('product_offers')) { $offersBase = $this->fornitore->productOffers()->where('is_active', true); $this->box['catalogo_modulo']['offers'] = (int) (clone $offersBase)->count(); $this->box['catalogo_modulo']['amazon_links'] = (int) (clone $offersBase)->whereIn('source_type', ['amazon_referral', 'amazon_creators_api'])->count(); } } if (Schema::hasTable('assistenza_tecnorepair_schede_legacy')) { $stats = $this->fornitore->tecnorepair_stats; $this->box['tecnorepair']['schede'] = (int) ($stats['total'] ?? 0); $this->box['tecnorepair']['aperte'] = (int) ($stats['open'] ?? 0); $this->box['tecnorepair']['chiuse'] = (int) ($stats['closed'] ?? 0); $this->box['tecnorepair']['seriali'] = (int) ($stats['serials'] ?? 0); } // Feature flags & defaults (per stabile) + fallback (per fornitore) $settings = null; if (Schema::hasTable('fornitore_stabile_impostazioni')) { $settings = FornitoreStabileImpostazione::query() ->where('stabile_id', (int) $activeStabileId) ->where('fornitore_id', (int) $this->fornitore->id) ->first(); if ($settings) { $this->box['fornitore_defaults']['voce_spesa_default_id'] = $settings->voce_spesa_default_id ? (int) $settings->voce_spesa_default_id : null; $this->box['fornitore_defaults']['conto_costo_default_id'] = $settings->conto_costo_default_id ? (int) $settings->conto_costo_default_id : null; } } $features = []; if (Schema::hasColumn('fornitori', 'fe_features')) { $features = is_array($this->fornitore->fe_features ?? null) ? $this->fornitore->fe_features : []; } elseif ($settings && is_array($settings->meta ?? null)) { $features = is_array($settings->meta['fe_features'] ?? null) ? $settings->meta['fe_features'] : []; } $this->box['fornitore_features'] = $features; $normalize = static function (?string $value): string { $value = strtoupper(trim((string) $value)); $value = str_replace(' ', '', $value); return $value; }; $ids = []; $piva = $normalize($this->fornitore->partita_iva); $cf = $normalize($this->fornitore->codice_fiscale); if ($piva !== '') { $ids[] = $piva; if (str_starts_with($piva, 'IT') && strlen($piva) > 2) { $ids[] = substr($piva, 2); } } if ($cf !== '') { $ids[] = $cf; } $ids = array_values(array_unique(array_filter($ids, fn($v) => $v !== ''))); // AdE (staging) if (! empty($ids) && Schema::hasTable('stg_fatture_ade')) { $adeBase = StgFatturaAde::query() ->where('amministratore_id', $amministratoreId) ->where(function (\Illuminate\Database\Eloquent\Builder $q) use ($activeStabileId): void { $q->where('stabile_id', $activeStabileId)->orWhereNull('stabile_id'); }) ->where(function (\Illuminate\Database\Eloquent\Builder $q) use ($ids): void { foreach ($ids as $id) { $q->orWhereRaw("REPLACE(UPPER(identificativo_fornitore), ' ', '') = ?", [$id]); } }); $this->box['ade']['count'] = (int) (clone $adeBase)->count(); $lordo = (float) (clone $adeBase)->selectRaw('COALESCE(SUM(imponibile + imposta), 0) as lordo')->value('lordo'); $this->box['ade']['lordo'] = $lordo; $this->anteprimaAde = (clone $adeBase) ->orderByDesc('data_emissione') ->orderByDesc('id') ->limit(8) ->get(['id', 'data_emissione', 'numero_documento', 'imponibile', 'imposta', 'sdi_file']) ->map(fn(StgFatturaAde $r) => [ 'id' => $r->id, 'data' => $r->data_emissione?->format('d/m/Y'), 'numero' => (string) ($r->numero_documento ?? ''), 'lordo' => (float) ($r->imponibile ?? 0) + (float) ($r->imposta ?? 0), 'sdi_file' => (string) ($r->sdi_file ?? ''), ]) ->all(); } // FE importate if (Schema::hasTable('fatture_elettroniche')) { $feBase = FatturaElettronica::query() ->where('stabile_id', $activeStabileId) ->where(function (\Illuminate\Database\Eloquent\Builder $q) use ($identityCandidates): void { $this->applySupplierIdentityFilterToFeQuery($q, $identityCandidates); }); $this->box['fe']['count'] = (int) (clone $feBase)->count(); $this->box['fe']['totale'] = (float) (clone $feBase)->selectRaw('COALESCE(SUM(totale), 0) as totale')->value('totale'); $this->box['fe']['contabilizzate'] = (int) (clone $feBase) ->where(function (\Illuminate\Database\Eloquent\Builder $q): void { $q->whereNotNull('registrazione_contabile_id')->orWhere('stato', 'contabilizzata'); }) ->count(); $this->anteprimaFe = (clone $feBase) ->orderByDesc('data_fattura') ->orderByDesc('id') ->limit(8) ->get(['id', 'data_fattura', 'numero_fattura', 'totale', 'sdi_file', 'stato']) ->map(fn(FatturaElettronica $r) => [ 'id' => $r->id, 'data' => $r->data_fattura?->format('d/m/Y'), 'numero' => (string) ($r->numero_fattura ?? ''), 'totale' => (float) ($r->totale ?? 0), 'sdi_file' => (string) ($r->sdi_file ?? ''), 'stato' => (string) ($r->stato ?? ''), ]) ->all(); } // Contabilità (fatture fornitori) if (Schema::hasTable('contabilita_fatture_fornitori')) { $contabBase = ContabilitaFatturaFornitore::query() ->where('stabile_id', $activeStabileId) ->where('fornitore_id', (int) $this->fornitore->id) ->when($identityCandidates !== [], function ($query) use ($identityCandidates): void { $query->where(function (\Illuminate\Database\Eloquent\Builder $inner) use ($identityCandidates): void { $inner->whereNull('fattura_elettronica_id') ->orWhereDoesntHave('fatturaElettronica') ->orWhereHas('fatturaElettronica', function (\Illuminate\Database\Eloquent\Builder $feQuery) use ($identityCandidates): void { $this->applySupplierIdentityFilterToFeQuery($feQuery, $identityCandidates); }); }); }); // Coerente con la logica della scheda contabile: aperto = totale - pagato. $totNetto = (float) (clone $contabBase)->selectRaw('COALESCE(SUM(netto_da_pagare), 0) as netto')->value('netto'); $this->box['contabilita']['pagate_count'] = (int) (clone $contabBase)->where('stato', 'pagato')->count(); $this->box['contabilita']['pagate_netto'] = (float) (clone $contabBase)->where('stato', 'pagato')->selectRaw('COALESCE(SUM(netto_da_pagare), 0) as netto')->value('netto'); $this->box['contabilita']['aperte_netto'] = round($totNetto - (float) $this->box['contabilita']['pagate_netto'], 2); $this->box['contabilita']['aperte_count'] = (int) (clone $contabBase)->where('stato', '!=', 'pagato')->count(); $this->anteprimaContabilita = (clone $contabBase) ->orderByDesc('data_documento') ->orderByDesc('id') ->limit(8) ->get(['id', 'data_documento', 'numero_documento', 'netto_da_pagare', 'stato']) ->map(fn(ContabilitaFatturaFornitore $r) => [ 'id' => $r->id, 'data' => $r->data_documento?->format('d/m/Y'), 'numero' => (string) ($r->numero_documento ?? ''), 'netto' => (float) ($r->netto_da_pagare ?? 0), 'stato' => (string) ($r->stato ?? ''), ]) ->all(); $this->fattureAssociate = (clone $contabBase) ->orderByDesc('data_documento') ->orderByDesc('id') ->limit(30) ->get(['id', 'data_documento', 'numero_documento', 'totale', 'netto_da_pagare', 'stato', 'fattura_elettronica_id']) ->map(fn(ContabilitaFatturaFornitore $r) => [ 'id' => (int) $r->id, 'data' => $r->data_documento?->format('d/m/Y'), 'numero' => (string) ($r->numero_documento ?? ''), 'totale' => (float) ($r->totale ?? 0), 'netto' => (float) ($r->netto_da_pagare ?? 0), 'stato' => (string) ($r->stato ?? ''), 'source' => ((int) ($r->fattura_elettronica_id ?? 0) > 0) ? 'FE' : 'manuale', ]) ->all(); } // Registro RA if (Schema::hasTable('registro_ritenute_acconto')) { $raBase = RegistroRitenuteAcconto::query() ->where('fornitore_id', (int) $this->fornitore->id) ->whereHas('gestione', fn(\Illuminate\Database\Eloquent\Builder $q) => $q->where('stabile_id', (int) $activeStabileId)); foreach (['da_versare', 'versata', 'compensata'] as $stato) { $this->box['ra'][$stato] = (float) (clone $raBase) ->where('stato_versamento', $stato) ->selectRaw('COALESCE(SUM(importo_ritenuta), 0) as ra') ->value('ra'); } $this->raVersateRows = (clone $raBase) ->whereIn('stato_versamento', ['versata', 'compensata']) ->orderByDesc('data_versamento') ->orderByDesc('id') ->limit(30) ->get(['id', 'data_versamento', 'importo_ritenuta', 'stato_versamento', 'codice_tributo', 'f24_riferimento']) ->map(fn(RegistroRitenuteAcconto $r) => [ 'id' => (int) $r->id, 'data_versamento' => $r->data_versamento?->format('d/m/Y'), 'importo' => (float) ($r->importo_ritenuta ?? 0), 'stato' => (string) ($r->stato_versamento ?? ''), 'tributo' => (string) ($r->codice_tributo ?? ''), 'f24' => (string) ($r->f24_riferimento ?? ''), ]) ->all(); } // Totale da pagare (priorità: contabilità → FE → AdE) if ((float) ($this->box['contabilita']['aperte_netto'] ?? 0) > 0) { $this->box['da_pagare'] = ['source' => 'contabilita', 'importo' => (float) $this->box['contabilita']['aperte_netto']]; } elseif ((int) ($this->box['fe']['count'] ?? 0) > 0) { $this->box['da_pagare'] = ['source' => 'fe', 'importo' => (float) ($this->box['fe']['totale'] ?? 0)]; } elseif ((int) ($this->box['ade']['count'] ?? 0) > 0) { $this->box['da_pagare'] = ['source' => 'ade', 'importo' => (float) ($this->box['ade']['lordo'] ?? 0)]; } } /** * @return array */ private function getSupplierIdentityCandidates(): array { $values = []; foreach ([(string) ($this->fornitore->partita_iva ?? ''), (string) ($this->fornitore->codice_fiscale ?? '')] as $raw) { $normalized = strtoupper(str_replace(' ', '', trim($raw))); if ($normalized === '' || $normalized === 'ND') { continue; } $values[] = $normalized; if (str_starts_with($normalized, 'IT') && strlen($normalized) > 2) { $values[] = substr($normalized, 2); } } $values = array_values(array_unique(array_filter($values, fn(string $value): bool => $value !== ''))); sort($values, SORT_NATURAL | SORT_FLAG_CASE); return $values; } /** * @param array $identityCandidates */ private function applySupplierIdentityFilterToFeQuery(\Illuminate\Database\Eloquent\Builder $query, array $identityCandidates): void { $fornitoreId = (int) $this->fornitore->id; if ($identityCandidates === []) { $query->where('fornitore_id', $fornitoreId); return; } $query->where(function (\Illuminate\Database\Eloquent\Builder $outer) use ($identityCandidates, $fornitoreId): void { $outer->where(function (\Illuminate\Database\Eloquent\Builder $identityQuery) use ($identityCandidates): void { foreach ($identityCandidates as $identity) { $identityQuery->orWhereRaw("REPLACE(UPPER(fornitore_piva), ' ', '') = ?", [$identity]) ->orWhereRaw("REPLACE(UPPER(fornitore_cf), ' ', '') = ?", [$identity]); } })->orWhere(function (\Illuminate\Database\Eloquent\Builder $fallbackQuery) use ($fornitoreId): void { $fallbackQuery->where('fornitore_id', $fornitoreId) ->where(function (\Illuminate\Database\Eloquent\Builder $missingPiva): void { $missingPiva->whereNull('fornitore_piva') ->orWhere('fornitore_piva', '') ->orWhere('fornitore_piva', 'ND'); }) ->where(function (\Illuminate\Database\Eloquent\Builder $missingCf): void { $missingCf->whereNull('fornitore_cf') ->orWhere('fornitore_cf', '') ->orWhere('fornitore_cf', 'ND'); }); }); }); } protected function getHeaderActions(): array { return []; } }