896 lines
32 KiB
PHP
896 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\Services\Cti\PbxRoutingService;
|
|
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 ?? ''));
|
|
$watchedExtensions = app(PbxRoutingService::class)->getWatchedExtensionsForUser($authUser);
|
|
$popupRestricted = (bool) ($authUser->pbx_popup_enabled ?? false) && count($watchedExtensions) > 0;
|
|
|
|
$latest = CommunicationMessage::query()
|
|
->whereIn('channel', ['smdr', 'panasonic_csta'])
|
|
->where('direction', 'inbound')
|
|
->whereNotNull('received_at')
|
|
->where('received_at', '>=', now()->subMinutes(8))
|
|
->when($popupRestricted, function ($q) use ($watchedExtensions): void {
|
|
$q->whereIn('target_extension', $watchedExtensions);
|
|
})
|
|
->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();
|
|
}
|
|
}
|