diff --git a/app/Console/Commands/NetgesconNodeRefreshCommand.php b/app/Console/Commands/NetgesconNodeRefreshCommand.php index 94457f0..0acc5b1 100644 --- a/app/Console/Commands/NetgesconNodeRefreshCommand.php +++ b/app/Console/Commands/NetgesconNodeRefreshCommand.php @@ -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; diff --git a/app/Filament/Pages/Condomini/AssembleeHub.php b/app/Filament/Pages/Condomini/AssembleeHub.php new file mode 100644 index 0000000..1a5b082 --- /dev/null +++ b/app/Filament/Pages/Condomini/AssembleeHub.php @@ -0,0 +1,109 @@ +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(); + } +} \ No newline at end of file diff --git a/app/Filament/Pages/Fornitore/ProdottiCatalogo.php b/app/Filament/Pages/Fornitore/ProdottiCatalogo.php index 815bf8e..efd31a6 100644 --- a/app/Filament/Pages/Fornitore/ProdottiCatalogo.php +++ b/app/Filament/Pages/Fornitore/ProdottiCatalogo.php @@ -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> */ + 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); + } } diff --git a/app/Filament/Pages/Strumenti/DocumentiArchivio.php b/app/Filament/Pages/Strumenti/DocumentiArchivio.php index 7f11913..32f34fc 100644 --- a/app/Filament/Pages/Strumenti/DocumentiArchivio.php +++ b/app/Filament/Pages/Strumenti/DocumentiArchivio.php @@ -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 */ + 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'] ?? []), diff --git a/app/Models/DocumentoArchivioFisicoItem.php b/app/Models/DocumentoArchivioFisicoItem.php index c91b544..fe94fbd 100644 --- a/app/Models/DocumentoArchivioFisicoItem.php +++ b/app/Models/DocumentoArchivioFisicoItem.php @@ -1,6 +1,7 @@ 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 diff --git a/app/Providers/Filament/AdminFilamentPanelProvider.php b/app/Providers/Filament/AdminFilamentPanelProvider.php index 80e8d79..24087de 100644 --- a/app/Providers/Filament/AdminFilamentPanelProvider.php +++ b/app/Providers/Filament/AdminFilamentPanelProvider.php @@ -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') diff --git a/app/Support/Archivio/ArchiveQrXmlPayload.php b/app/Support/Archivio/ArchiveQrXmlPayload.php new file mode 100644 index 0000000..604e55c --- /dev/null +++ b/app/Support/Archivio/ArchiveQrXmlPayload.php @@ -0,0 +1,127 @@ + $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 = ''; + + foreach ($fields as $tag => $value) { + if ($value === '') { + continue; + } + + $xml .= '<' . $tag . '>' . self::escape($value) . ''; + } + + $xml .= ''; + + return $xml; + } + + /** @param array $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 $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 $data + * @return array + */ + 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 $data + * @return array + */ + 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 $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'); + } +} \ No newline at end of file diff --git a/docs/roadmaps/DOCUMENTI-ARCHIVIO-DAFARE.md b/docs/roadmaps/DOCUMENTI-ARCHIVIO-DAFARE.md index 5f2bd7a..cd1d77e 100644 --- a/docs/roadmaps/DOCUMENTI-ARCHIVIO-DAFARE.md +++ b/docs/roadmaps/DOCUMENTI-ARCHIVIO-DAFARE.md @@ -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. diff --git a/resources/views/filament/pages/condomini/assemblee-hub.blade.php b/resources/views/filament/pages/condomini/assemblee-hub.blade.php new file mode 100644 index 0000000..19de98d --- /dev/null +++ b/resources/views/filament/pages/condomini/assemblee-hub.blade.php @@ -0,0 +1,98 @@ + +
+ @if(! $this->stabileAttivo) +
+ Nessuno stabile attivo disponibile. Seleziona prima uno stabile per aprire l'hub assemblee. +
+ @else +
+
+
+
{{ $this->stabileAttivo->denominazione ?: 'Stabile attivo' }}
+
+ Prima slice HUB Assemblee: convocazioni, cronologia, allegati e avanzamento operativo dello stabile. +
+
+
+ Stabile ID {{ (int) $this->stabileAttivo->id }} +
+
+
+ +
+
+
Assemblee totali
+
{{ (int) ($this->stats['totale'] ?? 0) }}
+
+
+
Convocate
+
{{ (int) ($this->stats['convocate'] ?? 0) }}
+
+
+
In calendario
+
{{ (int) ($this->stats['in_calendario'] ?? 0) }}
+
+
+
Con verbale
+
{{ (int) ($this->stats['con_verbale'] ?? 0) }}
+
+
+ +
+
+
+
Cronologia assemblee
+
Vista iniziale per pilotare convocazioni, allegati, OdG, verbali e ticket derivati nelle prossime slice.
+
+
+ +
+ @forelse($this->assembleeRows as $assemblea) +
+
+
+
+ {{ $assemblea->tipo ?: 'Assemblea ordinaria' }} +
+
+ 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' }} +
+ @if((string) ($assemblea->luogo ?? '') !== '') +
Luogo: {{ $assemblea->luogo }}
+ @endif +
+
+ {{ $assemblea->stato ?: 'da impostare' }} +
+
+ +
+
OdG: {{ (int) ($assemblea->ordine_giorno_count ?? 0) }}
+
Convocazioni: {{ (int) ($assemblea->convocazioni_count ?? 0) }}
+
Presenze: {{ (int) ($assemblea->presenze_count ?? 0) }}
+
Documenti: {{ (int) ($assemblea->documenti_count ?? 0) }}
+
+ + @if((string) ($assemblea->note ?? '') !== '') +
+ {{ \\Illuminate\\Support\\Str::limit((string) $assemblea->note, 220) }} +
+ @endif + +
+ Verbale {{ $assemblea->verbale ? 'presente' : 'non ancora collegato' }}. +
+
+ @empty +
+ Nessuna assemblea registrata per lo stabile attivo. +
+ @endforelse +
+
+ @endif +
+
\ No newline at end of file diff --git a/resources/views/filament/pages/fornitore/prodotti-catalogo.blade.php b/resources/views/filament/pages/fornitore/prodotti-catalogo.blade.php index b5ac16c..7bf9bac 100644 --- a/resources/views/filament/pages/fornitore/prodotti-catalogo.blade.php +++ b/resources/views/filament/pages/fornitore/prodotti-catalogo.blade.php @@ -45,6 +45,28 @@ + @if(! $this->amazonConfigured) +
+
Cosa serve per attivare l'import Amazon
+
+ Per completare l'integrazione servono le credenziali Creators API, i due endpoint operativi e l'attivazione delle variabili ambiente sul nodo dove gira NetGescon. +
+ +
+ @foreach($this->amazonSetupChecklist as $item) +
+
{{ $item['key'] }}{{ $item['required'] ? ' · obbligatoria' : ' · opzionale' }}
+
{{ $item['display'] }}
+
+ @endforeach +
+ +
+ 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. +
+
+ @endif + @if(count($amazonRows) > 0)
@foreach($amazonRows as $row) diff --git a/resources/views/filament/pages/strumenti/documenti-archivio.blade.php b/resources/views/filament/pages/strumenti/documenti-archivio.blade.php index f3400f2..239de2e 100644 --- a/resources/views/filament/pages/strumenti/documenti-archivio.blade.php +++ b/resources/views/filament/pages/strumenti/documenti-archivio.blade.php @@ -349,11 +349,11 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
- +
- @foreach($this->scannerCategoryOptions as $categoryKey => $categoryLabel) @endforeach @@ -361,7 +361,7 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
- @foreach($this->getArchivioClasseOptions() as $classKey => $classLabel) @endforeach @@ -406,7 +406,12 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
- +
@@ -447,6 +452,26 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
Metadati archivio pronti per QR, collocazione fisica e ricerche OCR successive.
+ +
+
Primo QR XML del singolo atto
+
Anteprima del payload che verrà salvato nei metadati archivio del documento: XML aperto per il QR stampato, envelope compresso opzionale e profilo NFC ridotto.
+
Codice archivio preview: {{ $this->scannerQrPreview['code'] !== '' ? $this->scannerQrPreview['code'] : 'in attesa dei dati minimi' }}
+
+
+
QR XML aperto · {{ (int) ($this->scannerQrPreview['xml_open_length'] ?? 0) }} caratteri
+ +
+
+
QR XML compresso
+ +
+
+
NFC lite · {{ (int) ($this->scannerQrPreview['xml_nfc_length'] ?? 0) }} caratteri
+ +
+
+
diff --git a/resources/views/filament/pages/strumenti/partials/generic-label-builder.blade.php b/resources/views/filament/pages/strumenti/partials/generic-label-builder.blade.php index 5f807f0..1c07fd1 100644 --- a/resources/views/filament/pages/strumenti/partials/generic-label-builder.blade.php +++ b/resources/views/filament/pages/strumenti/partials/generic-label-builder.blade.php @@ -386,6 +386,26 @@
Il QR codifica l'identità archivistica e la relazione col contenitore padre, non l'host o un URL pubblico.
Template, binding campi DB e nodo della catena vengono salvati nell'etichetta, così la stampa reale segue il layout scelto.
+ +
+
Payload XML e NFC
+
Il QR stampato resta aperto e leggibile. Il profilo compresso è opzionale. L'NFC usa una variante minima pensata per il tag.
+ +
+
+
QR XML aperto · {{ (int) data_get($preview, 'payload.qr_payload_length', 0) }} caratteri
+ +
+
+
QR XML compresso
+ +
+
+
NFC lite · {{ (int) data_get($preview, 'payload.nfc_payload_length', 0) }} caratteri
+ +
+
+