From 1c09aae635514975c6dc1a1896f87ad4397eb437 Mon Sep 17 00:00:00 2001 From: michele Date: Mon, 20 Apr 2026 22:53:24 +0000 Subject: [PATCH] Fix document ACL and clean up post-it view --- CHANGELOG.md | 5 + .../Pages/Strumenti/DocumentiArchivio.php | 7 + .../Pages/Strumenti/PostItGestione.php | 255 ++++++++++++++++++ app/Models/Documento.php | 2 + .../DocumentoArchivioVisibilityService.php | 45 +++- ...dd_allowed_user_ids_to_documenti_table.php | 26 ++ docs/DOCUMENTI-NFC-E-ACQUISIZIONE.md | 153 +++++++++++ .../strumenti/post-it-gestione.blade.php | 72 ++++- 8 files changed, 556 insertions(+), 9 deletions(-) create mode 100644 database/migrations/2026_04_20_220000_add_allowed_user_ids_to_documenti_table.php create mode 100644 docs/DOCUMENTI-NFC-E-ACQUISIZIONE.md diff --git a/CHANGELOG.md b/CHANGELOG.md index df82099..0c49e67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ # Changelog ## [Unreleased] +- Fixed document archive visibility queries to work safely even when single-user ACL columns are not yet present in older databases, and added the missing `documenti.allowed_user_ids` schema field for per-document access control. +- Restored a cleaner two-column layout in Post-it Gestione boards and hid visual noise from calls that terminate on internal response group `601` while keeping the underlying records in the system. +- Added actionable banking reconciliation for tenant rents and supplier payments directly from the MPS movement queue, with automatic operational matching metadata and supplier RA register synchronization when a withholding-bearing supplier invoice is paid. +- Normalized module manifest handling so active modules can expose permissions, menu items, and metadata consistently even when their `module.json` files use different shapes. +- Added a dedicated single-node Docker install path for local NetGescon distribution tests, with a local-only compose stack, node env example, bootstrap script, backup restore script, and an Ubuntu runbook for restoring a site backup onto a fresh machine. - Consolidated the banking workflow around Contabilita > Casse e banche > Movimenti, now used as a 4-tab hub for account overview, official balances and statement PDFs, imported bank ledger movements, and first-pass reconciliation candidates. - Moved official bank balance and statement management out of Situazione iniziale, fixed bank document URLs to use storage-backed paths, and anchored balance reconstruction on `conto_id` so legacy accounts without IBAN still reconcile correctly. - Added stable-level Documentazione, Posta ufficiale and Protocollo comunicazioni tabs, plus a real admin-wide Comunicazioni regia page reusing Google accounts, Gmail import, Drive templates and protocol records. diff --git a/app/Filament/Pages/Strumenti/DocumentiArchivio.php b/app/Filament/Pages/Strumenti/DocumentiArchivio.php index bd02d38..88aadac 100644 --- a/app/Filament/Pages/Strumenti/DocumentiArchivio.php +++ b/app/Filament/Pages/Strumenti/DocumentiArchivio.php @@ -410,6 +410,12 @@ private function getDocumentoFormSchema(): array ->options(array_combine($this->audienceGroups, $this->audienceGroups)) ->multiple() ->native(false), + Select::make('allowed_user_ids') + ->label('Utenti specifici ammessi') + ->options($this->getCollaboratorUserOptions()) + ->multiple() + ->native(false) + ->searchable(), FileUpload::make('pdf') ->label('PDF') ->disk('local') @@ -792,6 +798,7 @@ private function createDocumentoFromForm(array $data, bool $isDemo): void 'visibility_scope' => (string) ($data['visibility_scope'] ?? 'interno'), 'visibility_roles' => $this->normalizeStringArray($data['visibility_roles'] ?? []), 'visibility_groups' => $this->normalizeStringArray($data['visibility_groups'] ?? []), + 'allowed_user_ids' => $this->normalizeIntArray($data['allowed_user_ids'] ?? []), 'data_upload' => now(), 'note' => $note, 'is_demo' => $isDemo, diff --git a/app/Filament/Pages/Strumenti/PostItGestione.php b/app/Filament/Pages/Strumenti/PostItGestione.php index 46a5123..c8523b6 100644 --- a/app/Filament/Pages/Strumenti/PostItGestione.php +++ b/app/Filament/Pages/Strumenti/PostItGestione.php @@ -4,6 +4,7 @@ use App\Filament\Pages\Gescon\RubricaUniversaleScheda; use App\Models\ChiamataPostIt; use App\Models\CommunicationMessage; +use App\Models\Fornitore; use App\Models\PbxClickToCallRequest; use App\Models\RubricaUniversale; use App\Models\Ticket; @@ -17,6 +18,7 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Schema; use Illuminate\Support\Str; +use App\Support\StabileContext; use UnitEnum; class PostItGestione extends Page @@ -69,6 +71,16 @@ class PostItGestione extends Page public ?int $selectedAppuntoId = null; + public ?string $selectedAppuntoOggetto = null; + + public ?string $selectedAppuntoNota = null; + + public string $appuntoAssegnazioneTipo = 'operatore'; + + public ?int $appuntoAssegnazioneUserId = null; + + public ?int $appuntoAssegnazioneFornitoreId = null; + /** @var array */ public array $riaperturaNote = []; @@ -87,6 +99,9 @@ class PostItGestione extends Page /** @var array */ private array $monitoredResponseGroups = ['601', '603']; + /** @var array */ + private array $hiddenVisualizationResponseGroups = ['601']; + public function mount(): void { $focus = (int) request()->query('focus_post_it', 0); @@ -100,6 +115,12 @@ public function mount(): void public function selectAppunto(int $postItId): void { $this->selectedAppuntoId = $postItId > 0 ? $postItId : null; + $this->loadSelectedAppuntoState(); + } + + public function updatedSelectedAppuntoId(): void + { + $this->loadSelectedAppuntoState(); } public function getPostItInserimentoUrl(): string @@ -245,6 +266,101 @@ public function riapriPostIt(int $postItId): void ->send(); } + public function saveSelectedAppunto(): void + { + if (! $this->isPostItTableReady() || ! $this->selectedAppuntoId) { + Notification::make()->title('Seleziona prima un appunto')->warning()->send(); + return; + } + + $this->validate([ + 'selectedAppuntoOggetto' => ['nullable', 'string', 'max:255'], + 'selectedAppuntoNota' => ['nullable', 'string'], + ]); + + $postIt = ChiamataPostIt::query()->find($this->selectedAppuntoId); + if (! $postIt) { + Notification::make()->title('Post-it non trovato')->danger()->send(); + return; + } + + $postIt->oggetto = $this->normalizeOptionalText($this->selectedAppuntoOggetto); + $postIt->nota = $this->normalizeOptionalText($this->selectedAppuntoNota); + $postIt->save(); + + Notification::make()->title('Appunto aggiornato')->success()->send(); + } + + public function assegnaSelectedAppunto(): void + { + if (! $this->isPostItAssignmentReady() || ! $this->selectedAppuntoId) { + Notification::make()->title('Assegnazione non disponibile')->warning()->send(); + return; + } + + $postIt = ChiamataPostIt::query()->find($this->selectedAppuntoId); + if (! $postIt) { + Notification::make()->title('Post-it non trovato')->danger()->send(); + return; + } + + $this->validate([ + 'appuntoAssegnazioneTipo' => ['required', 'in:operatore,collaboratore,fornitore'], + 'appuntoAssegnazioneUserId' => ['nullable', 'integer', 'exists:users,id'], + 'appuntoAssegnazioneFornitoreId' => ['nullable', 'integer', 'exists:fornitori,id'], + ]); + + $payload = [ + 'assegnazione_tipo' => $this->appuntoAssegnazioneTipo, + 'assegnato_a_user_id' => null, + 'assegnato_a_fornitore_id' => null, + ]; + + if (in_array($this->appuntoAssegnazioneTipo, ['operatore', 'collaboratore'], true)) { + if (! $this->appuntoAssegnazioneUserId) { + Notification::make()->title('Seleziona un utente')->warning()->send(); + return; + } + + $payload['assegnato_a_user_id'] = $this->appuntoAssegnazioneUserId; + } + + if ($this->appuntoAssegnazioneTipo === 'fornitore') { + if (! $this->appuntoAssegnazioneFornitoreId) { + Notification::make()->title('Seleziona un fornitore')->warning()->send(); + return; + } + + $payload['assegnato_a_fornitore_id'] = $this->appuntoAssegnazioneFornitoreId; + } + + $postIt->update($payload); + + if ($postIt->ticket) { + $ticketPayload = []; + + if ($this->appuntoAssegnazioneTipo === 'fornitore') { + $ticketPayload['assegnato_a_fornitore_id'] = $this->appuntoAssegnazioneFornitoreId; + $ticketPayload['assegnato_a_user_id'] = null; + } else { + $ticketPayload['assegnato_a_user_id'] = $this->appuntoAssegnazioneUserId; + } + + if ($ticketPayload !== []) { + $postIt->ticket->update($ticketPayload); + } + + if (method_exists($postIt->ticket, 'messages') && Schema::hasTable('ticket_messages')) { + $postIt->ticket->messages()->create([ + 'user_id' => Auth::id(), + 'messaggio' => 'Assegnazione da Gestione Post-it: ' . $this->getSelectedAppuntoAssignmentLabel(), + ]); + } + } + + Notification::make()->title('Assegnazione aggiornata')->success()->send(); + } + public function getRecentiProperty() { if (! $this->isPostItTableReady()) { @@ -280,6 +396,10 @@ private function shouldShowPostIt(ChiamataPostIt $postIt): bool return false; } + if ($this->isHiddenVisualizationResponseGroupPostIt($postIt)) { + return false; + } + return ! $this->isLegacyFilteredSmdrPostIt($postIt); } @@ -412,6 +532,62 @@ public function getSelectedAppuntoProperty(): ?ChiamataPostIt ->find($this->selectedAppuntoId); } + public function isPostItAssignmentReady(): bool + { + return $this->isPostItTableReady() + && Schema::hasColumn('chiamate_post_it', 'assegnazione_tipo') + && Schema::hasColumn('chiamate_post_it', 'assegnato_a_user_id') + && Schema::hasColumn('chiamate_post_it', 'assegnato_a_fornitore_id'); + } + + public function getOperatoriOptionsProperty(): array + { + return User::query() + ->role(['super-admin', 'admin', 'amministratore']) + ->orderBy('name') + ->limit(100) + ->pluck('name', 'id') + ->map(fn($name): string => (string) $name) + ->all(); + } + + public function getCollaboratoriOptionsProperty(): array + { + $adminId = $this->resolveCurrentAmministratoreId(); + + return User::query() + ->whereHas('roles', function ($q): void { + $q->where('name', 'collaboratore'); + }) + ->when($adminId > 0, function ($query) use ($adminId): void { + $query->whereHas('stabiliAssegnati', function ($inner) use ($adminId): void { + $inner->where('amministratore_id', $adminId); + }); + }) + ->orderBy('name') + ->limit(150) + ->pluck('name', 'id') + ->map(fn($name): string => (string) $name) + ->all(); + } + + public function getFornitoriOptionsProperty(): array + { + $adminId = $this->resolveCurrentAmministratoreId(); + + return Fornitore::query() + ->when($adminId > 0, fn($query) => $query->where('amministratore_id', $adminId)) + ->orderByRaw("COALESCE(ragione_sociale, CONCAT(COALESCE(nome, ''), ' ', COALESCE(cognome, '')))") + ->limit(150) + ->get(['id', 'ragione_sociale', 'nome', 'cognome']) + ->mapWithKeys(function (Fornitore $fornitore): array { + $label = trim((string) ($fornitore->ragione_sociale ?: (($fornitore->nome ?? '') . ' ' . ($fornitore->cognome ?? '')))); + + return [(int) $fornitore->id => ($label !== '' ? $label : ('Fornitore #' . $fornitore->id))]; + }) + ->all(); + } + public function getOperationalNoteSummary(ChiamataPostIt $postIt, int $limit = 180): string { return Str::limit($this->extractOperationalNoteContent($postIt), $limit); @@ -918,6 +1094,63 @@ private function normalizeGroupedCallerKey(string $value): string return $normalized !== '' ? $normalized : 'sconosciuto'; } + private function loadSelectedAppuntoState(): void + { + $postIt = $this->selectedAppunto; + if (! $postIt) { + $this->selectedAppuntoOggetto = null; + $this->selectedAppuntoNota = null; + $this->appuntoAssegnazioneTipo = 'operatore'; + $this->appuntoAssegnazioneUserId = null; + $this->appuntoAssegnazioneFornitoreId = null; + return; + } + + $this->selectedAppuntoOggetto = $postIt->oggetto; + $this->selectedAppuntoNota = $postIt->nota; + $this->appuntoAssegnazioneTipo = in_array((string) ($postIt->assegnazione_tipo ?? ''), ['operatore', 'collaboratore', 'fornitore'], true) + ? (string) $postIt->assegnazione_tipo + : 'operatore'; + $this->appuntoAssegnazioneUserId = isset($postIt->assegnato_a_user_id) ? (int) $postIt->assegnato_a_user_id : null; + $this->appuntoAssegnazioneFornitoreId = isset($postIt->assegnato_a_fornitore_id) ? (int) $postIt->assegnato_a_fornitore_id : null; + } + + private function getSelectedAppuntoAssignmentLabel(): string + { + return match ($this->appuntoAssegnazioneTipo) { + 'fornitore' => 'fornitore #' . (int) ($this->appuntoAssegnazioneFornitoreId ?? 0), + 'collaboratore' => 'collaboratore #' . (int) ($this->appuntoAssegnazioneUserId ?? 0), + default => 'operatore #' . (int) ($this->appuntoAssegnazioneUserId ?? 0), + }; + } + + private function resolveCurrentAmministratoreId(): int + { + $user = Auth::user(); + if (! $user instanceof User) { + return 0; + } + + if ($user->amministratore) { + return (int) $user->amministratore->id; + } + + $stabile = StabileContext::getActiveStabile($user); + + return (int) ($stabile?->amministratore_id ?? 0); + } + + private function normalizeOptionalText(mixed $value): ?string + { + if (! is_string($value)) { + return null; + } + + $text = trim($value); + + return $text !== '' ? $text : null; + } + private function normalizeGroupedPhone(string $phone): string { $digits = preg_replace('/\D+/', '', $phone) ?: ''; @@ -1390,6 +1623,28 @@ private function isMonitoredResponseGroupExtension(?string $extension): bool return in_array($extension, $this->monitoredResponseGroups, true); } + private function isHiddenVisualizationResponseGroupPostIt(ChiamataPostIt $postIt): bool + { + $message = $this->resolveMessageFromPostIt($postIt); + if (! $message instanceof CommunicationMessage || (string) $message->channel !== 'smdr') { + return false; + } + + $extensions = array_values(array_unique(array_filter([ + $this->normalizeExtension((string) ($message->target_extension ?? '')), + $this->normalizeExtension((string) data_get($message->metadata, 'smdr.extension', '')), + $this->normalizeExtension((string) data_get($message->metadata, 'smdr.target_extension', '')), + ]))); + + foreach ($extensions as $extension) { + if (in_array($extension, $this->hiddenVisualizationResponseGroups, true)) { + return true; + } + } + + return false; + } + public function getPostItCallStateMeta(ChiamataPostIt $postIt): array { if ($this->isMissedPostIt($postIt)) { diff --git a/app/Models/Documento.php b/app/Models/Documento.php index f390cbf..0b34f6b 100755 --- a/app/Models/Documento.php +++ b/app/Models/Documento.php @@ -61,6 +61,7 @@ class Documento extends Model 'visibility_scope', 'visibility_groups', 'visibility_roles', + 'allowed_user_ids', 'approvato', 'archiviato', 'urgente', @@ -78,6 +79,7 @@ class Documento extends Model 'metadati_ocr' => 'array', 'visibility_groups' => 'array', 'visibility_roles' => 'array', + 'allowed_user_ids' => 'array', 'dimensione_file' => 'integer', 'data_documento' => 'date', 'data_scadenza' => 'date', diff --git a/app/Services/Documenti/DocumentoArchivioVisibilityService.php b/app/Services/Documenti/DocumentoArchivioVisibilityService.php index fd18312..6899ade 100644 --- a/app/Services/Documenti/DocumentoArchivioVisibilityService.php +++ b/app/Services/Documenti/DocumentoArchivioVisibilityService.php @@ -8,6 +8,7 @@ use App\Models\User; use App\Support\StabileContext; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Facades\Schema; class DocumentoArchivioVisibilityService { @@ -94,35 +95,67 @@ public function canAccessDocumentoStabile(User $user, DocumentoStabile $document private function applyVisibilityConstraint(Builder $query, User $user, string $table): void { + if (! $this->hasVisibilityColumn($table, 'visibility_scope')) { + return; + } + $roles = $user->getRoleNames()->map(fn($role): string => (string) $role)->values()->all(); $groupLabels = $this->resolveAudienceGroupsForUser($user); $userId = (int) $user->id; + $hasRolesColumn = $this->hasVisibilityColumn($table, 'visibility_roles'); + $hasGroupsColumn = $this->hasVisibilityColumn($table, 'visibility_groups'); + $hasAllowedUsersColumn = $this->hasVisibilityColumn($table, 'allowed_user_ids'); - $query->where(function (Builder $visibilityQuery) use ($table, $roles, $groupLabels, $userId): void { + $query->where(function (Builder $visibilityQuery) use ($table, $roles, $groupLabels, $userId, $hasRolesColumn, $hasGroupsColumn, $hasAllowedUsersColumn): void { $visibilityQuery->whereNull($table . '.visibility_scope') ->orWhere($table . '.visibility_scope', 'interno') ->orWhere($table . '.visibility_scope', 'studio') - ->orWhere(function (Builder $restrictedQuery) use ($table, $roles, $groupLabels, $userId): void { + ->orWhere(function (Builder $restrictedQuery) use ($table, $roles, $groupLabels, $userId, $hasRolesColumn, $hasGroupsColumn, $hasAllowedUsersColumn): void { $restrictedQuery->where($table . '.visibility_scope', 'restricted') - ->where(function (Builder $matchQuery) use ($table, $roles, $groupLabels, $userId): void { - if ($roles !== []) { + ->where(function (Builder $matchQuery) use ($table, $roles, $groupLabels, $userId, $hasRolesColumn, $hasGroupsColumn, $hasAllowedUsersColumn): void { + $hasAudienceConstraint = false; + + if ($hasRolesColumn && $roles !== []) { foreach ($roles as $role) { $matchQuery->orWhereJsonContains($table . '.visibility_roles', $role); } + + $hasAudienceConstraint = true; } - if ($groupLabels !== []) { + if ($hasGroupsColumn && $groupLabels !== []) { foreach ($groupLabels as $groupLabel) { $matchQuery->orWhereJsonContains($table . '.visibility_groups', $groupLabel); } + + $hasAudienceConstraint = true; } - $matchQuery->orWhereJsonContains($table . '.allowed_user_ids', $userId); + if ($hasAllowedUsersColumn) { + $matchQuery->orWhereJsonContains($table . '.allowed_user_ids', $userId); + $hasAudienceConstraint = true; + } + + if (! $hasAudienceConstraint) { + $matchQuery->whereRaw('1 = 0'); + } }); }); }); } + private function hasVisibilityColumn(string $table, string $column): bool + { + static $cache = []; + + $cacheKey = $table . '.' . $column; + if (! array_key_exists($cacheKey, $cache)) { + $cache[$cacheKey] = Schema::hasColumn($table, $column); + } + + return $cache[$cacheKey]; + } + private function resolveAudienceGroupsForUser(User $user): array { $groups = []; diff --git a/database/migrations/2026_04_20_220000_add_allowed_user_ids_to_documenti_table.php b/database/migrations/2026_04_20_220000_add_allowed_user_ids_to_documenti_table.php new file mode 100644 index 0000000..3614e18 --- /dev/null +++ b/database/migrations/2026_04_20_220000_add_allowed_user_ids_to_documenti_table.php @@ -0,0 +1,26 @@ +json('allowed_user_ids')->nullable()->after('visibility_roles'); + } + }); + } + + public function down(): void + { + Schema::table('documenti', function (Blueprint $table): void { + if (Schema::hasColumn('documenti', 'allowed_user_ids')) { + $table->dropColumn('allowed_user_ids'); + } + }); + } +}; \ No newline at end of file diff --git a/docs/DOCUMENTI-NFC-E-ACQUISIZIONE.md b/docs/DOCUMENTI-NFC-E-ACQUISIZIONE.md new file mode 100644 index 0000000..9d104de --- /dev/null +++ b/docs/DOCUMENTI-NFC-E-ACQUISIZIONE.md @@ -0,0 +1,153 @@ +# Documenti, NFC e acquisizione + +## Stato attuale archivio documenti + +La pagina Strumenti > Documenti gestisce gia un flusso rapido di archiviazione: + +- selezione stabile; +- scelta cartella archivio con ACL; +- categoria protocollo e titolo; +- metadati principali del documento; +- upload PDF; +- protocollo progressivo automatico; +- collegamento eventuale a movimento contabile; +- indicazione faldone fisico per l'archivio cartaceo. + +Dopo il salvataggio il documento viene gia memorizzato con: + +- file PDF archiviato; +- protocollo interno; +- cartella logica; +- visibilita per ruolo, gruppo e utente singolo; +- estrazione testo best-effort per ricerca tramite `PdfTextExtractionService`. + +## Indicizzazione e OCR + +La base applicativa e gia predisposta per: + +- testo OCR o testo estratto dal PDF; +- metadati OCR; +- ricerca documentale su contenuto; +- classificazione guidata con cartelle e categorie. + +La parte da verificare operativamente e la qualita reale dell'estrazione sui documenti scansionati e sui PDF immagine. Il prossimo test utile e: + +1. caricare un PDF nativo; +2. caricare un PDF da scanner; +3. verificare contenuto OCR salvato e ricercabilita. + +## PDF watermark e firma + +Fattibile lato server: + +- watermark visivo con protocollo, stabile, data e utente; +- timbro di archiviazione; +- overlay su PDF acquisiti; +- ristampa etichetta o frontespizio. + +Da distinguere dalla firma digitale vera: + +- watermark: semplice e interno, subito fattibile; +- firma PAdES/CAdES: richiede certificato, firma remota o dispositivo di firma. + +Quindi la strada pragmatica e: + +1. watermark/timbro immediato per i PDF archiviati; +2. firma digitale solo quando decidiamo provider o certificato operativo. + +## Short link con bcards.it + +Il dominio `bcards.it` si presta bene a generare short link per: + +- apertura rapida cartella archivio; +- apertura scheda stabile; +- apertura documento o checklist; +- accesso a presenza assemblea; +- accesso a votazione assemblea; +- accesso a pagina di scansione o acquisizione. + +Architettura consigliata: + +- short code univoco nel dominio corto; +- redirect verso URL NetGescon firmata o tokenizzata; +- scadenza opzionale; +- tracciamento accessi; +- associazione a tag NFC o QR. + +## iPhone e autenticazione dispositivo + +Safari su iPhone non espone seriale hardware, IMEI o MAC address alla pagina web. + +La soluzione corretta e: + +- primo accesso con login normale; +- enrollment del dispositivo; +- passkey/WebAuthn o token revocabile; +- NFC che apre short link; +- il browser completa l'autenticazione con la credenziale gia registrata. + +## ACR122U e tag NFC da PC + +Con il lettore/scrittore ACR122U su PC si puo lavorare, ma non in modo affidabile da una semplice pagina web pura per tutti i browser. + +Strade realistiche: + +- Chrome/Edge desktop con WebUSB/WebHID: possibile solo in alcuni scenari, non ideale come base di produzione; +- bridge locale su PC con PC/SC o libnfc: soluzione robusta; +- pagina web NetGescon che dialoga con un piccolo servizio locale: soluzione consigliata. + +Uso previsto: + +- leggere UID o NDEF del tag; +- scrivere short link `bcards.it/...`; +- associare il tag a faldone, cartella, documento o stabile; +- richiamare in un click la scheda giusta. + +## Presenze e votazioni assemblea + +Fattibile nello stesso impianto: + +- convocazione assemblea; +- check-in presenza via link corto, QR o NFC; +- conteggio deleghe e millesimi presenti; +- apertura votazioni per punto ODG; +- raccolta voto da device autenticato; +- verbale e report finale. + +Per farlo bene conviene usare due livelli: + +- regia assemblea da desktop operatore; +- accesso rapido da smartphone per presenza e voto quando previsto. + +## Scanner Epson da pagina web + +Pilotare uno scanner Epson direttamente da una pagina web standard non e generalmente affidabile. + +Le strade reali sono: + +- scanner di rete che salva in cartella o invia via mail; +- agente locale Windows che usa TWAIN/WIA/SDK Epson; +- pagina web che invia il comando al servizio locale e acquisisce il PDF risultante. + +Per operativita immediata la soluzione piu semplice e: + +1. scansione verso cartella controllata; +2. upload/ingestione rapida in NetGescon; +3. OCR e archiviazione automatica. + +## Dymo LAN 11354 + +Domani si puo provare una stampa etichetta su rete con formato `11354` usando: + +- template HTML/PDF per etichetta; +- spool di stampa verso stampante LAN; +- contenuti minimi: codice stabile, tipo archivio, faldone/cartella, short link o QR. + +## 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. \ No newline at end of file diff --git a/resources/views/filament/pages/strumenti/post-it-gestione.blade.php b/resources/views/filament/pages/strumenti/post-it-gestione.blade.php index 6aa4487..c26300c 100644 --- a/resources/views/filament/pages/strumenti/post-it-gestione.blade.php +++ b/resources/views/filament/pages/strumenti/post-it-gestione.blade.php @@ -189,7 +189,22 @@
Appunto completo
-
{{ $this->selectedAppunto->nota ?: '—' }}
+
+ + +
+ Salva appunto completo + @if(! $this->selectedAppunto->ticket_id && $this->selectedAppunto->stato !== 'chiusa') + Converti in ticket + @endif +
+
@@ -203,6 +218,57 @@
+ @if($this->isPostItAssignmentReady()) +
+
Assegnazione
+
+ + + @if($appuntoAssegnazioneTipo === 'operatore') + + @elseif($appuntoAssegnazioneTipo === 'collaboratore') + + @else + + @endif +
+
+ Salva assegnazione +
+
+ @endif +
Apri scheda Post-it @if($this->selectedAppunto->rubrica) @@ -226,7 +292,7 @@

Qui vedi i Post-it raggruppati per numero chiamante. Le chiamate interne restano nel database ma non vengono visualizzate.

-
+
@forelse($this->recentiRaggruppati as $gruppo) @php($box = $gruppo['latest'])
@@ -306,7 +372,7 @@

I Post-it chiusi restano collegati al numero o al nominativo e possono essere riaperti quando serve. Anche qui le chiamate interne sono escluse.

-
+
@forelse($this->recentiChiusiRaggruppati as $gruppo) @php($box = $gruppo['latest'])