Add archive QR XML payloads and staging support updates

This commit is contained in:
michele 2026-04-28 16:56:18 +00:00
parent d7e9a1c39d
commit e8440e873c
12 changed files with 849 additions and 69 deletions

View File

@ -3,7 +3,6 @@
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Process;
class NetgesconNodeRefreshCommand extends Command
@ -215,8 +214,8 @@ public function handle(): int
'event:cache' => 'NODE REFRESH: php artisan event:cache',
'queue:restart' => 'NODE REFRESH: php artisan queue:restart',
] as $artisanCommand => $label) {
$exit = Artisan::call($artisanCommand);
$output = trim((string) Artisan::output());
$exit = Artisan::call($artisanCommand);
$output = trim((string) Artisan::output());
$fullOutput[] = $label;
$fullOutput[] = $output;

View File

@ -0,0 +1,109 @@
<?php
namespace App\Filament\Pages\Condomini;
use App\Filament\Pages\Strumenti\DocumentiArchivio;
use App\Models\Assemblea;
use App\Models\Stabile;
use App\Models\User;
use App\Support\StabileContext;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Pages\Page;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use UnitEnum;
class AssembleeHub extends Page
{
protected static ?string $navigationLabel = 'HUB Assemblee';
protected static ?string $title = 'HUB Assemblee';
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-megaphone';
protected static UnitEnum|string|null $navigationGroup = 'Stabile';
protected static ?int $navigationSort = 35;
protected static ?string $slug = 'condomini/assemblee-hub';
protected string $view = 'filament.pages.condomini.assemblee-hub';
public ?Stabile $stabileAttivo = null;
public static function canAccess(): bool
{
$user = Auth::user();
return $user instanceof User
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
}
public function mount(): void
{
$user = Auth::user();
if (! $user instanceof User) {
return;
}
$activeId = StabileContext::resolveActiveStabileId($user);
if (! $activeId) {
return;
}
$this->stabileAttivo = StabileContext::accessibleStabili($user)->firstWhere('id', $activeId);
}
protected function getHeaderActions(): array
{
return [
Action::make('apri_documenti')
->label('Archivio documenti')
->icon('heroicon-o-folder-open')
->url(fn(): string => DocumentiArchivio::getUrl(panel: 'admin-filament')),
];
}
public function getStatsProperty(): array
{
$stabileId = (int) ($this->stabileAttivo?->id ?? 0);
if ($stabileId <= 0) {
return ['totale' => 0, 'convocate' => 0, 'in_calendario' => 0, 'con_verbale' => 0];
}
$query = Assemblea::query()->where('stabile_id', $stabileId);
return [
'totale' => (clone $query)->count(),
'convocate' => (clone $query)->where('stato', 'convocata')->count(),
'in_calendario'=> (clone $query)
->where(function ($builder): void {
$today = now();
$builder->whereDate('data_prima_convocazione', '>=', $today->toDateString())
->orWhereDate('data_seconda_convocazione', '>=', $today->toDateString());
})
->count(),
'con_verbale' => (clone $query)->whereHas('verbale')->count(),
];
}
public function getAssembleeRowsProperty(): Collection
{
$stabileId = (int) ($this->stabileAttivo?->id ?? 0);
if ($stabileId <= 0) {
return collect();
}
return Assemblea::query()
->with(['verbale:id,assemblea_id'])
->withCount(['ordineGiorno', 'convocazioni', 'presenze', 'documenti'])
->where('stabile_id', $stabileId)
->orderByDesc('data_seconda_convocazione')
->orderByDesc('data_prima_convocazione')
->orderByDesc('id')
->limit(16)
->get();
}
}

View File

@ -13,6 +13,7 @@
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use RuntimeException;
use UnitEnum;
@ -229,4 +230,33 @@ protected function buildAmazonSearchUrl(string $term): string
return 'https://www.amazon.it/s?' . http_build_query($query);
}
/** @return array<int, array<string, mixed>> */
public function getAmazonSetupChecklistProperty(): array
{
$items = [
['key' => 'AMAZON_CREATORS_ENABLED', 'required' => true, 'value' => (bool) config('services.amazon.creators_enabled', false) ? 'true' : 'false'],
['key' => 'AMAZON_CREATORS_API_BASE_URL', 'required' => true, 'value' => (string) config('services.amazon.creators_api_base_url', '')],
['key' => 'AMAZON_CREATORS_TOKEN_URL', 'required' => true, 'value' => (string) config('services.amazon.creators_token_url', '')],
['key' => 'AMAZON_CREATORS_CREDENTIAL_ID', 'required' => true, 'value' => (string) config('services.amazon.creators_credential_id', '')],
['key' => 'AMAZON_CREATORS_CREDENTIAL_SECRET', 'required' => true, 'value' => (string) config('services.amazon.creators_credential_secret', '')],
['key' => 'AMAZON_CREATORS_SEARCH_PATH', 'required' => true, 'value' => (string) data_get(config('services.amazon.creators_paths', []), 'searchItems', '')],
['key' => 'AMAZON_CREATORS_GET_ITEMS_PATH', 'required' => true, 'value' => (string) data_get(config('services.amazon.creators_paths', []), 'getItems', '')],
['key' => 'AMAZON_MARKETPLACE', 'required' => false, 'value' => (string) config('services.amazon.marketplace', 'www.amazon.it')],
['key' => 'AMAZON_REFERRAL_TAG', 'required' => false, 'value' => (string) config('services.amazon.referral_tag', '')],
];
return array_map(static function (array $item): array {
$rawValue = trim((string) ($item['value'] ?? ''));
$isBoolean = ($item['key'] ?? '') === 'AMAZON_CREATORS_ENABLED';
$configured = $isBoolean ? $rawValue === 'true' : $rawValue !== '';
return [
'key' => (string) ($item['key'] ?? ''),
'required' => (bool) ($item['required'] ?? false),
'configured' => $configured,
'display' => $rawValue !== '' ? Str::limit($rawValue, 54) : 'non valorizzata',
];
}, $items);
}
}

View File

