Fix post-it live flow and legacy imports

This commit is contained in:
michele 2026-03-31 11:55:43 +00:00
parent 8c10069a71
commit cd5d004561
7 changed files with 584 additions and 100 deletions

View File

@ -817,7 +817,7 @@ private function normalizePhone(?string $phone): ?string
* Recupera identità tenant da altre righe condomin della stessa unità, quando la riga corrente è incompleta. * Recupera identità tenant da altre righe condomin della stessa unità, quando la riga corrente è incompleta.
* @return array{cf:?string,email:?string,telefono:?string,cellulare:?string} * @return array{cf:?string,email:?string,telefono:?string,cellulare:?string}
*/ */
private function lookupTenantIdentityFromStaging(string $conn, string $codStabile, string $scala, string $interno, mixed $piano, mixed $tenantNameHint): array private function lookupTenantIdentityFromStaging(string $conn, string $codStabile, ?string $scala, ?string $interno, mixed $piano, mixed $tenantNameHint): array
{ {
try { try {
if (! Schema::connection($conn)->hasTable('condomin')) { if (! Schema::connection($conn)->hasTable('condomin')) {

View File

@ -14,6 +14,8 @@ class ImportLegacyFornitoriTagsCommand extends Command
protected $description = 'Importa e normalizza i tag fornitori dalla colonna legacy descrizione (gescon_import.fornitori).'; protected $description = 'Importa e normalizza i tag fornitori dalla colonna legacy descrizione (gescon_import.fornitori).';
private bool $hasLegacyDescrizioneColumn = false;
public function handle(): int public function handle(): int
{ {
if (! config('database.connections.gescon_import')) { if (! config('database.connections.gescon_import')) {
@ -22,30 +24,38 @@ public function handle(): int
} }
$dryRun = (bool) $this->option('dry-run'); $dryRun = (bool) $this->option('dry-run');
$this->hasLegacyDescrizioneColumn = Schema::hasColumn('fornitori', 'legacy_descrizione');
$filterIds = collect((array) $this->option('fornitore-id')) $filterIds = collect((array) $this->option('fornitore-id'))
->map(fn($v) => (int) $v) ->map(fn($v) => (int) $v)
->filter(fn(int $v): bool => $v > 0) ->filter(fn(int $v): bool => $v > 0)
->values() ->values()
->all(); ->all();
[$legacyTable, $descrColumn] = $this->resolveLegacySource(); $source = $this->resolveLegacySource();
if ($legacyTable === null || $descrColumn === null) { if ($source === null) {
$this->warn('Sorgente legacy non trovata: manca tabella/colonna descrizione su gescon_import.'); $this->warn('Sorgente legacy non trovata: manca tabella/colonna descrizione su gescon_import.');
return self::FAILURE; return self::FAILURE;
} }
$legacyRows = DB::connection('gescon_import') $legacyRows = DB::connection('gescon_import')
->table($legacyTable) ->table($source['table'])
->select([ ->select([
'id', $this->selectColumnOrNull($source['id_column'] ?? null, 'id'),
'cod_forn', $this->selectColumnOrNull($source['code_column'] ?? null, 'cod_forn'),
'ragione_sociale', $this->selectCoalescedColumnOrNull($source['name_columns'] ?? [], 'ragione_sociale'),
'partita_iva', $this->selectColumnOrNull($source['piva_column'] ?? null, 'partita_iva'),
'codice_fiscale', $this->selectColumnOrNull($source['cf_column'] ?? null, 'codice_fiscale'),
DB::raw($descrColumn . ' as descrizione_legacy'), $this->selectColumnOrNull($source['description_column'] ?? null, 'descrizione_legacy'),
$this->selectColumnOrNull($source['notes_column'] ?? null, 'note_legacy'),
]) ])
->whereNotNull($descrColumn) ->where(function ($query) use ($source): void {
->where($descrColumn, '<>', '') foreach (($source['text_columns'] ?? []) as $column) {
$query->orWhere(function ($inner) use ($column): void {
$inner->whereNotNull($column)
->where($column, '<>', '');
});
}
})
->get(); ->get();
if ($legacyRows->isEmpty()) { if ($legacyRows->isEmpty()) {
@ -67,7 +77,8 @@ public function handle(): int
continue; continue;
} }
$incoming = $this->extractTags((string) ($legacy->descrizione_legacy ?? '')); $legacyDescrizione = $this->buildLegacyDescription($legacy);
$incoming = $this->extractTags($legacyDescrizione);
if (count($incoming) === 0) { if (count($incoming) === 0) {
$skipped++; $skipped++;
continue; continue;
@ -77,7 +88,6 @@ public function handle(): int
$merged = array_values(array_unique(array_merge($current, $incoming))); $merged = array_values(array_unique(array_merge($current, $incoming)));
sort($merged, SORT_NATURAL | SORT_FLAG_CASE); sort($merged, SORT_NATURAL | SORT_FLAG_CASE);
$legacyDescrizione = trim((string) ($legacy->descrizione_legacy ?? ''));
$newTagsValue = implode(', ', $merged); $newTagsValue = implode(', ', $merged);
$changed = ((string) ($fornitore->tags ?? '')) !== $newTagsValue $changed = ((string) ($fornitore->tags ?? '')) !== $newTagsValue
|| ((string) ($fornitore->legacy_descrizione ?? '')) !== $legacyDescrizione; || ((string) ($fornitore->legacy_descrizione ?? '')) !== $legacyDescrizione;
@ -93,8 +103,10 @@ public function handle(): int
continue; continue;
} }
$fornitore->tags = $newTagsValue; $fornitore->tags = $newTagsValue;
$fornitore->legacy_descrizione = $legacyDescrizione !== '' ? $legacyDescrizione : null; if ($this->hasLegacyDescrizioneColumn) {
$fornitore->legacy_descrizione = $legacyDescrizione !== '' ? $legacyDescrizione : null;
}
$fornitore->save(); $fornitore->save();
$updated++; $updated++;
} }
@ -105,24 +117,129 @@ public function handle(): int
return self::SUCCESS; return self::SUCCESS;
} }
private function resolveLegacySource(): array private function resolveLegacySource(): ?array
{ {
$tables = ['fornitori', 'fornitori_gescon']; $sources = [
$columns = ['descrizione', 'descr', 'note']; [
'table' => 'mdb_fornitori',
'id_column' => 'id_fornitore',
'code_column' => 'cod_forn',
'name_columns' => ['denominazione', 'ragione_sociale', 'cognome', 'nome'],
'piva_column' => 'p_iva',
'cf_column' => 'cod_fisc',
'description_column' => 'descrizione',
'notes_column' => 'note',
'text_columns' => ['descrizione', 'note'],
],
[
'table' => 'fornitori',
'id_column' => 'id',
'code_column' => 'cod_forn',
'name_columns' => ['ragione_sociale', 'cognome', 'nome'],
'piva_column' => 'partita_iva',
'cf_column' => 'codice_fiscale',
'description_column' => 'descrizione',
'notes_column' => 'note',
'text_columns' => ['descrizione', 'note'],
],
[
'table' => 'fornitori_gescon',
'id_column' => 'id',
'code_column' => 'cod_forn',
'name_columns' => ['ragione_sociale'],
'piva_column' => 'partita_iva',
'cf_column' => 'codice_fiscale',
'description_column' => 'descr',
'notes_column' => 'note',
'text_columns' => ['descr', 'note'],
],
];
foreach ($tables as $table) { foreach ($sources as $source) {
if (! Schema::connection('gescon_import')->hasTable($table)) { if (! Schema::connection('gescon_import')->hasTable($source['table'])) {
continue; continue;
} }
foreach ($columns as $column) { $hasColumn = fn(string $column): bool => Schema::connection('gescon_import')->hasColumn($source['table'], $column);
if (Schema::connection('gescon_import')->hasColumn($table, $column)) {
return [$table, $column]; $source['id_column'] = isset($source['id_column']) && $hasColumn($source['id_column'])
? $source['id_column']
: null;
$source['code_column'] = isset($source['code_column']) && $hasColumn($source['code_column'])
? $source['code_column']
: null;
$source['piva_column'] = isset($source['piva_column']) && $hasColumn($source['piva_column'])
? $source['piva_column']
: null;
$source['cf_column'] = isset($source['cf_column']) && $hasColumn($source['cf_column'])
? $source['cf_column']
: null;
$source['name_columns'] = array_values(array_filter(
$source['name_columns'] ?? [],
fn(string $column): bool => $hasColumn($column)
));
$availableTextColumns = [];
foreach (($source['text_columns'] ?? []) as $column) {
if ($hasColumn($column)) {
$availableTextColumns[] = $column;
} }
} }
if ($availableTextColumns === []) {
continue;
}
$source['text_columns'] = $availableTextColumns;
$source['description_column'] = in_array((string) ($source['description_column'] ?? ''), $availableTextColumns, true)
? $source['description_column']
: null;
$source['notes_column'] = in_array((string) ($source['notes_column'] ?? ''), $availableTextColumns, true)
? $source['notes_column']
: null;
return $source;
}
return null;
}
private function selectColumnOrNull(?string $column, string $alias): \Illuminate\Database\Query\Expression
{
if ($column === null) {
return DB::raw('NULL as ' . $alias);
}
return DB::raw($column . ' as ' . $alias);
}
/**
* @param array<int, string> $columns
*/
private function selectCoalescedColumnOrNull(array $columns, string $alias): \Illuminate\Database\Query\Expression
{
$usable = array_values(array_filter($columns, fn(string $column): bool => $column !== ''));
if ($usable === []) {
return DB::raw('NULL as ' . $alias);
}
$segments = array_map(fn(string $column): string => 'NULLIF(TRIM(' . $column . '), \'\')', $usable);
return DB::raw('COALESCE(' . implode(', ', $segments) . ') as ' . $alias);
}
private function buildLegacyDescription(object $legacy): string
{
$parts = [];
foreach (['descrizione_legacy', 'note_legacy'] as $field) {
$value = trim((string) ($legacy->{$field} ?? ''));
if ($value !== '') {
$parts[] = $value;
}
} }
return [null, null]; return implode(' | ', array_values(array_unique($parts)));
} }
private function resolveLocalFornitore(object $legacy): ?Fornitore private function resolveLocalFornitore(object $legacy): ?Fornitore
@ -165,6 +282,15 @@ private function resolveLocalFornitore(object $legacy): ?Fornitore
return Fornitore::query()->where('ragione_sociale', $ragione)->first(); return Fornitore::query()->where('ragione_sociale', $ragione)->first();
} }
$normalizedRagione = $this->normalizeComparableString($ragione);
if ($normalizedRagione !== '') {
return Fornitore::query()
->get(['id', 'ragione_sociale'])
->first(function (Fornitore $fornitore) use ($normalizedRagione): bool {
return $this->normalizeComparableString((string) ($fornitore->ragione_sociale ?? '')) === $normalizedRagione;
});
}
return null; return null;
} }
@ -179,7 +305,7 @@ private function extractTags(string $input): array
} }
$parts = preg_split('/[,;|\n\r\/]+/', $input) ?: []; $parts = preg_split('/[,;|\n\r\/]+/', $input) ?: [];
$tags = []; $tags = $this->extractKeywordTags($input);
foreach ($parts as $part) { foreach ($parts as $part) {
$normalized = $this->canonicalizeTag($part); $normalized = $this->canonicalizeTag($part);
@ -195,14 +321,93 @@ private function extractTags(string $input): array
return $tags; return $tags;
} }
/**
* @return array<int, string>
*/
private function extractKeywordTags(string $input): array
{
$haystack = mb_strtolower(trim($input));
if ($haystack === '') {
return [];
}
$keywords = [
'idr' => 'idraulico',
'termoidraul' => 'termoidraulico',
'elettric' => 'elettricista',
'energia elet' => 'energia elettrica',
'ascens' => 'ascensorista',
'puliz' => 'pulizie',
'giardin' => 'giardiniere',
'spurgh' => 'spurgo',
'edil' => 'edile',
'murator' => 'edile',
'fabbro' => 'fabbro',
'ferrament' => 'ferramenta',
'serrament' => 'serramenti',
'portier' => 'portierato',
'citofon' => 'citofonia',
'videosorv' => 'videosorveglianza',
'sicurezz' => 'sicurezza',
'antincend' => 'antincendio',
'estintor' => 'antincendio',
'calda' => 'caldaia',
'riscald' => 'riscaldamento',
'condizion' => 'condizionamento',
'gas' => 'gas',
'acqua' => 'acqua',
'fogna' => 'fognatura',
'antenna' => 'antenna',
'telecom' => 'telefonia',
'pec' => 'pec',
'aruba' => 'pec',
'posta' => 'postali',
'raccomand' => 'postali',
'spediz' => 'spedizioni',
'infest' => 'disinfestazione',
'amminist' => 'amministrazione',
'assicur' => 'assicurazione',
'avvocat' => 'legale',
'legale' => 'legale',
'commercial' => 'commercialista',
'geometr' => 'geometra',
'privacy' => 'privacy',
'timbri' => 'timbri',
'targhe' => 'targhe',
'paghe' => 'paghe contributi',
'contribut' => 'paghe contributi',
'utenz' => 'utenze',
];
$tags = [];
foreach ($keywords as $needle => $canonical) {
if (str_contains($haystack, $needle)) {
$tags[] = $canonical;
}
}
$tags = array_values(array_unique($tags));
sort($tags, SORT_NATURAL | SORT_FLAG_CASE);
return $tags;
}
private function canonicalizeTag(string $raw): ?string private function canonicalizeTag(string $raw): ?string
{ {
$clean = trim(mb_strtolower($raw)); $clean = trim(mb_strtolower($raw), " \t\n\r\0\x0B-_,.;:");
$clean = preg_replace('/\s+/', ' ', $clean) ?? ''; $clean = preg_replace('/\s+/', ' ', $clean) ?? '';
if ($clean === '' || mb_strlen($clean) < 3) { if ($clean === '' || mb_strlen($clean) < 3) {
return null; return null;
} }
if (str_contains($clean, '@') || preg_match('/\d{2,}/', $clean)) {
return null;
}
if (preg_match('/\b(tel|telefono|fax|cell|cellulare|pec|email|codice cliente|parlare con|sig\.?|sig\.ra)\b/u', $clean)) {
return null;
}
$map = [ $map = [
'idr' => 'idraulico', 'idr' => 'idraulico',
'idraul' => 'idraulico', 'idraul' => 'idraulico',
@ -213,14 +418,31 @@ private function canonicalizeTag(string $raw): ?string
'giardin' => 'giardiniere', 'giardin' => 'giardiniere',
'assicur' => 'assicurazione', 'assicur' => 'assicurazione',
'manut' => 'manutenzione', 'manut' => 'manutenzione',
'murator' => 'edile',
'privacy' => 'privacy',
'ferrament'=> 'ferramenta',
]; ];
foreach ($map as $needle => $canonical) { foreach ($map as $needle => $canonical) {
if (str_starts_with($clean, $needle)) { if (str_contains($clean, $needle)) {
return $canonical; return $canonical;
} }
} }
$lettersOnly = preg_replace('/[^[:alpha:] ]+/u', '', $clean) ?? '';
$wordCount = count(array_filter(explode(' ', $lettersOnly), fn(string $word): bool => $word !== ''));
if (mb_strlen(trim($lettersOnly)) < 3 || $wordCount > 3 || mb_strlen($clean) > 28) {
return null;
}
return $clean; return $clean;
} }
private function normalizeComparableString(string $value): string
{
$normalized = mb_strtolower(trim($value));
$normalized = preg_replace('/[^[:alnum:]]+/u', '', $normalized) ?? '';
return $normalized;
}
} }

