netgescon-day0/app/Filament/Pages/Supporto/TicketMobile.php

894 lines
32 KiB
PHP

<?php
namespace App\Filament\Pages\Supporto;
use App\Filament\Pages\Contabilita\EstrattoContoSoggetto;
use App\Filament\Pages\Gescon\RubricaUniversaleScheda;
use App\Models\CategoriaTicket;
use App\Models\ChiamataPostIt;
use App\Models\CommunicationMessage;
use App\Models\PbxClickToCallRequest;
use App\Models\RubricaUniversale;
use App\Models\Soggetto;
use App\Models\Stabile;
use App\Models\Ticket;
use App\Models\TicketAttachment;
use App\Models\User;
use App\Support\StabileContext;
use BackedEnum;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Livewire\WithFileUploads;
use Throwable;
use UnitEnum;
class TicketMobile extends Page
{
use WithFileUploads;
protected static ?string $navigationLabel = 'Ticket Mobile';
protected static ?string $title = 'Ticket Mobile';
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-device-phone-mobile';
protected static UnitEnum|string|null $navigationGroup = 'Supporto';
protected static ?int $navigationSort = 25;
protected static ?string $slug = 'supporto/ticket-mobile';
protected string $view = 'filament.pages.supporto.ticket-mobile';
public string $status = 'open';
public string $callerSearch = '';
public ?int $selectedCallerId = null;
public ?string $newTicketTitolo = null;
public ?string $newTicketDescrizione = null;
public ?string $newTicketLuogo = null;
public string $newTicketPriorita = 'Media';
public ?int $newTicketCategoriaId = null;
public ?string $newTicketTipoIntervento = null;
/** @var array<int, mixed> */
public array $newTicketAttachments = [];
/** @var array<int, mixed> */
public array $newTicketCameraShots = [];
/** @var array<int, string> */
public array $newTicketAttachmentDescriptions = [];
public ?string $newTicketFotoNote = null;
/** @var \Illuminate\Support\Collection<int, Ticket> */
public $tickets;
/** @var \Illuminate\Support\Collection<int, RubricaUniversale> */
public $callerMatches;
/** @var array<string,mixed>|null */
public ?array $liveIncomingCall = null;
public bool $liveCallCanClickToCall = false;
/** @var array<string,int> */
public array $ticketCounters = [
'open' => 0,
'urgent' => 0,
'closed' => 0,
'all' => 0,
];
/** @var array<int, array{id:int,nome:string}> */
public array $categorieTicketOptions = [];
/** @var array<string,string> */
public array $interventiPreimpostati = [
'idraulico_perdita' => 'Idraulico - Perdita acqua',
'elettrico_luci' => 'Elettrico - Luci/Quadro',
'ascensore_bloccato' => 'Ascensore - Bloccato',
'citofono_guasto' => 'Citofono - Guasto',
'pulizia_straordinaria' => 'Pulizia - Straordinaria',
'infissi_serrature' => 'Infissi/Serrature',
'fognatura' => 'Fognatura/Odori',
'verde_potatura' => 'Verde - Potatura',
'altro' => 'Altro',
];
public static function canAccess(): bool
{
$user = Auth::user();
return $user instanceof User
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
}
public function mount(): void
{
$this->tickets = collect();
$this->callerMatches = collect();
$this->newTicketPriorita = 'Media';
$this->newTicketTipoIntervento = 'altro';
$this->loadCategorieTicketOptions();
if (request()->query('status') === 'all') {
$this->status = 'all';
}
$this->refreshLiveCallBanner();
$this->refreshData();
}
public function updatedStatus(): void
{
$this->refreshData();
}
public function updatedCallerSearch(): void
{
$this->searchCaller();
}
public function refreshData(): void
{
$this->loadCounters();
$this->loadTickets();
$this->searchCaller();
$this->refreshLiveCallBanner();
}
public function refreshLiveCallBanner(): void
{
$authUser = Auth::user();
if (! $authUser instanceof User) {
$this->liveIncomingCall = null;
$this->liveCallCanClickToCall = false;
return;
}
$userExtension = trim((string) ($authUser->pbx_extension ?? ''));
$popupRestricted = (bool) ($authUser->pbx_popup_enabled ?? false) && $userExtension !== '';
$latest = CommunicationMessage::query()
->where('channel', 'smdr')
->where('direction', 'inbound')
->whereNotNull('received_at')
->where('received_at', '>=', now()->subMinutes(8))
->when($popupRestricted, function ($q) use ($userExtension): void {
$q->where('target_extension', $userExtension);
})
->latest('id')
->first(['id', 'phone_number', 'target_extension', 'received_at', 'message_text', 'metadata']);
if (! $latest) {
$this->liveIncomingCall = null;
$this->liveCallCanClickToCall = false;
return;
}
$phone = $this->normalizePhoneDigits((string) ($latest->phone_number ?? ''));
if ($phone === '') {
$phone = $this->normalizePhoneDigits((string) data_get($latest->metadata, 'smdr.dial_number', ''));
}
if ($phone === '') {
$this->liveIncomingCall = null;
$this->liveCallCanClickToCall = false;
return;
}
$rubrica = $this->matchRubricaByPhone($phone);
$soggetto = $this->matchSoggetto($rubrica, $phone);
$this->liveIncomingCall = [
'message_id' => (int) $latest->id,
'phone' => $phone,
'received_at' => optional($latest->received_at)->format('d/m/Y H:i:s'),
'line' => (string) ($latest->message_text ?? ''),
'target_extension' => (string) ($latest->target_extension ?? ''),
'rubrica_id' => (int) ($rubrica?->id ?? 0),
'rubrica_nome' => (string) ($rubrica?->nome_completo ?: $rubrica?->ragione_sociale ?: ''),
'soggetto_id' => (int) ($soggetto?->id ?? 0),
];
$this->liveCallCanClickToCall = (bool) ($authUser->pbx_click_to_call_enabled ?? false)
&& $userExtension !== '';
}
public function creaPostItDaChiamataInIngresso(): void
{
if (! $this->liveIncomingCall || empty($this->liveIncomingCall['phone'])) {
return;
}
$user = Auth::user();
if (! $user instanceof User) {
return;
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId) {
Notification::make()
->title('Seleziona prima uno stabile attivo')
->warning()
->send();
return;
}
$phone = (string) $this->liveIncomingCall['phone'];
$line = (string) ($this->liveIncomingCall['line'] ?? '');
$this->prefillTicketFromLiveCall($phone, $line);
ChiamataPostIt::query()->create([
'stabile_id' => (int) $stabileId,
'creato_da_user_id' => (int) $user->id,
'rubrica_id' => (int) ($this->selectedCallerId ?? 0) ?: null,
'telefono' => $phone,
'nome_chiamante' => (string) (($this->liveIncomingCall['rubrica_nome'] ?? '') ?: ('Chiamata ' . $phone)),
'direzione' => 'in_arrivo',
'origine' => 'smdr_live_banner',
'origine_id' => (string) ($this->liveIncomingCall['message_id'] ?? ''),
'oggetto' => 'Richiesta telefonica da valutare',
'nota' => $line !== '' ? $line : ('Chiamata in ingresso da ' . $phone),
'priorita' => 'Media',
'stato' => 'post_it',
'chiamata_il' => now(),
]);
Notification::make()
->title('Post-it creato dalla chiamata in arrivo')
->success()
->send();
}
public function usaChiamataInIngresso(): void
{
if (! $this->liveIncomingCall || empty($this->liveIncomingCall['phone'])) {
return;
}
$phone = (string) $this->liveIncomingCall['phone'];
$line = (string) ($this->liveIncomingCall['line'] ?? '');
$this->prefillTicketFromLiveCall($phone, $line);
Notification::make()
->title('Ticket precompilato dalla chiamata in arrivo')
->success()
->send();
}
public function richiediClickToCallDaInterno(): void
{
if (! $this->liveIncomingCall || empty($this->liveIncomingCall['phone'])) {
return;
}
$user = Auth::user();
if (! $user instanceof User) {
return;
}
$extension = trim((string) ($user->pbx_extension ?? ''));
if (! (bool) ($user->pbx_click_to_call_enabled ?? false) || $extension === '') {
Notification::make()->title('Click-to-call non abilitato per il tuo utente')->warning()->send();
return;
}
$phone = $this->normalizePhoneDigits((string) $this->liveIncomingCall['phone']);
if ($phone === '') {
Notification::make()->title('Numero non valido')->warning()->send();
return;
}
$stabileId = StabileContext::resolveActiveStabileId($user);
PbxClickToCallRequest::query()->create([
'requested_by_user_id' => (int) $user->id,
'assigned_user_id' => (int) $user->id,
'stabile_id' => $stabileId,
'communication_message_id' => (int) ($this->liveIncomingCall['message_id'] ?? 0) ?: null,
'source_extension' => $extension,
'target_number' => $phone,
'status' => 'pending',
'note' => 'Richiesta da banner chiamata in arrivo',
'requested_at' => now(),
'metadata' => [
'from_live_banner' => true,
'target_extension' => (string) ($this->liveIncomingCall['target_extension'] ?? ''),
],
]);
Notification::make()
->title('Richiesta click-to-call registrata')
->body('Interno ' . $extension . ' -> ' . $phone)
->success()
->send();
}
public function getLiveRubricaUrl(): ?string
{
$rubricaId = (int) ($this->liveIncomingCall['rubrica_id'] ?? 0);
if ($rubricaId <= 0) {
return null;
}
return RubricaUniversaleScheda::getUrl(['record' => $rubricaId], panel: 'admin-filament');
}
public function getLiveEstrattoContoUrl(): ?string
{
$soggettoId = (int) ($this->liveIncomingCall['soggetto_id'] ?? 0);
if ($soggettoId <= 0) {
return null;
}
return EstrattoContoSoggetto::getUrl(['record' => $soggettoId], panel: 'admin-filament');
}
public function getRubricaFilamentUrl(): string
{
return route('filament.admin-filament.pages.gescon.anagrafica.rubrica-universale');
}
public function getDashboardFilamentUrl(): string
{
return route('filament.admin-filament.pages.dashboard');
}
public function getTicketArchivioUrl(): string
{
return TicketGestione::getUrl(panel: 'admin-filament', parameters: ['status' => 'all']);
}
public function getHelpUrl(): string
{
return TicketMobileHelp::getUrl(panel: 'admin-filament');
}
public function getTicketDettaglioUrl(int $ticketId): string
{
return TicketScheda::getUrl(panel: 'admin-filament') . '?ticket=' . $ticketId;
}
public function prendiInCarico(int $ticketId): void
{
$this->aggiornaStatoTicket($ticketId, 'Preso in Carico', true);
}
public function avviaLavorazione(int $ticketId): void
{
$this->aggiornaStatoTicket($ticketId, 'In Lavorazione');
}
public function risolviTicket(int $ticketId): void
{
$this->aggiornaStatoTicket($ticketId, 'Risolto');
}
public function chiudiTicket(int $ticketId): void
{
$this->aggiornaStatoTicket($ticketId, 'Chiuso');
}
private function loadTickets(): void
{
$user = Auth::user();
if (! $user instanceof User) {
$this->tickets = collect();
return;
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId) {
$this->tickets = collect();
return;
}
$query = Ticket::query()
->with(['categoriaTicket'])
->where('stabile_id', $stabileId);
if ($this->status === 'open') {
$query->whereIn('stato', ['Aperto', 'Preso in Carico', 'In Lavorazione']);
} elseif ($this->status === 'urgent') {
$query->where('priorita', 'Urgente')
->whereIn('stato', ['Aperto', 'Preso in Carico', 'In Lavorazione']);
} elseif ($this->status === 'closed') {
$query->whereIn('stato', ['Risolto', 'Chiuso']);
}
$this->tickets = $query->latest('data_apertura')->limit(30)->get();
}
private function loadCounters(): void
{
$user = Auth::user();
if (! $user instanceof User) {
$this->ticketCounters = ['open' => 0, 'urgent' => 0, 'closed' => 0, 'all' => 0];
return;
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId) {
$this->ticketCounters = ['open' => 0, 'urgent' => 0, 'closed' => 0, 'all' => 0];
return;
}
$base = Ticket::query()->where('stabile_id', $stabileId);
$this->ticketCounters = [
'open' => (clone $base)->whereIn('stato', ['Aperto', 'Preso in Carico', 'In Lavorazione'])->count(),
'urgent' => (clone $base)->where('priorita', 'Urgente')->whereIn('stato', ['Aperto', 'Preso in Carico', 'In Lavorazione'])->count(),
'closed' => (clone $base)->whereIn('stato', ['Risolto', 'Chiuso'])->count(),
'all' => (clone $base)->count(),
];
}
private function searchCaller(): void
{
$raw = trim($this->callerSearch);
if ($raw === '') {
$this->callerMatches = collect();
$this->selectedCallerId = null;
return;
}
$digits = preg_replace('/\D+/', '', $raw) ?: '';
$needleText = '%' . mb_strtolower($raw) . '%';
$needle = '%' . $digits . '%';
$this->callerMatches = RubricaUniversale::query()
->where(function ($q) use ($needle, $needleText, $digits): void {
$q->orWhereRaw("LOWER(COALESCE(nome, '')) LIKE ?", [$needleText])
->orWhereRaw("LOWER(COALESCE(cognome, '')) LIKE ?", [$needleText])
->orWhereRaw("LOWER(COALESCE(ragione_sociale, '')) LIKE ?", [$needleText])
->orWhereRaw("LOWER(COALESCE(email, '')) LIKE ?", [$needleText]);
if ($digits !== '') {
$q->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_cellulare, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$needle])
->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_ufficio, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$needle])
->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_casa, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$needle]);
}
})
->orderByRaw('CASE WHEN id = ? THEN 0 ELSE 1 END', [(int) ($this->selectedCallerId ?? 0)])
->orderBy('nome')
->orderBy('cognome')
->limit(12)
->get();
if ($this->selectedCallerId && ! $this->callerMatches->contains('id', $this->selectedCallerId)) {
$this->selectedCallerId = null;
}
}
public function selezionaCaller(int $rubricaId): void
{
$match = $this->callerMatches->firstWhere('id', $rubricaId);
if (! $match) {
$match = RubricaUniversale::query()->find($rubricaId);
}
if (! $match) {
return;
}
$this->selectedCallerId = (int) $match->id;
$this->callerSearch = trim((string) ($match->nome_completo ?: $match->ragione_sociale ?: ''));
if (blank($this->newTicketTitolo)) {
$this->newTicketTitolo = 'Segnalazione da ' . ($match->nome_completo ?: $match->ragione_sociale ?: 'contatto');
}
$this->searchCaller();
}
public function creaTicketRapido(): void
{
$this->validate([
'newTicketTitolo' => ['required', 'string', 'max:255'],
'newTicketDescrizione' => ['required', 'string'],
'newTicketLuogo' => ['nullable', 'string', 'max:255'],
'newTicketPriorita' => ['required', 'in:Bassa,Media,Alta,Urgente'],
'newTicketCategoriaId' => ['nullable', 'integer', 'exists:categorie_ticket,id'],
'newTicketTipoIntervento' => ['nullable', 'string', 'max:80'],
'newTicketFotoNote' => ['nullable', 'string', 'max:2000'],
'newTicketCameraShots' => ['nullable', 'array', 'max:10'],
'newTicketCameraShots.*' => ['file', 'max:10240', 'mimes:jpg,jpeg,png,webp'],
'newTicketAttachments' => ['nullable', 'array', 'max:10'],
'newTicketAttachments.*' => ['file', 'max:10240', 'mimes:jpg,jpeg,png,webp,pdf,doc,docx,xls,xlsx,txt'],
'newTicketAttachmentDescriptions' => ['nullable', 'array'],
'newTicketAttachmentDescriptions.*' => ['nullable', 'string', 'max:255'],
]);
$totalUploads = count($this->newTicketCameraShots) + count($this->newTicketAttachments);
if ($totalUploads > 10) {
Notification::make()
->title('Troppi allegati selezionati')
->body('Puoi caricare al massimo 10 file totali (foto + documenti) per ticket.')
->warning()
->send();
return;
}
$user = Auth::user();
if (! $user instanceof User) {
return;
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId) {
Notification::make()
->title('Seleziona prima uno stabile attivo')
->danger()
->send();
return;
}
$caller = null;
if ($this->selectedCallerId) {
$caller = RubricaUniversale::query()->find($this->selectedCallerId);
}
$descrizione = trim((string) $this->newTicketDescrizione);
if ($caller) {
$telefono = $caller->telefono_cellulare ?: ($caller->telefono_ufficio ?: $caller->telefono_casa);
$descrizione .= "\n\nChiamante selezionato: " . ($caller->nome_completo ?: $caller->ragione_sociale ?: ('Rubrica #' . $caller->id));
if ($telefono) {
$descrizione .= "\nTelefono: {$telefono}";
}
if ($caller->tipo_utenza_call) {
$descrizione .= "\nProfilo: {$caller->tipo_utenza_call}";
}
if ($caller->riferimento_stabile) {
$descrizione .= "\nRif. stabile: {$caller->riferimento_stabile}";
}
if ($caller->riferimento_unita) {
$descrizione .= "\nRif. unità: {$caller->riferimento_unita}";
}
}
if (filled($this->newTicketFotoNote)) {
$descrizione .= "\n\nNota fotografica: " . trim((string) $this->newTicketFotoNote);
}
if (filled($this->newTicketTipoIntervento)) {
$label = $this->interventiPreimpostati[(string) $this->newTicketTipoIntervento] ?? (string) $this->newTicketTipoIntervento;
$descrizione .= "\nTipo intervento: " . $label;
}
$ticket = Ticket::query()->create([
'stabile_id' => $stabileId,
'aperto_da_user_id' => $user->id,
'titolo' => (string) $this->newTicketTitolo,
'descrizione' => $descrizione,
'categoria_ticket_id' => $this->newTicketCategoriaId,
'luogo_intervento' => $this->newTicketLuogo,
'data_apertura' => now(),
'stato' => 'Aperto',
'priorita' => $this->newTicketPriorita,
]);
$savedAttachments = $this->salvaAllegatiTicket($ticket, $user->id);
Notification::make()
->title('Ticket creato')
->body(
$savedAttachments > 0
? 'Ticket #' . $ticket->id . ' creato con successo con ' . $savedAttachments . ' allegati.'
: 'Ticket #' . $ticket->id . ' creato con successo'
)
->success()
->send();
$this->newTicketTitolo = null;
$this->newTicketDescrizione = null;
$this->newTicketLuogo = null;
$this->newTicketPriorita = 'Media';
$this->newTicketCategoriaId = null;
$this->newTicketTipoIntervento = 'altro';
$this->newTicketCameraShots = [];
$this->newTicketAttachments = [];
$this->newTicketAttachmentDescriptions = [];
$this->newTicketFotoNote = null;
$this->refreshData();
}
public function updatedNewTicketCameraShots(): void
{
$this->syncAttachmentDescriptionSlots();
}
public function updatedNewTicketAttachments(): void
{
$this->syncAttachmentDescriptionSlots();
}
public function removeNewTicketFile(int $index): void
{
$cameraCount = count($this->newTicketCameraShots);
if ($index < $cameraCount) {
unset($this->newTicketCameraShots[$index]);
$this->newTicketCameraShots = array_values($this->newTicketCameraShots);
} else {
$docIndex = $index - $cameraCount;
unset($this->newTicketAttachments[$docIndex]);
$this->newTicketAttachments = array_values($this->newTicketAttachments);
}
$this->syncAttachmentDescriptionSlots();
}
/**
* @return array<int, array{index:int,name:string,mime:string,is_image:bool,preview_url:?string,description:string}>
*/
public function getSelectedUploadsProperty(): array
{
$rows = [];
$files = array_merge($this->newTicketCameraShots, $this->newTicketAttachments);
foreach ($files as $index => $file) {
if (! is_object($file)) {
continue;
}
$name = (string) (method_exists($file, 'getClientOriginalName') ? $file->getClientOriginalName() : ('file-' . $index));
$mime = (string) (method_exists($file, 'getClientMimeType') ? $file->getClientMimeType() : 'application/octet-stream');
$isImage = str_starts_with($mime, 'image/');
$previewUrl = null;
if ($isImage && method_exists($file, 'temporaryUrl')) {
try {
$previewUrl = (string) $file->temporaryUrl();
} catch (Throwable) {
$previewUrl = null;
}
}
$rows[] = [
'index' => (int) $index,
'name' => $name,
'mime' => $mime,
'is_image' => $isImage,
'preview_url' => $previewUrl,
'description' => (string) ($this->newTicketAttachmentDescriptions[$index] ?? ''),
];
}
return $rows;
}
private function loadCategorieTicketOptions(): void
{
$this->categorieTicketOptions = CategoriaTicket::query()
->orderBy('nome')
->limit(200)
->get(['id', 'nome'])
->map(fn(CategoriaTicket $cat): array=> [
'id' => (int) $cat->id,
'nome' => (string) $cat->nome,
])
->all();
}
private function salvaAllegatiTicket(Ticket $ticket, int $userId): int
{
if (! Schema::hasTable('ticket_attachments')) {
return 0;
}
$saved = 0;
$files = array_merge($this->newTicketCameraShots, $this->newTicketAttachments);
foreach ($files as $index => $file) {
if (! is_object($file) || ! method_exists($file, 'store')) {
continue;
}
try {
$path = $file->store('ticket-mobile/' . $ticket->id, 'public');
TicketAttachment::query()->create([
'ticket_id' => (int) $ticket->id,
'ticket_update_id' => null,
'user_id' => $userId,
'file_path' => $path,
'original_file_name' => (string) (method_exists($file, 'getClientOriginalName') ? $file->getClientOriginalName() : basename($path)),
'mime_type' => (string) (method_exists($file, 'getClientMimeType') ? $file->getClientMimeType() : 'application/octet-stream'),
'size' => (int) (method_exists($file, 'getSize') ? $file->getSize() : 0),
'description' => $this->resolveAttachmentDescription((int) $index),
]);
$saved++;
} catch (Throwable $e) {
Log::warning('TicketMobile attachment save failed', [
'ticket_id' => $ticket->id,
'error' => $e->getMessage(),
]);
}
}
return $saved;
}
private function syncAttachmentDescriptionSlots(): void
{
$files = array_merge($this->newTicketCameraShots, $this->newTicketAttachments);
$next = [];
foreach ($files as $index => $_file) {
$next[$index] = (string) ($this->newTicketAttachmentDescriptions[$index] ?? '');
}
$this->newTicketAttachmentDescriptions = $next;
}
private function resolveAttachmentDescription(int $index): string
{
$specific = trim((string) ($this->newTicketAttachmentDescriptions[$index] ?? ''));
if ($specific !== '') {
return $specific;
}
if (filled($this->newTicketFotoNote)) {
return 'Nota foto: ' . trim((string) $this->newTicketFotoNote);
}
return 'Allegato da Ticket Mobile';
}
private function prefillTicketFromLiveCall(string $phone, string $line): void
{
$this->callerSearch = $phone;
$this->searchCaller();
$rubricaId = (int) ($this->liveIncomingCall['rubrica_id'] ?? 0);
if ($rubricaId > 0) {
$this->selezionaCaller($rubricaId);
} elseif ($this->callerMatches->count() === 1) {
$only = $this->callerMatches->first();
if ($only) {
$this->selezionaCaller((int) $only->id);
}
}
if (blank($this->newTicketTitolo)) {
$this->newTicketTitolo = 'Chiamata in ingresso ' . $phone;
}
if (blank($this->newTicketDescrizione)) {
$this->newTicketDescrizione = 'Segnalazione aperta durante chiamata in ingresso da ' . $phone . ".\n" . $line;
}
}
private function normalizePhoneDigits(string $value): string
{
return preg_replace('/\D+/', '', $value) ?: '';
}
private function matchRubricaByPhone(string $digits): ?RubricaUniversale
{
if ($digits === '') {
return null;
}
$needle = '%' . $digits . '%';
$tail = strlen($digits) >= 7 ? substr($digits, -7) : $digits;
$tailNeedle = '%' . $tail . '%';
return RubricaUniversale::query()
->where(function ($q) use ($needle, $tailNeedle): void {
$q->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_cellulare, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$needle])
->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_ufficio, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$needle])
->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_casa, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$needle])
->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_cellulare, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$tailNeedle])
->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_ufficio, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$tailNeedle])
->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_casa, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$tailNeedle]);
})
->orderByDesc('updated_at')
->first();
}
private function matchSoggetto(?RubricaUniversale $rubrica, string $phone): ?Soggetto
{
if ($rubrica) {
$cf = trim((string) ($rubrica->codice_fiscale ?? ''));
if ($cf !== '') {
$found = Soggetto::query()->where('codice_fiscale', $cf)->first();
if ($found) {
return $found;
}
}
$piva = trim((string) ($rubrica->partita_iva ?? ''));
if ($piva !== '') {
$found = Soggetto::query()->where('partita_iva', $piva)->first();
if ($found) {
return $found;
}
}
}
$tail = strlen($phone) >= 7 ? substr($phone, -7) : $phone;
if ($tail === '') {
return null;
}
return Soggetto::query()->where('telefono', 'like', '%' . $tail . '%')->first();
}
public function excerpt(?string $text, int $limit = 160): string
{
return Str::limit((string) $text, $limit);
}
public function getCallerStabileLabel(int $rubricaId): ?string
{
$stabile = Stabile::query()->where('rubrica_id', $rubricaId)->first();
if (! $stabile) {
return null;
}
return $stabile->denominazione ?: ('Stabile #' . $stabile->id);
}
private function aggiornaStatoTicket(int $ticketId, string $nuovoStato, bool $assegnaAUtente = false): void
{
$user = Auth::user();
if (! $user instanceof User) {
return;
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId) {
return;
}
$ticket = Ticket::query()
->where('id', $ticketId)
->where('stabile_id', $stabileId)
->first();
if (! $ticket) {
Notification::make()
->title('Ticket non trovato o non accessibile')
->danger()
->send();
return;
}
$payload = ['stato' => $nuovoStato];
if ($assegnaAUtente) {
$payload['assegnato_a_user_id'] = $user->id;
}
if ($nuovoStato === 'Risolto' && ! $ticket->data_risoluzione_effettiva) {
$payload['data_risoluzione_effettiva'] = now()->toDateString();
}
if ($nuovoStato === 'Chiuso' && ! $ticket->data_chiusura_effettiva) {
$payload['data_chiusura_effettiva'] = now()->toDateString();
}
$ticket->update($payload);
Notification::make()
->title('Ticket #' . $ticket->id . ' aggiornato a ' . $nuovoStato)
->success()
->send();
$this->refreshData();
}
}