@ -10,6 +10,7 @@
use App\Models\User;
use App\Services\Documenti\DocumentoArchivioVisibilityService;
use App\Services\Documenti\PdfTextExtractionService;
use App\Support\Archivio\ArchiveQrXmlPayload;
use App\Support\StabileContext;
use BackedEnum;
use Carbon\Carbon;
@ -909,6 +910,23 @@ public function getGenericLabelPreviewProperty(): array
$amazonUrl = trim((string) ($this->normalizeAmazonPurchaseUrl($this->genericLabelForm['amazon_url'] ?? null) ?? ''));
$qrProfiles = ArchiveQrXmlPayload::buildProfiles([
'id' => 'AF-QR-PREVIEW',
'legacy' => 'AF:AF-QR-PREVIEW',
'type' => Str::upper((string) ($this->genericLabelForm['tipo_item'] ?? 'SCATOLA')),
'stabile_id' => (string) ((int) ($this->resolveActiveStabile()?->id ?? 0)),
'parent' => trim((string) ($this->genericLabelForm['parent_id'] ?? '')) !== '' ? 'COLLEGATO' : 'RADICE',
'code' => $mnemonicCode,
'title' => trim((string) ($this->genericLabelForm['titolo'] ?? '')),
'archive_class' => 'GENERIC_LABEL',
'location' => trim(implode('/', array_filter([
$this->genericLabelForm['magazzino'] ?? null,
$this->genericLabelForm['scaffale'] ?? null,
$this->genericLabelForm['ripiano'] ?? null,
$this->genericLabelForm['ubicazione_dettaglio'] ?? null,
]))),
]);
$payload = [
'mnemonic_code' => $mnemonicCode,
'titolo' => trim((string) ($this->genericLabelForm['titolo'] ?? '')),
@ -936,14 +954,11 @@ public function getGenericLabelPreviewProperty(): array
$stabileAddress,
]))),
'codice_univoco' => 'AF-QR-PREVIEW',
'qr_payload' => implode('|', array_filter([
'NGDOC',
'AF:AF-QR-PREVIEW',
'MN:' . $mnemonicCode,
'TIPO:' . Str::upper((string) ($this->genericLabelForm['tipo_item'] ?? 'SCATOLA')),
'STB:' . ((int) ($this->resolveActiveStabile()?->id ?? 0)),
trim((string) ($this->genericLabelForm['parent_id'] ?? '')) !== '' ? 'PARENT:COLLEGATO' : 'PARENT:RADICE',
])),
'qr_payload' => (string) ($qrProfiles['xml_open'] ?? ''),
'qr_payload_zip' => (string) ($qrProfiles['xml_zip'] ?? ''),
'nfc_payload' => (string) ($qrProfiles['xml_nfc'] ?? ''),
'qr_payload_length' => (int) ($qrProfiles['xml_open_length'] ?? 0),
'nfc_payload_length'=> (int) ($qrProfiles['xml_nfc_length'] ?? 0),
'amazon_url' => $amazonUrl,
];
@ -1978,7 +1993,7 @@ private function initializeScannerAcquisitionForm(): void
'data_scadenza' => null,
'importo' => null,
'faldone' => '',
'supporto_fisico' => '',
'supporto_fisico' => 'Busta trasparente con anelli da archiviare',
'modello_etichetta' => '11354',
'busta_numero' => '',
'fascicolo_numero' => '',
@ -1993,6 +2008,49 @@ private function initializeScannerAcquisitionForm(): void
];
}
/** @return array<string, mixed> */
public function getScannerQrPreviewProperty(): array
{
$title = trim((string) ($this->scannerAcquisitionForm['titolo'] ?? ''));
$supporto = trim((string) ($this->scannerAcquisitionForm['supporto_fisico'] ?? ''));
$stabileId = (int) ($this->scannerAcquisitionForm['stabile_id'] ?? 0);
$categoria = trim((string) ($this->scannerAcquisitionForm['categoria'] ?? 'altro'));
$classe = trim((string) ($this->scannerAcquisitionForm['archivio_classe'] ?? 'altro'));
$busta = trim((string) ($this->scannerAcquisitionForm['busta_numero'] ?? ''));
$fascicolo = trim((string) ($this->scannerAcquisitionForm['fascicolo_numero'] ?? ''));
$contenitore = trim((string) ($this->scannerAcquisitionForm['contenitore_numero'] ?? ''));
$archiveCode = trim(implode('-', array_filter([
strtoupper($categoria !== '' ? $categoria : 'DOC'),
now()->format('Y'),
$busta !== '' ? 'B' . $busta : null,
$fascicolo !== '' ? 'F' . $fascicolo : null,
$contenitore !== '' ? 'C' . $contenitore : null,
])));
$profiles = ArchiveQrXmlPayload::buildProfiles([
'id' => $archiveCode !== '' ? $archiveCode : 'DOC-PREVIEW',
'legacy' => 'DOC:' . ($archiveCode !== '' ? $archiveCode : 'DOC-PREVIEW'),
'type' => 'ATTO',
'stabile_id' => (string) $stabileId,
'parent' => $supporto !== '' ? strtoupper(Str::slug($supporto, '-')) : 'RADICE',
'code' => $archiveCode,
'title' => $title,
'document_code' => $archiveCode,
'archive_class' => strtoupper($classe),
'location' => $supporto,
]);
return [
'code' => $archiveCode,
'xml_open' => (string) ($profiles['xml_open'] ?? ''),
'xml_zip' => (string) ($profiles['xml_zip'] ?? ''),
'xml_nfc' => (string) ($profiles['xml_nfc'] ?? ''),
'xml_open_length' => (int) ($profiles['xml_open_length'] ?? 0),
'xml_nfc_length' => (int) ($profiles['xml_nfc_length'] ?? 0),
];
}
private function initializeDriveTemplateForm(): void
{
$settings = $this->getArchiveHubSettings();
@ -3037,6 +3095,18 @@ private function createDocumentoFromForm(array $data, bool $isDemo): void
$riferimentoAcquisizione = trim((string) ($data['riferimento_acquisizione'] ?? ''));
$scannerNote = trim((string) ($data['scanner_note'] ?? ''));
$origineDocumento = trim((string) ($data['origine_documento'] ?? ''));
$documentQrProfiles = ArchiveQrXmlPayload::buildProfiles([
'id' => $archiveReference,
'legacy' => 'DOC:' . $archiveReference,
'type' => 'ATTO',
'stabile_id' => (string) $stabileId,
'parent' => trim((string) ($data['supporto_fisico'] ?? '')) !== '' ? strtoupper(Str::slug((string) $data['supporto_fisico'], '-')) : 'RADICE',
'code' => $archiveReference,
'title' => $titolo,
'document_code' => $protocollo,
'archive_class' => strtoupper($archivioClasse !== '' ? $archivioClasse : $categoria),
'location' => trim((string) ($data['supporto_fisico'] ?? '')),
]);
if ($scannerNote !== '') {
$note = trim($note . ($note !== '' ? "\n" : '') . 'Scanner: ' . $scannerNote);
@ -3083,6 +3153,9 @@ private function createDocumentoFromForm(array $data, bool $isDemo): void
'origine_documento' => $origineDocumento !== '' ? $origineDocumento : null,
'qr_ready' => true,
'nfc_ready' => true,
'qr_payload_xml' => (string) ($documentQrProfiles['xml_open'] ?? ''),
'qr_payload_zip' => (string) ($documentQrProfiles['xml_zip'] ?? ''),
'nfc_payload_xml' => (string) ($documentQrProfiles['xml_nfc'] ?? ''),
],
'visibility_scope' => (string) ($data['visibility_scope'] ?? 'interno'),
'visibility_roles' => $this->normalizeStringArray($data['visibility_roles'] ?? []),

View File

@ -1,6 +1,7 @@
<?php
namespace App\Models;
use App\Support\Archivio\ArchiveQrXmlPayload;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -178,15 +179,17 @@ public function buildQrPayload(): string
trim((string) ($this->ubicazione_dettaglio ?? '')),
])->filter()->implode('/');
return implode('|', array_filter([
'NGDOC',
'AF:' . trim((string) $this->codice_univoco),
'TIPO:' . Str::upper((string) $this->tipo_item),
'STB:' . (int) $this->stabile_id,
'PARENT:' . ($parentCode !== '' ? $parentCode : 'RADICE'),
$documentCode !== '' ? 'DOC:' . $documentCode : null,
$location !== '' ? 'LOC:' . $location : null,
]));
return ArchiveQrXmlPayload::build([
'id' => trim((string) $this->codice_univoco),
'legacy' => 'AF:' . trim((string) $this->codice_univoco),
'type' => Str::upper((string) $this->tipo_item),
'stabile_id' => (string) ((int) $this->stabile_id),
'parent' => $parentCode !== '' ? $parentCode : 'RADICE',
'document_code' => $documentCode,
'location' => $location,
'title' => trim((string) $this->titolo),
'archive_class' => 'ARCHIVIO_FISICO',
]);
}
public function buildPublicQrUrl(): string

