3541 lines
154 KiB
PHP
3541 lines
154 KiB
PHP
<?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 l’elenco 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;
|
||
}
|
||
}
|