View File

@ -128,6 +128,7 @@ public function mount(int | string $record): void
} }
$this->rubrica = RubricaUniversale::query()->findOrFail((int) $record); $this->rubrica = RubricaUniversale::query()->findOrFail((int) $record);
$this->applyLegacyIdentityDefaults();
$activeStabile = StabileContext::getActiveStabile($user); $activeStabile = StabileContext::getActiveStabile($user);
$adminId = (int) ($activeStabile?->amministratore_id ?: 0); $adminId = (int) ($activeStabile?->amministratore_id ?: 0);
@ -301,6 +302,17 @@ public function mount(int | string $record): void
$this->hydrateStudioCollaboratoreWorkspace(); $this->hydrateStudioCollaboratoreWorkspace();
} }
private function applyLegacyIdentityDefaults(): void
{
$legacyIdentity = $this->extractLegacyIdentityFromNote($this->rubrica->note ?? null);
if (! $this->rubrica->titolo_id && ! empty($legacyIdentity['titolo_id'])) {
$this->rubrica->titolo_id = (int) $legacyIdentity['titolo_id'];
$this->rubrica->saveQuietly();
$this->rubrica->refresh();
}
}
public function startInlineEdit(): void public function startInlineEdit(): void
{ {
$this->fillInlineForm(); $this->fillInlineForm();

View File

@ -34,6 +34,8 @@ class PostItGestione extends Page
protected string $view = 'filament.pages.strumenti.post-it-gestione'; protected string $view = 'filament.pages.strumenti.post-it-gestione';
public ?int $focusPostItId = null;
public string $activeTab = 'storico'; public string $activeTab = 'storico';
public string $tecnicoCallView = 'esterne'; public string $tecnicoCallView = 'esterne';
@ -53,6 +55,16 @@ class PostItGestione extends Page
/** @var array<string, array{id:int|null,name:string|null}> */ /** @var array<string, array{id:int|null,name:string|null}> */
private array $pbxExtensionCache = []; private array $pbxExtensionCache = [];
public function mount(): void
{
$focus = (int) request()->query('focus_post_it', 0);
$this->focusPostItId = $focus > 0 ? $focus : null;
if (request()->query('tab') === 'storico') {
$this->activeTab = 'storico';
}
}
public function getPostItInserimentoUrl(): string public function getPostItInserimentoUrl(): string
{ {
return PostIt::getUrl(panel: 'admin-filament'); return PostIt::getUrl(panel: 'admin-filament');
@ -196,11 +208,16 @@ public function getRecentiProperty()
} }
try { try {
return ChiamataPostIt::query() $query = ChiamataPostIt::query()
->with(['rubrica', 'stabile', 'ticket', 'creatoDa']) ->with(['rubrica', 'stabile', 'ticket', 'creatoDa'])
->orderByDesc('chiamata_il') ->orderByDesc('chiamata_il')
->limit(50) ->orderByDesc('id');
->get();
if ($this->focusPostItId) {
$query->orderByRaw('CASE WHEN id = ? THEN 0 ELSE 1 END', [$this->focusPostItId]);
}
return $query->limit(50)->get();
} catch (QueryException) { } catch (QueryException) {
return collect(); return collect();
} }

View File

@ -3,6 +3,7 @@
use App\Filament\Pages\Contabilita\EstrattoContoSoggetto; use App\Filament\Pages\Contabilita\EstrattoContoSoggetto;
use App\Filament\Pages\Gescon\RubricaUniversaleScheda; use App\Filament\Pages\Gescon\RubricaUniversaleScheda;
use App\Filament\Pages\Strumenti\PostItGestione;
use App\Models\CategoriaTicket; use App\Models\CategoriaTicket;
use App\Models\ChiamataPostIt; use App\Models\ChiamataPostIt;
use App\Models\PbxClickToCallRequest; use App\Models\PbxClickToCallRequest;
@ -68,9 +69,17 @@ class TicketMobile extends Page
/** @var array<int, mixed> */ /** @var array<int, mixed> */
public array $newTicketCameraShots = []; public array $newTicketCameraShots = [];
/** @var array<int, mixed> */
public array $pendingTicketAttachments = [];
/** @var array<int, mixed> */
public array $pendingTicketCameraShots = [];
/** @var array<int, string> */ /** @var array<int, string> */
public array $newTicketAttachmentDescriptions = []; public array $newTicketAttachmentDescriptions = [];
public string $newTicketDraftProtocol = '';
public ?string $newTicketFotoNote = null; public ?string $newTicketFotoNote = null;
/** @var \Illuminate\Support\Collection<int, Ticket> */ /** @var \Illuminate\Support\Collection<int, Ticket> */
@ -122,6 +131,7 @@ public function mount(): void
$this->callerMatches = collect(); $this->callerMatches = collect();
$this->newTicketPriorita = 'Media'; $this->newTicketPriorita = 'Media';
$this->newTicketTipoIntervento = 'altro'; $this->newTicketTipoIntervento = 'altro';
$this->resetDraftUploadState();
$this->loadCategorieTicketOptions(); $this->loadCategorieTicketOptions();
if (request()->query('status') === 'all') { if (request()->query('status') === 'all') {
@ -160,7 +170,7 @@ public function refreshLiveCallBanner(): void
return; return;
} }
$this->liveIncomingCall = app(LiveIncomingCallService::class)->getForUser($authUser); $this->liveIncomingCall = app(LiveIncomingCallService::class)->getForUser($authUser);
$this->liveCallCanClickToCall = (bool) ($this->liveIncomingCall['can_click_to_call'] ?? false); $this->liveCallCanClickToCall = (bool) ($this->liveIncomingCall['can_click_to_call'] ?? false);
} }
@ -188,7 +198,7 @@ public function creaPostItDaChiamataInIngresso(): void
$line = (string) ($this->liveIncomingCall['line'] ?? ''); $line = (string) ($this->liveIncomingCall['line'] ?? '');
$this->prefillTicketFromLiveCall($phone, $line); $this->prefillTicketFromLiveCall($phone, $line);
ChiamataPostIt::query()->create([ $postIt = ChiamataPostIt::query()->create([
'stabile_id' => (int) $stabileId, 'stabile_id' => (int) $stabileId,
'creato_da_user_id' => (int) $user->id, 'creato_da_user_id' => (int) $user->id,
'rubrica_id' => (int) ($this->selectedCallerId ?? 0) ?: null, 'rubrica_id' => (int) ($this->selectedCallerId ?? 0) ?: null,
@ -208,6 +218,14 @@ public function creaPostItDaChiamataInIngresso(): void
->title('Post-it creato dalla chiamata in arrivo') ->title('Post-it creato dalla chiamata in arrivo')
->success() ->success()
->send(); ->send();
$this->redirect(
PostItGestione::getUrl([
'tab' => 'storico',
'focus_post_it' => (int) $postIt->id,
], panel: 'admin-filament'),
navigate: true,
);
} }
public function usaChiamataInIngresso(): void public function usaChiamataInIngresso(): void
@ -365,15 +383,15 @@ private function loadTickets(): void
return; return;
} }
$stabileId = StabileContext::resolveActiveStabileId($user); $stabileIds = $this->resolveTicketScopeStabileIds($this->status === 'all');
if (! $stabileId) { if ($stabileIds === []) {
$this->tickets = collect(); $this->tickets = collect();
return; return;
} }
$query = Ticket::query() $query = Ticket::query()
->with(['categoriaTicket']) ->with(['categoriaTicket'])
->where('stabile_id', $stabileId); ->whereIn('stabile_id', $stabileIds);
if ($this->status === 'open') { if ($this->status === 'open') {
$query->whereIn('stato', ['Aperto', 'Preso in Carico', 'In Lavorazione']); $query->whereIn('stato', ['Aperto', 'Preso in Carico', 'In Lavorazione']);
@ -395,13 +413,13 @@ private function loadCounters(): void
return; return;
} }
$stabileId = StabileContext::resolveActiveStabileId($user); $stabileIds = $this->resolveTicketScopeStabileIds($this->status === 'all');
if (! $stabileId) { if ($stabileIds === []) {
$this->ticketCounters = ['open' => 0, 'urgent' => 0, 'closed' => 0, 'all' => 0]; $this->ticketCounters = ['open' => 0, 'urgent' => 0, 'closed' => 0, 'all' => 0];
return; return;
} }
$base = Ticket::query()->where('stabile_id', $stabileId); $base = Ticket::query()->whereIn('stabile_id', $stabileIds);
$this->ticketCounters = [ $this->ticketCounters = [
'open' => (clone $base)->whereIn('stato', ['Aperto', 'Preso in Carico', 'In Lavorazione'])->count(), 'open' => (clone $base)->whereIn('stato', ['Aperto', 'Preso in Carico', 'In Lavorazione'])->count(),
@ -484,22 +502,22 @@ public function creaTicketRapido(): void
'newTicketAttachmentDescriptions' => ['nullable', 'array'], 'newTicketAttachmentDescriptions' => ['nullable', 'array'],
'newTicketAttachmentDescriptions.*' => ['nullable', 'string', 'max:255'], 'newTicketAttachmentDescriptions.*' => ['nullable', 'string', 'max:255'],
], [ ], [
'newTicketTitolo.required' => 'Inserisci il titolo del ticket.', 'newTicketTitolo.required' => 'Inserisci il titolo del ticket.',
'newTicketTitolo.max' => 'Il titolo puo contenere al massimo 255 caratteri.', 'newTicketTitolo.max' => 'Il titolo puo contenere al massimo 255 caratteri.',
'newTicketDescrizione.required' => 'Inserisci una descrizione del problema.', 'newTicketDescrizione.required' => 'Inserisci una descrizione del problema.',
'newTicketLuogo.max' => 'Il luogo intervento puo contenere al massimo 255 caratteri.', 'newTicketLuogo.max' => 'Il luogo intervento puo contenere al massimo 255 caratteri.',
'newTicketPriorita.in' => 'Seleziona una priorita valida.', 'newTicketPriorita.in' => 'Seleziona una priorita valida.',
'newTicketCategoriaId.exists' => 'La categoria selezionata non e valida.', 'newTicketCategoriaId.exists' => 'La categoria selezionata non e valida.',
'newTicketTipoIntervento.max' => 'Il tipo intervento selezionato non e valido.', 'newTicketTipoIntervento.max' => 'Il tipo intervento selezionato non e valido.',
'newTicketFotoNote.max' => 'La nota generale foto puo contenere al massimo 2000 caratteri.', 'newTicketFotoNote.max' => 'La nota generale foto puo contenere al massimo 2000 caratteri.',
'newTicketCameraShots.max' => 'Puoi caricare al massimo 10 foto per volta.', 'newTicketCameraShots.max' => 'Puoi caricare al massimo 10 foto per volta.',
'newTicketCameraShots.*.file' => 'Ogni foto deve essere un file valido.', 'newTicketCameraShots.*.file' => 'Ogni foto deve essere un file valido.',
'newTicketCameraShots.*.max' => 'Ogni foto deve pesare al massimo 10 MB.', 'newTicketCameraShots.*.max' => 'Ogni foto deve pesare al massimo 10 MB.',
'newTicketCameraShots.*.mimes' => 'Le foto devono essere JPG, PNG o WEBP.', 'newTicketCameraShots.*.mimes' => 'Le foto devono essere JPG, PNG o WEBP.',
'newTicketAttachments.max' => 'Puoi caricare al massimo 10 allegati per volta.', 'newTicketAttachments.max' => 'Puoi caricare al massimo 10 allegati per volta.',
'newTicketAttachments.*.file' => 'Ogni allegato deve essere un file valido.', 'newTicketAttachments.*.file' => 'Ogni allegato deve essere un file valido.',
'newTicketAttachments.*.max' => 'Ogni allegato deve pesare al massimo 10 MB.', 'newTicketAttachments.*.max' => 'Ogni allegato deve pesare al massimo 10 MB.',
'newTicketAttachments.*.mimes' => 'Gli allegati devono essere immagini, PDF o documenti Office/testo.', 'newTicketAttachments.*.mimes' => 'Gli allegati devono essere immagini, PDF o documenti Office/testo.',
'newTicketAttachmentDescriptions.*.max' => 'Ogni descrizione allegato puo contenere al massimo 255 caratteri.', 'newTicketAttachmentDescriptions.*.max' => 'Ogni descrizione allegato puo contenere al massimo 255 caratteri.',
], [ ], [
'newTicketTitolo' => 'titolo', 'newTicketTitolo' => 'titolo',
@ -595,27 +613,25 @@ public function creaTicketRapido(): void
->success() ->success()
->send(); ->send();
$this->newTicketTitolo = null; $this->newTicketTitolo = null;
$this->newTicketDescrizione = null; $this->newTicketDescrizione = null;
$this->newTicketLuogo = null; $this->newTicketLuogo = null;
$this->newTicketPriorita = 'Media'; $this->newTicketPriorita = 'Media';
$this->newTicketCategoriaId = null; $this->newTicketCategoriaId = null;
$this->newTicketTipoIntervento = 'altro'; $this->newTicketTipoIntervento = 'altro';
$this->newTicketCameraShots = []; $this->newTicketFotoNote = null;
$this->newTicketAttachments = []; $this->resetDraftUploadState();
$this->newTicketAttachmentDescriptions = [];
$this->newTicketFotoNote = null;
$this->refreshData(); $this->refreshData();
} }
public function updatedNewTicketCameraShots(): void public function updatedPendingTicketCameraShots(): void
{ {
$this->syncAttachmentDescriptionSlots(); $this->appendPendingUploads('camera');
} }
public function updatedNewTicketAttachments(): void public function updatedPendingTicketAttachments(): void
{ {
$this->syncAttachmentDescriptionSlots(); $this->appendPendingUploads('attachment');
} }
public function removeNewTicketFile(int $index): void public function removeNewTicketFile(int $index): void
@ -635,7 +651,7 @@ public function removeNewTicketFile(int $index): void
} }
/** /**
* @return array<int, array{index:int,name:string,mime:string,is_image:bool,preview_url:?string,description:string}> * @return array<int, array{index:int,name:string,original_name:string,protocol:string,mime:string,is_image:bool,preview_url:?string,description:string}>
*/ */
public function getSelectedUploadsProperty(): array public function getSelectedUploadsProperty(): array
{ {
@ -647,9 +663,10 @@ public function getSelectedUploadsProperty(): array
continue; continue;
} }
$name = (string) (method_exists($file, 'getClientOriginalName') ? $file->getClientOriginalName() : ('file-' . $index)); $name = (string) (method_exists($file, 'getClientOriginalName') ? $file->getClientOriginalName() : ('file-' . $index));
$mime = (string) (method_exists($file, 'getClientMimeType') ? $file->getClientMimeType() : 'application/octet-stream'); $mime = (string) (method_exists($file, 'getClientMimeType') ? $file->getClientMimeType() : 'application/octet-stream');
$isImage = str_starts_with($mime, 'image/'); $isImage = str_starts_with($mime, 'image/');
$protocol = $this->buildDraftProtocolCode((int) $index, $isImage);
$previewUrl = null; $previewUrl = null;
if ($isImage && method_exists($file, 'temporaryUrl')) { if ($isImage && method_exists($file, 'temporaryUrl')) {
@ -661,12 +678,14 @@ public function getSelectedUploadsProperty(): array
} }
$rows[] = [ $rows[] = [
'index' => (int) $index, 'index' => (int) $index,
'name' => $name, 'name' => $this->buildDraftUploadDisplayName((int) $index, $name, $isImage),
'mime' => $mime, 'original_name' => $name,
'is_image' => $isImage, 'protocol' => $protocol,
'preview_url' => $previewUrl, 'mime' => $mime,
'description' => (string) ($this->newTicketAttachmentDescriptions[$index] ?? ''), 'is_image' => $isImage,
'preview_url' => $previewUrl,
'description' => (string) ($this->newTicketAttachmentDescriptions[$index] ?? ''),
]; ];
} }
@ -702,7 +721,11 @@ private function salvaAllegatiTicket(Ticket $ticket, int $userId): int
} }
try { try {
$stored = app(TicketAttachmentUploadService::class)->store($file, 'ticket-mobile/' . $ticket->id); $displayName = $this->buildStoredUploadDisplayName($ticket, (int) $index, $file);
$stored = app(TicketAttachmentUploadService::class)->store($file, 'ticket-mobile/' . $ticket->id, [
'stored_basename' => pathinfo($displayName, PATHINFO_FILENAME),
'display_name' => $displayName,
]);
TicketAttachment::query()->create([ TicketAttachment::query()->create([
'ticket_id' => (int) $ticket->id, 'ticket_id' => (int) $ticket->id,
@ -712,7 +735,12 @@ private function salvaAllegatiTicket(Ticket $ticket, int $userId): int
'original_file_name' => $stored['original_name'], 'original_file_name' => $stored['original_name'],
'mime_type' => $stored['mime'], 'mime_type' => $stored['mime'],
'size' => $stored['size'], 'size' => $stored['size'],
'description' => $this->resolveAttachmentDescription((int) $index), 'description' => $this->resolveAttachmentDescription(
(int) $index,
is_array($stored['metadata'] ?? null) ? $stored['metadata'] : [],
$stored['original_name'] ?? null,
$stored['source_original_name'] ?? null,
),
]); ]);
$saved++; $saved++;
@ -739,18 +767,152 @@ private function syncAttachmentDescriptionSlots(): void
$this->newTicketAttachmentDescriptions = $next; $this->newTicketAttachmentDescriptions = $next;
} }
private function resolveAttachmentDescription(int $index): string private function resolveAttachmentDescription(int $index, array $metadata = [], ?string $displayName = null, ?string $sourceOriginalName = null): string
{ {
$parts = [];
$specific = trim((string) ($this->newTicketAttachmentDescriptions[$index] ?? '')); $specific = trim((string) ($this->newTicketAttachmentDescriptions[$index] ?? ''));
if ($specific !== '') { if ($specific !== '') {
return $specific; $parts[] = $specific;
} elseif (filled($this->newTicketFotoNote)) {
$parts[] = 'Nota foto: ' . trim((string) $this->newTicketFotoNote);
} else {
$parts[] = 'Allegato da Ticket Mobile';
} }
if (filled($this->newTicketFotoNote)) { if (filled($displayName)) {
return 'Nota foto: ' . trim((string) $this->newTicketFotoNote); $parts[] = 'File: ' . trim((string) $displayName);
} }
return 'Allegato da Ticket Mobile'; $sourceName = trim((string) $sourceOriginalName);
if ($sourceName !== '' && $sourceName !== trim((string) $displayName)) {
$parts[] = 'Orig: ' . $sourceName;
}
$exifDate = trim((string) ($metadata['exif_datetime'] ?? ''));
if ($exifDate !== '') {
$parts[] = 'EXIF: ' . $exifDate;
}
$device = trim((string) ($metadata['device'] ?? ''));
if ($device !== '') {
$parts[] = 'Device: ' . $device;
}
$lat = $metadata['gps_decimal']['lat'] ?? null;
$lng = $metadata['gps_decimal']['lng'] ?? null;
if (is_numeric($lat) && is_numeric($lng)) {
$parts[] = 'GPS: ' . number_format((float) $lat, 5, '.', '') . ',' . number_format((float) $lng, 5, '.', '');
$mapsUrl = trim((string) ($metadata['google_maps_url'] ?? ''));
if ($mapsUrl !== '') {
$parts[] = 'Maps: ' . $mapsUrl;
}
}
return Str::limit(implode(' | ', array_filter($parts)), 255);
}
private function appendPendingUploads(string $source): void
{
$pendingProperty = $source === 'camera' ? 'pendingTicketCameraShots' : 'pendingTicketAttachments';
$targetProperty = $source === 'camera' ? 'newTicketCameraShots' : 'newTicketAttachments';
$pending = array_values(array_filter($this->{$pendingProperty}, fn($file) => is_object($file)));
if ($pending === []) {
return;
}
$currentTotal = count($this->newTicketCameraShots) + count($this->newTicketAttachments);
$available = max(0, 10 - $currentTotal);
if ($available <= 0) {
$this->{$pendingProperty} = [];
Notification::make()
->title('Limite allegati raggiunto')
->body('Puoi tenere in coda al massimo 10 file totali per ticket.')
->warning()
->send();
return;
}
$accepted = array_slice($pending, 0, $available);
$this->{$targetProperty} = array_values(array_merge($this->{$targetProperty}, $accepted));
$this->{$pendingProperty} = [];
$this->syncAttachmentDescriptionSlots();
$discarded = count($pending) - count($accepted);
if ($discarded > 0) {
Notification::make()
->title('Coda allegati ridotta')
->body('Sono stati accodati solo i primi ' . count($accepted) . ' file disponibili: limite massimo 10.')
->warning()
->send();
}
}
private function resetDraftUploadState(): void
{
$this->newTicketCameraShots = [];
$this->newTicketAttachments = [];
$this->pendingTicketCameraShots = [];
$this->pendingTicketAttachments = [];
$this->newTicketAttachmentDescriptions = [];
$this->newTicketDraftProtocol = 'TM-' . now()->format('Ymd-His');
}
private function buildDraftProtocolCode(int $index, bool $isImage): string
{
return $this->newTicketDraftProtocol . '-' . ($isImage ? 'F' : 'A') . str_pad((string) ($index + 1), 2, '0', STR_PAD_LEFT);
}
private function buildDraftUploadDisplayName(int $index, string $originalName, bool $isImage): string
{
$extension = strtolower((string) pathinfo($originalName, PATHINFO_EXTENSION));
return $this->buildDraftProtocolCode($index, $isImage) . ($extension !== '' ? '.' . $extension : '');
}
private function buildStoredUploadDisplayName(Ticket $ticket, int $index, object $file): string
{
$originalName = (string) (method_exists($file, 'getClientOriginalName') ? $file->getClientOriginalName() : ('file-' . $index));
$extension = strtolower((string) pathinfo($originalName, PATHINFO_EXTENSION));
$mime = (string) (method_exists($file, 'getClientMimeType') ? $file->getClientMimeType() : 'application/octet-stream');
$isImage = str_starts_with($mime, 'image/');
$baseName = 'ticket-' . str_pad((string) $ticket->id, 6, '0', STR_PAD_LEFT) . '-' . ($isImage ? 'foto-' : 'all-') . str_pad((string) ($index + 1), 2, '0', STR_PAD_LEFT);
return $baseName . ($extension !== '' ? '.' . $extension : '');
}
/**
* @return array<int, int>
*/
private function resolveTicketScopeStabileIds(bool $includeAllAccessible = false): array
{
$user = Auth::user();
if (! $user instanceof User) {
return [];
}
$accessibleIds = StabileContext::accessibleStabili($user)
->pluck('id')
->map(fn($value) => (int) $value)
->filter(fn(int $value) => $value > 0)
->values()
->all();
if ($accessibleIds === []) {
return [];
}
if ($includeAllAccessible) {
return $accessibleIds;
}
$activeId = StabileContext::resolveActiveStabileId($user);
return $activeId ? [$activeId] : [(int) $accessibleIds[0]];
} }
private function prefillTicketFromLiveCall(string $phone, string $line): void private function prefillTicketFromLiveCall(string $phone, string $line): void
@ -822,14 +984,14 @@ private function aggiornaStatoTicket(int $ticketId, string $nuovoStato, bool $as
return; return;
} }
$stabileId = StabileContext::resolveActiveStabileId($user); $stabileIds = $this->resolveTicketScopeStabileIds(true);
if (! $stabileId) { if ($stabileIds === []) {
return; return;
} }
$ticket = Ticket::query() $ticket = Ticket::query()
->where('id', $ticketId) ->where('id', $ticketId)
->where('stabile_id', $stabileId) ->whereIn('stabile_id', $stabileIds)
->first(); ->first();
if (! $ticket) { if (! $ticket) {

View File

@ -1,7 +1,7 @@
<?php <?php
namespace App\Livewire\Filament; namespace App\Livewire\Filament;
use App\Filament\Pages\Strumenti\PostItGestione;
use App\Models\ChiamataPostIt; use App\Models\ChiamataPostIt;
use App\Models\User; use App\Models\User;
use App\Services\Cti\LiveIncomingCallService; use App\Services\Cti\LiveIncomingCallService;
@ -44,7 +44,7 @@ public function createPostIt(): void
return; return;
} }
ChiamataPostIt::query()->create([ $postIt = ChiamataPostIt::query()->create([
'stabile_id' => (int) $stabileId, 'stabile_id' => (int) $stabileId,
'creato_da_user_id' => (int) $user->id, 'creato_da_user_id' => (int) $user->id,
'rubrica_id' => (int) ($this->liveIncomingCall['rubrica_id'] ?? 0) ?: null, 'rubrica_id' => (int) ($this->liveIncomingCall['rubrica_id'] ?? 0) ?: null,
@ -61,6 +61,14 @@ public function createPostIt(): void
]); ]);
Notification::make()->title('Post-it creato dalla testata')->success()->send(); Notification::make()->title('Post-it creato dalla testata')->success()->send();
$this->redirect(
PostItGestione::getUrl([
'tab' => 'storico',
'focus_post_it' => (int) $postIt->id,
], panel: 'admin-filament'),
navigate: true,
);
} }
public function openTicketMobile() public function openTicketMobile()
@ -76,4 +84,4 @@ public function render()
{ {
return view('livewire.filament.topbar-live-call'); return view('livewire.filament.topbar-live-call');
} }
} }