View File

@ -60,7 +60,7 @@ public function panel(Panel $panel): Panel
$user = Auth::user();
return $user instanceof User
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
}),
NavigationItem::make('Cellulare / WebApp')
->group('NetGescon')
@ -70,7 +70,7 @@ public function panel(Panel $panel): Panel
$user = Auth::user();
return $user instanceof User
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
}),
NavigationItem::make('Ticket Mobile')
->group('NetGescon')
@ -80,7 +80,7 @@ public function panel(Panel $panel): Panel
$user = Auth::user();
return $user instanceof User
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
}),
NavigationItem::make('Scheda Stabile')
->group('NetGescon')
@ -90,7 +90,7 @@ public function panel(Panel $panel): Panel
$user = Auth::user();
return $user instanceof User
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
}),
NavigationItem::make('Catalogo Fornitore')
->group('Fornitore')

View File

@ -0,0 +1,127 @@
<?php
namespace App\Support\Archivio;
final class ArchiveQrXmlPayload
{
public const VERSION = '1';
public const PROFILE_LITE = 'MASTER-XML-LITE';
public const PROFILE_NFC = 'MASTER-NFC-LITE';
public const COMPRESSED_PREFIX = 'NGXZ1:';
/** @param array<string, mixed> $data */
public static function build(array $data, string $profile = self::PROFILE_LITE): string
{
$normalized = self::normalize($data);
$checksum = self::checksum($normalized + ['profile' => $profile]);
$fields = [
'id' => $normalized['id'],
'legacy' => $normalized['legacy'],
'type' => $normalized['type'],
'sid' => $normalized['stabile_id'],
'parent' => $normalized['parent'],
'code' => $normalized['code'],
'title' => $normalized['title'],
'doc' => $normalized['document_code'],
'class' => $normalized['archive_class'],
'loc' => $normalized['location'],
'cs' => $checksum,
];
$xml = '<ngxa v="' . self::VERSION . '" p="' . self::escape($profile) . '">';
foreach ($fields as $tag => $value) {
if ($value === '') {
continue;
}
$xml .= '<' . $tag . '>' . self::escape($value) . '</' . $tag . '>';
}
$xml .= '</ngxa>';
return $xml;
}
/** @param array<string, mixed> $data */
public static function buildCompressed(array $data, string $profile = self::PROFILE_LITE): string
{
$xml = self::build($data, $profile);
$compressed = gzcompress($xml, 9);
if ($compressed === false) {
return self::COMPRESSED_PREFIX . base64_encode($xml);
}
return self::COMPRESSED_PREFIX . base64_encode($compressed);
}
/** @param array<string, mixed> $data */
public static function buildNfcLite(array $data): string
{
return self::build([
'id' => $data['id'] ?? '',
'legacy' => $data['legacy'] ?? '',
'type' => $data['type'] ?? '',
'stabile_id' => $data['stabile_id'] ?? '',
'parent' => $data['parent'] ?? 'RADICE',
'code' => $data['code'] ?? '',
'archive_class' => $data['archive_class'] ?? '',
], self::PROFILE_NFC);
}
/** @param array<string, mixed> $data
* @return array<string, mixed>
*/
public static function buildProfiles(array $data): array
{
$xmlOpen = self::build($data, self::PROFILE_LITE);
$xmlNfc = self::buildNfcLite($data);
return [
'xml_open' => $xmlOpen,
'xml_open_length' => strlen($xmlOpen),
'xml_zip' => self::buildCompressed($data, self::PROFILE_LITE),
'xml_nfc' => $xmlNfc,
'xml_nfc_length' => strlen($xmlNfc),
];
}
/** @param array<string, mixed> $data
* @return array<string, string>
*/
private static function normalize(array $data): array
{
$id = trim((string) ($data['id'] ?? ''));
return [
'id' => $id,
'legacy' => trim((string) ($data['legacy'] ?? ($id !== '' ? 'AF:' . $id : ''))),
'type' => trim((string) ($data['type'] ?? '')),
'stabile_id' => trim((string) ($data['stabile_id'] ?? '')),
'parent' => trim((string) ($data['parent'] ?? 'RADICE')),
'code' => trim((string) ($data['code'] ?? '')),
'title' => trim((string) ($data['title'] ?? '')),
'document_code'=> trim((string) ($data['document_code'] ?? '')),
'archive_class'=> trim((string) ($data['archive_class'] ?? '')),
'location' => trim((string) ($data['location'] ?? '')),
];
}
/** @param array<string, string> $data */
private static function checksum(array $data): string
{
ksort($data);
return strtoupper(substr(sha1(json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: ''), 0, 12));
}
private static function escape(string $value): string
{
return htmlspecialchars($value, ENT_XML1 | ENT_COMPAT, 'UTF-8');
}
}

