feat: aggiunge archivio e modal alla lavagna post-it

This commit is contained in:
michele 2026-04-04 14:04:03 +00:00
parent ab7b804d08
commit b9b9e999f7
2 changed files with 264 additions and 48 deletions

View File

@ -48,6 +48,10 @@ class PostItGestione extends Page
public string $tecnicoScopeFilter = 'tutti';
public ?string $selectedGroupModalKey = null;
public string $selectedGroupModalMode = 'open';
/** @var array<int,string> */
public array $riaperturaNote = [];
@ -68,7 +72,7 @@ public function mount(): void
$focus = (int) request()->query('focus_post_it', 0);
$this->focusPostItId = $focus > 0 ? $focus : null;
if (in_array((string) request()->query('tab'), ['storico', 'lavagna'], true)) {
if (in_array((string) request()->query('tab'), ['storico', 'lavagna', 'archivio'], true)) {
$this->activeTab = (string) request()->query('tab');
}
}
@ -245,53 +249,76 @@ public function getRecentiVisibiliProperty()
->values();
}
public function getRecentiApertiVisibiliProperty()
{
return $this->recentiVisibili
->filter(fn(ChiamataPostIt $postIt): bool => (string) $postIt->stato !== 'chiusa')
->values();
}
public function getRecentiChiusiVisibiliProperty()
{
return $this->recentiVisibili
->filter(fn(ChiamataPostIt $postIt): bool => (string) $postIt->stato === 'chiusa')
->values();
}
public function getRecentiRaggruppatiProperty()
{
$groups = [];
return $this->buildGroupedCollection($this->recentiApertiVisibili);
}
foreach ($this->recentiVisibili as $postIt) {
if ($this->isInternalPostIt($postIt)) {
continue;
}
public function getRecentiChiusiRaggruppatiProperty()
{
return $this->buildGroupedCollection($this->recentiChiusiVisibili);
}
$groupKey = $this->buildGroupedPostItKey($postIt);
if (isset($groups[$groupKey])) {
$groups[$groupKey]['items'][] = $postIt;
$groups[$groupKey]['count']++;
$groups[$groupKey]['last_at'] = optional($postIt->chiamata_il)->format('d/m/Y H:i') ?: '-';
$groups[$groupKey]['latest'] = $this->resolveLatestGroupedPostIt($groups[$groupKey]['items']);
$groups[$groupKey]['caller_label'] = $this->resolveGroupedCallerLabel($groups[$groupKey]['items']);
continue;
}
public function openGroupModal(string $modalKey, string $mode = 'open'): void
{
$this->selectedGroupModalKey = $modalKey;
$this->selectedGroupModalMode = in_array($mode, ['open', 'closed'], true) ? $mode : 'open';
}
$groups[$groupKey] = [
'key' => $groupKey,
'items' => [$postIt],
'count' => 1,
'latest' => $postIt,
'caller_label' => $postIt->nome_chiamante ?: 'Chiamante non identificato',
'phone' => (string) ($postIt->telefono ?? ''),
'first_at' => optional($postIt->chiamata_il)->format('d/m/Y H:i') ?: '-',
'last_at' => optional($postIt->chiamata_il)->format('d/m/Y H:i') ?: '-',
];
public function closeGroupModal(): void
{
$this->selectedGroupModalKey = null;
$this->selectedGroupModalMode = 'open';
}
public function getSelectedGroupModalProperty(): ?array
{
if ($this->selectedGroupModalKey === null) {
return null;
}
return collect(array_values($groups))
->map(function (array $group): array {
usort($group['items'], function (ChiamataPostIt $left, ChiamataPostIt $right): int {
return ($right->chiamata_il?->getTimestamp() ?? 0) <=> ($left->chiamata_il?->getTimestamp() ?? 0);
});
$groups = $this->selectedGroupModalMode === 'closed'
? $this->recentiChiusiRaggruppati
: $this->recentiRaggruppati;
$group['latest'] = $this->resolveLatestGroupedPostIt($group['items']);
$oldest = collect($group['items'])
->sortBy(fn(ChiamataPostIt $item) => $item->chiamata_il?->getTimestamp() ?? 0)
->first();
$group['first_at'] = optional($oldest?->chiamata_il)->format('d/m/Y H:i') ?: '-';
$group['last_at'] = optional($group['latest']->chiamata_il)->format('d/m/Y H:i') ?: '-';
return $groups->firstWhere('modal_key', $this->selectedGroupModalKey);
}
return $group;
public function getSelectedGroupModalDaysProperty()
{
$group = $this->selectedGroupModal;
if (! is_array($group)) {
return collect();
}
return collect($group['items'])
->groupBy(function (ChiamataPostIt $item): string {
return $item->chiamata_il?->format('d/m/Y') ?: 'Data non disponibile';
})
->map(function ($items, string $day): array {
$sorted = collect($items)
->sortByDesc(fn(ChiamataPostIt $item) => $item->chiamata_il?->getTimestamp() ?? 0)
->values();
return [
'day' => $day,
'items' => $sorted,
];
})
->sortByDesc(fn(array $group) => $group['latest']->chiamata_il?->getTimestamp() ?? 0)
->values();
}
@ -585,6 +612,11 @@ private function buildTicketDescriptionFromPostIt(int $postItId, ChiamataPostIt
private function buildGroupedPostItKey(ChiamataPostIt $postIt): string
{
$rubricaId = $this->resolveGroupedRubricaId($postIt);
if ($rubricaId !== null) {
return 'rubrica:' . $rubricaId;
}
$phone = $this->normalizeGroupedPhone((string) ($postIt->telefono ?? ''));
if ($phone !== '') {
return 'phone:' . $phone;
@ -600,6 +632,11 @@ private function isInternalPostIt(ChiamataPostIt $postIt): bool
return $this->isInternalSmdrMessage($message);
}
$phone = preg_replace('/\D+/', '', (string) ($postIt->telefono ?? ''));
if (is_string($phone) && $phone !== '' && $this->isKnownPbxExtension($phone)) {
return true;
}
return false;
}
@ -625,6 +662,16 @@ private function normalizeGroupedPhone(string $phone): string
return preg_replace('/\D+/', '', $phone) ?: '';
}
private function resolveGroupedRubricaId(ChiamataPostIt $postIt): ?int
{
$rubricaId = (int) ($postIt->rubrica_id ?? 0);
if ($rubricaId > 0) {
return $rubricaId;
}
return $this->resolveRubricaIdByPhone((string) ($postIt->telefono ?? ''));
}
/**
* @param array<int,ChiamataPostIt> $items
*/
@ -643,6 +690,17 @@ private function resolveLatestGroupedPostIt(array $items): ChiamataPostIt
private function resolveGroupedCallerLabel(array $items): string
{
foreach ($items as $item) {
$rubricaId = $this->resolveGroupedRubricaId($item);
if ($rubricaId) {
$rubrica = RubricaUniversale::query()->find($rubricaId, ['ragione_sociale', 'nome', 'cognome']);
if ($rubrica) {
$label = trim((string) ($rubrica->ragione_sociale ?: (($rubrica->nome ?? '') . ' ' . ($rubrica->cognome ?? ''))));
if ($label !== '') {
return $label;
}
}
}
$label = trim((string) ($item->nome_chiamante ?? ''));
if ($label !== '') {
return $label;
@ -652,6 +710,70 @@ private function resolveGroupedCallerLabel(array $items): string
return 'Chiamante non identificato';
}
private function resolveGroupedPhoneLabel(array $items): string
{
foreach ($items as $item) {
$phone = trim((string) ($item->telefono ?? ''));
if ($phone !== '') {
return $phone;
}
}
return '';
}
private function buildGroupedCollection($items)
{
$groups = [];
foreach ($items as $postIt) {
if ($this->isInternalPostIt($postIt)) {
continue;
}
$groupKey = $this->buildGroupedPostItKey($postIt);
if (isset($groups[$groupKey])) {
$groups[$groupKey]['items'][] = $postIt;
$groups[$groupKey]['count']++;
continue;
}
$groups[$groupKey] = [
'key' => $groupKey,
'modal_key' => md5($groupKey),
'items' => [$postIt],
'count' => 1,
'latest' => $postIt,
'caller_label' => $postIt->nome_chiamante ?: 'Chiamante non identificato',
'phone' => (string) ($postIt->telefono ?? ''),
'first_at' => optional($postIt->chiamata_il)->format('d/m/Y H:i') ?: '-',
'last_at' => optional($postIt->chiamata_il)->format('d/m/Y H:i') ?: '-',
];
}
return collect(array_values($groups))
->map(function (array $group): array {
usort($group['items'], function (ChiamataPostIt $left, ChiamataPostIt $right): int {
return ($right->chiamata_il?->getTimestamp() ?? 0) <=> ($left->chiamata_il?->getTimestamp() ?? 0);
});
$group['latest'] = $this->resolveLatestGroupedPostIt($group['items']);
$oldest = collect($group['items'])
->sortBy(fn(ChiamataPostIt $item) => $item->chiamata_il?->getTimestamp() ?? 0)
->first();
$group['first_at'] = optional($oldest?->chiamata_il)->format('d/m/Y H:i') ?: '-';
$group['last_at'] = optional($group['latest']->chiamata_il)->format('d/m/Y H:i') ?: '-';
$group['caller_label'] = $this->resolveGroupedCallerLabel($group['items']);
$group['phone'] = $this->resolveGroupedPhoneLabel($group['items']);
$group['visible_items'] = collect($group['items'])->take(4)->values()->all();
$group['hidden_count'] = max(0, count($group['items']) - count($group['visible_items']));
return $group;
})
->sortByDesc(fn(array $group) => $group['latest']->chiamata_il?->getTimestamp() ?? 0)
->values();
}
public function richiediClickToCallDaMessaggio(int $messageId): void
{
$user = Auth::user();

View File

@ -18,6 +18,9 @@
<button type="button" wire:click="$set('activeTab', 'lavagna')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $activeTab === 'lavagna' ? 'bg-amber-600 text-white' : 'bg-amber-50 text-amber-900 hover:bg-amber-100' }}">
Lavagna chiamate
</button>
<button type="button" wire:click="$set('activeTab', 'archivio')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $activeTab === 'archivio' ? 'bg-slate-700 text-white' : 'bg-slate-100 text-slate-700 hover:bg-slate-200' }}">
Archivio chiusi
</button>
<button type="button" wire:click="$set('activeTab', 'smdr')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $activeTab === 'smdr' ? 'bg-indigo-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">
Chiamate Tecniche SMDR
</button>
@ -118,10 +121,10 @@
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
@forelse($this->recentiRaggruppati as $gruppo)
@php($box = $gruppo['latest'])
<article class="rounded-2xl border border-amber-200 bg-[#fff4b8] p-4 shadow-sm">
<article class="flex min-h-[320px] flex-col rounded-2xl border border-amber-200 bg-[#fff4b8] p-4 shadow-sm">
<div class="flex items-start justify-between gap-3">
<div>
<div class="text-sm font-semibold uppercase tracking-wide text-amber-900">{{ $box->oggetto ?: 'Post-it chiamate' }}</div>
<div class="text-sm font-semibold uppercase tracking-wide text-amber-900">{{ $box->direzione === 'in_uscita' ? 'Chiamata in uscita' : 'Chiamata in arrivo' }}</div>
<div class="mt-1 text-sm text-amber-950">
{{ $gruppo['caller_label'] }}
@if($gruppo['phone'] !== '')
@ -140,27 +143,32 @@
Aperto dal {{ $gruppo['first_at'] }} al {{ $gruppo['last_at'] }}
</div>
<div class="mt-4 space-y-3 border-t border-amber-300 pt-3">
@foreach($gruppo['items'] as $item)
<div class="rounded-xl border border-amber-300 bg-amber-50/70 p-3">
<div class="mt-4 flex-1 space-y-2 border-t border-amber-300 pt-3">
@foreach($gruppo['visible_items'] as $item)
<div class="rounded-xl border border-amber-300 bg-amber-50/70 p-2.5">
<div class="flex items-start justify-between gap-3">
<div>
<div class="text-sm font-medium text-amber-950">{{ $item->oggetto ?: 'Senza oggetto' }}</div>
<div class="text-xs text-amber-800">{{ optional($item->chiamata_il)->format('d/m/Y H:i') }}</div>
<div class="text-xs font-medium text-amber-950">{{ $item->oggetto ?: 'Senza oggetto' }}</div>
<div class="text-[11px] text-amber-800">{{ optional($item->chiamata_il)->format('d/m/Y H:i') }}</div>
</div>
<span class="rounded-md bg-white/70 px-2 py-1 text-[11px] text-amber-900">{{ $item->stato }}</span>
</div>
@if($item->stabile)
<div class="mt-1 text-xs text-amber-900">{{ $item->stabile->denominazione ?: ('Stabile #' . $item->stabile->id) }}</div>
<div class="mt-1 text-[11px] text-amber-900">{{ $item->stabile->denominazione ?: ('Stabile #' . $item->stabile->id) }}</div>
@endif
<div class="mt-2 whitespace-pre-wrap text-xs text-amber-950">{{ $item->nota }}</div>
</div>
@endforeach
@if($gruppo['hidden_count'] > 0)
<div class="rounded-xl border border-dashed border-amber-300 bg-white/50 p-2.5 text-[11px] text-amber-900">
Altre {{ $gruppo['hidden_count'] }} chiamate visibili nel dettaglio.
</div>
@endif
</div>
<div class="mt-4 flex flex-wrap gap-2">
<x-filament::button size="sm" color="warning" wire:click="openGroupModal('{{ $gruppo['modal_key'] }}', 'open')">Vedi dettaglio</x-filament::button>
<a href="{{ $this->getPostItPageUrl((int) $box->id) }}" class="inline-flex items-center rounded-md bg-amber-700 px-3 py-1.5 text-xs font-medium text-white hover:bg-amber-600">Apri Post-it</a>
@if($box->ticket_id)
<a href="{{ \App\Filament\Pages\Supporto\TicketGestione::getUrl(panel: 'admin-filament') }}#ticket-{{ (int) $box->ticket_id }}" class="inline-flex items-center rounded-md border border-amber-400 bg-white/70 px-3 py-1.5 text-xs text-amber-900">Apri ticket</a>
@ -172,6 +180,49 @@
@endforelse
</div>
</div>
@elseif($activeTab === 'archivio')
<div class="rounded-xl border border-slate-200 bg-slate-50/50 p-4">
<div class="mb-4">
<h2 class="text-base font-semibold text-slate-900">Archivio Post-it chiusi</h2>
<p class="text-sm text-slate-700">I Post-it chiusi restano collegati al numero o al nominativo e possono essere riaperti quando serve. Anche qui le chiamate interne sono escluse.</p>
</div>
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
@forelse($this->recentiChiusiRaggruppati as $gruppo)
@php($box = $gruppo['latest'])
<article class="flex min-h-[260px] flex-col rounded-2xl border border-slate-200 bg-white p-4 shadow-sm">
<div class="flex items-start justify-between gap-3">
<div>
<div class="text-sm font-semibold uppercase tracking-wide text-slate-800">Archivio chiuso</div>
<div class="mt-1 text-sm text-slate-950">{{ $gruppo['caller_label'] }}@if($gruppo['phone'] !== '') - {{ $gruppo['phone'] }}@endif</div>
</div>
<span class="rounded-full bg-slate-100 px-2.5 py-1 text-xs font-semibold text-slate-700">{{ $gruppo['count'] }} chiusi</span>
</div>
<div class="mt-3 text-xs text-slate-600">Dal {{ $gruppo['first_at'] }} al {{ $gruppo['last_at'] }}</div>
<div class="mt-4 flex-1 space-y-2 border-t border-slate-200 pt-3">
@foreach($gruppo['visible_items'] as $item)
<div class="rounded-xl border border-slate-200 bg-slate-50 p-2.5">
<div class="text-xs font-medium text-slate-900">{{ $item->oggetto ?: 'Senza oggetto' }}</div>
<div class="text-[11px] text-slate-600">{{ optional($item->chiamata_il)->format('d/m/Y H:i') }}</div>
</div>
@endforeach
@if($gruppo['hidden_count'] > 0)
<div class="rounded-xl border border-dashed border-slate-300 bg-white p-2.5 text-[11px] text-slate-700">Altri {{ $gruppo['hidden_count'] }} Post-it chiusi nel dettaglio.</div>
@endif
</div>
<div class="mt-4 flex flex-wrap gap-2">
<x-filament::button size="sm" color="gray" wire:click="openGroupModal('{{ $gruppo['modal_key'] }}', 'closed')">Vedi dettaglio</x-filament::button>
<a href="{{ $this->getPostItPageUrl((int) $box->id) }}" class="inline-flex items-center rounded-md border border-slate-300 px-3 py-1.5 text-xs text-slate-700">Apri ultimo Post-it</a>
</div>
</article>
@empty
<div class="rounded-lg border border-dashed border-slate-300 bg-white p-4 text-sm text-slate-700 md:col-span-2 xl:col-span-3">Nessun Post-it chiuso disponibile in archivio.</div>
@endforelse
</div>
</div>
@elseif(in_array($activeTab, ['smdr', 'csta'], true))
<div class="rounded-xl border bg-white p-4">
@php($isCstaTab = $activeTab === 'csta')
@ -333,6 +384,49 @@
</div>
</div>
@endif
@if($this->selectedGroupModal)
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
<div class="max-h-[85vh] w-full max-w-5xl overflow-hidden rounded-2xl bg-white shadow-2xl">
<div class="flex items-center justify-between border-b px-5 py-4">
<div>
<div class="text-sm font-semibold text-slate-900">{{ $this->selectedGroupModal['caller_label'] }}@if($this->selectedGroupModal['phone'] !== '') - {{ $this->selectedGroupModal['phone'] }}@endif</div>
<div class="text-xs text-slate-500">{{ $this->selectedGroupModal['count'] }} chiamate totali · {{ $selectedGroupModalMode === 'closed' ? 'archivio chiusi' : 'lavagna aperti' }}</div>
</div>
<button type="button" wire:click="closeGroupModal" class="rounded-md bg-slate-100 px-3 py-1.5 text-xs text-slate-700 hover:bg-slate-200">Chiudi</button>
</div>
<div class="max-h-[70vh] overflow-y-auto p-5">
<div class="space-y-5">
@foreach($this->selectedGroupModalDays as $day)
<section>
<div class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-500">{{ $day['day'] }}</div>
<div class="space-y-3">
@foreach($day['items'] as $item)
<div class="rounded-xl border bg-slate-50 p-3">
<div class="flex items-start justify-between gap-3">
<div>
<div class="text-sm font-medium text-slate-900">{{ $item->oggetto ?: 'Senza oggetto' }}</div>
<div class="text-xs text-slate-600">{{ optional($item->chiamata_il)->format('d/m/Y H:i') }}</div>
</div>
<span class="rounded-md bg-white px-2 py-1 text-[11px] text-slate-700">{{ $item->stato }}</span>
</div>
@if($item->stabile)
<div class="mt-1 text-xs text-slate-700">{{ $item->stabile->denominazione ?: ('Stabile #' . $item->stabile->id) }}</div>
@endif
<div class="mt-2 whitespace-pre-wrap text-xs text-slate-800">{{ $item->nota }}</div>
</div>
@endforeach
</div>
</section>
@endforeach
</div>
</div>
</div>
</div>
@endif
@endif
</div>
</x-filament-panels::page>