View File

@ -15,9 +15,12 @@
<button type="button" wire:click="$set('activeTab', 'storico')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $activeTab === 'storico' ? 'bg-indigo-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}"> <button type="button" wire:click="$set('activeTab', 'storico')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $activeTab === 'storico' ? 'bg-indigo-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">
Storico Post-it Storico Post-it
</button> </button>
<button type="button" wire:click="$set('activeTab', 'tecnico')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $activeTab === 'tecnico' ? 'bg-indigo-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}"> <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 Chiamate Tecniche SMDR
</button> </button>
<button type="button" wire:click="$set('activeTab', 'csta')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $activeTab === 'csta' ? 'bg-indigo-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">
CTI / CSTA e Click-to-Call
</button>
</div> </div>
</div> </div>
@ -32,10 +35,16 @@
<div class="space-y-3"> <div class="space-y-3">
@forelse($this->recenti as $riga) @forelse($this->recenti as $riga)
<div class="rounded-lg border p-3"> <div @class([
'rounded-lg border p-3',
'border-emerald-400 bg-emerald-50/60 ring-1 ring-emerald-200' => (int) ($this->focusPostItId ?? 0) === (int) $riga->id,
])>
<div class="flex flex-wrap items-center justify-between gap-2"> <div class="flex flex-wrap items-center justify-between gap-2">
<div> <div>
<div class="font-medium">{{ $riga->oggetto ?: 'Senza oggetto' }}</div> <div class="font-medium">{{ $riga->oggetto ?: 'Senza oggetto' }}</div>
@if((int) ($this->focusPostItId ?? 0) === (int) $riga->id)
<div class="mt-1 text-[11px] font-semibold uppercase tracking-wide text-emerald-700">Post-it appena creato</div>
@endif
<div class="text-sm text-gray-600"> <div class="text-sm text-gray-600">
{{ $riga->nome_chiamante ?: 'Chiamante non identificato' }} {{ $riga->nome_chiamante ?: 'Chiamante non identificato' }}
@if($riga->telefono) @if($riga->telefono)
@ -93,10 +102,59 @@
@endforelse @endforelse
</div> </div>
</div> </div>
@else @elseif(in_array($activeTab, ['smdr', 'csta'], true))
<div class="rounded-xl border bg-white p-4"> <div class="rounded-xl border bg-white p-4">
<h2 class="mb-1 text-base font-semibold">Elenco tecnico chiamate (1 riga = 1 evento)</h2> @php($isCstaTab = $activeTab === 'csta')
<p class="mb-3 text-xs text-gray-500">Qui vedi in colonne i dati disponibili dal centralino: interno, linea/trunk, numero, durata, costo, utente assegnato, stabile, raw line, ecc.</p> <h2 class="mb-1 text-base font-semibold">{{ $isCstaTab ? 'Elenco tecnico CTI / CSTA' : 'Elenco tecnico chiamate SMDR' }}</h2>
<p class="mb-3 text-xs text-gray-500">
{{ $isCstaTab
? 'Vista test PBX per eventi CSTA, provider, interno coinvolto e richieste click-to-call. Il provider viene letto dai metadati così il cruscotto resta riusabile anche con FreePBX o altri centralini.'
: 'Qui vedi in colonne i dati disponibili dal centralino via SMDR: interno, linea/trunk, numero, durata, costo, utente assegnato, stabile e raw line.' }}
</p>
@if($isCstaTab)
<div class="mb-4 rounded-lg border border-blue-200 bg-blue-50 p-3 text-xs text-blue-900">
<div class="font-semibold">Controllo chiamate PBX</div>
<div class="mt-1">Da questo tab puoi verificare gli eventi CSTA e registrare richieste click-to-call verso il PBX dell'interno assegnato. La UI non e vincolata a Panasonic: il provider e esposto come metadato.</div>
</div>
<div class="mb-4 rounded-lg border border-slate-200 bg-slate-50 p-3">
<div class="mb-2 flex items-center justify-between gap-2">
<div class="text-xs font-semibold uppercase tracking-wide text-slate-600">Ultime richieste click-to-call</div>
<div class="text-[11px] text-slate-500">Fonte: coda locale PBX</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full border text-[11px]">
<thead class="bg-white">
<tr>
<th class="border px-2 py-1 text-left">Richiesta</th>
<th class="border px-2 py-1 text-left">Interno</th>
<th class="border px-2 py-1 text-left">Numero</th>
<th class="border px-2 py-1 text-left">Stato</th>
<th class="border px-2 py-1 text-left">Provider</th>
<th class="border px-2 py-1 text-left">Quando</th>
</tr>
</thead>
<tbody>
@forelse($this->recentClickToCallRequests as $req)
<tr>
<td class="border px-2 py-1">#{{ $req->id }}</td>
<td class="border px-2 py-1">{{ $req->source_extension ?: '-' }}</td>
<td class="border px-2 py-1">{{ $req->target_number ?: '-' }}</td>
<td class="border px-2 py-1">{{ $req->status ?: '-' }}</td>
<td class="border px-2 py-1">{{ strtoupper(str_replace('_', ' ', (string) data_get($req->metadata, 'provider', data_get($req->metadata, 'source_channel', '-')))) }}</td>
<td class="border px-2 py-1">{{ optional($req->requested_at)->format('d/m/Y H:i:s') ?: optional($req->created_at)->format('d/m/Y H:i:s') }}</td>
</tr>
@empty
<tr>
<td colspan="6" class="border px-2 py-3 text-center text-slate-500">Nessuna richiesta click-to-call registrata per il contesto utente corrente.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
@endif
<div class="mb-4 grid grid-cols-1 gap-3 rounded-lg border border-slate-200 bg-slate-50 p-3 md:grid-cols-4"> <div class="mb-4 grid grid-cols-1 gap-3 rounded-lg border border-slate-200 bg-slate-50 p-3 md:grid-cols-4">
<div> <div>
@ -137,10 +195,11 @@
<tr> <tr>
<th class="border px-2 py-1 text-left">ID</th> <th class="border px-2 py-1 text-left">ID</th>
<th class="border px-2 py-1 text-left">Data/Ora</th> <th class="border px-2 py-1 text-left">Data/Ora</th>
<th class="border px-2 py-1 text-left">Provider</th>
<th class="border px-2 py-1 text-left">Direzione</th> <th class="border px-2 py-1 text-left">Direzione</th>
<th class="border px-2 py-1 text-left">Ambito</th> <th class="border px-2 py-1 text-left">Ambito</th>
<th class="border px-2 py-1 text-left">Interno</th> <th class="border px-2 py-1 text-left">Interno</th>
<th class="border px-2 py-1 text-left">Linea/CO</th> <th class="border px-2 py-1 text-left">Linea / Evento</th>
<th class="border px-2 py-1 text-left">Numero</th> <th class="border px-2 py-1 text-left">Numero</th>
<th class="border px-2 py-1 text-left">Nominativo</th> <th class="border px-2 py-1 text-left">Nominativo</th>
<th class="border px-2 py-1 text-left">Durata</th> <th class="border px-2 py-1 text-left">Durata</th>
@ -162,10 +221,11 @@
<tr> <tr>
<td class="border px-2 py-1">{{ $m->id }}</td> <td class="border px-2 py-1">{{ $m->id }}</td>
<td class="border px-2 py-1">{{ optional($m->received_at)->format('d/m/Y H:i:s') }}</td> <td class="border px-2 py-1">{{ optional($m->received_at)->format('d/m/Y H:i:s') }}</td>
<td class="border px-2 py-1">{{ $this->getTecnicoProviderLabel($m) }}</td>
<td class="border px-2 py-1">{{ $m->direction }}</td> <td class="border px-2 py-1">{{ $m->direction }}</td>
<td class="border px-2 py-1">{{ $this->getTecnicoScopeLabel($m) }}</td> <td class="border px-2 py-1">{{ $this->getTecnicoScopeLabel($m) }}</td>
<td class="border px-2 py-1">{{ $this->getTecnicoInternoLabel($m) }}</td> <td class="border px-2 py-1">{{ $this->getTecnicoInternoLabel($m) }}</td>
<td class="border px-2 py-1">{{ (string) ($smdr['co'] ?? '-') }}</td> <td class="border px-2 py-1">{{ $this->getTecnicoLineaLabel($m) }}</td>
<td class="border px-2 py-1">{{ (string) ($m->phone_number ?: ($smdr['dial_number'] ?? '-')) }}</td> <td class="border px-2 py-1">{{ (string) ($m->phone_number ?: ($smdr['dial_number'] ?? '-')) }}</td>
<td class="border px-2 py-1">{{ $rubricaNomeTech ?: '-' }}</td> <td class="border px-2 py-1">{{ $rubricaNomeTech ?: '-' }}</td>
<td class="border px-2 py-1">{{ $duration !== '' ? $duration : '-' }}</td> <td class="border px-2 py-1">{{ $duration !== '' ? $duration : '-' }}</td>
@ -179,6 +239,9 @@
@if($rubricaUrlTech && ! $this->isInternalSmdrMessage($m)) @if($rubricaUrlTech && ! $this->isInternalSmdrMessage($m))
<a href="{{ $rubricaUrlTech }}" class="inline-flex items-center rounded-md border px-2 py-1 text-[11px] text-indigo-700 hover:bg-indigo-50">Apri Rubrica</a> <a href="{{ $rubricaUrlTech }}" class="inline-flex items-center rounded-md border px-2 py-1 text-[11px] text-indigo-700 hover:bg-indigo-50">Apri Rubrica</a>
@endif @endif
@if($activeTab === 'csta')
<x-filament::button size="xs" color="info" wire:click="richiediClickToCallDaMessaggio({{ (int) $m->id }})">Click-to-call</x-filament::button>
@endif
@if(!$m->post_it_id) @if(!$m->post_it_id)
<x-filament::button size="xs" wire:click="creaPostItDaMessaggio({{ (int) $m->id }})">Crea Post-it</x-filament::button> <x-filament::button size="xs" wire:click="creaPostItDaMessaggio({{ (int) $m->id }})">Crea Post-it</x-filament::button>
@else @else
@ -188,7 +251,7 @@
</tr> </tr>
@empty @empty
<tr> <tr>
<td colspan="15" class="border px-2 py-3 text-center text-gray-500">Nessun evento SMDR disponibile.</td> <td colspan="16" class="border px-2 py-3 text-center text-gray-500">{{ $isCstaTab ? 'Nessun evento CSTA disponibile.' : 'Nessun evento SMDR disponibile.' }}</td>
</tr> </tr>
@endforelse @endforelse
</tbody> </tbody>