View File

@ -2,44 +2,130 @@ # Documenti Archivio DAFARE
## Obiettivo operativo
Costruire un flusso unico che parta dal file digitale, generi il codice mnemonico/QR, stampi l'etichetta corretta e accompagni il documento dentro la catena fisica fino alla collocazione finale.
Costruire un flusso unico che parta dal file digitale, generi una codifica master stabile, stampi etichette QR leggibili, colleghi eventuali tag NFC o RFID e accompagni il documento dentro la catena fisica fino alla collocazione finale o al passaggio consegne tra amministratori.
## Slice da fare subito
## Stato reale da cui partiamo
1. Rifinire il builder etichette su `etichette-generiche`.
2. Tenere le impostazioni accanto alla riga o al campo che controllano.
3. Rendere piu esplicita la modifica e la ristampa delle etichette gia create.
4. Portare la gestione di supporto e ubicazione nella tab `movimentazione-codici`.
5. Preparare il flusso `file base -> primo supporto -> contenitori successivi`.
- Nel modulo documenti esistono gia tre concetti separati da unificare: Categoria, Classe archivio e Voce archivio.
- Nel codice attuale il punto piu vicino e app/Filament/Pages/Strumenti/DocumentiArchivio.php, dove la tab scansione usa categoria e archivio_classe, mentre l'archivio fisico registra la voce separatamente.
- Per le assemblee esiste gia una base dati con Assemblea, OrdineGiorno, Convocazione, PresenzaAssemblea, Verbale, DocumentoAssemblea e collegamenti allo stabile.
- Il lavoro da fare ora non e inventare concetti nuovi, ma unificare nomenclatura, codifica e flussi sopra strutture gia presenti.
## Convenzioni archivio da fissare
## Decisione guida
### Codice categoria archivio
Tutto deve convergere su una sola codifica master archivistica, usata in quattro contesti:
- `CONTR-ANNO-###`: contratti di servizio
- `ASSIC-ANNO-###`: polizze assicurative
- `CERT-ANNO-###`: certificati e autorizzazioni
- `PREV-ANNO-###`: preventivi e offerte
- `FATT-ANNO-###`: fatture e documenti fiscali
- `VERB-ANNO-###`: verbali assemblee
- `DOC-ANNO-###`: altri documenti
1. Nome logico del documento e riferimento archivistico umano.
2. Payload QR o tag NFC.
3. Chiave di import/export per passaggio consegne tra amministratori.
4. Ancoraggio comune per archivio digitale, archivio fisico, assemblee e future automazioni hardware.
### Regole comuni
## Gruppo A - Codifica master unificata archivio
- Il prefisso categoria va salvato anche nella voce archivio su database.
- Il valore deve essere in formato Linux-safe: maiuscolo, senza spazi, uniforme.
### Obiettivo
Sostituire i tre campi disallineati con un unico concetto di classificazione archivistica master, mantenendo compatibilita con le schermate esistenti.
### Modello logico proposto
- codice_master: identificatore archivistico stabile e stampabile
- famiglia_archivio: macro famiglia funzionale
- classe_archivio: classe normalizzata interna
- voce_archivio: descrizione umana operativa
- versione_schema: versione della codifica XML, QR e NFC
### Convenzioni archivio da fissare
#### Famiglie iniziali
- CONTR: contratti di servizio
- ASSIC: polizze assicurative
- CERT: certificati e autorizzazioni
- PREV: preventivi e offerte
- FATT: fatture e documenti fiscali
- VERB: verbali assemblee
- ASSEMB: convocazioni, allegati, presenze e verbali assembleari
- DOC: altri documenti
#### Regole comuni
- Il codice deve essere Linux-safe: maiuscolo, senza spazi, solo caratteri stabili.
- Il codice mnemonico deve diventare sia nome logico del file digitale sia fallback umano dell'etichetta stampata.
- Il QR o TAG deve contenere l'identificatore unico universale del nodo archivistico, leggibile da smartphone o lettore fisico.
- Il passaggio consegne tra amministratori deve riusare lo stesso codice mnemonico/QR senza ricodifiche locali.
- Il QR o tag deve contenere sempre almeno schema, codice_master, tipo_record, stabile, hash o checksum.
- Il passaggio consegne tra amministratori deve riusare lo stesso codice senza ricodifiche locali.
- La descrizione umana puo cambiare, il codice master no.
### Formato consigliato del codice master
- FAMIGLIA-ANNO-SEQ
- esempi:
- CONTR-2026-001
- VERB-2026-014
- ASSEMB-2026-005
### Data nei file
- Inserire la data nel nome file con formato `YYMMGG`.
- Inserire la data nel nome file con formato YYMMGG.
- Prevedere una futura impostazione generale dell'amministratore per cambiare il formato globale della data.
## Flussi da realizzare
## Gruppo B - XML master per QR, NFC e passaggio consegne
### 1. Gestione file base
### Obiettivo
Usare XML come formato di trasporto e scambio per non dipendere da link esterni, URL o host specifici.
### Vincolo tecnico da non perdere di vista
- Un QR standard non e affidabile per 6000 byte in lettura smartphone generalista.
- Per uso reale conviene progettare un XML compatto e mantenere un payload sicuro molto piu basso.
- Un NTAG215 offre circa 504 byte utili: questo obbliga a usare un XML molto compatto o una variante abbreviata dedicata al tag NFC.
- Quindi servono due profili dello stesso schema:
- MASTER-XML-FULL per esportazioni, passaggi consegne e QR ad alta capienza
- MASTER-XML-LITE per QR rapidi e soprattutto per NTAG215
### Campi minimi del profilo full
- schema
- versione
- tipo_record
- codice_master
- stabile_id o codice_stabile
- titolo
- note sintetiche
- nome_file
- classe_archivio normalizzata
- voce_archivio umana
- tag liberi
- riferimento RFID o NFC se presente
- pagine o consistenza fogli
- checksum
### Campi minimi del profilo lite
- schema
- versione
- tipo_record
- codice_master
- stabile
- classe
- checksum
### Uso operativo previsto
- QR su singolo documento
- QR su cartellina, faldone, scatola o foglio consegna
- QR master finale su verbale di passaggio consegne con tabella di codifica incorporata
- Tag NFC adesivo per scrittura e rilettura rapida da app o bridge locale
### Regola importante
Se la lettura trova un codice_master gia presente nel DB, il sistema deve collegare e arricchire il record esistente; se non lo trova, deve proporre import guidato o creazione rapida.
## Gruppo C - Flusso documenti e archivio fisico
### Flussi da realizzare
#### 1. Gestione file base
Una Blade dedicata deve permettere di registrare il singolo file o una raccolta di fogli con:
@ -47,12 +133,13 @@ ### 1. Gestione file base
- descrizione
- note
- tag
- categoria come tag strutturato
- classificazione master strutturata
- numero totale dei fogli o dell'incartamento
- selezione del layout etichetta da usare
- stampa etichetta con QR e fallback testuale nel titolo e sotto il QR
- eventuale lettura o scrittura NFC
### 2. Primo contenitore fisico
#### 2. Primo contenitore fisico
Dal file base bisogna poter scegliere il primo supporto:
@ -63,7 +150,7 @@ ### 2. Primo contenitore fisico
Il record deve restare collegato al documento digitale per consentire nuove stampe dell'etichetta in qualsiasi momento.
### 3. Contenitori successivi e movimentazione
#### 3. Contenitori successivi e movimentazione
Serve una gestione separata dove il materiale gia preparato venga caricato nei contenitori successivi:
@ -74,43 +161,230 @@ ### 3. Contenitori successivi e movimentazione
- scaffale
- posto finale
La tab `movimentazione-codici` deve diventare il centro operativo per:
La tab movimentazione-codici deve diventare il centro operativo per:
- cambiare il contenitore padre
- aggiornare supporto operativo
- aggiornare magazzino, scaffale, ripiano e dettaglio ubicazione
- leggere codici da scanner, smartphone, QR o plugin locale
- leggere codici da scanner, smartphone, QR o bridge locale
- associare o riscrivere NFC quando consentito dal profilo utente
## Etichette 11354 e binding DB
### Slice da fare subito su documenti
- Per il formato `11354` prevedere un campo di collegamento ai dati DB da riversare automaticamente in stampa.
1. Rifinire il builder etichette su etichette-generiche.
2. Tenere le impostazioni accanto alla riga o al campo che controllano.
3. Rendere piu esplicita la modifica e la ristampa delle etichette gia create.
4. Portare la gestione di supporto e ubicazione nella tab movimentazione-codici.
5. Preparare il flusso file base -> primo supporto -> contenitori successivi.
6. Introdurre una tabella unica di classificazione da cui derivano Categoria, Classe archivio e Voce archivio.
## Gruppo D - Etichette 11354 e binding DB
- Per il formato 11354 prevedere un campo di collegamento ai dati DB da riversare automaticamente in stampa.
- Ogni riga deve poter leggere un campo DB oppure testo libero.
- Le impostazioni della singola riga devono stare vicino alla riga stessa.
- La ristampa deve essere disponibile anche se fallisce la lettura automatica del QR.
- Il payload QR stampato deve poter scegliere tra XML lite, XML full o semplice codice master.
## Catalogo supporti e Amazon
## Gruppo E - Plugin locale o bridge dispositivi
- Ogni supporto fisico scelto dall'utente deve poter avere il pulsante Amazon.
- Il link Amazon deve arrivare dal catalogo prodotti gestito dal super admin.
- L'archivio fisico non deve duplicare il catalogo: deve consumare il dato gia scelto.
### Obiettivo
## Plugin locale / bridge dispositivi
Un agent locale deve fare da ponte fra browser e hardware, sia in lettura che in scrittura.
Serve un plugin HTTP locale o agent Windows per passare dati hardware fuori dal browser:
### Periferiche previste
- TWAIN/WIA scanner
- lettori barcode e QR
- NFC
- seriali
- eventuali periferiche di stampa/lettura
- eventuali periferiche di stampa e lettura
La webapp deve restare il pannello operativo, mentre il bridge locale si occupa della parte hardware.
### Flussi minimi del bridge
- GET /devices: elenco periferiche disponibili
- POST /scan: acquisizione scanner o lettore QR
- POST /nfc/read: lettura tag
- POST /nfc/write: scrittura XML lite su tag NFC
- POST /label/print: stampa etichetta con payload deciso dalla webapp
### Nota tecnica
La webapp resta il pannello operativo. Il bridge locale si occupa della parte hardware e non diventa mai il sistema autorevole dei dati.
## Gruppo F - Passaggio consegne tra amministratori
### Obiettivo
Preparare una procedura standard di uscita ed entrata che usa documenti umani piu un QR master/XML per ricostruire il contesto senza lavoro manuale ridondante.
### Flusso proposto
1. L'amministratore seleziona i faldoni, le cartelline e i documenti da consegnare.
2. Il sistema produce un foglio consegna umano stampabile.
3. In coda al foglio viene aggiunto un QR master con XML di riepilogo.
4. L'amministratore ricevente scansiona il QR master.
5. Il sistema propone import, ricostruzione dell'albero archivistico e collegamento ai record gia esistenti.
### Contenuto minimo del QR master di consegna
- identificativo passaggio
- amministratore uscente
- amministratore entrante se noto
- stabile o gruppo stabili
- elenco contenitori e codici master
- schema di classificazione usato
- checksum generale
## Gruppo G - HUB Assemblee dentro Stabili
### Obiettivo
Creare un hub operativo unico per ogni stabile che gestisca l'intero ciclo assembleare, dalla convocazione al verbale e alle attivita post-assemblea.
### Base dati gia presente
- Assemblea
- OrdineGiorno
- Convocazione
- PresenzaAssemblea
- Verbale
- DocumentoAssemblea
### Cosa deve contenere l'HUB
- elenco assemblee per stabile in ordine cronologico
- prima e seconda convocazione
- allegati inviati per convocazione
- verifica convocazioni prodotte e spedite
- gestione destinatari email, PEC, RR e futuri provider postalizzazione
- stato spedizione per ciascun destinatario
- tabella millesimale di riferimento per assemblea o per singolo punto
- gestione punti all'ordine del giorno
- allegati e conteggi collegati a ogni punto OdG
- norma o criterio di votazione associato al punto
- gestione verbale e relativo invio
- generazione ticket operativi derivati dalla delibera
- registrazione presenza diretta o per delega anche via QR del partecipante
### Slice 1 consigliata per l'HUB
1. Pagina indice Assemblee Hub nel gruppo menu Stabile.
2. Lista assemblee filtrabile per stabile, stato e data.
3. Scheda assemblea con le due convocazioni e pannello allegati.
4. Sezione destinatari convocazione con stato invio e canale.
5. Sezione OdG con collegamento ad allegati e conti.
### Slice 2
1. Gestione presenze, deleghe e quorum.
2. Regole di voto per punto OdG.
3. Verbale, invio verbale e storico esiti.
4. Creazione ticket da delibera.
### Slice 3
1. QR partecipante per check-in.
2. Import automatico deleghe.
3. Integrazione con postalizzazione online.
4. Collegamento forte con archivio documentale e codice master.
## Gruppo H - Area pubblica normativa e knowledge base
### Obiettivo
Aprire sul sito pubblico una sezione informativa sulla gestione condominiale, con contenuti normativi referenziati e governati da ruoli editoriali distinti dall'area amministrativa riservata.
### Contenuti previsti
- articoli di riferimento
- commi e leggi collegate
- note applicative
- collegamenti a casi pratici o domande frequenti
- possibile forum o blog esterno federato ma non mescolato alla parte riservata
### Governance desiderata
- redazione affidata a un avvocato o associazione
- pubblicazione separata dai permessi amministrativi ordinari
- workflow bozza, revisione, pubblicazione
## Gruppo I - Bacheca amministratore e raccolta voti preliminari
### Obiettivo
Consentire all'amministratore di pubblicare comunicazioni e aprire una raccolta di consensi o indicazioni prima dell'assemblea.
### Funzioni richieste
- invio email con accesso alla bacheca
- notizia pubblica o riservata per stabile o gruppo utenti
- allegati
- stato lettura
- richiesta approvazione o voto preliminare
- tracciamento dei riscontri prima dell'assemblea
- collegamento eventuale a un punto OdG futuro
## Gruppo L - Catalogo supporti e Amazon
- Ogni supporto fisico scelto dall'utente deve poter avere il pulsante Amazon.
- Il link Amazon deve arrivare dal catalogo prodotti gestito dal super admin.
- L'archivio fisico non deve duplicare il catalogo: deve consumare il dato gia scelto.
## Gruppo M - Catalogo fornitore, Amazon e TecnoRepair
### Amazon prodotti fornitore
- La UI catalogo fornitore esiste gia in fornitore/prodotti.
- Il servizio Creators API esiste gia, ma per usarlo davvero su staging servono le variabili ambiente AMAZON_CREATORS_* valorizzate sul nodo.
- Variabili richieste per chiudere il setup:
- AMAZON_CREATORS_ENABLED=true
- AMAZON_CREATORS_API_BASE_URL
- AMAZON_CREATORS_TOKEN_URL
- AMAZON_CREATORS_CREDENTIAL_ID
- AMAZON_CREATORS_CREDENTIAL_SECRET
- AMAZON_CREATORS_SEARCH_PATH
- AMAZON_CREATORS_GET_ITEMS_PATH
- Variabili opzionali ma consigliate:
- AMAZON_MARKETPLACE
- AMAZON_REFERRAL_TAG
- Obiettivo operativo: cercare, salvare e aggiornare candidati Amazon direttamente nel catalogo fornitore interno.
### TecnoRepair e scheda riparazione
- Esiste gia una base import TecnoRepair e una sincronizzazione prodotto catalogo da MDB.
- Da completare il collegamento con la singola scheda riparazione per gestire meglio stato chiusura e riconsegna.
- Campi legacy da intercettare o mappare esplicitamente:
- isRiparato
- isRiconsegnato
- Tmarche
- Tapparecchi
- Cod_Marca come chiave di legame fra marca e apparecchio
- Obiettivo funzionale:
- marcare chiusa la singola riparazione quando il legacy la considera riparata
- distinguere chiusa da riconsegnata
- usare Tmarche e Tapparecchi per costruire o normalizzare marchio e modello nel catalogo prodotti
- collegare il prodotto TecnoRepair alla scheda di lavorazione e agli eventuali candidati Amazon equivalenti o sostitutivi
## Primo ordine di esecuzione consigliato
1. Tabella unica di classificazione archivio e normalizzazione dei tre campi attuali.
2. XML schema lite e full con checksum e codice master.
3. Stampa QR coerente con il nuovo schema.
4. Lettura QR e collegamento ai record gia esistenti.
5. Hub Assemblee slice 1 dentro il gruppo Stabile.
6. Bridge locale NFC e scrittura NTAG215.
7. Foglio passaggio consegne con QR master.
8. Area pubblica normativa e bacheca.
## Backlog successivo
1. Foto ubicazione finale come nel ticket.
2. Vista modifica/ristampa dalle ultime etichette.
2. Vista modifica e ristampa dalle ultime etichette.
3. Blade separata per file singolo e per raccolta fogli.
4. Conteggio pagine persistente su database.
5. Impostazioni globali amministratore per formato data e convenzioni etichetta.
6. Integrazione completa col bridge locale di comunicazione.
7. QR multilivello per scatole o interi lotti documentali.
8. Import ed export XML firmato o verificato con checksum forte.
9. Collegamento tra verbali assemblea, delibere e ticket generati.

