netgescon-day0/app/Filament/Pages/Strumenti/DocumentiArchivio.php

1949 lines
78 KiB
PHP

<?php
namespace App\Filament\Pages\Strumenti;
use App\Models\Documento;
use App\Models\DocumentoArchivioCartella;
use App\Models\DocumentoArchivioFisicoItem;
use App\Models\MovimentoContabile;
use App\Models\Stabile;
use App\Models\User;
use App\Services\Documenti\DocumentoArchivioVisibilityService;
use App\Services\Documenti\PdfTextExtractionService;
use App\Support\StabileContext;
use BackedEnum;
use Carbon\Carbon;
use Dompdf\Dompdf;
use Filament\Actions\Action;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Tables\Columns\BadgeColumn;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Livewire\Attributes\Url;
use UnitEnum;
class DocumentiArchivio extends Page implements HasTable
{
use InteractsWithTable;
#[Url( as : 'tab')]
public string $hubTab = 'protocollo-comunicazioni';
public string $protocolloSearch = '';
public string $protocolloCategory = '';
public string $protocolloView = 'cards';
#[Url(as: 'folder')]
public ?int $currentFolderId = null;
public array $physicalArchiveForm = [];
public array $physicalArchiveMoveForm = [];
public string $physicalArchiveSearch = '';
public string $physicalArchiveTypeFilter = '';
protected static ?string $navigationLabel = 'Documenti (PDF)';
protected static ?string $title = 'Documenti (PDF)';
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-document-text';
protected static UnitEnum|string|null $navigationGroup = 'Strumenti';
protected static ?int $navigationSort = 61;
protected static ?string $slug = 'strumenti/documenti';
protected string $view = 'filament.pages.strumenti.documenti-archivio';
private ?DocumentoArchivioVisibilityService $visibilityService = null;
public static function canAccess(): bool
{
$user = Auth::user();
return $user instanceof User
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
}
public function mount(): void
{
$this->hubTab = $this->normalizeHubTab($this->hubTab);
$this->syncInvoiceFolders();
$this->initializePhysicalArchiveForms();
$this->mountInteractsWithTable();
}
public function selectHubTab(string $tab): void
{
$this->hubTab = $this->normalizeHubTab($tab);
if ($this->hubTab === 'archivio-digitale') {
$this->syncInvoiceFolders();
}
}
public function setProtocolloView(string $view): void
{
$this->protocolloView = in_array($view, ['cards', 'list'], true) ? $view : 'cards';
}
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');
}
$query = $this->getVisibilityService()
->scopeVisibleDocumenti(Documento::query()->with('cartellaArchivio'), $user);
if ($this->hubTab === 'archivio-digitale') {
$stabileId = (int) ($this->resolveActiveStabile()?->id ?? 0);
if ($stabileId > 0) {
$query->where('stabile_id', $stabileId)
->where('tipo_documento', 'fattura');
if ($this->currentFolderId) {
$query->where('cartella_archivio_id', $this->currentFolderId);
} else {
$query->whereRaw('1 = 0');
}
}
}
return $query;
}
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_file')
->label('Nome archivio')
->state(fn(Documento $record): string => trim((string) ($record->nome_file ?: $record->nome ?: '—')))
->searchable()
->limit(65)
->wrap(),
BadgeColumn::make('tipo_documento')
->label('Categoria')
->toggleable()
->formatStateUsing(fn($state, Documento $record) => $state ?: ($record->tipologia ?: '—')),
TextColumn::make('archive_reference')
->label('Percorso archivio')
->limit(55)
->wrap()
->toggleable(),
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('metadati_archivio.classe')
->label('Classe archivio')
->state(fn(Documento $record): string => (string) data_get($record->metadati_archivio ?? [], 'classe_label', '—'))
->toggleable(),
TextColumn::make('metadati_archivio.supporto_fisico')
->label('Supporto')
->state(fn(Documento $record): string => (string) data_get($record->metadati_archivio ?? [], 'supporto_fisico', '—'))
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('metadati_archivio.posizione_fisica')
->label('Posizione fisica')
->state(fn(Documento $record): string => $this->formatPhysicalArchiveLocation($record))
->toggleable(),
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')
->hiddenLabel()
->icon('heroicon-o-eye')
->tooltip('Visualizza PDF')
->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')
->hiddenLabel()
->icon('heroicon-o-arrow-down-tray')
->tooltip('Scarica PDF')
->url(fn(Documento $record) => route('filament.documenti.download', $record), true)
->visible(fn(Documento $record) => $this->hasPdf($record)),
Action::make('stampa_pdf')
->hiddenLabel()
->icon('heroicon-o-printer')
->tooltip('Apri per stampa')
->url(fn(Documento $record) => route('filament.documenti.download', $record) . '?inline=1', true)
->visible(fn(Documento $record) => $this->hasPdf($record)),
Action::make('stampa_pdf_watermark')
->hiddenLabel()
->icon('heroicon-o-document-duplicate')
->tooltip('Apri con watermark archivio')
->url(fn(Documento $record) => route('filament.documenti.download', $record) . '?inline=1&watermark=1', true)
->visible(fn(Documento $record) => $this->hasPdf($record)),
Action::make('archivia_fisico')
->hiddenLabel()
->icon('heroicon-o-archive-box')
->tooltip('Collega archivio fisico')
->fillForm(fn(Documento $record): array => [
'supporto_fisico' => data_get($record->metadati_archivio ?? [], 'supporto_fisico'),
'modello_etichetta' => data_get($record->metadati_archivio ?? [], 'modello_etichetta', '11354'),
'busta_numero' => data_get($record->metadati_archivio ?? [], 'busta_numero'),
'fascicolo_numero' => data_get($record->metadati_archivio ?? [], 'fascicolo_numero'),
'contenitore_numero' => data_get($record->metadati_archivio ?? [], 'contenitore_numero'),
'periodo_riferimento_da' => data_get($record->metadati_archivio ?? [], 'periodo_da'),
'periodo_riferimento_a' => data_get($record->metadati_archivio ?? [], 'periodo_a'),
'watermark_predefinito' => (bool) data_get($record->metadati_archivio ?? [], 'watermark_predefinito', false),
])
->form([
Select::make('supporto_fisico')
->label('Supporto fisico')
->options(array_combine($this->archivioFisicoTypes, $this->archivioFisicoTypes))
->searchable(),
Select::make('modello_etichetta')
->label('Modello etichetta')
->options($this->getModelliEtichettaOptions())
->default('11354')
->native(false),
TextInput::make('contenitore_numero')
->label('Contenitore / faldone'),
TextInput::make('busta_numero')
->label('Busta n.'),
TextInput::make('fascicolo_numero')
->label('Fascicolo n.'),
DatePicker::make('periodo_riferimento_da')
->label('Periodo dal'),
DatePicker::make('periodo_riferimento_a')
->label('Periodo al'),
Toggle::make('watermark_predefinito')
->label('Watermark predefinito in stampa')
->default(false),
])
->action(function (Documento $record, array $data): void {
$meta = is_array($record->metadati_archivio ?? null) ? $record->metadati_archivio : [];
$meta['supporto_fisico'] = $this->normalizeNullableText($data['supporto_fisico'] ?? null);
$meta['modello_etichetta'] = $this->normalizeNullableText($data['modello_etichetta'] ?? null);
$meta['contenitore_numero'] = $this->normalizeNullableText($data['contenitore_numero'] ?? null);
$meta['busta_numero'] = $this->normalizeNullableText($data['busta_numero'] ?? null);
$meta['fascicolo_numero'] = $this->normalizeNullableText($data['fascicolo_numero'] ?? null);
$meta['periodo_da'] = $data['periodo_riferimento_da'] ?? null;
$meta['periodo_a'] = $data['periodo_riferimento_a'] ?? null;
$meta['watermark_predefinito'] = (bool) ($data['watermark_predefinito'] ?? false);
$record->metadati_archivio = $meta;
$record->save();
Notification::make()
->title('Archivio fisico aggiornato')
->success()
->send();
}),
Action::make('etichetta_11354')
->hiddenLabel()
->icon('heroicon-o-tag')
->tooltip('Etichetta 11354')
->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')
->hiddenLabel()
->icon('heroicon-o-tag')
->tooltip('Etichetta 11354 verticale')
->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')
->hiddenLabel()
->icon('heroicon-o-tag')
->tooltip('Etichetta 99014')
->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)',
'planimetria' => 'Planimetrie (PLAN)',
'fascicolo' => 'Fascicolo fabbricato (FASC)',
'anagrafica' => 'Anagrafica condominiale (ANAG)',
'tabella' => 'Tabelle millesimali (TABM)',
'regolamento' => 'Regolamento (REGO)',
'scritture' => 'Scritture contabili (CONT)',
'chiavi' => 'Chiavi stabile (CHIA)',
'timbri' => 'Timbri stabile (TIMB)',
'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()),
];
}
public function openDigitalFolder(?int $folderId = null): void
{
$this->syncInvoiceFolders();
if ($folderId === null) {
$this->currentFolderId = null;
return;
}
$folder = DocumentoArchivioCartella::query()->find($folderId);
if (! $folder instanceof DocumentoArchivioCartella) {
$this->currentFolderId = null;
return;
}
$stabileId = (int) ($this->resolveActiveStabile()?->id ?? 0);
if ($stabileId > 0 && (int) $folder->stabile_id !== $stabileId) {
return;
}
$this->currentFolderId = (int) $folder->id;
}
public function getDigitalArchiveRootLabelProperty(): string
{
$stabile = $this->resolveActiveStabile();
return trim((string) ($stabile?->denominazione ?: 'Stabile attivo'));
}
public function getDigitalArchiveCurrentFolderProperty(): ?DocumentoArchivioCartella
{
if (! $this->currentFolderId) {
return null;
}
return DocumentoArchivioCartella::query()->find($this->currentFolderId);
}
public function getDigitalArchiveFoldersProperty(): array
{
$stabile = $this->resolveActiveStabile();
if (! $stabile instanceof Stabile) {
return [];
}
$root = $this->ensureInvoiceRootFolder((int) $stabile->id);
if (! $root instanceof DocumentoArchivioCartella) {
return [];
}
if (! $this->currentFolderId) {
return [[
'id' => (int) $root->id,
'nome' => (string) $root->nome,
'descrizione' => 'Cartella fatture PDF',
'conteggio' => (int) Documento::query()
->where('stabile_id', $stabile->id)
->where('tipo_documento', 'fattura')
->count(),
]];
}
return DocumentoArchivioCartella::query()
->where('parent_id', $this->currentFolderId)
->where('is_active', true)
->orderBy('sort_order')
->orderBy('nome')
->get()
->map(fn(DocumentoArchivioCartella $folder): array => [
'id' => (int) $folder->id,
'nome' => (string) $folder->nome,
'descrizione' => (string) ($folder->percorso_logico ?: $folder->nome),
'conteggio' => (int) $folder->documenti()->count(),
])
->all();
}
public function getDigitalArchiveBreadcrumbsProperty(): array
{
$items = [[
'label' => $this->digitalArchiveRootLabel,
'folder_id' => null,
]];
$current = $this->digitalArchiveCurrentFolder;
if (! $current instanceof DocumentoArchivioCartella) {
return $items;
}
$chain = collect();
while ($current instanceof DocumentoArchivioCartella) {
$chain->prepend([
'label' => (string) $current->nome,
'folder_id' => (int) $current->id,
]);
$current = $current->parent;
}
return array_merge($items, $chain->all());
}
/** @return array<int, string> */
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<int, string> */
public function getAudienceGroupsProperty(): array
{
return [
'Condomini',
'Inquilini',
'Fornitori',
'Collaboratori interni',
'Amministratore',
'Studio completo',
];
}
/** @return array<int, string> */
public function getArchivioFisicoTypesProperty(): array
{
return [
'Busta trasparente con anelli da archiviare',
'Cartella a tre lembi',
'Faldone con anelli',
'Faldone con lacci',
'Scatola',
];
}
/** @return array<string, string> */
public function getArchivioClasseOptions(): array
{
return [
'libro_verbali' => 'Libro verbali',
'planimetria_stabile' => 'Planimetria stabile',
'elaborato_planimetrico' => 'Elaborato planimetrico',
'scheda_anagrafica_condominiale' => 'Scheda anagrafica condominiale',
'fascicolo_fabbricato' => 'Fascicolo fabbricato',
'polizza_fabbricato' => 'Polizza global fabbricato',
'certificazioni_impianti' => 'Certificazioni e conformita impianti',
'decreti_vincolo' => 'Decreti di vincolo',
'tabelle_millesimali' => 'Tabelle millesimali',
'regolamento_condominio' => 'Regolamento di condominio',
'rendiconti_approvati' => 'Rendiconti approvati',
'raccolta_contratti' => 'Raccolta contratti stipulati',
'servitu_diritti_terzi' => 'Servitu e diritti di terzi',
'giustificativi_spesa' => 'Giustificativi di spesa',
'scritture_contabili' => 'Scritture contabili e sostituto imposta',
'codice_fiscale' => 'Codice fiscale',
'partita_iva_amministratore' => 'Partita IVA amministratore',
'chiavi_stabile' => 'Chiavi stabile',
'timbri_stabile' => 'Timbri stabile',
'altro' => 'Altro contenuto archivio',
];
}
/** @return array<string, string> */
public function getModelliEtichettaOptions(): array
{
return [
'11354' => 'DYMO 11354',
'99014' => 'DYMO 99014',
];
}
/** @return array<int, array{numero:int,titolo:string}> */
public function getArchivioObbligatorioRowsProperty(): array
{
return [
['numero' => 1, 'titolo' => 'Libro dei verbali eventualmente vidimato'],
['numero' => 2, 'titolo' => 'Planimetria dello stabile aggiornata'],
['numero' => 3, 'titolo' => 'Elaborato planimetrico parti comuni'],
['numero' => 4, 'titolo' => 'Scheda anagrafica condominiale e riferimenti catastali'],
['numero' => 5, 'titolo' => 'Schede tecniche impianti e beni comuni'],
['numero' => 15, 'titolo' => 'Polizza global fabbricato'],
['numero' => 16, 'titolo' => 'Attestati di conformita e nulla osta'],
['numero' => 18, 'titolo' => 'Tabelle millesimali ed elaborati di calcolo'],
['numero' => 19, 'titolo' => 'Regolamento di condominio'],
['numero' => 20, 'titolo' => 'Rendiconti approvati ultimi cinque anni'],
['numero' => 21, 'titolo' => 'Raccolta contratti stipulati'],
['numero' => 22, 'titolo' => 'Servitu e diritti di terzi'],
['numero' => 23, 'titolo' => 'Giustificativi di spesa a conservazione lunga'],
['numero' => 24, 'titolo' => 'Scritture contabili e sostituto imposta'],
['numero' => 25, 'titolo' => 'Codice fiscale'],
['numero' => 26, 'titolo' => 'Partita IVA amministratore'],
['numero' => 27, 'titolo' => 'Chiavi dello stabile'],
['numero' => 28, 'titolo' => 'Timbri dello stabile'],
];
}
public function getPhysicalArchiveTypeOptionsProperty(): array
{
return DocumentoArchivioFisicoItem::TIPI;
}
public function getPhysicalArchiveParentOptionsProperty(): array
{
$stabileId = (int) ($this->resolveActiveStabile()?->id ?? 0);
if ($stabileId <= 0) {
return [];
}
return DocumentoArchivioFisicoItem::query()
->where('stabile_id', $stabileId)
->whereIn('tipo_item', ['busta_trasparente', 'cartella_tre_lembi', 'faldone_anelli', 'faldone_lacci', 'scatola'])
->orderBy('codice_univoco')
->get(['id', 'codice_univoco', 'titolo'])
->mapWithKeys(fn(DocumentoArchivioFisicoItem $item): array => [
(string) $item->id => trim($item->codice_univoco . ' · ' . $item->titolo),
])
->all();
}
public function getPhysicalArchiveDocumentOptionsProperty(): array
{
$stabileId = (int) ($this->resolveActiveStabile()?->id ?? 0);
if ($stabileId <= 0) {
return [];
}
return Documento::query()
->where('stabile_id', $stabileId)
->orderByDesc('data_documento')
->orderByDesc('id')
->limit(200)
->get(['id', 'nome_file', 'nome', 'data_documento'])
->mapWithKeys(function (Documento $documento): array {
$label = trim((string) ($documento->nome_file ?: $documento->nome ?: ('Documento #' . $documento->id)));
if ($documento->data_documento) {
$label .= ' · ' . $documento->data_documento->format('d-m-Y');
}
return [(string) $documento->id => $label];
})
->all();
}
public function getPhysicalArchiveRowsProperty(): array
{
$stabileId = (int) ($this->resolveActiveStabile()?->id ?? 0);
if ($stabileId <= 0) {
return [];
}
$query = DocumentoArchivioFisicoItem::query()
->with(['parent', 'documento'])
->where('stabile_id', $stabileId)
->orderByDesc('data_archiviazione')
->orderByDesc('id');
if (trim($this->physicalArchiveSearch) !== '') {
$search = trim($this->physicalArchiveSearch);
$query->where(function (Builder $builder) use ($search): void {
$builder->where('titolo', 'like', '%' . $search . '%')
->orWhere('codice_univoco', 'like', '%' . $search . '%')
->orWhere('voce_titolo', 'like', '%' . $search . '%')
->orWhere('note', 'like', '%' . $search . '%');
});
}
if (trim($this->physicalArchiveTypeFilter) !== '') {
$query->where('tipo_item', $this->physicalArchiveTypeFilter);
}
return $query
->limit(150)
->get()
->map(function (DocumentoArchivioFisicoItem $item): array {
return [
'id' => (int) $item->id,
'codice_univoco' => (string) $item->codice_univoco,
'tipo' => (string) $item->tipo_label,
'titolo' => (string) $item->titolo,
'voce' => trim(collect([$item->voce_numero ? ('Voce ' . $item->voce_numero) : null, $item->voce_titolo])->filter()->implode(' · ')),
'data_archiviazione' => $item->data_archiviazione?->format('d-m-Y') ?: '—',
'parent' => $item->parent ? trim($item->parent->codice_univoco . ' · ' . $item->parent->titolo) : 'Radice',
'percorso' => (string) $item->percorso_fisico,
'documento' => $item->documento ? trim((string) ($item->documento->nome_file ?: $item->documento->nome ?: ('Documento #' . $item->documento->id))) : null,
'ubicazione' => collect([$item->magazzino, $item->scaffale, $item->ripiano, $item->ubicazione_dettaglio])->filter()->implode(' · '),
'modello_etichetta' => (string) ($item->modello_etichetta ?: '11354'),
];
})
->all();
}
public function getPhysicalArchiveContainerTreeProperty(): array
{
$stabileId = (int) ($this->resolveActiveStabile()?->id ?? 0);
if ($stabileId <= 0) {
return [];
}
return DocumentoArchivioFisicoItem::query()
->where('stabile_id', $stabileId)
->whereNull('parent_id')
->orderBy('codice_univoco')
->get()
->map(function (DocumentoArchivioFisicoItem $item): array {
return [
'id' => (int) $item->id,
'codice_univoco' => (string) $item->codice_univoco,
'titolo' => (string) $item->titolo,
'tipo' => (string) $item->tipo_label,
'children_count' => (int) $item->children()->count(),
];
})
->all();
}
public function savePhysicalArchiveEntry(): void
{
$user = Auth::user();
if (! $user instanceof User) {
Notification::make()->title('Utente non valido')->danger()->send();
return;
}
$stabileId = (int) ($this->resolveActiveStabile()?->id ?? 0);
if ($stabileId <= 0) {
Notification::make()->title('Stabile attivo non disponibile')->danger()->send();
return;
}
$titolo = trim((string) ($this->physicalArchiveForm['titolo'] ?? ''));
$tipoItem = trim((string) ($this->physicalArchiveForm['tipo_item'] ?? 'documento'));
if ($titolo === '' || ! array_key_exists($tipoItem, DocumentoArchivioFisicoItem::TIPI)) {
Notification::make()->title('Titolo e tipologia sono obbligatori')->danger()->send();
return;
}
$parentId = is_numeric($this->physicalArchiveForm['parent_id'] ?? null) ? (int) $this->physicalArchiveForm['parent_id'] : null;
$parent = $parentId
? DocumentoArchivioFisicoItem::query()->where('stabile_id', $stabileId)->find($parentId)
: null;
$voceNumero = is_numeric($this->physicalArchiveForm['voce_numero'] ?? null) ? (int) $this->physicalArchiveForm['voce_numero'] : null;
$voceTitolo = trim((string) ($this->physicalArchiveForm['voce_titolo'] ?? ''));
if ($voceNumero && $voceTitolo === '') {
$voceTitolo = collect($this->archivioObbligatorioRows)
->firstWhere('numero', $voceNumero)['titolo'] ?? '';
}
$item = DocumentoArchivioFisicoItem::query()->create([
'stabile_id' => $stabileId,
'parent_id' => $parent?->id,
'documento_id' => is_numeric($this->physicalArchiveForm['documento_id'] ?? null) ? (int) $this->physicalArchiveForm['documento_id'] : null,
'voce_numero' => $voceNumero,
'voce_titolo' => $voceTitolo !== '' ? $voceTitolo : null,
'tipo_item' => $tipoItem,
'titolo' => $titolo,
'note' => $this->normalizeNullableText($this->physicalArchiveForm['note'] ?? null),
'data_archiviazione' => $this->physicalArchiveForm['data_archiviazione'] ?? now()->toDateString(),
'supporto_fisico' => $this->normalizeNullableText($this->physicalArchiveForm['supporto_fisico'] ?? null),
'modello_etichetta' => $this->normalizeNullableText($this->physicalArchiveForm['modello_etichetta'] ?? null) ?: '11354',
'magazzino' => $this->normalizeNullableText($this->physicalArchiveForm['magazzino'] ?? null),
'scaffale' => $this->normalizeNullableText($this->physicalArchiveForm['scaffale'] ?? null),
'ripiano' => $this->normalizeNullableText($this->physicalArchiveForm['ripiano'] ?? null),
'ubicazione_dettaglio' => $this->normalizeNullableText($this->physicalArchiveForm['ubicazione_dettaglio'] ?? null),
'created_by' => (int) $user->id,
'updated_by' => (int) $user->id,
'metadati' => [
'origine_digitale' => is_numeric($this->physicalArchiveForm['documento_id'] ?? null),
],
]);
if ($item->documento instanceof Documento) {
$meta = is_array($item->documento->metadati_archivio ?? null) ? $item->documento->metadati_archivio : [];
$meta['physical_archive_item_id'] = (int) $item->id;
$meta['physical_archive_code'] = (string) $item->codice_univoco;
$meta['contenitore_numero'] = $parent?->codice_univoco;
$meta['supporto_fisico'] = $item->supporto_fisico;
$item->documento->forceFill([
'metadati_archivio' => $meta,
])->save();
}
$this->initializePhysicalArchiveForms();
Notification::make()
->title('Voce archivio fisico registrata')
->body('Codice: ' . $item->codice_univoco)
->success()
->send();
}
public function movePhysicalArchiveItem(): void
{
$stabileId = (int) ($this->resolveActiveStabile()?->id ?? 0);
if ($stabileId <= 0) {
Notification::make()->title('Stabile attivo non disponibile')->danger()->send();
return;
}
$sourceCode = trim((string) ($this->physicalArchiveMoveForm['source_code'] ?? ''));
$targetCode = trim((string) ($this->physicalArchiveMoveForm['target_code'] ?? ''));
if ($sourceCode === '' || $targetCode === '') {
Notification::make()->title('Codice sorgente e destinazione obbligatori')->danger()->send();
return;
}
$source = DocumentoArchivioFisicoItem::query()->where('stabile_id', $stabileId)->where('codice_univoco', $sourceCode)->first();
$target = DocumentoArchivioFisicoItem::query()->where('stabile_id', $stabileId)->where('codice_univoco', $targetCode)->first();
if (! $source instanceof DocumentoArchivioFisicoItem || ! $target instanceof DocumentoArchivioFisicoItem) {
Notification::make()->title('Codici archivio non trovati')->danger()->send();
return;
}
if ((int) $source->id === (int) $target->id) {
Notification::make()->title('La destinazione deve essere diversa dalla sorgente')->danger()->send();
return;
}
$source->forceFill([
'parent_id' => (int) $target->id,
'updated_by' => Auth::id(),
])->save();
$this->physicalArchiveMoveForm = [
'source_code' => '',
'target_code' => '',
];
Notification::make()
->title('Movimentazione registrata')
->body($source->codice_univoco . ' → ' . $target->codice_univoco)
->success()
->send();
}
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;
}
public function getProtocolloComunicazioniStatsProperty(): array
{
$query = $this->getProtocolloComunicazioniQuery();
return [
'totale' => (clone $query)->count(),
'con_pdf' => (clone $query)
->where(function (Builder $builder): void {
$builder->whereNotNull('percorso_file')
->orWhereNotNull('path_file');
})
->count(),
'questo_mese' => (clone $query)
->whereDate('created_at', '>=', now()->startOfMonth()->toDateString())
->count(),
'categorie' => (clone $query)
->reorder()
->select('tipo_documento')
->distinct()
->count('tipo_documento'),
];
}
public function getProtocolloComunicazioniCategoryOptionsProperty(): array
{
$visibleCategories = (clone $this->getProtocolloComunicazioniQuery())
->reorder()
->select('tipo_documento')
->distinct()
->pluck('tipo_documento')
->map(fn($value): string => trim((string) $value))
->filter()
->values()
->all();
$labels = $this->getCategorieOptions();
return collect($visibleCategories)
->mapWithKeys(fn(string $value): array => [$value => $labels[$value] ?? Str::headline($value)])
->all();
}
public function getProtocolloComunicazioniRowsProperty(): array
{
$limit = $this->protocolloView === 'list' ? 80 : 18;
$labels = $this->getCategorieOptions();
return $this->getProtocolloComunicazioniQuery()
->with('cartellaArchivio')
->limit($limit)
->get()
->map(function (Documento $documento) use ($labels): array {
$openUrl = route('filament.documenti.download', $documento) . '?inline=1';
$tipo = trim((string) ($documento->tipo_documento ?? ''));
return [
'id' => (int) $documento->id,
'protocollo' => trim((string) ($documento->numero_protocollo ?: 'DOC-' . $documento->id)),
'titolo' => trim((string) ($documento->nome ?: $documento->nome_file ?: ('Documento #' . $documento->id))),
'descrizione' => trim((string) ($documento->descrizione ?: Str::limit((string) ($documento->contenuto_ocr ?? ''), 140, '...'))),
'categoria' => $labels[$tipo] ?? ($tipo !== '' ? Str::headline($tipo) : 'Protocollo generale'),
'fornitore' => trim((string) ($documento->fornitore ?? '')),
'data' => $documento->data_documento?->format('d/m/Y')
?: ($documento->created_at?->format('d/m/Y') ?: '—'),
'cartella' => trim((string) ($documento->cartellaArchivio?->nome ?? 'Protocollo generale')),
'archive_reference' => trim((string) ($documento->archive_reference ?? '')),
'has_pdf' => $this->hasPdf($documento),
'preview_url' => $openUrl,
'download_url' => route('filament.documenti.download', $documento),
'print_url' => $openUrl,
];
})
->all();
}
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(),
Select::make('archivio_classe')
->label('Classe archivio condominiale')
->options($this->getArchivioClasseOptions())
->searchable()
->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('supporto_fisico')
->label('Supporto fisico')
->options(array_combine($this->archivioFisicoTypes, $this->archivioFisicoTypes))
->searchable()
->nullable(),
Select::make('modello_etichetta')
->label('Modello etichetta')
->options($this->getModelliEtichettaOptions())
->default('11354')
->native(false)
->nullable(),
TextInput::make('busta_numero')
->label('Busta n.')
->maxLength(30),
TextInput::make('fascicolo_numero')
->label('Fascicolo n.')
->maxLength(30),
TextInput::make('contenitore_numero')
->label('Contenitore / faldone')
->placeholder('Es. 12')
->maxLength(30),
DatePicker::make('periodo_riferimento_da')
->label('Periodo dal'),
DatePicker::make('periodo_riferimento_a')
->label('Periodo al'),
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 ammessi')
->options($this->getCollaboratorUserOptions())
->multiple()
->native(false)
->searchable(),
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 initializePhysicalArchiveForms(): void
{
$this->physicalArchiveForm = [
'data_archiviazione' => now()->toDateString(),
'voce_numero' => null,
'voce_titolo' => '',
'titolo' => '',
'note' => '',
'tipo_item' => 'documento',
'supporto_fisico' => 'Busta trasparente con anelli da archiviare',
'parent_id' => null,
'documento_id' => null,
'magazzino' => '',
'scaffale' => '',
'ripiano' => '',
'ubicazione_dettaglio' => '',
'modello_etichetta' => '11354',
];
$this->physicalArchiveMoveForm = [
'source_code' => '',
'target_code' => '',
];
}
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 syncInvoiceFolders(): void
{
$stabile = $this->resolveActiveStabile();
if (! $stabile instanceof Stabile) {
$this->currentFolderId = null;
return;
}
$root = $this->ensureInvoiceRootFolder((int) $stabile->id);
if (! $root instanceof DocumentoArchivioCartella) {
return;
}
Documento::query()
->where('stabile_id', $stabile->id)
->where('tipo_documento', 'fattura')
->orderBy('data_documento')
->orderBy('id')
->get(['id', 'data_documento', 'cartella_archivio_id'])
->each(function (Documento $documento) use ($stabile, $root): void {
$year = (int) (($documento->data_documento?->format('Y')) ?: now()->format('Y'));
$yearFolder = $this->ensureInvoiceYearFolder((int) $stabile->id, $root, $year);
if (! $yearFolder instanceof DocumentoArchivioCartella) {
return;
}
if ((int) ($documento->cartella_archivio_id ?? 0) !== (int) $yearFolder->id) {
$documento->forceFill([
'cartella_archivio_id' => (int) $yearFolder->id,
])->save();
}
});
if ($this->currentFolderId) {
$exists = DocumentoArchivioCartella::query()
->whereKey($this->currentFolderId)
->where('stabile_id', $stabile->id)
->exists();
if (! $exists) {
$this->currentFolderId = null;
}
}
}
private function ensureInvoiceRootFolder(int $stabileId): ?DocumentoArchivioCartella
{
if ($stabileId <= 0) {
return null;
}
return DocumentoArchivioCartella::query()->firstOrCreate(
[
'stabile_id' => $stabileId,
'parent_id' => null,
'slug' => 'fatture-pdf',
],
[
'nome' => 'fatture_PDF',
'codice' => 'FATTURE',
'percorso_logico' => 'fatture_PDF',
'visibility_scope' => 'interno',
'created_by' => Auth::id(),
'updated_by' => Auth::id(),
]
);
}
private function ensureInvoiceYearFolder(int $stabileId, DocumentoArchivioCartella $root, int $year): ?DocumentoArchivioCartella
{
if ($stabileId <= 0 || $year < 2000) {
return null;
}
return DocumentoArchivioCartella::query()->firstOrCreate(
[
'stabile_id' => $stabileId,
'parent_id' => (int) $root->id,
'slug' => (string) $year,
],
[
'nome' => (string) $year,
'codice' => 'FATT-' . $year,
'percorso_logico' => trim((string) $root->nome) . '/' . $year,
'visibility_scope' => 'interno',
'created_by' => Auth::id(),
'updated_by' => Auth::id(),
]
);
}
private function formatPhysicalArchiveLocation(Documento $record): string
{
$meta = is_array($record->metadati_archivio ?? null) ? $record->metadati_archivio : [];
$parts = array_filter([
$this->normalizeNullableText($meta['supporto_fisico'] ?? null),
$this->normalizeNullableText($meta['contenitore_numero'] ?? null),
$this->normalizeNullableText($meta['busta_numero'] ?? null) ? 'Busta ' . $this->normalizeNullableText($meta['busta_numero'] ?? null) : null,
$this->normalizeNullableText($meta['fascicolo_numero'] ?? null) ? 'Fascicolo ' . $this->normalizeNullableText($meta['fascicolo_numero'] ?? null) : null,
]);
return $parts === [] ? '—' : implode(' · ', $parts);
}
private function normalizeHubTab(string $tab): string
{
$allowed = ['protocollo-comunicazioni', 'archivio-digitale', 'template-drive', 'archivio-fisico', 'permessi'];
return in_array($tab, $allowed, true) ? $tab : 'protocollo-comunicazioni';
}
private function getProtocolloComunicazioniQuery(): Builder
{
$user = Auth::user();
if (! $user instanceof User) {
return Documento::query()->whereRaw('1 = 0');
}
$query = $this->getVisibilityService()
->scopeVisibleDocumenti(Documento::query(), $user)
->where(function (Builder $builder): void {
$builder->whereNull('tipo_documento')
->orWhere('tipo_documento', '!=', 'fattura');
});
$stabileId = (int) ($this->resolveActiveStabile()?->id ?? 0);
if ($stabileId > 0) {
$query->where('stabile_id', $stabileId);
}
$category = trim($this->protocolloCategory);
if ($category !== '') {
$query->where('tipo_documento', $category);
}
$search = trim($this->protocolloSearch);
if ($search !== '') {
$query->where(function (Builder $builder) use ($search): void {
$builder->where('numero_protocollo', 'like', '%' . $search . '%')
->orWhere('nome', 'like', '%' . $search . '%')
->orWhere('nome_file', 'like', '%' . $search . '%')
->orWhere('descrizione', 'like', '%' . $search . '%')
->orWhere('fornitore', 'like', '%' . $search . '%')
->orWhere('archive_reference', 'like', '%' . $search . '%');
});
}
return $query
->orderByDesc('data_documento')
->orderByDesc('created_at')
->orderByDesc('id');
}
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;
}
$resolvedFolderId = $this->resolveDefaultFolderIdForDocument($stabileId, $categoria, $year, $data['cartella_archivio_id'] ?? null);
// Rinomina il file su storage in modo ordinato
$finalDir = 'documenti/stabili/ID-' . $stabileId . '/' . $year;
$finalName = $this->buildDocumentStorageFilename($categoria, $dataDocumento, $protocollo, (string) ($data['fornitore'] ?? ''), $titolo);
$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);
$archivioClasse = trim((string) ($data['archivio_classe'] ?? ''));
$archiveReference = $this->buildArchiveReference($stabileId, $categoria, $year, $data);
$doc = Documento::query()->create([
'documentable_type' => null,
'documentable_id' => null,
'stabile_id' => $stabileId,
'cartella_archivio_id' => $resolvedFolderId,
'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,
'archive_reference' => $archiveReference,
'nome_file' => $finalName,
'mime_type' => 'application/pdf',
'path_file' => $finalPath,
'percorso_file' => $finalPath,
'estensione' => 'pdf',
'dimensione_file' => $size,
'metadati_archivio' => [
'classe' => $archivioClasse !== '' ? $archivioClasse : null,
'classe_label' => $this->getArchivioClasseOptions()[$archivioClasse] ?? null,
'supporto_fisico' => $this->normalizeNullableText($data['supporto_fisico'] ?? null),
'modello_etichetta' => $this->normalizeNullableText($data['modello_etichetta'] ?? null),
'busta_numero' => $this->normalizeNullableText($data['busta_numero'] ?? null),
'fascicolo_numero' => $this->normalizeNullableText($data['fascicolo_numero'] ?? null),
'contenitore_numero' => $this->normalizeNullableText($data['contenitore_numero'] ?? null),
'periodo_da' => $data['periodo_riferimento_da'] ?? null,
'periodo_a' => $data['periodo_riferimento_a'] ?? null,
'codice_etichetta' => $archiveReference,
'qr_ready' => true,
'nfc_ready' => true,
],
'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'] ?? []),
'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 . ' · Archivio: ' . $archiveReference)
->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 = '<h1>' . e($titolo) . '</h1>';
$html .= '<p><strong>Protocollo:</strong> ' . e($protocollo) . '</p>';
$html .= '<p>' . e($descrizione) . '</p>';
$html .= '<hr>';
$html .= '<p><strong>Fornitore:</strong> Acme Elevators Srl</p>';
$html .= '<p><strong>Dal:</strong> 01/01/2024 <strong>Al:</strong> 31/12/2024</p>';
$html .= '<p><strong>Totale annuo:</strong> € 2.400,00</p>';
$html .= '<p><strong>Modalità pagamento:</strong> Trimestrale</p>';
$html .= '<p><strong>Data stipula:</strong> 15/12/2023</p>';
$dompdf = new Dompdf();
$dompdf->loadHtml('<html><body style="font-family: DejaVu Sans, Arial, sans-serif;">' . $html . '</body></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',
'planimetria' => 'PLAN',
'fascicolo' => 'FASC',
'anagrafica' => 'ANAG',
'tabella' => 'TABM',
'regolamento' => 'REGO',
'scritture' => 'CONT',
'chiavi' => 'CHIA',
'timbri' => 'TIMB',
'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 buildArchiveReference(int $stabileId, string $categoria, int $year, array $data): string
{
$stabile = Stabile::query()->find($stabileId);
$mnemonic = $this->mnemonicCodeHint;
if (! $mnemonic && $stabile instanceof Stabile) {
$label = Str::upper((string) ($stabile->denominazione ?? 'STABILE'));
$letters = preg_replace('/[^A-Z]/', '', Str::ascii($label)) ?: 'STB';
$digits = preg_replace('/\D/', '', $label);
$mnemonic = str_pad(substr($letters, 0, 3), 3, 'X') . str_pad(substr($digits !== '' ? $digits : (string) $stabile->id, -5), 5, '0', STR_PAD_LEFT);
}
$contenitore = $this->normalizeNullableText($data['contenitore_numero'] ?? $data['faldone'] ?? null) ?: '00';
$fascicolo = $this->normalizeNullableText($data['fascicolo_numero'] ?? null) ?: '00/' . $year;
return implode(' · ', array_filter([
$mnemonic ?: 'STB00000',
strtoupper($categoria),
(string) $year,
'BUSTA ' . $contenitore,
'FASC ' . $fascicolo,
]));
}
private function buildDocumentStorageFilename(string $categoria, mixed $dataDocumento, string $protocollo, string $fornitore, string $titolo): string
{
$safeDate = '00000000';
try {
if ($dataDocumento instanceof \DateTimeInterface) {
$safeDate = $dataDocumento->format('Ymd');
} elseif (is_string($dataDocumento) && trim($dataDocumento) !== '') {
$safeDate = Carbon::parse(trim($dataDocumento))->format('Ymd');
}
} catch (\Throwable) {
$safeDate = now()->format('Ymd');
}
$sourceLabel = $categoria === 'fattura'
? ($fornitore !== '' ? $fornitore : $titolo)
: $titolo;
$safeLabel = trim((string) Str::of($sourceLabel)->ascii()->replaceMatches('/[^A-Za-z0-9]+/', ' ')->squish());
$safeLabel = str_replace(' ', '_', Str::limit($safeLabel !== '' ? $safeLabel : 'documento', 60, ''));
$safeProtocollo = str_replace(['/', ' '], '-', trim($protocollo));
if ($categoria === 'fattura') {
return $safeDate . '_' . $safeProtocollo . '_' . Str::lower($safeLabel) . '.pdf';
}
return $safeProtocollo . '_' . Str::lower($safeLabel) . '.pdf';
}
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 resolveDefaultFolderIdForDocument(int $stabileId, string $categoria, int $year, mixed $folderId): ?int
{
$resolvedFolderId = $this->resolveFolderIdForDocument($stabileId, $folderId);
if ($resolvedFolderId) {
return $resolvedFolderId;
}
if ($categoria !== 'fattura') {
return null;
}
$root = $this->ensureInvoiceRootFolder($stabileId);
if (! $root instanceof DocumentoArchivioCartella) {
return null;
}
return $this->ensureInvoiceYearFolder($stabileId, $root, $year)?->id;
}
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();
}
private function normalizeNullableText(mixed $value): ?string
{
$value = trim((string) $value);
return $value !== '' ? $value : null;
}
}