diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a374fe..de6e63c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ # Changelog ## [Unreleased] - Initial open-source release prep. +- Added immediate local preview for Ticket Mobile photos and attachments before final send on phone. +- Split insurance handling out of Ticket Gestione into dedicated Tab 4 and added centralized Supporto claims list. +- Fixed supplier ticket-detail access by resolving both intervention-id and ticket-id safely in supplier context. +- Added supplier quick modal from ticket list plus ticket-attachment thumbnails and modal preview in supplier scheda. +- Aligned supplier collaborator external-supplier search to the same result-selection pattern already used in ticket assignment. - Added Git-first staging update guidance in Supporto > Modifiche, centered on the `netgescon:git-sync` command and UI sync flow from Gitea. - Added materialization of `persone` and `persone_unita_relazioni` from imported unit nominatives, plus a dedicated `relazioni` step in `gescon:import-full`. - Hardened Panasonic Windows live bridge scripts, runtime publish flow, and TAPI diagnostics for local-path deployment and aggregated Panasonic addresses. diff --git a/app/Filament/Pages/Fornitore/Collaboratori.php b/app/Filament/Pages/Fornitore/Collaboratori.php index aa8b92b..1991d32 100644 --- a/app/Filament/Pages/Fornitore/Collaboratori.php +++ b/app/Filament/Pages/Fornitore/Collaboratori.php @@ -97,6 +97,9 @@ class Collaboratori extends Page /** @var array> */ public array $fornitoreOptions = []; + /** @var array> */ + public array $fornitoreMatches = []; + public static function canAccess(): bool { $user = Auth::user(); @@ -125,6 +128,7 @@ public function updatedCollaboratoreTipo(): void if ($this->collaboratoreTipo !== FornitoreDipendente::TIPO_FORNITORE_ESTERNO) { $this->fornitoreEsternoId = null; $this->fornitoreSearch = ''; + $this->fornitoreMatches = []; $this->createUserAccess = false; } @@ -136,6 +140,32 @@ public function updatedFornitoreSearch(): void $this->refreshFornitoreOptions(); } + public function selezionaFornitoreEsterno(int $fornitoreId): void + { + $match = collect($this->fornitoreMatches)->firstWhere('id', $fornitoreId); + + if (! is_array($match)) { + $fornitore = $this->resolveFornitoreEsterno($fornitoreId); + if (! $fornitore instanceof Fornitore) { + return; + } + + $match = $this->mapFornitoreOption($fornitore); + } + + $this->fornitoreEsternoId = (int) $match['id']; + $this->fornitoreSearch = (string) $match['label']; + $this->fornitoreMatches = []; + } + + public function resetFornitoreEsternoSelection(): void + { + $this->fornitoreEsternoId = null; + $this->fornitoreSearch = ''; + $this->fornitoreMatches = []; + $this->refreshFornitoreOptions(); + } + public function createCollaboratore(): void { if (! $this->fornitore instanceof Fornitore) { @@ -467,6 +497,7 @@ protected function refreshFornitoreOptions(): void { if (! $this->fornitore instanceof Fornitore || $this->collaboratoreTipo !== FornitoreDipendente::TIPO_FORNITORE_ESTERNO) { $this->fornitoreOptions = []; + $this->fornitoreMatches = []; return; } @@ -491,17 +522,19 @@ function (Builder $query) use ($search): void { ->orderByRaw('CASE WHEN amministratore_id = ? THEN 0 ELSE 1 END', [(int) ($this->fornitore->amministratore_id ?? 0)]) ->orderByRaw('COALESCE(ragione_sociale, nome, cognome)') ->limit(20) - ->get(['id', 'ragione_sociale', 'nome', 'cognome', 'email', 'telefono', 'cellulare', 'tags']) - ->map(fn(Fornitore $fornitore) => [ - 'id' => (int) $fornitore->id, - 'label' => $this->getFornitoreLabel($fornitore), - 'meta' => trim(collect([ - $fornitore->email, - $fornitore->telefono ?: $fornitore->cellulare, - $fornitore->tags, - ])->filter()->implode(' · ')), - ]) + ->get(['id', 'ragione_sociale', 'nome', 'cognome', 'email', 'telefono', 'cellulare', 'tags', 'note']) + ->map(fn(Fornitore $fornitore) => $this->mapFornitoreOption($fornitore)) ->all(); + + $this->fornitoreMatches = $this->fornitoreOptions; + + if ($this->fornitoreEsternoId && ! collect($this->fornitoreMatches)->contains('id', $this->fornitoreEsternoId)) { + $selected = $this->resolveFornitoreEsterno((int) $this->fornitoreEsternoId); + if ($selected instanceof Fornitore) { + array_unshift($this->fornitoreMatches, $this->mapFornitoreOption($selected)); + $this->fornitoreMatches = array_slice($this->fornitoreMatches, 0, 20); + } + } } protected function resolveFornitoreEsterno(int $fornitoreId): ?Fornitore @@ -537,12 +570,30 @@ protected function resetForm(): void $this->collaboratoreTipo = FornitoreDipendente::TIPO_INTERNO; $this->fornitoreSearch = ''; $this->fornitoreEsternoId = null; + $this->fornitoreMatches = []; $this->note = ''; $this->createUserAccess = false; $this->accessPassword = ''; $this->refreshFornitoreOptions(); } + /** + * @return array + */ + protected function mapFornitoreOption(Fornitore $fornitore): array + { + return [ + 'id' => (int) $fornitore->id, + 'label' => $this->getFornitoreLabel($fornitore), + 'meta' => trim(collect([ + $fornitore->email, + $fornitore->telefono ?: $fornitore->cellulare, + $fornitore->tags, + ])->filter()->implode(' · ')), + 'note' => Str::limit(trim((string) ($fornitore->note ?? '')), 120), + ]; + } + protected function upsertRubricaInterna(array $validated, FornitoreDipendente $dipendente): int { $email = trim((string) ($validated['email'] ?? '')); diff --git a/app/Filament/Pages/Fornitore/TicketInterventoScheda.php b/app/Filament/Pages/Fornitore/TicketInterventoScheda.php index b7e37c4..3407d64 100644 --- a/app/Filament/Pages/Fornitore/TicketInterventoScheda.php +++ b/app/Filament/Pages/Fornitore/TicketInterventoScheda.php @@ -11,6 +11,7 @@ use BackedEnum; use Filament\Notifications\Notification; use Filament\Pages\Page; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Schema; use Livewire\WithFileUploads; @@ -91,6 +92,8 @@ class TicketInterventoScheda extends Page public string $qrToken = ''; + public ?array $attachmentPreview = null; + public static function canAccess(): bool { $user = Auth::user(); @@ -101,10 +104,26 @@ public static function canAccess(): bool public function mount(int | string $record): void { - $this->intervento = TicketIntervento::query()->findOrFail((int) $record); + $recordId = (int) $record; + abort_unless($recordId > 0, 404); - [$fornitore, $dipendente] = $this->resolveOperatoreContext((int) $this->intervento->fornitore_id); - abort_unless($fornitore instanceof Fornitore, 404); + $user = Auth::user(); + $isAdmin = $user instanceof User && $user->hasAnyRole(['super-admin', 'admin', 'amministratore']); + + if ($isAdmin) { + $intervento = $this->resolveInterventoRecord($recordId); + abort_unless($intervento instanceof TicketIntervento, 404); + + [$fornitore, $dipendente] = $this->resolveOperatoreContext((int) $intervento->fornitore_id); + } else { + [$fornitore, $dipendente] = $this->resolveOperatoreContext(); + abort_unless($fornitore instanceof Fornitore, 403, 'Profilo fornitore non collegato.'); + + $intervento = $this->resolveInterventoRecord($recordId, $fornitore, $dipendente); + abort_unless($intervento instanceof TicketIntervento, 404, 'Ticket/intervento non disponibile per questo fornitore.'); + } + + $this->intervento = $intervento; $this->fornitore = $fornitore; $this->dipendente = $dipendente; @@ -305,6 +324,7 @@ public function saveRapporto(): void $this->messaggioVeloce = ''; $this->proformaCodice = ''; $this->proformaNote = ''; + $this->dispatch('fornitore-local-previews-clear'); $this->reloadIntervento(); @@ -332,6 +352,34 @@ public function getTicketsUrl(): string return TicketOperativi::getUrl(['fornitore' => (int) $this->fornitore->id], panel: 'admin-filament'); } + public function getAttachmentPublicUrl(TicketAttachment $attachment): string + { + return route('filament.tickets.attachments.view', ['attachment' => (int) $attachment->id]); + } + + public function openAttachmentPreview(int $attachmentId): void + { + $attachment = $this->intervento->ticket->attachments->firstWhere('id', $attachmentId); + if (! $attachment instanceof TicketAttachment) { + return; + } + + $this->attachmentPreview = [ + 'id' => (int) $attachment->id, + 'name' => (string) ($attachment->original_file_name ?? 'allegato'), + 'description' => (string) ($attachment->description ?? ''), + 'url' => $this->getAttachmentPublicUrl($attachment), + 'is_image' => $attachment->isImage(), + 'is_pdf' => $attachment->isPdf(), + 'mime' => $attachment->resolvedMimeType(), + ]; + } + + public function closeAttachmentPreview(): void + { + $this->attachmentPreview = null; + } + public function getCollaboratoriUrl(): string { return Collaboratori::getUrl(['fornitore' => (int) $this->fornitore->id], panel: 'admin-filament'); @@ -342,6 +390,31 @@ public function canAssignDipendente(): bool return ! ($this->dipendente instanceof FornitoreDipendente) && count($this->dipendentiOptions) > 0; } + protected function resolveInterventoRecord(int $recordId, ?Fornitore $fornitore = null, ?FornitoreDipendente $dipendente = null): ?TicketIntervento + { + $query = TicketIntervento::query(); + + if ($fornitore instanceof Fornitore) { + $query->where('fornitore_id', (int) $fornitore->id); + } + + if ($dipendente instanceof FornitoreDipendente) { + $query->where(function (Builder $builder) use ($dipendente): void { + $builder->whereNull('eseguito_da_dipendente_id') + ->orWhere('eseguito_da_dipendente_id', (int) $dipendente->id); + }); + } + + return $query + ->where(function (Builder $builder) use ($recordId): void { + $builder->whereKey($recordId) + ->orWhere('ticket_id', $recordId); + }) + ->orderByRaw('CASE WHEN id = ? THEN 0 ELSE 1 END', [$recordId]) + ->latest('id') + ->first(); + } + protected function reloadIntervento(): void { $this->intervento->load([ diff --git a/app/Filament/Pages/Fornitore/TicketOperativi.php b/app/Filament/Pages/Fornitore/TicketOperativi.php index a331ccd..f3ea09e 100644 --- a/app/Filament/Pages/Fornitore/TicketOperativi.php +++ b/app/Filament/Pages/Fornitore/TicketOperativi.php @@ -4,9 +4,11 @@ use App\Filament\Pages\Fornitore\Concerns\ResolvesOperatoreContext; use App\Models\Fornitore; use App\Models\FornitoreDipendente; +use App\Models\TicketAttachment; use App\Models\TicketIntervento; use App\Models\User; use BackedEnum; +use Filament\Notifications\Notification; use Filament\Pages\Page; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Facades\Auth; @@ -48,6 +50,8 @@ class TicketOperativi extends Page 'fatturabili' => 0, ]; + public ?array $detailModal = null; + public static function canAccess(): bool { $user = Auth::user(); @@ -148,6 +152,71 @@ public function getContabilitaUrl(): string return Contabilita::getUrl(panel: 'admin-filament'); } + public function openInterventoModal(int $interventoId): void + { + if (! $this->fornitoreId) { + Notification::make()->title('Seleziona prima un fornitore')->warning()->send(); + return; + } + + [$fornitore, $dipendente] = $this->resolveOperatoreContext($this->fornitoreId); + + $intervento = $this->buildBaseQuery($fornitore, $dipendente) + ->with(['ticket.attachments.user', 'ticket.messages.user']) + ->find($interventoId); + + if (! $intervento instanceof TicketIntervento) { + Notification::make()->title('Intervento non disponibile per questo fornitore')->danger()->send(); + return; + } + + $base = $this->buildInterventoRow($intervento); + $attachments = $intervento->ticket->attachments + ->map(function (TicketAttachment $attachment): array { + return [ + 'id' => (int) $attachment->id, + 'name' => (string) ($attachment->original_file_name ?? 'allegato'), + 'description' => (string) ($attachment->description ?? ''), + 'url' => route('filament.tickets.attachments.view', ['attachment' => (int) $attachment->id]), + 'is_image' => $attachment->isImage(), + 'is_pdf' => $attachment->isPdf(), + 'created_at' => optional($attachment->created_at)->format('d/m/Y H:i') ?: '-', + ]; + }) + ->all(); + + $messages = $intervento->ticket->messages + ->sortByDesc('created_at') + ->take(6) + ->map(fn($message) => [ + 'author' => (string) ($message->user->name ?? 'Sistema'), + 'body' => (string) ($message->messaggio ?? ''), + 'created_at' => optional($message->created_at)->format('d/m/Y H:i') ?: '-', + ]) + ->values() + ->all(); + + $this->detailModal = [ + 'id' => (int) $intervento->id, + 'ticket_id' => (int) $intervento->ticket_id, + 'titolo' => (string) ($intervento->ticket->titolo ?? '-'), + 'stato' => (string) $intervento->stato, + 'stabile' => (string) ($intervento->ticket->stabile->denominazione ?? '-'), + 'contatto' => $base['contatto'], + 'telefono' => $base['telefono'], + 'problema' => $base['problema'], + 'descrizione' => (string) ($intervento->ticket->descrizione ?? ''), + 'url' => TicketInterventoScheda::getUrl(['record' => (int) $intervento->id], panel: 'admin-filament'), + 'attachments' => $attachments, + 'messages' => $messages, + ]; + } + + public function closeInterventoModal(): void + { + $this->detailModal = null; + } + protected function buildBaseQuery(Fornitore $fornitore, ?FornitoreDipendente $dipendente): Builder { $query = TicketIntervento::query() diff --git a/app/Filament/Pages/Supporto/GestioneAssicurazioni.php b/app/Filament/Pages/Supporto/GestioneAssicurazioni.php new file mode 100644 index 0000000..57cc93a --- /dev/null +++ b/app/Filament/Pages/Supporto/GestioneAssicurazioni.php @@ -0,0 +1,145 @@ +> */ + public array $rows = []; + + /** @var array */ + public array $totals = [ + 'aperte' => 0, + 'chiuse' => 0, + 'tutte' => 0, + ]; + + public static function canAccess(): bool + { + $user = Auth::user(); + + return $user instanceof User + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + } + + public function mount(): void + { + $this->status = (string) request()->query('status', 'aperte'); + $this->focusTicketId = (int) request()->query('ticket', 0) ?: null; + $this->refreshData(); + } + + public function updatedStatus(): void + { + $this->refreshData(); + } + + public function setStatus(string $status): void + { + $this->status = in_array($status, ['aperte', 'chiuse', 'tutte'], true) ? $status : 'aperte'; + $this->refreshData(); + } + + public function refreshData(): void + { + $stabileIds = $this->resolveAccessibleStabileIds(); + if ($stabileIds === []) { + $this->rows = []; + $this->totals = ['aperte' => 0, 'chiuse' => 0, 'tutte' => 0]; + return; + } + + $base = InsuranceClaim::query() + ->with(['ticket.stabile:id,denominazione', 'insurancePolicy:id,company_name,policy_number']) + ->whereIn('stabile_id', $stabileIds); + + if ($this->focusTicketId) { + $base->where('ticket_id', (int) $this->focusTicketId); + } + + $query = clone $base; + if ($this->status === 'aperte') { + $query->whereNotIn('status', ['chiusa', 'chiuso', 'archiviata']); + } elseif ($this->status === 'chiuse') { + $query->whereIn('status', ['chiusa', 'chiuso', 'archiviata']); + } + + $this->rows = $query + ->latest('opened_at') + ->latest('id') + ->limit(120) + ->get() + ->map(function (InsuranceClaim $claim): array { + return [ + 'id' => (int) $claim->id, + 'ticket_id' => (int) $claim->ticket_id, + 'ticket_title' => (string) ($claim->ticket->titolo ?? '-'), + 'stabile' => (string) ($claim->ticket->stabile->denominazione ?? '-'), + 'claim_number' => (string) ($claim->claim_number ?? ''), + 'policy_reference' => (string) ($claim->policy_reference ?? ''), + 'policy_label' => trim(implode(' · ', array_filter([ + (string) ($claim->insurancePolicy->company_name ?? ''), + (string) ($claim->insurancePolicy->policy_number ?? ''), + ]))), + 'status' => (string) ($claim->status ?? 'aperta'), + 'opened_at' => optional($claim->opened_at)->format('d/m/Y H:i') ?: '-', + 'next_action' => (string) (data_get($claim->metadata, 'next_action') ?? ''), + 'documents_sent_at' => filled(data_get($claim->metadata, 'documents_sent_at')) ? (string) data_get($claim->metadata, 'documents_sent_at') : '', + 'ticket_url' => TicketGestione::getUrl([ + 'ticket' => (int) $claim->ticket_id, + 'tab' => 'assicurazione', + ], panel: 'admin-filament'), + ]; + }) + ->all(); + + $this->totals = [ + 'aperte' => (clone $base)->whereNotIn('status', ['chiusa', 'chiuso', 'archiviata'])->count(), + 'chiuse' => (clone $base)->whereIn('status', ['chiusa', 'chiuso', 'archiviata'])->count(), + 'tutte' => (clone $base)->count(), + ]; + } + + /** + * @return array + */ + private function resolveAccessibleStabileIds(): array + { + $user = Auth::user(); + if (! $user instanceof User) { + return []; + } + + return StabileContext::accessibleStabili($user) + ->pluck('id') + ->map(fn($value) => (int) $value) + ->filter(fn(int $value) => $value > 0) + ->values() + ->all(); + } +} \ No newline at end of file diff --git a/app/Filament/Pages/Supporto/TicketGestione.php b/app/Filament/Pages/Supporto/TicketGestione.php index f754d32..a9ef040 100644 --- a/app/Filament/Pages/Supporto/TicketGestione.php +++ b/app/Filament/Pages/Supporto/TicketGestione.php @@ -141,6 +141,8 @@ public function mount(): void $queryTab = (string) request()->query('tab', ''); if ($queryTab === 'scheda') { $this->activeTab = 'scheda'; + } elseif ($queryTab === 'assicurazione') { + $this->activeTab = 'assicurazione'; } if (request()->query('status') === 'all') { @@ -255,6 +257,16 @@ public function apriScheda(int $ticketId): void $this->syncSelectedTicketState(); } + public function apriAssicurazioneTicket(?int $ticketId = null): void + { + if (($ticketId ?? 0) > 0) { + $this->selectedTicketId = (int) $ticketId; + } + + $this->activeTab = 'assicurazione'; + $this->syncSelectedTicketState(); + } + public function apriElenco(): void { $this->activeTab = 'elenco'; @@ -275,6 +287,17 @@ public function getFornitoreAnagraficaUrl(int $fornitoreId): string return \App\Filament\Pages\Gescon\FornitoreScheda::getUrl(['record' => $fornitoreId], panel: 'admin-filament'); } + public function getInsuranceClaimsUrl(): string + { + $parameters = []; + + if ((int) ($this->selectedTicketId ?? 0) > 0) { + $parameters['ticket'] = (int) $this->selectedTicketId; + } + + return GestioneAssicurazioni::getUrl($parameters, panel: 'admin-filament'); + } + public function getAttachmentPublicUrl(TicketAttachment $attachment): string { return route('filament.tickets.attachments.view', ['attachment' => (int) $attachment->id]); diff --git a/app/Filament/Pages/Supporto/TicketMobile.php b/app/Filament/Pages/Supporto/TicketMobile.php index cf14ffe..848b1c5 100644 --- a/app/Filament/Pages/Supporto/TicketMobile.php +++ b/app/Filament/Pages/Supporto/TicketMobile.php @@ -624,6 +624,7 @@ public function creaTicketRapido(): void $this->newTicketTipoIntervento = 'altro'; $this->newTicketFotoNote = null; $this->resetDraftUploadState(); + $this->dispatch('ticket-mobile-draft-reset'); $this->refreshData(); } @@ -849,6 +850,8 @@ private function appendPendingUploads(string $source): void if (count($accepted) > 0) { $queueTotal = count($this->newTicketCameraShots) + count($this->newTicketAttachments); + $this->dispatch('ticket-mobile-local-previews-clear', source: $source); + Notification::make() ->title('File accodati alla bozza ticket') ->body(count($accepted) . ' file aggiunti. Totale attuale in coda: ' . $queueTotal . '.') diff --git a/docs/00-TODO-COPILOT-TRACKER.md b/docs/00-TODO-COPILOT-TRACKER.md index d88ec74..3223a37 100644 --- a/docs/00-TODO-COPILOT-TRACKER.md +++ b/docs/00-TODO-COPILOT-TRACKER.md @@ -1,6 +1,6 @@ # NetGescon — TODO Copilot Tracker -Data aggiornamento: 15 dicembre 2025 +Data aggiornamento: 6 aprile 2026 Questo file tiene traccia di: - cosa è stato fatto (implementato e/o corretto) @@ -68,6 +68,14 @@ ### A) Riallineare le “Gestioni” per la contabilità ## 3) Da fare (funzionalità richieste, in ordine consigliato) +### 3.0 Area fornitore/manutentore 2026 +- Unificare la rubrica operativa usando la stessa maschera principale condivisa tra amministratore, fornitore e moduli collegati; differenziare accesso e persistenza per ruolo, non l impianto della view. +- Aggiungere una pagina ad hoc per i ticket interni del fornitore con elenco unico di lavorazioni: ticket passati dall amministratore + lavorazioni interne + storico operativo. +- Estendere `fornitore/collaboratori` con gestione orari di lavoro, disponibilita e futura assegnazione lavori ricorrenti. +- Introdurre il dominio lavori ricorrenti/programmati distinto dal ticket occasionale, con contratti durevoli, clienti serviti, frequenze e calendario interventi. +- Separare nella scheda fornitore i blocchi TecnoRepair / seriali e catalogo operativo interno in pagine/tab dedicate. +- Portare avanti catalogo prodotti unificato con vari fornitori, listini acquisto/vendita multipli, seriali, varianti modello/colore, foto, allegati e riferimenti esterni tipo ICECAT. + ### 3.1 Contabilità — fatture fornitori “alla Gescon” - Rendere la maschera fattura fornitore più fedele allo screenshot (nomi/ordine tab + colonna azioni). - Aggancio/creazione fornitore (contabilità + anagrafica unica/rubrica) direttamente dalla maschera. diff --git a/docs/README.md b/docs/README.md index e95b353..f5275a0 100755 --- a/docs/README.md +++ b/docs/README.md @@ -89,6 +89,7 @@ ## 🧭 Documentazione & Indici - `support/ARCHITETTURA-STORAGE-TENANCY.md` – decisione architetturale su isolamento file per amministratore/stabile e valutazione pragmatica del modello Docker per stabile. - `support/BUG-RISOLTI-E-VALIDAZIONI.md` – registro sintetico bug corretti, fix applicati e verifiche ancora da completare. - `support/CONTINUITA-SVILUPPO-AGENT.md` – procedura minima per mantenere allineati manuale, note commit e sviluppo con agent. +- `support/FORNITORE-OPERATIVITA-ROADMAP-2026-04.md` – roadmap operativa fornitore/manutentore: rubrica unica, ticket interni, lavori ricorrenti, TecnoRepair, catalogo prodotti e seriali. - `docs/automation/` – commit log mensile, summary giornalieri e protocollo handoff automatici. - `07-IMPORT-UNITA-IMMOBILIARI.md` – blueprint operativo per importare unità e collegare proprietari/inquilini. - `08-ANAGRAFICA-UNICA-UI.md` – specifica UX della scheda anagrafica unica e relativi widget condominiali. diff --git a/docs/support/AGGIORNAMENTI-STAGING-GIT.md b/docs/support/AGGIORNAMENTI-STAGING-GIT.md index 4d49c0c..65014f7 100644 --- a/docs/support/AGGIORNAMENTI-STAGING-GIT.md +++ b/docs/support/AGGIORNAMENTI-STAGING-GIT.md @@ -116,3 +116,33 @@ ## Regola finale - punto di esecuzione assistita: UI `Supporto > Modifiche` - comando tecnico sottostante: `netgescon:git-sync` - documentazione di cio che si sta distribuendo: file Markdown versionati nel repository + +## Rilascio operativo 2026-04-06 + +Questo e il blocco che puo essere aggiornato adesso con la procedura Git-first senza correzioni manuali extra su staging. + +Incluso nel rilascio: + +- Ticket Mobile: anteprima immediata locale di foto e allegati prima dell invio definitivo. +- Ticket Gestione: nuova `Tab 4 - Assicurazioni` con nota rapida nello scheda ticket e pagina elenco pratiche dedicata. +- Area fornitore: correzione del 403 spurio sulla scheda ticket/intervento quando il link usa l id ticket invece dell id intervento. +- Area fornitore: apertura rapida in modal dalla lista ticket, allegati con miniature e preview in modal. +- Collaboratori fornitore: ricerca subfornitore con lo stesso pattern interattivo usato nella ricerca fornitore dei ticket. + +Procedura consigliata per questo rilascio: + +1. verificare che il commit sia presente su `origin/main` +2. aprire `Supporto > Modifiche` +3. usare `Aggiorna staging da Gitea` +4. aspettare la conclusione del sync Git e della QA sicura inclusa +5. validare subito i punti visibili: + - Ticket Mobile mostra l anteprima prima di inviare + - Ticket Gestione mostra `Tab 4 - Assicurazioni` + - `/admin-filament/fornitore/tickets/{id}` non produce 403 per i casi operativi normali + - la lista ticket fornitore apre anche la modal rapida + +Regola per evitare nuovi problemi: + +- non correggere staging a mano se il problema e gia nel repository +- prima si corregge Day0, poi commit, poi push, poi update UI da Gitea +- se un fix urgente viene fatto su staging per emergenza, va riportato subito nel repository prima del prossimo aggiornamento diff --git a/docs/support/ASSISTENZA-TECNOREPAIR-MDB.md b/docs/support/ASSISTENZA-TECNOREPAIR-MDB.md index 8ab799e..b979bdc 100644 --- a/docs/support/ASSISTENZA-TECNOREPAIR-MDB.md +++ b/docs/support/ASSISTENZA-TECNOREPAIR-MDB.md @@ -99,3 +99,42 @@ ## Decisione corrente - Si: NetGescon puo leggere l'MDB subito. - Si: possiamo usare l'MDB come sorgente legacy di partenza. - No: non conviene usare la scrittura diretta verso MDB come modello operativo principale. + +## Scrittura DB TecnoRepair: risposta pragmatica + +La risposta dipende dal tipo reale di archivio che vogliamo aggiornare. + +### Caso A: file Access `.mdb` + +- Lettura: si, gia fattibile e stabile con `mdb-tools`. +- Scrittura operativa diretta: no, non e il canale consigliato. +- Motivo: la scrittura su MDB da Linux resta troppo fragile per diventare il backoffice quotidiano di produzione. + +### Caso B: database SQL o staging NetGescon derivato da TecnoRepair + +- Si, questo e il modello consigliato. +- NetGescon diventa il sistema di lavoro. +- TecnoRepair legacy resta sorgente iniziale o archivio da sincronizzare con connettori dedicati. + +### Modello consigliato per rubrica e sincronizzazione cellulare + +Per ottenere gestione coerente tra NetGescon, telefono e Google Contacts: + +1. rubrica unica NetGescon come sorgente operativa principale +2. sincronizzazione verso Google Contacts per il mobile +3. import/sync da TecnoRepair verso NetGescon sui contatti legacy +4. eventuale export di ritorno verso sistemi esterni solo tramite bridge controllato, non con scrittura diretta nel file MDB + +Questo approccio permette: + +- una sola maschera rubrica condivisa per tutti i ruoli +- aggiornamenti coerenti anche da cellulare +- audit e storicizzazione in NetGescon +- minore rischio di corruzione archivio legacy + +## Decisione operativa aggiornata + +- rubrica condivisa: si +- sincronizzazione con Google Contacts: si, come canale mobile consigliato +- scrittura diretta nel file `TecnoRepairDB.mdb`: no, da evitare come processo ordinario +- scrittura su tabelle NetGescon/staging SQL con bridge di sincronizzazione: si, e il percorso corretto diff --git a/docs/support/FORNITORE-OPERATIVITA-ROADMAP-2026-04.md b/docs/support/FORNITORE-OPERATIVITA-ROADMAP-2026-04.md new file mode 100644 index 0000000..94fe7e2 --- /dev/null +++ b/docs/support/FORNITORE-OPERATIVITA-ROADMAP-2026-04.md @@ -0,0 +1,144 @@ +# Roadmap operativa fornitore/manutentore + +Data: 6 aprile 2026 + +## 1. Obiettivo + +Rendere l area fornitore realmente usabile come backoffice operativo quotidiano, senza creare viste isolate solo per un caso specifico. + +Direzione confermata: + +- UI e Blade riusabili tra ruoli diversi +- differenziazione principale a livello di accesso dati e permessi +- stessa esperienza utente su ticket, rubrica, allegati e schede prodotto + +## 2. Stato gia rilasciato + +- correzione accesso scheda ticket/intervento fornitore +- modal rapida dalla lista ticket fornitore +- miniature allegati ticket e preview modal lato fornitore +- anteprima immediata file in scheda intervento fornitore +- ricerca subfornitore/collaboratore con pattern coerente ai ticket + +## 3. Rubrica unica condivisa + +Decisione: + +- usare sempre la stessa maschera rubrica come base comune +- i ruoli diversi devono vedere workflow coerenti +- le differenze vanno applicate nel layer DB, permessi, collegamenti e campi secondari + +Impatto: + +- un fix UI fatto una volta resta valido per tutti +- minore divergenza tra amministratore, fornitore e moduli tecnici +- base migliore per sincronizzazione mobile e Google Contacts + +## 4. TecnoRepair e sorgente dati + +Decisione tecnica: + +- leggere il legacy MDB in modo sicuro +- non usare il file Access come database operativo da scrivere direttamente +- usare NetGescon come base dati di lavoro e storico operativo +- prevedere bridge di sincronizzazione verso/da sistemi esterni solo in modo controllato + +Percorso consigliato: + +1. import legacy TecnoRepair in staging SQL NetGescon +2. matching verso rubrica, fornitori, prodotti, seriali e schede assistenza +3. eventuale sync con Google Contacts per la parte rubrica mobile +4. eventuali export/sync di ritorno solo su connettori dedicati + +## 5. Ticket interni e lavorazioni unificate + +Da realizzare: + +- pagina dedicata con elenco unico lavorazioni fornitore +- viste aggregate per: + - ticket assegnati dall amministratore + - ticket interni aperti dal fornitore + - lavorazioni concluse / archivio + - lavorazioni in attesa / da pianificare + +Regola: + +- il ticket e il contenitore del lavoro occasionale/non preventivato +- le lavorazioni interne devono convivere nello stesso pannello operativo, senza obbligare a due strumenti separati per il manutentore + +## 6. Lavori ricorrenti e contratti + +Dominio da introdurre separatamente dal ticket: + +- contratti di servizio continuativi +- clienti serviti +- sedi/impianti/contesti serviti +- frequenza interventi +- calendario pianificato +- assegnazione operatori +- consuntivi e scostamenti + +Esempi target: + +- pulizie +- portierato +- disinfestazione +- manutenzione tecnica periodica +- servizi continuativi con SLA o visite programmate + +## 7. Collaboratori e turni + +Estensione richiesta sulla pagina collaboratori: + +- riuso del componente di ricerca stile ticket +- memorizzazione come collaboratore/dipendente o subfornitore +- futura gestione orari di lavoro e disponibilita +- base per distribuire lavori ricorrenti e assegnazioni automatiche/manuali + +## 8. Catalogo operativo interno + +Modello richiesto: + +- prodotto unico interno +- piu fornitori per lo stesso prodotto +- listini acquisto multipli comparabili +- listini vendita differenziabili per cliente o profilo cliente +- seriali singoli o multipli +- assemblati con seriali figli +- varianti modello/colore +- foto, allegati e link esterni tipo ICECAT + +Decisione chiave: + +- il codice pubblicato/operativo deve essere quello interno NetGescon +- i riferimenti esterni restano alias o sorgenti di import, non chiavi principali dell archivio + +## 9. Scheda fornitore per pagine separate + +Separazione richiesta per la scheda fornitore: + +1. dati essenziali in scheda principale +2. pagina/tab `TecnoRepair / seriali` +3. pagina/tab `Catalogo operativo interno` + +Contenuto previsto in `TecnoRepair / seriali`: + +- import MDB collegato al fornitore +- seriali assistenza agganciati automaticamente +- storico schede legacy + +Contenuto previsto in `Catalogo operativo interno`: + +- anagrafica prodotti/servizi del fornitore +- listini acquisto/vendita +- varianti e seriali +- allegati e immagini +- riferimenti esterni interni al gestionale + +## 10. Prossimi slice consigliati + +1. pagina ticket interni/lavorazioni unificate del fornitore +2. estensione collaboratori con disponibilita/orari +3. separazione scheda fornitore in tab/pagine dedicate +4. staging SQL TecnoRepair per rubrica, schede e seriali +5. catalogo prodotti unificato con vendor multipli e serializzazione \ No newline at end of file diff --git a/resources/views/filament/pages/fornitore/collaboratori.blade.php b/resources/views/filament/pages/fornitore/collaboratori.blade.php index 8f6e17e..b6d8f6d 100644 --- a/resources/views/filament/pages/fornitore/collaboratori.blade.php +++ b/resources/views/filament/pages/fornitore/collaboratori.blade.php @@ -49,15 +49,33 @@ Cerca fornitore - + + @elseif(filled($fornitoreSearch) && ! $fornitoreEsternoId) +
Nessun fornitore trovato con questa ricerca.
+ @endif +
Il subfornitore usa il suo account esistente. Qui salvi solo il collegamento operativo.
diff --git a/resources/views/filament/pages/fornitore/ticket-intervento-scheda.blade.php b/resources/views/filament/pages/fornitore/ticket-intervento-scheda.blade.php index 9e7b61f..a35757f 100644 --- a/resources/views/filament/pages/fornitore/ticket-intervento-scheda.blade.php +++ b/resources/views/filament/pages/fornitore/ticket-intervento-scheda.blade.php @@ -1,7 +1,17 @@ @php($ticket = $this->intervento->ticket) -
+
@@ -134,14 +144,59 @@
+
+
Anteprima immediata allegati fornitore
+
Le immagini e i documenti selezionati sono visibili subito prima del salvataggio del rapportino.
+ +
+
Foto lavoro
+
+ +
+
+ +
+
Documenti lavoro
+
+ +
+
+
+
@foreach($descrizioneFile as $index => $item)
+ + @if($detailModal) +
+
+
+
+
Ticket #{{ $detailModal['ticket_id'] }} · {{ $detailModal['titolo'] }}
+
{{ $detailModal['stabile'] }} · stato {{ $detailModal['stato'] }}
+
+
+ Apri scheda completa + +
+
+ +
+
+
+
+
+
+
Contatto
+
{{ $detailModal['contatto'] !== '' ? $detailModal['contatto'] : '-' }}
+
+
+
Telefono
+
{{ $detailModal['telefono'] !== '' ? $detailModal['telefono'] : '-' }}
+
+
+
Problema
+
{{ $detailModal['problema'] }}
+
+
+
{{ $detailModal['descrizione'] !== '' ? $detailModal['descrizione'] : 'Nessuna descrizione disponibile.' }}
+
+ +
+
Ultime comunicazioni
+
+ @forelse($detailModal['messages'] as $message) +
+
{{ $message['author'] }}
+
{{ $message['body'] }}
+
{{ $message['created_at'] }}
+
+ @empty +
Nessuna comunicazione disponibile.
+ @endforelse +
+
+
+ + +
+
+
+
+ @endif \ No newline at end of file diff --git a/resources/views/filament/pages/supporto/gestione-assicurazioni.blade.php b/resources/views/filament/pages/supporto/gestione-assicurazioni.blade.php new file mode 100644 index 0000000..95e012e --- /dev/null +++ b/resources/views/filament/pages/supporto/gestione-assicurazioni.blade.php @@ -0,0 +1,89 @@ + +
+
+
+
+
Pratiche assicurative
+
Elenco centralizzato dei sinistri aperti o chiusi, separato dalla lavorazione generale dei ticket.
+
+ +
+
+ +
+ + + +
+ +
+
+ + + + + + + + + + + + + + + @forelse($rows as $row) + + + + + + + + + + + @empty + + + + @endforelse + +
TicketStabileSinistroPolizzaStatoProssima azioneAperta il
+
#{{ $row['ticket_id'] }} - {{ $row['ticket_title'] }}
+ @if($focusTicketId && (int) $focusTicketId === (int) $row['ticket_id']) +
Pratica aperta dal ticket corrente
+ @endif +
{{ $row['stabile'] }} +
{{ $row['claim_number'] !== '' ? $row['claim_number'] : 'Da definire' }}
+ @if($row['policy_reference'] !== '') +
Rif. {{ $row['policy_reference'] }}
+ @endif +
{{ $row['policy_label'] !== '' ? $row['policy_label'] : '-' }} + {{ $row['status'] }} + + {{ $row['next_action'] !== '' ? $row['next_action'] : '-' }} + @if($row['documents_sent_at'] !== '') +
Documenti inviati: {{ \Carbon\Carbon::parse($row['documents_sent_at'])->format('d/m/Y H:i') }}
+ @endif +
{{ $row['opened_at'] }} + Apri Tab 4 +
Nessuna pratica assicurativa trovata per il filtro selezionato.
+
+
+
+
\ No newline at end of file diff --git a/resources/views/filament/pages/supporto/ticket-gestione.blade.php b/resources/views/filament/pages/supporto/ticket-gestione.blade.php index 0ae145b..e89a84d 100644 --- a/resources/views/filament/pages/supporto/ticket-gestione.blade.php +++ b/resources/views/filament/pages/supporto/ticket-gestione.blade.php @@ -24,6 +24,9 @@ +
@if($activeTab === 'elenco') -