View File

@ -0,0 +1,98 @@
<x-filament-panels::page>
<div class="space-y-4">
@if(! $this->stabileAttivo)
<div class="rounded-xl border border-amber-300 bg-amber-50 p-4 text-sm text-amber-800">
Nessuno stabile attivo disponibile. Seleziona prima uno stabile per aprire l'hub assemblee.
</div>
@else
<div class="rounded-xl border bg-white p-4">
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<div class="text-lg font-semibold text-slate-900">{{ $this->stabileAttivo->denominazione ?: 'Stabile attivo' }}</div>
<div class="mt-1 text-sm text-slate-500">
Prima slice HUB Assemblee: convocazioni, cronologia, allegati e avanzamento operativo dello stabile.
</div>
</div>
<div class="rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold text-slate-700">
Stabile ID {{ (int) $this->stabileAttivo->id }}
</div>
</div>
</div>
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
<div class="rounded-xl border bg-white p-4">
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Assemblee totali</div>
<div class="mt-2 text-3xl font-semibold text-slate-900">{{ (int) ($this->stats['totale'] ?? 0) }}</div>
</div>
<div class="rounded-xl border bg-white p-4">
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Convocate</div>
<div class="mt-2 text-3xl font-semibold text-slate-900">{{ (int) ($this->stats['convocate'] ?? 0) }}</div>
</div>
<div class="rounded-xl border bg-white p-4">
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">In calendario</div>
<div class="mt-2 text-3xl font-semibold text-slate-900">{{ (int) ($this->stats['in_calendario'] ?? 0) }}</div>
</div>
<div class="rounded-xl border bg-white p-4">
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Con verbale</div>
<div class="mt-2 text-3xl font-semibold text-slate-900">{{ (int) ($this->stats['con_verbale'] ?? 0) }}</div>
</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">Cronologia assemblee</div>
<div class="mt-1 text-xs text-slate-500">Vista iniziale per pilotare convocazioni, allegati, OdG, verbali e ticket derivati nelle prossime slice.</div>
</div>
</div>
<div class="mt-4 space-y-3">
@forelse($this->assembleeRows as $assemblea)
<div class="rounded-xl border border-slate-200 bg-slate-50 p-4">
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<div class="text-sm font-semibold text-slate-900">
{{ $assemblea->tipo ?: 'Assemblea ordinaria' }}
</div>
<div class="mt-1 text-xs text-slate-500">
Prima convocazione:
{{ $assemblea->data_prima_convocazione ? $assemblea->data_prima_convocazione->format('d/m/Y H:i') : 'n/d' }}
· Seconda convocazione:
{{ $assemblea->data_seconda_convocazione ? $assemblea->data_seconda_convocazione->format('d/m/Y H:i') : 'n/d' }}
</div>
@if((string) ($assemblea->luogo ?? '') !== '')
<div class="mt-1 text-xs text-slate-500">Luogo: {{ $assemblea->luogo }}</div>
@endif
</div>
<div class="rounded-full px-3 py-1 text-xs font-semibold {{ (string) ($assemblea->stato ?? '') === 'convocata' ? 'bg-amber-50 text-amber-700' : ((string) ($assemblea->stato ?? '') === 'svolta' ? 'bg-emerald-50 text-emerald-700' : 'bg-slate-100 text-slate-700') }}">
{{ $assemblea->stato ?: 'da impostare' }}
</div>
</div>
<div class="mt-3 grid gap-2 text-xs text-slate-600 md:grid-cols-4">
<div class="rounded-lg bg-white px-3 py-2">OdG: <span class="font-semibold text-slate-900">{{ (int) ($assemblea->ordine_giorno_count ?? 0) }}</span></div>
<div class="rounded-lg bg-white px-3 py-2">Convocazioni: <span class="font-semibold text-slate-900">{{ (int) ($assemblea->convocazioni_count ?? 0) }}</span></div>
<div class="rounded-lg bg-white px-3 py-2">Presenze: <span class="font-semibold text-slate-900">{{ (int) ($assemblea->presenze_count ?? 0) }}</span></div>
<div class="rounded-lg bg-white px-3 py-2">Documenti: <span class="font-semibold text-slate-900">{{ (int) ($assemblea->documenti_count ?? 0) }}</span></div>
</div>
@if((string) ($assemblea->note ?? '') !== '')
<div class="mt-3 rounded-lg border border-dashed border-slate-300 bg-white px-3 py-2 text-xs text-slate-600">
{{ \\Illuminate\\Support\\Str::limit((string) $assemblea->note, 220) }}
</div>
@endif
<div class="mt-3 text-[11px] text-slate-500">
Verbale {{ $assemblea->verbale ? 'presente' : 'non ancora collegato' }}.
</div>
</div>
@empty
<div class="rounded-xl border border-dashed p-6 text-sm text-slate-500">
Nessuna assemblea registrata per lo stabile attivo.
</div>
@endforelse
</div>
</div>
@endif
</div>
</x-filament-panels::page>

