Add scanner document acquisition tab
This commit is contained in:
parent
49a49cdb08
commit
c2dc58f0b9
|
|
@ -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,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user