From a6dbc5d6960c7a735571f3db1484fa31b7b5462f Mon Sep 17 00:00:00 2001 From: michele Date: Tue, 17 Mar 2026 14:12:19 +0000 Subject: [PATCH] feat: complete supplier filament operations and mail config --- app/Filament/Pages/Condomini/StabilePosta.php | 204 ++++++++ .../Pages/Fornitore/Collaboratori.php | 462 ++++++++++++++++++ .../Concerns/ResolvesOperatoreContext.php | 221 +++++++++ app/Filament/Pages/Fornitore/Contabilita.php | 158 ++++++ .../Fornitore/TicketInterventoScheda.php | 404 +++++++++++++++ .../Pages/Fornitore/TicketOperativi.php | 182 +++++++ app/Filament/Pages/Gescon/Anagrafica.php | 10 + app/Filament/Pages/Gescon/GesconHome.php | 10 + app/Filament/Pages/Gescon/Ordinarie.php | 9 + app/Filament/Pages/Gescon/Riscaldamento.php | 10 + app/Filament/Pages/Gescon/Utilita.php | 10 + app/Filament/Pages/Gescon/Varie.php | 10 + .../Impostazioni/SchedaAmministratore.php | 98 ++++ .../Pages/Strumenti/PassaggiDiConsegne.php | 10 + app/Filament/Pages/UnitaImmobiliarePage.php | 8 + app/Filament/Pages/UnitaImmobiliareV2.php | 9 + app/Filament/Pages/UnitaImmobiliareV3.php | 10 + app/Models/FornitoreDipendente.php | 46 +- app/Models/TicketIntervento.php | 5 + ...rnitore_collaboratori_for_subfornitura.php | 66 +++ docs/000-FILAMENT/15-FORNITORE-OPERATIVO.md | 66 +++ docs/000-FILAMENT/16-POSTA-ELETTRONICA.md | 40 ++ .../pages/condomini/stabile-posta.blade.php | 25 + .../pages/condomini/stabile.blade.php | 3 + .../pages/fornitore/collaboratori.blade.php | 162 ++++++ .../pages/fornitore/contabilita.blade.php | 138 ++++++ .../ticket-intervento-scheda.blade.php | 232 +++++++++ .../fornitore/ticket-operativi.blade.php | 94 ++++ 28 files changed, 2701 insertions(+), 1 deletion(-) create mode 100644 app/Filament/Pages/Condomini/StabilePosta.php create mode 100644 app/Filament/Pages/Fornitore/Collaboratori.php create mode 100644 app/Filament/Pages/Fornitore/Concerns/ResolvesOperatoreContext.php create mode 100644 app/Filament/Pages/Fornitore/Contabilita.php create mode 100644 app/Filament/Pages/Fornitore/TicketInterventoScheda.php create mode 100644 app/Filament/Pages/Fornitore/TicketOperativi.php create mode 100644 database/migrations/2026_03_17_101000_expand_fornitore_collaboratori_for_subfornitura.php create mode 100644 docs/000-FILAMENT/15-FORNITORE-OPERATIVO.md create mode 100644 docs/000-FILAMENT/16-POSTA-ELETTRONICA.md create mode 100644 resources/views/filament/pages/condomini/stabile-posta.blade.php create mode 100644 resources/views/filament/pages/fornitore/collaboratori.blade.php create mode 100644 resources/views/filament/pages/fornitore/contabilita.blade.php create mode 100644 resources/views/filament/pages/fornitore/ticket-intervento-scheda.blade.php create mode 100644 resources/views/filament/pages/fornitore/ticket-operativi.blade.php diff --git a/app/Filament/Pages/Condomini/StabilePosta.php b/app/Filament/Pages/Condomini/StabilePosta.php new file mode 100644 index 0000000..06495d8 --- /dev/null +++ b/app/Filament/Pages/Condomini/StabilePosta.php @@ -0,0 +1,204 @@ +hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + } + + public function mount(): void + { + $user = Auth::user(); + if (! $user instanceof User) { + abort(403); + } + + $stabileId = request()->integer('stabile_id') ?: StabileContext::resolveActiveStabileId($user); + abort_unless($stabileId > 0, 404, 'Nessuno stabile attivo selezionato.'); + + $stabile = StabileContext::accessibleStabili($user)->firstWhere('id', $stabileId); + abort_unless($stabile instanceof Stabile, 404); + + $this->stabile = $stabile; + + $posta = (array) Arr::get($stabile->configurazione_avanzata ?? [], 'posta', []); + + $this->getSchema('form')?->fill([ + 'pec_condominio' => (string) ($stabile->pec_condominio ?? ''), + 'pec_amministratore' => (string) ($stabile->pec_amministratore ?? ''), + 'posta' => array_merge([ + 'caselle' => [], + 'acquisizione' => [ + 'salva_documenti' => true, + 'salva_contabilita' => true, + 'mittenti_assicurazione' => '', + 'descrizione_default' => 'Documento acquisito da casella stabile', + ], + ], $posta), + ]); + } + + public function form(Schema $schema): Schema + { + return $schema + ->statePath('data') + ->components([ + Section::make('PEC e recapiti stabili') + ->columns(2) + ->schema([ + TextInput::make('pec_condominio') + ->label('PEC condominio') + ->email() + ->maxLength(255), + TextInput::make('pec_amministratore') + ->label('PEC amministratore per lo stabile') + ->email() + ->maxLength(255), + ]), + + Section::make('Caselle da leggere per questo stabile') + ->description('Qui prepariamo Gmail, IMAP o PEC dello stabile. La lettura automatica importerà messaggi e allegati nei prossimi step.') + ->schema([ + Repeater::make('posta.caselle') + ->label('Caselle collegate') + ->default([]) + ->schema([ + Toggle::make('enabled') + ->label('Attiva') + ->default(true), + Select::make('tipo') + ->label('Tipo casella') + ->options([ + 'gmail' => 'Gmail / Google Workspace', + 'imap' => 'IMAP ordinaria', + 'pec' => 'PEC via IMAP', + ]) + ->default('imap') + ->required(), + TextInput::make('label') + ->label('Etichetta') + ->required() + ->maxLength(120), + TextInput::make('email') + ->label('Email casella') + ->email() + ->maxLength(255), + TextInput::make('host') + ->label('Host IMAP') + ->maxLength(255), + TextInput::make('port') + ->label('Porta') + ->numeric(), + Select::make('encryption') + ->label('Cifratura') + ->options([ + 'ssl' => 'SSL', + 'tls' => 'TLS', + 'none' => 'Nessuna', + ]) + ->default('ssl'), + TextInput::make('username') + ->label('Username') + ->maxLength(255), + TextInput::make('password') + ->label('Password') + ->password() + ->revealable() + ->maxLength(255), + TextInput::make('folder') + ->label('Cartella da leggere') + ->maxLength(120) + ->default('INBOX'), + TextInput::make('gmail_query') + ->label('Filtro Gmail / query') + ->maxLength(255), + TextInput::make('mittenti_autorizzati') + ->label('Mittenti ammessi') + ->maxLength(500) + ->helperText('Email separate da virgola; utile per assicurazioni o FE.'), + ]) + ->columns(2) + ->columnSpanFull(), + ]), + + Section::make('Acquisizione allegati') + ->columns(2) + ->schema([ + Toggle::make('posta.acquisizione.salva_documenti') + ->label('Archivia allegati nei documenti') + ->default(true), + Toggle::make('posta.acquisizione.salva_contabilita') + ->label('Rendi disponibili gli allegati per la contabilità') + ->default(true), + TextInput::make('posta.acquisizione.mittenti_assicurazione') + ->label('Mittenti assicurazione') + ->maxLength(500) + ->helperText('Elenco email separate da virgola per identificare i messaggi dell’assicuratore.'), + TextInput::make('posta.acquisizione.descrizione_default') + ->label('Descrizione default allegato') + ->maxLength(255), + ]), + ]); + } + + public function save(): void + { + abort_unless($this->stabile instanceof Stabile, 404); + + $state = is_array($this->data ?? null) ? $this->data : []; + $config = (array) ($this->stabile->configurazione_avanzata ?? []); + $config['posta'] = is_array($state['posta'] ?? null) ? $state['posta'] : []; + + $this->stabile->pec_condominio = trim((string) ($state['pec_condominio'] ?? '')) ?: null; + $this->stabile->pec_amministratore = trim((string) ($state['pec_amministratore'] ?? '')) ?: null; + $this->stabile->configurazione_avanzata = $config; + $this->stabile->save(); + + Notification::make() + ->title('Configurazione posta stabile salvata') + ->success() + ->send(); + } +} \ No newline at end of file diff --git a/app/Filament/Pages/Fornitore/Collaboratori.php b/app/Filament/Pages/Fornitore/Collaboratori.php new file mode 100644 index 0000000..c4de4a1 --- /dev/null +++ b/app/Filament/Pages/Fornitore/Collaboratori.php @@ -0,0 +1,462 @@ +> */ + public array $rows = []; + + /** @var array> */ + public array $fornitoreOptions = []; + + public static function canAccess(): bool + { + $user = Auth::user(); + + return $user instanceof User + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'fornitore']); + } + + public function mount(): void + { + [$fornitore] = $this->resolveOperatoreContext(allowAdminWithoutSupplier: true); + + if (! $fornitore instanceof Fornitore) { + $this->missingAdminContext = true; + return; + } + + $this->fornitore = $fornitore; + $this->refreshFornitoreOptions(); + $this->refreshRows(); + } + + public function updatedCollaboratoreTipo(): void + { + if ($this->collaboratoreTipo !== FornitoreDipendente::TIPO_FORNITORE_ESTERNO) { + $this->fornitoreEsternoId = null; + $this->fornitoreSearch = ''; + $this->createUserAccess = false; + } + + $this->refreshFornitoreOptions(); + } + + public function updatedFornitoreSearch(): void + { + $this->refreshFornitoreOptions(); + } + + public function createCollaboratore(): void + { + if (! $this->fornitore instanceof Fornitore) { + return; + } + + $validated = $this->validate([ + 'collaboratoreTipo' => ['required', 'string', 'in:' . implode(',', [FornitoreDipendente::TIPO_INTERNO, FornitoreDipendente::TIPO_FORNITORE_ESTERNO])], + 'nome' => ['nullable', 'string', 'max:100'], + 'cognome' => ['nullable', 'string', 'max:100'], + 'email' => ['nullable', 'email', 'max:255'], + 'telefono' => ['nullable', 'string', 'max:50'], + 'note' => ['nullable', 'string', 'max:2000'], + 'createUserAccess' => ['nullable', 'boolean'], + 'accessPassword' => ['nullable', 'string', 'min:8', 'max:100'], + 'fornitoreEsternoId' => ['nullable', 'integer'], + ]); + + $tipo = (string) $validated['collaboratoreTipo']; + + if ($tipo === FornitoreDipendente::TIPO_INTERNO && trim((string) ($validated['nome'] ?? '')) === '') { + Notification::make()->title('Inserisci il nome del collaboratore')->warning()->send(); + return; + } + + $fornitoreEsterno = null; + if ($tipo === FornitoreDipendente::TIPO_FORNITORE_ESTERNO) { + $fornitoreEsterno = $this->resolveFornitoreEsterno((int) ($validated['fornitoreEsternoId'] ?? 0)); + + if (! $fornitoreEsterno instanceof Fornitore) { + Notification::make()->title('Seleziona un altro fornitore come collaboratore')->warning()->send(); + return; + } + + if ((int) $fornitoreEsterno->id === (int) $this->fornitore->id) { + Notification::make()->title('Il fornitore principale non puo essere aggiunto come subfornitore')->warning()->send(); + return; + } + } + + $email = $tipo === FornitoreDipendente::TIPO_FORNITORE_ESTERNO + ? trim((string) ($fornitoreEsterno?->email ?? '')) + : trim((string) ($validated['email'] ?? '')); + + if ($email !== '') { + $existsQuery = FornitoreDipendente::query() + ->where('fornitore_id', (int) $this->fornitore->id) + ->whereRaw('LOWER(email) = ?', [mb_strtolower($email)]); + + if ($tipo === FornitoreDipendente::TIPO_FORNITORE_ESTERNO && $fornitoreEsterno instanceof Fornitore) { + $existsQuery->orWhere('fornitore_esterno_id', (int) $fornitoreEsterno->id); + } + + $exists = $existsQuery->exists(); + + if ($exists) { + Notification::make()->title('Esiste gia un collaboratore con questa email')->warning()->send(); + return; + } + } + + $dipendente = new FornitoreDipendente(); + $dipendente->fornitore_id = (int) $this->fornitore->id; + $dipendente->fornitore_esterno_id = $fornitoreEsterno?->id; + $dipendente->rubrica_id = $fornitoreEsterno?->rubrica_id; + $dipendente->tipo_collaboratore = $tipo; + $dipendente->nome = $tipo === FornitoreDipendente::TIPO_FORNITORE_ESTERNO + ? (string) ($fornitoreEsterno?->ragione_sociale ?: $fornitoreEsterno?->nome ?: 'Subfornitore') + : trim((string) $validated['nome']); + $dipendente->cognome = $tipo === FornitoreDipendente::TIPO_FORNITORE_ESTERNO + ? null + : (trim((string) ($validated['cognome'] ?? '')) ?: null); + $dipendente->email = $email !== '' ? $email : null; + $dipendente->telefono = $tipo === FornitoreDipendente::TIPO_FORNITORE_ESTERNO + ? (trim((string) ($fornitoreEsterno?->telefono ?: $fornitoreEsterno?->cellulare ?: '')) ?: null) + : (trim((string) ($validated['telefono'] ?? '')) ?: null); + $dipendente->note = trim((string) ($validated['note'] ?? '')) ?: null; + $dipendente->attivo = true; + $dipendente->created_by_user_id = Auth::id(); + $dipendente->updated_by_user_id = Auth::id(); + + $createdPassword = null; + $plainPassword = trim((string) ($validated['accessPassword'] ?? '')); + + if ($tipo === FornitoreDipendente::TIPO_FORNITORE_ESTERNO && $fornitoreEsterno instanceof Fornitore) { + $linkedUser = User::query()->whereRaw('LOWER(email) = ?', [mb_strtolower((string) ($fornitoreEsterno->email ?? ''))])->first(); + if ($linkedUser instanceof User) { + $dipendente->user_id = (int) $linkedUser->id; + } + } + + if ($tipo === FornitoreDipendente::TIPO_INTERNO && (bool) ($validated['createUserAccess'] ?? false) && $email !== '') { + $user = User::query()->whereRaw('LOWER(email) = ?', [mb_strtolower($email)])->first(); + + if (! $user instanceof User) { + $createdPassword = $plainPassword !== '' ? $plainPassword : Str::random(12); + $user = User::query()->create([ + 'name' => trim($dipendente->nome . ' ' . ($dipendente->cognome ?? '')), + 'email' => $email, + 'password' => Hash::make($createdPassword), + 'email_verified_at' => now(), + 'is_active' => true, + ]); + } elseif ($plainPassword !== '') { + $user->password = Hash::make($plainPassword); + $user->save(); + $createdPassword = $plainPassword; + } + + $user->assignRole('fornitore'); + $dipendente->user_id = (int) $user->id; + } + + $dipendente->save(); + + $this->resetForm(); + $this->refreshRows(); + + $notification = Notification::make()->title('Collaboratore salvato')->success(); + if ($createdPassword !== null && $email !== '') { + $this->lastGeneratedPassword = $createdPassword; + $notification->body('Accesso creato per ' . $email . '. Password temporanea: ' . $createdPassword); + } + + $notification->send(); + } + + public function abilitaAccesso(int $dipendenteId): void + { + if (! $this->fornitore instanceof Fornitore) { + return; + } + + $dipendente = FornitoreDipendente::query() + ->where('fornitore_id', (int) $this->fornitore->id) + ->find($dipendenteId); + + if (! $dipendente instanceof FornitoreDipendente) { + Notification::make()->title('Collaboratore non trovato')->danger()->send(); + return; + } + + if ((string) $dipendente->tipo_collaboratore === FornitoreDipendente::TIPO_FORNITORE_ESTERNO) { + Notification::make()->title('Per un altro fornitore si usa il suo accesso gia esistente')->warning()->send(); + return; + } + + $email = trim((string) ($dipendente->email ?? '')); + if ($email === '') { + Notification::make()->title('Email collaboratore mancante')->warning()->send(); + return; + } + + $targetUser = User::query()->whereRaw('LOWER(email) = ?', [mb_strtolower($email)])->first(); + $created = false; + + if (! $targetUser instanceof User) { + $newPassword = Str::random(12); + $targetUser = User::query()->create([ + 'name' => trim((string) ($dipendente->nome_completo ?: 'Utente fornitore')), + 'email' => $email, + 'password' => Hash::make($newPassword), + 'email_verified_at' => now(), + 'is_active' => true, + ]); + $this->lastGeneratedPassword = $newPassword; + $created = true; + } + + $targetUser->assignRole('fornitore'); + + $dipendente->user_id = (int) $targetUser->id; + $dipendente->attivo = true; + $dipendente->updated_by_user_id = Auth::id(); + $dipendente->save(); + + $this->refreshRows(); + + Notification::make() + ->title('Accesso collaboratore abilitato') + ->body($created + ? 'Nuova password temporanea: ' . ($this->lastGeneratedPassword ?? '(non disponibile)') + : 'Accesso collegato ad utente esistente.') + ->success() + ->send(); + } + + public function resetPasswordDipendenteUser(int $userId): void + { + if (! $this->fornitore instanceof Fornitore) { + return; + } + + $dipendente = FornitoreDipendente::query() + ->where('fornitore_id', (int) $this->fornitore->id) + ->where('user_id', $userId) + ->first(); + + if (! $dipendente instanceof FornitoreDipendente) { + Notification::make()->title('Utente non collegato a questo fornitore')->danger()->send(); + return; + } + + $target = User::query()->find($userId); + if (! $target instanceof User) { + Notification::make()->title('Utente non trovato')->danger()->send(); + return; + } + + $newPassword = Str::random(12); + $target->password = Hash::make($newPassword); + $target->save(); + + $this->lastGeneratedPassword = $newPassword; + + Notification::make() + ->title('Password collaboratore resettata') + ->body('Nuova password temporanea: ' . $newPassword) + ->success() + ->send(); + } + + public function toggleDipendenteAttivo(int $dipendenteId): void + { + if (! $this->fornitore instanceof Fornitore) { + return; + } + + $dipendente = FornitoreDipendente::query() + ->where('fornitore_id', (int) $this->fornitore->id) + ->find($dipendenteId); + + if (! $dipendente instanceof FornitoreDipendente) { + Notification::make()->title('Collaboratore non trovato')->danger()->send(); + return; + } + + $dipendente->attivo = ! (bool) $dipendente->attivo; + $dipendente->updated_by_user_id = Auth::id(); + $dipendente->save(); + + $this->refreshRows(); + Notification::make()->title('Stato collaboratore aggiornato')->success()->send(); + } + + public function getTicketsUrl(): string + { + if ($this->fornitore instanceof Fornitore) { + return TicketOperativi::getUrl(['fornitore' => (int) $this->fornitore->id], panel: 'admin-filament'); + } + + return TicketOperativi::getUrl(panel: 'admin-filament'); + } + + public function getContabilitaUrl(): string + { + if ($this->fornitore instanceof Fornitore) { + return Contabilita::getUrl(['fornitore' => (int) $this->fornitore->id], panel: 'admin-filament'); + } + + return Contabilita::getUrl(panel: 'admin-filament'); + } + + protected function refreshRows(): void + { + if (! $this->fornitore instanceof Fornitore) { + $this->rows = []; + return; + } + + $this->rows = FornitoreDipendente::query() + ->with(['user', 'fornitoreEsterno']) + ->where('fornitore_id', (int) $this->fornitore->id) + ->orderBy('attivo', 'desc') + ->orderBy('tipo_collaboratore') + ->orderBy('cognome') + ->orderBy('nome') + ->get() + ->map(fn(FornitoreDipendente $dipendente) => [ + 'id' => (int) $dipendente->id, + 'nome' => (string) $dipendente->nome_completo, + 'email' => (string) ($dipendente->email ?? ''), + 'telefono' => (string) ($dipendente->telefono ?? ''), + 'note' => (string) ($dipendente->note ?? ''), + 'attivo' => (bool) $dipendente->attivo, + 'user_id' => $dipendente->user_id ? (int) $dipendente->user_id : null, + 'tipo' => (string) $dipendente->tipo_collaboratore, + 'tipo_label' => (string) $dipendente->tipo_label, + 'fornitore_esterno_nome' => (string) ($dipendente->fornitoreEsterno?->ragione_sociale ?: trim((string) (($dipendente->fornitoreEsterno?->nome ?? '') . ' ' . ($dipendente->fornitoreEsterno?->cognome ?? '')))), + ]) + ->all(); + } + + protected function refreshFornitoreOptions(): void + { + if (! $this->fornitore instanceof Fornitore || $this->collaboratoreTipo !== FornitoreDipendente::TIPO_FORNITORE_ESTERNO) { + $this->fornitoreOptions = []; + return; + } + + $search = trim($this->fornitoreSearch); + + $this->fornitoreOptions = Fornitore::query() + ->whereKeyNot((int) $this->fornitore->id) + ->when( + $search !== '', + function (Builder $query) use ($search): void { + $query->where(function (Builder $builder) use ($search): void { + $builder->where('ragione_sociale', 'like', '%' . $search . '%') + ->orWhere('nome', 'like', '%' . $search . '%') + ->orWhere('cognome', 'like', '%' . $search . '%') + ->orWhere('email', 'like', '%' . $search . '%') + ->orWhere('telefono', 'like', '%' . $search . '%') + ->orWhere('cellulare', 'like', '%' . $search . '%') + ->orWhere('tags', 'like', '%' . $search . '%'); + }); + } + ) + ->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(' · ')), + ]) + ->all(); + } + + protected function resolveFornitoreEsterno(int $fornitoreId): ?Fornitore + { + if ($fornitoreId <= 0) { + return null; + } + + return Fornitore::query()->find($fornitoreId); + } + + protected function resetForm(): void + { + $this->nome = ''; + $this->cognome = ''; + $this->email = ''; + $this->telefono = ''; + $this->collaboratoreTipo = FornitoreDipendente::TIPO_INTERNO; + $this->fornitoreSearch = ''; + $this->fornitoreEsternoId = null; + $this->note = ''; + $this->createUserAccess = false; + $this->accessPassword = ''; + $this->refreshFornitoreOptions(); + } +} diff --git a/app/Filament/Pages/Fornitore/Concerns/ResolvesOperatoreContext.php b/app/Filament/Pages/Fornitore/Concerns/ResolvesOperatoreContext.php new file mode 100644 index 0000000..e2b730e --- /dev/null +++ b/app/Filament/Pages/Fornitore/Concerns/ResolvesOperatoreContext.php @@ -0,0 +1,221 @@ +hasAnyRole(['super-admin', 'admin', 'amministratore'])) { + $fornitoreId = $forcedFornitoreId ?: (int) request()->query('fornitore', 0); + + if ($fornitoreId <= 0) { + $interventoRoute = request()->route('intervento'); + if ($interventoRoute instanceof TicketIntervento) { + $fornitoreId = (int) ($interventoRoute->fornitore_id ?? 0); + } + } + + if ($fornitoreId > 0) { + $fornitore = Fornitore::query()->find($fornitoreId); + abort_unless($fornitore instanceof Fornitore, 404); + + return [$fornitore, null]; + } + + if ($allowAdminWithoutSupplier) { + return [null, null]; + } + } + + $requestedFornitoreId = $forcedFornitoreId ?: (int) request()->query('fornitore', 0); + $currentSupplier = $this->resolveCurrentUserSupplier($user); + + if ($requestedFornitoreId > 0) { + $collaboratore = $this->resolveCollaboratoreForUser($requestedFornitoreId, $currentSupplier?->id); + if ($collaboratore instanceof FornitoreDipendente) { + $fornitore = Fornitore::query()->find($requestedFornitoreId); + abort_unless($fornitore instanceof Fornitore, 404); + + $collaboratore->ultimo_accesso_at = now(); + $collaboratore->save(); + + return [$fornitore, $collaboratore]; + } + + if ($currentSupplier instanceof Fornitore && (int) $currentSupplier->id === $requestedFornitoreId) { + return [$currentSupplier, null]; + } + } + + $dipendente = null; + $fornitore = $currentSupplier; + + if (! $fornitore && $user) { + $dipendente = $this->resolveCollaboratoreForUser(null, null); + + if ($dipendente instanceof FornitoreDipendente) { + $fornitore = Fornitore::query()->find((int) $dipendente->fornitore_id); + $dipendente->ultimo_accesso_at = now(); + $dipendente->save(); + } + } + + abort_unless($fornitore instanceof Fornitore, 403, 'Profilo fornitore non collegato.'); + + return [$fornitore, $dipendente]; + } + + protected function resolveCurrentUserSupplier($user): ?Fornitore + { + if (! $user) { + return null; + } + + $email = mb_strtolower(trim((string) $user->email)); + if ($email === '') { + return null; + } + + return Fornitore::query() + ->whereRaw('LOWER(email) = ?', [$email]) + ->withCount(['ticketInterventi', 'dipendenti']) + ->orderByDesc('ticket_interventi_count') + ->orderByDesc('dipendenti_count') + ->orderByDesc('id') + ->first(); + } + + protected function resolveCollaboratoreForUser(?int $fornitoreId = null, ?int $currentSupplierId = null): ?FornitoreDipendente + { + $user = Auth::user(); + if (! $user) { + return null; + } + + $query = FornitoreDipendente::query() + ->where('attivo', true) + ->when($fornitoreId, fn($builder) => $builder->where('fornitore_id', $fornitoreId)) + ->where(function ($builder) use ($user, $currentSupplierId): void { + $builder->where('user_id', (int) $user->id) + ->orWhereRaw('LOWER(email) = ?', [mb_strtolower((string) $user->email)]); + + if (($currentSupplierId ?? 0) > 0) { + $builder->orWhere('fornitore_esterno_id', (int) $currentSupplierId); + } + }) + ->orderByRaw('CASE WHEN user_id = ? THEN 0 ELSE 1 END', [(int) $user->id]) + ->orderByRaw('CASE WHEN fornitore_esterno_id IS NULL THEN 1 ELSE 0 END') + ->orderByDesc('id'); + + return $query->first(); + } + + protected function authorizeIntervento(TicketIntervento $intervento, Fornitore $fornitore): void + { + abort_unless((int) $intervento->fornitore_id === (int) $fornitore->id, 403); + } + + protected function authorizeDipendenteIntervento(TicketIntervento $intervento, ?FornitoreDipendente $dipendente): void + { + if (! $dipendente instanceof FornitoreDipendente) { + return; + } + + $assignedDipendenteId = (int) ($intervento->eseguito_da_dipendente_id ?? 0); + abort_if($assignedDipendenteId > 0 && $assignedDipendenteId !== (int) $dipendente->id, 403, 'Intervento assegnato a un altro operatore del fornitore.'); + } + + /** + * @return array{ingresso:string,contatto:string,telefono:string,problema:string,apparato:string} + */ + protected function buildInterventoRow(TicketIntervento $intervento): array + { + $descrizione = (string) ($intervento->ticket->descrizione ?? ''); + $caller = $this->extractCallerData($descrizione); + + return [ + 'ingresso' => optional($intervento->created_at)->format('d/m/Y H:i') ?: '-', + 'contatto' => $caller['contatto'], + 'telefono' => $caller['telefono'], + 'problema' => $caller['problema'] !== '' ? $caller['problema'] : ((string) ($intervento->ticket->titolo ?? '-')), + 'apparato' => $this->extractApparato((string) ($intervento->rapporto_fornitore ?? '')), + ]; + } + + /** + * @return array{contatto:string,telefono:string,problema:string} + */ + protected function extractCallerData(string $descrizione): array + { + $contatto = '-'; + $telefono = ''; + $problema = ''; + + $lines = preg_split('/\r\n|\r|\n/', $descrizione) ?: []; + foreach ($lines as $line) { + $trim = trim($line); + if ($problema === '' && $trim !== '') { + $problema = $trim; + } + + if (str_starts_with($trim, 'Chiamante selezionato:')) { + $contatto = trim((string) str_replace('Chiamante selezionato:', '', $trim)); + } + + if (str_starts_with($trim, 'Telefono:')) { + $telefono = trim((string) str_replace('Telefono:', '', $trim)); + } + } + + return [ + 'contatto' => $contatto, + 'telefono' => $telefono, + 'problema' => $problema, + ]; + } + + protected function extractApparato(string $rapporto): string + { + foreach ((preg_split('/\r\n|\r|\n/', $rapporto) ?: []) as $line) { + $trim = trim($line); + if (str_starts_with(mb_strtolower($trim), 'apparato:')) { + return trim((string) substr($trim, strlen('apparato:'))); + } + } + + return '-'; + } + + protected function buildApparatoSummary(string $marca, string $modello, string $seriale): string + { + $marca = trim($marca); + $modello = trim($modello); + $seriale = trim($seriale); + + if ($marca === '' && $modello === '' && $seriale === '') { + return ''; + } + + return 'Apparato: ' + . ($marca !== '' ? ('marca=' . $marca . ' ') : '') + . ($modello !== '' ? ('modello=' . $modello . ' ') : '') + . ($seriale !== '' ? ('seriale=' . $seriale) : ''); + } + + protected function getFornitoreLabel(Fornitore $fornitore): string + { + $label = trim((string) ($fornitore->ragione_sociale ?: trim(($fornitore->nome ?? '') . ' ' . ($fornitore->cognome ?? '')))); + + return $label !== '' ? $label : ('Fornitore #' . (int) $fornitore->id); + } +} diff --git a/app/Filament/Pages/Fornitore/Contabilita.php b/app/Filament/Pages/Fornitore/Contabilita.php new file mode 100644 index 0000000..d208131 --- /dev/null +++ b/app/Filament/Pages/Fornitore/Contabilita.php @@ -0,0 +1,158 @@ + 0, + 'pagate' => 0, + 'registrate' => 0, + 'totale_aperto' => 0.0, + 'totale_registrato' => 0.0, + ]; + + /** @var array> */ + public array $fattureRows = []; + + /** @var array> */ + public array $queueRows = []; + + public static function canAccess(): bool + { + $user = Auth::user(); + + return $user instanceof User + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'fornitore']); + } + + public function mount(): void + { + [$fornitore] = $this->resolveOperatoreContext(allowAdminWithoutSupplier: true); + + if (! $fornitore instanceof Fornitore) { + $this->missingAdminContext = true; + return; + } + + $this->fornitore = $fornitore; + $this->fornitoreLabel = $this->getFornitoreLabel($fornitore); + $this->loadData(); + } + + public function getTicketsUrl(): string + { + if ($this->fornitore instanceof Fornitore) { + return TicketOperativi::getUrl(['fornitore' => (int) $this->fornitore->id], panel: 'admin-filament'); + } + + return TicketOperativi::getUrl(panel: 'admin-filament'); + } + + public function getCollaboratoriUrl(): string + { + if ($this->fornitore instanceof Fornitore) { + return Collaboratori::getUrl(['fornitore' => (int) $this->fornitore->id], panel: 'admin-filament'); + } + + return Collaboratori::getUrl(panel: 'admin-filament'); + } + + protected function loadData(): void + { + if (! $this->fornitore instanceof Fornitore) { + return; + } + + $this->queueRows = TicketIntervento::query() + ->with(['ticket.stabile.amministratore']) + ->where('fornitore_id', (int) $this->fornitore->id) + ->whereIn('stato', ['in_verifica', 'fatturabile', 'fatturato']) + ->orderByDesc('updated_at') + ->limit(40) + ->get() + ->map(fn(TicketIntervento $intervento) => [ + 'ticket_id' => (int) $intervento->ticket_id, + 'intervento_id' => (int) $intervento->id, + 'stato' => (string) $intervento->stato, + 'stabile' => (string) ($intervento->ticket?->stabile?->denominazione ?? '-'), + 'amministratore' => (string) ($intervento->ticket?->stabile?->amministratore?->denominazione_studio ?? '-'), + 'titolo' => (string) ($intervento->ticket?->titolo ?? '-'), + 'aggiornato' => optional($intervento->updated_at)->format('d/m/Y H:i') ?: '-', + 'url' => TicketInterventoScheda::getUrl(['record' => (int) $intervento->id], panel: 'admin-filament'), + ]) + ->all(); + + if (! Schema::hasTable('contabilita_fatture_fornitori')) { + $this->contabilitaReady = false; + return; + } + + $this->contabilitaReady = true; + + $fatture = FatturaFornitore::query() + ->with(['stabile.amministratore']) + ->where('fornitore_id', (int) $this->fornitore->id) + ->orderByDesc('data_documento') + ->orderByDesc('id') + ->limit(100) + ->get(); + + $this->stats = [ + 'aperte' => $fatture->whereIn('stato', ['inserito', 'contabilizzato'])->count(), + 'pagate' => $fatture->where('stato', 'pagato')->count(), + 'registrate' => $fatture->count(), + 'totale_aperto' => (float) $fatture->whereIn('stato', ['inserito', 'contabilizzato'])->sum('netto_da_pagare'), + 'totale_registrato' => (float) $fatture->sum('totale'), + ]; + + $this->fattureRows = $fatture + ->map(fn(FatturaFornitore $fattura) => [ + 'id' => (int) $fattura->id, + 'data_documento' => optional($fattura->data_documento)->format('d/m/Y') ?: '-', + 'numero_documento' => (string) ($fattura->numero_documento ?? '-'), + 'stato' => (string) ($fattura->stato ?? 'inserito'), + 'stabile' => (string) ($fattura->stabile?->denominazione ?? '-'), + 'amministratore' => (string) ($fattura->stabile?->amministratore?->denominazione_studio ?? '-'), + 'descrizione' => (string) ($fattura->descrizione ?? ''), + 'totale' => (float) ($fattura->totale ?? 0), + 'netto' => (float) ($fattura->netto_da_pagare ?? 0), + ]) + ->all(); + } +} \ No newline at end of file diff --git a/app/Filament/Pages/Fornitore/TicketInterventoScheda.php b/app/Filament/Pages/Fornitore/TicketInterventoScheda.php new file mode 100644 index 0000000..502df40 --- /dev/null +++ b/app/Filament/Pages/Fornitore/TicketInterventoScheda.php @@ -0,0 +1,404 @@ + '-', + 'telefono' => '', + 'problema' => '', + ]; + + /** @var array> */ + public array $storicoRows = []; + + /** @var array> */ + public array $dipendentiOptions = []; + + public ?int $dipendenteId = null; + + public string $rapportoFornitore = ''; + + public ?int $tempoMinuti = null; + + /** @var array */ + public array $articoliUtilizzati = ['', '', '']; + + public string $apparatoMarca = ''; + + public string $apparatoModello = ''; + + public string $apparatoSeriale = ''; + + /** @var array */ + public array $fotoLavoro = []; + + /** @var array */ + public array $documentiLavoro = []; + + /** @var array */ + public array $descrizioneFile = ['', '', '', '', '', '']; + + public string $messaggioVeloce = ''; + + public bool $lavoroStandard = false; + + public bool $richiestaProforma = false; + + public string $proformaCodice = ''; + + public string $proformaNote = ''; + + public string $qrToken = ''; + + public static function canAccess(): bool + { + $user = Auth::user(); + + return $user instanceof User + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'fornitore']); + } + + public function mount(int | string $record): void + { + $this->intervento = TicketIntervento::query()->findOrFail((int) $record); + + [$fornitore, $dipendente] = $this->resolveOperatoreContext((int) $this->intervento->fornitore_id); + abort_unless($fornitore instanceof Fornitore, 404); + + $this->fornitore = $fornitore; + $this->dipendente = $dipendente; + + $this->authorizeIntervento($this->intervento, $this->fornitore); + $this->authorizeDipendenteIntervento($this->intervento, $this->dipendente); + + $this->reloadIntervento(); + } + + public function assignDipendente(): void + { + if ($this->dipendente instanceof FornitoreDipendente) { + Notification::make()->title('Assegnazione consentita solo al coordinatore fornitore')->warning()->send(); + return; + } + + $validated = $this->validate([ + 'dipendenteId' => ['nullable', 'integer'], + ]); + + $dipendente = null; + $selectedId = (int) ($validated['dipendenteId'] ?? 0); + + if ($selectedId > 0) { + $dipendente = FornitoreDipendente::query() + ->where('fornitore_id', (int) $this->fornitore->id) + ->where('attivo', true) + ->find($selectedId); + + if (! $dipendente instanceof FornitoreDipendente) { + Notification::make()->title('Collaboratore non valido per questo fornitore')->danger()->send(); + return; + } + } + + $this->intervento->eseguito_da_dipendente_id = $dipendente?->id; + $this->intervento->save(); + + $stamp = now()->format('d/m/Y H:i'); + $message = $dipendente + ? '[' . $stamp . '] Intervento assegnato al collaboratore fornitore: ' . $dipendente->ruolo_operativo_label . '.' + : '[' . $stamp . '] Assegnazione collaboratore rimossa: intervento riportato al coordinamento del fornitore.'; + + $this->intervento->ticket->messages()->create([ + 'user_id' => Auth::id(), + 'messaggio' => $message, + 'canale' => 'interno', + 'direzione' => 'inbound', + 'inviato_il' => now(), + ]); + + $this->reloadIntervento(); + + Notification::make()->title('Assegnazione aggiornata')->success()->send(); + } + + public function startIntervento(): void + { + $this->intervento->update([ + 'stato' => 'in_corso', + 'preso_in_carico_da_user_id' => Auth::id(), + 'eseguito_da_dipendente_id' => $this->dipendente?->id ?? $this->intervento->eseguito_da_dipendente_id, + 'iniziato_at' => $this->intervento->iniziato_at ?? now(), + ]); + + $this->intervento->ticket->update(['stato' => 'In Lavorazione']); + + $this->reloadIntervento(); + + Notification::make()->title('Intervento preso in carico')->success()->send(); + } + + public function saveRapporto(): void + { + $validated = $this->validate([ + 'rapportoFornitore' => ['required', 'string', 'max:5000'], + 'tempoMinuti' => ['nullable', 'integer', 'min:1', 'max:1440'], + 'articoliUtilizzati' => ['nullable', 'array'], + 'articoliUtilizzati.*' => ['nullable', 'string', 'max:120'], + 'fotoLavoro' => ['nullable', 'array', 'max:10'], + 'fotoLavoro.*' => ['nullable', 'image', 'max:10240'], + 'documentiLavoro' => ['nullable', 'array', 'max:10'], + 'documentiLavoro.*' => ['nullable', 'file', 'max:10240', 'mimes:pdf,doc,docx,xls,xlsx,txt,jpg,jpeg,png,webp'], + 'descrizioneFile' => ['nullable', 'array'], + 'descrizioneFile.*' => ['nullable', 'string', 'max:255'], + 'apparatoMarca' => ['nullable', 'string', 'max:120'], + 'apparatoModello' => ['nullable', 'string', 'max:120'], + 'apparatoSeriale' => ['nullable', 'string', 'max:120'], + 'messaggioVeloce' => ['nullable', 'string', 'max:1500'], + 'lavoroStandard' => ['nullable', 'boolean'], + 'richiestaProforma' => ['nullable', 'boolean'], + 'proformaCodice' => ['nullable', 'string', 'max:80'], + 'proformaNote' => ['nullable', 'string', 'max:2000'], + 'qrToken' => ['nullable', 'string', 'max:40'], + ]); + + $rapporto = trim((string) $validated['rapportoFornitore']); + $apparato = $this->buildApparatoSummary( + (string) ($validated['apparatoMarca'] ?? ''), + (string) ($validated['apparatoModello'] ?? ''), + (string) ($validated['apparatoSeriale'] ?? '') + ); + + if ($apparato !== '') { + $rapporto .= "\n\n" . $apparato; + } + + $payload = [ + 'rapporto_fornitore' => $rapporto, + 'tempo_minuti' => $validated['tempoMinuti'] ?? null, + 'terminato_at' => now(), + 'stato' => ! empty($validated['lavoroStandard']) ? 'fatturabile' : 'in_verifica', + 'eseguito_da_dipendente_id' => $this->dipendente?->id ?? $this->intervento->eseguito_da_dipendente_id, + ]; + + $articoli = collect($validated['articoliUtilizzati'] ?? []) + ->map(fn($item) => trim((string) $item)) + ->filter() + ->values() + ->all(); + + if ($articoli !== []) { + $payload['articoli_utilizzati'] = $articoli; + } + + if ($this->fotoLavoro !== []) { + $firstPhoto = $this->fotoLavoro[0] ?? null; + if ($firstPhoto) { + $payload['foto_path'] = $firstPhoto->store('ticket-interventi/foto/' . $this->intervento->id, 'public'); + } + } + + $allFiles = array_merge($this->fotoLavoro, $this->documentiLavoro); + if ($allFiles !== [] && Schema::hasTable('ticket_attachments')) { + foreach ($allFiles as $index => $file) { + if (! $file) { + continue; + } + + $path = $file->store('ticket-interventi/allegati/' . $this->intervento->id, 'public'); + + TicketAttachment::query()->create([ + 'ticket_id' => (int) $this->intervento->ticket_id, + 'ticket_update_id' => null, + 'user_id' => (int) Auth::id(), + 'file_path' => $path, + 'original_file_name' => (string) $file->getClientOriginalName(), + 'mime_type' => (string) $file->getClientMimeType(), + 'size' => (int) $file->getSize(), + 'description' => trim((string) ($this->descrizioneFile[$index] ?? '')) ?: 'Allegato rapporto fornitore', + ]); + } + } + + $tokenInserito = trim((string) ($validated['qrToken'] ?? '')); + if ($tokenInserito !== '' && strcasecmp($tokenInserito, (string) $this->intervento->qr_token) === 0) { + $payload['qr_scansionato_at'] = now(); + } + + $this->intervento->update($payload); + + $stamp = now()->format('d/m/Y H:i'); + $this->intervento->ticket->messages()->create([ + 'user_id' => Auth::id(), + 'messaggio' => '[' . $stamp . '] Rapporto intervento caricato dal fornitore.', + 'canale' => 'interno', + 'direzione' => 'inbound', + 'inviato_il' => now(), + ]); + + $proformaCodice = trim((string) ($validated['proformaCodice'] ?? '')); + $proformaNote = trim((string) ($validated['proformaNote'] ?? '')); + if (! empty($validated['richiestaProforma']) || $proformaCodice !== '' || $proformaNote !== '') { + $this->intervento->ticket->messages()->create([ + 'user_id' => Auth::id(), + 'messaggio' => '[' . $stamp . '] Proforma fornitore ' . ($proformaCodice !== '' ? ('codice ' . $proformaCodice) : '(senza codice)') . ($proformaNote !== '' ? (' - ' . $proformaNote) : ''), + 'canale' => 'interno', + 'direzione' => 'inbound', + 'inviato_il' => now(), + ]); + } + + $quickMessage = trim((string) ($validated['messaggioVeloce'] ?? '')); + if ($quickMessage !== '') { + $this->intervento->ticket->messages()->create([ + 'user_id' => Auth::id(), + 'messaggio' => '[' . $stamp . '] Messaggio veloce fornitore: ' . $quickMessage, + 'canale' => 'interno', + 'direzione' => 'inbound', + 'inviato_il' => now(), + ]); + } + + $this->fotoLavoro = []; + $this->documentiLavoro = []; + $this->descrizioneFile = ['', '', '', '', '', '']; + $this->messaggioVeloce = ''; + $this->proformaCodice = ''; + $this->proformaNote = ''; + + $this->reloadIntervento(); + + Notification::make()->title('Rapporto inviato in verifica amministratore')->success()->send(); + } + + public function inviaSollecito(): void + { + $stamp = now()->format('d/m/Y H:i'); + $this->intervento->ticket->messages()->create([ + 'user_id' => Auth::id(), + 'messaggio' => '[' . $stamp . '] Sollecito fornitore su ticket/intervento in corso.', + 'canale' => 'interno', + 'direzione' => 'inbound', + 'inviato_il' => now(), + ]); + + $this->reloadIntervento(); + + Notification::make()->title('Sollecito inviato allo studio amministrativo')->success()->send(); + } + + public function getTicketsUrl(): string + { + return TicketOperativi::getUrl(['fornitore' => (int) $this->fornitore->id], panel: 'admin-filament'); + } + + public function getCollaboratoriUrl(): string + { + return Collaboratori::getUrl(['fornitore' => (int) $this->fornitore->id], panel: 'admin-filament'); + } + + public function canAssignDipendente(): bool + { + return ! ($this->dipendente instanceof FornitoreDipendente) && count($this->dipendentiOptions) > 0; + } + + protected function reloadIntervento(): void + { + $this->intervento->load([ + 'ticket.stabile', + 'ticket.messages.user', + 'ticket.attachments.user', + 'ticket.apertoDaUser', + 'ticket.assegnatoAUser', + 'ticket.assegnatoAFornitore', + 'eseguitoDaDipendente', + ]); + + $this->intervento->refresh(); + $this->intervento->load([ + 'ticket.stabile', + 'ticket.messages.user', + 'ticket.attachments.user', + 'ticket.apertoDaUser', + 'ticket.assegnatoAUser', + 'ticket.assegnatoAFornitore', + 'eseguitoDaDipendente', + ]); + + $this->caller = $this->extractCallerData((string) ($this->intervento->ticket?->descrizione ?? '')); + $this->storicoRows = TicketIntervento::query() + ->with(['ticket']) + ->where('fornitore_id', (int) $this->fornitore->id) + ->where('id', '!=', (int) $this->intervento->id) + ->when( + (int) ($this->intervento->ticket?->soggetto_richiedente_id ?? 0) > 0, + fn($query) => $query->whereHas('ticket', fn($ticketQuery) => $ticketQuery->where('soggetto_richiedente_id', (int) $this->intervento->ticket->soggetto_richiedente_id)), + fn($query) => $query->whereHas('ticket', fn($ticketQuery) => $ticketQuery->where('stabile_id', (int) ($this->intervento->ticket?->stabile_id ?? 0))) + ) + ->latest('id') + ->limit(12) + ->get() + ->map(fn(TicketIntervento $old) => [ + 'ticket_id' => (int) $old->ticket_id, + 'titolo' => (string) ($old->ticket->titolo ?? '-'), + 'stato' => (string) $old->stato, + 'updated_at' => optional($old->updated_at)->format('d/m/Y H:i') ?: '-', + 'note' => (string) ($old->note_amministratore ?? ''), + ]) + ->all(); + + $this->dipendentiOptions = $this->fornitore->dipendenti() + ->where('attivo', true) + ->with('fornitoreEsterno') + ->orderBy('cognome') + ->orderBy('nome') + ->get() + ->map(fn(FornitoreDipendente $dipendente) => [ + 'id' => (int) $dipendente->id, + 'label' => $dipendente->ruolo_operativo_label . ($dipendente->email ? ' · ' . $dipendente->email : ''), + ]) + ->all(); + + $this->dipendenteId = (int) ($this->intervento->eseguito_da_dipendente_id ?? 0) ?: null; + $this->rapportoFornitore = (string) ($this->intervento->rapporto_fornitore ?? ''); + $this->tempoMinuti = $this->intervento->tempo_minuti ? (int) $this->intervento->tempo_minuti : null; + $this->articoliUtilizzati = array_pad((array) ($this->intervento->articoli_utilizzati ?? []), 3, ''); + $this->qrToken = ''; + } +} diff --git a/app/Filament/Pages/Fornitore/TicketOperativi.php b/app/Filament/Pages/Fornitore/TicketOperativi.php new file mode 100644 index 0000000..a331ccd --- /dev/null +++ b/app/Filament/Pages/Fornitore/TicketOperativi.php @@ -0,0 +1,182 @@ +> */ + public array $rows = []; + + /** @var array */ + public array $totals = [ + 'aperti' => 0, + 'chiusi' => 0, + 'fatturabili' => 0, + ]; + + public static function canAccess(): bool + { + $user = Auth::user(); + + return $user instanceof User + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'fornitore']); + } + + public function mount(): void + { + $this->status = (string) request()->query('stato', 'aperti'); + [$fornitore] = $this->resolveOperatoreContext(allowAdminWithoutSupplier: true); + + if (! $fornitore instanceof Fornitore) { + $this->missingAdminContext = true; + $this->rows = []; + return; + } + + $this->fornitoreId = (int) $fornitore->id; + $this->fornitoreLabel = $this->getFornitoreLabel($fornitore); + + $this->refreshData(); + } + + public function updatedStatus(): void + { + $this->refreshData(); + } + + public function refreshData(): void + { + if (! $this->fornitoreId) { + $this->rows = []; + $this->totals = ['aperti' => 0, 'chiusi' => 0, 'fatturabili' => 0]; + return; + } + + [$fornitore, $dipendente] = $this->resolveOperatoreContext($this->fornitoreId); + + $query = $this->buildBaseQuery($fornitore, $dipendente); + $this->applyStatusFilter($query, $this->status); + + $this->rows = $query + ->limit(100) + ->get() + ->map(function (TicketIntervento $intervento): array { + $base = $this->buildInterventoRow($intervento); + + return array_merge($base, [ + '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 ?? '-'), + 'operatore' => (string) $intervento->operatore_assegnato_label, + 'tempo_minuti' => (int) ($intervento->tempo_minuti ?? 0), + 'updated_at' => optional($intervento->updated_at)->format('d/m/Y H:i') ?: '-', + 'url' => TicketInterventoScheda::getUrl(['record' => $intervento->id], panel: 'admin-filament'), + ]); + }) + ->all(); + + $this->totals = [ + 'aperti' => (clone $this->buildBaseQuery($fornitore, $dipendente)) + ->whereNotIn('stato', ['chiuso']) + ->count(), + 'chiusi' => (clone $this->buildBaseQuery($fornitore, $dipendente)) + ->whereIn('stato', ['chiuso', 'fatturato']) + ->count(), + 'fatturabili' => (clone $this->buildBaseQuery($fornitore, $dipendente)) + ->whereIn('stato', ['fatturabile', 'fatturato']) + ->count(), + ]; + } + + public function setStatus(string $status): void + { + $this->status = $status; + $this->refreshData(); + } + + public function getCollaboratoriUrl(): string + { + if ($this->fornitoreId && Auth::user()?->hasAnyRole(['super-admin', 'admin', 'amministratore'])) { + return Collaboratori::getUrl(['fornitore' => $this->fornitoreId], panel: 'admin-filament'); + } + + return Collaboratori::getUrl(panel: 'admin-filament'); + } + + public function getContabilitaUrl(): string + { + if ($this->fornitoreId && Auth::user()?->hasAnyRole(['super-admin', 'admin', 'amministratore'])) { + return Contabilita::getUrl(['fornitore' => $this->fornitoreId], panel: 'admin-filament'); + } + + return Contabilita::getUrl(panel: 'admin-filament'); + } + + protected function buildBaseQuery(Fornitore $fornitore, ?FornitoreDipendente $dipendente): Builder + { + $query = TicketIntervento::query() + ->with(['ticket.stabile', 'eseguitoDaDipendente']) + ->where('fornitore_id', (int) $fornitore->id) + ->orderByDesc('created_at'); + + 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; + } + + protected function applyStatusFilter(Builder $query, string $status): void + { + if ($status === 'chiusi') { + $query->whereIn('stato', ['chiuso', 'fatturato']); + return; + } + + if ($status === 'fatturabili') { + $query->whereIn('stato', ['fatturabile', 'fatturato']); + return; + } + + $query->whereNotIn('stato', ['chiuso']); + } +} diff --git a/app/Filament/Pages/Gescon/Anagrafica.php b/app/Filament/Pages/Gescon/Anagrafica.php index 5d46a0c..e76da54 100644 --- a/app/Filament/Pages/Gescon/Anagrafica.php +++ b/app/Filament/Pages/Gescon/Anagrafica.php @@ -2,8 +2,10 @@ namespace App\Filament\Pages\Gescon; +use App\Models\User; use BackedEnum; use Filament\Pages\Page; +use Illuminate\Support\Facades\Auth; use UnitEnum; class Anagrafica extends Page @@ -23,4 +25,12 @@ class Anagrafica extends Page protected string $view = 'filament.pages.gescon.section'; public string $sectionKey = 'anagrafica'; + + public static function canAccess(): bool + { + $user = Auth::user(); + + return $user instanceof User + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + } } diff --git a/app/Filament/Pages/Gescon/GesconHome.php b/app/Filament/Pages/Gescon/GesconHome.php index b1d4a3e..c5ddfc0 100644 --- a/app/Filament/Pages/Gescon/GesconHome.php +++ b/app/Filament/Pages/Gescon/GesconHome.php @@ -2,8 +2,10 @@ namespace App\Filament\Pages\Gescon; +use App\Models\User; use BackedEnum; use Filament\Pages\Page; +use Illuminate\Support\Facades\Auth; use UnitEnum; class GesconHome extends Page @@ -21,4 +23,12 @@ class GesconHome extends Page protected static ?string $slug = 'gescon'; protected string $view = 'filament.pages.gescon.home'; + + public static function canAccess(): bool + { + $user = Auth::user(); + + return $user instanceof User + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + } } diff --git a/app/Filament/Pages/Gescon/Ordinarie.php b/app/Filament/Pages/Gescon/Ordinarie.php index eaefdef..0e9a3b4 100644 --- a/app/Filament/Pages/Gescon/Ordinarie.php +++ b/app/Filament/Pages/Gescon/Ordinarie.php @@ -1,6 +1,7 @@ hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + } + public ?string $mastrinoTabella = null; public ?string $mastrinoCodSpe = null; public array $mastrinoRows = []; diff --git a/app/Filament/Pages/Gescon/Riscaldamento.php b/app/Filament/Pages/Gescon/Riscaldamento.php index 602dcc1..340015a 100644 --- a/app/Filament/Pages/Gescon/Riscaldamento.php +++ b/app/Filament/Pages/Gescon/Riscaldamento.php @@ -2,8 +2,10 @@ namespace App\Filament\Pages\Gescon; +use App\Models\User; use BackedEnum; use Filament\Pages\Page; +use Illuminate\Support\Facades\Auth; use UnitEnum; class Riscaldamento extends Page @@ -23,4 +25,12 @@ class Riscaldamento extends Page protected string $view = 'filament.pages.gescon.section'; public string $sectionKey = 'riscaldamento'; + + public static function canAccess(): bool + { + $user = Auth::user(); + + return $user instanceof User + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + } } diff --git a/app/Filament/Pages/Gescon/Utilita.php b/app/Filament/Pages/Gescon/Utilita.php index 1063cb0..a3878ec 100644 --- a/app/Filament/Pages/Gescon/Utilita.php +++ b/app/Filament/Pages/Gescon/Utilita.php @@ -2,8 +2,10 @@ namespace App\Filament\Pages\Gescon; +use App\Models\User; use BackedEnum; use Filament\Pages\Page; +use Illuminate\Support\Facades\Auth; use UnitEnum; class Utilita extends Page @@ -23,4 +25,12 @@ class Utilita extends Page protected string $view = 'filament.pages.gescon.section'; public string $sectionKey = 'utilita'; + + public static function canAccess(): bool + { + $user = Auth::user(); + + return $user instanceof User + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + } } diff --git a/app/Filament/Pages/Gescon/Varie.php b/app/Filament/Pages/Gescon/Varie.php index 468ad20..3670cac 100644 --- a/app/Filament/Pages/Gescon/Varie.php +++ b/app/Filament/Pages/Gescon/Varie.php @@ -2,8 +2,10 @@ namespace App\Filament\Pages\Gescon; +use App\Models\User; use BackedEnum; use Filament\Pages\Page; +use Illuminate\Support\Facades\Auth; use UnitEnum; class Varie extends Page @@ -23,4 +25,12 @@ class Varie extends Page protected string $view = 'filament.pages.gescon.section'; public string $sectionKey = 'varie'; + + public static function canAccess(): bool + { + $user = Auth::user(); + + return $user instanceof User + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + } } diff --git a/app/Filament/Pages/Impostazioni/SchedaAmministratore.php b/app/Filament/Pages/Impostazioni/SchedaAmministratore.php index 76f609f..6b43226 100644 --- a/app/Filament/Pages/Impostazioni/SchedaAmministratore.php +++ b/app/Filament/Pages/Impostazioni/SchedaAmministratore.php @@ -11,6 +11,7 @@ use BackedEnum; use Filament\Forms\Components\FileUpload; use Filament\Forms\Components\Placeholder; +use Filament\Forms\Components\Repeater; use Filament\Forms\Components\Select; use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; @@ -342,6 +343,103 @@ public function form(Schema $schema): Schema ->label('Gruppo rubrica Google') ->maxLength(255) ->helperText('Es: NetGesCon / Condomini'), + Toggle::make('impostazioni.google.mail_enabled') + ->label('Abilita lettura Gmail') + ->default(false), + TextInput::make('impostazioni.google.gmail_label') + ->label('Label Gmail da leggere') + ->maxLength(255) + ->helperText('Es: NetGescon/Assicurazioni'), + TextInput::make('impostazioni.google.gmail_query') + ->label('Query Gmail') + ->maxLength(255) + ->helperText('Es: from:assicurazione@example.it has:attachment newer_than:30d'), + Toggle::make('impostazioni.google.archive_after_import') + ->label('Archivia dopo l\'acquisizione') + ->default(false), + Toggle::make('impostazioni.google.import_attachments') + ->label('Importa allegati') + ->default(true), + TextInput::make('impostazioni.google.allowed_senders') + ->label('Mittenti ammessi') + ->maxLength(500) + ->helperText('Email separate da virgola; utile per assicurazioni o inoltri FE.'), + ]), + + Section::make('Caselle studio / PEC da collegare') + ->description('Caselle dello studio amministratore. Per le caselle specifiche del condominio usa la nuova voce Posta stabile nella scheda Stabile.') + ->schema([ + Repeater::make('impostazioni.posta.caselle') + ->label('Caselle collegate') + ->default([]) + ->schema([ + Toggle::make('enabled') + ->label('Attiva') + ->default(true), + Select::make('tipo') + ->label('Tipo') + ->options([ + 'gmail' => 'Gmail / Google Workspace', + 'imap' => 'IMAP ordinaria', + 'pec' => 'PEC via IMAP', + ]) + ->default('imap') + ->required(), + TextInput::make('label') + ->label('Etichetta') + ->required() + ->maxLength(120), + TextInput::make('email') + ->label('Email casella') + ->email() + ->maxLength(255), + TextInput::make('host') + ->label('Host IMAP') + ->maxLength(255), + TextInput::make('port') + ->label('Porta') + ->numeric(), + Select::make('encryption') + ->label('Cifratura') + ->options([ + 'ssl' => 'SSL', + 'tls' => 'TLS', + 'none' => 'Nessuna', + ]) + ->default('ssl'), + TextInput::make('username') + ->label('Username') + ->maxLength(255), + TextInput::make('password') + ->label('Password') + ->password() + ->revealable() + ->maxLength(255), + TextInput::make('folder') + ->label('Cartella da leggere') + ->maxLength(120) + ->default('INBOX'), + ]) + ->columns(2) + ->columnSpanFull(), + ]), + + Section::make('Acquisizione messaggi e allegati') + ->columns(2) + ->schema([ + Toggle::make('impostazioni.posta.routing.salva_documenti') + ->label('Archivia allegati nei documenti') + ->default(true), + Toggle::make('impostazioni.posta.routing.salva_contabilita') + ->label('Rendi disponibili gli allegati in contabilità') + ->default(true), + TextInput::make('impostazioni.posta.routing.mittenti_assicurazione') + ->label('Mittenti assicurazione') + ->maxLength(500) + ->helperText('Email separate da virgola per filtrare i messaggi dell’assicuratore.'), + TextInput::make('impostazioni.posta.routing.descrizione_default') + ->label('Descrizione default allegato') + ->maxLength(255), ]), Section::make('WhatsApp Cloud API (Meta)') diff --git a/app/Filament/Pages/Strumenti/PassaggiDiConsegne.php b/app/Filament/Pages/Strumenti/PassaggiDiConsegne.php index 4ce3a03..c4e2221 100644 --- a/app/Filament/Pages/Strumenti/PassaggiDiConsegne.php +++ b/app/Filament/Pages/Strumenti/PassaggiDiConsegne.php @@ -2,8 +2,10 @@ namespace App\Filament\Pages\Strumenti; +use App\Models\User; use BackedEnum; use Filament\Pages\Page; +use Illuminate\Support\Facades\Auth; use UnitEnum; class PassaggiDiConsegne extends Page @@ -21,4 +23,12 @@ class PassaggiDiConsegne extends Page protected static ?string $slug = 'strumenti/passaggi-di-consegne'; protected string $view = 'filament.pages.strumenti.passaggi-di-consegne'; + + public static function canAccess(): bool + { + $user = Auth::user(); + + return $user instanceof User + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + } } diff --git a/app/Filament/Pages/UnitaImmobiliarePage.php b/app/Filament/Pages/UnitaImmobiliarePage.php index 470626d..838499b 100644 --- a/app/Filament/Pages/UnitaImmobiliarePage.php +++ b/app/Filament/Pages/UnitaImmobiliarePage.php @@ -38,6 +38,14 @@ class UnitaImmobiliarePage extends Page implements HasForms { use InteractsWithForms; + public static function canAccess(): bool + { + $user = Auth::user(); + + return $user instanceof User + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + } + protected int $stabileId = 0; protected ?string $backUrl = null; diff --git a/app/Filament/Pages/UnitaImmobiliareV2.php b/app/Filament/Pages/UnitaImmobiliareV2.php index 13edc90..557a474 100644 --- a/app/Filament/Pages/UnitaImmobiliareV2.php +++ b/app/Filament/Pages/UnitaImmobiliareV2.php @@ -2,6 +2,7 @@ namespace App\Filament\Pages; +use App\Models\User; use BackedEnum; use Filament\Notifications\Notification; use Illuminate\Support\Facades\Auth; @@ -23,6 +24,14 @@ class UnitaImmobiliareV2 extends UnitaImmobiliarePage protected static bool $shouldRegisterNavigation = false; + public static function canAccess(): bool + { + $user = Auth::user(); + + return $user instanceof User + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + } + public function mount(): void { // Manteniamo la route storica ma riportiamo alla pagina unica. diff --git a/app/Filament/Pages/UnitaImmobiliareV3.php b/app/Filament/Pages/UnitaImmobiliareV3.php index 8956676..12d6b0a 100644 --- a/app/Filament/Pages/UnitaImmobiliareV3.php +++ b/app/Filament/Pages/UnitaImmobiliareV3.php @@ -2,8 +2,10 @@ namespace App\Filament\Pages; +use App\Models\User; use BackedEnum; use Filament\Notifications\Notification; +use Illuminate\Support\Facades\Auth; use UnitEnum; class UnitaImmobiliareV3 extends UnitaImmobiliarePage @@ -22,6 +24,14 @@ class UnitaImmobiliareV3 extends UnitaImmobiliarePage protected static bool $shouldRegisterNavigation = false; + public static function canAccess(): bool + { + $user = Auth::user(); + + return $user instanceof User + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + } + public function mount(): void { // Manteniamo la route storica ma riportiamo alla pagina unica. diff --git a/app/Models/FornitoreDipendente.php b/app/Models/FornitoreDipendente.php index 76746ab..2599649 100644 --- a/app/Models/FornitoreDipendente.php +++ b/app/Models/FornitoreDipendente.php @@ -8,11 +8,18 @@ class FornitoreDipendente extends Model { use HasFactory; + public const TIPO_INTERNO = 'interno'; + + public const TIPO_FORNITORE_ESTERNO = 'fornitore_esterno'; + protected $table = 'fornitore_dipendenti'; protected $fillable = [ 'fornitore_id', + 'fornitore_esterno_id', + 'rubrica_id', 'user_id', + 'tipo_collaboratore', 'nome', 'cognome', 'email', @@ -39,6 +46,16 @@ public function user() return $this->belongsTo(User::class, 'user_id'); } + public function fornitoreEsterno() + { + return $this->belongsTo(Fornitore::class, 'fornitore_esterno_id'); + } + + public function rubrica() + { + return $this->belongsTo(RubricaUniversale::class, 'rubrica_id'); + } + public function creatoDaUser() { return $this->belongsTo(User::class, 'created_by_user_id'); @@ -51,6 +68,33 @@ public function aggiornatoDaUser() public function getNomeCompletoAttribute(): string { - return trim((string) ($this->nome . ' ' . ($this->cognome ?? ''))); + $label = trim((string) ($this->nome . ' ' . ($this->cognome ?? ''))); + + if ($label !== '') { + return $label; + } + + $supplierLabel = trim((string) ($this->fornitoreEsterno?->ragione_sociale ?: trim((string) (($this->fornitoreEsterno?->nome ?? '') . ' ' . ($this->fornitoreEsterno?->cognome ?? ''))))); + + return $supplierLabel !== '' ? $supplierLabel : 'Collaboratore fornitore'; + } + + public function getTipoLabelAttribute(): string + { + return match ((string) $this->tipo_collaboratore) { + self::TIPO_FORNITORE_ESTERNO => 'Altro fornitore', + default => 'Collaboratore interno', + }; + } + + public function getRuoloOperativoLabelAttribute(): string + { + if ((string) $this->tipo_collaboratore === self::TIPO_FORNITORE_ESTERNO) { + $supplierLabel = trim((string) ($this->fornitoreEsterno?->ragione_sociale ?: trim((string) (($this->fornitoreEsterno?->nome ?? '') . ' ' . ($this->fornitoreEsterno?->cognome ?? ''))))); + + return $supplierLabel !== '' ? 'Subfornitore: ' . $supplierLabel : 'Subfornitore esterno'; + } + + return $this->nome_completo !== '' ? $this->nome_completo : 'Collaboratore interno'; } } diff --git a/app/Models/TicketIntervento.php b/app/Models/TicketIntervento.php index d9aa94e..ac7ad0c 100644 --- a/app/Models/TicketIntervento.php +++ b/app/Models/TicketIntervento.php @@ -83,4 +83,9 @@ public function getCompletabileAttribute(): bool { return $this->check_foto_ok && $this->check_qr_ok && $this->check_admin_ok; } + + public function getOperatoreAssegnatoLabelAttribute(): string + { + return $this->eseguitoDaDipendente?->ruolo_operativo_label ?: 'Responsabile fornitore'; + } } diff --git a/database/migrations/2026_03_17_101000_expand_fornitore_collaboratori_for_subfornitura.php b/database/migrations/2026_03_17_101000_expand_fornitore_collaboratori_for_subfornitura.php new file mode 100644 index 0000000..d714e1a --- /dev/null +++ b/database/migrations/2026_03_17_101000_expand_fornitore_collaboratori_for_subfornitura.php @@ -0,0 +1,66 @@ +string('tipo_collaboratore', 40) + ->default('interno') + ->after('user_id'); + } + + if (! Schema::hasColumn('fornitore_dipendenti', 'fornitore_esterno_id')) { + $table->foreignId('fornitore_esterno_id') + ->nullable() + ->after('fornitore_id') + ->constrained('fornitori') + ->nullOnDelete(); + } + + if (! Schema::hasColumn('fornitore_dipendenti', 'rubrica_id')) { + $table->foreignId('rubrica_id') + ->nullable() + ->after('fornitore_esterno_id') + ->constrained('rubrica_universale') + ->nullOnDelete(); + } + + $table->index(['fornitore_id', 'tipo_collaboratore'], 'forn_dip_tipo_idx'); + $table->index(['fornitore_esterno_id'], 'forn_dip_forn_est_idx'); + }); + } + + public function down(): void + { + if (! Schema::hasTable('fornitore_dipendenti')) { + return; + } + + Schema::table('fornitore_dipendenti', function (Blueprint $table): void { + $table->dropIndex('forn_dip_tipo_idx'); + $table->dropIndex('forn_dip_forn_est_idx'); + + if (Schema::hasColumn('fornitore_dipendenti', 'rubrica_id')) { + $table->dropConstrainedForeignId('rubrica_id'); + } + + if (Schema::hasColumn('fornitore_dipendenti', 'fornitore_esterno_id')) { + $table->dropConstrainedForeignId('fornitore_esterno_id'); + } + + if (Schema::hasColumn('fornitore_dipendenti', 'tipo_collaboratore')) { + $table->dropColumn('tipo_collaboratore'); + } + }); + } +}; diff --git a/docs/000-FILAMENT/15-FORNITORE-OPERATIVO.md b/docs/000-FILAMENT/15-FORNITORE-OPERATIVO.md new file mode 100644 index 0000000..186db46 --- /dev/null +++ b/docs/000-FILAMENT/15-FORNITORE-OPERATIVO.md @@ -0,0 +1,66 @@ +# Fornitore Operativo + +## Obiettivo + +La sezione fornitore deve avere due livelli d'uso distinti: + +- Mobile e tablet: accesso rapido ai ticket assegnati, presa in carico, assegnazione collaboratore, rapportino operativo, allegati, sollecito, conferma apparato o seriale. +- Desktop: gestione estesa del fornitore, rubrica propria, collaboratori e subfornitori, preventivi, proforma, inventario seriali, ciclo di fatturazione e chiusura amministrativa. + +## Stato attuale dopo questo step + +Il fornitore oggi puo vedere e gestire: + +- I ticket/interventi assegnati al proprio profilo fornitore. +- Lo stato operativo dell'intervento. +- La presa in carico del ticket. +- L'assegnazione dell'intervento a un collaboratore interno oppure a un altro fornitore registrato come subfornitore. +- Il rapportino di intervento. +- Tempo impiegato, ricambi o attivita, allegati, messaggio veloce allo studio. +- Marca, modello e seriale del bene lavorato dentro il rapporto operativo. +- La richiesta di proforma e le note operative di chiusura. +- Lo storico correlato e le comunicazioni del ticket. + +Il fornitore non puo modificare: + +- I dati originari del ticket inseriti o gestiti dall'amministratore. +- Il contenuto sorgente della segnalazione del chiamante. +- L'anagrafica amministrativa assegnata dal ticket. + +## Collaboratori fornitore + +La tabella collaboratori del fornitore ora distingue: + +- Collaboratore interno: operatore o dipendente del fornitore. +- Altro fornitore: subfornitore esterno usato per delegare l'esecuzione. + +Regole: + +- Il collaboratore interno puo avere un account dedicato. +- Il subfornitore usa il proprio account esistente, non va duplicato. +- L'intervento resta del fornitore principale, ma l'esecutore operativo puo essere un subfornitore collegato. + +## Strategia Mobile vs Desktop + +### Mobile e tablet + +- Lista ticket compatta. +- Scheda intervento compatta. +- Pulsanti grandi per presa in carico, assegnazione, allegati e rapporto. +- Nessuna schermata contabile o gestionale pesante. + +### Desktop + +- Rubrica fornitore. +- Gestione seriali e magazzino minimo. +- Preventivi e listini descrittivi. +- Proforma e collegamento alla fatturazione. +- Vista multi-amministratore e storico lavori. + +## Prossimi step consigliati + +1. Creare una rubrica proprietaria del fornitore, separata dai dati del ticket amministratore. +2. Estrarre apparato, seriale, marca e modello in entita dedicate invece di lasciarli solo nel testo rapporto. +3. Introdurre un oggetto rapportino con righe lavorazione e materiali. +4. Introdurre preventivo fornitore e conversione in proforma. +5. Introdurre visibilita multi-amministratore filtrata per studio e governo globale da super-admin. diff --git a/docs/000-FILAMENT/16-POSTA-ELETTRONICA.md b/docs/000-FILAMENT/16-POSTA-ELETTRONICA.md new file mode 100644 index 0000000..e1c9d28 --- /dev/null +++ b/docs/000-FILAMENT/16-POSTA-ELETTRONICA.md @@ -0,0 +1,40 @@ +# Posta Elettronica + +## Stato attuale + +In questo step la gestione posta è stata impostata come configurazione operativa, pronta per la lettura reale nei prossimi passaggi. + +### Studio amministratore + +La scheda amministratore ora può contenere: + +- configurazione Google Workspace per Gmail +- label e query Gmail per la lettura selettiva +- import allegati e archivio post-import +- caselle aggiuntive studio in modalità Gmail, IMAP o PEC via IMAP +- regole base di instradamento allegati verso documenti e contabilità + +### Stabile + +Ogni stabile ora può avere: + +- PEC condominio +- PEC amministratore riferita allo stabile +- una o più caselle email/PEC/IMAP dedicate allo stabile +- regole base per acquisire allegati da assicurazioni o altri mittenti rilevanti + +## Obiettivo operativo + +La pipeline prevista è: + +1. leggere Gmail o IMAP dello studio o dello stabile +2. individuare i messaggi rilevanti con filtri mittente/query/cartella +3. acquisire allegati e descrizione +4. archiviare nei documenti +5. esporre gli allegati anche alla contabilità quando servono FE, PEC o documenti assicurativi + +## Note implementative + +- Le configurazioni dello studio sono salvate in `amministratori.impostazioni`. +- Le configurazioni dello stabile sono salvate in `stabili.configurazione_avanzata['posta']`. +- La lettura automatica dei messaggi non è ancora attiva in questo step: è pronto il perimetro di configurazione. \ No newline at end of file diff --git a/resources/views/filament/pages/condomini/stabile-posta.blade.php b/resources/views/filament/pages/condomini/stabile-posta.blade.php new file mode 100644 index 0000000..ee542ce --- /dev/null +++ b/resources/views/filament/pages/condomini/stabile-posta.blade.php @@ -0,0 +1,25 @@ + +
+
+
+
+
Posta stabile
+
+ @if($stabile) + Caselle email e PEC collegate a {{ $stabile->denominazione ?: ('Stabile #' . $stabile->id) }}. + @endif +
+
+ Torna allo stabile +
+
+ +
+ {{ $this->getSchema('form') }} + +
+ Salva configurazione +
+
+
+
\ No newline at end of file diff --git a/resources/views/filament/pages/condomini/stabile.blade.php b/resources/views/filament/pages/condomini/stabile.blade.php index 3a3b758..f225779 100644 --- a/resources/views/filament/pages/condomini/stabile.blade.php +++ b/resources/views/filament/pages/condomini/stabile.blade.php @@ -30,6 +30,9 @@ Movimenti / Import estratto + + Posta stabile + diff --git a/resources/views/filament/pages/fornitore/collaboratori.blade.php b/resources/views/filament/pages/fornitore/collaboratori.blade.php new file mode 100644 index 0000000..30e0f5d --- /dev/null +++ b/resources/views/filament/pages/fornitore/collaboratori.blade.php @@ -0,0 +1,162 @@ + +
+
+
+
+
Collaboratori fornitore
+
+ @if($fornitore) + Gestione operatori, subfornitori e accessi per {{ $fornitore->ragione_sociale ?: trim(($fornitore->nome ?? '') . ' ' . ($fornitore->cognome ?? '')) }}. + @else + Seleziona un fornitore dalla gestione ticket per aprire questa vista. + @endif +
+
+ +
+
+ + @if($this->missingAdminContext) +
+ Questa vista per amministratori richiede un fornitore selezionato. +
+ @else +
+
+
Nuovo collaboratore
+
Puoi registrare sia un collaboratore interno sia un altro fornitore da usare come subfornitore operativo.
+
+ + + @if($collaboratoreTipo === 'fornitore_esterno') + + + @else + + + + + @endif + + @if($collaboratoreTipo === 'interno') + + + @else +
+ Il subfornitore usa il suo account esistente. Qui salvi solo il collegamento operativo. +
+ @endif + Salva collaboratore +
+
+ +
+
Elenco collaboratori
+
+ + + + + + + + + + + + + @forelse($rows as $row) + + + + + + + + + @empty + + + + @endforelse + +
NomeTipoContattiAccessoStato
+
{{ $row['nome'] }}
+ @if($row['note'] !== '') +
{{ $row['note'] }}
+ @endif +
+ {{ $row['tipo_label'] }} + @if($row['fornitore_esterno_nome'] !== '') +
{{ $row['fornitore_esterno_nome'] }}
+ @endif +
+
{{ $row['email'] !== '' ? $row['email'] : '-' }}
+
{{ $row['telefono'] !== '' ? $row['telefono'] : '-' }}
+
+ @if($row['user_id']) + Account collegato + @else + Nessun account + @endif + + {{ $row['attivo'] ? 'Attivo' : 'Disattivo' }} + +
+ @if($row['tipo'] === 'interno' && ! $row['user_id']) + + @elseif($row['tipo'] === 'interno' && $row['user_id']) + + @else + Accesso dal fornitore collegato + @endif + +
+
Nessun collaboratore registrato.
+
+
+
+ @endif +
+
\ No newline at end of file diff --git a/resources/views/filament/pages/fornitore/contabilita.blade.php b/resources/views/filament/pages/fornitore/contabilita.blade.php new file mode 100644 index 0000000..cc9cb21 --- /dev/null +++ b/resources/views/filament/pages/fornitore/contabilita.blade.php @@ -0,0 +1,138 @@ + +
+
+
+
+
Contabilita fornitore
+
+ @if($fornitoreLabel) + Vista contabile sintetica per {{ $fornitoreLabel }}. + @else + Seleziona un fornitore per aprire questa vista. + @endif +
+
+ +
+
+ + @if($missingAdminContext) +
+ Questa vista per amministratori richiede un fornitore selezionato. +
+ @else +
+
+
Fatture aperte
+
{{ $stats['aperte'] }}
+
Registrate ma non ancora pagate.
+
+
+
Fatture pagate
+
{{ $stats['pagate'] }}
+
Pagamenti risultanti in contabilità.
+
+
+
Netto aperto
+
{{ number_format($stats['totale_aperto'], 2, ',', '.') }} €
+
Da incassare sui documenti registrati.
+
+
+
Totale registrato
+
{{ number_format($stats['totale_registrato'], 2, ',', '.') }} €
+
Storico fatture del fornitore su NetGescon.
+
+
+ +
+
+
Lavori pronti per chiusura / fatturazione
+
+ + + + + + + + + + + + @forelse($queueRows as $row) + + + + + + + + @empty + + + + @endforelse + +
TicketAmministratoreStatoAggiornato
+
#{{ $row['ticket_id'] }} - {{ $row['titolo'] }}
+
{{ $row['stabile'] }}
+
{{ $row['amministratore'] }} + {{ $row['stato'] }} + {{ $row['aggiornato'] }} + Apri +
Nessun lavoro in coda per la chiusura.
+
+
+ +
+
Fatture registrate su NetGescon
+ @if(! $contabilitaReady) +
+ La contabilità fornitore dedicata non è ancora stata attivata su questo ambiente. Questa pagina è pronta a leggerla appena il modulo contabile è disponibile. +
+ @else +
+ + + + + + + + + + @forelse($fattureRows as $row) + + + + + + @empty + + + + @endforelse + +
DocumentoStudioTotale
+
{{ $row['numero_documento'] }}
+
{{ $row['data_documento'] }} · {{ $row['stato'] }}
+ @if($row['descrizione'] !== '') +
{{ $row['descrizione'] }}
+ @endif +
+
{{ $row['amministratore'] }}
+
{{ $row['stabile'] }}
+
+
{{ number_format($row['totale'], 2, ',', '.') }} €
+
Netto {{ number_format($row['netto'], 2, ',', '.') }} €
+
Nessuna fattura registrata per questo fornitore.
+
+ @endif +
+
+ @endif +
+
\ No newline at end of file diff --git a/resources/views/filament/pages/fornitore/ticket-intervento-scheda.blade.php b/resources/views/filament/pages/fornitore/ticket-intervento-scheda.blade.php new file mode 100644 index 0000000..65c91ea --- /dev/null +++ b/resources/views/filament/pages/fornitore/ticket-intervento-scheda.blade.php @@ -0,0 +1,232 @@ + + @php($ticket = $this->intervento->ticket) + +
+
+
+
+
Intervento ticket #{{ $this->intervento->ticket_id }}
+
{{ $ticket->titolo ?? '-' }} · {{ $ticket->stabile->denominazione ?? '-' }}
+
Fornitore: {{ $this->fornitore->ragione_sociale ?: trim(($this->fornitore->nome ?? '') . ' ' . ($this->fornitore->cognome ?? '')) }}
+
+ +
+
+ +
+
+
+
Scheda segnalazione
+
+
+
Richiedente
+
{{ $caller['contatto'] ?: '-' }}
+
+
+
Telefono
+
+ @if($caller['telefono'] !== '') + {{ $caller['telefono'] }} + @else + - + @endif +
+
+
+
Stato intervento
+
{{ $this->intervento->stato }}
+
+
+
Operatore
+
{{ $this->intervento->operatore_assegnato_label }}
+
+
+
{{ $ticket->descrizione }}
+
+ +
+
+
Operativo e rapportino
+ @if($this->intervento->stato === 'assegnato') + Prendi in carico + @endif +
+ + @if($this->canAssignDipendente()) +
+
Assegnazione operatore
+
+ + Salva assegnazione +
+
+ @endif + +
+ + + +
+ + + +
+ + +
+ +
+ @foreach($articoliUtilizzati as $index => $item) + + @endforeach +
+ +
+ + +
+ +
+ @foreach($descrizioneFile as $index => $item) + + @endforeach +
+ +
+ +
+ + +
+
+ +
+ + +
+ +
+ Salva avanzamento + Invia sollecito +
+
+ +
+
Comunicazioni ticket
+
+ @forelse($ticket->messages as $msg) +
+
{{ $msg->user->name ?? 'Sistema' }}
+
{{ $msg->messaggio }}
+
{{ optional($msg->created_at)->format('d/m/Y H:i') }}
+
+ @empty +
Nessuna comunicazione disponibile.
+ @endforelse +
+
+
+ +
+
+
Stato operativo
+
+
Stato: {{ $this->intervento->stato }}
+
Iniziato: {{ optional($this->intervento->iniziato_at)->format('d/m/Y H:i') ?: '-' }}
+
Terminato: {{ optional($this->intervento->terminato_at)->format('d/m/Y H:i') ?: '-' }}
+
Rif. chiusura: {{ $this->intervento->riferimento_chiusura ?: '-' }}
+
QR registrato: {{ $this->intervento->qr_token ?: '-' }}
+
+
+ +
+
Allegati ticket
+
+ @forelse($ticket->attachments as $attachment) +
+
{{ $attachment->original_file_name }}
+
{{ $attachment->description ?: 'Nessuna descrizione' }}
+ Apri allegato +
+ @empty +
Nessun allegato disponibile.
+ @endforelse +
+
+ +
+
Storico correlato
+
+ @forelse($storicoRows as $row) +
+
#{{ $row['ticket_id'] }} - {{ $row['titolo'] }}
+
{{ $row['stato'] }} · {{ $row['updated_at'] }}
+ @if($row['note'] !== '') +
{{ \Illuminate\Support\Str::limit($row['note'], 90) }}
+ @endif +
+ @empty +
Nessuno storico correlato.
+ @endforelse +
+
+
+
+
+
\ No newline at end of file diff --git a/resources/views/filament/pages/fornitore/ticket-operativi.blade.php b/resources/views/filament/pages/fornitore/ticket-operativi.blade.php new file mode 100644 index 0000000..6316981 --- /dev/null +++ b/resources/views/filament/pages/fornitore/ticket-operativi.blade.php @@ -0,0 +1,94 @@ + +
+
+
+
+
Ticket operativi fornitore
+
+ @if($this->fornitoreLabel) + Vista operativa attiva per {{ $this->fornitoreLabel }}. + @else + Seleziona un fornitore dalla gestione ticket per aprire questa vista. + @endif +
+
+ +
+
+ + @if($this->missingAdminContext) +
+ Questa vista per amministratori richiede un fornitore selezionato. Aprila dalla gestione ticket oppure usa un link con il parametro fornitore. +
+ @else +
+ + + +
+ +
+
+ + + + + + + + + + + + + + @forelse($rows as $row) + + + + + + + + + + @empty + + + + @endforelse + +
TicketContattoProblemaOperatoreStatoAggiornato
+
#{{ $row['ticket_id'] }} - {{ $row['titolo'] }}
+
{{ $row['stabile'] }} · ingresso {{ $row['ingresso'] }}
+
+
{{ $row['contatto'] }}
+
{{ $row['telefono'] !== '' ? $row['telefono'] : 'Telefono non disponibile' }}
+
+
{{ $row['problema'] }}
+
Apparato: {{ $row['apparato'] }}
+
{{ $row['operatore'] }} + {{ $row['stato'] }} + {{ $row['updated_at'] }} + Apri scheda +
Nessun intervento trovato per questo filtro.
+
+
+ @endif +
+
\ No newline at end of file