feat(ticket-postit): supplier progress, timestamped notes, reopen flow

This commit is contained in:
michele 2026-03-12 08:25:34 +00:00
parent 334cc754f9
commit 2b7dcb6ab7
4 changed files with 127 additions and 9 deletions

View File

@ -30,6 +30,9 @@ class PostItGestione extends Page
protected string $view = 'filament.pages.strumenti.post-it-gestione';
/** @var array<int,string> */
public array $riaperturaNote = [];
public function getPostItInserimentoUrl(): string
{
return PostIt::getUrl(panel: 'admin-filament');
@ -106,6 +109,66 @@ public function chiudiPostIt(int $postItId): void
->send();
}
public function riapriPostIt(int $postItId): void
{
if (! $this->isPostItTableReady()) {
return;
}
$postIt = ChiamataPostIt::query()->find($postItId);
if (! $postIt) {
return;
}
$noteInput = trim((string) ($this->riaperturaNote[$postItId] ?? ''));
$stamp = now()->format('d/m/Y H:i');
$noteLine = $noteInput !== ''
? '[' . $stamp . '] Riapertura: ' . $noteInput
: '[' . $stamp . '] Riapertura da gestione Post-it';
$existing = trim((string) ($postIt->nota ?? ''));
$postIt->nota = $existing !== '' ? ($existing . "\n" . $noteLine) : $noteLine;
$postIt->stato = $postIt->ticket_id ? 'ticket' : 'post_it';
if (Schema::hasColumn('chiamate_post_it', 'chiusa_il')) {
$postIt->chiusa_il = null;
}
$postIt->save();
if ($postIt->ticket_id) {
$ticket = Ticket::query()->find((int) $postIt->ticket_id);
if ($ticket && in_array((string) $ticket->stato, ['Chiuso', 'Risolto'], true)) {
$ticket->stato = 'Preso in Carico';
if (Schema::hasColumn('tickets', 'data_chiusura_effettiva')) {
$ticket->data_chiusura_effettiva = null;
}
if (Schema::hasColumn('tickets', 'data_risoluzione_effettiva')) {
$ticket->data_risoluzione_effettiva = null;
}
$ticket->save();
if (method_exists($ticket, 'messages') && Schema::hasTable('ticket_messages')) {
$ticket->messages()->create([
'user_id' => Auth::id(),
'messaggio' => $noteLine,
]);
}
}
}
unset($this->riaperturaNote[$postItId]);
Notification::make()
->title('Post-it riaperto')
->body('Riapertura completata con nota di tracciamento.')
->success()
->send();
}
public function getRecentiProperty()
{
if (! $this->isPostItTableReady()) {

View File

@ -281,9 +281,10 @@ public function aggiungiNotaInterna(): void
'notaInterna' => ['required', 'string', 'max:5000'],
]);
$stamp = now()->format('d/m/Y H:i');
$ticket->messages()->create([
'user_id' => Auth::id(),
'messaggio' => trim((string) $this->notaInterna),
'messaggio' => '[' . $stamp . '] ' . trim((string) $this->notaInterna),
]);
$this->notaInterna = null;
@ -417,24 +418,56 @@ private function refreshFornitoriAttiviRows(): void
$ticketApertiStati = ['Aperto', 'Preso in Carico', 'In Lavorazione', 'In Attesa Approvazione', 'In Attesa Ricambi'];
$assignedByFornitore = Ticket::query()
$assignedTickets = Ticket::query()
->where('stabile_id', $stabileId)
->whereIn('stato', $ticketApertiStati)
->whereNotNull('assegnato_a_fornitore_id')
->selectRaw('assegnato_a_fornitore_id as fornitore_id, COUNT(*) as ticket_attivi')
->get(['id', 'assegnato_a_fornitore_id', 'stato', 'updated_at']);
$assignedByFornitore = $assignedTickets
->groupBy('assegnato_a_fornitore_id')
->pluck('ticket_attivi', 'fornitore_id');
->map(fn($rows) => $rows->count());
$ticketSummaries = $assignedTickets
->groupBy('assegnato_a_fornitore_id')
->map(function ($rows) {
return $rows->sortByDesc('updated_at')->take(6)->map(function ($ticket): string {
return '#' . ((int) $ticket->id) . ' (' . (string) $ticket->stato . ')';
})->values()->all();
});
$openInterventiByFornitore = collect();
$interventoStatiByFornitore = collect();
$lastInterventoAtByFornitore = collect();
if (Schema::hasTable('ticket_interventi')) {
$openInterventiByFornitore = TicketIntervento::query()
$openInterventi = TicketIntervento::query()
->whereIn('stato', ['assegnato', 'in_corso', 'in_verifica'])
->whereHas('ticket', function ($q) use ($stabileId, $ticketApertiStati): void {
$q->where('stabile_id', $stabileId)->whereIn('stato', $ticketApertiStati);
})
->selectRaw('fornitore_id, COUNT(*) as interventi_aperti')
->orderByDesc('updated_at')
->get(['fornitore_id', 'stato', 'updated_at']);
$openInterventiByFornitore = $openInterventi
->groupBy('fornitore_id')
->pluck('interventi_aperti', 'fornitore_id');
->map(fn($rows) => $rows->count());
$interventoStatiByFornitore = $openInterventi
->groupBy('fornitore_id')
->map(function ($rows): string {
return $rows->pluck('stato')
->map(fn($stato) => str_replace('_', ' ', (string) $stato))
->unique()
->take(3)
->implode(', ');
});
$lastInterventoAtByFornitore = $openInterventi
->groupBy('fornitore_id')
->map(function ($rows): ?string {
$last = $rows->sortByDesc('updated_at')->first();
return $last && $last->updated_at ? $last->updated_at->format('d/m/Y H:i') : null;
});
}
$fornitoriIds = collect($assignedByFornitore->keys())
@ -458,6 +491,9 @@ private function refreshFornitoriAttiviRows(): void
'telefono' => (string) ($fornitore->telefono ?: $fornitore->cellulare),
'ticket_attivi' => (int) ($assignedByFornitore->get((string) $fornitore->id) ?? $assignedByFornitore->get((int) $fornitore->id) ?? 0),
'interventi_aperti' => (int) ($openInterventiByFornitore->get((string) $fornitore->id) ?? $openInterventiByFornitore->get((int) $fornitore->id) ?? 0),
'ticket_focus' => (array) ($ticketSummaries->get((string) $fornitore->id) ?? $ticketSummaries->get((int) $fornitore->id) ?? []),
'intervento_stati' => (string) ($interventoStatiByFornitore->get((string) $fornitore->id) ?? $interventoStatiByFornitore->get((int) $fornitore->id) ?? ''),
'ultimo_aggiornamento' => (string) ($lastInterventoAtByFornitore->get((string) $fornitore->id) ?? $lastInterventoAtByFornitore->get((int) $fornitore->id) ?? ''),
];
}

