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

3541 lines
154 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Filament\Pages\Strumenti;
use App\Models\Documento;
use App\Models\DocumentoArchivioCartella;
use App\Models\DocumentoArchivioFisicoItem;
use App\Models\Impostazione;
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\Archivio\ArchiveQrXmlPayload;
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\Support\Enums\Width;
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\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Livewire\Attributes\Url;
use Livewire\WithFileUploads;
use UnitEnum;
class DocumentiArchivio extends Page implements HasTable
{
use InteractsWithTable;
use WithFileUploads;
protected Width|string|null $maxContentWidth = 'full';
private const ARCHIVE_HUB_SETTINGS_KEY = 'documenti.archivio_hub.settings';
#[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 $scannerAcquisitionForm = [];
public $scannerPdf = null;
public array $genericLabelForm = [];
public string $genericLabelTemplateLabel = '';
public array $physicalArchiveForm = [];
public array $physicalArchiveMoveForm = [];
public array $driveTemplateForm = [];
public string $physicalArchiveSearch = '';
public string $physicalArchiveTypeFilter = '';
#[Url( as : 'code')]
public string $archiveLookupCode = '';
public string $archiveLookupSearch = '';
public ?int $archiveLookupSelectedId = null;
protected static ?string $navigationLabel = 'Documenti';
protected static ?string $title = 'Documenti';
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->initializeScannerAcquisitionForm();
$this->initializeGenericLabelForm();
$this->initializeDriveTemplateForm();
if (trim($this->archiveLookupCode) !== '') {
$this->applyArchiveLookupInput($this->archiveLookupCode);
}
$this->mountInteractsWithTable();
}
public function selectHubTab(string $tab): void
{
$this->hubTab = $this->normalizeHubTab($tab);
if ($this->hubTab === 'archivio-digitale') {
$this->syncInvoiceFolders();
}
if ($this->hubTab === 'scansione-documenti') {
$this->initializeScannerAcquisitionForm();
}
if ($this->hubTab === 'template-drive') {
$this->initializeDriveTemplateForm();
}
if ($this->hubTab === 'lettore-codici' && trim($this->archiveLookupCode) !== '') {
$this->applyArchiveLookupInput($this->archiveLookupCode);
}
}
public function updatedArchiveLookupCode(string $value): void
{
$value = trim($value);
if ($value === '') {
$this->archiveLookupSelectedId = null;
return;
}
$this->applyArchiveLookupInput($value);
}
public function openArchiveLookupFromCode(): void
{
$this->applyArchiveLookupInput($this->archiveLookupCode);
}
public function selectArchiveLookupItem(int $itemId): void
{
$stabileId = (int) ($this->resolveActiveStabile()?->id ?? 0);
$item = DocumentoArchivioFisicoItem::query()
->when($stabileId > 0, fn(Builder $query) => $query->where('stabile_id', $stabileId))
->find($itemId);
if (! $item instanceof DocumentoArchivioFisicoItem) {
Notification::make()->title('Voce archivio non trovata')->danger()->send();
return;
}
$this->archiveLookupSelectedId = (int) $item->id;
$this->archiveLookupCode = (string) $item->codice_univoco;
$this->hubTab = 'lettore-codici';
}
public function setProtocolloView(string $view): void
{
$this->protocolloView = in_array($view, ['cards', 'list'], true) ? $view : 'cards';
}
public function getScannerAcquisitionChannelOptions(): array
{
return [
'scanner_upload' => 'Upload PDF da scanner',
'scanner_rete' => 'Scanner Epson verso cartella/rete',
'scanner_email' => 'Scanner via email',
'mobile_upload' => 'Upload mobile o camera',
'import_manuale' => 'Acquisizione manuale',
];
}
public function getScannerAcquisitionStabiliOptionsProperty(): array
{
return $this->getStabiliOptions();
}
public function getScannerAcquisitionFolderOptionsProperty(): array
{
return $this->getArchivioFolderOptions((int) ($this->scannerAcquisitionForm['stabile_id'] ?? 0));
}
public function getScannerCategoryOptionsProperty(): array
{
return $this->getCategorieOptions();
}
public function getScannerVisibilityScopeOptionsProperty(): array
{
return $this->getVisibilityScopeOptions();
}
public function saveScannedDocument(): void
{
$uploadedPdf = $this->scannerPdf;
if (! is_object($uploadedPdf) || ! method_exists($uploadedPdf, 'store')) {
Notification::make()->title('PDF scansione obbligatorio')->danger()->send();
return;
}
$stabileId = (int) ($this->scannerAcquisitionForm['stabile_id'] ?? 0);
if ($stabileId <= 0) {
Notification::make()->title('Stabile obbligatorio')->danger()->send();
return;
}
$storedPath = $uploadedPdf->store('tmp/scanner-acquisizione', 'local');
$riferimentoAcquisizione = trim((string) ($this->scannerAcquisitionForm['riferimento_acquisizione'] ?? ''));
if ($riferimentoAcquisizione === '') {
$riferimentoAcquisizione = 'SCAN:' . $stabileId . ':' . now()->format('YmdHis');
}
$this->createDocumentoFromForm([
'stabile_id' => $stabileId,
'cartella_archivio_id' => $this->scannerAcquisitionForm['cartella_archivio_id'] ?? null,
'categoria' => (string) ($this->scannerAcquisitionForm['categoria'] ?? 'altro'),
'archivio_classe' => (string) ($this->scannerAcquisitionForm['archivio_classe'] ?? 'altro'),
'titolo' => (string) ($this->scannerAcquisitionForm['titolo'] ?? ''),
'descrizione' => (string) ($this->scannerAcquisitionForm['descrizione'] ?? ''),
'fornitore' => (string) ($this->scannerAcquisitionForm['fornitore'] ?? ''),
'data_documento' => $this->scannerAcquisitionForm['data_documento'] ?? now()->toDateString(),
'data_scadenza' => $this->scannerAcquisitionForm['data_scadenza'] ?? null,
'importo' => $this->scannerAcquisitionForm['importo'] ?? null,
'movimento_contabile_id' => null,
'faldone' => (string) ($this->scannerAcquisitionForm['faldone'] ?? ''),
'supporto_fisico' => $this->scannerAcquisitionForm['supporto_fisico'] ?? null,
'modello_etichetta' => $this->scannerAcquisitionForm['modello_etichetta'] ?? '11354',
'busta_numero' => $this->scannerAcquisitionForm['busta_numero'] ?? null,
'fascicolo_numero' => $this->scannerAcquisitionForm['fascicolo_numero'] ?? null,
'contenitore_numero' => $this->scannerAcquisitionForm['contenitore_numero'] ?? null,
'periodo_riferimento_da' => $this->scannerAcquisitionForm['periodo_riferimento_da'] ?? null,
'periodo_riferimento_a' => $this->scannerAcquisitionForm['periodo_riferimento_a'] ?? null,
'visibility_scope' => (string) ($this->scannerAcquisitionForm['visibility_scope'] ?? 'interno'),
'visibility_roles' => [],
'visibility_groups' => [],
'allowed_user_ids' => [],
'pdf' => $storedPath,
'canale_acquisizione' => (string) ($this->scannerAcquisitionForm['canale_acquisizione'] ?? 'scanner_upload'),
'scanner_profile' => (string) ($this->scannerAcquisitionForm['scanner_profile'] ?? ''),
'riferimento_acquisizione' => $riferimentoAcquisizione,
'scanner_note' => (string) ($this->scannerAcquisitionForm['scanner_note'] ?? ''),
'origine_documento' => 'scanner_documenti_hub',
], false);
$this->initializeScannerAcquisitionForm();
$this->scannerPdf = null;
}
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('Codice archivio')
->state(fn(Documento $record): string => $record->resolveArchiveDisplayReference())
->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);
if (trim((string) ($meta['codice_univoco_documento'] ?? '')) === '') {
$meta['codice_univoco_documento'] = $record->resolveArchiveCode();
}
$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 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
{
$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
{
$settings = $this->getArchiveHubSettings();
$items = $settings['drive_template_folders'] ?? [];
$yearRoot = trim((string) ($settings['drive_template_year_root'] ?? ''));
$yearModel = trim((string) ($settings['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));
}
public function getDriveTemplateSourceProperty(): string
{
return $this->hasStoredArchiveHubSettings() ? 'database' : 'config';
}
public function getCanManageDriveTemplatesProperty(): bool
{
$user = Auth::user();
return $user instanceof User && $user->hasRole('super-admin');
}
public function getGenericLabelRowsProperty(): array
{
$stabileId = (int) ($this->resolveActiveStabile()?->id ?? 0);
if ($stabileId <= 0) {
return [];
}
return DocumentoArchivioFisicoItem::query()
->where('stabile_id', $stabileId)
->latest('id')
->take(24)
->get()
->filter(fn(DocumentoArchivioFisicoItem $item): bool => (bool) data_get($item->metadati, 'generic_label', false))
->map(function (DocumentoArchivioFisicoItem $item): array {
$printOptions = $this->buildGenericLabelPrintOptions((int) $item->id);
$defaultPrintOption = ($item->modello_etichetta ?: '11354') === '99014' ? '99014' : '11354';
return [
'id' => (int) $item->id,
'codice_univoco' => (string) $item->codice_univoco,
'titolo' => (string) $item->titolo,
'tipo' => (string) $item->tipo_label,
'percorso' => (string) $item->percorso_fisico,
'ubicazione' => trim(implode(' · ', array_filter([
$this->normalizeNullableText($item->magazzino),
$this->normalizeNullableText($item->scaffale),
$this->normalizeNullableText($item->ripiano),
$this->normalizeNullableText($item->ubicazione_dettaglio),
]))),
'linea_secondaria' => (string) data_get($item->metadati, 'linea_secondaria', ''),
'amazon_url' => (string) data_get($item->metadati, 'amazon_url', ''),
'preset_label' => (string) ($this->genericLabelPresetOptions[data_get($item->metadati, 'generic_label_template.preset_key', '')] ?? 'Custom'),
'template_label' => (string) ($this->genericLabelTemplateOptions[data_get($item->metadati, 'generic_label_template.template_name', '')] ?? 'Layout personalizzato'),
'print_options' => $printOptions,
'default_print' => array_key_exists($defaultPrintOption, $printOptions) ? $defaultPrintOption : array_key_first($printOptions),
];
})
->values()
->all();
}
/** @return array<string, array{label: string, url: string}> */
private function buildGenericLabelPrintOptions(int $itemId): array
{
$baseUrl = route('filament.archivio-fisico.etichetta', ['item' => $itemId]);
return [
'11354' => [
'label' => 'DYMO 11354 orizzontale',
'url' => $baseUrl . '?format=11354&autoprint=1&dymo_fix=0&dymo_rot=0&dymo_dx=0&dymo_dy=-1',
],
'11354_vertical' => [
'label' => 'DYMO 11354 verticale',
'url' => $baseUrl . '?format=11354&autoprint=1&dymo_fix=1&dymo_rot=90&dymo_dx=0&dymo_dy=0',
],
'99014' => [
'label' => 'DYMO 99014 larga',
'url' => $baseUrl . '?format=99014&autoprint=1&dymo_fix=0&dymo_rot=0&dymo_dx=3.5&dymo_dy=3.5',
],
];
}
/** @return array<string, string> */
public function getGenericLabelPresetOptionsProperty(): array
{
return collect($this->getGenericLabelPresetDefinitions())
->mapWithKeys(fn(array $preset, string $key): array=> [$key => (string) ($preset['label'] ?? Str::headline($key))])
->all();
}
/** @return array<int, array<string, string>> */
public function getGenericLabelSavedTemplatesProperty(): array
{
return collect($this->getStoredGenericLabelPresetDefinitions())
->map(function (array $preset, string $key): array {
$templateName = (string) ($preset['template_name'] ?? 'classic-right');
return [
'key' => $key,
'label' => (string) ($preset['label'] ?? Str::headline($key)),
'tipo_item' => (string) ($preset['tipo_item'] ?? 'scatola'),
'modello' => (string) ($preset['modello_etichetta'] ?? '11354'),
'template_name' => $templateName,
'template_label' => (string) ($this->genericLabelTemplateOptions[$templateName] ?? 'Template'),
];
})
->values()
->all();
}
/** @return array<string, string> */
public function getGenericLabelTemplateOptionsProperty(): array
{
return [
'archive-99014' => 'Archivio 99014 con righe manuali',
'classic-right' => 'Classica QR a destra',
'classic-bottom' => 'Classica QR in basso',
'band-top' => 'Fascia alta + QR laterale',
'compact-grid' => 'Compatta a griglia',
];
}
/** @return array<string, string> */
public function getGenericLabelTemplateFieldOptionsProperty(): array
{
return [
'mnemonic_code' => 'Codice mnemonico',
'titolo' => 'Titolo etichetta',
'linea_secondaria' => 'Seconda riga',
'tipo_label' => 'Tipologia contenitore',
'supporto_fisico' => 'Supporto',
'percorso_compatto' => 'Percorso archivio',
'ubicazione' => 'Ubicazione',
'note' => 'Nota breve',
'stabile' => 'Stabile attivo',
'stabile_code' => 'Codice stabile',
'stabile_address' => 'Indirizzo stabile',
'stabile_summary' => 'Stabile compatto',
'codice_univoco' => 'Codice univoco',
'amazon_url' => 'Link acquisto',
'none' => 'Nessun campo',
];
}
/** @return array<string, string> */
public function getGenericLabelStableFieldOptionsProperty(): array
{
return [
'stabile' => 'Nome stabile',
'stabile_code' => 'Codice stabile',
'stabile_address' => 'Indirizzo stabile',
'stabile_summary' => 'Stabile compatto',
'none' => 'Nessuna riga',
];
}
/** @return array<string, string> */
public function getGenericLabelLineModeOptionsProperty(): array
{
return [
'source' => 'Campo archivio',
'text' => 'Testo libero',
'blank_line' => 'Riga bianca scrivibile',
'none' => 'Nessuna riga',
];
}
/** @return array<string, string> */
public function getGenericLabelFontSizeOptionsProperty(): array
{
return [
'xs' => 'Extra piccolo',
'sm' => 'Piccolo',
'md' => 'Medio',
'lg' => 'Grande',
'xl' => 'Extra grande',
];
}
/** @return array<string, mixed> */
public function getGenericLabelPreviewProperty(): array
{
$templateName = trim((string) ($this->genericLabelForm['template_name'] ?? 'classic-right'));
$template = $this->getGenericLabelTemplateDefinition($templateName);
$stabile = $this->resolveActiveStabile();
$mnemonicCode = trim((string) ($this->genericLabelForm['mnemonic_code'] ?? ''));
if ($mnemonicCode === '') {
$mnemonicCode = (string) ($this->mnemonicCodeHint ?? 'GER00079');
}
$stabileCode = '';
$stabileAddress = '';
if ($stabile instanceof Stabile) {
$stabileCode = trim((string) ($stabile->codice_stabile ?? ''));
$stabileAddress = trim((string) ($stabile->indirizzo_completo ?? ''));
}
$amazonUrl = trim((string) ($this->normalizeAmazonPurchaseUrl($this->genericLabelForm['amazon_url'] ?? null) ?? ''));
$qrProfiles = ArchiveQrXmlPayload::buildProfiles([
'id' => 'AF-QR-PREVIEW',
'legacy' => 'AF:AF-QR-PREVIEW',
'type' => Str::upper((string) ($this->genericLabelForm['tipo_item'] ?? 'SCATOLA')),
'stabile_id' => (string) ((int) ($this->resolveActiveStabile()?->id ?? 0)),
'parent' => trim((string) ($this->genericLabelForm['parent_id'] ?? '')) !== '' ? 'COLLEGATO' : 'RADICE',
'code' => $mnemonicCode,
'title' => trim((string) ($this->genericLabelForm['titolo'] ?? '')),
'archive_class' => 'GENERIC_LABEL',
'location' => trim(implode('/', array_filter([
$this->genericLabelForm['magazzino'] ?? null,
$this->genericLabelForm['scaffale'] ?? null,
$this->genericLabelForm['ripiano'] ?? null,
$this->genericLabelForm['ubicazione_dettaglio'] ?? null,
]))),
]);
$payload = [
'mnemonic_code' => $mnemonicCode,
'titolo' => trim((string) ($this->genericLabelForm['titolo'] ?? '')),
'linea_secondaria' => trim((string) ($this->genericLabelForm['linea_secondaria'] ?? '')),
'tipo_label' => DocumentoArchivioFisicoItem::TIPI[(string) ($this->genericLabelForm['tipo_item'] ?? 'scatola')] ?? 'Contenitore',
'supporto_fisico' => trim((string) ($this->genericLabelForm['supporto_fisico'] ?? '')),
'percorso_compatto' => trim(implode(' / ', array_filter([
$this->resolveActiveStabile()?->denominazione,
$this->genericLabelForm['supporto_fisico'] ?? null,
$this->genericLabelForm['titolo'] ?? null,
]))),
'ubicazione' => trim(implode(' · ', array_filter([
$this->genericLabelForm['magazzino'] ?? null,
$this->genericLabelForm['scaffale'] ?? null,
$this->genericLabelForm['ripiano'] ?? null,
$this->genericLabelForm['ubicazione_dettaglio'] ?? null,
]))),
'note' => trim((string) ($this->genericLabelForm['note'] ?? '')),
'stabile' => trim((string) ($this->resolveActiveStabile()?->denominazione ?? 'Stabile attivo')),
'stabile_code' => $stabileCode,
'stabile_address' => $stabileAddress,
'stabile_summary' => trim(implode(' · ', array_filter([
trim((string) ($this->resolveActiveStabile()?->denominazione ?? '')),
$stabileCode,
$stabileAddress,
]))),
'codice_univoco' => 'AF-QR-PREVIEW',
'qr_payload' => (string) ($qrProfiles['xml_open'] ?? ''),
'qr_payload_zip' => (string) ($qrProfiles['xml_zip'] ?? ''),
'nfc_payload' => (string) ($qrProfiles['xml_nfc'] ?? ''),
'qr_payload_length' => (int) ($qrProfiles['xml_open_length'] ?? 0),
'nfc_payload_length' => (int) ($qrProfiles['xml_nfc_length'] ?? 0),
'qr_markup' => $this->buildGenericLabelQrMarkup((string) ($qrProfiles['xml_open'] ?? '')),
'amazon_url' => $amazonUrl,
];
$stableLines = $this->buildGenericLabelLineRows($payload);
return [
'template_name' => $templateName,
'template' => $template,
'format' => (string) ($this->genericLabelForm['modello_etichetta'] ?? '11354'),
'qr_position' => (string) ($this->genericLabelForm['qr_position'] ?? ($template['qr_position'] ?? 'right-center')),
'qr_scale' => (string) ($this->genericLabelForm['qr_scale'] ?? 'md'),
'slots' => [
'title' => $this->resolveGenericLabelTemplateValue((string) ($this->genericLabelForm['title_source'] ?? 'titolo'), $payload),
'subtitle' => $this->resolveGenericLabelTemplateValue((string) ($this->genericLabelForm['subtitle_source'] ?? 'linea_secondaria'), $payload),
'meta' => $this->resolveGenericLabelTemplateValue((string) ($this->genericLabelForm['meta_source'] ?? 'percorso_compatto'), $payload),
'note' => $this->resolveGenericLabelTemplateValue((string) ($this->genericLabelForm['note_source'] ?? 'note'), $payload),
],
'payload' => $payload,
'mnemonic_code' => $mnemonicCode,
'font_sizes' => [
'mnemonic' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['mnemonic_font_size'] ?? 'xl')),
'title' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['title_font_size'] ?? 'xl')),
'subtitle' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['subtitle_font_size'] ?? 'md')),
'lines' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['lines_font_size'] ?? 'md')),
'note' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['note_font_size'] ?? 'sm')),
],
'stable_lines' => $stableLines,
'blank_lines' => [],
'amazon_url' => $amazonUrl,
];
}
private function buildGenericLabelQrMarkup(string $data): string
{
$data = trim($data);
if ($data === '') {
return '<span aria-hidden="true"></span>';
}
if (class_exists(\chillerlan\QRCode\QROptions::class) && class_exists(\chillerlan\QRCode\QRCode::class)) {
$options = new \chillerlan\QRCode\QROptions([
'outputType' => \chillerlan\QRCode\QRCode::OUTPUT_MARKUP_SVG,
'eccLevel' => \chillerlan\QRCode\QRCode::ECC_M,
'addQuietzone' => true,
'quietzoneSize' => 1,
'connectPaths' => true,
]);
$svg = (new \chillerlan\QRCode\QRCode($options))->render($data);
if (str_starts_with($svg, 'data:image/svg+xml;base64,')) {
$decoded = base64_decode(substr($svg, strlen('data:image/svg+xml;base64,')), true);
if (is_string($decoded) && $decoded !== '') {
$svg = $decoded;
}
}
if (str_contains($svg, '<svg')) {
$svg = preg_replace('/<svg\b/', '<svg aria-label="QR anteprima etichetta" role="img" preserveAspectRatio="xMidYMid meet"', $svg, 1) ?: $svg;
return $svg;
}
}
return '<img alt="QR anteprima etichetta" src="https://api.qrserver.com/v1/create-qr-code/?size=220x220&data=' . rawurlencode($data) . '">';
}
/** @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['codice_univoco_documento'] = (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 saveGenericArchiveLabel(): 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->genericLabelForm['titolo'] ?? ''));
$tipoItem = trim((string) ($this->genericLabelForm['tipo_item'] ?? 'scatola'));
if ($titolo === '' || ! array_key_exists($tipoItem, DocumentoArchivioFisicoItem::TIPI)) {
Notification::make()->title('Titolo e tipologia sono obbligatori')->danger()->send();
return;
}
$parentId = is_numeric($this->genericLabelForm['parent_id'] ?? null) ? (int) $this->genericLabelForm['parent_id'] : null;
$parent = $parentId
? DocumentoArchivioFisicoItem::query()->where('stabile_id', $stabileId)->find($parentId)
: null;
$amazonUrl = $this->normalizeAmazonPurchaseUrl($this->genericLabelForm['amazon_url'] ?? null);
$item = DocumentoArchivioFisicoItem::query()->create([
'stabile_id' => $stabileId,
'parent_id' => $parent?->id,
'tipo_item' => $tipoItem,
'titolo' => $titolo,
'note' => $this->normalizeNullableText($this->genericLabelForm['note'] ?? null),
'data_archiviazione' => now()->toDateString(),
'supporto_fisico' => $this->normalizeNullableText($this->genericLabelForm['supporto_fisico'] ?? null),
'modello_etichetta' => $this->normalizeNullableText($this->genericLabelForm['modello_etichetta'] ?? null) ?: '11354',
'magazzino' => $this->normalizeNullableText($this->genericLabelForm['magazzino'] ?? null),
'scaffale' => $this->normalizeNullableText($this->genericLabelForm['scaffale'] ?? null),
'ripiano' => $this->normalizeNullableText($this->genericLabelForm['ripiano'] ?? null),
'ubicazione_dettaglio' => $this->normalizeNullableText($this->genericLabelForm['ubicazione_dettaglio'] ?? null),
'created_by' => (int) $user->id,
'updated_by' => (int) $user->id,
'metadati' => [
'generic_label' => true,
'linea_secondaria' => $this->normalizeNullableText($this->genericLabelForm['linea_secondaria'] ?? null),
'amazon_url' => $amazonUrl,
'generic_label_template' => [
'preset_key' => $this->normalizeNullableText($this->genericLabelForm['preset_key'] ?? null),
'template_name' => $this->normalizeNullableText($this->genericLabelForm['template_name'] ?? null) ?: 'classic-right',
'mnemonic_code' => $this->normalizeNullableText($this->genericLabelForm['mnemonic_code'] ?? null) ?: $this->mnemonicCodeHint,
'title_source' => $this->normalizeNullableText($this->genericLabelForm['title_source'] ?? null) ?: 'titolo',
'subtitle_source' => $this->normalizeNullableText($this->genericLabelForm['subtitle_source'] ?? null) ?: 'linea_secondaria',
'meta_source' => $this->normalizeNullableText($this->genericLabelForm['meta_source'] ?? null) ?: 'percorso_compatto',
'note_source' => $this->normalizeNullableText($this->genericLabelForm['note_source'] ?? null) ?: 'note',
'stable_line_one_source' => $this->normalizeNullableText($this->genericLabelForm['stable_line_one_source'] ?? null) ?: 'stabile_summary',
'stable_line_two_source' => $this->normalizeNullableText($this->genericLabelForm['stable_line_two_source'] ?? null) ?: 'blank_line',
'blank_lines_count' => max(0, min(4, (int) ($this->genericLabelForm['blank_lines_count'] ?? 2))),
'qr_position' => $this->normalizeNullableText($this->genericLabelForm['qr_position'] ?? null) ?: 'right-center',
'qr_scale' => $this->normalizeNullableText($this->genericLabelForm['qr_scale'] ?? null) ?: 'md',
'mnemonic_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['mnemonic_font_size'] ?? 'xl')),
'title_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['title_font_size'] ?? 'xl')),
'subtitle_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['subtitle_font_size'] ?? 'md')),
'lines_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['lines_font_size'] ?? 'md')),
'note_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['note_font_size'] ?? 'sm')),
'line_rows' => $this->serializeGenericLabelLineRows(),
],
],
]);
$this->initializeGenericLabelForm();
Notification::make()
->title('Etichetta generica pronta')
->body('Codice: ' . $item->codice_univoco)
->success()
->send();
}
public function saveCurrentGenericLabelAsTemplate(): void
{
$label = trim($this->genericLabelTemplateLabel);
if ($label === '') {
Notification::make()->title('Inserisci un nome template')->danger()->send();
return;
}
$settings = $this->getArchiveHubSettings();
$stored = is_array($settings['generic_label_templates'] ?? null) ? $settings['generic_label_templates'] : [];
$existingKey = collect($stored)->search(function (mixed $template) use ($label): bool {
return is_array($template) && trim((string) ($template['label'] ?? '')) === $label;
});
$templateKey = is_string($existingKey) ? $existingKey : $this->makeGenericLabelTemplateKey($label, $stored);
$stored[$templateKey] = $this->buildGenericLabelTemplatePayload($label);
$settings['generic_label_templates'] = $stored;
$this->upsertArchiveHubSetting(
json_encode($settings, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
'Impostazioni archivio hub: template drive, link acquisto e template etichette.'
);
$this->genericLabelForm['preset_key'] = $templateKey;
Notification::make()
->title('Template etichetta salvato')
->body('Template: ' . $label)
->success()
->send();
}
public function saveDriveTemplateSettings(): void
{
if (! $this->canManageDriveTemplates) {
Notification::make()->title('Solo super-admin puo aggiornare il template')->danger()->send();
return;
}
$folders = collect(preg_split('/\r\n|\r|\n/', (string) ($this->driveTemplateForm['folders_text'] ?? '')) ?: [])
->map(fn(string $item): string => trim($item))
->filter(fn(string $item): bool => $item !== '')
->unique()
->values()
->all();
$settings = [
'drive_template_folders' => $folders,
'drive_template_year_root' => trim((string) ($this->driveTemplateForm['year_root'] ?? '')),
'drive_template_year_model' => trim((string) ($this->driveTemplateForm['year_model'] ?? '')),
'amazon_affiliate_tag' => trim((string) ($this->driveTemplateForm['amazon_affiliate_tag'] ?? '')),
'amazon_base_url' => trim((string) ($this->driveTemplateForm['amazon_base_url'] ?? '')),
];
$this->upsertArchiveHubSetting(
json_encode($settings, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
'Impostazioni archivio hub: template drive e link acquisto etichette.'
);
$this->initializeDriveTemplateForm();
Notification::make()
->title('Template Drive aggiornato')
->body('La struttura ora viene letta dal database.')
->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;
}
$supportoFisico = $this->normalizeNullableText($this->physicalArchiveMoveForm['supporto_fisico'] ?? null);
$magazzino = $this->normalizeNullableText($this->physicalArchiveMoveForm['magazzino'] ?? null) ?? $target->magazzino ?? $source->magazzino;
$scaffale = $this->normalizeNullableText($this->physicalArchiveMoveForm['scaffale'] ?? null) ?? $target->scaffale ?? $source->scaffale;
$ripiano = $this->normalizeNullableText($this->physicalArchiveMoveForm['ripiano'] ?? null) ?? $target->ripiano ?? $source->ripiano;
$ubicazioneDettaglio = $this->normalizeNullableText($this->physicalArchiveMoveForm['ubicazione_dettaglio'] ?? null) ?? $target->ubicazione_dettaglio ?? $source->ubicazione_dettaglio;
$payload = [
'parent_id' => (int) $target->id,
'updated_by' => Auth::id(),
'magazzino' => $magazzino,
'scaffale' => $scaffale,
'ripiano' => $ripiano,
'ubicazione_dettaglio' => $ubicazioneDettaglio,
];
if ($supportoFisico !== null) {
$payload['supporto_fisico'] = $supportoFisico;
}
$source->forceFill($payload)->save();
$ubicazione = collect([$magazzino, $scaffale, $ripiano, $ubicazioneDettaglio])->filter()->implode(' · ');
$this->physicalArchiveMoveForm = [
'source_code' => '',
'target_code' => '',
'supporto_fisico' => '',
'magazzino' => '',
'scaffale' => '',
'ripiano' => '',
'ubicazione_dettaglio' => '',
];
Notification::make()
->title('Movimentazione registrata')
->body(trim($source->codice_univoco . ' → ' . $target->codice_univoco . ($ubicazione !== '' ? ' · ' . $ubicazione : '')))
->success()
->send();
}
public function getArchiveLookupResultsProperty(): array
{
$search = trim($this->archiveLookupSearch);
$codeInput = trim($this->archiveLookupCode);
$needle = $search !== '' ? $search : $codeInput;
$exactCode = $this->extractArchiveCodeFromInput($codeInput !== '' ? $codeInput : $search);
$stabileId = (int) ($this->resolveActiveStabile()?->id ?? 0);
$query = DocumentoArchivioFisicoItem::query()
->with(['parent:id,codice_univoco,titolo', 'documento:id,nome,nome_file,archive_reference'])
->withCount('children')
->when($stabileId > 0, fn(Builder $builder) => $builder->where('stabile_id', $stabileId))
->orderByDesc('updated_at')
->orderByDesc('id');
if ($needle !== '') {
$query->where(function (Builder $builder) use ($needle, $exactCode): void {
$builder->where('codice_univoco', 'like', '%' . $needle . '%')
->orWhere('titolo', 'like', '%' . $needle . '%')
->orWhere('note', 'like', '%' . $needle . '%')
->orWhere('nfc_reference', 'like', '%' . $needle . '%')
->orWhere('qr_code_data', 'like', '%' . $needle . '%');
if ($exactCode !== '') {
$builder->orWhere('codice_univoco', $exactCode)
->orWhere('qr_code_data', 'like', '%AF:' . $exactCode . '%');
}
});
}
return $query
->limit($needle !== '' ? 18 : 12)
->get()
->map(fn(DocumentoArchivioFisicoItem $item): array=> [
'id' => (int) $item->id,
'codice_univoco' => (string) $item->codice_univoco,
'titolo' => (string) $item->titolo,
'tipo' => (string) $item->tipo_label,
'parent' => $item->parent ? trim($item->parent->codice_univoco . ' · ' . $item->parent->titolo) : 'Radice',
'children_count' => (int) $item->children_count,
'documento' => $item->documento ? trim((string) ($item->documento->nome_file ?: $item->documento->nome ?: ('Documento #' . $item->documento->id))): null,
'percorso' => (string) $item->percorso_fisico,
'ubicazione' => collect([$item->magazzino, $item->scaffale, $item->ripiano, $item->ubicazione_dettaglio])->filter()->implode(' · '),
])
->all();
}
public function getArchiveLookupSelectedItemProperty(): ?DocumentoArchivioFisicoItem
{
$selectedId = (int) ($this->archiveLookupSelectedId ?? 0);
if ($selectedId <= 0) {
return null;
}
$stabileId = (int) ($this->resolveActiveStabile()?->id ?? 0);
return DocumentoArchivioFisicoItem::query()
->with([
'parent.parent',
'children.parent',
'children.documento',
'documento',
'stabile',
])
->when($stabileId > 0, fn(Builder $builder) => $builder->where('stabile_id', $stabileId))
->find($selectedId);
}
public function getArchiveLookupSelectedChainProperty(): array
{
$item = $this->archiveLookupSelectedItem;
if (! $item instanceof DocumentoArchivioFisicoItem) {
return [];
}
$chain = [];
$current = $item;
while ($current instanceof DocumentoArchivioFisicoItem) {
array_unshift($chain, [
'id' => (int) $current->id,
'codice_univoco' => (string) $current->codice_univoco,
'titolo' => (string) $current->titolo,
'tipo' => (string) $current->tipo_label,
]);
$current = $current->parent;
}
return $chain;
}
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_file' => (clone $query)
->get()
->filter(fn(Documento $documento): bool => $this->hasStoredFile($documento))
->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 ?? ''));
$fileMeta = $this->resolveDocumentoMediaMeta($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_code' => $documento->resolveArchiveCode(),
'archive_reference' => trim((string) ($documento->archive_reference ?? '')),
'has_file' => $fileMeta['has_file'],
'has_pdf' => $fileMeta['is_pdf'],
'can_print' => $fileMeta['can_print'],
'file_extension' => $fileMeta['extension'],
'file_kind' => $fileMeta['kind_label'],
'file_badge' => $fileMeta['badge_label'],
'preview_url' => $fileMeta['has_file'] ? $openUrl : null,
'download_url' => $fileMeta['has_file'] ? route('filament.documenti.download', $documento) : null,
'print_url' => $fileMeta['can_print'] ? $openUrl : null,
];
})
->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' => '',
'supporto_fisico' => '',
'magazzino' => '',
'scaffale' => '',
'ripiano' => '',
'ubicazione_dettaglio' => '',
];
}
private function initializeGenericLabelForm(): void
{
$this->genericLabelForm = [
'preset_key' => '',
'mnemonic_code' => (string) ($this->mnemonicCodeHint ?? 'GER00079'),
'titolo' => '',
'linea_secondaria' => '',
'note' => '',
'tipo_item' => 'scatola',
'supporto_fisico' => 'Scatola',
'parent_id' => null,
'magazzino' => '',
'scaffale' => '',
'ripiano' => '',
'ubicazione_dettaglio' => '',
'modello_etichetta' => '11354',
'amazon_url' => '',
'template_name' => 'archive-99014',
'title_source' => 'titolo',
'subtitle_source' => 'linea_secondaria',
'meta_source' => 'percorso_compatto',
'note_source' => 'note',
'stable_line_one_source' => 'stabile_summary',
'stable_line_two_source' => 'blank_line',
'blank_lines_count' => 2,
'qr_position' => 'bottom-right',
'qr_scale' => 'lg',
'line_1_mode' => 'source',
'line_1_source' => 'stabile_summary',
'line_1_text' => '',
'line_2_mode' => 'blank_line',
'line_2_source' => 'none',
'line_2_text' => '',
'line_3_mode' => 'blank_line',
'line_3_source' => 'none',
'line_3_text' => '',
'line_4_mode' => 'none',
'line_4_source' => 'none',
'line_4_text' => '',
'mnemonic_font_size' => 'xl',
'title_font_size' => 'xl',
'subtitle_font_size' => 'md',
'lines_font_size' => 'md',
'note_font_size' => 'sm',
];
$this->genericLabelTemplateLabel = '';
}
private function initializeScannerAcquisitionForm(): void
{
$this->scannerAcquisitionForm = [
'stabile_id' => (int) ($this->resolveActiveStabile()?->id ?? $this->getDefaultStabileId() ?? 0),
'cartella_archivio_id' => $this->currentFolderId,
'categoria' => 'altro',
'archivio_classe' => 'altro',
'titolo' => '',
'descrizione' => '',
'fornitore' => '',
'data_documento' => now()->toDateString(),
'data_scadenza' => null,
'importo' => null,
'faldone' => '',
'supporto_fisico' => 'Busta trasparente con anelli da archiviare',
'modello_etichetta' => '11354',
'busta_numero' => '',
'fascicolo_numero' => '',
'contenitore_numero' => '',
'periodo_riferimento_da' => null,
'periodo_riferimento_a' => null,
'visibility_scope' => 'interno',
'canale_acquisizione' => 'scanner_upload',
'scanner_profile' => '',
'riferimento_acquisizione' => '',
'scanner_note' => '',
];
}
/** @return array<string, mixed> */
public function getScannerQrPreviewProperty(): array
{
$title = trim((string) ($this->scannerAcquisitionForm['titolo'] ?? ''));
$supporto = trim((string) ($this->scannerAcquisitionForm['supporto_fisico'] ?? ''));
$stabileId = (int) ($this->scannerAcquisitionForm['stabile_id'] ?? 0);
$categoria = trim((string) ($this->scannerAcquisitionForm['categoria'] ?? 'altro'));
$classe = trim((string) ($this->scannerAcquisitionForm['archivio_classe'] ?? 'altro'));
$busta = trim((string) ($this->scannerAcquisitionForm['busta_numero'] ?? ''));
$fascicolo = trim((string) ($this->scannerAcquisitionForm['fascicolo_numero'] ?? ''));
$contenitore = trim((string) ($this->scannerAcquisitionForm['contenitore_numero'] ?? ''));
$archiveCode = trim(implode('-', array_filter([
strtoupper($categoria !== '' ? $categoria : 'DOC'),
now()->format('Y'),
$busta !== '' ? 'B' . $busta : null,
$fascicolo !== '' ? 'F' . $fascicolo : null,
$contenitore !== '' ? 'C' . $contenitore : null,
])));
$profiles = ArchiveQrXmlPayload::buildProfiles([
'id' => $archiveCode !== '' ? $archiveCode : 'DOC-PREVIEW',
'legacy' => 'DOC:' . ($archiveCode !== '' ? $archiveCode : 'DOC-PREVIEW'),
'type' => 'ATTO',
'stabile_id' => (string) $stabileId,
'parent' => $supporto !== '' ? strtoupper(Str::slug($supporto, '-')) : 'RADICE',
'code' => $archiveCode,
'title' => $title,
'document_code' => $archiveCode,
'archive_class' => strtoupper($classe),
'location' => $supporto,
]);
return [
'code' => $archiveCode,
'xml_open' => (string) ($profiles['xml_open'] ?? ''),
'xml_zip' => (string) ($profiles['xml_zip'] ?? ''),
'xml_nfc' => (string) ($profiles['xml_nfc'] ?? ''),
'xml_open_length' => (int) ($profiles['xml_open_length'] ?? 0),
'xml_nfc_length' => (int) ($profiles['xml_nfc_length'] ?? 0),
];
}
private function initializeDriveTemplateForm(): void
{
$settings = $this->getArchiveHubSettings();
$this->driveTemplateForm = [
'folders_text' => implode("\n", $settings['drive_template_folders'] ?? []),
'year_root' => (string) ($settings['drive_template_year_root'] ?? ''),
'year_model' => (string) ($settings['drive_template_year_model'] ?? ''),
'amazon_affiliate_tag' => (string) ($settings['amazon_affiliate_tag'] ?? ''),
'amazon_base_url' => (string) ($settings['amazon_base_url'] ?? 'https://www.amazon.it/dp'),
];
}
private function getArchiveHubSettings(): array
{
$fallback = [
'drive_template_folders' => config('netgescon.google.drive_template_folders', []),
'drive_template_year_root' => (string) config('netgescon.google.drive_template_year_root', ''),
'drive_template_year_model' => (string) config('netgescon.google.drive_template_year_model', ''),
'amazon_affiliate_tag' => '',
'amazon_base_url' => 'https://www.amazon.it/dp',
'generic_label_templates' => [],
];
$stored = $this->readArchiveHubSettingValue();
if (! is_string($stored) || trim($stored) === '') {
return $fallback;
}
$decoded = json_decode($stored, true);
if (! is_array($decoded)) {
return $fallback;
}
return array_merge($fallback, $decoded);
}
private function hasStoredArchiveHubSettings(): bool
{
return $this->archiveHubSettingsQuery()->exists();
}
private function readArchiveHubSettingValue(): ?string
{
$value = $this->archiveHubSettingsQuery()->value('valore');
return is_string($value) ? $value : null;
}
private function upsertArchiveHubSetting(string $value, string $description): void
{
$modelClass = $this->resolveArchiveHubSettingsModelClass();
if ($modelClass !== null) {
$modelClass::query()->updateOrCreate(
['chiave' => self::ARCHIVE_HUB_SETTINGS_KEY],
[
'valore' => $value,
'descrizione' => $description,
]
);
return;
}
DB::table('impostazioni')->updateOrInsert(
['chiave' => self::ARCHIVE_HUB_SETTINGS_KEY],
[
'valore' => $value,
'descrizione' => $description,
]
);
}
private function archiveHubSettingsQuery(): Builder | \Illuminate\Database\Query\Builder
{
$modelClass = $this->resolveArchiveHubSettingsModelClass();
return $modelClass !== null
? $modelClass::query()->where('chiave', self::ARCHIVE_HUB_SETTINGS_KEY)
: DB::table('impostazioni')->where('chiave', self::ARCHIVE_HUB_SETTINGS_KEY);
}
private function resolveArchiveHubSettingsModelClass(): ?string
{
$class = Impostazione::class;
if (! class_exists($class) || ! is_subclass_of($class, Model::class)) {
return null;
}
return $class;
}
private function normalizeAmazonPurchaseUrl(mixed $value): ?string
{
$input = trim((string) $value);
if ($input === '') {
return null;
}
$settings = $this->getArchiveHubSettings();
$baseUrl = rtrim((string) ($settings['amazon_base_url'] ?? 'https://www.amazon.it/dp'), '/');
if (preg_match('/^[A-Z0-9]{10}$/i', $input) === 1) {
$input = $baseUrl . '/' . strtoupper($input);
} elseif (! preg_match('#^https?://#i', $input)) {
$input = 'https://' . ltrim($input, '/');
}
$tag = trim((string) ($settings['amazon_affiliate_tag'] ?? ''));
if ($tag !== '' && ! str_contains($input, 'tag=')) {
$input .= (str_contains($input, '?') ? '&' : '?') . 'tag=' . rawurlencode($tag);
}
return $input;
}
public function applyGenericLabelPreset(string $presetKey): void
{
$preset = $this->getGenericLabelPresetDefinitions()[$presetKey] ?? null;
if (! is_array($preset)) {
return;
}
$this->genericLabelForm = array_merge($this->genericLabelForm, [
'preset_key' => $presetKey,
'mnemonic_code' => (string) ($preset['mnemonic_code'] ?? ($this->mnemonicCodeHint ?? 'GER00079')),
'titolo' => (string) ($preset['titolo'] ?? ''),
'linea_secondaria' => (string) ($preset['linea_secondaria'] ?? ''),
'tipo_item' => (string) ($preset['tipo_item'] ?? 'scatola'),
'supporto_fisico' => (string) ($preset['supporto_fisico'] ?? ''),
'modello_etichetta' => (string) ($preset['modello_etichetta'] ?? '11354'),
'template_name' => (string) ($preset['template_name'] ?? 'classic-right'),
'title_source' => (string) data_get($preset, 'sources.title', 'titolo'),
'subtitle_source' => (string) data_get($preset, 'sources.subtitle', 'linea_secondaria'),
'meta_source' => (string) data_get($preset, 'sources.meta', 'percorso_compatto'),
'note_source' => (string) data_get($preset, 'sources.note', 'note'),
'stable_line_one_source' => (string) ($preset['stable_line_one_source'] ?? 'stabile_summary'),
'stable_line_two_source' => (string) ($preset['stable_line_two_source'] ?? 'blank_line'),
'blank_lines_count' => max(0, min(4, (int) ($preset['blank_lines_count'] ?? 2))),
'qr_position' => (string) ($preset['qr_position'] ?? 'right-center'),
'qr_scale' => (string) ($preset['qr_scale'] ?? 'md'),
'mnemonic_font_size' => $this->normalizeGenericLabelFontSize((string) ($preset['mnemonic_font_size'] ?? 'xl')),
'title_font_size' => $this->normalizeGenericLabelFontSize((string) ($preset['title_font_size'] ?? 'xl')),
'subtitle_font_size' => $this->normalizeGenericLabelFontSize((string) ($preset['subtitle_font_size'] ?? 'md')),
'lines_font_size' => $this->normalizeGenericLabelFontSize((string) ($preset['lines_font_size'] ?? 'md')),
'note_font_size' => $this->normalizeGenericLabelFontSize((string) ($preset['note_font_size'] ?? 'sm')),
]);
$lineRows = is_array($preset['line_rows'] ?? null) ? $preset['line_rows'] : [];
for ($index = 1; $index <= 4; $index++) {
$row = $lineRows[$index - 1] ?? $this->getLegacyGenericLabelLineRow($index, $preset);
$this->genericLabelForm['line_' . $index . '_mode'] = $this->normalizeGenericLabelLineMode((string) ($row['mode'] ?? 'none'));
$this->genericLabelForm['line_' . $index . '_source'] = (string) ($row['source'] ?? 'none');
$this->genericLabelForm['line_' . $index . '_text'] = (string) ($row['text'] ?? '');
}
}
public function applySelectedGenericLabelPreset(): void
{
$this->applyGenericLabelPreset((string) ($this->genericLabelForm['preset_key'] ?? ''));
}
/** @return array<string, array<string, mixed>> */
private function getGenericLabelPresetDefinitions(): array
{
return array_merge(
$this->getBuiltInGenericLabelPresetDefinitions(),
$this->getStoredGenericLabelPresetDefinitions(),
);
}
/** @return array<string, array<string, mixed>> */
private function getBuiltInGenericLabelPresetDefinitions(): array
{
return [
'foglio-singolo' => [
'label' => 'Foglio / documento singolo',
'titolo' => 'FOGLIO ARCHIVIATO',
'linea_secondaria' => 'Documento cartaceo singolo',
'tipo_item' => 'foglio',
'supporto_fisico' => 'Foglio archiviato',
'modello_etichetta' => '11354',
'template_name' => 'compact-grid',
'qr_position' => 'right-top',
'qr_scale' => 'sm',
'sources' => [
'title' => 'titolo',
'subtitle' => 'linea_secondaria',
'meta' => 'codice_univoco',
'note' => 'percorso_compatto',
],
],
'cartellina-operativa' => [
'label' => 'Cartellina',
'titolo' => 'CARTELLINA OPERATIVA',
'linea_secondaria' => 'Primo nodo sotto-documento',
'tipo_item' => 'cartellina',
'supporto_fisico' => 'Cartellina archivio',
'modello_etichetta' => '11354',
'template_name' => 'classic-right',
'qr_position' => 'right-center',
'qr_scale' => 'md',
'sources' => [
'title' => 'titolo',
'subtitle' => 'stabile',
'meta' => 'percorso_compatto',
'note' => 'ubicazione',
],
],
'busta-anelli' => [
'label' => 'Busta ad anelli',
'titolo' => 'BUSTA AD ANELLI',
'linea_secondaria' => 'Nodo intermedio cartaceo',
'tipo_item' => 'busta_trasparente',
'supporto_fisico' => 'Busta trasparente ad anelli',
'modello_etichetta' => '11354',
'template_name' => 'compact-grid',
'qr_position' => 'right-top',
'qr_scale' => 'sm',
'sources' => [
'title' => 'titolo',
'subtitle' => 'linea_secondaria',
'meta' => 'tipo_label',
'note' => 'codice_univoco',
],
],
'scatola-contratti' => [
'label' => 'Scatola contratti',
'titolo' => 'SCATOLA CONTRATTI',
'linea_secondaria' => 'Fornitori / manutenzioni / polizze',
'tipo_item' => 'scatola',
'supporto_fisico' => 'Scatola archivio',
'modello_etichetta' => '11354',
'template_name' => 'classic-right',
'qr_position' => 'right-center',
'qr_scale' => 'md',
'sources' => [
'title' => 'titolo',
'subtitle' => 'linea_secondaria',
'meta' => 'percorso_compatto',
'note' => 'ubicazione',
],
],
'raccoglitore-verbali' => [
'label' => 'Raccoglitore verbali',
'titolo' => 'RACCOGLITORE VERBALI',
'linea_secondaria' => 'Assemblee / delibere / allegati',
'tipo_item' => 'faldone_anelli',
'supporto_fisico' => 'Raccoglitore ad anelli',
'modello_etichetta' => '99014',
'template_name' => 'band-top',
'qr_position' => 'bottom-right',
'qr_scale' => 'lg',
'sources' => [
'title' => 'titolo',
'subtitle' => 'stabile',
'meta' => 'linea_secondaria',
'note' => 'percorso_compatto',
],
],
'busta-documenti' => [
'label' => 'Busta documenti',
'titolo' => 'BUSTA DOCUMENTI',
'linea_secondaria' => 'Protocollo operativo',
'tipo_item' => 'busta_trasparente',
'supporto_fisico' => 'Busta trasparente',
'modello_etichetta' => '11354',
'template_name' => 'compact-grid',
'qr_position' => 'right-top',
'qr_scale' => 'sm',
'sources' => [
'title' => 'titolo',
'subtitle' => 'linea_secondaria',
'meta' => 'tipo_label',
'note' => 'codice_univoco',
],
],
'trasferimento-amministratore' => [
'label' => 'Trasferimento amministratore',
'titolo' => 'TRASFERIMENTO PROTOCOLLI',
'linea_secondaria' => 'Contenitore generale di passaggio consegne',
'tipo_item' => 'scatola',
'supporto_fisico' => 'Contenitore trasferimento amministratore',
'modello_etichetta' => '99014',
'template_name' => 'band-top',
'qr_position' => 'bottom-right',
'qr_scale' => 'lg',
'sources' => [
'title' => 'titolo',
'subtitle' => 'linea_secondaria',
'meta' => 'stabile',
'note' => 'percorso_compatto',
],
],
'magazzino-logistico' => [
'label' => 'Nodo logistico magazzino',
'titolo' => 'NODO LOGISTICO',
'linea_secondaria' => 'Magazzino / scaffale / ripiano',
'tipo_item' => 'posto',
'supporto_fisico' => 'Nodo logistico',
'modello_etichetta' => '99014',
'template_name' => 'classic-bottom',
'qr_position' => 'bottom-center',
'qr_scale' => 'lg',
'sources' => [
'title' => 'titolo',
'subtitle' => 'ubicazione',
'meta' => 'stabile',
'note' => 'note',
],
],
'scaffale-archivio' => [
'label' => 'Scaffale archivio',
'titolo' => 'SCAFFALE ARCHIVIO',
'linea_secondaria' => 'Contiene scatole e faldoni',
'tipo_item' => 'scaffale',
'supporto_fisico' => 'Scaffale',
'modello_etichetta' => '99014',
'template_name' => 'band-top',
'qr_position' => 'bottom-right',
'qr_scale' => 'lg',
'sources' => [
'title' => 'titolo',
'subtitle' => 'ubicazione',
'meta' => 'stabile',
'note' => 'percorso_compatto',
],
],
'posto-finale' => [
'label' => 'Posto / ubicazione finale',
'titolo' => 'POSTO ARCHIVIO',
'linea_secondaria' => 'Ultimo nodo della catena QR',
'tipo_item' => 'posto',
'supporto_fisico' => 'Ubicazione finale',
'modello_etichetta' => '99014',
'template_name' => 'classic-bottom',
'qr_position' => 'bottom-center',
'qr_scale' => 'lg',
'sources' => [
'title' => 'titolo',
'subtitle' => 'ubicazione',
'meta' => 'stabile',
'note' => 'note',
],
],
'faldone-archivio-99014' => [
'label' => 'Faldone archivio 99014',
'mnemonic_code' => (string) ($this->mnemonicCodeHint ?? 'GER00079'),
'titolo' => 'FALDONE ARCHIVIO',
'linea_secondaria' => 'Contenitore operativo con note manuali',
'tipo_item' => 'faldone_anelli',
'supporto_fisico' => 'Faldone con anelli',
'modello_etichetta' => '99014',
'template_name' => 'archive-99014',
'stable_line_one_source' => 'stabile_summary',
'stable_line_two_source' => 'blank_line',
'blank_lines_count' => 2,
'qr_position' => 'bottom-right',
'qr_scale' => 'lg',
'sources' => [
'title' => 'titolo',
'subtitle' => 'linea_secondaria',
'meta' => 'stabile_summary',
'note' => 'note',
],
],
];
}
/** @return array<string, array<string, mixed>> */
private function getStoredGenericLabelPresetDefinitions(): array
{
$settings = $this->getArchiveHubSettings();
$stored = $settings['generic_label_templates'] ?? [];
if (! is_array($stored)) {
return [];
}
return collect($stored)
->filter(fn(mixed $template): bool => is_array($template))
->map(function (array $template): array {
$template['label'] = trim((string) ($template['label'] ?? 'Template personalizzato'));
return $template;
})
->all();
}
/** @param array<string, array<string, mixed>> $stored */
private function makeGenericLabelTemplateKey(string $label, array $stored): string
{
$baseKey = 'custom-' . (Str::slug($label) ?: 'template');
$key = $baseKey;
$suffix = 2;
while (array_key_exists($key, $stored)) {
$key = $baseKey . '-' . $suffix;
$suffix++;
}
return $key;
}
/** @return array<string, mixed> */
private function buildGenericLabelTemplatePayload(string $label): array
{
return [
'label' => $label,
'mnemonic_code' => trim((string) ($this->genericLabelForm['mnemonic_code'] ?? '')),
'titolo' => trim((string) ($this->genericLabelForm['titolo'] ?? '')),
'linea_secondaria' => trim((string) ($this->genericLabelForm['linea_secondaria'] ?? '')),
'tipo_item' => (string) ($this->genericLabelForm['tipo_item'] ?? 'scatola'),
'supporto_fisico' => trim((string) ($this->genericLabelForm['supporto_fisico'] ?? '')),
'modello_etichetta' => (string) ($this->genericLabelForm['modello_etichetta'] ?? '11354'),
'template_name' => (string) ($this->genericLabelForm['template_name'] ?? 'classic-right'),
'stable_line_one_source' => (string) ($this->genericLabelForm['stable_line_one_source'] ?? 'stabile_summary'),
'stable_line_two_source' => (string) ($this->genericLabelForm['stable_line_two_source'] ?? 'blank_line'),
'blank_lines_count' => max(0, min(4, (int) ($this->genericLabelForm['blank_lines_count'] ?? 2))),
'qr_position' => (string) ($this->genericLabelForm['qr_position'] ?? 'right-center'),
'qr_scale' => (string) ($this->genericLabelForm['qr_scale'] ?? 'md'),
'mnemonic_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['mnemonic_font_size'] ?? 'xl')),
'title_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['title_font_size'] ?? 'xl')),
'subtitle_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['subtitle_font_size'] ?? 'md')),
'lines_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['lines_font_size'] ?? 'md')),
'note_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['note_font_size'] ?? 'sm')),
'line_rows' => $this->serializeGenericLabelLineRows(),
'sources' => [
'title' => (string) ($this->genericLabelForm['title_source'] ?? 'titolo'),
'subtitle' => (string) ($this->genericLabelForm['subtitle_source'] ?? 'linea_secondaria'),
'meta' => (string) ($this->genericLabelForm['meta_source'] ?? 'percorso_compatto'),
'note' => (string) ($this->genericLabelForm['note_source'] ?? 'note'),
],
];
}
/** @return array<string, mixed> */
private function getGenericLabelTemplateDefinition(string $templateName): array
{
$templates = [
'archive-99014' => ['qr_position' => 'bottom-right', 'accent' => 'slate', 'mode' => 'archive-99014'],
'classic-right' => ['qr_position' => 'right-center', 'accent' => 'slate'],
'classic-bottom' => ['qr_position' => 'bottom-center', 'accent' => 'sky'],
'band-top' => ['qr_position' => 'bottom-right', 'accent' => 'emerald'],
'compact-grid' => ['qr_position' => 'right-top', 'accent' => 'amber'],
];
return $templates[$templateName] ?? $templates['classic-right'];
}
/** @param array<string, string> $payload */
private function resolveGenericLabelTemplateValue(string $fieldKey, array $payload): string
{
if ($fieldKey === 'none') {
return '';
}
return trim((string) ($payload[$fieldKey] ?? ''));
}
/** @param array<string, string> $payload */
private function resolveGenericLabelStableLineValue(string $fieldKey, array $payload): string
{
if ($fieldKey === 'none') {
return '';
}
if ($fieldKey === 'blank_line') {
return '__BLANK_LINE__';
}
return trim((string) ($payload[$fieldKey] ?? ''));
}
/** @param array<string, string> $payload
* @return array<int, string>
*/
private function buildGenericLabelLineRows(array $payload): array
{
$rows = [];
for ($index = 1; $index <= 4; $index++) {
$mode = $this->normalizeGenericLabelLineMode((string) ($this->genericLabelForm['line_' . $index . '_mode'] ?? 'none'));
if ($mode === 'blank_line') {
$rows[] = '__BLANK_LINE__';
continue;
}
if ($mode === 'text') {
$rows[] = trim((string) ($this->genericLabelForm['line_' . $index . '_text'] ?? ''));
continue;
}
if ($mode === 'source') {
$rows[] = $this->resolveGenericLabelStableLineValue((string) ($this->genericLabelForm['line_' . $index . '_source'] ?? 'none'), $payload);
}
}
return $rows;
}
/** @return array<int, array<string, string>> */
private function serializeGenericLabelLineRows(): array
{
$rows = [];
for ($index = 1; $index <= 4; $index++) {
$rows[] = [
'mode' => $this->normalizeGenericLabelLineMode((string) ($this->genericLabelForm['line_' . $index . '_mode'] ?? 'none')),
'source' => (string) ($this->genericLabelForm['line_' . $index . '_source'] ?? 'none'),
'text' => trim((string) ($this->genericLabelForm['line_' . $index . '_text'] ?? '')),
];
}
return $rows;
}
/** @param array<string, mixed> $preset
* @return array<string, string>
*/
private function getLegacyGenericLabelLineRow(int $index, array $preset): array
{
if ($index === 1) {
return [
'mode' => 'source',
'source' => (string) ($preset['stable_line_one_source'] ?? 'stabile_summary'),
'text' => '',
];
}
if ($index === 2) {
$source = (string) ($preset['stable_line_two_source'] ?? 'blank_line');
return [
'mode' => $source === 'blank_line' ? 'blank_line' : 'source',
'source' => $source,
'text' => '',
];
}
$blankCount = max(0, min(4, (int) ($preset['blank_lines_count'] ?? 0)));
return [
'mode' => $index <= ($blankCount + 2) ? 'blank_line' : 'none',
'source' => 'none',
'text' => '',
];
}
private function normalizeGenericLabelLineMode(string $mode): string
{
return in_array($mode, ['source', 'text', 'blank_line', 'none'], true) ? $mode : 'none';
}
private function normalizeGenericLabelFontSize(string $size): string
{
return in_array($size, ['xs', 'sm', 'md', 'lg', 'xl'], true) ? $size : 'md';
}
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', 'scansione-documenti', 'etichette-generiche', 'movimentazione-codici', 'lettore-codici', 'template-drive', 'archivio-fisico', 'permessi'];
return in_array($tab, $allowed, true) ? $tab : 'protocollo-comunicazioni';
}
private function applyArchiveLookupInput(string $input): void
{
$value = trim($input);
if ($value === '') {
return;
}
$exactCode = $this->extractArchiveCodeFromInput($value);
$stabileId = (int) ($this->resolveActiveStabile()?->id ?? 0);
$item = DocumentoArchivioFisicoItem::query()
->when($stabileId > 0, fn(Builder $builder) => $builder->where('stabile_id', $stabileId))
->where(function (Builder $builder) use ($value, $exactCode): void {
$builder->where('codice_univoco', $value)
->orWhere('nfc_reference', $value)
->orWhere('qr_code_data', $value);
if ($exactCode !== '') {
$builder->orWhere('codice_univoco', $exactCode)
->orWhere('qr_code_data', 'like', '%AF:' . $exactCode . '%');
}
})
->orderByDesc('id')
->first();
if (! $item instanceof DocumentoArchivioFisicoItem) {
return;
}
$this->archiveLookupSelectedId = (int) $item->id;
$this->archiveLookupCode = $exactCode !== '' ? $exactCode : $value;
}
private function extractArchiveCodeFromInput(string $input): string
{
$input = trim($input);
if ($input === '') {
return '';
}
if (preg_match('/AF:([A-Z0-9\-]+)/i', $input, $match) === 1) {
return trim((string) ($match[1] ?? ''));
}
if (preg_match('/\b(AF-[A-Z0-9\-]+)\b/i', $input, $match) === 1) {
return trim((string) ($match[1] ?? ''));
}
return $input;
}
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);
$canaleAcquisizione = trim((string) ($data['canale_acquisizione'] ?? ''));
$scannerProfile = trim((string) ($data['scanner_profile'] ?? ''));
$riferimentoAcquisizione = trim((string) ($data['riferimento_acquisizione'] ?? ''));
$scannerNote = trim((string) ($data['scanner_note'] ?? ''));
$origineDocumento = trim((string) ($data['origine_documento'] ?? ''));
$documentQrProfiles = ArchiveQrXmlPayload::buildProfiles([
'id' => $archiveReference,
'legacy' => 'DOC:' . $archiveReference,
'type' => 'ATTO',
'stabile_id' => (string) $stabileId,
'parent' => trim((string) ($data['supporto_fisico'] ?? '')) !== '' ? strtoupper(Str::slug((string) $data['supporto_fisico'], '-')) : 'RADICE',
'code' => $archiveReference,
'title' => $titolo,
'document_code' => $protocollo,
'archive_class' => strtoupper($archivioClasse !== '' ? $archivioClasse : $categoria),
'location' => trim((string) ($data['supporto_fisico'] ?? '')),
]);
if ($scannerNote !== '') {
$note = trim($note . ($note !== '' ? "\n" : '') . 'Scanner: ' . $scannerNote);
}
$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,
'codice_univoco_documento' => $archiveReference,
'canale_acquisizione' => $canaleAcquisizione !== '' ? $canaleAcquisizione : null,
'scanner_profile' => $scannerProfile !== '' ? $scannerProfile : null,
'riferimento_acquisizione' => $riferimentoAcquisizione !== '' ? $riferimentoAcquisizione : null,
'origine_documento' => $origineDocumento !== '' ? $origineDocumento : null,
'qr_ready' => true,
'nfc_ready' => true,
'qr_payload_xml' => (string) ($documentQrProfiles['xml_open'] ?? ''),
'qr_payload_zip' => (string) ($documentQrProfiles['xml_zip'] ?? ''),
'nfc_payload_xml' => (string) ($documentQrProfiles['xml_nfc'] ?? ''),
],
'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 hasStoredFile(Documento $doc): bool
{
foreach (array_filter([(string) ($doc->percorso_file ?? ''), (string) ($doc->path_file ?? '')]) as $candidate) {
if (Storage::disk('local')->exists($candidate) || Storage::disk('public')->exists($candidate)) {
return true;
}
}
return false;
}
/** @return array{has_file:bool,is_pdf:bool,can_print:bool,extension:string,kind_label:string,badge_label:string} */
private function resolveDocumentoMediaMeta(Documento $documento): array
{
$filename = trim((string) ($documento->nome_file ?: basename((string) ($documento->percorso_file ?: $documento->path_file ?: ''))));
$extension = strtolower((string) pathinfo($filename, PATHINFO_EXTENSION));
$hasFile = $this->hasStoredFile($documento);
$imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg'];
$textExtensions = ['txt', 'csv', 'md', 'json', 'xml', 'html'];
$officeExtensions = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods'];
$isPdf = $this->hasPdf($documento) || $extension === 'pdf';
$isImage = in_array($extension, $imageExtensions, true);
$isText = in_array($extension, $textExtensions, true);
$isOffice = in_array($extension, $officeExtensions, true);
$kindLabel = match (true) {
$isPdf => 'PDF',
$isImage => 'Immagine',
$isOffice => 'Office',
$isText => 'Testo',
$extension !== '' => strtoupper($extension),
default => 'File',
};
return [
'has_file' => $hasFile,
'is_pdf' => $isPdf,
'can_print' => $hasFile && ($isPdf || $isImage || $isText),
'extension' => $extension !== '' ? strtoupper($extension) : 'FILE',
'kind_label' => $kindLabel,
'badge_label' => $kindLabel . ($extension !== '' && strtoupper($extension) !== $kindLabel ? ' · ' . strtoupper($extension) : ''),
];
}
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;
}
}