cod_stabile ?? '')), trim((string) ($stabile->codice_stabile ?? '')), trim((string) ($stabile->denominazione ?? '')), ])); return $parts !== [] ? implode(' - ', $parts) : ('Stabile #' . (int) $stabile->id); } public function getActiveStabileDestinationSummaryProperty(): string { $user = Auth::user(); if (! $user instanceof User) { return 'Stabile non selezionato'; } $stabile = StabileContext::getActiveStabile($user); if (! $stabile instanceof Stabile) { return 'Stabile non selezionato'; } $summary = $this->getActiveStabileLabelProperty(); $cf = $this->resolveStabileSoggettoCf($stabile); if ($cf !== '') { $summary .= "\nCodice fiscale soggetto usato per lo scarico: {$cf}"; } if ($this->isLikelyTechnicalFeShell($stabile)) { $summary .= "\nATTENZIONE: lo stabile attivo sembra uno shell tecnico FE separato. Per scaricare le fatture del condominio cambia lo stabile in topbar prima di procedere."; } return $summary; } private function safeUtf8(string $value): string { $value = preg_replace('/^\xEF\xBB\xBF/', '', $value) ?? $value; if (function_exists('mb_check_encoding') && ! mb_check_encoding($value, 'UTF-8')) { if (function_exists('mb_convert_encoding')) { $value = mb_convert_encoding($value, 'UTF-8', 'UTF-8,ISO-8859-1,Windows-1252'); } } $fixed = @iconv('UTF-8', 'UTF-8//IGNORE', $value); if (is_string($fixed) && $fixed !== '') { $value = $fixed; } return preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $value) ?? $value; } /** * @return array{status: string, created_letture?: int, updated_letture?: int, tickets?: int, message?: string} */ private function autoLinkWaterReadingsForInvoice(int $fatturaId, int $userId): array { $fattura = FatturaElettronica::query()->find($fatturaId); if (! $fattura instanceof FatturaElettronica) { return ['status' => 'skipped', 'message' => 'Fattura non trovata']; } $hasPdf = is_string($fattura->allegato_pdf_path ?? null) && trim((string) $fattura->allegato_pdf_path) !== ''; $hasStoredPayload = is_string($fattura->consumo_raw ?? null) && trim((string) $fattura->consumo_raw) !== ''; if (! $hasPdf && ! $hasStoredPayload) { return ['status' => 'skipped', 'message' => 'Nessun PDF/payload acqua disponibile']; } try { if ($userId > 0) { app(FatturaElettronicaProtocolloService::class)->ensureDocumentoProtocollato($fattura, $userId); $fattura->refresh(); } } catch (\Throwable) { // ignore } $documento = Documento::query() ->where('documentable_type', FatturaElettronica::class) ->where('documentable_id', (int) $fattura->id) ->first(); $parsed = []; if ($documento instanceof Documento) { $text = is_string($documento->contenuto_ocr) ? trim((string) $documento->contenuto_ocr) : ''; if ($text === '') { try { $result = app(PdfTextExtractionService::class)->extractAndStore($documento, false); if (($result['status'] ?? null) === 'ok') { $documento->refresh(); $text = is_string($documento->contenuto_ocr) ? trim((string) $documento->contenuto_ocr) : ''; } } catch (\Throwable) { $text = ''; } } if ($text !== '') { $parsed = app(AcquaPdfTextParser::class)->parse($text); } } $storedRaw = is_string($fattura->consumo_raw ?? null) ? trim((string) $fattura->consumo_raw) : ''; if ($storedRaw !== '' && $parsed === []) { $decoded = json_decode($storedRaw, true); if (is_array($decoded)) { $parsed = $decoded; } } $codici = is_array($parsed['codici'] ?? null) ? $parsed['codici'] : []; $contatore = is_array($parsed['contatore'] ?? null) ? $parsed['contatore'] : []; $consumi = is_array($parsed['consumi'] ?? null) ? $parsed['consumi'] : []; $generale = is_array($parsed['generale'] ?? null) ? $parsed['generale'] : []; $finestra = is_array($parsed['finestra_autolettura'] ?? null) ? $parsed['finestra_autolettura'] : []; $letture = is_array($parsed['riepilogo_letture'] ?? null) ? $parsed['riepilogo_letture'] : []; $quadro = is_array($parsed['quadro_dettaglio'] ?? null) ? $parsed['quadro_dettaglio'] : []; $tariffe = is_array($parsed['tariffe'] ?? null) ? $parsed['tariffe'] : []; $ivaInfo = is_array($parsed['iva'] ?? null) ? $parsed['iva'] : []; $hasAny = false; foreach ([$codici['utenza'] ?? null, $codici['cliente'] ?? null, $codici['contratto'] ?? null, $contatore['matricola'] ?? null] as $value) { if (is_string($value) && trim($value) !== '') { $hasAny = true; break; } } $hasTariffData = count(array_filter([ $generale['periodicita_fatturazione'] ?? null, $tariffe['profilo'] ?? null, $ivaInfo['codice'] ?? null, $quadro['quota_fissa'] ?? null, ], static fn($value): bool => $value !== null && $value !== '')) > 0; if (! $hasAny && count($consumi) < 1 && ! $hasTariffData) { return ['status' => 'skipped', 'message' => 'Nessun dato acqua utile rilevato']; } $payload = [ 'type' => 'acqua', 'source' => 'pdf_ocr', 'codici' => [ 'utenza' => $codici['utenza'] ?? null, 'cliente' => $codici['cliente'] ?? null, 'contratto' => $codici['contratto'] ?? null, ], 'contatore' => [ 'matricola' => $contatore['matricola'] ?? null, ], 'consumi' => array_values($consumi), 'generale' => $generale, 'finestra_autolettura' => $finestra, 'riepilogo_letture' => array_values($letture), 'quadro_dettaglio' => $quadro, 'tariffe' => $tariffe, 'iva' => [ 'codice' => $ivaInfo['codice'] ?? null, 'aliquota_percentuale' => $ivaInfo['aliquota_percentuale'] ?? null, 'descrizione' => $ivaInfo['descrizione'] ?? null, ], 'parsed_at' => now()->toISOString(), ]; $totalMc = null; $reference = null; if ($consumi !== []) { $sum = 0.0; $hasNumeric = false; foreach ($consumi as $consumo) { if (isset($consumo['valore']) && is_numeric($consumo['valore'])) { $sum += (float) $consumo['valore']; $hasNumeric = true; } } if ($hasNumeric) { $totalMc = $sum; } $first = $consumi[0] ?? []; $dal = $first['dal'] ?? null; $al = $first['al'] ?? null; if (is_string($dal) && is_string($al) && $dal !== '' && $al !== '') { $reference = $dal . ' - ' . $al; } } $fattura->consumo_unita = 'mc'; $fattura->consumo_valore = $totalMc; $fattura->consumo_riferimento = $reference; $fattura->consumo_raw = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $fattura->save(); try { app(ConsumiAcquaTariffeIngestionService::class)->ingest($fattura, $parsed, null); } catch (\Throwable) { // best-effort } $ingestion = app(ConsumiAcquaIngestionService::class)->ingest($fattura, $parsed, null, $userId, false, null); if (($ingestion['status'] ?? null) === 'ok') { try { app(ConsumiAcquaTariffeIngestionService::class)->ingest( $fattura, $parsed, isset($ingestion['servizio_id']) && is_numeric($ingestion['servizio_id']) ? (int) $ingestion['servizio_id'] : null ); } catch (\Throwable) { // best-effort } } return $ingestion; } public int $cassettoAnno; /** * Ultimi scarichi da Cassetto Fiscale per lo stabile attivo. */ public function getCassettoDownloadLogs(int $limit = 10): \Illuminate\Support\Collection { $user = Auth::user(); if (! $user instanceof User) { return collect(); } $stabile = StabileContext::getActiveStabile($user); if (! $stabile instanceof Stabile) { return collect(); } $amministratore = $stabile->amministratore; if (! $amministratore) { return collect(); } return DB::table('fe_cassetto_download_logs') ->where('stabile_id', (int) $stabile->id) ->where('amministratore_id', (int) $amministratore->id) ->orderByDesc('created_at') ->limit(max(1, min(50, $limit))) ->get(); } /** * Stato trimestri per un anno (Q1..Q4) per lo stabile attivo. * @return array */ public function getCassettoQuarterStatus(int $anno): array { $anno = max(2000, min(2100, (int) $anno)); $user = Auth::user(); if (! $user instanceof User) { return []; } $stabile = StabileContext::getActiveStabile($user); if (! $stabile instanceof Stabile) { return []; } $amministratore = $stabile->amministratore; if (! $amministratore) { return []; } $ranges = [ 'Q1' => [$anno . '-01-01', $anno . '-03-31'], 'Q2' => [$anno . '-04-01', $anno . '-06-30'], 'Q3' => [$anno . '-07-01', $anno . '-09-30'], 'Q4' => [$anno . '-10-01', $anno . '-12-31'], ]; $out = []; foreach ($ranges as $q => [$dal, $al]) { $log = DB::table('fe_cassetto_download_logs') ->where('amministratore_id', (int) $amministratore->id) ->where('stabile_id', (int) $stabile->id) ->whereDate('dal', $dal) ->whereDate('al', $al) ->orderByDesc('id') ->first(); $stato = 'MAI'; $filesTotal = 0; $imported = 0; $duplicates = 0; $errors = 0; $createdAt = null; $logId = null; if ($log) { $logId = is_numeric($log->id ?? null) ? (int) $log->id : null; $filesTotal = (int) ($log->files_total ?? 0); $imported = (int) ($log->imported ?? 0); $duplicates = (int) ($log->duplicates ?? 0); $errors = (int) ($log->errors ?? 0); $createdAt = is_string($log->created_at ?? null) ? (string) $log->created_at : null; $status = strtolower((string) ($log->status ?? '')); $isComplete = ( $status === 'ok' && $errors === 0 && $filesTotal > 0 && ($imported + $duplicates) >= $filesTotal ); if ($isComplete) { $stato = 'COMPLETO'; } elseif (in_array($status, ['started', 'running', 'in_progress'], true)) { $stato = 'IN CORSO'; } elseif ($errors > 0 || $status === 'error' || $status === 'failed') { $stato = 'ERRORE'; } else { $stato = 'PARZIALE'; } } $out[] = [ 'quarter' => $q, 'dal' => $dal, 'al' => $al, 'stato' => $stato, 'log_id' => $logId, 'files_total' => $filesTotal, 'imported' => $imported, 'duplicates' => $duplicates, 'errors' => $errors, 'created_at' => $createdAt, ]; } return $out; } public function downloadCassettoQuarter(string $quarter, string $dal, string $al, int $anno): void { $key = $quarter . '-' . $anno; if (! empty($this->quarterDownloads[$key])) { return; } $this->quarterDownloads[$key] = time(); $user = Auth::user(); if (! $user instanceof User) { Notification::make()->title('Utente non valido')->danger()->send(); $this->quarterDownloads[$key] = false; return; } $stabile = StabileContext::getActiveStabile($user); if (! $stabile instanceof Stabile) { Notification::make()->title('Seleziona uno stabile')->danger()->send(); $this->quarterDownloads[$key] = false; return; } if ($this->isLikelyTechnicalFeShell($stabile)) { $this->notifyTechnicalFeShellBlocked(); $this->quarterDownloads[$key] = false; return; } $amministratoreId = (int) ($stabile->amministratore_id ?? 0); if ($amministratoreId <= 0) { Notification::make()->title('Amministratore non valido')->danger()->send(); $this->quarterDownloads[$key] = false; return; } if ($this->isCassettoQuarterCompleteFor($amministratoreId, (int) $stabile->id, $dal, $al)) { Notification::make() ->title('Trimestre già completo') ->body($this->safeUtf8("{$quarter} {$anno} risulta già scaricato (COMPLETO). Per rieseguire lo stesso periodo usa 'Scarica da Cassetto Fiscale' e abilita 'Reimport / completa' oppure 'Non saltare'.")) ->warning() ->send(); return; } try { $job = new RunCassettoFiscaleDownload( $amministratoreId, (int) $stabile->id, $dal, $al, false, (int) $user->id, [ 'force_update' => false, 'importa_righe' => 'auto', 'create_fornitore_if_missing' => true, ], ); } catch (\Throwable $e) { report($e); Notification::make()->title('Errore')->body($this->safeUtf8($e->getMessage()))->danger()->send(); return; } // Eseguiamo dopo la risposta HTTP per non dipendere da queue worker attivo. app()->terminating(function () use ($job): void { try { $job->handle(app(CassettoFiscaleDownloadService::class)); } catch (\Throwable $e) { report($e); } }); Notification::make() ->title('Scarico avviato') ->body($this->safeUtf8("Trimestre {$quarter} {$anno} avviato. Aggiorna tra poco la tab scarichi per vedere il log.")) ->success() ->send(); } private function isCassettoQuarterCompleteFor(int $amministratoreId, int $stabileId, string $dalYmd, string $alYmd): bool { $row = DB::table('fe_cassetto_download_logs') ->where('amministratore_id', $amministratoreId) ->where('stabile_id', $stabileId) ->whereDate('dal', $dalYmd) ->whereDate('al', $alYmd) ->where('status', 'ok') ->orderByDesc('id') ->first(); if (! $row) { return false; } $filesTotal = (int) ($row->files_total ?? 0); $imported = (int) ($row->imported ?? 0); $duplicates = (int) ($row->duplicates ?? 0); $errors = (int) ($row->errors ?? 0); return $errors === 0 && $filesTotal > 0 && ($imported + $duplicates) >= $filesTotal; } private function resolveStabileSoggettoCf(Stabile $stabile): string { $cf = trim((string) ($stabile->codice_fiscale ?? '')); if ($cf === '') { try { $stabile->loadMissing('rubrica'); $cf = trim((string) ($stabile->rubrica?->codice_fiscale ?? '')); } catch (\Throwable) { // ignore } } return strtoupper(trim($cf)); } private function isLikelyTechnicalFeShell(Stabile $stabile): bool { $stabileCf = $this->resolveStabileSoggettoCf($stabile); $adminCf = strtoupper(trim((string) ($stabile->amministratore?->codice_fiscale_studio ?? ''))); $code = strtoupper(trim((string) ($stabile->codice_stabile ?? ''))); $legacyCode = trim((string) ($stabile->cod_stabile ?? '')); return $stabileCf !== '' && $adminCf !== '' && $stabileCf === $adminCf && (str_starts_with($code, 'FE') || $legacyCode === ''); } private function notifyTechnicalFeShellBlocked(): void { Notification::make() ->title('Stabile tecnico FE separato') ->body('Lo scarico è bloccato perché lo stabile attivo usa il codice fiscale dello studio/amministratore. Se devi scaricare le fatture del condominio, cambia lo stabile attivo in topbar e seleziona il condominio reale.') ->warning() ->send(); } public function getArchivioRigheVisteCount(): int { $records = $this->getTableRecords(); if ($records instanceof LengthAwarePaginator) { return (int) $records->total(); } if ($records instanceof Paginator) { return (int) $records->count(); } return is_countable($records) ? count($records) : 0; } protected static ?string $navigationLabel = 'Fatture ricevute'; protected static ?string $title = 'Fatture ricevute'; protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-shield-check'; protected static UnitEnum|string|null $navigationGroup = 'Contabilità'; protected static ?int $navigationSort = 35; protected static ?string $slug = 'contabilita/fatture-ricevute'; protected string $view = 'filament.pages.contabilita.fatture-elettroniche-p7m-ricevute'; public static function canAccess(): bool { $user = Auth::user(); if (! $user instanceof User) { return false; } if ($user->hasAnyRole(['super-admin', 'admin'])) { return true; } if ($user->hasRole('fornitore')) { return true; } return $user->can('contabilita.fatture-elettroniche'); } public function mount(): void { $this->cassettoAnno = (int) now()->format('Y'); $this->mountInteractsWithTable(); } protected function getHeaderActions(): array { return [ Action::make('rigenera_pdf_da_xml') ->label('Rigenera PDF da XML') ->icon('heroicon-o-document-arrow-up') ->visible(function (): bool { $u = Auth::user(); return $u instanceof User && $u->hasAnyRole(['super-admin', 'admin']); }) ->form([ Toggle::make('solo_mancanti') ->label('Solo fatture senza PDF') ->default(true), TextInput::make('limit') ->label('Max fatture') ->numeric() ->default(200) ->required(), ]) ->action(function (array $data): void { $user = Auth::user(); if (! $user instanceof User) { Notification::make()->title('Utente non valido')->danger()->send(); return; } $stabile = StabileContext::getActiveStabile($user); if (! $stabile instanceof Stabile) { Notification::make()->title('Seleziona uno stabile')->danger()->send(); return; } $limit = (int) ($data['limit'] ?? 200); $limit = max(1, min(2000, $limit)); $soloMancanti = (bool) ($data['solo_mancanti'] ?? true); $q = FatturaElettronica::query() ->where('stabile_id', (int) $stabile->id) ->where(function (Builder $qq) { $qq ->where(function (Builder $q2) { $q2->whereNotNull('xml_content')->where('xml_content', '!=', ''); }) ->orWhere(function (Builder $q2) { $q2->whereNotNull('xml_path')->where('xml_path', '!=', ''); }); }); if ($soloMancanti) { $q->where(function (Builder $qq) { $qq ->whereNull('allegato_pdf_path') ->orWhere('allegato_pdf_path', '') ->orWhereNull('allegato_pdf_hash') ->orWhere('allegato_pdf_hash', ''); }); } else { // Include anche i PDF "generati" (non allegati originali), per rigenerazione con XSL Assosoftware. $q->where(function (Builder $qq) { $qq->whereNull('allegato_pdf_hash')->orWhere('allegato_pdf_hash', ''); }); } $rows = $q ->orderBy('id') ->limit($limit) ->get(['id', 'allegato_pdf_path', 'allegato_pdf_hash', 'xml_path', 'xml_content', 'stabile_id']); if ($rows->isEmpty()) { Notification::make()->title('Nessuna fattura da rigenerare')->warning()->send(); return; } $svc = app(FatturaElettronicaProtocolloService::class); $ok = 0; $skipped = 0; $errors = 0; $errorLines = []; foreach ($rows as $fattura) { try { $hasOriginalAttachment = is_string($fattura->allegato_pdf_hash ?? null) && trim((string) $fattura->allegato_pdf_hash) !== ''; if ($hasOriginalAttachment) { $skipped++; continue; } $pdfPath = is_string($fattura->allegato_pdf_path ?? null) ? trim((string) $fattura->allegato_pdf_path) : ''; if ($pdfPath !== '' && Storage::disk('local')->exists($pdfPath)) { $oldDir = trim((string) (dirname($pdfPath))) . '/__OLD'; $oldPath = $oldDir . '/' . now()->format('Ymd-His') . '-' . basename($pdfPath); try { Storage::disk('local')->makeDirectory($oldDir); Storage::disk('local')->move($pdfPath, $oldPath); } catch (\Throwable) { // ignore: se non riesce a spostare, rigeneriamo comunque (il service eviterà overwrite) } } $fattura->allegato_pdf_path = null; $fattura->allegato_pdf_nome = null; $fattura->save(); $svc->ensureDocumentoProtocollato($fattura, (int) $user->id); $ok++; } catch (\Throwable $e) { $errors++; if (count($errorLines) < 10) { $errorLines[] = $this->safeUtf8('#' . (int) $fattura->id . ' — ' . $e->getMessage()); } } } $body = 'Selezionate: ' . (int) $rows->count(); $body .= ' · Rigenerate: ' . (int) $ok; $body .= ' · Saltate (PDF allegato): ' . (int) $skipped; $body .= ' · Errori: ' . (int) $errors; $n = Notification::make()->title('Rigenerazione completata'); if ($errors > 0) { $n->body($this->safeUtf8($body . "\n" . implode("\n", $errorLines)))->warning()->send(); } else { $n->body($this->safeUtf8($body))->success()->send(); } }), Action::make('importa_zip_cassetto') ->label('Importa ZIP Cassetto') ->icon('heroicon-o-arrow-up-tray') ->visible(function (): bool { $u = Auth::user(); return $u instanceof User && $u->hasAnyRole(['super-admin', 'admin', 'amministratore']); }) ->form([ FileUpload::make('zip_file') ->label('ZIP del Cassetto (già scaricato)') ->directory(function (): string { $user = Auth::user(); if (! $user instanceof User) { return 'tmp/cassetto-zip'; } $stabile = StabileContext::getActiveStabile($user); if (! $stabile) { return 'tmp/cassetto-zip'; } $adminCode = $stabile?->amministratore?->codice_amministratore; $adminId = (int) ($stabile?->amministratore_id ?: 0); $adminFolder = $adminCode ?: ('ID-' . $adminId); return 'amministratori/' . $adminFolder . '/temp/upload/cassetto-zip'; }) ->disk('local') ->acceptedFileTypes(['application/zip', 'application/x-zip-compressed']) ->required(), Toggle::make('force_update') ->label('Reimport / completa anche se già presente') ->default(false), Toggle::make('no_skip') ->label('Non saltare se già importato (no_skip)') ->default(false), Toggle::make('metadati') ->label('Considera anche metadati') ->default(false), Select::make('importa_righe') ->label('Import righe') ->options([ 'auto' => 'Auto (segue impostazione fornitore)', 'si' => 'Sì (forza import righe)', 'no' => 'No (non importare righe)', ]) ->default('auto') ->required(), Toggle::make('create_fornitore_if_missing') ->label('Crea fornitore se non esiste') ->default(true), ]) ->action(function (array $data): void { $user = Auth::user(); if (! $user instanceof User) { Notification::make()->title('Utente non valido')->danger()->send(); return; } $stabile = StabileContext::getActiveStabile($user); if (! $stabile instanceof Stabile) { Notification::make()->title('Seleziona uno stabile')->danger()->send(); return; } $zipPath = (string) ($data['zip_file'] ?? ''); if ($zipPath === '') { Notification::make()->title('ZIP mancante')->danger()->send(); return; } $absolute = Storage::disk('local')->path($zipPath); if (! is_file($absolute)) { Notification::make()->title('ZIP non trovato')->danger()->send(); return; } $exit = Artisan::call('fe:cassetto-import-local', [ 'stabile_id' => (int) $stabile->id, 'path' => $absolute, '--user_id' => (int) $user->id, '--metadati' => (bool) ($data['metadati'] ?? false) ? 1 : 0, '--force_update' => (bool) ($data['force_update'] ?? false) ? 1 : 0, '--no_skip' => (bool) ($data['no_skip'] ?? false) ? 1 : 0, '--importa_righe' => (string) ($data['importa_righe'] ?? 'auto'), '--create_fornitore_if_missing' => (bool) ($data['create_fornitore_if_missing'] ?? true) ? 1 : 0, ]); $out = trim((string) Artisan::output()); $lines = $out === '' ? [] : preg_split('/\r\n|\r|\n/', $out); $tail = $lines ? implode("\n", array_slice($lines, -10)) : ''; if ($exit !== 0) { Notification::make() ->title('Import ZIP fallito') ->body($this->safeUtf8($tail !== '' ? $tail : 'Comando terminato con errore')) ->danger() ->send(); return; } Notification::make() ->title('Import ZIP completato') ->body($this->safeUtf8($tail !== '' ? $tail : 'Controlla la tab “Scarichi” per log/stato trimestri.')) ->success() ->send(); }), Action::make('scarica_cassetto') ->label('Scarica da Cassetto Fiscale') ->icon('heroicon-o-arrow-down-tray') ->visible(function (): bool { $u = Auth::user(); return $u instanceof User && $u->hasAnyRole(['super-admin', 'admin', 'amministratore']); }) ->form([ Placeholder::make('stabile_attivo_info') ->label('Stabile di destinazione') ->content(fn(): string => $this->getActiveStabileDestinationSummaryProperty()), Select::make('anno') ->label('Anno') ->options(function (): array { $y = (int) now()->format('Y'); $out = []; for ($i = 0; $i < 10; $i++) { $out[(string) ($y - $i)] = (string) ($y - $i); } return $out; }) ->default(fn() => now()->format('Y')) ->reactive(), Select::make('periodo') ->label('Periodo suggerito') ->options([ 'Q1' => '01/01 - 31/03', 'Q2' => '01/04 - 30/06', 'Q3' => '01/07 - 30/09', 'Q4' => '01/10 - 31/12', 'M_CUR' => 'Mese corrente', 'D1' => 'Solo oggi (1 giorno)', 'CUSTOM' => 'Personalizzato (manuale)', ]) ->default('Q1') ->reactive() ->afterStateUpdated(function ($state, callable $set, callable $get): void { $anno = (string) ($get('anno') ?? now()->format('Y')); $q = (string) ($state ?? 'Q1'); [$dal, $al] = match ($q) { 'Q2' => ['01-04-' . $anno, '30-06-' . $anno], 'Q3' => ['01-07-' . $anno, '30-09-' . $anno], 'Q4' => ['01-10-' . $anno, '31-12-' . $anno], 'M_CUR' => [now()->startOfMonth()->format('d-m-Y'), now()->endOfMonth()->format('d-m-Y')], 'D1' => [now()->format('d-m-Y'), now()->format('d-m-Y')], 'CUSTOM' => [ (string) ($get('dal') ?? now()->format('d-m-Y')), (string) ($get('al') ?? now()->format('d-m-Y')), ], default => ['01-01-' . $anno, '31-03-' . $anno], }; $set('dal', $dal); $set('al', $al); }), TextInput::make('dal') ->label('Dal (gg-mm-aaaa)') ->default(fn(callable $get) => '01-01-' . (string) ($get('anno') ?? now()->format('Y'))) ->helperText('Puoi inserire anche un solo giorno (dal = al).') ->required(), TextInput::make('al') ->label('Al (gg-mm-aaaa)') ->default(fn(callable $get) => '31-03-' . (string) ($get('anno') ?? now()->format('Y'))) ->helperText('Intervallo massimo 92 giorni.') ->required(), Toggle::make('metadati') ->label('Scarica anche metadati') ->default(false), Toggle::make('force_update') ->label('Reimport / completa anche se già presente') ->default(false), Toggle::make('no_skip') ->label('Non saltare se lo stesso periodo ha già un log OK') ->default(false), Select::make('importa_righe') ->label('Import righe') ->options([ 'auto' => 'Auto (segue impostazione fornitore)', 'si' => 'Sì (forza import righe)', 'no' => 'No (non importare righe)', ]) ->default('auto') ->required(), Toggle::make('create_fornitore_if_missing') ->label('Crea fornitore se non esiste') ->default(true), ]) ->action(function (array $data): void { $user = Auth::user(); if (! $user instanceof User) { Notification::make()->title('Utente non valido')->danger()->send(); return; } $stabile = StabileContext::getActiveStabile($user); if (! $stabile instanceof Stabile) { Notification::make()->title('Seleziona uno stabile')->danger()->send(); return; } if ($this->isLikelyTechnicalFeShell($stabile)) { $this->notifyTechnicalFeShellBlocked(); return; } $amministratore = $stabile->amministratore; if (! $amministratore) { Notification::make()->title('Amministratore non trovato')->danger()->send(); return; } $dalStr = (string) ($data['dal'] ?? ''); $alStr = (string) ($data['al'] ?? ''); $dal = \DateTimeImmutable::createFromFormat('d-m-Y', $dalStr) ?: null; $al = \DateTimeImmutable::createFromFormat('d-m-Y', $alStr) ?: null; if (! $dal || ! $al) { Notification::make()->title('Date non valide')->danger()->send(); return; } $forceUpdate = (bool) ($data['force_update'] ?? false); $noSkip = (bool) ($data['no_skip'] ?? false); $allowRerun = $forceUpdate || $noSkip; // Blocco trimestri: se il trimestre è già completo, evita ri-scarico (salvo force_update). $periodo = (string) ($data['periodo'] ?? ''); $anno = is_numeric($data['anno'] ?? null) ? (int) $data['anno'] : (int) $dal->format('Y'); if (! $allowRerun && in_array($periodo, ['Q1', 'Q2', 'Q3', 'Q4'], true)) { [$expectedDal, $expectedAl] = match ($periodo) { 'Q2' => [$anno . '-04-01', $anno . '-06-30'], 'Q3' => [$anno . '-07-01', $anno . '-09-30'], 'Q4' => [$anno . '-10-01', $anno . '-12-31'], default => [$anno . '-01-01', $anno . '-03-31'], }; if ($dal->format('Y-m-d') === $expectedDal && $al->format('Y-m-d') === $expectedAl) { if ($this->isCassettoQuarterCompleteFor((int) $amministratore->id, (int) $stabile->id, $expectedDal, $expectedAl)) { Notification::make() ->title('Trimestre già completo') ->body('Per questo trimestre risulta uno scarico OK completo. Per forzare, abilita “Reimport / completa anche se già presente”.') ->warning() ->send(); return; } } } // Anti-duplicati veloce: se esiste già un OK per lo stesso periodo, non avviamo niente. if (! $allowRerun) { $cf = trim((string) ($stabile->codice_fiscale ?? '')); if ($cf === '') { try { $stabile->loadMissing('rubrica'); $cf = trim((string) ($stabile->rubrica?->codice_fiscale ?? '')); } catch (\Throwable) { // ignore } } $cf = strtoupper(trim($cf)); if ($cf !== '') { $requestHash = hash('sha256', implode('|', [ 'cf=' . $cf, 'dal=' . $dal->format('d-m-Y'), 'al=' . $al->format('d-m-Y'), 'metadati=' . (((bool) ($data['metadati'] ?? false)) ? '1' : '0'), 'stabile=' . (int) $stabile->id, 'amm=' . (int) $amministratore->id, ])); $existingOk = DB::table('fe_cassetto_download_logs') ->where('amministratore_id', (int) $amministratore->id) ->where('stabile_id', (int) $stabile->id) ->where('request_hash', $requestHash) ->where('status', 'ok') ->orderByDesc('id') ->first(); if ($existingOk) { Notification::make() ->title('Già scaricato') ->body('Esiste già uno scarico OK per questo periodo (log #' . (int) $existingOk->id . ').') ->warning() ->send(); return; } } } // Max 3 mesi (circa 92 giorni) per richiesta $diffDays = (int) $dal->diff($al)->format('%r%a'); if ($diffDays < 0) { Notification::make()->title('Intervallo date non valido')->danger()->send(); return; } if ($diffDays > 92) { Notification::make()->title('Intervallo troppo ampio (max 3 mesi)')->danger()->send(); return; } // Esecuzione "in background" senza dipendere da un queue worker: // registriamo un callback di terminazione che parte dopo l'invio della risposta HTTP. app()->terminating(function () use ($amministratore, $stabile, $dal, $al, $data, $user): void { try { $job = new RunCassettoFiscaleDownload( (int) $amministratore->id, (int) $stabile->id, $dal->format('Y-m-d'), $al->format('Y-m-d'), (bool) ($data['metadati'] ?? false), (int) $user->id, [ 'force_update' => (bool) ($data['force_update'] ?? false), 'no_skip' => (bool) ($data['no_skip'] ?? false), 'importa_righe' => (string) ($data['importa_righe'] ?? 'auto'), 'create_fornitore_if_missing' => (bool) ($data['create_fornitore_if_missing'] ?? true), ], ); $job->handle(app(CassettoFiscaleDownloadService::class)); } catch (\Throwable $e) { report($e); } }); Notification::make() ->title('Scarico avviato') ->body("Operazione avviata in background. Se hai abilitato 'Reimport / completa' o 'Non saltare', lo stesso periodo verrà rieseguito.\nAggiorna tra poco e controlla la sezione 'Ultimi scarichi'.") ->success() ->send(); }), Action::make('importa_fe') ->label('Importa FE') ->icon('heroicon-o-arrow-up-tray') ->form([ FileUpload::make('fe_files') ->label('File FE (XML/P7M/ZIP)') ->directory(function (): string { $user = Auth::user(); if (! $user instanceof User) { return 'tmp/fatture-fe'; } $stabile = StabileContext::getActiveStabile($user); if (! $stabile) { return 'tmp/fatture-fe'; } $adminCode = $stabile?->amministratore?->codice_amministratore; $adminId = (int) ($stabile?->amministratore_id ?: 0); $adminFolder = $adminCode ?: ('ID-' . $adminId); return 'amministratori/' . $adminFolder . '/temp/upload/fatture-fe'; }) ->disk('local') ->acceptedFileTypes([ '.p7m', '.xml', '.zip', 'application/xml', 'text/xml', 'application/pkcs7-mime', 'application/x-pkcs7-mime', 'application/pkcs7-signature', 'application/x-pkcs7-signature', 'application/octet-stream', 'application/zip', 'application/x-zip-compressed', ]) ->multiple() ->required(), Select::make('stabile_forzato_id') ->label('Destinazione stabile (solo super-admin)') ->helperText('Se valorizzato, ignora lo smistamento automatico (CF/SDI/PEC).') ->options(function () { $user = Auth::user(); if (! $user instanceof User) { return []; } return StabileContext::accessibleStabili($user) ->mapWithKeys(function (Stabile $s) { $adminCode = (string) ($s->amministratore?->codice_amministratore ?: $s->amministratore?->codice_univoco ?: ''); $prefix = $adminCode !== '' ? ($adminCode . ' — ') : ''; return [(string) $s->id => $prefix . $s->codice_stabile . ' - ' . $s->denominazione]; }) ->all(); }) ->searchable() ->nullable() ->visible(function (): bool { $u = Auth::user(); return $u instanceof User && $u->hasAnyRole(['super-admin', 'admin']); }), Toggle::make('force_update') ->label('Reimport / completa anche se già presente') ->default(false), Select::make('importa_righe') ->label('Import righe') ->options([ 'auto' => 'Auto (segue impostazione fornitore)', 'si' => 'Sì (forza import righe)', 'no' => 'No (non importare righe)', ]) ->default('auto') ->required(), Toggle::make('create_fornitore_if_missing') ->label('Crea fornitore se non esiste') ->helperText('Se il fornitore non viene trovato (P.IVA/CF), crea una nuova anagrafica fornitore collegata allo stabile.') ->default(true), ]) ->action(function (array $data): void { $user = Auth::user(); if (! $user instanceof User) { Notification::make()->title('Utente non valido')->danger()->send(); return; } $forcedStabileId = (int) ($data['stabile_forzato_id'] ?? 0); $canForce = $forcedStabileId > 0 && $user->hasAnyRole(['super-admin', 'admin']); if ($canForce && ! Stabile::query()->attivi()->whereKey($forcedStabileId)->exists()) { Notification::make()->title('Stabile forzato non valido')->danger()->send(); return; } $paths = $data['fe_files'] ?? []; $paths = is_array($paths) ? $paths : [$paths]; $importer = new FatturaElettronicaImporter(new FatturaElettronicaXmlParser()); $extractor = app(P7mExtractor::class); $parser = new FatturaElettronicaXmlParser(); $accessibleStabili = StabileContext::accessibleStabili($user)->pluck('id')->map(fn($v) => (int) $v)->all(); $accessibleMap = array_fill_keys($accessibleStabili, true); $imported = 0; $duplicates = 0; $errors = 0; $waterCreated = 0; $waterUpdated = 0; $duplicateDetails = []; $errorDetails = []; $debugLines = []; $debugPath = null; $debugYm = now()->format('Y/m'); $debugPath = 'fe-import-debug/' . $debugYm . '/' . (string) Str::uuid() . '.log'; foreach ($paths as $path) { $permanentP7mPath = null; $permanentXmlPath = null; try { $path = (string) $path; $filename = basename($path); $lower = Str::lower($filename); // ZIP: estrai e importa tutti gli XML/P7M contenuti if (Str::endsWith($lower, '.zip')) { $absZip = Storage::disk('local')->path($path); if (! is_file($absZip)) { throw new \RuntimeException('ZIP non trovato'); } $tmpDir = 'tmp/fe-extract/' . now()->format('Y/m') . '/' . (string) Str::uuid(); Storage::disk('local')->makeDirectory($tmpDir); $absTmp = Storage::disk('local')->path($tmpDir); $zip = new ZipArchive(); $open = $zip->open($absZip); if ($open !== true) { throw new \RuntimeException('ZIP non valido'); } $zip->extractTo($absTmp); $zip->close(); $it = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($absTmp)); foreach ($it as $fileInfo) { if (! $fileInfo instanceof \SplFileInfo || ! $fileInfo->isFile()) { continue; } $innerName = (string) $fileInfo->getFilename(); $innerLower = Str::lower($innerName); if (! (Str::endsWith($innerLower, '.xml') || Str::endsWith($innerLower, '.p7m'))) { continue; } $bytes = file_get_contents($fileInfo->getPathname()); if (! is_string($bytes) || $bytes === '') { $errors++; $errorDetails[] = $filename . '::' . $innerName . ' — file vuoto'; continue; } $isP7m = Str::endsWith($innerLower, '.p7m'); $xml = $isP7m ? $extractor->extractXmlFromP7m($fileInfo->getPathname()) : $bytes; if (! is_string($xml) || trim($xml) === '') { $errors++; $errorDetails[] = $filename . '::' . $innerName . ' — XML non valido/assente'; continue; } $parsed = $parser->parse($xml); $stabileId = $canForce ? $forcedStabileId : $this->resolveStabileIdFromParsed($parsed); if (! $stabileId) { $errors++; $errorDetails[] = $filename . '::' . $innerName . ' — impossibile determinare lo stabile dalla FE'; continue; } if (! isset($accessibleMap[$stabileId])) { $errors++; $errorDetails[] = $filename . '::' . $innerName . ' — stabile non accessibile per l’utente'; continue; } $unique = (string) Str::uuid(); $ym = now()->format('Y/m'); $inbox = $this->inboxBaseForStabile($stabileId, $ym); $permanentP7mPath = null; if ($isP7m) { $permanentP7mPath = $inbox . '/' . $unique . '-' . $innerName; Storage::disk('local')->put($permanentP7mPath, $bytes); } $xmlFilename = $isP7m ? (pathinfo($innerName, PATHINFO_FILENAME) . '.xml') : $innerName; $permanentXmlPath = $inbox . '/' . $unique . '-' . $xmlFilename; Storage::disk('local')->put($permanentXmlPath, $xml); $result = $importer->importXml($xml, 0, $xmlFilename, [ 'xml_path' => $permanentXmlPath, 'p7m_path' => $permanentP7mPath, 'nome_file_p7m' => $isP7m ? $innerName : null, 'force_update' => (bool) ($data['force_update'] ?? false), 'auto_repair_duplicate' => true, 'importa_righe' => (string) ($data['importa_righe'] ?? 'auto'), 'create_fornitore_if_missing' => (bool) ($data['create_fornitore_if_missing'] ?? false), 'allow_fallback_stabile' => false, 'force_stabile_id' => $canForce ? $forcedStabileId : 0, 'user_id' => (int) $user->id, ]); try { $status = (string) ($result['status'] ?? 'unknown'); $msg = (string) ($result['message'] ?? ''); $dbg = $result['debug'] ?? null; $debugLines[] = now()->format('c') . ' | ' . $status . ' | ' . $filename . '::' . $innerName . ($msg !== '' ? ' | ' . $msg : '') . (is_array($dbg) ? ' | debug=' . json_encode($dbg, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : ''); } catch (\Throwable) { // ignore } if (in_array((string) ($result['status'] ?? ''), ['imported', 'duplicate'], true) && is_numeric($result['id'] ?? null)) { $waterResult = $this->autoLinkWaterReadingsForInvoice((int) $result['id'], (int) $user->id); if (($waterResult['status'] ?? null) === 'ok') { $waterCreated += (int) ($waterResult['created_letture'] ?? 0); $waterUpdated += (int) ($waterResult['updated_letture'] ?? 0); } } if (($result['status'] ?? null) === 'imported') { $imported++; } elseif (($result['status'] ?? null) === 'duplicate') { $duplicates++; $duplicateDetails[] = $filename . '::' . $innerName . (($result['message'] ?? null) ? ' — ' . (string) $result['message'] : ''); try { if ($permanentXmlPath) { Storage::disk('local')->delete($permanentXmlPath); } if ($permanentP7mPath) { Storage::disk('local')->delete($permanentP7mPath); } } catch (\Throwable) { // ignore } } else { $errors++; $errorDetails[] = $filename . '::' . $innerName . (($result['message'] ?? null) ? ' — ' . (string) $result['message'] : ''); try { if ($permanentXmlPath) { Storage::disk('local')->delete($permanentXmlPath); } if ($permanentP7mPath) { Storage::disk('local')->delete($permanentP7mPath); } } catch (\Throwable) { // ignore } } } try { Storage::disk('local')->deleteDirectory($tmpDir); } catch (\Throwable) { // ignore } continue; } $bytes = Storage::disk('local')->get($path); if (! is_string($bytes) || $bytes === '') { throw new \RuntimeException('File vuoto'); } $isP7m = Str::endsWith($lower, '.p7m'); $xml = $isP7m ? $extractor->extractXmlFromP7m(Storage::disk('local')->path($path)) : $bytes; if (! is_string($xml) || trim($xml) === '') { throw new \RuntimeException('XML non valido/assente'); } // Smistamento obbligatorio: deve risolvere a uno stabile univoco e accessibile. $parsed = $parser->parse($xml); $stabileId = $canForce ? $forcedStabileId : $this->resolveStabileIdFromParsed($parsed); if (! $stabileId) { throw new \RuntimeException('Impossibile determinare lo stabile dalla FE (CF/SDI/PEC non trovati o non univoci)'); } if (! isset($accessibleMap[$stabileId])) { throw new \RuntimeException('Stabile non accessibile per l’utente'); } $unique = (string) Str::uuid(); $ym = now()->format('Y/m'); $inbox = $this->inboxBaseForStabile($stabileId, $ym); if ($isP7m) { $permanentP7mPath = $inbox . '/' . $unique . '-' . $filename; Storage::disk('local')->put($permanentP7mPath, $bytes); } $xmlFilename = $isP7m ? (pathinfo($filename, PATHINFO_FILENAME) . '.xml') : $filename; $permanentXmlPath = $inbox . '/' . $unique . '-' . $xmlFilename; Storage::disk('local')->put($permanentXmlPath, $xml); $result = $importer->importXml($xml, 0, $xmlFilename, [ 'xml_path' => $permanentXmlPath, 'p7m_path' => $permanentP7mPath, 'nome_file_p7m' => $isP7m ? $filename : null, 'force_update' => (bool) ($data['force_update'] ?? false), 'auto_repair_duplicate' => true, 'importa_righe' => (string) ($data['importa_righe'] ?? 'auto'), 'create_fornitore_if_missing' => (bool) ($data['create_fornitore_if_missing'] ?? false), 'allow_fallback_stabile' => false, 'force_stabile_id' => $canForce ? $forcedStabileId : 0, 'user_id' => (int) $user->id, ]); try { $status = (string) ($result['status'] ?? 'unknown'); $msg = (string) ($result['message'] ?? ''); $dbg = $result['debug'] ?? null; $debugLines[] = now()->format('c') . ' | ' . $status . ' | ' . $filename . ($msg !== '' ? ' | ' . $msg : '') . (is_array($dbg) ? ' | debug=' . json_encode($dbg, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : ''); } catch (\Throwable) { // ignore } if (in_array((string) ($result['status'] ?? ''), ['imported', 'duplicate'], true) && is_numeric($result['id'] ?? null)) { $waterResult = $this->autoLinkWaterReadingsForInvoice((int) $result['id'], (int) $user->id); if (($waterResult['status'] ?? null) === 'ok') { $waterCreated += (int) ($waterResult['created_letture'] ?? 0); $waterUpdated += (int) ($waterResult['updated_letture'] ?? 0); } } if (($result['status'] ?? null) === 'imported') { $imported++; } elseif (($result['status'] ?? null) === 'duplicate') { $duplicates++; $duplicateDetails[] = $this->safeUtf8($filename . (($result['message'] ?? null) ? ' — ' . (string) $result['message'] : '')); try { if ($permanentXmlPath) { Storage::disk('local')->delete($permanentXmlPath); } if ($permanentP7mPath) { Storage::disk('local')->delete($permanentP7mPath); } } catch (\Throwable) { // ignore } } else { $errors++; $errorDetails[] = $this->safeUtf8($filename . (($result['message'] ?? null) ? ' — ' . (string) $result['message'] : '')); try { if ($permanentXmlPath) { Storage::disk('local')->delete($permanentXmlPath); } if ($permanentP7mPath) { Storage::disk('local')->delete($permanentP7mPath); } } catch (\Throwable) { // ignore } } } catch (\Throwable $e) { $errors++; $filename = basename((string) $path); $debugNote = ''; try { $lower = Str::lower($filename); if (Str::endsWith($lower, '.p7m') && isset($bytes) && is_string($bytes) && $bytes !== '') { $debugDir = 'fe-import-errors/' . now()->format('Y/m'); $debugPath = $debugDir . '/' . (string) Str::uuid() . '-' . $filename; Storage::disk('local')->put($debugPath, $bytes); $debugNote = ' (salvato per debug: ' . $debugPath . ')'; } } catch (\Throwable) { // ignore } $errorDetails[] = $this->safeUtf8($filename . ' — ' . $e->getMessage() . $debugNote); try { $debugLines[] = $this->safeUtf8(now()->format('c') . ' | error | ' . $filename . ' | ' . $e->getMessage() . $debugNote); } catch (\Throwable) { // ignore } try { if ($permanentXmlPath) { Storage::disk('local')->delete($permanentXmlPath); } if ($permanentP7mPath) { Storage::disk('local')->delete($permanentP7mPath); } } catch (\Throwable) { // ignore } } finally { try { Storage::disk('local')->delete($path); } catch (\Throwable) { // ignore } } } $logNote = ''; if (($duplicates > 0 || $errors > 0) && $debugPath && count($debugLines) > 0) { try { Storage::disk('local')->put($debugPath, implode("\n", $debugLines) . "\n"); $logNote = "\nLog diagnostica: {$debugPath}"; } catch (\Throwable) { // ignore } } Notification::make() ->title('Import completato') ->body($this->safeUtf8("Importate: {$imported} · Duplicate: {$duplicates} · Errori: {$errors} · Letture acqua create: {$waterCreated} · aggiornate: {$waterUpdated}" . $logNote)) ->success() ->send(); if ($duplicates > 0 && count($duplicateDetails) > 0) { Notification::make() ->title('Duplicati (non importati)') ->body($this->safeUtf8(implode("\n", array_slice($duplicateDetails, 0, 10)) . (count($duplicateDetails) > 10 ? "\n…" : ''))) ->warning() ->send(); } if ($errors > 0 && count($errorDetails) > 0) { Notification::make() ->title('Errori durante l’import') ->body($this->safeUtf8(implode("\n", array_slice($errorDetails, 0, 10)) . (count($errorDetails) > 10 ? "\n…" : ''))) ->danger() ->send(); } }), Action::make('importa_ade_csv') ->label('Importa CSV AdE') ->icon('heroicon-o-clipboard-document-list') ->form([ FileUpload::make('csv_file') ->label('CSV AdE (separatore ; )') ->directory(function (): string { $user = Auth::user(); if (! $user instanceof User) { return 'tmp/ade-csv'; } $stabile = StabileContext::getActiveStabile($user); if (! $stabile) { return 'tmp/ade-csv'; } $adminCode = $stabile?->amministratore?->codice_amministratore; $adminId = (int) ($stabile?->amministratore_id ?: 0); $adminFolder = $adminCode ?: ('ID-' . $adminId); return 'amministratori/' . $adminFolder . '/temp/upload/ade-csv'; }) ->disk('local') ->acceptedFileTypes([ 'text/csv', 'text/plain', 'application/vnd.ms-excel', ]) ->required(), Select::make('stabile_target_id') ->label('Target stabile (solo super-admin)') ->helperText('Se non valorizzato, usa lo stabile attivo.') ->options(function () { $user = Auth::user(); if (! $user instanceof User) { return []; } return StabileContext::accessibleStabili($user) ->mapWithKeys(function (Stabile $s) { $adminCode = (string) ($s->amministratore?->codice_amministratore ?: $s->amministratore?->codice_univoco ?: ''); $prefix = $adminCode !== '' ? ($adminCode . ' — ') : ''; return [(string) $s->id => $prefix . $s->codice_stabile . ' - ' . $s->denominazione]; }) ->all(); }) ->searchable() ->nullable() ->visible(function (): bool { $u = Auth::user(); return $u instanceof User && $u->hasAnyRole(['super-admin', 'admin']); }), TextInput::make('cartella_legacy') ->label('Cartella legacy (opzionale)') ->helperText('Esempio: 0021 (solo metadato, non influenza il matching).') ->maxLength(10) ->nullable(), ]) ->action(function (array $data): void { $user = Auth::user(); if (! $user instanceof User) { Notification::make()->title('Utente non valido')->danger()->send(); return; } $activeStabileId = StabileContext::resolveActiveStabileId($user); if (! $activeStabileId) { Notification::make()->title('Stabile attivo non impostato')->danger()->send(); return; } $forcedStabileId = (int) ($data['stabile_target_id'] ?? 0); $canForce = $forcedStabileId > 0 && $user->hasAnyRole(['super-admin', 'admin']); $targetStabileId = $canForce ? $forcedStabileId : (int) $activeStabileId; $accessibleStabili = StabileContext::accessibleStabili($user)->pluck('id')->map(fn($v) => (int) $v)->all(); $accessibleMap = array_fill_keys($accessibleStabili, true); if (! isset($accessibleMap[$targetStabileId])) { Notification::make()->title('Stabile non accessibile per l’utente')->danger()->send(); return; } $stabile = Stabile::query()->select(['id', 'amministratore_id'])->find($targetStabileId); $amministratoreId = (int) ($stabile?->amministratore_id ?: 0); if ($amministratoreId <= 0) { Notification::make()->title('Amministratore non valido')->danger()->send(); return; } $csvPath = (string) ($data['csv_file'] ?? ''); if ($csvPath === '') { Notification::make()->title('CSV mancante')->danger()->send(); return; } $absolute = Storage::disk('local')->path($csvPath); $cartella = isset($data['cartella_legacy']) ? trim((string) $data['cartella_legacy']) : ''; Artisan::call('gescon:import-fatture-ade-csv', array_filter([ 'amministratore_id' => $amministratoreId, '--stabile_id' => $targetStabileId, '--cartella' => $cartella !== '' ? $cartella : null, '--path' => $absolute, ], fn($v) => $v !== null && $v !== '')); $out = trim((string) Artisan::output()); $lines = $out === '' ? [] : preg_split('/\r\n|\r|\n/', $out); $tail = $lines ? implode("\n", array_slice($lines, -8)) : 'Import completato.'; Notification::make() ->title('Import AdE completato') ->body($this->safeUtf8($tail)) ->success() ->send(); }), ]; } private function inboxBaseForStabile(int $stabileId, string $ym): string { $stabile = Stabile::query() ->with(['amministratore:id,codice_amministratore,codice_univoco']) ->select(['id', 'amministratore_id', 'codice_stabile', 'codice_univoco']) ->find($stabileId); if ($stabile) { $base = ArchivioPaths::stabileBase($stabile, $stabile->amministratore); if (is_string($base) && $base !== '') { return $base . '/fatture-ricevute/inbox/' . $ym; } } $adminId = (int) ($stabile?->amministratore_id ?: 0); return 'amministratori/ID-' . $adminId . '/stabili/ID-' . (int) $stabileId . '/fatture-ricevute/inbox/' . $ym; } private function resolveStabileIdFromParsed(array $parsed): ?int { $cf = isset($parsed['destinatario_cf']) ? strtoupper(str_replace(' ', '', trim((string) $parsed['destinatario_cf']))) : null; $piva = isset($parsed['destinatario_piva']) ? strtoupper(str_replace(' ', '', trim((string) $parsed['destinatario_piva']))) : null; $codice = isset($parsed['codice_destinatario']) ? strtoupper(str_replace(' ', '', trim((string) $parsed['codice_destinatario']))) : null; $pec = $parsed['pec_destinatario'] ?? null; if ($cf) { $match = Stabile::query() ->whereNotNull('codice_fiscale') ->whereRaw("REPLACE(UPPER(codice_fiscale), ' ', '') = ?", [$cf]) ->where('attivo', true) ->get(); if ($match->count() === 1) { return (int) $match->first()->id; } } if ($codice) { $match = Stabile::query() ->whereNotNull('codice_destinatario_sdi') ->whereRaw("REPLACE(UPPER(codice_destinatario_sdi), ' ', '') = ?", [$codice]) ->where('attivo', true) ->get(); if ($match->count() === 1) { return (int) $match->first()->id; } } if ($pec) { $pec = trim(strtolower((string) $pec)); $match = Stabile::query() ->where(function ($q) use ($pec) { $q->whereRaw('LOWER(pec_condominio) = ?', [$pec]) ->orWhereRaw('LOWER(pec_amministratore) = ?', [$pec]); }) ->where('attivo', true) ->get(); if ($match->count() === 1) { return (int) $match->first()->id; } } if ($piva) { $match = Stabile::query() ->select('stabili.id') ->join('amministratori', 'amministratori.id', '=', 'stabili.amministratore_id') ->where('stabili.attivo', true) ->whereRaw("REPLACE(UPPER(amministratori.partita_iva), ' ', '') = ?", [$piva]) ->get(); if ($match->count() === 1) { return (int) $match->first()->id; } } return null; } private function archiveBaseForStabile(int $stabileId): string { $stabile = Stabile::query() ->with(['amministratore:id,codice_amministratore,codice_univoco']) ->select(['id', 'amministratore_id', 'codice_stabile', 'codice_univoco']) ->find($stabileId); $ym = now()->format('Y/m'); if ($stabile) { $base = ArchivioPaths::stabileBase($stabile, $stabile->amministratore); if (is_string($base) && $base !== '') { return $base . '/fatture-ricevute/' . $ym; } } $adminId = (int) ($stabile?->amministratore_id ?: 0); return 'amministratori/ID-' . $adminId . '/stabili/ID-' . (int) $stabileId . '/fatture-ricevute/' . $ym; } protected function getTableQuery(): Builder { $user = Auth::user(); if (! $user instanceof User) { return FatturaElettronica::query()->whereRaw('1 = 0'); } if ($user->hasRole('fornitore')) { [$fornitore] = $this->resolveOperatoreContext(); if (! $fornitore instanceof Fornitore) { return FatturaElettronica::query()->whereRaw('1 = 0'); } return FatturaElettronica::query() ->where('fornitore_id', (int) $fornitore->id); } $activeStabileId = StabileContext::resolveActiveStabileId($user); if (! $activeStabileId) { return FatturaElettronica::query()->whereRaw('1 = 0'); } return FatturaElettronica::query() ->where('stabile_id', $activeStabileId); } public function table(Table $table): Table { return $table ->striped() ->paginationPageOptions([10, 25, 50, 100]) ->defaultPaginationPageOption(50) ->defaultSort('data_fattura', 'desc') ->filters([ SelectFilter::make('anno') ->label('Anno fattura') ->options(function (): array { $user = Auth::user(); if (! $user instanceof User) { return []; } $activeStabileId = StabileContext::resolveActiveStabileId($user); if (! $activeStabileId) { return []; } return FatturaElettronica::query() ->where('stabile_id', $activeStabileId) ->whereNotNull('data_fattura') ->selectRaw('YEAR(data_fattura) as anno') ->distinct() ->orderByDesc('anno') ->pluck('anno', 'anno') ->mapWithKeys(fn($anno) => [(string) $anno => (string) $anno]) ->all(); }) ->query(function (Builder $query, array $data): Builder { $anno = is_numeric($data['value'] ?? null) ? (int) $data['value'] : 0; return $anno > 0 ? $query->whereYear('data_fattura', $anno) : $query; }), SelectFilter::make('ordine') ->label('Ordine') ->options([ 'data_desc' => 'Data (più recenti)', 'data_asc' => 'Data (più vecchie)', 'numero_asc' => 'Numero (A → Z)', 'numero_desc' => 'Numero (Z → A)', 'totale_desc' => 'Totale (più alto)', 'totale_asc' => 'Totale (più basso)', ]) ->default('data_desc') ->query(function (Builder $query, array $data): Builder { $v = (string) ($data['value'] ?? 'data_desc'); return match ($v) { 'data_asc' => $query->reorder('data_fattura', 'asc')->orderBy('id', 'asc'), 'numero_asc' => $query->reorder('numero_fattura', 'asc')->orderBy('id', 'asc'), 'numero_desc' => $query->reorder('numero_fattura', 'desc')->orderBy('id', 'desc'), 'totale_asc' => $query->reorder('totale', 'asc')->orderBy('id', 'asc'), 'totale_desc' => $query->reorder('totale', 'desc')->orderBy('id', 'desc'), default => $query->reorder('data_fattura', 'desc')->orderBy('id', 'desc'), }; }), ]) ->columns([ TextColumn::make('fornitore_denominazione') ->label('Fornitore') ->searchable() ->wrap(), TextColumn::make('numero_fattura') ->label('Numero') ->searchable() ->sortable(), TextColumn::make('data_fattura') ->label('Data') ->date('d/m/Y') ->sortable(), TextColumn::make('totale') ->label('Totale') ->money('EUR') ->sortable(), BadgeColumn::make('stato') ->label('Stato') ->colors([ 'warning' => 'ricevuta', 'success' => 'pagata', 'primary' => 'contabilizzata', 'danger' => 'rifiutata', ]), TextColumn::make('nome_file_p7m') ->label('P7M') ->toggleable(isToggledHiddenByDefault: true) ->wrap(), TextColumn::make('destinatario_cf') ->label('Identificativo destinatario') ->formatStateUsing(fn(FatturaElettronica $record): string => trim((string) ($record->destinatario_cf ?: $record->destinatario_piva ?: '—'))) ->toggleable(isToggledHiddenByDefault: true) ->wrap(), ]) ->actions([ Action::make('apri') ->label('Apri') ->icon('heroicon-o-eye') ->url(fn(FatturaElettronica $record) => FatturaElettronicaScheda::getUrl([ 'record' => $record->id, 'back' => request()->getRequestUri(), ], panel: 'admin-filament')), ]); } }