View File

@ -45,6 +45,28 @@
</div>
</div>
@if(! $this->amazonConfigured)
<div class="mt-4 rounded-xl border border-amber-200 bg-amber-50 p-4">
<div class="text-sm font-semibold text-amber-900">Cosa serve per attivare l'import Amazon</div>
<div class="mt-1 text-xs text-amber-800">
Per completare l'integrazione servono le credenziali Creators API, i due endpoint operativi e l'attivazione delle variabili ambiente sul nodo dove gira NetGescon.
</div>
<div class="mt-3 grid gap-2 md:grid-cols-2">
@foreach($this->amazonSetupChecklist as $item)
<div class="rounded-lg border px-3 py-2 text-xs {{ $item['configured'] ? 'border-emerald-200 bg-emerald-50 text-emerald-800' : 'border-amber-200 bg-white text-amber-900' }}">
<div class="font-semibold">{{ $item['key'] }}{{ $item['required'] ? ' · obbligatoria' : ' · opzionale' }}</div>
<div class="mt-1">{{ $item['display'] }}</div>
</div>
@endforeach
</div>
<div class="mt-3 rounded-lg border border-slate-200 bg-white px-3 py-3 text-xs text-slate-700">
Con questi dati completi posso chiudere il collegamento Creators API, far partire la ricerca reale da staging e salvare i candidati Amazon nel catalogo interno del fornitore senza passaggi manuali aggiuntivi.
</div>
</div>
@endif
@if(count($amazonRows) > 0)
<div class="mt-4 grid gap-3 lg:grid-cols-2">
@foreach($amazonRows as $row)