View File

@ -55,6 +55,14 @@
<x-filament::button size="sm" color="gray" wire:click="chiudiPostIt({{ $riga->id }})">Chiudi</x-filament::button>
@endif
@php($ticketClosed = $riga->ticket && in_array((string) $riga->ticket->stato, ['Chiuso', 'Risolto'], true))
@if($riga->stato === 'chiusa' || $ticketClosed)
<div class="flex flex-wrap items-center gap-2 rounded-md border bg-slate-50 px-2 py-1">
<input type="text" wire:model.defer="riaperturaNote.{{ (int) $riga->id }}" class="w-56 rounded-md border-gray-300 text-xs" placeholder="Nota riapertura (opzionale)" />
<x-filament::button size="sm" color="warning" wire:click="riapriPostIt({{ (int) $riga->id }})">Riapri post-it/ticket</x-filament::button>
</div>
@endif
@if($riga->ticket_id)
<a href="{{ \App\Filament\Pages\Supporto\TicketGestione::getUrl(panel: 'admin-filament') }}#ticket-{{ (int) $riga->ticket_id }}" class="inline-flex items-center rounded-md border px-3 py-1 text-xs">Apri Ticket (#{{ $riga->ticket_id }})</a>
@endif

View File

@ -89,7 +89,7 @@
<div class="mt-4 rounded-xl border bg-white p-4">
<div class="text-sm font-semibold">Fornitori attivi su ticket aperti</div>
<div class="mt-1 text-xs text-gray-500">Elenco dinamico dei fornitori che stanno gestendo almeno un ticket/intervento aperto nello stabile attivo.</div>
<div class="mt-1 text-xs text-gray-500">Mostra solo fornitori con ticket/interventi in corso, con stato operativo e ultimo aggiornamento.</div>
<div class="mt-3 overflow-x-auto">
<table class="min-w-full border-collapse border text-xs">
@ -99,6 +99,7 @@
<th class="border px-2 py-2 text-left">Contatti</th>
<th class="border px-2 py-2 text-left">Ticket attivi</th>
<th class="border px-2 py-2 text-left">Interventi aperti</th>
<th class="border px-2 py-2 text-left">Come procede</th>
<th class="border px-2 py-2 text-left">Azioni</th>
</tr>
</thead>
@ -109,6 +110,15 @@
<td class="border px-2 py-2">{{ $f['email'] ?: '-' }}<br>{{ $f['telefono'] ?: '-' }}</td>
<td class="border px-2 py-2">{{ (int) $f['ticket_attivi'] }}</td>
<td class="border px-2 py-2">{{ (int) $f['interventi_aperti'] }}</td>
<td class="border px-2 py-2">
<div class="space-y-1 text-[11px]">
<div><span class="font-medium">Stati intervento:</span> {{ $f['intervento_stati'] ?: 'N/D' }}</div>
<div><span class="font-medium">Ultimo aggiornamento:</span> {{ $f['ultimo_aggiornamento'] ?: '-' }}</div>
@if(!empty($f['ticket_focus']))
<div class="text-gray-600">{{ implode(', ', $f['ticket_focus']) }}</div>
@endif
</div>
</td>
<td class="border px-2 py-2">
<div class="flex flex-wrap gap-1">
<a href="{{ $this->getFornitoreOperativoUrl((int) $f['id']) }}" class="inline-flex items-center rounded-md bg-indigo-700 px-2 py-1 text-[11px] font-medium text-white hover:bg-indigo-600">Scheda operativa</a>
@ -118,7 +128,7 @@
</tr>
@empty
<tr>
<td colspan="5" class="border px-2 py-4 text-center text-gray-500">Nessun fornitore attivo su ticket aperti.</td>
<td colspan="6" class="border px-2 py-4 text-center text-gray-500">Nessun fornitore attivo su ticket aperti.</td>
</tr>
@endforelse
</tbody>
@ -170,6 +180,7 @@
<div class="rounded-xl border bg-white p-4">
<div class="text-sm font-semibold">Nota interna</div>
<div class="mt-1 text-xs text-gray-500">Puoi aggiungere piu note: ogni nota viene salvata con data/ora automatica.</div>
<label class="mt-3 block text-sm">
<span class="mb-1 block font-medium">Nuova nota</span>
<textarea rows="4" wire:model.defer="notaInterna" class="w-full rounded-lg border-gray-300" placeholder="Aggiungi aggiornamento interno"></textarea>