1772 lines
62 KiB
PHP
1772 lines
62 KiB
PHP
<?php
|
|
namespace App\Filament\Pages\Strumenti;
|
|
|
|
use App\Filament\Pages\Gescon\RubricaUniversaleScheda;
|
|
use App\Models\ChiamataPostIt;
|
|
use App\Models\CommunicationMessage;
|
|
use App\Models\Fornitore;
|
|
use App\Models\PbxClickToCallRequest;
|
|
use App\Models\RubricaUniversale;
|
|
use App\Models\Ticket;
|
|
use App\Models\User;
|
|
use App\Services\Anagrafiche\RubricaPhoneLookupService;
|
|
use App\Services\Cti\PbxRoutingService;
|
|
use App\Support\StabileContext;
|
|
use BackedEnum;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Pages\Page;
|
|
use Illuminate\Database\QueryException;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Support\Str;
|
|
use UnitEnum;
|
|
|
|
class PostItGestione extends Page
|
|
{
|
|
/** @var array<string,string> */
|
|
private const SMDR_PUBLIC_LINE_FALLBACKS = [
|
|
'1' => '0639731100',
|
|
'01' => '0639731100',
|
|
'0001' => '0639731100',
|
|
'3' => '0688812703',
|
|
'03' => '0688812703',
|
|
'0003' => '0688812703',
|
|
];
|
|
|
|
protected static ?string $title = 'Post-it Gestione';
|
|
|
|
protected static ?string $navigationLabel = 'Post-it Gestione';
|
|
|
|
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-clipboard-document-list';
|
|
|
|
protected static ?string $slug = 'strumenti/post-it-gestione';
|
|
|
|
protected static bool $shouldRegisterNavigation = true;
|
|
|
|
protected static UnitEnum|string|null $navigationGroup = 'Strumenti';
|
|
|
|
protected static ?int $navigationSort = 11;
|
|
|
|
protected string $view = 'filament.pages.strumenti.post-it-gestione';
|
|
|
|
public ?int $focusPostItId = null;
|
|
|
|
public string $activeTab = 'storico';
|
|
|
|
public string $tecnicoCallView = 'esterne';
|
|
|
|
public string $tecnicoDirectionFilter = 'tutte';
|
|
|
|
public string $tecnicoSearch = '';
|
|
|
|
public string $tecnicoLineFilter = '';
|
|
|
|
public string $tecnicoScopeFilter = 'tutti';
|
|
|
|
public ?string $selectedGroupModalKey = null;
|
|
|
|
public string $selectedGroupModalMode = 'open';
|
|
|
|
public string $appuntiOwnerFilter = 'mine';
|
|
|
|
public ?int $selectedAppuntoId = null;
|
|
|
|
public ?string $selectedAppuntoOggetto = null;
|
|
|
|
public ?string $selectedAppuntoNota = null;
|
|
|
|
public string $appuntoAssegnazioneTipo = 'operatore';
|
|
|
|
public ?int $appuntoAssegnazioneUserId = null;
|
|
|
|
public ?int $appuntoAssegnazioneFornitoreId = null;
|
|
|
|
/** @var array<int,string> */
|
|
public array $riaperturaNote = [];
|
|
|
|
/** @var array<int,string> */
|
|
public array $conversioneNote = [];
|
|
|
|
/** @var array<string, int|null> */
|
|
private array $rubricaPhoneCache = [];
|
|
|
|
/** @var array<string, array{id:int|null,name:string|null}> */
|
|
private array $pbxExtensionCache = [];
|
|
|
|
/** @var array<int|string, CommunicationMessage|null> */
|
|
private array $postItMessageCache = [];
|
|
|
|
/** @var array<int,string> */
|
|
private array $monitoredResponseGroups = ['601', '603'];
|
|
|
|
/** @var array<int,string> */
|
|
private array $hiddenVisualizationResponseGroups = ['601'];
|
|
|
|
public function mount(): void
|
|
{
|
|
$focus = (int) request()->query('focus_post_it', 0);
|
|
$this->focusPostItId = $focus > 0 ? $focus : null;
|
|
|
|
if (in_array((string) request()->query('tab'), ['storico', 'appunti', 'lavagna', 'archivio'], true)) {
|
|
$this->activeTab = (string) request()->query('tab');
|
|
}
|
|
}
|
|
|
|
public function selectAppunto(int $postItId): void
|
|
{
|
|
$this->selectedAppuntoId = $postItId > 0 ? $postItId : null;
|
|
$this->loadSelectedAppuntoState();
|
|
}
|
|
|
|
public function updatedSelectedAppuntoId(): void
|
|
{
|
|
$this->loadSelectedAppuntoState();
|
|
}
|
|
|
|
public function getPostItInserimentoUrl(): string
|
|
{
|
|
return PostIt::getUrl(panel: 'admin-filament');
|
|
}
|
|
|
|
public function getPostItPageUrl(int $postItId): string
|
|
{
|
|
return PostIt::getUrl(panel: 'admin-filament') . '?focus_post_it=' . $postItId;
|
|
}
|
|
|
|
public function convertiInTicket(int $postItId): void
|
|
{
|
|
if (! $this->isPostItTableReady()) {
|
|
Notification::make()
|
|
->title('Tabella chiamate non pronta')
|
|
->body('Esegui prima le migrazioni per usare la gestione Post-it.')
|
|
->danger()
|
|
->send();
|
|
return;
|
|
}
|
|
|
|
$postIt = ChiamataPostIt::query()->find($postItId);
|
|
if (! $postIt) {
|
|
return;
|
|
}
|
|
|
|
if ($postIt->ticket_id) {
|
|
Notification::make()->title('Gia convertito in ticket')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
if (! $postIt->stabile_id) {
|
|
Notification::make()
|
|
->title('Stabile mancante')
|
|
->body('Associa prima uno stabile al Post-it per poter creare il ticket.')
|
|
->danger()
|
|
->send();
|
|
return;
|
|
}
|
|
|
|
$ticket = Ticket::query()->create([
|
|
'stabile_id' => $postIt->stabile_id,
|
|
'aperto_da_user_id' => Auth::id(),
|
|
'titolo' => $postIt->oggetto ?: ('Chiamata da ' . ($postIt->nome_chiamante ?: 'Contatto sconosciuto')),
|
|
'descrizione' => $this->buildTicketDescriptionFromPostIt($postItId, $postIt),
|
|
'data_apertura' => now(),
|
|
'stato' => 'Aperto',
|
|
'priorita' => $postIt->priorita,
|
|
]);
|
|
|
|
$postIt->ticket_id = $ticket->id;
|
|
$postIt->stato = 'ticket';
|
|
$postIt->save();
|
|
|
|
Notification::make()
|
|
->title('Ticket creato')
|
|
->body("Ticket #{$ticket->id} creato dal Post-it #{$postIt->id}")
|
|
->success()
|
|
->send();
|
|
|
|
unset($this->conversioneNote[$postItId]);
|
|
}
|
|
|
|
public function chiudiPostIt(int $postItId): void
|
|
{
|
|
if (! $this->isPostItTableReady()) {
|
|
return;
|
|
}
|
|
|
|
$postIt = ChiamataPostIt::query()->find($postItId);
|
|
if (! $postIt) {
|
|
return;
|
|
}
|
|
|
|
$postIt->stato = 'chiusa';
|
|
$postIt->save();
|
|
|
|
Notification::make()
|
|
->title('Post-it chiuso')
|
|
->success()
|
|
->send();
|
|
}
|
|
|
|
public function riapriPostIt(int $postItId): void
|
|
{
|
|
if (! $this->isPostItTableReady()) {
|
|
return;
|
|
}
|
|
|
|
$postIt = ChiamataPostIt::query()->find($postItId);
|
|
if (! $postIt) {
|
|
return;
|
|
}
|
|
|
|
$noteInput = trim((string) ($this->riaperturaNote[$postItId] ?? ''));
|
|
$stamp = now()->format('d/m/Y H:i');
|
|
$noteLine = $noteInput !== ''
|
|
? '[' . $stamp . '] Riapertura: ' . $noteInput
|
|
: '[' . $stamp . '] Riapertura da gestione Post-it';
|
|
|
|
$existing = trim((string) ($postIt->nota ?? ''));
|
|
$postIt->nota = $existing !== '' ? ($existing . "\n" . $noteLine) : $noteLine;
|
|
$postIt->stato = $postIt->ticket_id ? 'ticket' : 'post_it';
|
|
|
|
if (Schema::hasColumn('chiamate_post_it', 'chiusa_il')) {
|
|
$postIt->chiusa_il = null;
|
|
}
|
|
|
|
$postIt->save();
|
|
|
|
if ($postIt->ticket_id) {
|
|
$ticket = Ticket::query()->find((int) $postIt->ticket_id);
|
|
if ($ticket && in_array((string) $ticket->stato, ['Chiuso', 'Risolto'], true)) {
|
|
$ticket->stato = 'Preso in Carico';
|
|
|
|
if (Schema::hasColumn('tickets', 'data_chiusura_effettiva')) {
|
|
$ticket->data_chiusura_effettiva = null;
|
|
}
|
|
|
|
if (Schema::hasColumn('tickets', 'data_risoluzione_effettiva')) {
|
|
$ticket->data_risoluzione_effettiva = null;
|
|
}
|
|
|
|
$ticket->save();
|
|
|
|
if (method_exists($ticket, 'messages') && Schema::hasTable('ticket_messages')) {
|
|
$ticket->messages()->create([
|
|
'user_id' => Auth::id(),
|
|
'messaggio' => $noteLine,
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
unset($this->riaperturaNote[$postItId]);
|
|
|
|
Notification::make()
|
|
->title('Post-it riaperto')
|
|
->body('Riapertura completata con nota di tracciamento.')
|
|
->success()
|
|
->send();
|
|
}
|
|
|
|
public function saveSelectedAppunto(): void
|
|
{
|
|
if (! $this->isPostItTableReady() || ! $this->selectedAppuntoId) {
|
|
Notification::make()->title('Seleziona prima un appunto')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$this->validate([
|
|
'selectedAppuntoOggetto' => ['nullable', 'string', 'max:255'],
|
|
'selectedAppuntoNota' => ['nullable', 'string'],
|
|
]);
|
|
|
|
$postIt = ChiamataPostIt::query()->find($this->selectedAppuntoId);
|
|
if (! $postIt) {
|
|
Notification::make()->title('Post-it non trovato')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$postIt->oggetto = $this->normalizeOptionalText($this->selectedAppuntoOggetto);
|
|
$postIt->nota = $this->normalizeOptionalText($this->selectedAppuntoNota);
|
|
$postIt->save();
|
|
|
|
Notification::make()->title('Appunto aggiornato')->success()->send();
|
|
}
|
|
|
|
public function assegnaSelectedAppunto(): void
|
|
{
|
|
if (! $this->isPostItAssignmentReady() || ! $this->selectedAppuntoId) {
|
|
Notification::make()->title('Assegnazione non disponibile')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$postIt = ChiamataPostIt::query()->find($this->selectedAppuntoId);
|
|
if (! $postIt) {
|
|
Notification::make()->title('Post-it non trovato')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$this->validate([
|
|
'appuntoAssegnazioneTipo' => ['required', 'in:operatore,collaboratore,fornitore'],
|
|
'appuntoAssegnazioneUserId' => ['nullable', 'integer', 'exists:users,id'],
|
|
'appuntoAssegnazioneFornitoreId' => ['nullable', 'integer', 'exists:fornitori,id'],
|
|
]);
|
|
|
|
$payload = [
|
|
'assegnazione_tipo' => $this->appuntoAssegnazioneTipo,
|
|
'assegnato_a_user_id' => null,
|
|
'assegnato_a_fornitore_id' => null,
|
|
];
|
|
|
|
if (in_array($this->appuntoAssegnazioneTipo, ['operatore', 'collaboratore'], true)) {
|
|
if (! $this->appuntoAssegnazioneUserId) {
|
|
Notification::make()->title('Seleziona un utente')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$payload['assegnato_a_user_id'] = $this->appuntoAssegnazioneUserId;
|
|
}
|
|
|
|
if ($this->appuntoAssegnazioneTipo === 'fornitore') {
|
|
if (! $this->appuntoAssegnazioneFornitoreId) {
|
|
Notification::make()->title('Seleziona un fornitore')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$payload['assegnato_a_fornitore_id'] = $this->appuntoAssegnazioneFornitoreId;
|
|
}
|
|
|
|
$postIt->update($payload);
|
|
|
|
if ($postIt->ticket) {
|
|
$ticketPayload = [];
|
|
|
|
if ($this->appuntoAssegnazioneTipo === 'fornitore') {
|
|
$ticketPayload['assegnato_a_fornitore_id'] = $this->appuntoAssegnazioneFornitoreId;
|
|
$ticketPayload['assegnato_a_user_id'] = null;
|
|
} else {
|
|
$ticketPayload['assegnato_a_user_id'] = $this->appuntoAssegnazioneUserId;
|
|
}
|
|
|
|
if ($ticketPayload !== []) {
|
|
$postIt->ticket->update($ticketPayload);
|
|
}
|
|
|
|
if (method_exists($postIt->ticket, 'messages') && Schema::hasTable('ticket_messages')) {
|
|
$postIt->ticket->messages()->create([
|
|
'user_id' => Auth::id(),
|
|
'messaggio' => 'Assegnazione da Gestione Post-it: ' . $this->getSelectedAppuntoAssignmentLabel(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
Notification::make()->title('Assegnazione aggiornata')->success()->send();
|
|
}
|
|
|
|
public function getRecentiProperty()
|
|
{
|
|
if (! $this->isPostItTableReady()) {
|
|
return collect();
|
|
}
|
|
|
|
try {
|
|
$query = ChiamataPostIt::query()
|
|
->with(['rubrica', 'stabile', 'ticket', 'creatoDa', 'assegnatoAUser:id,name', 'assegnatoAFornitore:id,ragione_sociale,nome,cognome'])
|
|
->orderByDesc('chiamata_il')
|
|
->orderByDesc('id');
|
|
|
|
if ($this->focusPostItId) {
|
|
$query->orderByRaw('CASE WHEN id = ? THEN 0 ELSE 1 END', [$this->focusPostItId]);
|
|
}
|
|
|
|
return $query->limit(80)->get();
|
|
} catch (QueryException) {
|
|
return collect();
|
|
}
|
|
}
|
|
|
|
public function getRecentiVisibiliProperty()
|
|
{
|
|
return $this->recenti
|
|
->filter(fn(ChiamataPostIt $postIt): bool => $this->shouldShowPostIt($postIt))
|
|
->values();
|
|
}
|
|
|
|
private function shouldShowPostIt(ChiamataPostIt $postIt): bool
|
|
{
|
|
if ($this->isInternalPostIt($postIt)) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->isHiddenVisualizationResponseGroupPostIt($postIt)) {
|
|
return false;
|
|
}
|
|
|
|
return ! $this->isLegacyFilteredSmdrPostIt($postIt);
|
|
}
|
|
|
|
private function hasOperationalNoteContent(ChiamataPostIt $postIt): bool
|
|
{
|
|
return trim($this->extractOperationalNoteContent($postIt)) !== '';
|
|
}
|
|
|
|
private function extractOperationalNoteContent(ChiamataPostIt $postIt): string
|
|
{
|
|
$note = trim((string) ($postIt->nota ?? ''));
|
|
if ($note === '') {
|
|
return '';
|
|
}
|
|
|
|
$origin = strtolower(trim((string) ($postIt->origine ?? '')));
|
|
if (! in_array($origin, ['smdr', 'smdr_table', 'smdr_live_banner', 'topbar_live_call', 'panasonic_csta_table', 'csta_table'], true)) {
|
|
return $note;
|
|
}
|
|
|
|
$lines = preg_split('/\r\n|\r|\n/', $note) ?: [];
|
|
$kept = [];
|
|
|
|
foreach ($lines as $line) {
|
|
$line = trim((string) $line);
|
|
if ($line === '') {
|
|
continue;
|
|
}
|
|
|
|
if (preg_match('/^\d{2}\/\d{2}\/\d{2}\s+\d{2}:\d{2}\s+\d+\b/i', $line) === 1) {
|
|
continue;
|
|
}
|
|
|
|
if (preg_match('/^chiamata ricevuta dal numero\b/i', $line) === 1) {
|
|
continue;
|
|
}
|
|
|
|
if (preg_match('/^interno\/gruppo chiamato:\b/i', $line) === 1) {
|
|
continue;
|
|
}
|
|
|
|
if (preg_match('/^nota operativa:\s*\d{2}\/\d{2}\/\d{2}/i', $line) === 1) {
|
|
continue;
|
|
}
|
|
|
|
if (preg_match('/^dettaglio sorgente:\s*\d{2}\/\d{2}\/\d{2}/i', $line) === 1) {
|
|
continue;
|
|
}
|
|
|
|
if (preg_match('/^contatto:\s*contatto non in rubrica$/i', $line) === 1) {
|
|
continue;
|
|
}
|
|
|
|
$kept[] = $line;
|
|
}
|
|
|
|
return trim(implode("\n", $kept));
|
|
}
|
|
|
|
public function getRecentiApertiVisibiliProperty()
|
|
{
|
|
return $this->recentiVisibili
|
|
->filter(fn(ChiamataPostIt $postIt): bool => (string) $postIt->stato !== 'chiusa')
|
|
->values();
|
|
}
|
|
|
|
public function getRecentiChiusiVisibiliProperty()
|
|
{
|
|
return $this->recentiVisibili
|
|
->filter(fn(ChiamataPostIt $postIt): bool => (string) $postIt->stato === 'chiusa')
|
|
->values();
|
|
}
|
|
|
|
public function getRecentiRaggruppatiProperty()
|
|
{
|
|
return $this->buildGroupedCollection($this->recentiApertiVisibili);
|
|
}
|
|
|
|
public function getRecentiChiusiRaggruppatiProperty()
|
|
{
|
|
return $this->buildGroupedCollection($this->recentiChiusiVisibili);
|
|
}
|
|
|
|
public function getAppuntiOperativiProperty()
|
|
{
|
|
if (! $this->isPostItTableReady()) {
|
|
return collect();
|
|
}
|
|
|
|
try {
|
|
$query = ChiamataPostIt::query()
|
|
->with(['rubrica', 'stabile', 'ticket', 'creatoDa', 'assegnatoAUser:id,name', 'assegnatoAFornitore:id,ragione_sociale,nome,cognome'])
|
|
->orderByDesc('chiamata_il')
|
|
->orderByDesc('id');
|
|
|
|
$authId = (int) (Auth::id() ?? 0);
|
|
if ($this->appuntiOwnerFilter === 'mine' && $authId > 0) {
|
|
$query->where(function ($inner) use ($authId): void {
|
|
$inner->where('creato_da_user_id', $authId)
|
|
->orWhere('assegnato_a_user_id', $authId);
|
|
});
|
|
}
|
|
|
|
$items = $query
|
|
->limit(200)
|
|
->get()
|
|
->filter(fn(ChiamataPostIt $postIt): bool => $this->shouldShowPostIt($postIt) && $this->hasOperationalNoteContent($postIt))
|
|
->values();
|
|
|
|
if ($items->isNotEmpty() && ! $items->contains(fn(ChiamataPostIt $postIt): bool => (int) $postIt->id === (int) $this->selectedAppuntoId)) {
|
|
$this->selectedAppuntoId = $this->focusPostItId && $items->contains(fn(ChiamataPostIt $postIt): bool => (int) $postIt->id === (int) $this->focusPostItId)
|
|
? (int) $this->focusPostItId
|
|
: (int) $items->first()->id;
|
|
}
|
|
|
|
return $items;
|
|
} catch (QueryException) {
|
|
return collect();
|
|
}
|
|
}
|
|
|
|
public function getSelectedAppuntoProperty(): ?ChiamataPostIt
|
|
{
|
|
if (! $this->isPostItTableReady() || ! $this->selectedAppuntoId) {
|
|
return null;
|
|
}
|
|
|
|
return ChiamataPostIt::query()
|
|
->with(['rubrica', 'stabile', 'ticket', 'creatoDa', 'assegnatoAUser:id,name', 'assegnatoAFornitore:id,ragione_sociale,nome,cognome'])
|
|
->find($this->selectedAppuntoId);
|
|
}
|
|
|
|
public function isPostItAssignmentReady(): bool
|
|
{
|
|
return $this->isPostItTableReady()
|
|
&& Schema::hasColumn('chiamate_post_it', 'assegnazione_tipo')
|
|
&& Schema::hasColumn('chiamate_post_it', 'assegnato_a_user_id')
|
|
&& Schema::hasColumn('chiamate_post_it', 'assegnato_a_fornitore_id');
|
|
}
|
|
|
|
public function getOperatoriOptionsProperty(): array
|
|
{
|
|
return User::query()
|
|
->role(['super-admin', 'admin', 'amministratore'])
|
|
->orderBy('name')
|
|
->limit(100)
|
|
->pluck('name', 'id')
|
|
->map(fn($name): string => (string) $name)
|
|
->all();
|
|
}
|
|
|
|
public function getCollaboratoriOptionsProperty(): array
|
|
{
|
|
$adminId = $this->resolveCurrentAmministratoreId();
|
|
|
|
return User::query()
|
|
->whereHas('roles', function ($q): void {
|
|
$q->where('name', 'collaboratore');
|
|
})
|
|
->when($adminId > 0, function ($query) use ($adminId): void {
|
|
$query->whereHas('stabiliAssegnati', function ($inner) use ($adminId): void {
|
|
$inner->where('amministratore_id', $adminId);
|
|
});
|
|
})
|
|
->orderBy('name')
|
|
->limit(150)
|
|
->pluck('name', 'id')
|
|
->map(fn($name): string => (string) $name)
|
|
->all();
|
|
}
|
|
|
|
public function getFornitoriOptionsProperty(): array
|
|
{
|
|
$adminId = $this->resolveCurrentAmministratoreId();
|
|
|
|
return Fornitore::query()
|
|
->when($adminId > 0, fn($query) => $query->where('amministratore_id', $adminId))
|
|
->orderByRaw("COALESCE(ragione_sociale, CONCAT(COALESCE(nome, ''), ' ', COALESCE(cognome, '')))")
|
|
->limit(150)
|
|
->get(['id', 'ragione_sociale', 'nome', 'cognome'])
|
|
->mapWithKeys(function (Fornitore $fornitore): array {
|
|
$label = trim((string) ($fornitore->ragione_sociale ?: (($fornitore->nome ?? '') . ' ' . ($fornitore->cognome ?? ''))));
|
|
|
|
return [(int) $fornitore->id => ($label !== '' ? $label : ('Fornitore #' . $fornitore->id))];
|
|
})
|
|
->all();
|
|
}
|
|
|
|
public function getOperationalNoteSummary(ChiamataPostIt $postIt, int $limit = 180): string
|
|
{
|
|
return Str::limit($this->extractOperationalNoteContent($postIt), $limit);
|
|
}
|
|
|
|
public function openGroupModal(string $modalKey, string $mode = 'open'): void
|
|
{
|
|
$this->selectedGroupModalKey = $modalKey;
|
|
$this->selectedGroupModalMode = in_array($mode, ['open', 'closed'], true) ? $mode : 'open';
|
|
}
|
|
|
|
public function closeGroupModal(): void
|
|
{
|
|
$this->selectedGroupModalKey = null;
|
|
$this->selectedGroupModalMode = 'open';
|
|
}
|
|
|
|
public function createRubricaDraftFromGroup(string $modalKey, string $mode = 'open'): void
|
|
{
|
|
$group = $this->findGroupedCollectionByModalKey($modalKey, $mode);
|
|
if (! is_array($group)) {
|
|
Notification::make()->title('Gruppo chiamate non trovato')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$phoneDigits = preg_replace('/\D+/', '', (string) ($group['phone'] ?? '')) ?: '';
|
|
if (! $this->canCreateRubricaFromPhone($phoneDigits)) {
|
|
Notification::make()
|
|
->title('Numero non adatto alla rubrica')
|
|
->body('Per creare una nuova anagrafica serve un numero esterno valido, non un interno breve.')
|
|
->warning()
|
|
->send();
|
|
return;
|
|
}
|
|
|
|
$existingId = $this->resolveRubricaIdByPhone($phoneDigits);
|
|
if ($existingId) {
|
|
$this->redirect(RubricaUniversaleScheda::getUrl(['record' => $existingId], panel: 'admin-filament'), navigate: true);
|
|
return;
|
|
}
|
|
|
|
$identity = $this->buildRubricaDraftIdentity((string) ($group['caller_label'] ?? ''), $phoneDigits);
|
|
$user = Auth::user();
|
|
$adminId = (int) ($user?->amministratore?->id ?? 0);
|
|
|
|
$rubrica = RubricaUniversale::query()->create([
|
|
'amministratore_id' => $adminId > 0 ? $adminId : null,
|
|
'nome' => $identity['nome'],
|
|
'cognome' => $identity['cognome'],
|
|
'ragione_sociale' => $identity['ragione_sociale'],
|
|
'tipo_contatto' => $identity['tipo_contatto'],
|
|
'telefono_cellulare' => $this->isLikelyMobilePhone($phoneDigits) ? $phoneDigits : null,
|
|
'telefono_ufficio' => $this->isLikelyMobilePhone($phoneDigits) ? null : $phoneDigits,
|
|
'categoria' => 'altro',
|
|
'stato' => 'attivo',
|
|
'data_inserimento' => now()->toDateString(),
|
|
'data_ultima_modifica' => now()->toDateString(),
|
|
'creato_da' => Auth::id(),
|
|
'modificato_da' => Auth::id(),
|
|
'note_segreteria' => 'Bozza creata da Lavagna chiamate per gestire richiamo e identificazione del contatto.',
|
|
]);
|
|
|
|
$itemIds = collect($group['items'])
|
|
->map(fn(ChiamataPostIt $item): int => (int) $item->id)
|
|
->filter(fn(int $id): bool => $id > 0)
|
|
->values()
|
|
->all();
|
|
|
|
if ($itemIds !== []) {
|
|
ChiamataPostIt::query()
|
|
->whereIn('id', $itemIds)
|
|
->update([
|
|
'rubrica_id' => (int) $rubrica->id,
|
|
]);
|
|
}
|
|
|
|
$this->rubricaPhoneCache = [];
|
|
|
|
Notification::make()
|
|
->title('Bozza anagrafica creata')
|
|
->body('Nuovo contatto collegato alla lavagna chiamate. Completa ora la scheda rubrica.')
|
|
->success()
|
|
->send();
|
|
|
|
$this->redirect(RubricaUniversaleScheda::getUrl(['record' => (int) $rubrica->id], panel: 'admin-filament'), navigate: true);
|
|
}
|
|
|
|
public function getSelectedGroupModalProperty(): ?array
|
|
{
|
|
return $this->selectedGroupModalKey !== null
|
|
? $this->findGroupedCollectionByModalKey($this->selectedGroupModalKey, $this->selectedGroupModalMode)
|
|
: null;
|
|
}
|
|
|
|
public function getSelectedGroupModalDaysProperty()
|
|
{
|
|
$group = $this->selectedGroupModal;
|
|
if (! is_array($group)) {
|
|
return collect();
|
|
}
|
|
|
|
return collect($group['items'])
|
|
->groupBy(function (ChiamataPostIt $item): string {
|
|
return $item->chiamata_il?->format('d/m/Y') ?: 'Data non disponibile';
|
|
})
|
|
->map(function ($items, string $day): array {
|
|
$sorted = collect($items)
|
|
->sortByDesc(fn(ChiamataPostIt $item) => $item->chiamata_il?->getTimestamp() ?? 0)
|
|
->values();
|
|
|
|
return [
|
|
'day' => $day,
|
|
'items' => $sorted,
|
|
];
|
|
})
|
|
->values();
|
|
}
|
|
|
|
public function getChiamateTecnicheProperty()
|
|
{
|
|
if (! Schema::hasTable('communication_messages')) {
|
|
return collect();
|
|
}
|
|
|
|
try {
|
|
$query = CommunicationMessage::query()
|
|
->with(['assignedUser:id,name', 'stabile:id,denominazione'])
|
|
->latest('id');
|
|
|
|
if ($this->activeTab === 'smdr') {
|
|
$query->where('channel', 'smdr');
|
|
} elseif ($this->activeTab === 'csta') {
|
|
$query->where('channel', 'panasonic_csta');
|
|
} else {
|
|
$query->whereIn('channel', ['smdr', 'panasonic_csta']);
|
|
}
|
|
|
|
if ($this->tecnicoCallView === 'interne') {
|
|
$query->where('direction', 'internal');
|
|
} elseif ($this->tecnicoCallView === 'esterne') {
|
|
$query->where('direction', '!=', 'internal');
|
|
}
|
|
|
|
if (in_array($this->tecnicoDirectionFilter, ['inbound', 'outbound', 'internal'], true)) {
|
|
$query->where('direction', $this->tecnicoDirectionFilter);
|
|
}
|
|
|
|
$search = trim($this->tecnicoSearch);
|
|
if ($search !== '') {
|
|
$query->where(function ($q) use ($search): void {
|
|
$q->where('phone_number', 'like', '%' . $search . '%')
|
|
->orWhere('target_extension', 'like', '%' . $search . '%')
|
|
->orWhere('message_text', 'like', '%' . $search . '%')
|
|
->orWhere('sender_name', 'like', '%' . $search . '%');
|
|
});
|
|
}
|
|
|
|
$items = $query
|
|
->limit(250)
|
|
->get();
|
|
|
|
if (in_array($this->tecnicoScopeFilter, ['studio', 'esterno', 'interno'], true)) {
|
|
$items = $items->filter(fn(CommunicationMessage $message): bool => $this->getTecnicoScope($message) === $this->tecnicoScopeFilter)->values();
|
|
}
|
|
|
|
$lineFilter = trim($this->tecnicoLineFilter);
|
|
if ($lineFilter !== '') {
|
|
$items = $items->filter(fn(CommunicationMessage $message): bool => $this->matchesTecnicoLineFilter($message, $lineFilter))->values();
|
|
}
|
|
|
|
return $items;
|
|
} catch (QueryException) {
|
|
return collect();
|
|
}
|
|
}
|
|
|
|
public function getRecentClickToCallRequestsProperty()
|
|
{
|
|
if (! Schema::hasTable('pbx_click_to_call_requests')) {
|
|
return collect();
|
|
}
|
|
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return collect();
|
|
}
|
|
|
|
$extension = $this->normalizeExtension((string) ($user->pbx_extension ?? ''));
|
|
|
|
return PbxClickToCallRequest::query()
|
|
->when($extension !== '', function ($query) use ($extension, $user): void {
|
|
$query->where(function ($inner) use ($extension, $user): void {
|
|
$inner->where('source_extension', $extension)
|
|
->orWhere('requested_by_user_id', (int) $user->id);
|
|
});
|
|
}, fn($query) => $query->where('requested_by_user_id', (int) $user->id))
|
|
->orderByDesc('requested_at')
|
|
->orderByDesc('id')
|
|
->limit(20)
|
|
->get();
|
|
}
|
|
|
|
public function creaPostItDaMessaggio(int $messageId): void
|
|
{
|
|
if (! $this->isPostItTableReady()) {
|
|
Notification::make()->title('Tabella post-it non pronta')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$message = CommunicationMessage::query()->find($messageId);
|
|
if (! $message) {
|
|
return;
|
|
}
|
|
|
|
if (! empty($message->post_it_id)) {
|
|
Notification::make()->title('Post-it già associato')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$sourceKey = (string) ($message->channel ?: 'cti');
|
|
$sourceLabel = $sourceKey === 'panasonic_csta' ? 'Panasonic CTI' : 'SMDR';
|
|
$smdr = (array) data_get($message->metadata, 'smdr', []);
|
|
$line = trim((string) ($message->message_text ?? ''));
|
|
$phone = (string) ($message->phone_number ?: ($smdr['dial_number'] ?? ''));
|
|
$nominativo = $this->getTecnicoNominativo($message);
|
|
$lineLabel = $this->getTecnicoLineaLabel($message);
|
|
$callerLabel = $this->resolveTecnicoCallerLabel($message, $phone, $nominativo);
|
|
$direction = match (strtolower(trim((string) $message->direction))) {
|
|
'inbound' => 'Chiamata ricevuta',
|
|
'outbound' => 'Chiamata effettuata',
|
|
'internal' => 'Chiamata interna',
|
|
default => 'Evento telefonico',
|
|
};
|
|
|
|
$oggetto = $direction;
|
|
if ($lineLabel !== '-') {
|
|
$oggetto .= (string) $message->channel === 'smdr'
|
|
? ' - linea ' . $lineLabel
|
|
: ' - ' . $lineLabel;
|
|
}
|
|
|
|
$notaParts = [];
|
|
if ($phone !== '') {
|
|
$notaParts[] = 'Numero: ' . $phone;
|
|
}
|
|
if ($nominativo) {
|
|
$notaParts[] = 'Contatto: ' . $nominativo;
|
|
}
|
|
if ($line !== '') {
|
|
$notaParts[] = 'Dettaglio sorgente: ' . $line;
|
|
}
|
|
|
|
$postIt = ChiamataPostIt::query()->create([
|
|
'stabile_id' => $this->resolveOperationalPostItStabileId($message, $phone),
|
|
'creato_da_user_id' => Auth::id(),
|
|
'telefono' => $phone !== '' ? $phone : $message->phone_number,
|
|
'nome_chiamante' => $callerLabel !== '' ? $callerLabel : ($sourceLabel . ' ' . strtoupper((string) $message->direction)),
|
|
'direzione' => $this->normalizePostItDirection((string) $message->direction),
|
|
'origine' => $sourceKey . '_table',
|
|
'origine_id' => (string) $message->id,
|
|
'durata_secondi' => is_numeric($smdr['duration_seconds'] ?? null) ? (int) $smdr['duration_seconds'] : null,
|
|
'oggetto' => $oggetto,
|
|
'nota' => $notaParts !== [] ? implode("\n", $notaParts) : ('Evento ' . $sourceLabel . ' senza dettagli operativi'),
|
|
'priorita' => 'Media',
|
|
'stato' => 'post_it',
|
|
'chiamata_il' => $message->received_at ?? now(),
|
|
]);
|
|
|
|
$message->post_it_id = $postIt->id;
|
|
$message->save();
|
|
|
|
Notification::make()
|
|
->title('Post-it creato da riga tecnica')
|
|
->body('Post-it #' . $postIt->id . ' collegato al messaggio #' . $message->id)
|
|
->success()
|
|
->send();
|
|
}
|
|
|
|
private function normalizePostItDirection(string $direction): string
|
|
{
|
|
$d = strtolower(trim($direction));
|
|
if ($d === 'inbound') {
|
|
return 'in_arrivo';
|
|
}
|
|
if ($d === 'outbound' || $d === 'internal') {
|
|
return 'in_uscita';
|
|
}
|
|
|
|
return 'in_arrivo';
|
|
}
|
|
|
|
public function isPostItTableReady(): bool
|
|
{
|
|
return Schema::hasTable('chiamate_post_it');
|
|
}
|
|
|
|
public function getRubricaUrlByPhone(?string $phone): ?string
|
|
{
|
|
$rubricaId = $this->resolveRubricaIdByPhone($phone);
|
|
if (! $rubricaId) {
|
|
return null;
|
|
}
|
|
|
|
return RubricaUniversaleScheda::getUrl(['record' => $rubricaId], panel: 'admin-filament');
|
|
}
|
|
|
|
public function getRubricaNomeByPhone(?string $phone): ?string
|
|
{
|
|
$rubricaId = $this->resolveRubricaIdByPhone($phone);
|
|
if (! $rubricaId) {
|
|
return null;
|
|
}
|
|
|
|
$rubrica = RubricaUniversale::query()->find($rubricaId, ['id', 'ragione_sociale', 'nome', 'cognome']);
|
|
if (! $rubrica) {
|
|
return null;
|
|
}
|
|
|
|
$nome = trim((string) ($rubrica->ragione_sociale ?: ((string) ($rubrica->nome ?? '') . ' ' . (string) ($rubrica->cognome ?? ''))));
|
|
return $nome !== '' ? $nome : null;
|
|
}
|
|
|
|
public function getTecnicoNominativo(CommunicationMessage $message): ?string
|
|
{
|
|
$extension = $this->extractExtensionFromMessage($message);
|
|
if ($this->isInternalSmdrMessage($message)) {
|
|
return $this->getCollaboratoreNomeByExtension($extension);
|
|
}
|
|
|
|
$phone = (string) ($message->phone_number ?: data_get($message->metadata, 'smdr.dial_number', ''));
|
|
|
|
return $this->getRubricaNomeByPhone($phone);
|
|
}
|
|
|
|
public function getTecnicoProviderLabel(CommunicationMessage $message): string
|
|
{
|
|
$provider = strtolower(trim((string) data_get($message->metadata, 'provider', '')));
|
|
if ($provider === '') {
|
|
$provider = (string) $message->channel;
|
|
}
|
|
|
|
return match ($provider) {
|
|
'smdr' => 'SMDR',
|
|
'panasonic_csta' => 'CSTA',
|
|
'panasonic_ns1000' => 'Panasonic NS1000',
|
|
'freepbx' => 'FreePBX',
|
|
default => strtoupper(str_replace('_', ' ', $provider)),
|
|
};
|
|
}
|
|
|
|
public function getTecnicoLineaLabel(CommunicationMessage $message): string
|
|
{
|
|
if ((string) $message->channel === 'smdr') {
|
|
return $this->resolveSmdrLineLabel($message);
|
|
}
|
|
|
|
$eventType = trim((string) data_get($message->metadata, 'event_type', ''));
|
|
$providerCallId = trim((string) data_get($message->metadata, 'provider_call_id', ''));
|
|
$parts = array_values(array_filter([$eventType, $providerCallId], fn(string $value): bool => $value !== ''));
|
|
|
|
return $parts !== [] ? implode(' / ', $parts) : '-';
|
|
}
|
|
|
|
public function shouldShowPostItStabile(ChiamataPostIt $postIt): bool
|
|
{
|
|
if (! $postIt->stabile) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->resolveGroupedRubricaId($postIt)) {
|
|
return true;
|
|
}
|
|
|
|
$message = $this->resolveMessageFromPostIt($postIt);
|
|
if ($message instanceof CommunicationMessage && ! $this->isInternalSmdrMessage($message)) {
|
|
return false;
|
|
}
|
|
|
|
return ! $this->isGenericCallerLabel((string) ($postIt->nome_chiamante ?? ''), (string) ($postIt->telefono ?? ''));
|
|
}
|
|
|
|
private function matchesTecnicoLineFilter(CommunicationMessage $message, string $filter): bool
|
|
{
|
|
$needle = mb_strtolower(trim($filter));
|
|
if ($needle === '') {
|
|
return true;
|
|
}
|
|
|
|
$haystacks = [
|
|
mb_strtolower($this->getTecnicoLineaLabel($message)),
|
|
mb_strtolower((string) ($message->target_extension ?? '')),
|
|
mb_strtolower((string) data_get($message->metadata, 'smdr.co', '')),
|
|
mb_strtolower((string) data_get($message->metadata, 'provider_call_id', '')),
|
|
mb_strtolower((string) ($message->message_text ?? '')),
|
|
];
|
|
|
|
foreach ($haystacks as $haystack) {
|
|
if ($haystack !== '' && str_contains($haystack, $needle)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private function buildTicketDescriptionFromPostIt(int $postItId, ChiamataPostIt $postIt): string
|
|
{
|
|
$parts = [];
|
|
|
|
$nota = trim((string) ($postIt->nota ?? ''));
|
|
if ($nota !== '') {
|
|
$parts[] = $nota;
|
|
}
|
|
|
|
$conversioneNote = trim((string) ($this->conversioneNote[$postItId] ?? ''));
|
|
if ($conversioneNote !== '') {
|
|
$parts[] = 'Nota operatore prima della conversione:' . "\n" . $conversioneNote;
|
|
}
|
|
|
|
$parts[] = 'Riferimento chiamata: #' . $postIt->id;
|
|
|
|
return implode("\n\n", $parts);
|
|
}
|
|
|
|
private function buildGroupedPostItKey(ChiamataPostIt $postIt): string
|
|
{
|
|
$phone = $this->normalizeGroupedPhone((string) ($postIt->telefono ?? ''));
|
|
if ($phone !== '') {
|
|
return 'phone:' . $phone;
|
|
}
|
|
|
|
$rubricaId = $this->resolveGroupedRubricaId($postIt);
|
|
if ($rubricaId !== null) {
|
|
return 'rubrica:' . $rubricaId;
|
|
}
|
|
|
|
return 'caller:' . $this->normalizeGroupedCallerKey((string) ($postIt->nome_chiamante ?? 'sconosciuto'));
|
|
}
|
|
|
|
private function isInternalPostIt(ChiamataPostIt $postIt): bool
|
|
{
|
|
$message = $this->resolveMessageFromPostIt($postIt);
|
|
if ($message instanceof CommunicationMessage) {
|
|
return $this->isInternalSmdrMessage($message);
|
|
}
|
|
|
|
$phone = preg_replace('/\D+/', '', (string) ($postIt->telefono ?? ''));
|
|
if (is_string($phone) && $this->isShortInternalNumber($phone)) {
|
|
return true;
|
|
}
|
|
|
|
if (is_string($phone) && $phone !== '' && $this->isKnownPbxExtension($phone)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private function isLegacyFilteredSmdrPostIt(ChiamataPostIt $postIt): bool
|
|
{
|
|
if ((string) ($postIt->origine ?? '') !== 'smdr') {
|
|
return false;
|
|
}
|
|
|
|
$raw = strtoupper(trim((string) ($postIt->nota ?? '')));
|
|
if ($raw === '') {
|
|
return false;
|
|
}
|
|
|
|
return ! str_contains($raw, '<I>');
|
|
}
|
|
|
|
private function resolveMessageFromPostIt(ChiamataPostIt $postIt): ?CommunicationMessage
|
|
{
|
|
$originKey = trim((string) ($postIt->origine_id ?? ''));
|
|
if ($originKey === '') {
|
|
return null;
|
|
}
|
|
|
|
if (array_key_exists($originKey, $this->postItMessageCache)) {
|
|
return $this->postItMessageCache[$originKey];
|
|
}
|
|
|
|
$message = null;
|
|
if (ctype_digit($originKey)) {
|
|
$message = CommunicationMessage::query()->find((int) $originKey);
|
|
} elseif ((string) ($postIt->origine ?? '') === 'smdr') {
|
|
$message = CommunicationMessage::query()
|
|
->where('channel', 'smdr')
|
|
->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(metadata, '$.smdr.fingerprint')) = ?", [$originKey])
|
|
->orderByDesc('id')
|
|
->first();
|
|
}
|
|
|
|
$this->postItMessageCache[$originKey] = $message;
|
|
|
|
return $message;
|
|
}
|
|
|
|
private function normalizeGroupedCallerKey(string $value): string
|
|
{
|
|
$normalized = preg_replace('/\s+/', ' ', trim(mb_strtolower($value))) ?? '';
|
|
|
|
return $normalized !== '' ? $normalized : 'sconosciuto';
|
|
}
|
|
|
|
private function loadSelectedAppuntoState(): void
|
|
{
|
|
$postIt = $this->selectedAppunto;
|
|
if (! $postIt) {
|
|
$this->selectedAppuntoOggetto = null;
|
|
$this->selectedAppuntoNota = null;
|
|
$this->appuntoAssegnazioneTipo = 'operatore';
|
|
$this->appuntoAssegnazioneUserId = null;
|
|
$this->appuntoAssegnazioneFornitoreId = null;
|
|
return;
|
|
}
|
|
|
|
$this->selectedAppuntoOggetto = $postIt->oggetto;
|
|
$this->selectedAppuntoNota = $postIt->nota;
|
|
$this->appuntoAssegnazioneTipo = in_array((string) ($postIt->assegnazione_tipo ?? ''), ['operatore', 'collaboratore', 'fornitore'], true)
|
|
? (string) $postIt->assegnazione_tipo
|
|
: 'operatore';
|
|
$this->appuntoAssegnazioneUserId = isset($postIt->assegnato_a_user_id) ? (int) $postIt->assegnato_a_user_id : null;
|
|
$this->appuntoAssegnazioneFornitoreId = isset($postIt->assegnato_a_fornitore_id) ? (int) $postIt->assegnato_a_fornitore_id : null;
|
|
}
|
|
|
|
private function getSelectedAppuntoAssignmentLabel(): string
|
|
{
|
|
return match ($this->appuntoAssegnazioneTipo) {
|
|
'fornitore' => 'fornitore #' . (int) ($this->appuntoAssegnazioneFornitoreId ?? 0),
|
|
'collaboratore' => 'collaboratore #' . (int) ($this->appuntoAssegnazioneUserId ?? 0),
|
|
default => 'operatore #' . (int) ($this->appuntoAssegnazioneUserId ?? 0),
|
|
};
|
|
}
|
|
|
|
private function resolveCurrentAmministratoreId(): int
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return 0;
|
|
}
|
|
|
|
if ($user->amministratore) {
|
|
return (int) $user->amministratore->id;
|
|
}
|
|
|
|
$stabile = StabileContext::getActiveStabile($user);
|
|
|
|
return (int) ($stabile?->amministratore_id ?? 0);
|
|
}
|
|
|
|
private function normalizeOptionalText(mixed $value): ?string
|
|
{
|
|
if (! is_string($value)) {
|
|
return null;
|
|
}
|
|
|
|
$text = trim($value);
|
|
|
|
return $text !== '' ? $text : null;
|
|
}
|
|
|
|
private function normalizeGroupedPhone(string $phone): string
|
|
{
|
|
$digits = preg_replace('/\D+/', '', $phone) ?: '';
|
|
|
|
return $this->isShortInternalNumber($digits) ? '' : $digits;
|
|
}
|
|
|
|
private function resolveGroupedRubricaId(ChiamataPostIt $postIt): ?int
|
|
{
|
|
$rubricaId = (int) ($postIt->rubrica_id ?? 0);
|
|
if ($rubricaId > 0) {
|
|
return $rubricaId;
|
|
}
|
|
|
|
return $this->resolveRubricaIdByPhone((string) ($postIt->telefono ?? ''));
|
|
}
|
|
|
|
/**
|
|
* @param array<int,ChiamataPostIt> $items
|
|
*/
|
|
private function resolveLatestGroupedPostIt(array $items): ChiamataPostIt
|
|
{
|
|
usort($items, function (ChiamataPostIt $left, ChiamataPostIt $right): int {
|
|
return ($right->chiamata_il?->getTimestamp() ?? 0) <=> ($left->chiamata_il?->getTimestamp() ?? 0);
|
|
});
|
|
|
|
return $items[0];
|
|
}
|
|
|
|
/**
|
|
* @param array<int,ChiamataPostIt> $items
|
|
*/
|
|
private function resolveGroupedCallerLabel(array $items): string
|
|
{
|
|
foreach ($items as $item) {
|
|
$rubricaId = $this->resolveGroupedRubricaId($item);
|
|
if ($rubricaId) {
|
|
$rubrica = RubricaUniversale::query()->find($rubricaId, ['ragione_sociale', 'nome', 'cognome']);
|
|
if ($rubrica) {
|
|
$label = trim((string) ($rubrica->ragione_sociale ?: (($rubrica->nome ?? '') . ' ' . ($rubrica->cognome ?? ''))));
|
|
if ($label !== '') {
|
|
return $label;
|
|
}
|
|
}
|
|
}
|
|
|
|
$label = trim((string) ($item->nome_chiamante ?? ''));
|
|
if ($label !== '' && ! $this->isGenericCallerLabel($label, (string) ($item->telefono ?? ''))) {
|
|
return $label;
|
|
}
|
|
}
|
|
|
|
return 'Contatto non in Rubrica';
|
|
}
|
|
|
|
private function resolveGroupedPhoneLabel(array $items): string
|
|
{
|
|
foreach ($items as $item) {
|
|
$phone = trim((string) ($item->telefono ?? ''));
|
|
if ($phone !== '') {
|
|
return $phone;
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
private function buildGroupedCollection($items)
|
|
{
|
|
$groups = [];
|
|
|
|
foreach ($items as $postIt) {
|
|
if ($this->isInternalPostIt($postIt)) {
|
|
continue;
|
|
}
|
|
|
|
$groupKey = $this->buildGroupedPostItKey($postIt);
|
|
if (isset($groups[$groupKey])) {
|
|
$groups[$groupKey]['items'][] = $postIt;
|
|
$groups[$groupKey]['count']++;
|
|
continue;
|
|
}
|
|
|
|
$groups[$groupKey] = [
|
|
'key' => $groupKey,
|
|
'modal_key' => md5($groupKey),
|
|
'items' => [$postIt],
|
|
'count' => 1,
|
|
'latest' => $postIt,
|
|
'caller_label' => $postIt->nome_chiamante ?: 'Chiamante non identificato',
|
|
'phone' => (string) ($postIt->telefono ?? ''),
|
|
'first_at' => optional($postIt->chiamata_il)->format('d/m/Y H:i') ?: '-',
|
|
'last_at' => optional($postIt->chiamata_il)->format('d/m/Y H:i') ?: '-',
|
|
];
|
|
}
|
|
|
|
return collect(array_values($groups))
|
|
->map(function (array $group): array {
|
|
usort($group['items'], function (ChiamataPostIt $left, ChiamataPostIt $right): int {
|
|
return ($right->chiamata_il?->getTimestamp() ?? 0) <=> ($left->chiamata_il?->getTimestamp() ?? 0);
|
|
});
|
|
|
|
$group['latest'] = $this->resolveLatestGroupedPostIt($group['items']);
|
|
$group['primary_post_it'] = $this->resolvePrimaryGroupedPostIt($group['items']);
|
|
$oldest = collect($group['items'])
|
|
->sortBy(fn(ChiamataPostIt $item) => $item->chiamata_il?->getTimestamp() ?? 0)
|
|
->first();
|
|
$group['first_at'] = optional($oldest?->chiamata_il)->format('d/m/Y H:i') ?: '-';
|
|
$group['last_at'] = optional($group['latest']->chiamata_il)->format('d/m/Y H:i') ?: '-';
|
|
$group['caller_label'] = $this->resolveGroupedCallerLabel($group['items']);
|
|
$group['phone'] = $this->resolveGroupedPhoneLabel($group['items']);
|
|
$group['rubrica_url'] = $this->getRubricaUrlByPhone((string) $group['phone']);
|
|
$group['answered_count'] = collect($group['items'])->filter(fn(ChiamataPostIt $item): bool => $this->isAnsweredPostIt($item))->count();
|
|
$group['missed_count'] = collect($group['items'])->filter(fn(ChiamataPostIt $item): bool => $this->isMissedPostIt($item))->count();
|
|
$group['can_create_rubrica'] = ! $group['rubrica_url'] && $this->canCreateRubricaFromPhone((string) $group['phone']);
|
|
$group['primary_action_label'] = $group['answered_count'] > 0 ? 'Apri Post-it' : 'Apri richiamo';
|
|
$group['visible_items'] = collect($group['items'])->take(4)->values()->all();
|
|
$group['hidden_count'] = max(0, count($group['items']) - count($group['visible_items']));
|
|
|
|
return $group;
|
|
})
|
|
->sortByDesc(fn(array $group) => $group['latest']->chiamata_il?->getTimestamp() ?? 0)
|
|
->values();
|
|
}
|
|
|
|
private function resolveOperationalPostItStabileId(CommunicationMessage $message, string $phone): ?int
|
|
{
|
|
if ($this->isInternalSmdrMessage($message)) {
|
|
return (int) ($message->stabile_id ?? 0) ?: null;
|
|
}
|
|
|
|
if ($this->resolveRubricaIdByPhone($phone)) {
|
|
return (int) ($message->stabile_id ?? 0) ?: null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function resolveTecnicoCallerLabel(CommunicationMessage $message, string $phone, ?string $nominativo): string
|
|
{
|
|
$label = trim((string) $nominativo);
|
|
if ($label !== '') {
|
|
return $label;
|
|
}
|
|
|
|
if ($this->isInternalSmdrMessage($message)) {
|
|
$extension = $this->extractExtensionFromMessage($message);
|
|
|
|
return $extension ? 'Interno ' . $extension : 'Interno PBX';
|
|
}
|
|
|
|
return 'Contatto non in Rubrica';
|
|
}
|
|
|
|
private function resolveSmdrLineLabel(CommunicationMessage $message): string
|
|
{
|
|
$stored = trim((string) data_get($message->metadata, 'smdr.line_label', ''));
|
|
if ($stored !== '') {
|
|
return $stored;
|
|
}
|
|
|
|
$rawLine = trim((string) data_get($message->metadata, 'smdr.co', ''));
|
|
if ($rawLine === '') {
|
|
return '-';
|
|
}
|
|
|
|
$routing = app(PbxRoutingService::class)->resolveByIncomingLine($rawLine);
|
|
$lineLabel = trim((string) ($routing['line_label'] ?? ''));
|
|
if ($lineLabel !== '') {
|
|
return $lineLabel;
|
|
}
|
|
|
|
$normalized = app(PbxRoutingService::class)->normalizeLineNumber($rawLine);
|
|
|
|
return self::SMDR_PUBLIC_LINE_FALLBACKS[$normalized] ?? $rawLine;
|
|
}
|
|
|
|
private function isGenericCallerLabel(string $label, string $phone = ''): bool
|
|
{
|
|
$normalizedLabel = mb_strtolower(trim($label));
|
|
if ($normalizedLabel === '') {
|
|
return true;
|
|
}
|
|
|
|
if (in_array($normalizedLabel, ['chiamante non identificato', 'contatto non in rubrica'], true)) {
|
|
return true;
|
|
}
|
|
|
|
if (str_starts_with($normalizedLabel, 'smdr ') || str_starts_with($normalizedLabel, 'panasonic cti ')) {
|
|
return true;
|
|
}
|
|
|
|
$labelDigits = preg_replace('/\D+/', '', $label) ?: '';
|
|
$phoneDigits = preg_replace('/\D+/', '', $phone) ?: '';
|
|
|
|
return $labelDigits !== '' && $phoneDigits !== '' && $labelDigits === $phoneDigits;
|
|
}
|
|
|
|
public function richiediClickToCallDaMessaggio(int $messageId): void
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return;
|
|
}
|
|
|
|
$extension = $this->normalizeExtension((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;
|
|
}
|
|
|
|
$message = CommunicationMessage::query()->find($messageId);
|
|
if (! $message) {
|
|
Notification::make()->title('Messaggio tecnico non trovato')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$phone = preg_replace('/\D+/', '', (string) ($message->phone_number ?: data_get($message->metadata, 'smdr.dial_number', '')));
|
|
if (! is_string($phone) || $phone === '') {
|
|
Notification::make()->title('Numero non valido')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
PbxClickToCallRequest::query()->create([
|
|
'requested_by_user_id' => (int) $user->id,
|
|
'assigned_user_id' => (int) $user->id,
|
|
'stabile_id' => $message->stabile_id,
|
|
'communication_message_id' => (int) $message->id,
|
|
'source_extension' => $extension,
|
|
'target_number' => $phone,
|
|
'status' => 'pending',
|
|
'note' => 'Richiesta da Post-it Gestione [' . $this->getTecnicoProviderLabel($message) . ']',
|
|
'requested_at' => now(),
|
|
'metadata' => [
|
|
'requested_from' => 'post_it_gestione',
|
|
'source_channel' => (string) $message->channel,
|
|
'provider' => (string) data_get($message->metadata, 'provider', $message->channel),
|
|
'target_extension' => (string) ($message->target_extension ?? ''),
|
|
],
|
|
]);
|
|
|
|
Notification::make()
|
|
->title('Richiesta click-to-call registrata')
|
|
->body('Interno ' . $extension . ' -> ' . $phone)
|
|
->success()
|
|
->send();
|
|
}
|
|
|
|
public function getCollaboratoreNomeByExtension(?string $extension): ?string
|
|
{
|
|
$normalized = $this->normalizeExtension($extension);
|
|
if ($normalized === null) {
|
|
return null;
|
|
}
|
|
|
|
if (array_key_exists($normalized, $this->pbxExtensionCache)) {
|
|
return $this->pbxExtensionCache[$normalized]['name'];
|
|
}
|
|
|
|
if (! Schema::hasColumn('users', 'pbx_extension')) {
|
|
$this->pbxExtensionCache[$normalized] = ['id' => null, 'name' => null];
|
|
return null;
|
|
}
|
|
|
|
$user = User::query()
|
|
->select(['id', 'name', 'pbx_extension'])
|
|
->where('pbx_extension', $normalized)
|
|
->first();
|
|
|
|
$result = [
|
|
'id' => $user ? (int) $user->id : null,
|
|
'name' => $user ? trim((string) $user->name) : null,
|
|
];
|
|
|
|
$this->pbxExtensionCache[$normalized] = $result;
|
|
|
|
return $result['name'];
|
|
}
|
|
|
|
public function isInternalSmdrMessage(CommunicationMessage $message): bool
|
|
{
|
|
if ((string) $message->direction === 'internal') {
|
|
return true;
|
|
}
|
|
|
|
$extension = $this->extractExtensionFromMessage($message);
|
|
$phone = preg_replace('/\D+/', '', (string) ($message->phone_number ?: data_get($message->metadata, 'smdr.dial_number', '')));
|
|
|
|
return $extension !== null && $phone !== '' && $phone === $extension;
|
|
}
|
|
|
|
public function getTecnicoInternoLabel(CommunicationMessage $message): string
|
|
{
|
|
$extension = $this->extractExtensionFromMessage($message);
|
|
if ($extension === null) {
|
|
return '-';
|
|
}
|
|
|
|
$name = $this->getCollaboratoreNomeByExtension($extension);
|
|
if ($name === null) {
|
|
return $extension;
|
|
}
|
|
|
|
return $extension . ' - ' . $name;
|
|
}
|
|
|
|
public function getTecnicoScope(CommunicationMessage $message): string
|
|
{
|
|
if ($this->isInternalSmdrMessage($message)) {
|
|
return 'interno';
|
|
}
|
|
|
|
if ($this->isManagedStudioNumberMessage($message)) {
|
|
return 'studio';
|
|
}
|
|
|
|
return 'esterno';
|
|
}
|
|
|
|
public function getTecnicoScopeLabel(CommunicationMessage $message): string
|
|
{
|
|
return match ($this->getTecnicoScope($message)) {
|
|
'interno' => 'Interno collaboratore',
|
|
'studio' => 'Numero studio gestito',
|
|
default => 'Numero esterno',
|
|
};
|
|
}
|
|
|
|
private function resolveRubricaIdByPhone(?string $phone): ?int
|
|
{
|
|
$digits = preg_replace('/\D+/', '', (string) $phone);
|
|
if (! is_string($digits) || $digits === '') {
|
|
return null;
|
|
}
|
|
|
|
if ($this->isShortInternalNumber($digits)) {
|
|
return null;
|
|
}
|
|
|
|
if ($this->isKnownPbxExtension($digits)) {
|
|
return null;
|
|
}
|
|
|
|
if (array_key_exists($digits, $this->rubricaPhoneCache)) {
|
|
return $this->rubricaPhoneCache[$digits];
|
|
}
|
|
|
|
$adminId = (int) (Auth::user()?->amministratore?->id ?? 0);
|
|
$result = app(RubricaPhoneLookupService::class)->findByPhone($digits, $adminId)?->id;
|
|
$result = is_numeric($result) ? (int) $result : null;
|
|
$this->rubricaPhoneCache[$digits] = $result;
|
|
|
|
return $result;
|
|
}
|
|
|
|
private function extractExtensionFromMessage(CommunicationMessage $message): ?string
|
|
{
|
|
$extension = (string) ($message->target_extension ?: data_get($message->metadata, 'smdr.extension', ''));
|
|
|
|
return $this->normalizeExtension($extension);
|
|
}
|
|
|
|
private function normalizeExtension(?string $extension): ?string
|
|
{
|
|
$digits = preg_replace('/\D+/', '', (string) $extension);
|
|
|
|
return is_string($digits) && $digits !== '' ? $digits : null;
|
|
}
|
|
|
|
private function isKnownPbxExtension(string $digits): bool
|
|
{
|
|
if (strlen($digits) > 6 || ! Schema::hasColumn('users', 'pbx_extension')) {
|
|
return false;
|
|
}
|
|
|
|
return User::query()->where('pbx_extension', $digits)->exists();
|
|
}
|
|
|
|
private function isShortInternalNumber(string $digits): bool
|
|
{
|
|
return $digits !== '' && strlen($digits) <= 4;
|
|
}
|
|
|
|
private function canCreateRubricaFromPhone(?string $phone): bool
|
|
{
|
|
$digits = preg_replace('/\D+/', '', (string) $phone) ?: '';
|
|
|
|
return $digits !== '' && ! $this->isShortInternalNumber($digits);
|
|
}
|
|
|
|
private function isLikelyMobilePhone(string $digits): bool
|
|
{
|
|
return str_starts_with($digits, '3') && strlen($digits) >= 9;
|
|
}
|
|
|
|
private function normalizePostItOutcome(?string $outcome): string
|
|
{
|
|
return Str::lower(trim((string) $outcome));
|
|
}
|
|
|
|
public function isAnsweredPostIt(ChiamataPostIt $postIt): bool
|
|
{
|
|
if ($this->isMissedResponseGroupPostIt($postIt)) {
|
|
return false;
|
|
}
|
|
|
|
$outcome = $this->normalizePostItOutcome($postIt->esito);
|
|
|
|
if (($postIt->durata_secondi ?? 0) > 0) {
|
|
return true;
|
|
}
|
|
|
|
return in_array($outcome, ['answered', 'risposta', 'gestita', 'connected', 'ok'], true);
|
|
}
|
|
|
|
private function isMissedPostIt(ChiamataPostIt $postIt): bool
|
|
{
|
|
if ($this->isMissedResponseGroupPostIt($postIt)) {
|
|
return true;
|
|
}
|
|
|
|
$outcome = $this->normalizePostItOutcome($postIt->esito);
|
|
|
|
if (in_array($outcome, ['persa', 'missed', 'no_answer', 'unanswered', 'non_risposta'], true)) {
|
|
return true;
|
|
}
|
|
|
|
return (string) $postIt->direzione === 'in_arrivo' && ! $this->isAnsweredPostIt($postIt) && (int) ($postIt->durata_secondi ?? 0) === 0;
|
|
}
|
|
|
|
private function isMissedResponseGroupPostIt(ChiamataPostIt $postIt): bool
|
|
{
|
|
if ((string) $postIt->direzione !== 'in_arrivo') {
|
|
return false;
|
|
}
|
|
|
|
$message = $this->resolveMessageFromPostIt($postIt);
|
|
if (! $message instanceof CommunicationMessage || (string) $message->channel !== 'smdr') {
|
|
return false;
|
|
}
|
|
|
|
$groupExtension = $this->normalizeExtension((string) data_get($message->metadata, 'smdr.extension', ''));
|
|
if (! $this->isMonitoredResponseGroupExtension($groupExtension)) {
|
|
$groupExtension = $this->normalizeExtension((string) ($message->target_extension ?? ''));
|
|
}
|
|
if (! $this->isMonitoredResponseGroupExtension($groupExtension)) {
|
|
return false;
|
|
}
|
|
|
|
$answerExtensions = array_values(array_unique(array_filter([
|
|
$this->normalizeExtension((string) data_get($message->metadata, 'smdr.target_extension', '')),
|
|
$this->normalizeExtension((string) ($message->target_extension ?? '')),
|
|
])));
|
|
|
|
foreach ($answerExtensions as $answerExtension) {
|
|
if (! $this->isMonitoredResponseGroupExtension($answerExtension)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private function isMonitoredResponseGroupExtension(?string $extension): bool
|
|
{
|
|
if ($extension === null || $extension === '') {
|
|
return false;
|
|
}
|
|
|
|
return in_array($extension, $this->monitoredResponseGroups, true);
|
|
}
|
|
|
|
private function isHiddenVisualizationResponseGroupPostIt(ChiamataPostIt $postIt): bool
|
|
{
|
|
$message = $this->resolveMessageFromPostIt($postIt);
|
|
if (! $message instanceof CommunicationMessage || (string) $message->channel !== 'smdr') {
|
|
return false;
|
|
}
|
|
|
|
$extensions = array_values(array_unique(array_filter([
|
|
$this->normalizeExtension((string) ($message->target_extension ?? '')),
|
|
$this->normalizeExtension((string) data_get($message->metadata, 'smdr.extension', '')),
|
|
$this->normalizeExtension((string) data_get($message->metadata, 'smdr.target_extension', '')),
|
|
])));
|
|
|
|
foreach ($extensions as $extension) {
|
|
if (in_array($extension, $this->hiddenVisualizationResponseGroups, true)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public function getPostItCallStateMeta(ChiamataPostIt $postIt): array
|
|
{
|
|
if ($this->isMissedPostIt($postIt)) {
|
|
return [
|
|
'label' => 'Mancata risposta',
|
|
'class' => 'bg-rose-100 text-rose-800',
|
|
];
|
|
}
|
|
|
|
if ($this->isAnsweredPostIt($postIt)) {
|
|
return [
|
|
'label' => (string) $postIt->direzione === 'in_uscita' ? 'Richiamo effettuato' : 'Risposta registrata',
|
|
'class' => 'bg-emerald-100 text-emerald-800',
|
|
];
|
|
}
|
|
|
|
return [
|
|
'label' => (string) $postIt->direzione === 'in_uscita' ? 'Richiamo da fare' : 'Da verificare',
|
|
'class' => 'bg-amber-100 text-amber-900',
|
|
];
|
|
}
|
|
|
|
/** @param array<int,ChiamataPostIt> $items */
|
|
private function resolvePrimaryGroupedPostIt(array $items): ChiamataPostIt
|
|
{
|
|
$answered = collect($items)
|
|
->filter(fn(ChiamataPostIt $item): bool => $this->isAnsweredPostIt($item))
|
|
->sortByDesc(fn(ChiamataPostIt $item) => $item->chiamata_il?->getTimestamp() ?? 0)
|
|
->values();
|
|
|
|
if ($answered->isNotEmpty()) {
|
|
return $answered->first();
|
|
}
|
|
|
|
return $this->resolveLatestGroupedPostIt($items);
|
|
}
|
|
|
|
private function findGroupedCollectionByModalKey(string $modalKey, string $mode = 'open'): ?array
|
|
{
|
|
$groups = $mode === 'closed'
|
|
? $this->recentiChiusiRaggruppati
|
|
: $this->recentiRaggruppati;
|
|
|
|
$group = $groups->firstWhere('modal_key', $modalKey);
|
|
|
|
return is_array($group) ? $group : null;
|
|
}
|
|
|
|
private function buildRubricaDraftIdentity(string $label, string $phoneDigits): array
|
|
{
|
|
$cleanLabel = trim(preg_replace('/\s+/', ' ', $label) ?? '');
|
|
$cleanLabel = $cleanLabel !== '' && $cleanLabel !== 'Chiamante non identificato'
|
|
? $cleanLabel
|
|
: ('Nuovo contatto ' . $phoneDigits);
|
|
|
|
if (str_contains($cleanLabel, '/') || preg_match('/\b(srl|sas|spa|snc|studio|condominio|associazione|impresa)\b/i', $cleanLabel) === 1) {
|
|
return [
|
|
'tipo_contatto' => 'persona_giuridica',
|
|
'nome' => null,
|
|
'cognome' => null,
|
|
'ragione_sociale' => $cleanLabel,
|
|
];
|
|
}
|
|
|
|
$parts = preg_split('/\s+/', $cleanLabel) ?: [];
|
|
if (count($parts) >= 2) {
|
|
$nome = array_shift($parts);
|
|
|
|
return [
|
|
'tipo_contatto' => 'persona_fisica',
|
|
'nome' => $nome,
|
|
'cognome' => implode(' ', $parts),
|
|
'ragione_sociale' => null,
|
|
];
|
|
}
|
|
|
|
return [
|
|
'tipo_contatto' => 'persona_fisica',
|
|
'nome' => $cleanLabel,
|
|
'cognome' => null,
|
|
'ragione_sociale' => null,
|
|
];
|
|
}
|
|
|
|
private function isManagedStudioNumberMessage(CommunicationMessage $message): bool
|
|
{
|
|
$phoneDigits = preg_replace('/\D+/', '', (string) ($message->phone_number ?: data_get($message->metadata, 'smdr.dial_number', '')));
|
|
if (! is_string($phoneDigits) || $phoneDigits === '') {
|
|
return false;
|
|
}
|
|
|
|
return in_array($phoneDigits, $this->getManagedStudioNumbers(), true);
|
|
}
|
|
|
|
/** @return array<int,string> */
|
|
private function getManagedStudioNumbers(): array
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user) {
|
|
return [];
|
|
}
|
|
|
|
$impostazioni = (array) (($user->amministratore?->impostazioni ?? []));
|
|
$centralino = (array) ($impostazioni['centralino'] ?? []);
|
|
|
|
$numbers = [];
|
|
foreach (['numero_principale', 'numero_backup', 'numero_emergenza'] as $key) {
|
|
$digits = preg_replace('/\D+/', '', (string) ($centralino[$key] ?? ''));
|
|
if (is_string($digits) && $digits !== '') {
|
|
$numbers[] = $digits;
|
|
}
|
|
}
|
|
|
|
return array_values(array_unique($numbers));
|
|
}
|
|
|
|
public static function canAccess(): bool
|
|
{
|
|
$user = Auth::user();
|
|
|
|
return $user instanceof User
|
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
|
}
|
|
}
|