View File

@ -349,11 +349,11 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
</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" />
<input type="text" wire:model.live.debounce.300ms="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">
<select wire:model.live="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
@ -361,7 +361,7 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
</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">
<select wire:model.live="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
@ -406,7 +406,12 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
</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" />
<select wire:model.live="scannerAcquisitionForm.supporto_fisico" class="w-full rounded-lg border-slate-200 text-sm shadow-sm">
<option value="Busta trasparente con anelli da archiviare">Busta ad anelli</option>
<option value="Cartella archivio">Cartella</option>
<option value="Cartellina progetto">Cartellina progetto</option>
<option value="Raccoglitore documenti singoli">Piccolo raccoglitore atti singoli</option>
</select>
</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>
@ -447,6 +452,26 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
<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 class="rounded-xl border bg-white p-4">
<div class="text-sm font-semibold text-slate-900">Primo QR XML del singolo atto</div>
<div class="mt-1 text-xs text-slate-500">Anteprima del payload che verrà salvato nei metadati archivio del documento: XML aperto per il QR stampato, envelope compresso opzionale e profilo NFC ridotto.</div>
<div class="mt-3 rounded-lg border border-slate-200 bg-slate-50 px-3 py-3 text-[11px] font-semibold text-slate-600">Codice archivio preview: {{ $this->scannerQrPreview['code'] !== '' ? $this->scannerQrPreview['code'] : 'in attesa dei dati minimi' }}</div>
<div class="mt-3 space-y-3">
<div>
<div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">QR XML aperto · {{ (int) ($this->scannerQrPreview['xml_open_length'] ?? 0) }} caratteri</div>
<textarea readonly rows="5" class="w-full rounded-lg border-slate-200 bg-white font-mono text-[11px] text-slate-700 shadow-sm">{{ $this->scannerQrPreview['xml_open'] ?? '' }}</textarea>
</div>
<div>
<div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">QR XML compresso</div>
<textarea readonly rows="4" class="w-full rounded-lg border-slate-200 bg-white font-mono text-[11px] text-slate-700 shadow-sm">{{ $this->scannerQrPreview['xml_zip'] ?? '' }}</textarea>
</div>
<div>
<div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">NFC lite · {{ (int) ($this->scannerQrPreview['xml_nfc_length'] ?? 0) }} caratteri</div>
<textarea readonly rows="4" class="w-full rounded-lg border-slate-200 bg-white font-mono text-[11px] text-slate-700 shadow-sm">{{ $this->scannerQrPreview['xml_nfc'] ?? '' }}</textarea>
</div>
</div>
</div>
</div>
</div>
</x-filament::section>

