hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); } public function mount(): void { $this->hubTab = $this->normalizeHubTab($this->hubTab); $this->mountInteractsWithTable(); } public function selectHubTab(string $tab): void { $this->hubTab = $this->normalizeHubTab($tab); } protected function getHeaderActions(): array { return [ Action::make('registra_documento') ->label('Registra documento (PDF)') ->icon('heroicon-o-plus') ->form($this->getDocumentoFormSchema()) ->action(function (array $data): void { $this->createDocumentoFromForm($data, false); }), Action::make('crea_demo_contratto_ascensori') ->label('Crea demo: Contratto Ascensori') ->icon('heroicon-o-sparkles') ->form([ Select::make('stabile_id') ->label('Stabile') ->options(fn() => $this->getStabiliOptions()) ->default(fn() => $this->getDefaultStabileId()) ->searchable() ->required(), TextInput::make('faldone') ->label('Faldone (opzionale)') ->placeholder('Es. CONTR-2024') ->maxLength(50), ]) ->action(function (array $data): void { $this->createDocumentoDemoContrattoAscensori((int) $data['stabile_id'], (string) ($data['faldone'] ?? '')); }), Action::make('nuova_cartella_archivio') ->label('Nuova cartella archivio') ->icon('heroicon-o-folder-plus') ->color('gray') ->form($this->getArchivioFolderFormSchema()) ->action(function (array $data): void { $this->createArchivioFolder($data); }), ]; } protected function getTableQuery(): Builder { $user = Auth::user(); if (! $user instanceof User) { return Documento::query()->whereRaw('1 = 0'); } return $this->getVisibilityService() ->scopeVisibleDocumenti(Documento::query()->with('cartellaArchivio'), $user); } public function table(Table $table): Table { return $table ->striped() ->defaultSort('created_at', 'desc') ->columns([ TextColumn::make('numero_protocollo') ->label('Protocollo') ->copyable() ->searchable(), TextColumn::make('nome') ->label('Titolo') ->searchable() ->limit(50), BadgeColumn::make('tipo_documento') ->label('Categoria') ->toggleable() ->formatStateUsing(fn($state, Documento $record) => $state ?: ($record->tipologia ?: '—')), TextColumn::make('descrizione') ->label('Descrizione') ->limit(50) ->toggleable(isToggledHiddenByDefault: true) ->searchable(), TextColumn::make('fornitore') ->label('Fornitore/Ente') ->searchable() ->limit(35) ->toggleable(), TextColumn::make('cartellaArchivio.nome') ->label('Cartella') ->toggleable() ->wrap(), TextColumn::make('contenuto_ocr') ->label('Testo (OCR)') ->limit(50) ->toggleable(isToggledHiddenByDefault: true) ->searchable(), TextColumn::make('data_documento') ->label('Data') ->date('d-m-Y') ->toggleable(), TextColumn::make('data_scadenza') ->label('Scadenza') ->date('d-m-Y') ->toggleable(), TextColumn::make('importo_collegato') ->label('Importo') ->money('EUR') ->toggleable(), IconColumn::make('archiviato') ->label('Arch.') ->boolean() ->toggleable(), ]) ->filters([ SelectFilter::make('tipo_documento') ->label('Categoria') ->options($this->getCategorieOptions()), ]) ->actions([ Action::make('visualizza_pdf') ->label('Vedi PDF') ->icon('heroicon-o-eye') ->visible(fn(Documento $record) => $this->hasPdf($record)) ->modalHeading(fn(Documento $record): string => trim((string) ($record->nome ?: 'Documento PDF'))) ->modalWidth('7xl') ->modalContent(function (Documento $record) { $openUrl = route('filament.documenti.download', $record); return view('filament.modals.pdf-viewer', [ 'url' => $openUrl . '?inline=1', 'openUrl' => $openUrl, 'title' => trim((string) ($record->nome ?: 'Documento PDF')), ]); }) ->modalSubmitAction(false) ->modalCancelActionLabel('Chiudi'), Action::make('scarica_pdf') ->label('PDF') ->icon('heroicon-o-arrow-down-tray') ->url(fn(Documento $record) => route('filament.documenti.download', $record), true) ->visible(fn(Documento $record) => $this->hasPdf($record)), Action::make('etichetta_11354') ->label('Etichetta 11354') ->icon('heroicon-o-tag') ->url(fn(Documento $record) => route('filament.documenti.etichetta', $record) . '?format=11354&dymo_fix=0&dymo_rot=0&dymo_dx=0&dymo_dy=3&autoprint=1', false), Action::make('etichetta_11354_driver_verticale') ->label('Etichetta 11354 (driver verticale)') ->icon('heroicon-o-tag') ->url(fn(Documento $record) => route('filament.documenti.etichetta', $record) . '?format=11354&dymo_fix=1&dymo_rot=-90&dymo_dx=0&dymo_dy=0&autoprint=1', false), Action::make('etichetta_99014') ->label('Etichetta 99014') ->icon('heroicon-o-tag') ->url(fn(Documento $record) => route('filament.documenti.etichetta', $record) . '?format=99014&autoprint=1', false), ]); } private function getCategorieOptions(): array { return [ 'contratto' => 'Contratti (CONTR)', 'assicurazione' => 'Assicurazioni (ASSIC)', 'certificato' => 'Certificati (CERT)', 'preventivo' => 'Preventivi (PREV)', 'fattura' => 'Fatture (FATT)', 'verbale' => 'Verbali (VERB)', 'altro' => 'Altri (DOC)', ]; } public function getHubStatsProperty(): array { $query = $this->getTableQuery(); return [ 'documenti' => (clone $query)->count(), 'archiviati' => (clone $query)->where('archiviato', true)->count(), 'in_scadenza' => (clone $query)->whereNotNull('data_scadenza')->whereDate('data_scadenza', '<=', now()->addDays(30)->toDateString())->count(), 'template_drive' => count($this->getDriveTemplateFoldersProperty()), ]; } /** @return array */ public function getDriveTemplateFoldersProperty(): array { $items = config('netgescon.google.drive_template_folders', []); $yearRoot = trim((string) config('netgescon.google.drive_template_year_root', '')); $yearModel = trim((string) config('netgescon.google.drive_template_year_model', '')); $folders = is_array($items) ? array_values(array_filter(array_map('strval', $items), fn(string $item) : bool => trim($item) !== '')) : []; if ($yearRoot !== '' && $yearModel !== '') { foreach ($folders as $folder) { $folders[] = $yearRoot . '/' . $yearModel . '/' . $folder; } } return array_values(array_unique($folders)); } /** @return array */ public function getAudienceGroupsProperty(): array { return [ 'Condomini', 'Inquilini', 'Fornitori', 'Collaboratori interni', 'Amministratore', 'Studio completo', ]; } /** @return array */ public function getArchivioFisicoTypesProperty(): array { return ['Faldone', 'Scatola', 'Cartella', 'Busta']; } public function getVisibleFolderRowsProperty(): array { $user = Auth::user(); if (! $user instanceof User) { return []; } $stabileId = $this->resolveActiveStabile()?->id; return $this->getVisibilityService() ->scopeVisibleCartelle(DocumentoArchivioCartella::query()->with('parent', 'stabile'), $user, $stabileId ? (int) $stabileId : null) ->orderBy('sort_order') ->orderBy('nome') ->get() ->map(function (DocumentoArchivioCartella $folder): array { $roles = collect($folder->visibility_roles ?? [])->map(fn($value): string => (string) $value)->filter()->values()->all(); $groups = collect($folder->visibility_groups ?? [])->map(fn($value): string => (string) $value)->filter()->values()->all(); $users = collect($folder->allowed_user_ids ?? [])->filter(fn($value): bool => is_numeric($value) && (int) $value > 0)->map(fn($value): int => (int) $value)->all(); $userNames = $users === [] ? [] : User::query()->whereIn('id', $users)->orderBy('name')->pluck('name')->map(fn($value): string => (string) $value)->all(); return [ 'id' => (int) $folder->id, 'nome' => (string) $folder->nome, 'path' => (string) ($folder->display_path ?: $folder->nome), 'stabile' => (string) ($folder->stabile?->denominazione ?: ('Stabile #' . (int) $folder->stabile_id)), 'scope' => (string) ($folder->visibility_scope ?: 'interno'), 'roles' => $roles, 'groups' => $groups, 'users' => $userNames, 'documents_count' => (int) $folder->documenti()->count(), ]; }) ->all(); } public function getMnemonicCodeHintProperty(): ?string { $stabile = $this->resolveActiveStabile(); if (! $stabile instanceof Stabile) { return null; } $label = Str::upper((string) ($stabile->denominazione ?? 'STABILE')); $letters = preg_replace('/[^A-Z]/', '', Str::ascii($label)) ?: 'STB'; $digits = preg_replace('/\D/', '', $label); $prefix = str_pad(substr($letters, 0, 3), 3, 'X'); $suffix = str_pad(substr($digits !== '' ? $digits : (string) $stabile->id, -5), 5, '0', STR_PAD_LEFT); return $prefix . $suffix; } private function getDocumentoFormSchema(): array { return [ Select::make('stabile_id') ->label('Stabile') ->options(fn() => $this->getStabiliOptions()) ->default(fn() => $this->getDefaultStabileId()) ->searchable() ->live() ->required(), Select::make('cartella_archivio_id') ->label('Cartella archivio') ->options(fn(callable $get): array => $this->getArchivioFolderOptions((int) ($get('stabile_id') ?: 0))) ->searchable() ->nullable(), Select::make('categoria') ->label('Categoria protocollo') ->options($this->getCategorieOptions()) ->required(), TextInput::make('titolo') ->label('Titolo') ->required() ->maxLength(255), Textarea::make('descrizione') ->label('Descrizione') ->rows(4), TextInput::make('fornitore') ->label('Fornitore / Ente / Compagnia') ->maxLength(255), DatePicker::make('data_documento') ->label('Data documento') ->default(now()) ->required(), DatePicker::make('data_scadenza') ->label('Data scadenza') ->nullable(), TextInput::make('importo') ->label('Importo (opzionale)') ->numeric() ->step(0.01) ->minValue(0) ->nullable(), Select::make('movimento_contabile_id') ->label('Movimento contabile (opzionale)') ->options(fn(callable $get) => $this->getMovimentiOptions((int) ($get('stabile_id') ?: 0))) ->searchable() ->nullable(), TextInput::make('faldone') ->label('Faldone (archiviazione fisica)') ->placeholder('Es. CONTR-2024') ->maxLength(50) ->nullable(), Select::make('visibility_scope') ->label('Visibilità') ->options($this->getVisibilityScopeOptions()) ->default('interno') ->native(false) ->required(), Select::make('visibility_roles') ->label('Ruoli ammessi') ->options($this->getVisibilityRoleOptions()) ->multiple() ->native(false), Select::make('visibility_groups') ->label('Gruppi audience') ->options(array_combine($this->audienceGroups, $this->audienceGroups)) ->multiple() ->native(false), FileUpload::make('pdf') ->label('PDF') ->disk('local') ->acceptedFileTypes(['application/pdf']) ->required(), ]; } private function getArchivioFolderFormSchema(): array { return [ Select::make('stabile_id') ->label('Stabile') ->options(fn(): array => $this->getStabiliOptions()) ->default(fn(): ?int => $this->getDefaultStabileId()) ->searchable() ->live() ->required(), Select::make('parent_id') ->label('Cartella padre') ->options(fn(callable $get): array => $this->getArchivioFolderOptions((int) ($get('stabile_id') ?: 0))) ->searchable() ->nullable(), TextInput::make('nome') ->label('Nome cartella') ->required() ->maxLength(160), TextInput::make('codice') ->label('Codice cartella') ->maxLength(80), Select::make('visibility_scope') ->label('Visibilità') ->options($this->getVisibilityScopeOptions()) ->default('interno') ->native(false) ->required(), Select::make('visibility_roles') ->label('Ruoli ammessi') ->options($this->getVisibilityRoleOptions()) ->multiple() ->native(false), Select::make('visibility_groups') ->label('Gruppi audience') ->options(array_combine($this->audienceGroups, $this->audienceGroups)) ->multiple() ->native(false), Select::make('allowed_user_ids') ->label('Utenti specifici') ->options($this->getCollaboratorUserOptions()) ->multiple() ->native(false), Textarea::make('note') ->label('Note cartella') ->rows(3), ]; } private function getStabiliOptions(): array { $user = Auth::user(); if (! $user instanceof User) { return []; } if ($user->hasAnyRole(['super-admin', 'admin'])) { return Stabile::query() ->orderBy('denominazione') ->limit(500) ->get(['id', 'denominazione']) ->mapWithKeys(fn(Stabile $s) => [(string) $s->id => (($s->denominazione ?: ('Stabile #' . $s->id)))]) ->all(); } $allowed = StabileContext::accessibleStabili($user); return $allowed ->sortBy(fn(Stabile $s) => $s->denominazione) ->take(500) ->mapWithKeys(fn(Stabile $s) => [(string) $s->id => (($s->denominazione ?: ('Stabile #' . $s->id)))]) ->all(); } private function getDefaultStabileId(): ?int { $user = Auth::user(); if (! $user instanceof User) { return null; } $activeId = StabileContext::getActiveStabileId($user); if (is_int($activeId) && $activeId > 0) { return $activeId; } return StabileContext::accessibleStabili($user)->first()?->id; } private function getVisibilityService(): DocumentoArchivioVisibilityService { return $this->visibilityService ??= app(DocumentoArchivioVisibilityService::class); } private function getVisibilityScopeOptions(): array { return [ 'interno' => 'Interno studio', 'studio' => 'Studio completo', 'restricted' => 'Limitato a ruoli / utenti', ]; } private function getVisibilityRoleOptions(): array { return [ 'super-admin' => 'Super admin', 'admin' => 'Admin', 'amministratore' => 'Amministratore', 'collaboratore' => 'Collaboratore', ]; } private function getCollaboratorUserOptions(): array { return User::query() ->role(['super-admin', 'admin', 'amministratore', 'collaboratore']) ->orderBy('name') ->limit(300) ->get(['id', 'name', 'email']) ->mapWithKeys(function (User $user): array { $label = trim((string) $user->name); if (trim((string) $user->email) !== '') { $label .= ' <' . trim((string) $user->email) . '>'; } return [(string) $user->id => $label !== '' ? $label : ('Utente #' . (int) $user->id)]; }) ->all(); } private function getArchivioFolderOptions(int $stabileId): array { if ($stabileId <= 0) { return []; } return DocumentoArchivioCartella::query() ->where('stabile_id', $stabileId) ->where('is_active', true) ->orderBy('sort_order') ->orderBy('nome') ->get(['id', 'nome', 'percorso_logico']) ->mapWithKeys(function (DocumentoArchivioCartella $folder): array { $label = trim((string) ($folder->percorso_logico ?: $folder->nome)); return [(string) $folder->id => $label !== '' ? $label : ('Cartella #' . (int) $folder->id)]; }) ->all(); } private function normalizeHubTab(string $tab): string { $allowed = ['archivio-digitale', 'template-drive', 'archivio-fisico', 'permessi']; return in_array($tab, $allowed, true) ? $tab : 'archivio-digitale'; } private function resolveActiveStabile(): ?Stabile { $user = Auth::user(); if (! $user instanceof User) { return null; } return StabileContext::getActiveStabile($user); } private function getMovimentiOptions(int $stabileId): array { if ($stabileId <= 0) { return []; } if (! class_exists(MovimentoContabile::class)) { return []; } return MovimentoContabile::query() ->where('stabile_id', $stabileId) ->orderByDesc('data_movimento') ->orderByDesc('id') ->limit(200) ->get(['id', 'descrizione', 'data_movimento', 'importo']) ->mapWithKeys(function (MovimentoContabile $m) { $label = '#' . $m->id; if ($m->data_movimento) { $label .= ' ' . $m->data_movimento->format('d-m-Y'); } if (is_string($m->descrizione) && trim($m->descrizione) !== '') { $label .= ' — ' . trim($m->descrizione); } if ($m->importo !== null) { $label .= ' — €' . number_format((float) $m->importo, 2, ',', '.'); } return [(string) $m->id => $label]; }) ->all(); } private function createArchivioFolder(array $data): void { $user = Auth::user(); if (! $user instanceof User) { Notification::make()->title('Utente non valido')->danger()->send(); return; } $stabileId = isset($data['stabile_id']) && is_numeric($data['stabile_id']) ? (int) $data['stabile_id'] : 0; if ($stabileId <= 0) { Notification::make()->title('Stabile obbligatorio')->danger()->send(); return; } $allowedIds = StabileContext::accessibleStabili($user)->pluck('id')->map(fn($value): int => (int) $value)->all(); if (! in_array($stabileId, $allowedIds, true) && ! $user->hasAnyRole(['super-admin', 'admin'])) { Notification::make()->title('Stabile non accessibile')->danger()->send(); return; } $name = trim((string) ($data['nome'] ?? '')); if ($name === '') { Notification::make()->title('Nome cartella obbligatorio')->danger()->send(); return; } $parentId = isset($data['parent_id']) && is_numeric($data['parent_id']) ? (int) $data['parent_id'] : null; $parent = $parentId ? DocumentoArchivioCartella::query()->where('stabile_id', $stabileId)->find($parentId) : null; $slug = Str::slug($name); if ($slug === '') { $slug = 'cartella-' . now()->format('YmdHis'); } $logicalPath = collect([ $parent?->percorso_logico, $name, ])->filter(fn($value): bool => is_string($value) && trim($value) !== '')->implode('/'); DocumentoArchivioCartella::query()->create([ 'stabile_id' => $stabileId, 'parent_id' => $parent?->id, 'nome' => $name, 'slug' => $slug, 'codice' => trim((string) ($data['codice'] ?? '')) ?: null, 'percorso_logico' => $logicalPath !== '' ? $logicalPath : $name, 'visibility_scope' => (string) ($data['visibility_scope'] ?? 'interno'), 'visibility_roles' => $this->normalizeStringArray($data['visibility_roles'] ?? []), 'visibility_groups' => $this->normalizeStringArray($data['visibility_groups'] ?? []), 'allowed_user_ids' => $this->normalizeIntArray($data['allowed_user_ids'] ?? []), 'note' => trim((string) ($data['note'] ?? '')) ?: null, 'created_by' => (int) $user->id, 'updated_by' => (int) $user->id, ]); Notification::make()->title('Cartella archivio creata')->success()->send(); } private function createDocumentoFromForm(array $data, bool $isDemo): void { $user = Auth::user(); if (! $user instanceof User) { Notification::make()->title('Utente non valido')->danger()->send(); return; } $stabileId = (int) ($data['stabile_id'] ?? 0); if ($stabileId <= 0) { Notification::make()->title('Stabile obbligatorio')->danger()->send(); return; } $categoria = (string) ($data['categoria'] ?? 'altro'); $titolo = (string) ($data['titolo'] ?? ''); if (trim($titolo) === '') { Notification::make()->title('Titolo obbligatorio')->danger()->send(); return; } $dataDocumento = $data['data_documento'] ?? null; $year = null; if ($dataDocumento instanceof \DateTimeInterface) { $year = (int) $dataDocumento->format('Y'); } elseif (is_string($dataDocumento) && trim($dataDocumento) !== '') { $raw = trim($dataDocumento); foreach (['Y-m-d', 'd-m-Y', 'd/m/Y', 'Y/m/d'] as $fmt) { try { $dt = Carbon::createFromFormat($fmt, $raw); if ($dt) { $year = (int) $dt->format('Y'); break; } } catch (\Throwable) { // try next } } if ($year === null) { try { $year = (int) Carbon::parse($raw)->format('Y'); } catch (\Throwable) { $year = null; } } } if (! is_int($year) || $year < 2000 || $year > 2100) { $year = (int) now()->format('Y'); } $protocollo = $this->generateProtocollo($categoria, $year); $storedPath = (string) ($data['pdf'] ?? ''); if ($storedPath === '') { Notification::make()->title('PDF obbligatorio')->danger()->send(); return; } // Rinomina il file su storage in modo ordinato $safeTitle = Str::slug($titolo); $finalDir = 'documenti/stabili/ID-' . $stabileId . '/' . $year; $finalName = $protocollo . '_' . ($safeTitle !== '' ? $safeTitle : 'documento') . '.pdf'; $finalPath = $finalDir . '/' . $finalName; try { if (Storage::disk('local')->exists($storedPath)) { $bytes = Storage::disk('local')->get($storedPath); Storage::disk('local')->put($finalPath, $bytes); Storage::disk('local')->delete($storedPath); } } catch (\Throwable) { // best-effort } $size = null; try { if (Storage::disk('local')->exists($finalPath)) { $size = Storage::disk('local')->size($finalPath); } } catch (\Throwable) { // ignore } $note = ''; $faldone = trim((string) ($data['faldone'] ?? '')); if ($faldone !== '') { $note = 'Faldone: ' . $faldone; } $tipologia = $this->mapCategoriaToTipologia($categoria); $doc = Documento::query()->create([ 'documentable_type' => null, 'documentable_id' => null, 'stabile_id' => $stabileId, 'cartella_archivio_id' => $this->resolveFolderIdForDocument($stabileId, $data['cartella_archivio_id'] ?? null), 'utente_id' => (int) $user->id, 'nome' => $titolo, 'descrizione' => (string) ($data['descrizione'] ?? ''), 'fornitore' => (string) ($data['fornitore'] ?? ''), 'tipologia' => $tipologia, 'tipo_documento' => $categoria, 'data_documento' => $dataDocumento, 'data_scadenza' => $data['data_scadenza'] ?? null, 'importo_collegato' => $data['importo'] ?? null, 'movimento_contabile_id' => $data['movimento_contabile_id'] ?? null, 'numero_protocollo' => $protocollo, 'nome_file' => $finalName, 'mime_type' => 'application/pdf', 'path_file' => $finalPath, 'percorso_file' => $finalPath, 'estensione' => 'pdf', 'dimensione_file' => $size, 'visibility_scope' => (string) ($data['visibility_scope'] ?? 'interno'), 'visibility_roles' => $this->normalizeStringArray($data['visibility_roles'] ?? []), 'visibility_groups' => $this->normalizeStringArray($data['visibility_groups'] ?? []), 'data_upload' => now(), 'note' => $note, 'is_demo' => $isDemo, ]); // Estrai testo per ricerca (best-effort) try { app(PdfTextExtractionService::class)->extractAndStore($doc, false); } catch (\Throwable) { // ignore } Notification::make() ->title('Documento registrato') ->body('Protocollo: ' . $protocollo) ->success() ->send(); } private function createDocumentoDemoContrattoAscensori(int $stabileId, string $faldone): void { $user = Auth::user(); if (! $user instanceof User) { Notification::make()->title('Utente non valido')->danger()->send(); return; } $year = 2024; $protocollo = $this->generateProtocollo('contratto', $year); $titolo = 'Contratto Manutenzione Ascensori 2024'; $descrizione = 'Contratto annuale per manutenzione impianti ascensore con Acme Elevators Srl.'; $html = '

' . e($titolo) . '

'; $html .= '

Protocollo: ' . e($protocollo) . '

'; $html .= '

' . e($descrizione) . '

'; $html .= '
'; $html .= '

Fornitore: Acme Elevators Srl

'; $html .= '

Dal: 01/01/2024 Al: 31/12/2024

'; $html .= '

Totale annuo: € 2.400,00

'; $html .= '

Modalità pagamento: Trimestrale

'; $html .= '

Data stipula: 15/12/2023

'; $dompdf = new Dompdf(); $dompdf->loadHtml('' . $html . ''); $dompdf->setPaper('A4'); $dompdf->render(); $pdfBytes = $dompdf->output(); $finalDir = 'documenti/stabili/ID-' . $stabileId . '/' . $year; $finalName = $protocollo . '_contratto_ascensori_2024.pdf'; $finalPath = $finalDir . '/' . $finalName; Storage::disk('local')->put($finalPath, $pdfBytes); $size = null; try { $size = Storage::disk('local')->size($finalPath); } catch (\Throwable) { // ignore } $note = ''; $faldone = trim($faldone); if ($faldone !== '') { $note = 'Faldone: ' . $faldone; } else { $note = 'Faldone: CONTR-2024'; } $doc = Documento::query()->create([ 'stabile_id' => $stabileId, 'utente_id' => (int) $user->id, 'nome' => $titolo, 'descrizione' => $descrizione, 'fornitore' => 'Acme Elevators Srl', 'tipologia' => 'contratto', 'tipo_documento' => 'contratto', 'data_documento' => '2023-12-15', 'data_scadenza' => '2024-12-31', 'importo_collegato' => 2400.00, 'numero_protocollo' => $protocollo, 'nome_file' => $finalName, 'mime_type' => 'application/pdf', 'path_file' => $finalPath, 'percorso_file' => $finalPath, 'estensione' => 'pdf', 'dimensione_file' => $size, 'visibility_scope' => 'interno', 'data_upload' => now(), 'note' => $note, 'is_demo' => true, ]); try { app(PdfTextExtractionService::class)->extractAndStore($doc, true); } catch (\Throwable) { // ignore } Notification::make() ->title('Demo creato') ->body('Protocollo: ' . $protocollo) ->success() ->send(); } private function generateProtocollo(string $categoria, int $year): string { $categoria = strtolower(trim($categoria)); $prefissi = [ 'contratto' => 'CONTR', 'assicurazione' => 'ASSIC', 'certificato' => 'CERT', 'preventivo' => 'PREV', // Importante: i protocolli fattura devono essere per-anno. // Usiamo FT come prefisso compatto e compatibile con l'archiviazione. 'fattura' => 'FT', 'verbale' => 'VERB', 'altro' => 'DOC', ]; $pref = $prefissi[$categoria] ?? 'DOC'; $ultimo = Documento::query() ->whereNotNull('numero_protocollo') ->where(function ($q) use ($pref, $year) { $q->where('numero_protocollo', 'like', $pref . '-' . $year . '-%'); // Compatibilità: alcuni documenti legacy potrebbero avere prefisso FATT. if ($pref === 'FT') { $q->orWhere('numero_protocollo', 'like', 'FATT-' . $year . '-%'); } }) ->orderBy('numero_protocollo', 'desc') ->first(['numero_protocollo']); $progressivo = 1; if ($ultimo && is_string($ultimo->numero_protocollo)) { $parts = explode('-', $ultimo->numero_protocollo); $last = (int) (end($parts) ?: 0); if ($last > 0) { $progressivo = $last + 1; } } return sprintf('%s-%d-%03d', $pref, $year, $progressivo); } private function hasPdf(Documento $doc): bool { $p = (string) ($doc->percorso_file ?: ($doc->path_file ?: '')); return $p !== '' && str_ends_with(strtolower($p), '.pdf'); } private function mapCategoriaToTipologia(string $categoria): string { $categoria = strtolower(trim($categoria)); // `documenti.tipologia` è un enum: manteniamoci sui valori previsti. return match ($categoria) { 'contratto' => 'contratto', 'certificato' => 'certificato', 'preventivo' => 'preventivo', 'fattura' => 'fattura', 'verbale' => 'verbale', default => 'altro', }; } private function resolveFolderIdForDocument(int $stabileId, mixed $folderId): ?int { if (! is_numeric($folderId)) { return null; } $folder = DocumentoArchivioCartella::query() ->where('stabile_id', $stabileId) ->whereKey((int) $folderId) ->first(['id']); return $folder ? (int) $folder->id : null; } private function normalizeStringArray(mixed $values): array { if (! is_array($values)) { return []; } return collect($values) ->map(fn($value): string => trim((string) $value)) ->filter(fn(string $value): bool => $value !== '') ->unique() ->values() ->all(); } private function normalizeIntArray(mixed $values): array { if (! is_array($values)) { return []; } return collect($values) ->filter(fn($value): bool => is_numeric($value) && (int) $value > 0) ->map(fn($value): int => (int) $value) ->unique() ->values() ->all(); } }