Add scanner document acquisition tab

This commit is contained in:
michele 2026-04-23 14:09:44 +00:00
parent 49a49cdb08
commit c2dc58f0b9
3 changed files with 799 additions and 53 deletions

View File

@ -4,6 +4,7 @@
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;
@ -34,11 +35,15 @@
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;
private const ARCHIVE_HUB_SETTINGS_KEY = 'documenti.archivio_hub.settings';
#[Url( as : 'tab')]
public string $hubTab = 'protocollo-comunicazioni';
@ -52,10 +57,18 @@ class DocumentiArchivio extends Page implements HasTable
#[Url( as : 'folder')]
public ?int $currentFolderId = null;
public array $scannerAcquisitionForm = [];
public $scannerPdf = null;
public array $genericLabelForm = [];
public array $physicalArchiveForm = [];
public array $physicalArchiveMoveForm = [];
public array $driveTemplateForm = [];
public string $physicalArchiveSearch = '';
public string $physicalArchiveTypeFilter = '';
@ -88,6 +101,9 @@ public function mount(): void
$this->hubTab = $this->normalizeHubTab($this->hubTab);
$this->syncInvoiceFolders();
$this->initializePhysicalArchiveForms();
$this->initializeScannerAcquisitionForm();
$this->initializeGenericLabelForm();
$this->initializeDriveTemplateForm();
$this->mountInteractsWithTable();
}
@ -98,6 +114,14 @@ public function selectHubTab(string $tab): void
if ($this->hubTab === 'archivio-digitale') {
$this->syncInvoiceFolders();
}
if ($this->hubTab === 'scansione-documenti') {
$this->initializeScannerAcquisitionForm();
}
if ($this->hubTab === 'template-drive') {
$this->initializeDriveTemplateForm();
}
}
public function setProtocolloView(string $view): void
@ -105,6 +129,94 @@ 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 [
@ -524,9 +636,10 @@ public function getDigitalArchiveBreadcrumbsProperty(): array
/** @return array<int, string> */
public function getDriveTemplateFoldersProperty(): array
{
$items = config('netgescon.google.drive_template_folders', []);
$yearRoot = trim((string) config('netgescon.google.drive_template_year_root', ''));
$yearModel = trim((string) config('netgescon.google.drive_template_year_model', ''));
$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) !== ''))
@ -541,6 +654,52 @@ public function getDriveTemplateFoldersProperty(): array
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 {
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', ''),
];
})
->values()
->all();
}
/** @return array<int, string> */
public function getAudienceGroupsProperty(): array
{
@ -822,6 +981,104 @@ public function savePhysicalArchiveEntry(): void
->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,
],
]);
$this->initializeGenericLabelForm();
Notification::make()
->title('Etichetta generica pronta')
->body('Codice: ' . $item->codice_univoco)
->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'] ?? '')),
];
Impostazione::query()->updateOrCreate(
['chiave' => self::ARCHIVE_HUB_SETTINGS_KEY],
[
'valore' => json_encode($settings, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
'descrizione' => '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);
@ -1206,6 +1463,118 @@ private function initializePhysicalArchiveForms(): void
];
}
private function initializeGenericLabelForm(): void
{
$this->genericLabelForm = [
'titolo' => '',
'linea_secondaria' => '',
'note' => '',
'tipo_item' => 'scatola',
'supporto_fisico' => 'Scatola',
'parent_id' => null,
'magazzino' => '',
'scaffale' => '',
'ripiano' => '',
'ubicazione_dettaglio' => '',
'modello_etichetta' => '11354',
'amazon_url' => '',
];
}
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' => '',
'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' => '',
];
}
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',
];
$stored = Impostazione::query()->where('chiave', self::ARCHIVE_HUB_SETTINGS_KEY)->value('valore');
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 Impostazione::query()->where('chiave', self::ARCHIVE_HUB_SETTINGS_KEY)->exists();
}
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;
}
private function getDefaultStabileId(): ?int
{
$user = Auth::user();
@ -1390,7 +1759,7 @@ private function formatPhysicalArchiveLocation(Documento $record): string
private function normalizeHubTab(string $tab): string
{
$allowed = ['protocollo-comunicazioni', 'archivio-digitale', 'template-drive', 'archivio-fisico', 'permessi'];
$allowed = ['protocollo-comunicazioni', 'archivio-digitale', 'scansione-documenti', 'etichette-generiche', 'movimentazione-codici', 'template-drive', 'archivio-fisico', 'permessi'];
return in_array($tab, $allowed, true) ? $tab : 'protocollo-comunicazioni';
}
@ -1631,6 +2000,15 @@ private function createDocumentoFromForm(array $data, bool $isDemo): void
$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'] ?? ''));
if ($scannerNote !== '') {
$note = trim($note . ($note !== '' ? "\n" : '') . 'Scanner: ' . $scannerNote);
}
$doc = Documento::query()->create([
'documentable_type' => null,
@ -1666,6 +2044,10 @@ private function createDocumentoFromForm(array $data, bool $isDemo): void
'periodo_da' => $data['periodo_riferimento_da'] ?? null,
'periodo_a' => $data['periodo_riferimento_a'] ?? null,
'codice_etichetta' => $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,
],

View File

@ -21,6 +21,33 @@ ## Stato attuale archivio documenti
- visibilita per ruolo, gruppo e utente singolo;
- estrazione testo best-effort per ricerca tramite `PdfTextExtractionService`.
## Tab scansione documenti
La pagina `Strumenti > Documenti` include ora una tab dedicata chiamata `Scansione documenti`.
Questa tab copre il flusso subito testabile in staging senza dipendere da un comando diretto browser -> scanner:
- upload del PDF prodotto dallo scanner;
- selezione stabile, cartella archivio, categoria e classe archivio;
- tracciamento del canale di acquisizione;
- salvataggio del riferimento batch o sessione;
- riuso dello stesso motore documento e della stessa pipeline OCR gia presenti.
Metadati aggiuntivi salvati nel documento:
- `canale_acquisizione`;
- `scanner_profile`;
- `riferimento_acquisizione`;
- `origine_documento`.
Canali previsti nella prima versione operativa:
- upload PDF da scanner;
- scanner Epson verso cartella o rete;
- scanner via email;
- upload mobile o camera;
- acquisizione manuale.
## Dashboard protocollo comunicazioni
La pagina Strumenti > Documenti ora apre di default una prima tab dedicata al protocollo comunicazioni.
@ -231,7 +258,7 @@ ## Scanner Epson da pagina web
Per operativita immediata la soluzione piu semplice e:
1. scansione verso cartella controllata;
2. upload/ingestione rapida in NetGescon;
2. upload/ingestione rapida nella tab `Scansione documenti` di NetGescon;
3. OCR e archiviazione automatica.
Per il modello Epson WF-C5710 la direzione pratica da testare non e il browser puro, ma una di queste due:
@ -268,9 +295,10 @@ ## Dymo LAN 11354
## Prossimi passi consigliati
1. test reale OCR e ricerca su 2-3 PDF campione;
2. attivazione watermark PDF in archiviazione;
3. tabella short links con dominio `bcards.it`;
4. bridge locale PC per ACR122U;
5. prova Dymo LAN con etichetta 11354;
6. definizione modulo presenza e voto assemblea.
1. test reale OCR e ricerca su 2-3 PDF campione caricati dalla tab `Scansione documenti`;
2. prova con un PDF generato da Epson ADF e verifica dei metadati `canale_acquisizione` e `riferimento_acquisizione`;
3. attivazione watermark PDF in archiviazione;
4. tabella short links con dominio `bcards.it`;
5. bridge locale PC per ACR122U;
6. prova Dymo LAN con etichetta 11354;
7. definizione modulo presenza e voto assemblea.

View File

@ -3,6 +3,9 @@
$tabs = [
'protocollo-comunicazioni' => 'Protocollo comunicazioni',
'archivio-digitale' => 'Archivio digitale',
'scansione-documenti' => 'Scansione documenti',
'etichette-generiche' => 'Etichette generiche',
'movimentazione-codici' => 'Movimentazione per codice',
'template-drive' => 'Template Drive',
'archivio-fisico' => 'Archivio fisico',
'permessi' => 'Permessi e cartelle',
@ -144,20 +147,6 @@
<div class="grid gap-4 md:grid-cols-2 2xl:grid-cols-3">
@forelse($this->protocolloComunicazioniRows as $row)
<article class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm">
<div class="aspect-[4/3] border-b border-slate-200 bg-slate-100">
@if($row['has_pdf'])
<iframe
src="{{ $row['preview_url'] }}#toolbar=0&navpanes=0&scrollbar=0"
class="h-full w-full"
loading="lazy"
title="Anteprima {{ $row['protocollo'] }}"
></iframe>
@else
<div class="flex h-full items-center justify-center px-6 text-center text-sm text-slate-500">
Anteprima PDF non disponibile per questo protocollo.
</div>
@endif
</div>
<div class="space-y-3 p-4">
<div class="flex flex-wrap items-center gap-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
<span class="rounded-full bg-slate-100 px-2.5 py-1 text-slate-700">{{ $row['protocollo'] }}</span>
@ -306,21 +295,390 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
</div>
</div>
</x-filament::section>
@elseif($this->hubTab === 'scansione-documenti')
<x-filament::section>
<x-slot name="heading">Scansione documenti</x-slot>
<x-slot name="description">Acquisizione staging-ready per PDF generati da scanner Epson, cartelle di rete o caricamento manuale. Il salvataggio usa lo stesso motore documento, quindi protocollo, archivio e OCR restano allineati.</x-slot>
<div class="grid gap-4 xl:grid-cols-[minmax(0,0.95fr)_minmax(0,1.05fr)]">
<div class="rounded-xl border bg-white p-4">
<div class="flex items-center justify-between gap-3">
<div>
<div class="text-sm font-semibold text-slate-900">Nuova acquisizione scanner</div>
<div class="mt-1 text-xs text-slate-500">Carica il PDF prodotto dallo scanner, assegna subito stabile, cartella e classe archivio, poi lascia al backend rinomina, protocollo e OCR.</div>
</div>
<div class="rounded-full bg-emerald-50 px-3 py-1 text-xs font-semibold text-emerald-700">OCR automatico</div>
</div>
<div class="mt-4 grid gap-3 md:grid-cols-2">
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Stabile</label>
<select wire:model.live="scannerAcquisitionForm.stabile_id" class="w-full rounded-lg border-slate-200 text-sm shadow-sm">
<option value="">Seleziona stabile</option>
@foreach($this->scannerAcquisitionStabiliOptions as $stabileId => $stabileLabel)
<option value="{{ $stabileId }}">{{ $stabileLabel }}</option>
@endforeach
</select>
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Canale acquisizione</label>
<select wire:model.defer="scannerAcquisitionForm.canale_acquisizione" class="w-full rounded-lg border-slate-200 text-sm shadow-sm">
@foreach($this->getScannerAcquisitionChannelOptions() as $channelKey => $channelLabel)
<option value="{{ $channelKey }}">{{ $channelLabel }}</option>
@endforeach
</select>
</div>
<div class="md:col-span-2">
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">PDF scansione</label>
<input type="file" wire:model="scannerPdf" accept="application/pdf" class="block w-full rounded-lg border border-slate-200 bg-white text-sm text-slate-700 shadow-sm file:mr-4 file:rounded-md file:border-0 file:bg-slate-900 file:px-3 file:py-2 file:text-sm file:font-medium file:text-white" />
<div wire:loading wire:target="scannerPdf" class="mt-2 text-xs text-slate-500">Caricamento PDF in corso...</div>
</div>
<div class="md:col-span-2">
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Titolo documento</label>
<input type="text" wire:model.defer="scannerAcquisitionForm.titolo" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. Contratto manutenzione ascensore 2025" />
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Categoria</label>
<select wire:model.defer="scannerAcquisitionForm.categoria" class="w-full rounded-lg border-slate-200 text-sm shadow-sm">
@foreach($this->scannerCategoryOptions as $categoryKey => $categoryLabel)
<option value="{{ $categoryKey }}">{{ $categoryLabel }}</option>
@endforeach
</select>
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Classe archivio</label>
<select wire:model.defer="scannerAcquisitionForm.archivio_classe" class="w-full rounded-lg border-slate-200 text-sm shadow-sm">
@foreach($this->getArchivioClasseOptions() as $classKey => $classLabel)
<option value="{{ $classKey }}">{{ $classLabel }}</option>
@endforeach
</select>
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Cartella archivio</label>
<select wire:model.defer="scannerAcquisitionForm.cartella_archivio_id" class="w-full rounded-lg border-slate-200 text-sm shadow-sm">
<option value="">Cartella automatica</option>
@foreach($this->scannerAcquisitionFolderOptions as $folderId => $folderLabel)
<option value="{{ $folderId }}">{{ $folderLabel }}</option>
@endforeach
</select>
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Visibilità</label>
<select wire:model.defer="scannerAcquisitionForm.visibility_scope" class="w-full rounded-lg border-slate-200 text-sm shadow-sm">
@foreach($this->scannerVisibilityScopeOptions as $scopeKey => $scopeLabel)
<option value="{{ $scopeKey }}">{{ $scopeLabel }}</option>
@endforeach
</select>
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Fornitore / mittente</label>
<input type="text" wire:model.defer="scannerAcquisitionForm.fornitore" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. OTIS" />
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Profilo scanner</label>
<input type="text" wire:model.defer="scannerAcquisitionForm.scanner_profile" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. Epson ADF fronte-retro 300dpi" />
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Data documento</label>
<input type="date" wire:model.defer="scannerAcquisitionForm.data_documento" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" />
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Riferimento batch</label>
<input type="text" wire:model.defer="scannerAcquisitionForm.riferimento_acquisizione" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. EPSON-ADF-20250308-01" />
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Faldone / raccoglitore</label>
<input type="text" wire:model.defer="scannerAcquisitionForm.faldone" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. Faldone contratti ascensori" />
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Supporto fisico</label>
<input type="text" wire:model.defer="scannerAcquisitionForm.supporto_fisico" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. Cartella trasparente" />
</div>
<div class="md:col-span-2">
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Descrizione operativa</label>
<textarea wire:model.defer="scannerAcquisitionForm.descrizione" rows="3" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Oggetto del documento, note utili per ricerca e protocollo..."></textarea>
</div>
<div class="md:col-span-2">
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Note scanner</label>
<textarea wire:model.defer="scannerAcquisitionForm.scanner_note" rows="3" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. PDF creato da cartella di rete EPSON, 22 pagine, fronte-retro"></textarea>
</div>
</div>
<div class="mt-4 flex flex-wrap gap-2">
<button type="button" wire:click="saveScannedDocument" class="inline-flex items-center rounded-lg bg-slate-900 px-4 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-slate-800">Acquisisci documento scanner</button>
</div>
</div>
<div class="space-y-4">
<div class="rounded-xl border bg-white p-4">
<div class="text-sm font-semibold text-slate-900">Flusso pratico per staging</div>
<div class="mt-3 grid gap-3 text-sm text-slate-600 md:grid-cols-2">
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3">Lo scanner genera un PDF. Qui lo carichi una sola volta e il sistema lo rinomina secondo stabile, anno e protocollo.</div>
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3">I metadati di acquisizione restano nel documento per distinguere upload manuale, cartella Epson, email o mobile.</div>
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3">La pipeline OCR esistente parte subito dopo il salvataggio, senza creare un ramo tecnico separato.</div>
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3">La cartella archivio puo essere fissata subito oppure lasciata automatica in base a categoria e anno.</div>
</div>
</div>
<div class="rounded-xl border border-amber-200 bg-amber-50 p-4">
<div class="text-sm font-semibold text-amber-900">EPSON e browser</div>
<div class="mt-2 text-sm text-amber-800">Per la prova staging non serve pilotare lo scanner dal browser. Il percorso operativo corretto resta: scansione su PDF dal dispositivo, poi acquisizione da questa tab con OCR e metadati gia coerenti col resto dell'archivio.</div>
</div>
<div class="rounded-xl border bg-white p-4">
<div class="text-sm font-semibold text-slate-900">Effetti del salvataggio</div>
<div class="mt-3 space-y-2 text-sm text-slate-600">
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3">Numero protocollo generato automaticamente in base a categoria e anno del documento.</div>
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3">Rinominazione ordinata del file nello storage locale del progetto.</div>
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3">Metadati archivio pronti per QR, collocazione fisica e ricerche OCR successive.</div>
</div>
</div>
</div>
</div>
</x-filament::section>
@elseif($this->hubTab === 'etichette-generiche')
<x-filament::section>
<x-slot name="heading">Etichette generiche per contenitori</x-slot>
<x-slot name="description">Tab dedicata alla stampa rapida di etichette per faldoni, scatole, raccoglitori e buste. Il QR resta obbligatorio e il codice generato diventa il riferimento stabile per annidamento e scansione.</x-slot>
<div class="grid gap-4 xl:grid-cols-[minmax(0,0.95fr)_minmax(0,1.05fr)]">
<div class="rounded-xl border bg-white p-4">
<div class="flex items-center justify-between gap-3">
<div>
<div class="text-sm font-semibold text-slate-900">Nuova etichetta generica</div>
<div class="mt-1 text-xs text-slate-500">Crea il contenitore, stampa subito e poi usalo come nodo della catena QR: fattura, cartella, dock, scatola, magazzino, scaffale.</div>
</div>
<div class="rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold text-slate-700">QR incluso</div>
</div>
<div class="mt-4 grid gap-3 md:grid-cols-2">
<div class="md:col-span-2">
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Titolo etichetta</label>
<input type="text" wire:model.defer="genericLabelForm.titolo" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. SCATOLA CONTRATTI ASCENSORI" />
</div>
<div class="md:col-span-2">
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Seconda riga</label>
<input type="text" wire:model.defer="genericLabelForm.linea_secondaria" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. Stabile Roma 12 · 2024/2026" />
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Tipologia</label>
<select wire:model.defer="genericLabelForm.tipo_item" class="w-full rounded-lg border-slate-200 text-sm shadow-sm">
@foreach($this->physicalArchiveTypeOptions as $typeKey => $typeLabel)
@if($typeKey !== 'documento')
<option value="{{ $typeKey }}">{{ $typeLabel }}</option>
@endif
@endforeach
</select>
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Modello etichetta</label>
<select wire:model.defer="genericLabelForm.modello_etichetta" class="w-full rounded-lg border-slate-200 text-sm shadow-sm">
@foreach($this->getModelliEtichettaOptions() as $labelKey => $labelText)
<option value="{{ $labelKey }}">{{ $labelText }}</option>
@endforeach
</select>
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Dentro contenitore</label>
<select wire:model.defer="genericLabelForm.parent_id" class="w-full rounded-lg border-slate-200 text-sm shadow-sm">
<option value="">Radice archivio</option>
@foreach($this->physicalArchiveParentOptions as $parentId => $parentLabel)
<option value="{{ $parentId }}">{{ $parentLabel }}</option>
@endforeach
</select>
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Supporto</label>
<input type="text" wire:model.defer="genericLabelForm.supporto_fisico" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. Scatola o raccoglitore" />
</div>
<div class="md:col-span-2">
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Link acquisto / Amazon</label>
<input type="text" wire:model.defer="genericLabelForm.amazon_url" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="URL completo o ASIN Amazon" />
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Magazzino</label>
<input type="text" wire:model.defer="genericLabelForm.magazzino" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. Deposito EUR" />
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Scaffale</label>
<input type="text" wire:model.defer="genericLabelForm.scaffale" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. B2" />
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Ripiano</label>
<input type="text" wire:model.defer="genericLabelForm.ripiano" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. 4" />
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Dettaglio ubicazione</label>
<input type="text" wire:model.defer="genericLabelForm.ubicazione_dettaglio" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. corridoio destro" />
</div>
<div class="md:col-span-2">
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Nota breve</label>
<textarea wire:model.defer="genericLabelForm.note" rows="3" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Contenuto, periodo, riferimenti operativi..."></textarea>
</div>
</div>
<div class="mt-4 flex flex-wrap gap-2">
<button type="button" wire:click="saveGenericArchiveLabel" class="inline-flex items-center rounded-lg bg-slate-900 px-4 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-slate-800">Crea e prepara la stampa</button>
</div>
</div>
<div class="space-y-4">
<div class="rounded-xl border bg-white p-4">
<div class="text-sm font-semibold text-slate-900">Standard operativo</div>
<div class="mt-3 grid gap-3 text-sm text-slate-600 md:grid-cols-2">
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3">Il codice univoco viene generato automaticamente e diventa il nodo da scansionare.</div>
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3">Il QR usa lo stesso schema pubblico gia host-agnostic del resto dell'archivio fisico.</div>
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3">Il link Amazon puo essere salvato come URL o come ASIN, con tag affiliato aggiunto dal super-admin.</div>
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3">Questa tab rende visibile la nuova etichetta senza doverla cercare dentro il registro generale.</div>
</div>
</div>
<div class="rounded-xl border bg-white p-4">
<div class="flex items-center justify-between gap-3">
<div class="text-sm font-semibold text-slate-900">Ultime etichette generiche</div>
<div class="text-xs text-slate-500">Stampa immediata 11354 / 99014</div>
</div>
<div class="mt-4 space-y-3">
@forelse($this->genericLabelRows as $row)
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3">
<div class="flex flex-col gap-3 xl:flex-row xl:items-start xl:justify-between">
<div>
<div class="flex flex-wrap items-center gap-2">
<span class="rounded-full bg-slate-900 px-2.5 py-1 text-[11px] font-semibold text-white">{{ $row['codice_univoco'] }}</span>
<span class="rounded-full bg-white px-2.5 py-1 text-[11px] font-medium text-slate-700">{{ $row['tipo'] }}</span>
</div>
<div class="mt-2 text-sm font-semibold text-slate-900">{{ $row['titolo'] }}</div>
@if($row['linea_secondaria'] !== '')
<div class="mt-1 text-xs text-slate-500">{{ $row['linea_secondaria'] }}</div>
@endif
<div class="mt-1 text-xs text-slate-500">Percorso: {{ $row['percorso'] }}</div>
@if($row['ubicazione'] !== '')
<div class="mt-1 text-xs text-slate-500">Ubicazione: {{ $row['ubicazione'] }}</div>
@endif
@if($row['amazon_url'] !== '')
<div class="mt-2"><a href="{{ $row['amazon_url'] }}" target="_blank" class="text-xs font-medium text-sky-700 underline decoration-sky-300 underline-offset-2">Apri link acquisto</a></div>
@endif
</div>
<div class="flex shrink-0 flex-wrap items-center gap-2">
<a href="{{ route('filament.archivio-fisico.etichetta', ['item' => $row['id']]) }}?format=11354&autoprint=1&dymo_fix=0&dymo_rot=0&dymo_dx=0&dymo_dy=-1" target="_blank" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-medium text-slate-700 shadow-sm hover:bg-slate-50">11354</a>
<a href="{{ route('filament.archivio-fisico.etichetta', ['item' => $row['id']]) }}?format=11354&autoprint=1&dymo_fix=1&dymo_rot=90&dymo_dx=0&dymo_dy=0" target="_blank" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-medium text-slate-700 shadow-sm hover:bg-slate-50">11354 verticale</a>
<a href="{{ route('filament.archivio-fisico.etichetta', ['item' => $row['id']]) }}?format=99014&autoprint=1&dymo_fix=0&dymo_rot=0&dymo_dx=3.5&dymo_dy=3.5" target="_blank" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-medium text-slate-700 shadow-sm hover:bg-slate-50">99014</a>
</div>
</div>
</div>
@empty
<div class="rounded-lg border border-dashed border-slate-300 bg-white px-4 py-6 text-sm text-slate-500">Nessuna etichetta generica creata ancora per lo stabile attivo.</div>
@endforelse
</div>
</div>
</div>
</div>
</x-filament::section>
@elseif($this->hubTab === 'movimentazione-codici')
<x-filament::section>
<x-slot name="heading">Movimentazione per codice</x-slot>
<x-slot name="description">Pagina autonoma per spostare contenitori e documenti tramite codice o scansione. Serve da base alla catena annidata e ai futuri flussi con scanner EPSON.</x-slot>
<div class="grid gap-4 xl:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)]">
<div class="space-y-4">
<div class="rounded-xl border bg-white p-4">
<div class="text-sm font-semibold text-slate-900">Sposta per codice</div>
<div class="mt-1 text-xs text-slate-500">Scansiona o inserisci codice sorgente e codice destinazione. Il contenitore padre viene aggiornato senza dover aprire il registro generale.</div>
<div class="mt-4 grid gap-3 md:grid-cols-2">
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Codice sorgente</label>
<input type="text" wire:model.defer="physicalArchiveMoveForm.source_code" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. AF-20-0001" />
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Codice destinazione</label>
<input type="text" wire:model.defer="physicalArchiveMoveForm.target_code" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. AF-20-0010" />
</div>
</div>
<div class="mt-4 flex flex-wrap gap-2">
<button type="button" wire:click="movePhysicalArchiveItem" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm transition hover:border-slate-400 hover:bg-slate-50">Registra movimentazione</button>
</div>
</div>
<div class="rounded-xl border border-amber-200 bg-amber-50 p-4">
<div class="text-sm font-semibold text-amber-900">Catena QR prevista</div>
<div class="mt-2 text-sm text-amber-800">QR Fattura -> QR Cartella trasparente -> QR Dock -> QR Scatola -> QR Magazzino -> QR Scaffale. Questa tab separa il movimento dal censimento del contenuto.</div>
</div>
</div>
<div class="rounded-xl border bg-white p-4">
<div class="text-sm font-semibold text-slate-900">Contenitori di primo livello</div>
<div class="mt-3 grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
@forelse($this->physicalArchiveContainerTree as $item)
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3">
<div class="text-sm font-semibold text-slate-900">{{ $item['codice_univoco'] }}</div>
<div class="mt-1 text-sm text-slate-700">{{ $item['titolo'] }}</div>
<div class="mt-1 text-xs text-slate-500">{{ $item['tipo'] }} · {{ $item['children_count'] }} elementi interni</div>
</div>
@empty
<div class="rounded-lg border border-dashed border-slate-300 bg-white px-4 py-6 text-sm text-slate-500 sm:col-span-2 xl:col-span-3">Nessun contenitore creato ancora per lo stabile attivo.</div>
@endforelse
</div>
</div>
</div>
</x-filament::section>
@elseif($this->hubTab === 'template-drive')
<x-filament::section>
<x-slot name="heading">Template cartelle Drive</x-slot>
<x-slot name="description">Questa struttura resta la base comune per archivio digitale, passaggio consegne e futura sincronizzazione Google.</x-slot>
<x-slot name="description">Questa struttura resta la base comune per archivio digitale, passaggio consegne e futura sincronizzazione Google. Ora puo essere gestita da database a livello super-admin, con fallback automatico alla config storica.</x-slot>
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
@foreach($this->driveTemplateFolders as $folder)
<div class="rounded-xl border bg-slate-50 px-4 py-3 text-sm text-slate-700">{{ $folder }}</div>
@endforeach
<div class="space-y-4">
<div class="rounded-xl border bg-white p-4">
<div class="flex flex-col gap-3 xl:flex-row xl:items-start xl:justify-between">
<div>
<div class="text-sm font-semibold text-slate-900">Origine struttura</div>
<div class="mt-1 text-xs text-slate-500">Fonte attuale: <span class="font-semibold text-slate-900">{{ $this->driveTemplateSource === 'database' ? 'database' : 'config' }}</span></div>
</div>
<div class="rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold text-slate-700">{{ count($this->driveTemplateFolders) }} cartelle logiche</div>
</div>
@if($this->canManageDriveTemplates)
<div class="mt-4 grid gap-3 md:grid-cols-2">
<div class="md:col-span-2">
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Cartelle base</label>
<textarea wire:model.defer="driveTemplateForm.folders_text" rows="10" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Una cartella per riga"></textarea>
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Radice annuale</label>
<input type="text" wire:model.defer="driveTemplateForm.year_root" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. archivio_per_anno" />
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Modello anno</label>
<input type="text" wire:model.defer="driveTemplateForm.year_model" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. _anno_modello" />
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Amazon tag affiliato</label>
<input type="text" wire:model.defer="driveTemplateForm.amazon_affiliate_tag" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. netgescon-21" />
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Amazon base URL</label>
<input type="text" wire:model.defer="driveTemplateForm.amazon_base_url" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. https://www.amazon.it/dp" />
</div>
</div>
<div class="mt-4 flex flex-wrap gap-2">
<button type="button" wire:click="saveDriveTemplateSettings" class="inline-flex items-center rounded-lg bg-slate-900 px-4 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-slate-800">Salva template su database</button>
</div>
@else
<div class="mt-4 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">La modifica del template e dei parametri Amazon e riservata al super-admin. Gli altri ruoli vedono comunque la struttura effettiva.</div>
@endif
</div>
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
@foreach($this->driveTemplateFolders as $folder)
<div class="rounded-xl border bg-slate-50 px-4 py-3 text-sm text-slate-700">{{ $folder }}</div>
@endforeach
</div>
</div>
</x-filament::section>
@elseif($this->hubTab === 'archivio-fisico')
<x-filament::section>
<x-slot name="heading">Archivio fisico</x-slot>
<x-slot name="description">Registro operativo del cartaceo: inserimento manuale, contenitori annidati, movimentazione per codice e stampa etichette 11354 o 99014.</x-slot>
<x-slot name="description">Registro operativo del cartaceo: inserimento manuale, contenitori annidati e collegamento al digitale. La stampa rapida delle etichette generiche e la movimentazione hanno ora una tab dedicata.</x-slot>
<div class="grid gap-4 xl:grid-cols-[minmax(0,1.25fr)_minmax(0,0.75fr)]">
<div class="space-y-4">
@ -421,28 +779,6 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
</div>
</div>
<div class="rounded-xl border bg-white p-4">
<div class="flex items-center justify-between gap-3">
<div>
<div class="text-sm font-semibold text-slate-900">Movimentazione per codice</div>
<div class="mt-1 text-xs text-slate-500">Scansioni o inserisci il codice sorgente e il contenitore di arrivo per spostare intere buste o faldoni.</div>
</div>
</div>
<div class="mt-4 grid gap-3 md:grid-cols-2">
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Codice sorgente</label>
<input type="text" wire:model.defer="physicalArchiveMoveForm.source_code" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. AF-20-0001" />
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Codice destinazione</label>
<input type="text" wire:model.defer="physicalArchiveMoveForm.target_code" class="w-full rounded-lg border-slate-200 text-sm shadow-sm" placeholder="Es. AF-20-0010" />
</div>
</div>
<div class="mt-4 flex flex-wrap gap-2">
<button type="button" wire:click="movePhysicalArchiveItem" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm transition hover:border-slate-400 hover:bg-slate-50">Registra movimentazione</button>
</div>
</div>
<div class="rounded-xl border bg-white p-4">
<div class="text-sm font-semibold text-slate-900">Contenitori di primo livello</div>
<div class="mt-3 grid gap-3 sm:grid-cols-2 xl:grid-cols-3">