View File

@ -386,6 +386,26 @@
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3">Il QR codifica l'identità archivistica e la relazione col contenitore padre, non l'host o un URL pubblico.</div>
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3">Template, binding campi DB e nodo della catena vengono salvati nell'etichetta, così la stampa reale segue il layout scelto.</div>
</div>
<div class="mt-4 rounded-xl border border-slate-200 bg-slate-50 p-4">
<div class="text-sm font-semibold text-slate-900">Payload XML e NFC</div>
<div class="mt-1 text-xs text-slate-500">Il QR stampato resta aperto e leggibile. Il profilo compresso è opzionale. L'NFC usa una variante minima pensata per il tag.</div>
<div class="mt-3 space-y-3">
<div>
<div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">QR XML aperto · {{ (int) data_get($preview, 'payload.qr_payload_length', 0) }} caratteri</div>
<textarea readonly rows="5" class="w-full rounded-lg border-slate-200 bg-white font-mono text-[11px] text-slate-700 shadow-sm">{{ data_get($preview, 'payload.qr_payload', '') }}</textarea>
</div>
<div>
<div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">QR XML compresso</div>
<textarea readonly rows="4" class="w-full rounded-lg border-slate-200 bg-white font-mono text-[11px] text-slate-700 shadow-sm">{{ data_get($preview, 'payload.qr_payload_zip', '') }}</textarea>
</div>
<div>
<div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">NFC lite · {{ (int) data_get($preview, 'payload.nfc_payload_length', 0) }} caratteri</div>
<textarea readonly rows="4" class="w-full rounded-lg border-slate-200 bg-white font-mono text-[11px] text-slate-700 shadow-sm">{{ data_get($preview, 'payload.nfc_payload', '') }}</textarea>
</div>
</div>
</div>
</div>
</aside>
</div>