fix: refine archive label printing and public qr

This commit is contained in:
michele 2026-04-22 21:54:56 +00:00
parent 8bc7783ce3
commit 49a49cdb08
10 changed files with 475 additions and 247 deletions

View File

@ -40,7 +40,8 @@ ## Struttura documentazione
## Stato operativo recente ## Stato operativo recente
- Strumenti > Documenti apre ora una dashboard `Protocollo comunicazioni` con ricerca, filtri e anteprima PDF. - Strumenti > Documenti apre ora una dashboard `Protocollo comunicazioni` con ricerca, filtri e anteprima PDF.
- Le etichette archivio fisico generano il QR in locale e supportano i formati DYMO `11354` e `99014`. - Le etichette archivio fisico generano il QR in locale, supportano i formati DYMO `11354` e `99014` e usano un URL pubblico configurabile tramite `APP_PUBLIC_URL`.
- La vista `Archivio digitale` usa ora una tabella a tutta larghezza con riepilogo rapido della cartella corrente.
- La topbar contestuale Filament usa layout a capo, senza scrollbar orizzontale. - La topbar contestuale Filament usa layout a capo, senza scrollbar orizzontale.
## Contribuire ## Contribuire

View File

@ -49,7 +49,7 @@ class DocumentiArchivio extends Page implements HasTable
public string $protocolloView = 'cards'; public string $protocolloView = 'cards';
#[Url(as: 'folder')] #[Url( as : 'folder')]
public ?int $currentFolderId = null; public ?int $currentFolderId = null;
public array $physicalArchiveForm = []; public array $physicalArchiveForm = [];
@ -292,15 +292,15 @@ public function table(Table $table): Table
->hiddenLabel() ->hiddenLabel()
->icon('heroicon-o-archive-box') ->icon('heroicon-o-archive-box')
->tooltip('Collega archivio fisico') ->tooltip('Collega archivio fisico')
->fillForm(fn(Documento $record): array => [ ->fillForm(fn(Documento $record): array=> [
'supporto_fisico' => data_get($record->metadati_archivio ?? [], 'supporto_fisico'), 'supporto_fisico' => data_get($record->metadati_archivio ?? [], 'supporto_fisico'),
'modello_etichetta' => data_get($record->metadati_archivio ?? [], 'modello_etichetta', '11354'), 'modello_etichetta' => data_get($record->metadati_archivio ?? [], 'modello_etichetta', '11354'),
'busta_numero' => data_get($record->metadati_archivio ?? [], 'busta_numero'), 'busta_numero' => data_get($record->metadati_archivio ?? [], 'busta_numero'),
'fascicolo_numero' => data_get($record->metadati_archivio ?? [], 'fascicolo_numero'), 'fascicolo_numero' => data_get($record->metadati_archivio ?? [], 'fascicolo_numero'),
'contenitore_numero' => data_get($record->metadati_archivio ?? [], 'contenitore_numero'), 'contenitore_numero' => data_get($record->metadati_archivio ?? [], 'contenitore_numero'),
'periodo_riferimento_da' => data_get($record->metadati_archivio ?? [], 'periodo_da'), 'periodo_riferimento_da' => data_get($record->metadati_archivio ?? [], 'periodo_da'),
'periodo_riferimento_a' => data_get($record->metadati_archivio ?? [], 'periodo_a'), 'periodo_riferimento_a' => data_get($record->metadati_archivio ?? [], 'periodo_a'),
'watermark_predefinito' => (bool) data_get($record->metadati_archivio ?? [], 'watermark_predefinito', false), 'watermark_predefinito' => (bool) data_get($record->metadati_archivio ?? [], 'watermark_predefinito', false),
]) ])
->form([ ->form([
Select::make('supporto_fisico') Select::make('supporto_fisico')
@ -327,16 +327,16 @@ public function table(Table $table): Table
->default(false), ->default(false),
]) ])
->action(function (Documento $record, array $data): void { ->action(function (Documento $record, array $data): void {
$meta = is_array($record->metadati_archivio ?? null) ? $record->metadati_archivio : []; $meta = is_array($record->metadati_archivio ?? null) ? $record->metadati_archivio : [];
$meta['supporto_fisico'] = $this->normalizeNullableText($data['supporto_fisico'] ?? null); $meta['supporto_fisico'] = $this->normalizeNullableText($data['supporto_fisico'] ?? null);
$meta['modello_etichetta'] = $this->normalizeNullableText($data['modello_etichetta'] ?? null); $meta['modello_etichetta'] = $this->normalizeNullableText($data['modello_etichetta'] ?? null);
$meta['contenitore_numero'] = $this->normalizeNullableText($data['contenitore_numero'] ?? null); $meta['contenitore_numero'] = $this->normalizeNullableText($data['contenitore_numero'] ?? null);
$meta['busta_numero'] = $this->normalizeNullableText($data['busta_numero'] ?? null); $meta['busta_numero'] = $this->normalizeNullableText($data['busta_numero'] ?? null);
$meta['fascicolo_numero'] = $this->normalizeNullableText($data['fascicolo_numero'] ?? null); $meta['fascicolo_numero'] = $this->normalizeNullableText($data['fascicolo_numero'] ?? null);
$meta['periodo_da'] = $data['periodo_riferimento_da'] ?? null; $meta['periodo_da'] = $data['periodo_riferimento_da'] ?? null;
$meta['periodo_a'] = $data['periodo_riferimento_a'] ?? null; $meta['periodo_a'] = $data['periodo_riferimento_a'] ?? null;
$meta['watermark_predefinito'] = (bool) ($data['watermark_predefinito'] ?? false); $meta['watermark_predefinito'] = (bool) ($data['watermark_predefinito'] ?? false);
$record->metadati_archivio = $meta; $record->metadati_archivio = $meta;
$record->save(); $record->save();
Notification::make() Notification::make()
@ -434,6 +434,30 @@ public function getDigitalArchiveCurrentFolderProperty(): ?DocumentoArchivioCart
return DocumentoArchivioCartella::query()->find($this->currentFolderId); return DocumentoArchivioCartella::query()->find($this->currentFolderId);
} }
public function getDigitalArchiveCurrentFolderDocumentCountProperty(): int
{
$stabileId = (int) ($this->resolveActiveStabile()?->id ?? 0);
if ($stabileId <= 0 || ! $this->currentFolderId) {
return 0;
}
return (int) Documento::query()
->where('stabile_id', $stabileId)
->where('tipo_documento', 'fattura')
->where('cartella_archivio_id', $this->currentFolderId)
->count();
}
public function getDigitalArchiveCurrentFolderHintProperty(): string
{
$folder = $this->digitalArchiveCurrentFolder;
if (! $folder instanceof DocumentoArchivioCartella) {
return 'Seleziona la cartella annuale per vedere lelenco dei PDF già ordinati per protocollo e data.';
}
return trim((string) ($folder->percorso_logico ?: $folder->nome));
}
public function getDigitalArchiveFoldersProperty(): array public function getDigitalArchiveFoldersProperty(): array
{ {
$stabile = $this->resolveActiveStabile(); $stabile = $this->resolveActiveStabile();
@ -448,10 +472,10 @@ public function getDigitalArchiveFoldersProperty(): array
if (! $this->currentFolderId) { if (! $this->currentFolderId) {
return [[ return [[
'id' => (int) $root->id, 'id' => (int) $root->id,
'nome' => (string) $root->nome, 'nome' => (string) $root->nome,
'descrizione' => 'Cartella fatture PDF', 'descrizione' => 'Cartella fatture PDF',
'conteggio' => (int) Documento::query() 'conteggio' => (int) Documento::query()
->where('stabile_id', $stabile->id) ->where('stabile_id', $stabile->id)
->where('tipo_documento', 'fattura') ->where('tipo_documento', 'fattura')
->count(), ->count(),
@ -464,11 +488,11 @@ public function getDigitalArchiveFoldersProperty(): array
->orderBy('sort_order') ->orderBy('sort_order')
->orderBy('nome') ->orderBy('nome')
->get() ->get()
->map(fn(DocumentoArchivioCartella $folder): array => [ ->map(fn(DocumentoArchivioCartella $folder): array=> [
'id' => (int) $folder->id, 'id' => (int) $folder->id,
'nome' => (string) $folder->nome, 'nome' => (string) $folder->nome,
'descrizione' => (string) ($folder->percorso_logico ?: $folder->nome), 'descrizione' => (string) ($folder->percorso_logico ?: $folder->nome),
'conteggio' => (int) $folder->documenti()->count(), 'conteggio' => (int) $folder->documenti()->count(),
]) ])
->all(); ->all();
} }
@ -476,7 +500,7 @@ public function getDigitalArchiveFoldersProperty(): array
public function getDigitalArchiveBreadcrumbsProperty(): array public function getDigitalArchiveBreadcrumbsProperty(): array
{ {
$items = [[ $items = [[
'label' => $this->digitalArchiveRootLabel, 'label' => $this->digitalArchiveRootLabel,
'folder_id' => null, 'folder_id' => null,
]]; ]];
@ -488,7 +512,7 @@ public function getDigitalArchiveBreadcrumbsProperty(): array
$chain = collect(); $chain = collect();
while ($current instanceof DocumentoArchivioCartella) { while ($current instanceof DocumentoArchivioCartella) {
$chain->prepend([ $chain->prepend([
'label' => (string) $current->nome, 'label' => (string) $current->nome,
'folder_id' => (int) $current->id, 'folder_id' => (int) $current->id,
]); ]);
$current = $current->parent; $current = $current->parent;
@ -546,26 +570,26 @@ public function getArchivioFisicoTypesProperty(): array
public function getArchivioClasseOptions(): array public function getArchivioClasseOptions(): array
{ {
return [ return [
'libro_verbali' => 'Libro verbali', 'libro_verbali' => 'Libro verbali',
'planimetria_stabile' => 'Planimetria stabile', 'planimetria_stabile' => 'Planimetria stabile',
'elaborato_planimetrico' => 'Elaborato planimetrico', 'elaborato_planimetrico' => 'Elaborato planimetrico',
'scheda_anagrafica_condominiale' => 'Scheda anagrafica condominiale', 'scheda_anagrafica_condominiale' => 'Scheda anagrafica condominiale',
'fascicolo_fabbricato' => 'Fascicolo fabbricato', 'fascicolo_fabbricato' => 'Fascicolo fabbricato',
'polizza_fabbricato' => 'Polizza global fabbricato', 'polizza_fabbricato' => 'Polizza global fabbricato',
'certificazioni_impianti' => 'Certificazioni e conformita impianti', 'certificazioni_impianti' => 'Certificazioni e conformita impianti',
'decreti_vincolo' => 'Decreti di vincolo', 'decreti_vincolo' => 'Decreti di vincolo',
'tabelle_millesimali' => 'Tabelle millesimali', 'tabelle_millesimali' => 'Tabelle millesimali',
'regolamento_condominio' => 'Regolamento di condominio', 'regolamento_condominio' => 'Regolamento di condominio',
'rendiconti_approvati' => 'Rendiconti approvati', 'rendiconti_approvati' => 'Rendiconti approvati',
'raccolta_contratti' => 'Raccolta contratti stipulati', 'raccolta_contratti' => 'Raccolta contratti stipulati',
'servitu_diritti_terzi' => 'Servitu e diritti di terzi', 'servitu_diritti_terzi' => 'Servitu e diritti di terzi',
'giustificativi_spesa' => 'Giustificativi di spesa', 'giustificativi_spesa' => 'Giustificativi di spesa',
'scritture_contabili' => 'Scritture contabili e sostituto imposta', 'scritture_contabili' => 'Scritture contabili e sostituto imposta',
'codice_fiscale' => 'Codice fiscale', 'codice_fiscale' => 'Codice fiscale',
'partita_iva_amministratore' => 'Partita IVA amministratore', 'partita_iva_amministratore' => 'Partita IVA amministratore',
'chiavi_stabile' => 'Chiavi stabile', 'chiavi_stabile' => 'Chiavi stabile',
'timbri_stabile' => 'Timbri stabile', 'timbri_stabile' => 'Timbri stabile',
'altro' => 'Altro contenuto archivio', 'altro' => 'Altro contenuto archivio',
]; ];
} }
@ -620,7 +644,7 @@ public function getPhysicalArchiveParentOptionsProperty(): array
->whereIn('tipo_item', ['busta_trasparente', 'cartella_tre_lembi', 'faldone_anelli', 'faldone_lacci', 'scatola']) ->whereIn('tipo_item', ['busta_trasparente', 'cartella_tre_lembi', 'faldone_anelli', 'faldone_lacci', 'scatola'])
->orderBy('codice_univoco') ->orderBy('codice_univoco')
->get(['id', 'codice_univoco', 'titolo']) ->get(['id', 'codice_univoco', 'titolo'])
->mapWithKeys(fn(DocumentoArchivioFisicoItem $item): array => [ ->mapWithKeys(fn(DocumentoArchivioFisicoItem $item): array=> [
(string) $item->id => trim($item->codice_univoco . ' · ' . $item->titolo), (string) $item->id => trim($item->codice_univoco . ' · ' . $item->titolo),
]) ])
->all(); ->all();
@ -682,17 +706,17 @@ public function getPhysicalArchiveRowsProperty(): array
->get() ->get()
->map(function (DocumentoArchivioFisicoItem $item): array { ->map(function (DocumentoArchivioFisicoItem $item): array {
return [ return [
'id' => (int) $item->id, 'id' => (int) $item->id,
'codice_univoco' => (string) $item->codice_univoco, 'codice_univoco' => (string) $item->codice_univoco,
'tipo' => (string) $item->tipo_label, 'tipo' => (string) $item->tipo_label,
'titolo' => (string) $item->titolo, 'titolo' => (string) $item->titolo,
'voce' => trim(collect([$item->voce_numero ? ('Voce ' . $item->voce_numero) : null, $item->voce_titolo])->filter()->implode(' · ')), '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') ?: '—', 'data_archiviazione' => $item->data_archiviazione?->format('d-m-Y') ?: '—',
'parent' => $item->parent ? trim($item->parent->codice_univoco . ' · ' . $item->parent->titolo) : 'Radice', 'parent' => $item->parent ? trim($item->parent->codice_univoco . ' · ' . $item->parent->titolo) : 'Radice',
'percorso' => (string) $item->percorso_fisico, 'percorso' => (string) $item->percorso_fisico,
'documento' => $item->documento ? trim((string) ($item->documento->nome_file ?: $item->documento->nome ?: ('Documento #' . $item->documento->id))) : null, '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(' · '), 'ubicazione' => collect([$item->magazzino, $item->scaffale, $item->ripiano, $item->ubicazione_dettaglio])->filter()->implode(' · '),
'modello_etichetta' => (string) ($item->modello_etichetta ?: '11354'), 'modello_etichetta' => (string) ($item->modello_etichetta ?: '11354'),
]; ];
}) })
->all(); ->all();
@ -712,10 +736,10 @@ public function getPhysicalArchiveContainerTreeProperty(): array
->get() ->get()
->map(function (DocumentoArchivioFisicoItem $item): array { ->map(function (DocumentoArchivioFisicoItem $item): array {
return [ return [
'id' => (int) $item->id, 'id' => (int) $item->id,
'codice_univoco' => (string) $item->codice_univoco, 'codice_univoco' => (string) $item->codice_univoco,
'titolo' => (string) $item->titolo, 'titolo' => (string) $item->titolo,
'tipo' => (string) $item->tipo_label, 'tipo' => (string) $item->tipo_label,
'children_count' => (int) $item->children()->count(), 'children_count' => (int) $item->children()->count(),
]; ];
}) })
@ -736,7 +760,7 @@ public function savePhysicalArchiveEntry(): void
return; return;
} }
$titolo = trim((string) ($this->physicalArchiveForm['titolo'] ?? '')); $titolo = trim((string) ($this->physicalArchiveForm['titolo'] ?? ''));
$tipoItem = trim((string) ($this->physicalArchiveForm['tipo_item'] ?? 'documento')); $tipoItem = trim((string) ($this->physicalArchiveForm['tipo_item'] ?? 'documento'));
if ($titolo === '' || ! array_key_exists($tipoItem, DocumentoArchivioFisicoItem::TIPI)) { if ($titolo === '' || ! array_key_exists($tipoItem, DocumentoArchivioFisicoItem::TIPI)) {
Notification::make()->title('Titolo e tipologia sono obbligatori')->danger()->send(); Notification::make()->title('Titolo e tipologia sono obbligatori')->danger()->send();
@ -744,7 +768,7 @@ public function savePhysicalArchiveEntry(): void
} }
$parentId = is_numeric($this->physicalArchiveForm['parent_id'] ?? null) ? (int) $this->physicalArchiveForm['parent_id'] : null; $parentId = is_numeric($this->physicalArchiveForm['parent_id'] ?? null) ? (int) $this->physicalArchiveForm['parent_id'] : null;
$parent = $parentId $parent = $parentId
? DocumentoArchivioFisicoItem::query()->where('stabile_id', $stabileId)->find($parentId) ? DocumentoArchivioFisicoItem::query()->where('stabile_id', $stabileId)->find($parentId)
: null; : null;
@ -756,34 +780,34 @@ public function savePhysicalArchiveEntry(): void
} }
$item = DocumentoArchivioFisicoItem::query()->create([ $item = DocumentoArchivioFisicoItem::query()->create([
'stabile_id' => $stabileId, 'stabile_id' => $stabileId,
'parent_id' => $parent?->id, 'parent_id' => $parent?->id,
'documento_id' => is_numeric($this->physicalArchiveForm['documento_id'] ?? null) ? (int) $this->physicalArchiveForm['documento_id'] : null, 'documento_id' => is_numeric($this->physicalArchiveForm['documento_id'] ?? null) ? (int) $this->physicalArchiveForm['documento_id'] : null,
'voce_numero' => $voceNumero, 'voce_numero' => $voceNumero,
'voce_titolo' => $voceTitolo !== '' ? $voceTitolo : null, 'voce_titolo' => $voceTitolo !== '' ? $voceTitolo : null,
'tipo_item' => $tipoItem, 'tipo_item' => $tipoItem,
'titolo' => $titolo, 'titolo' => $titolo,
'note' => $this->normalizeNullableText($this->physicalArchiveForm['note'] ?? null), 'note' => $this->normalizeNullableText($this->physicalArchiveForm['note'] ?? null),
'data_archiviazione' => $this->physicalArchiveForm['data_archiviazione'] ?? now()->toDateString(), 'data_archiviazione' => $this->physicalArchiveForm['data_archiviazione'] ?? now()->toDateString(),
'supporto_fisico' => $this->normalizeNullableText($this->physicalArchiveForm['supporto_fisico'] ?? null), 'supporto_fisico' => $this->normalizeNullableText($this->physicalArchiveForm['supporto_fisico'] ?? null),
'modello_etichetta' => $this->normalizeNullableText($this->physicalArchiveForm['modello_etichetta'] ?? null) ?: '11354', 'modello_etichetta' => $this->normalizeNullableText($this->physicalArchiveForm['modello_etichetta'] ?? null) ?: '11354',
'magazzino' => $this->normalizeNullableText($this->physicalArchiveForm['magazzino'] ?? null), 'magazzino' => $this->normalizeNullableText($this->physicalArchiveForm['magazzino'] ?? null),
'scaffale' => $this->normalizeNullableText($this->physicalArchiveForm['scaffale'] ?? null), 'scaffale' => $this->normalizeNullableText($this->physicalArchiveForm['scaffale'] ?? null),
'ripiano' => $this->normalizeNullableText($this->physicalArchiveForm['ripiano'] ?? null), 'ripiano' => $this->normalizeNullableText($this->physicalArchiveForm['ripiano'] ?? null),
'ubicazione_dettaglio' => $this->normalizeNullableText($this->physicalArchiveForm['ubicazione_dettaglio'] ?? null), 'ubicazione_dettaglio' => $this->normalizeNullableText($this->physicalArchiveForm['ubicazione_dettaglio'] ?? null),
'created_by' => (int) $user->id, 'created_by' => (int) $user->id,
'updated_by' => (int) $user->id, 'updated_by' => (int) $user->id,
'metadati' => [ 'metadati' => [
'origine_digitale' => is_numeric($this->physicalArchiveForm['documento_id'] ?? null), 'origine_digitale' => is_numeric($this->physicalArchiveForm['documento_id'] ?? null),
], ],
]); ]);
if ($item->documento instanceof Documento) { if ($item->documento instanceof Documento) {
$meta = is_array($item->documento->metadati_archivio ?? null) ? $item->documento->metadati_archivio : []; $meta = is_array($item->documento->metadati_archivio ?? null) ? $item->documento->metadati_archivio : [];
$meta['physical_archive_item_id'] = (int) $item->id; $meta['physical_archive_item_id'] = (int) $item->id;
$meta['physical_archive_code'] = (string) $item->codice_univoco; $meta['physical_archive_code'] = (string) $item->codice_univoco;
$meta['contenitore_numero'] = $parent?->codice_univoco; $meta['contenitore_numero'] = $parent?->codice_univoco;
$meta['supporto_fisico'] = $item->supporto_fisico; $meta['supporto_fisico'] = $item->supporto_fisico;
$item->documento->forceFill([ $item->documento->forceFill([
'metadati_archivio' => $meta, 'metadati_archivio' => $meta,
])->save(); ])->save();
@ -827,7 +851,7 @@ public function movePhysicalArchiveItem(): void
} }
$source->forceFill([ $source->forceFill([
'parent_id' => (int) $target->id, 'parent_id' => (int) $target->id,
'updated_by' => Auth::id(), 'updated_by' => Auth::id(),
])->save(); ])->save();
@ -902,8 +926,8 @@ public function getProtocolloComunicazioniStatsProperty(): array
$query = $this->getProtocolloComunicazioniQuery(); $query = $this->getProtocolloComunicazioniQuery();
return [ return [
'totale' => (clone $query)->count(), 'totale' => (clone $query)->count(),
'con_pdf' => (clone $query) 'con_pdf' => (clone $query)
->where(function (Builder $builder): void { ->where(function (Builder $builder): void {
$builder->whereNotNull('percorso_file') $builder->whereNotNull('percorso_file')
->orWhereNotNull('path_file'); ->orWhereNotNull('path_file');
@ -912,7 +936,7 @@ public function getProtocolloComunicazioniStatsProperty(): array
'questo_mese' => (clone $query) 'questo_mese' => (clone $query)
->whereDate('created_at', '>=', now()->startOfMonth()->toDateString()) ->whereDate('created_at', '>=', now()->startOfMonth()->toDateString())
->count(), ->count(),
'categorie' => (clone $query) 'categorie' => (clone $query)
->reorder() ->reorder()
->select('tipo_documento') ->select('tipo_documento')
->distinct() ->distinct()
@ -935,13 +959,13 @@ public function getProtocolloComunicazioniCategoryOptionsProperty(): array
$labels = $this->getCategorieOptions(); $labels = $this->getCategorieOptions();
return collect($visibleCategories) return collect($visibleCategories)
->mapWithKeys(fn(string $value): array => [$value => $labels[$value] ?? Str::headline($value)]) ->mapWithKeys(fn(string $value): array=> [$value => $labels[$value] ?? Str::headline($value)])
->all(); ->all();
} }
public function getProtocolloComunicazioniRowsProperty(): array public function getProtocolloComunicazioniRowsProperty(): array
{ {
$limit = $this->protocolloView === 'list' ? 80 : 18; $limit = $this->protocolloView === 'list' ? 80 : 18;
$labels = $this->getCategorieOptions(); $labels = $this->getCategorieOptions();
return $this->getProtocolloComunicazioniQuery() return $this->getProtocolloComunicazioniQuery()
@ -950,23 +974,23 @@ public function getProtocolloComunicazioniRowsProperty(): array
->get() ->get()
->map(function (Documento $documento) use ($labels): array { ->map(function (Documento $documento) use ($labels): array {
$openUrl = route('filament.documenti.download', $documento) . '?inline=1'; $openUrl = route('filament.documenti.download', $documento) . '?inline=1';
$tipo = trim((string) ($documento->tipo_documento ?? '')); $tipo = trim((string) ($documento->tipo_documento ?? ''));
return [ return [
'id' => (int) $documento->id, 'id' => (int) $documento->id,
'protocollo' => trim((string) ($documento->numero_protocollo ?: 'DOC-' . $documento->id)), 'protocollo' => trim((string) ($documento->numero_protocollo ?: 'DOC-' . $documento->id)),
'titolo' => trim((string) ($documento->nome ?: $documento->nome_file ?: ('Documento #' . $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, '...'))), 'descrizione' => trim((string) ($documento->descrizione ?: Str::limit((string) ($documento->contenuto_ocr ?? ''), 140, '...'))),
'categoria' => $labels[$tipo] ?? ($tipo !== '' ? Str::headline($tipo) : 'Protocollo generale'), 'categoria' => $labels[$tipo] ?? ($tipo !== '' ? Str::headline($tipo) : 'Protocollo generale'),
'fornitore' => trim((string) ($documento->fornitore ?? '')), 'fornitore' => trim((string) ($documento->fornitore ?? '')),
'data' => $documento->data_documento?->format('d/m/Y') 'data' => $documento->data_documento?->format('d/m/Y')
?: ($documento->created_at?->format('d/m/Y') ?: '—'), ?: ($documento->created_at?->format('d/m/Y') ?: '—'),
'cartella' => trim((string) ($documento->cartellaArchivio?->nome ?? 'Protocollo generale')), 'cartella' => trim((string) ($documento->cartellaArchivio?->nome ?? 'Protocollo generale')),
'archive_reference' => trim((string) ($documento->archive_reference ?? '')), 'archive_reference' => trim((string) ($documento->archive_reference ?? '')),
'has_pdf' => $this->hasPdf($documento), 'has_pdf' => $this->hasPdf($documento),
'preview_url' => $openUrl, 'preview_url' => $openUrl,
'download_url' => route('filament.documenti.download', $documento), 'download_url' => route('filament.documenti.download', $documento),
'print_url' => $openUrl, 'print_url' => $openUrl,
]; ];
}) })
->all(); ->all();
@ -1160,20 +1184,20 @@ private function getStabiliOptions(): array
private function initializePhysicalArchiveForms(): void private function initializePhysicalArchiveForms(): void
{ {
$this->physicalArchiveForm = [ $this->physicalArchiveForm = [
'data_archiviazione' => now()->toDateString(), 'data_archiviazione' => now()->toDateString(),
'voce_numero' => null, 'voce_numero' => null,
'voce_titolo' => '', 'voce_titolo' => '',
'titolo' => '', 'titolo' => '',
'note' => '', 'note' => '',
'tipo_item' => 'documento', 'tipo_item' => 'documento',
'supporto_fisico' => 'Busta trasparente con anelli da archiviare', 'supporto_fisico' => 'Busta trasparente con anelli da archiviare',
'parent_id' => null, 'parent_id' => null,
'documento_id' => null, 'documento_id' => null,
'magazzino' => '', 'magazzino' => '',
'scaffale' => '', 'scaffale' => '',
'ripiano' => '', 'ripiano' => '',
'ubicazione_dettaglio' => '', 'ubicazione_dettaglio' => '',
'modello_etichetta' => '11354', 'modello_etichetta' => '11354',
]; ];
$this->physicalArchiveMoveForm = [ $this->physicalArchiveMoveForm = [
@ -1279,7 +1303,7 @@ private function syncInvoiceFolders(): void
->orderBy('id') ->orderBy('id')
->get(['id', 'data_documento', 'cartella_archivio_id']) ->get(['id', 'data_documento', 'cartella_archivio_id'])
->each(function (Documento $documento) use ($stabile, $root): void { ->each(function (Documento $documento) use ($stabile, $root): void {
$year = (int) (($documento->data_documento?->format('Y')) ?: now()->format('Y')); $year = (int) (($documento->data_documento?->format('Y')) ?: now()->format('Y'));
$yearFolder = $this->ensureInvoiceYearFolder((int) $stabile->id, $root, $year); $yearFolder = $this->ensureInvoiceYearFolder((int) $stabile->id, $root, $year);
if (! $yearFolder instanceof DocumentoArchivioCartella) { if (! $yearFolder instanceof DocumentoArchivioCartella) {
return; return;
@ -1313,16 +1337,16 @@ private function ensureInvoiceRootFolder(int $stabileId): ?DocumentoArchivioCart
return DocumentoArchivioCartella::query()->firstOrCreate( return DocumentoArchivioCartella::query()->firstOrCreate(
[ [
'stabile_id' => $stabileId, 'stabile_id' => $stabileId,
'parent_id' => null, 'parent_id' => null,
'slug' => 'fatture-pdf', 'slug' => 'fatture-pdf',
], ],
[ [
'nome' => 'fatture_PDF', 'nome' => 'fatture_PDF',
'codice' => 'FATTURE', 'codice' => 'FATTURE',
'percorso_logico' => 'fatture_PDF', 'percorso_logico' => 'fatture_PDF',
'visibility_scope' => 'interno', 'visibility_scope' => 'interno',
'created_by' => Auth::id(), 'created_by' => Auth::id(),
'updated_by' => Auth::id(), 'updated_by' => Auth::id(),
] ]
); );
} }
@ -1336,16 +1360,16 @@ private function ensureInvoiceYearFolder(int $stabileId, DocumentoArchivioCartel
return DocumentoArchivioCartella::query()->firstOrCreate( return DocumentoArchivioCartella::query()->firstOrCreate(
[ [
'stabile_id' => $stabileId, 'stabile_id' => $stabileId,
'parent_id' => (int) $root->id, 'parent_id' => (int) $root->id,
'slug' => (string) $year, 'slug' => (string) $year,
], ],
[ [
'nome' => (string) $year, 'nome' => (string) $year,
'codice' => 'FATT-' . $year, 'codice' => 'FATT-' . $year,
'percorso_logico' => trim((string) $root->nome) . '/' . $year, 'percorso_logico' => trim((string) $root->nome) . '/' . $year,
'visibility_scope' => 'interno', 'visibility_scope' => 'interno',
'created_by' => Auth::id(), 'created_by' => Auth::id(),
'updated_by' => Auth::id(), 'updated_by' => Auth::id(),
] ]
); );
} }
@ -1604,8 +1628,8 @@ private function createDocumentoFromForm(array $data, bool $isDemo): void
$note = 'Faldone: ' . $faldone; $note = 'Faldone: ' . $faldone;
} }
$tipologia = $this->mapCategoriaToTipologia($categoria); $tipologia = $this->mapCategoriaToTipologia($categoria);
$archivioClasse = trim((string) ($data['archivio_classe'] ?? '')); $archivioClasse = trim((string) ($data['archivio_classe'] ?? ''));
$archiveReference = $this->buildArchiveReference($stabileId, $categoria, $year, $data); $archiveReference = $this->buildArchiveReference($stabileId, $categoria, $year, $data);
$doc = Documento::query()->create([ $doc = Documento::query()->create([
@ -1632,18 +1656,18 @@ private function createDocumentoFromForm(array $data, bool $isDemo): void
'estensione' => 'pdf', 'estensione' => 'pdf',
'dimensione_file' => $size, 'dimensione_file' => $size,
'metadati_archivio' => [ 'metadati_archivio' => [
'classe' => $archivioClasse !== '' ? $archivioClasse : null, 'classe' => $archivioClasse !== '' ? $archivioClasse : null,
'classe_label' => $this->getArchivioClasseOptions()[$archivioClasse] ?? null, 'classe_label' => $this->getArchivioClasseOptions()[$archivioClasse] ?? null,
'supporto_fisico' => $this->normalizeNullableText($data['supporto_fisico'] ?? null), 'supporto_fisico' => $this->normalizeNullableText($data['supporto_fisico'] ?? null),
'modello_etichetta' => $this->normalizeNullableText($data['modello_etichetta'] ?? null), 'modello_etichetta' => $this->normalizeNullableText($data['modello_etichetta'] ?? null),
'busta_numero' => $this->normalizeNullableText($data['busta_numero'] ?? null), 'busta_numero' => $this->normalizeNullableText($data['busta_numero'] ?? null),
'fascicolo_numero' => $this->normalizeNullableText($data['fascicolo_numero'] ?? null), 'fascicolo_numero' => $this->normalizeNullableText($data['fascicolo_numero'] ?? null),
'contenitore_numero' => $this->normalizeNullableText($data['contenitore_numero'] ?? null), 'contenitore_numero' => $this->normalizeNullableText($data['contenitore_numero'] ?? null),
'periodo_da' => $data['periodo_riferimento_da'] ?? null, 'periodo_da' => $data['periodo_riferimento_da'] ?? null,
'periodo_a' => $data['periodo_riferimento_a'] ?? null, 'periodo_a' => $data['periodo_riferimento_a'] ?? null,
'codice_etichetta' => $archiveReference, 'codice_etichetta' => $archiveReference,
'qr_ready' => true, 'qr_ready' => true,
'nfc_ready' => true, 'nfc_ready' => true,
], ],
'visibility_scope' => (string) ($data['visibility_scope'] ?? 'interno'), 'visibility_scope' => (string) ($data['visibility_scope'] ?? 'interno'),
'visibility_roles' => $this->normalizeStringArray($data['visibility_roles'] ?? []), 'visibility_roles' => $this->normalizeStringArray($data['visibility_roles'] ?? []),
@ -1807,18 +1831,18 @@ private function generateProtocollo(string $categoria, int $year): string
private function buildArchiveReference(int $stabileId, string $categoria, int $year, array $data): string private function buildArchiveReference(int $stabileId, string $categoria, int $year, array $data): string
{ {
$stabile = Stabile::query()->find($stabileId); $stabile = Stabile::query()->find($stabileId);
$mnemonic = $this->mnemonicCodeHint; $mnemonic = $this->mnemonicCodeHint;
if (! $mnemonic && $stabile instanceof Stabile) { if (! $mnemonic && $stabile instanceof Stabile) {
$label = Str::upper((string) ($stabile->denominazione ?? 'STABILE')); $label = Str::upper((string) ($stabile->denominazione ?? 'STABILE'));
$letters = preg_replace('/[^A-Z]/', '', Str::ascii($label)) ?: 'STB'; $letters = preg_replace('/[^A-Z]/', '', Str::ascii($label)) ?: 'STB';
$digits = preg_replace('/\D/', '', $label); $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); $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'; $contenitore = $this->normalizeNullableText($data['contenitore_numero'] ?? $data['faldone'] ?? null) ?: '00';
$fascicolo = $this->normalizeNullableText($data['fascicolo_numero'] ?? null) ?: '00/' . $year; $fascicolo = $this->normalizeNullableText($data['fascicolo_numero'] ?? null) ?: '00/' . $year;
return implode(' · ', array_filter([ return implode(' · ', array_filter([
$mnemonic ?: 'STB00000', $mnemonic ?: 'STB00000',
@ -1846,8 +1870,8 @@ private function buildDocumentStorageFilename(string $categoria, mixed $dataDocu
? ($fornitore !== '' ? $fornitore : $titolo) ? ($fornitore !== '' ? $fornitore : $titolo)
: $titolo; : $titolo;
$safeLabel = trim((string) Str::of($sourceLabel)->ascii()->replaceMatches('/[^A-Za-z0-9]+/', ' ')->squish()); $safeLabel = trim((string) Str::of($sourceLabel)->ascii()->replaceMatches('/[^A-Za-z0-9]+/', ' ')->squish());
$safeLabel = str_replace(' ', '_', Str::limit($safeLabel !== '' ? $safeLabel : 'documento', 60, '')); $safeLabel = str_replace(' ', '_', Str::limit($safeLabel !== '' ? $safeLabel : 'documento', 60, ''));
$safeProtocollo = str_replace(['/', ' '], '-', trim($protocollo)); $safeProtocollo = str_replace(['/', ' '], '-', trim($protocollo));
if ($categoria === 'fattura') { if ($categoria === 'fattura') {

View File

@ -164,9 +164,19 @@ public function etichettaArchivioFisico(Request $request, DocumentoArchivioFisic
abort(403); abort(403);
} }
$format = $request->query('format', '11354'); $format = $request->query('format', '11354');
$format = in_array($format, ['11354', '99014'], true) ? $format : '11354'; $format = in_array($format, ['11354', '99014'], true) ? $format : '11354';
$defaults = $this->getDymoDefaultsForUser($format); $labelPrinterMode = $request->boolean('autoprint')
|| $request->has('dymo_fix')
|| $request->has('dymo_rot')
|| $request->has('dymo_dx')
|| $request->has('dymo_dy');
$defaults = $labelPrinterMode ? $this->getDymoDefaultsForUser($format) : [
'fix' => false,
'rot' => 0,
'dx' => 0.0,
'dy' => 0.0,
];
$dymoFix = $request->query('dymo_fix'); $dymoFix = $request->query('dymo_fix');
if ($dymoFix === null) { if ($dymoFix === null) {
@ -181,24 +191,38 @@ public function etichettaArchivioFisico(Request $request, DocumentoArchivioFisic
$dymoDx = $request->has('dymo_dx') ? (float) $request->query('dymo_dx') : (float) ($defaults['dx'] ?? 0); $dymoDx = $request->has('dymo_dx') ? (float) $request->query('dymo_dx') : (float) ($defaults['dx'] ?? 0);
$dymoDy = $request->has('dymo_dy') ? (float) $request->query('dymo_dy') : (float) ($defaults['dy'] ?? 0); $dymoDy = $request->has('dymo_dy') ? (float) $request->query('dymo_dy') : (float) ($defaults['dy'] ?? 0);
if ($format === '11354' && $dymoFix) {
if (! in_array($dymoRot, [-90, 90], true)) {
$dymoRot = -90;
}
} else {
if (! in_array($dymoRot, [-180, -90, 0, 90, 180], true)) {
$dymoRot = 0;
}
}
$dymoDx = max(-15.0, min(15.0, $dymoDx));
$dymoDy = max(-15.0, min(15.0, $dymoDy));
return view('filament.print.etichetta-archivio-fisico', [ return view('filament.print.etichetta-archivio-fisico', [
'item' => $item->load(['parent', 'children', 'stabile', 'documento']), 'item' => $item->load(['parent', 'children', 'stabile', 'documento']),
'format' => $format, 'format' => $format,
'dymoFix' => $dymoFix, 'labelPrinterMode' => $labelPrinterMode,
'dymoRot' => $dymoRot, 'dymoFix' => $dymoFix,
'dymoDx' => $dymoDx, 'dymoRot' => $dymoRot,
'dymoDy' => $dymoDy, 'dymoDx' => $dymoDx,
'publicUrl' => $item->qr_code_data, 'dymoDy' => $dymoDy,
'publicUrl' => $item->buildPublicQrUrl(),
]); ]);
} }
public function showPublicArchivioFisicoItem(Request $request, DocumentoArchivioFisicoItem $item) public function showPublicArchivioFisicoItem(Request $request, DocumentoArchivioFisicoItem $item)
{ {
if (! $request->hasValidSignature()) { if (! $request->hasValidSignature(false)) {
abort(403); abort(403);
} }
$chain = collect(); $chain = collect();
$current = $item; $current = $item;
while ($current instanceof DocumentoArchivioFisicoItem) { while ($current instanceof DocumentoArchivioFisicoItem) {
$chain->prepend($current); $chain->prepend($current);
@ -206,7 +230,7 @@ public function showPublicArchivioFisicoItem(Request $request, DocumentoArchivio
} }
return view('filament.pages.strumenti.archivio-fisico-show', [ return view('filament.pages.strumenti.archivio-fisico-show', [
'item' => $item->load(['children.children', 'stabile', 'documento', 'parent']), 'item' => $item->load(['children.children', 'stabile', 'documento', 'parent']),
'chain' => $chain, 'chain' => $chain,
]); ]);
} }
@ -301,7 +325,7 @@ private function buildWatermarkedPdf(string $inputPath, Documento $documento): ?
@unlink($outputPath); @unlink($outputPath);
$postScript = '<< /BeginPage { gsave 0.90 setgray /Helvetica-Bold findfont 14 scalefont setfont 140 90 translate 38 rotate 0 0 moveto (' $postScript = '<< /BeginPage { gsave 0.90 setgray /Helvetica-Bold findfont 14 scalefont setfont 140 90 translate 38 rotate 0 0 moveto ('
. $this->escapePostScriptString($watermarkText) . $this->escapePostScriptString($watermarkText)
. ') show grestore } >> setpagedevice'; . ') show grestore } >> setpagedevice';
$process = new Process([ $process = new Process([
@ -332,7 +356,7 @@ private function buildWatermarkedPdf(string $inputPath, Documento $documento): ?
private function buildWatermarkText(Documento $documento): string private function buildWatermarkText(Documento $documento): string
{ {
$archiveReference = trim((string) ($documento->archive_reference ?? '')); $archiveReference = trim((string) ($documento->archive_reference ?? ''));
$meta = is_array($documento->metadati_archivio ?? null) ? $documento->metadati_archivio : []; $meta = is_array($documento->metadati_archivio ?? null) ? $documento->metadati_archivio : [];
$physicalParts = array_filter([ $physicalParts = array_filter([
trim((string) ($meta['supporto_fisico'] ?? '')), trim((string) ($meta['supporto_fisico'] ?? '')),

View File

@ -1,5 +1,4 @@
<?php <?php
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
@ -42,19 +41,19 @@ class DocumentoArchivioFisicoItem extends Model
protected $casts = [ protected $casts = [
'data_archiviazione' => 'date', 'data_archiviazione' => 'date',
'sort_order' => 'integer', 'sort_order' => 'integer',
'metadati' => 'array', 'metadati' => 'array',
'created_at' => 'datetime', 'created_at' => 'datetime',
'updated_at' => 'datetime', 'updated_at' => 'datetime',
]; ];
public const TIPI = [ public const TIPI = [
'documento' => 'Documento cartaceo', 'documento' => 'Documento cartaceo',
'busta_trasparente' => 'Busta trasparente con anelli da archiviare', 'busta_trasparente' => 'Busta trasparente con anelli da archiviare',
'cartella_tre_lembi' => 'Cartella a tre lembi', 'cartella_tre_lembi' => 'Cartella a tre lembi',
'faldone_anelli' => 'Faldone con anelli', 'faldone_anelli' => 'Faldone con anelli',
'faldone_lacci' => 'Faldone con lacci', 'faldone_lacci' => 'Faldone con lacci',
'scatola' => 'Scatola', 'scatola' => 'Scatola',
]; ];
public function stabile(): BelongsTo public function stabile(): BelongsTo
@ -94,7 +93,7 @@ public function getTipoLabelAttribute(): string
public function getPercorsoFisicoAttribute(): string public function getPercorsoFisicoAttribute(): string
{ {
$parts = []; $parts = [];
$current = $this; $current = $this;
while ($current instanceof self) { while ($current instanceof self) {
@ -107,15 +106,15 @@ public function getPercorsoFisicoAttribute(): string
public function getQrCodeImageAttribute(): string public function getQrCodeImageAttribute(): string
{ {
$data = trim((string) $this->qr_code_data); $data = $this->buildPublicQrUrl();
if ($data !== '' && class_exists(\chillerlan\QRCode\QROptions::class) && class_exists(\chillerlan\QRCode\QRCode::class)) { if ($data !== '' && class_exists(\chillerlan\QRCode\QROptions::class) && class_exists(\chillerlan\QRCode\QRCode::class)) {
$options = new \chillerlan\QRCode\QROptions([ $options = new \chillerlan\QRCode\QROptions([
'outputType' => \chillerlan\QRCode\QRCode::OUTPUT_MARKUP_SVG, 'outputType' => \chillerlan\QRCode\QRCode::OUTPUT_MARKUP_SVG,
'eccLevel' => \chillerlan\QRCode\QRCode::ECC_M, 'eccLevel' => \chillerlan\QRCode\QRCode::ECC_M,
'addQuietzone' => true, 'addQuietzone' => true,
'quietzoneSize' => 1, 'quietzoneSize' => 1,
'connectPaths' => true, 'connectPaths' => true,
]); ]);
$svg = (new \chillerlan\QRCode\QRCode($options))->render($data); $svg = (new \chillerlan\QRCode\QRCode($options))->render($data);
@ -142,11 +141,11 @@ public function getQrCodeImageAttribute(): string
public static function generateCodice(int $stabileId): string public static function generateCodice(int $stabileId): string
{ {
$prefix = 'AF-' . $stabileId . '-'; $prefix = 'AF-' . $stabileId . '-';
$sequence = 1; $sequence = 1;
do { do {
$code = $prefix . str_pad((string) $sequence, 4, '0', STR_PAD_LEFT); $code = $prefix . str_pad((string) $sequence, 4, '0', STR_PAD_LEFT);
$exists = self::query()->where('codice_univoco', $code)->exists(); $exists = self::query()->where('codice_univoco', $code)->exists();
$sequence++; $sequence++;
} while ($exists); } while ($exists);
@ -156,15 +155,26 @@ public static function generateCodice(int $stabileId): string
public function refreshQrCodeData(): void public function refreshQrCodeData(): void
{ {
$url = URL::temporarySignedRoute( $this->forceFill([
'qr_code_data' => $this->buildPublicQrUrl(),
])->saveQuietly();
}
public function buildPublicQrUrl(): string
{
$relativeSignedPath = URL::temporarySignedRoute(
'public.archivio-fisico.show', 'public.archivio-fisico.show',
now()->addYears(5), now()->addYears(5),
['item' => $this->id] ['item' => $this->id],
false,
); );
$this->forceFill([ $baseUrl = rtrim((string) config('app.public_url', config('app.url')), '/');
'qr_code_data' => $url, if ($baseUrl === '') {
])->saveQuietly(); return $relativeSignedPath;
}
return $baseUrl . '/' . ltrim($relativeSignedPath, '/');
} }
protected static function booted(): void protected static function booted(): void

View File

@ -60,7 +60,7 @@ public function panel(Panel $panel): Panel
$user = Auth::user(); $user = Auth::user();
return $user instanceof User return $user instanceof User
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
}), }),
NavigationItem::make('Cellulare / WebApp') NavigationItem::make('Cellulare / WebApp')
->group('NetGescon') ->group('NetGescon')
@ -70,7 +70,7 @@ public function panel(Panel $panel): Panel
$user = Auth::user(); $user = Auth::user();
return $user instanceof User return $user instanceof User
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
}), }),
NavigationItem::make('Ticket Mobile') NavigationItem::make('Ticket Mobile')
->group('NetGescon') ->group('NetGescon')
@ -80,7 +80,7 @@ public function panel(Panel $panel): Panel
$user = Auth::user(); $user = Auth::user();
return $user instanceof User return $user instanceof User
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
}), }),
NavigationItem::make('Scheda Stabile') NavigationItem::make('Scheda Stabile')
->group('NetGescon') ->group('NetGescon')
@ -90,7 +90,7 @@ public function panel(Panel $panel): Panel
$user = Auth::user(); $user = Auth::user();
return $user instanceof User return $user instanceof User
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
}), }),
NavigationItem::make('Catalogo Fornitore') NavigationItem::make('Catalogo Fornitore')
->group('Fornitore') ->group('Fornitore')
@ -102,8 +102,8 @@ public function panel(Panel $panel): Panel
return $user instanceof User && $user->hasRole('fornitore'); return $user instanceof User && $user->hasRole('fornitore');
}), }),
]) ])
->discoverResources(in: app_path('Filament/Resources'), for : 'App\\Filament\\Resources') ->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
->discoverPages(in: app_path('Filament/Pages'), for : 'App\\Filament\\Pages') ->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
->pages([ ->pages([
Dashboard::class, Dashboard::class,
SupportoDashboard::class, SupportoDashboard::class,
@ -111,7 +111,7 @@ public function panel(Panel $panel): Panel
PrimaNotaModifica::class, PrimaNotaModifica::class,
ContoMastrino::class, ContoMastrino::class,
]) ])
->discoverWidgets(in: app_path('Filament/Widgets'), for : 'App\\Filament\\Widgets') ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
->widgets([ ->widgets([
AccountWidget::class, AccountWidget::class,
TicketMobileOverview::class, TicketMobileOverview::class,
@ -171,4 +171,4 @@ protected function resolveSupplierCatalogUrl(): string
return TicketOperativi::getUrl(panel: 'admin-filament'); return TicketOperativi::getUrl(panel: 'admin-filament');
} }
} }

View File

@ -37,6 +37,8 @@
'url' => env('APP_URL', 'http://localhost'), 'url' => env('APP_URL', 'http://localhost'),
'public_url' => env('APP_PUBLIC_URL', env('APP_URL', 'http://localhost')),
'asset_url' => env('ASSET_URL'), 'asset_url' => env('ASSET_URL'),
/* /*

View File

@ -53,6 +53,12 @@ ## Archivio fisico ed etichette
- stampa etichette nei formati `DYMO 11354` e `DYMO 99014`; - stampa etichette nei formati `DYMO 11354` e `DYMO 99014`;
- pagina pubblica firmata per dettaglio contenitore o documento tramite QR. - pagina pubblica firmata per dettaglio contenitore o documento tramite QR.
Taratura operativa corrente:
- `11354`: layout orizzontale principale con correzione verticale leggera verso l'alto;
- `11354 verticale`: variante dedicata ai driver che ruotano l'etichetta, ora con rotazione corretta lato stampa;
- `99014`: QR spostato in basso a destra per liberare spazio scrivibile nella parte alta.
Per i documenti digitali resta disponibile anche l'assegnazione dei metadati fisici principali: Per i documenti digitali resta disponibile anche l'assegnazione dei metadati fisici principali:
- supporto fisico; - supporto fisico;
@ -75,6 +81,26 @@ ## QR locale sulle etichette
Questo elimina la dipendenza da URL esterni per il QR e rende piu stabile il preview di stampa. Questo elimina la dipendenza da URL esterni per il QR e rende piu stabile il preview di stampa.
## QR pubblico e dominio esterno
Il QR dell'archivio fisico non deve piu dipendere dall'IP interno del server.
Implementazione attuale:
- la firma della route pubblica viene generata in forma relativa;
- il link finale viene costruito usando `APP_PUBLIC_URL`, con fallback su `APP_URL`;
- la validazione avviene lato controller con firma relativa, cosi il QR resta valido anche dietro dominio pubblico o reverse proxy.
Configurazione richiesta in ambiente:
1. impostare `APP_PUBLIC_URL` con il dominio realmente raggiungibile dall'esterno;
2. lasciare `APP_URL` per il contesto applicativo interno se serve;
3. rigenerare o aggiornare i QR gia salvati solo se si vuole riallineare anche il dato persistito nel database.
Esempio:
- `APP_PUBLIC_URL=https://netgescon.example.it`
## Stampa PDF e watermark archivio ## Stampa PDF e watermark archivio
Per i documenti PDF sono ora previste due aperture distinte dalla tab archivio: Per i documenti PDF sono ora previste due aperture distinte dalla tab archivio:
@ -208,6 +234,24 @@ ## Scanner Epson da pagina web
2. upload/ingestione rapida in NetGescon; 2. upload/ingestione rapida in NetGescon;
3. OCR e archiviazione automatica. 3. OCR e archiviazione automatica.
Per il modello Epson WF-C5710 la direzione pratica da testare non e il browser puro, ma una di queste due:
- scansione verso cartella di rete osservata da NetGescon;
- piccolo bridge locale o di rete che espone una chiamata HTTP interna e usa il canale scanner disponibile sul dispositivo.
Questo evita il vincolo dei browser su TWAIN/WIA e mantiene il flusso pilotabile dalla pagina archivio.
## Archivio digitale
La vista `Strumenti > Documenti > Archivio digitale` e stata alleggerita per lavorare meglio dentro la cartella corrente.
Stato attuale:
- breadcrumb e cartelle restano in alto;
- la tabella documenti ora usa tutta la larghezza utile;
- le informazioni laterali sono state compattate in tre card sintetiche sopra la tabella;
- la pagina mostra nome cartella attiva, numero documenti del livello e stato operativo digitale/fisico.
## Dymo LAN 11354 ## Dymo LAN 11354
Domani si puo provare una stampa etichetta su rete con formato `11354` usando: Domani si puo provare una stampa etichetta su rete con formato `11354` usando:

View File

@ -283,24 +283,26 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
@endforelse @endforelse
</div> </div>
<div class="grid gap-3 xl:grid-cols-[minmax(0,1fr)_320px]"> <div class="grid gap-3 lg:grid-cols-3">
<div> <div class="rounded-xl border bg-white p-4">
{{ $this->table }} <div class="text-xs uppercase tracking-wide text-slate-500">Cartella attiva</div>
<div class="mt-2 text-sm font-semibold text-slate-900">{{ $this->digitalArchiveCurrentFolder?->nome ?? 'Nessuna cartella selezionata' }}</div>
<div class="mt-1 text-xs text-slate-500">{{ $this->digitalArchiveCurrentFolderHint }}</div>
</div> </div>
<div class="space-y-3"> <div class="rounded-xl border bg-white p-4">
<div class="rounded-xl border bg-white p-4"> <div class="text-xs uppercase tracking-wide text-slate-500">Documenti nel livello</div>
<div class="text-sm font-semibold text-slate-900">Flusso digitale e fisico</div> <div class="mt-2 text-2xl font-semibold text-slate-900">{{ $this->digitalArchiveCurrentFolderDocumentCount }}</div>
<div class="mt-2 text-sm text-slate-600">Usa l'icona archivio per assegnare contenitore, busta, fascicolo ed etichetta del documento cartaceo. La posizione verra poi richiamata anche nel watermark PDF.</div> <div class="mt-1 text-xs text-slate-500">Solo fatture PDF collegate alla cartella corrente.</div>
</div>
<div class="rounded-xl border bg-white p-4">
<div class="text-sm font-semibold text-slate-900">Stampa</div>
<div class="mt-2 text-sm text-slate-600">Icona stampante: PDF senza timbro. Icona duplicato: PDF con watermark archivio digitale e fisico, se la codifica e presente.</div>
</div>
<div class="rounded-xl border bg-white p-4">
<div class="text-sm font-semibold text-slate-900">Scanner</div>
<div class="mt-2 text-sm text-slate-600">La struttura attuale e pronta per il prossimo aggancio scanner: acquisizione PDF, naming ordinato, assegnazione immediata a cartella digitale e posizione fisica.</div>
</div>
</div> </div>
<div class="rounded-xl border bg-white p-4">
<div class="text-xs uppercase tracking-wide text-slate-500">Operatività</div>
<div class="mt-2 text-sm font-semibold text-slate-900">Archivio digitale e fisico allineati</div>
<div class="mt-1 text-xs text-slate-500">Da qui puoi aprire, stampare, watermarkare e assegnare subito la collocazione cartacea.</div>
</div>
</div>
<div>
{{ $this->table }}
</div> </div>
</div> </div>
</x-filament::section> </x-filament::section>
@ -509,8 +511,9 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
@endif @endif
</div> </div>
<div class="flex shrink-0 flex-wrap items-center gap-2"> <div class="flex shrink-0 flex-wrap items-center gap-2">
<a href="{{ route('filament.archivio-fisico.etichetta', ['item' => $row['id']]) }}?format=11354&autoprint=1" target="_blank" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-medium text-slate-700 shadow-sm hover:bg-slate-50">11354</a> <a href="{{ route('filament.archivio-fisico.etichetta', ['item' => $row['id']]) }}?format=11354&autoprint=1&dymo_fix=0&dymo_rot=0&dymo_dx=0&dymo_dy=-1" target="_blank" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-medium text-slate-700 shadow-sm hover:bg-slate-50">11354</a>
<a href="{{ route('filament.archivio-fisico.etichetta', ['item' => $row['id']]) }}?format=99014&autoprint=1" target="_blank" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-medium text-slate-700 shadow-sm hover:bg-slate-50">99014</a> <a href="{{ route('filament.archivio-fisico.etichetta', ['item' => $row['id']]) }}?format=11354&autoprint=1&dymo_fix=1&dymo_rot=90&dymo_dx=0&dymo_dy=0" target="_blank" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-medium text-slate-700 shadow-sm hover:bg-slate-50">11354 verticale</a>
<a href="{{ route('filament.archivio-fisico.etichetta', ['item' => $row['id']]) }}?format=99014&autoprint=1&dymo_fix=0&dymo_rot=0&dymo_dx=3.5&dymo_dy=3.5" target="_blank" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-medium text-slate-700 shadow-sm hover:bg-slate-50">99014</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -10,17 +10,60 @@
@php($dymoDx = isset($dymoDx) ? (float) $dymoDx : 0.0) @php($dymoDx = isset($dymoDx) ? (float) $dymoDx : 0.0)
@php($dymoDy = isset($dymoDy) ? (float) $dymoDy : 0.0) @php($dymoDy = isset($dymoDy) ? (float) $dymoDy : 0.0)
@php($autoPrint = request()->has('autoprint') && filter_var(request()->query('autoprint'), FILTER_VALIDATE_BOOL)) @php($autoPrint = request()->has('autoprint') && filter_var(request()->query('autoprint'), FILTER_VALIDATE_BOOL))
@php($labelPrinterMode = isset($labelPrinterMode) ? (bool) $labelPrinterMode : false)
@php($pageWidth = $fmt === '99014' ? '54mm' : '57mm')
@php($pageHeight = $fmt === '99014' ? '101mm' : '32mm')
<style> <style>
@page { margin: 0; size: {{ $fmt === '99014' ? '54mm 101mm' : '57mm 32mm' }}; } @page {
margin: {{ $labelPrinterMode ? '0' : '10mm' }};
size: {{ $labelPrinterMode ? ($fmt === '99014' ? '54mm 101mm' : '57mm 32mm') : 'A4 portrait' }};
}
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
width: {{ $fmt === '99014' ? '54mm' : '57mm' }}; width: 100%;
height: {{ $fmt === '99014' ? '101mm' : '32mm' }}; min-height: 100%;
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
overflow: hidden; overflow: visible;
background: {{ $labelPrinterMode ? 'white' : '#eef1f5' }};
}
.sheet {
box-sizing: border-box;
width: {{ $labelPrinterMode ? $pageWidth : '210mm' }};
min-height: {{ $labelPrinterMode ? $pageHeight : '297mm' }};
padding: {{ $labelPrinterMode ? '0' : '12mm' }};
display: flex;
align-items: {{ $labelPrinterMode ? 'stretch' : 'flex-start' }};
justify-content: {{ $labelPrinterMode ? 'stretch' : 'flex-start' }};
margin: 0 auto;
background: white;
overflow: hidden;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
break-inside: avoid;
break-after: avoid-page;
page-break-inside: avoid;
page-break-after: avoid;
}
.label {
box-sizing: border-box;
width: {{ $pageWidth }};
height: {{ $pageHeight }};
padding: 0;
flex: 0 0 auto;
position: relative;
}
.calib {
position: absolute;
inset: 0;
transform-origin: 0 0;
}
.label-content {
box-sizing: border-box;
width: 100%;
height: 100%;
padding: {{ $fmt === '99014' ? '3.5mm' : '2mm' }};
} }
.label { box-sizing: border-box; width: 100%; height: 100%; padding: {{ $fmt === '99014' ? '3.5mm' : '2mm' }}; }
.panel { .panel {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -32,7 +75,16 @@
padding: {{ $fmt === '99014' ? '2.4mm' : '1.5mm' }}; padding: {{ $fmt === '99014' ? '2.4mm' : '1.5mm' }};
align-items: stretch; align-items: stretch;
} }
.panel.panel-99014 {
display: block;
position: relative;
padding: 2.8mm;
}
.content { min-width: 0; } .content { min-width: 0; }
.panel.panel-99014 .content {
padding-right: 0;
padding-bottom: 21mm;
}
.code { font-weight: 800; font-size: {{ $fmt === '99014' ? '14px' : '10px' }}; letter-spacing: 0.4px; } .code { font-weight: 800; font-size: {{ $fmt === '99014' ? '14px' : '10px' }}; letter-spacing: 0.4px; }
.title { font-weight: 700; font-size: {{ $fmt === '99014' ? '13px' : '9px' }}; line-height: 1.15; margin-top: 1mm; } .title { font-weight: 700; font-size: {{ $fmt === '99014' ? '13px' : '9px' }}; line-height: 1.15; margin-top: 1mm; }
.meta { font-size: {{ $fmt === '99014' ? '10px' : '7px' }}; line-height: 1.2; color: #222; margin-top: 1.1mm; } .meta { font-size: {{ $fmt === '99014' ? '10px' : '7px' }}; line-height: 1.2; color: #222; margin-top: 1.1mm; }
@ -46,6 +98,14 @@
padding: 0.4mm; padding: 0.4mm;
box-sizing: border-box; box-sizing: border-box;
} }
.panel.panel-99014 .qr {
position: absolute;
right: 2.8mm;
bottom: 2.8mm;
width: 18mm;
height: 18mm;
padding: 0;
}
.qr svg, .qr svg,
.qr img { .qr img {
display: block; display: block;
@ -58,13 +118,61 @@
aspect-ratio: 1 / 1; aspect-ratio: 1 / 1;
} }
.path { font-size: {{ $fmt === '99014' ? '9px' : '6.6px' }}; line-height: 1.2; color: #444; margin-top: 1mm; word-break: break-word; } .path { font-size: {{ $fmt === '99014' ? '9px' : '6.6px' }}; line-height: 1.2; color: #444; margin-top: 1mm; word-break: break-word; }
@media screen {
body {
padding: 12mm 0;
}
.sheet {
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.18);
}
}
@media print { @media print {
html, body { width: 100%; height: 100%; } html, body {
.label { transform: translate({{ $dymoDx }}mm, {{ $dymoDy }}mm) rotate({{ $dymoRot }}deg); transform-origin: top left; } width: 100%;
@if($fmt === '11354' && $dymoFix) min-height: 100%;
@page { size: 32mm 57mm; } background: white;
html, body { width: 32mm; height: 57mm; } }
.label { width: 57mm; height: 32mm; transform: translate({{ $dymoDx }}mm, calc(57mm + {{ $dymoDy }}mm)) rotate(-90deg); transform-origin: top left; } .sheet {
box-shadow: none;
margin: 0;
padding: {{ $labelPrinterMode ? '0' : '12mm' }};
min-height: auto;
}
@if($labelPrinterMode)
html, body {
overflow: hidden;
}
.sheet {
position: fixed;
left: 0;
top: 0;
}
.calib {
transform: translate({{ $dymoDx }}mm, {{ $dymoDy }}mm) rotate({{ $dymoRot }}deg);
}
@if($fmt === '11354' && $dymoFix)
@page { size: 32mm 57mm; }
html, body {
width: 32mm;
height: 57mm;
}
.sheet {
width: 32mm;
height: 57mm;
padding: 0;
}
.calib {
width: 57mm;
height: 32mm;
@if($dymoRot === 90)
transform: translate({{ $dymoDx }}mm, {{ $dymoDy }}mm) translateX(32mm) rotate(90deg);
@else
transform: translate({{ $dymoDx }}mm, {{ $dymoDy }}mm) translateY(57mm) rotate(-90deg);
@endif
}
@endif
@else
.calib { transform: none !important; }
@endif @endif
} }
</style> </style>
@ -76,27 +184,39 @@
@endif @endif
</head> </head>
<body> <body>
<div class="label"> <div class="sheet">
<div class="panel"> <div class="label">
<div class="content"> <div class="calib">
<div class="code">{{ $item->codice_univoco }}</div> <div class="label-content">
<div class="title">{{ $item->titolo }}</div> <div @class(['panel', 'panel-99014' => $fmt === '99014'])>
<div class="meta"> <div class="content">
<div><strong>Tipo:</strong> {{ $item->tipo_label }}</div> <div class="code">{{ $item->codice_univoco }}</div>
@if($item->voce_numero || $item->voce_titolo) <div class="title">{{ $item->titolo }}</div>
<div><strong>Voce:</strong> {{ $item->voce_numero ? 'Voce ' . $item->voce_numero : '' }} {{ $item->voce_titolo }}</div> <div class="meta">
@endif @if($item->stabile)
@if($item->parent) <div><strong>Stabile:</strong> {{ $item->stabile->denominazione }}</div>
<div><strong>Dentro:</strong> {{ $item->parent->codice_univoco }}</div> @endif
@endif <div><strong>Tipo:</strong> {{ $item->tipo_label }}</div>
@if($item->documento) @if($item->voce_numero || $item->voce_titolo)
<div><strong>Digitale:</strong> {{ $item->documento->nome_file ?: $item->documento->nome }}</div> <div><strong>Voce:</strong> {{ $item->voce_numero ? 'Voce ' . $item->voce_numero : '' }} {{ $item->voce_titolo }}</div>
@endif @endif
@if($item->data_archiviazione)
<div><strong>Data:</strong> {{ $item->data_archiviazione->format('d-m-Y') }}</div>
@endif
@if($item->parent)
<div><strong>Dentro:</strong> {{ $item->parent->codice_univoco }}</div>
@endif
@if($item->documento)
<div><strong>Digitale:</strong> {{ $item->documento->nome_file ?: $item->documento->nome }}</div>
@endif
</div>
<div class="path">{{ $item->percorso_fisico }}</div>
</div>
<div class="qr">
{!! $item->qr_code_image !!}
</div>
</div>
</div> </div>
<div class="path">{{ $item->percorso_fisico }}</div>
</div>
<div class="qr">
{!! $item->qr_code_image !!}
</div> </div>
</div> </div>
</div> </div>

View File

@ -727,7 +727,7 @@
// --- AUTHENTICATION ROUTES --- // --- AUTHENTICATION ROUTES ---
require __DIR__ . '/auth.php'; require __DIR__ . '/auth.php';
Route::middleware('signed')->get('/archivio-fisico/{item}', [\App\Http\Controllers\Filament\DocumentiPrintController::class, 'showPublicArchivioFisicoItem']) Route::get('/archivio-fisico/{item}', [\App\Http\Controllers\Filament\DocumentiPrintController::class, 'showPublicArchivioFisicoItem'])
->name('public.archivio-fisico.show'); ->name('public.archivio-fisico.show');
Route::middleware(['auth'])->group(function () { Route::middleware(['auth'])->group(function () {