From c2dc58f0b93873aeb49917eb9e7dde7041697902 Mon Sep 17 00:00:00 2001 From: michele Date: Thu, 23 Apr 2026 14:09:44 +0000 Subject: [PATCH] Add scanner document acquisition tab --- .../Pages/Strumenti/DocumentiArchivio.php | 390 +++++++++++++++- docs/DOCUMENTI-NFC-E-ACQUISIZIONE.md | 42 +- .../strumenti/documenti-archivio.blade.php | 420 ++++++++++++++++-- 3 files changed, 799 insertions(+), 53 deletions(-) diff --git a/app/Filament/Pages/Strumenti/DocumentiArchivio.php b/app/Filament/Pages/Strumenti/DocumentiArchivio.php index 89007d6..8439d9f 100644 --- a/app/Filament/Pages/Strumenti/DocumentiArchivio.php +++ b/app/Filament/Pages/Strumenti/DocumentiArchivio.php @@ -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 */ 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 */ 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, ], diff --git a/docs/DOCUMENTI-NFC-E-ACQUISIZIONE.md b/docs/DOCUMENTI-NFC-E-ACQUISIZIONE.md index 2bf2af6..d895ebd 100644 --- a/docs/DOCUMENTI-NFC-E-ACQUISIZIONE.md +++ b/docs/DOCUMENTI-NFC-E-ACQUISIZIONE.md @@ -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. diff --git a/resources/views/filament/pages/strumenti/documenti-archivio.blade.php b/resources/views/filament/pages/strumenti/documenti-archivio.blade.php index aabd210..6eee167 100644 --- a/resources/views/filament/pages/strumenti/documenti-archivio.blade.php +++ b/resources/views/filament/pages/strumenti/documenti-archivio.blade.php @@ -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 @@
@forelse($this->protocolloComunicazioniRows as $row)
-
- @if($row['has_pdf']) - - @else -
- Anteprima PDF non disponibile per questo protocollo. -
- @endif -
{{ $row['protocollo'] }} @@ -306,21 +295,390 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
+ @elseif($this->hubTab === 'scansione-documenti') + + Scansione documenti + 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. + +
+
+
+
+
Nuova acquisizione scanner
+
Carica il PDF prodotto dallo scanner, assegna subito stabile, cartella e classe archivio, poi lascia al backend rinomina, protocollo e OCR.
+
+
OCR automatico
+
+ +
+
+ + +
+
+ + +
+
+ + +
Caricamento PDF in corso...
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+ +
+
+
Flusso pratico per staging
+
+
Lo scanner genera un PDF. Qui lo carichi una sola volta e il sistema lo rinomina secondo stabile, anno e protocollo.
+
I metadati di acquisizione restano nel documento per distinguere upload manuale, cartella Epson, email o mobile.
+
La pipeline OCR esistente parte subito dopo il salvataggio, senza creare un ramo tecnico separato.
+
La cartella archivio puo essere fissata subito oppure lasciata automatica in base a categoria e anno.
+
+
+ +
+
EPSON e browser
+
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.
+
+ +
+
Effetti del salvataggio
+
+
Numero protocollo generato automaticamente in base a categoria e anno del documento.
+
Rinominazione ordinata del file nello storage locale del progetto.
+
Metadati archivio pronti per QR, collocazione fisica e ricerche OCR successive.
+
+
+
+
+
+ @elseif($this->hubTab === 'etichette-generiche') + + Etichette generiche per contenitori + 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. + +
+
+
+
+
Nuova etichetta generica
+
Crea il contenitore, stampa subito e poi usalo come nodo della catena QR: fattura, cartella, dock, scatola, magazzino, scaffale.
+
+
QR incluso
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+ +
+
+
Standard operativo
+
+
Il codice univoco viene generato automaticamente e diventa il nodo da scansionare.
+
Il QR usa lo stesso schema pubblico gia host-agnostic del resto dell'archivio fisico.
+
Il link Amazon puo essere salvato come URL o come ASIN, con tag affiliato aggiunto dal super-admin.
+
Questa tab rende visibile la nuova etichetta senza doverla cercare dentro il registro generale.
+
+
+ +
+
+
Ultime etichette generiche
+
Stampa immediata 11354 / 99014
+
+
+ @forelse($this->genericLabelRows as $row) +
+
+
+
+ {{ $row['codice_univoco'] }} + {{ $row['tipo'] }} +
+
{{ $row['titolo'] }}
+ @if($row['linea_secondaria'] !== '') +
{{ $row['linea_secondaria'] }}
+ @endif +
Percorso: {{ $row['percorso'] }}
+ @if($row['ubicazione'] !== '') +
Ubicazione: {{ $row['ubicazione'] }}
+ @endif + @if($row['amazon_url'] !== '') + + @endif +
+ +
+
+ @empty +
Nessuna etichetta generica creata ancora per lo stabile attivo.
+ @endforelse +
+
+
+
+
+ @elseif($this->hubTab === 'movimentazione-codici') + + Movimentazione per codice + Pagina autonoma per spostare contenitori e documenti tramite codice o scansione. Serve da base alla catena annidata e ai futuri flussi con scanner EPSON. + +
+
+
+
Sposta per codice
+
Scansiona o inserisci codice sorgente e codice destinazione. Il contenitore padre viene aggiornato senza dover aprire il registro generale.
+
+
+ + +
+
+ + +
+
+
+ +
+
+ +
+
Catena QR prevista
+
QR Fattura -> QR Cartella trasparente -> QR Dock -> QR Scatola -> QR Magazzino -> QR Scaffale. Questa tab separa il movimento dal censimento del contenuto.
+
+
+ +
+
Contenitori di primo livello
+
+ @forelse($this->physicalArchiveContainerTree as $item) +
+
{{ $item['codice_univoco'] }}
+
{{ $item['titolo'] }}
+
{{ $item['tipo'] }} · {{ $item['children_count'] }} elementi interni
+
+ @empty +
Nessun contenitore creato ancora per lo stabile attivo.
+ @endforelse +
+
+
+
@elseif($this->hubTab === 'template-drive') Template cartelle Drive - Questa struttura resta la base comune per archivio digitale, passaggio consegne e futura sincronizzazione Google. + 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. -
- @foreach($this->driveTemplateFolders as $folder) -
{{ $folder }}
- @endforeach +
+
+
+
+
Origine struttura
+
Fonte attuale: {{ $this->driveTemplateSource === 'database' ? 'database' : 'config' }}
+
+
{{ count($this->driveTemplateFolders) }} cartelle logiche
+
+ + @if($this->canManageDriveTemplates) +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+ @else +
La modifica del template e dei parametri Amazon e riservata al super-admin. Gli altri ruoli vedono comunque la struttura effettiva.
+ @endif +
+ +
+ @foreach($this->driveTemplateFolders as $folder) +
{{ $folder }}
+ @endforeach +
@elseif($this->hubTab === 'archivio-fisico') Archivio fisico - Registro operativo del cartaceo: inserimento manuale, contenitori annidati, movimentazione per codice e stampa etichette 11354 o 99014. + 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.
@@ -421,28 +779,6 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
-
-
-
-
Movimentazione per codice
-
Scansioni o inserisci il codice sorgente e il contenitore di arrivo per spostare intere buste o faldoni.
-
-
-
-
- - -
-
- - -
-
-
- -
-
-
Contenitori di primo livello