Add PBX topbar and mobile ticket UX
This commit is contained in:
parent
b0bdf1a020
commit
4ed0b392d1
|
|
@ -2,6 +2,7 @@
|
|||
namespace App\Filament\Pages\Impostazioni;
|
||||
|
||||
use App\Models\Amministratore;
|
||||
use App\Models\CommunicationMessage;
|
||||
use App\Models\Fornitore;
|
||||
use App\Models\FornitoreDipendente;
|
||||
use App\Models\Stabile;
|
||||
|
|
@ -718,6 +719,128 @@ public function form(Schema $schema): Schema
|
|||
]),
|
||||
]),
|
||||
|
||||
Tab::make('PBX / TAPI')
|
||||
->schema([
|
||||
Section::make('Profilo centralino e bridge')
|
||||
->columns(3)
|
||||
->schema([
|
||||
Select::make('impostazioni.pbx.provider')
|
||||
->label('Tipo centralino')
|
||||
->options([
|
||||
'panasonic_ns1000' => 'Panasonic NS1000',
|
||||
'panasonic_generic' => 'Panasonic generico',
|
||||
'wildix' => 'Wildix',
|
||||
'3cx' => '3CX',
|
||||
'custom' => 'Altro / custom',
|
||||
])
|
||||
->default('panasonic_ns1000'),
|
||||
Select::make('impostazioni.pbx.channel')
|
||||
->label('Canale CRM')
|
||||
->options([
|
||||
'panasonic_csta' => 'Panasonic CSTA / CTI',
|
||||
'tapi' => 'Windows TAPI',
|
||||
'hybrid' => 'Ibrido TAPI + CSTA',
|
||||
])
|
||||
->default('hybrid'),
|
||||
Select::make('impostazioni.pbx.bridge_mode')
|
||||
->label('Modalita bridge')
|
||||
->options([
|
||||
'group-first' => 'Group-first',
|
||||
'extension-first' => 'Extension-first',
|
||||
'none' => 'Solo monitoraggio',
|
||||
])
|
||||
->default('group-first'),
|
||||
TextInput::make('impostazioni.pbx.tapi_host')
|
||||
->label('Host Windows watcher')
|
||||
->maxLength(255)
|
||||
->placeholder('Es. ws-panasonic-01'),
|
||||
TextInput::make('impostazioni.pbx.tapi_provider')
|
||||
->label('Provider TAPI / TSP')
|
||||
->maxLength(255)
|
||||
->default('CTSP0000.tsp'),
|
||||
TextInput::make('impostazioni.pbx.watch_extensions')
|
||||
->label('Interni monitorati')
|
||||
->maxLength(255)
|
||||
->placeholder('201,205,206,601,603')
|
||||
->helperText('Lista CSV degli interni/gruppi che il watcher Windows deve osservare.'),
|
||||
Toggle::make('impostazioni.pbx.popup_enabled')
|
||||
->label('Popup CRM attivi')
|
||||
->default(true),
|
||||
Toggle::make('impostazioni.pbx.click_to_call_enabled')
|
||||
->label('Click-to-call attivo')
|
||||
->default(true),
|
||||
Textarea::make('impostazioni.pbx.routing_notes')
|
||||
->label('Note routing PBX')
|
||||
->rows(3)
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
|
||||
Section::make('Gruppi di risposta')
|
||||
->schema([
|
||||
Repeater::make('impostazioni.pbx.response_groups')
|
||||
->label('Gruppi di risposta Panasonic')
|
||||
->defaultItems(0)
|
||||
->schema([
|
||||
TextInput::make('extension')
|
||||
->label('Interno gruppo')
|
||||
->maxLength(20)
|
||||
->placeholder('601'),
|
||||
TextInput::make('label')
|
||||
->label('Descrizione')
|
||||
->maxLength(120)
|
||||
->placeholder('Gruppo giorno'),
|
||||
TextInput::make('mode')
|
||||
->label('Modalita')
|
||||
->maxLength(60)
|
||||
->placeholder('giorno / notte / backup'),
|
||||
TextInput::make('target_extension')
|
||||
->label('Interno di fallback')
|
||||
->maxLength(20)
|
||||
->placeholder('205'),
|
||||
])
|
||||
->columns(4)
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
|
||||
Section::make('Linee entranti e DID')
|
||||
->schema([
|
||||
Repeater::make('impostazioni.pbx.incoming_lines')
|
||||
->label('Linee in entrata')
|
||||
->defaultItems(0)
|
||||
->schema([
|
||||
TextInput::make('number')
|
||||
->label('Numero / DID')
|
||||
->maxLength(40)
|
||||
->placeholder('0811234567'),
|
||||
TextInput::make('label')
|
||||
->label('Descrizione linea')
|
||||
->maxLength(120)
|
||||
->placeholder('Linea assistenza'),
|
||||
TextInput::make('route_group')
|
||||
->label('Gruppo target')
|
||||
->maxLength(20)
|
||||
->placeholder('601'),
|
||||
TextInput::make('target_extension')
|
||||
->label('Interno target')
|
||||
->maxLength(20)
|
||||
->placeholder('205'),
|
||||
Textarea::make('notes')
|
||||
->label('Note')
|
||||
->rows(2)
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->columns(4)
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
|
||||
Section::make('Cruscotto chiamate Panasonic / TAPI')
|
||||
->schema([
|
||||
Placeholder::make('pbx_tapi_panel')
|
||||
->hiddenLabel()
|
||||
->content(fn() => view('filament.pages.impostazioni.partials.scheda-amministratore-pbx-tapi')),
|
||||
]),
|
||||
]),
|
||||
|
||||
Tab::make('Operativita / Accessi')
|
||||
->schema([
|
||||
Section::make('Azioni operative e gestione accessi')
|
||||
|
|
@ -729,7 +852,7 @@ public function form(Schema $schema): Schema
|
|||
]),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function runProductionUpgrade(): void
|
||||
{
|
||||
|
|
@ -1189,6 +1312,219 @@ public function getCollaboratoriCentralinoRowsProperty(): array
|
|||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{status:string,label:string,provider:string,channel:string,bridge_mode:string,watched_extensions:array<int,string>,response_groups:array<int,array<string,mixed>>,incoming_lines:array<int,array<string,mixed>>,last_event_at:?string,last_event_type:?string,total_messages:int,last_24h:int,missed_calls:int,assessment:string}
|
||||
*/
|
||||
public function getPbxSummaryProperty(): array
|
||||
{
|
||||
$settings = (array) Arr::get($this->amministratore->impostazioni ?? [], 'pbx', []);
|
||||
$provider = (string) ($settings['provider'] ?? 'panasonic_ns1000');
|
||||
$channel = (string) ($settings['channel'] ?? 'hybrid');
|
||||
$bridgeMode = (string) ($settings['bridge_mode'] ?? 'group-first');
|
||||
$watchedExtensions = $this->resolveWatchedPbxExtensions($settings);
|
||||
$responseGroups = array_values(array_filter((array) ($settings['response_groups'] ?? []), fn($row): bool => is_array($row)));
|
||||
$incomingLines = array_values(array_filter((array) ($settings['incoming_lines'] ?? []), fn($row): bool => is_array($row)));
|
||||
|
||||
if (! Schema::hasTable('communication_messages')) {
|
||||
return [
|
||||
'status' => ($watchedExtensions !== [] || $responseGroups !== [] || $incomingLines !== []) ? 'configured' : 'to-configure',
|
||||
'label' => ($watchedExtensions !== [] || $responseGroups !== [] || $incomingLines !== []) ? 'Configurato, in attesa eventi' : 'Da configurare',
|
||||
'provider' => $provider,
|
||||
'channel' => $channel,
|
||||
'bridge_mode' => $bridgeMode,
|
||||
'watched_extensions' => $watchedExtensions,
|
||||
'response_groups' => $responseGroups,
|
||||
'incoming_lines' => $incomingLines,
|
||||
'last_event_at' => null,
|
||||
'last_event_type' => null,
|
||||
'total_messages' => 0,
|
||||
'last_24h' => 0,
|
||||
'missed_calls' => 0,
|
||||
'assessment' => 'La tabella communication_messages non e disponibile in questo ambiente, quindi il cruscotto mostra solo la configurazione PBX.',
|
||||
];
|
||||
}
|
||||
|
||||
$query = $this->pbxMessagesQuery();
|
||||
$latest = (clone $query)->orderByDesc('received_at')->orderByDesc('id')->first();
|
||||
$total = (clone $query)->count();
|
||||
$last24h = (clone $query)->where('received_at', '>=', now()->subDay())->count();
|
||||
$missedCalls = (clone $query)->where(function ($q): void {
|
||||
$q->where('message_text', 'like', '%persa%')
|
||||
->orWhere('metadata->outcome', 'like', '%miss%')
|
||||
->orWhere('metadata->outcome', 'like', '%no_answer%');
|
||||
})->count();
|
||||
|
||||
$status = 'to-configure';
|
||||
$label = 'Da configurare';
|
||||
if ($total > 0) {
|
||||
$status = 'active';
|
||||
$label = 'CRM collegato';
|
||||
} elseif ($watchedExtensions !== [] || $responseGroups !== [] || $incomingLines !== []) {
|
||||
$status = 'configured';
|
||||
$label = 'Configurato, in attesa eventi';
|
||||
}
|
||||
|
||||
$assessment = $bridgeMode === 'group-first'
|
||||
? 'Con modalita group-first il watcher Windows puo vedere eventi sugli interni singoli ma il bridge usa come sorgente autorevole i gruppi di risposta. Questo e coerente con i log Panasonic gia acquisiti.'
|
||||
: 'La configurazione e predisposta per usare gli eventi PBX direttamente nel CRM. Verifica i log del watcher Windows per confermare il flusso sul canale selezionato.';
|
||||
|
||||
return [
|
||||
'status' => $status,
|
||||
'label' => $label,
|
||||
'provider' => $provider,
|
||||
'channel' => $channel,
|
||||
'bridge_mode' => $bridgeMode,
|
||||
'watched_extensions'=> $watchedExtensions,
|
||||
'response_groups' => $responseGroups,
|
||||
'incoming_lines' => $incomingLines,
|
||||
'last_event_at' => $latest?->received_at?->format('d/m/Y H:i:s'),
|
||||
'last_event_type' => $latest ? (string) data_get($latest->metadata, 'event_type', $latest->status) : null,
|
||||
'total_messages' => $total,
|
||||
'last_24h' => $last24h,
|
||||
'missed_calls' => $missedCalls,
|
||||
'assessment' => $assessment,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
public function getPbxRecentMessagesProperty(): array
|
||||
{
|
||||
if (! Schema::hasTable('communication_messages')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->pbxMessagesQuery()
|
||||
->with('assignedUser:id,name')
|
||||
->orderByDesc('received_at')
|
||||
->orderByDesc('id')
|
||||
->limit(12)
|
||||
->get(['id', 'phone_number', 'target_extension', 'assigned_user_id', 'direction', 'status', 'received_at', 'metadata'])
|
||||
->map(fn(CommunicationMessage $message): array => [
|
||||
'id' => (int) $message->id,
|
||||
'received_at' => $message->received_at?->format('d/m/Y H:i:s') ?? '-',
|
||||
'phone_number' => (string) ($message->phone_number ?? '-'),
|
||||
'target_extension' => (string) ($message->target_extension ?? data_get($message->metadata, 'called_extension', '-')),
|
||||
'direction' => (string) ($message->direction ?? '-'),
|
||||
'status' => (string) ($message->status ?? '-'),
|
||||
'event_type' => (string) data_get($message->metadata, 'event_type', '-'),
|
||||
'provider' => (string) data_get($message->metadata, 'provider', $message->channel),
|
||||
'user' => (string) ($message->assignedUser?->name ?? '-'),
|
||||
'studio_role' => (string) data_get($message->metadata, 'studio_role', '-'),
|
||||
'studio_mode' => (string) data_get($message->metadata, 'studio_mode', '-'),
|
||||
])
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,array<string,string>>
|
||||
*/
|
||||
public function getPbxRoutingRowsProperty(): array
|
||||
{
|
||||
$settings = (array) Arr::get($this->amministratore->impostazioni ?? [], 'pbx', []);
|
||||
$centralino = (array) Arr::get($this->amministratore->impostazioni ?? [], 'centralino', []);
|
||||
$groupRows = array_values(array_filter((array) ($settings['response_groups'] ?? []), fn($row): bool => is_array($row)));
|
||||
$incoming = array_values(array_filter((array) ($settings['incoming_lines'] ?? []), fn($row): bool => is_array($row)));
|
||||
$routingRows = [
|
||||
[
|
||||
'type' => 'Studio',
|
||||
'label' => 'Interno centralino',
|
||||
'extension' => (string) Arr::get($centralino, 'interno_centralino', '-'),
|
||||
'target' => 'Smistamento studio',
|
||||
'note' => (string) Arr::get($centralino, 'numero_principale', '-'),
|
||||
],
|
||||
[
|
||||
'type' => 'Studio',
|
||||
'label' => 'Operatore',
|
||||
'extension' => (string) Arr::get($centralino, 'interno_operatore', '-'),
|
||||
'target' => 'Utente operatore',
|
||||
'note' => 'Instradamento diretto',
|
||||
],
|
||||
[
|
||||
'type' => 'Studio',
|
||||
'label' => 'Amministratore',
|
||||
'extension' => (string) Arr::get($centralino, 'interno_amministratore', '-'),
|
||||
'target' => 'Utente amministratore',
|
||||
'note' => 'Instradamento diretto',
|
||||
],
|
||||
[
|
||||
'type' => 'Gruppo',
|
||||
'label' => 'Gruppo giorno',
|
||||
'extension' => (string) Arr::get($centralino, 'interno_gruppo_giorno', '-'),
|
||||
'target' => 'Modalita giorno',
|
||||
'note' => 'Routing shared',
|
||||
],
|
||||
[
|
||||
'type' => 'Gruppo',
|
||||
'label' => 'Gruppo notte',
|
||||
'extension' => (string) Arr::get($centralino, 'interno_gruppo_notte', '-'),
|
||||
'target' => 'Modalita notte',
|
||||
'note' => 'Routing shared',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($groupRows as $row) {
|
||||
$routingRows[] = [
|
||||
'type' => 'Gruppo',
|
||||
'label' => (string) ($row['label'] ?? 'Gruppo risposta'),
|
||||
'extension' => (string) ($row['extension'] ?? '-'),
|
||||
'target' => (string) ($row['target_extension'] ?? '-'),
|
||||
'note' => (string) ($row['mode'] ?? '-'),
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($incoming as $row) {
|
||||
$routingRows[] = [
|
||||
'type' => 'Linea',
|
||||
'label' => (string) ($row['label'] ?? 'Linea entrante'),
|
||||
'extension' => (string) ($row['number'] ?? '-'),
|
||||
'target' => trim((string) (($row['route_group'] ?? '') !== '' ? $row['route_group'] : ($row['target_extension'] ?? '-'))),
|
||||
'note' => (string) ($row['notes'] ?? '-'),
|
||||
];
|
||||
}
|
||||
|
||||
return $routingRows;
|
||||
}
|
||||
|
||||
private function pbxMessagesQuery()
|
||||
{
|
||||
$extensions = $this->resolveWatchedPbxExtensions((array) Arr::get($this->amministratore->impostazioni ?? [], 'pbx', []));
|
||||
|
||||
return CommunicationMessage::query()
|
||||
->where('channel', 'panasonic_csta')
|
||||
->where(function ($q) use ($extensions): void {
|
||||
$q->where('metadata->amministratore_id', (int) $this->amministratore->id);
|
||||
|
||||
if ($extensions !== [] && Schema::hasColumn('communication_messages', 'target_extension')) {
|
||||
$q->orWhereIn('target_extension', $extensions);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,string>
|
||||
*/
|
||||
private function resolveWatchedPbxExtensions(array $settings): array
|
||||
{
|
||||
$centralinoExtensions = [
|
||||
(string) Arr::get($this->amministratore->impostazioni ?? [], 'centralino.interno_centralino', ''),
|
||||
(string) Arr::get($this->amministratore->impostazioni ?? [], 'centralino.interno_operatore', ''),
|
||||
(string) Arr::get($this->amministratore->impostazioni ?? [], 'centralino.interno_amministratore', ''),
|
||||
(string) Arr::get($this->amministratore->impostazioni ?? [], 'centralino.interno_gruppo_giorno', ''),
|
||||
(string) Arr::get($this->amministratore->impostazioni ?? [], 'centralino.interno_gruppo_notte', ''),
|
||||
];
|
||||
|
||||
$csvExtensions = preg_split('/\s*,\s*/', (string) ($settings['watch_extensions'] ?? ''), -1, PREG_SPLIT_NO_EMPTY) ?: [];
|
||||
$groupExtensions = array_map(fn($row): string => (string) ($row['extension'] ?? ''), array_filter((array) ($settings['response_groups'] ?? []), fn($row): bool => is_array($row)));
|
||||
$lineTargets = array_map(fn($row): string => (string) (($row['target_extension'] ?? '') !== '' ? $row['target_extension'] : ($row['route_group'] ?? '')), array_filter((array) ($settings['incoming_lines'] ?? []), fn($row): bool => is_array($row)));
|
||||
|
||||
return array_values(array_unique(array_filter(array_map(
|
||||
fn($value): string => preg_replace('/\D+/', '', (string) $value) ?: '',
|
||||
array_merge($centralinoExtensions, $csvExtensions, $groupExtensions, $lineTargets)
|
||||
))));
|
||||
}
|
||||
|
||||
public function salvaInternoCollaboratore(int $userId): void
|
||||
{
|
||||
$user = User::query()->find($userId);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
use App\Filament\Pages\Gescon\RubricaUniversaleScheda;
|
||||
use App\Models\ChiamataPostIt;
|
||||
use App\Models\CommunicationMessage;
|
||||
use App\Models\PbxClickToCallRequest;
|
||||
use App\Models\RubricaUniversale;
|
||||
use App\Models\Ticket;
|
||||
use App\Models\User;
|
||||
|
|
@ -214,9 +215,16 @@ public function getChiamateTecnicheProperty()
|
|||
try {
|
||||
$query = CommunicationMessage::query()
|
||||
->with(['assignedUser:id,name', 'stabile:id,denominazione'])
|
||||
->whereIn('channel', ['smdr', 'panasonic_csta'])
|
||||
->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') {
|
||||
|
|
@ -251,6 +259,32 @@ public function getChiamateTecnicheProperty()
|
|||
}
|
||||
}
|
||||
|
||||
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()) {
|
||||
|
|
@ -268,10 +302,10 @@ public function creaPostItDaMessaggio(int $messageId): void
|
|||
return;
|
||||
}
|
||||
|
||||
$sourceKey = (string) ($message->channel ?: 'cti');
|
||||
$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 ?? ''));
|
||||
$smdr = (array) data_get($message->metadata, 'smdr', []);
|
||||
$line = trim((string) ($message->message_text ?? ''));
|
||||
|
||||
$postIt = ChiamataPostIt::query()->create([
|
||||
'stabile_id' => $message->stabile_id,
|
||||
|
|
@ -355,6 +389,86 @@ public function getTecnicoNominativo(CommunicationMessage $message): ?string
|
|||
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') {
|
||||
$line = trim((string) data_get($message->metadata, 'smdr.co', ''));
|
||||
return $line !== '' ? $line : '-';
|
||||
}
|
||||
|
||||
$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 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);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
use App\Models\User;
|
||||
use App\Services\Documenti\ImageTextExtractionService;
|
||||
use App\Services\Documenti\PdfTextExtractionService;
|
||||
use App\Services\Support\TicketAttachmentUploadService;
|
||||
use App\Support\StabileContext;
|
||||
use BackedEnum;
|
||||
use Filament\Notifications\Notification;
|
||||
|
|
@ -540,21 +541,16 @@ public function caricaAllegati(): void
|
|||
continue;
|
||||
}
|
||||
|
||||
$path = $file->store('ticket-gestione/' . $ticket->id, 'public');
|
||||
$mime = TicketAttachment::normalizeMimeType(
|
||||
method_exists($file, 'getClientMimeType') ? (string) $file->getClientMimeType() : '',
|
||||
method_exists($file, 'getClientOriginalName') ? (string) $file->getClientOriginalName() : '',
|
||||
$path
|
||||
);
|
||||
$stored = app(TicketAttachmentUploadService::class)->store($file, 'ticket-gestione/' . $ticket->id);
|
||||
|
||||
$attachment = TicketAttachment::query()->create([
|
||||
'ticket_id' => $ticket->id,
|
||||
'ticket_update_id' => null,
|
||||
'user_id' => Auth::id(),
|
||||
'file_path' => $path,
|
||||
'original_file_name' => (string) $file->getClientOriginalName(),
|
||||
'mime_type' => $mime,
|
||||
'size' => (int) $file->getSize(),
|
||||
'file_path' => $stored['path'],
|
||||
'original_file_name' => $stored['original_name'],
|
||||
'mime_type' => $stored['mime'],
|
||||
'size' => $stored['size'],
|
||||
'description' => $this->descrizioneAllegati ?: 'Allegato da gestione ticket',
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -5,15 +5,14 @@
|
|||
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\Services\Cti\LiveIncomingCallService;
|
||||
use App\Services\Support\TicketAttachmentUploadService;
|
||||
use App\Support\StabileContext;
|
||||
use BackedEnum;
|
||||
use Filament\Notifications\Notification;
|
||||
|
|
@ -130,6 +129,7 @@ public function mount(): void
|
|||
}
|
||||
|
||||
$this->refreshLiveCallBanner();
|
||||
$this->applyLiveCallQueryPrefill();
|
||||
$this->refreshData();
|
||||
}
|
||||
|
||||
|
|
@ -160,54 +160,8 @@ public function refreshLiveCallBanner(): void
|
|||
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 !== '';
|
||||
$this->liveIncomingCall = app(LiveIncomingCallService::class)->getForUser($authUser);
|
||||
$this->liveCallCanClickToCall = (bool) ($this->liveIncomingCall['can_click_to_call'] ?? false);
|
||||
}
|
||||
|
||||
public function creaPostItDaChiamataInIngresso(): void
|
||||
|
|
@ -322,6 +276,10 @@ public function richiediClickToCallDaInterno(): void
|
|||
|
||||
public function getLiveRubricaUrl(): ?string
|
||||
{
|
||||
if (! empty($this->liveIncomingCall['rubrica_url'])) {
|
||||
return (string) $this->liveIncomingCall['rubrica_url'];
|
||||
}
|
||||
|
||||
$rubricaId = (int) ($this->liveIncomingCall['rubrica_id'] ?? 0);
|
||||
if ($rubricaId <= 0) {
|
||||
return null;
|
||||
|
|
@ -332,6 +290,10 @@ public function getLiveRubricaUrl(): ?string
|
|||
|
||||
public function getLiveEstrattoContoUrl(): ?string
|
||||
{
|
||||
if (! empty($this->liveIncomingCall['estratto_conto_url'])) {
|
||||
return (string) $this->liveIncomingCall['estratto_conto_url'];
|
||||
}
|
||||
|
||||
$soggettoId = (int) ($this->liveIncomingCall['soggetto_id'] ?? 0);
|
||||
if ($soggettoId <= 0) {
|
||||
return null;
|
||||
|
|
@ -345,6 +307,16 @@ public function getRubricaFilamentUrl(): string
|
|||
return route('filament.admin-filament.pages.gescon.anagrafica.rubrica-universale');
|
||||
}
|
||||
|
||||
public function getAdminMobileHubUrl(): string
|
||||
{
|
||||
return route('admin.mobile');
|
||||
}
|
||||
|
||||
public function getCondominoMobileTicketUrl(): string
|
||||
{
|
||||
return route('condomino.tickets.mobile');
|
||||
}
|
||||
|
||||
public function getDashboardFilamentUrl(): string
|
||||
{
|
||||
return route('filament.admin-filament.pages.dashboard');
|
||||
|
|
@ -701,16 +673,16 @@ private function salvaAllegatiTicket(Ticket $ticket, int $userId): int
|
|||
}
|
||||
|
||||
try {
|
||||
$path = $file->store('ticket-mobile/' . $ticket->id, 'public');
|
||||
$stored = app(TicketAttachmentUploadService::class)->store($file, 'ticket-mobile/' . $ticket->id);
|
||||
|
||||
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),
|
||||
'file_path' => $stored['path'],
|
||||
'original_file_name' => $stored['original_name'],
|
||||
'mime_type' => $stored['mime'],
|
||||
'size' => $stored['size'],
|
||||
'description' => $this->resolveAttachmentDescription((int) $index),
|
||||
]);
|
||||
|
||||
|
|
@ -781,55 +753,22 @@ private function normalizePhoneDigits(string $value): string
|
|||
return preg_replace('/\D+/', '', $value) ?: '';
|
||||
}
|
||||
|
||||
private function matchRubricaByPhone(string $digits): ?RubricaUniversale
|
||||
private function applyLiveCallQueryPrefill(): void
|
||||
{
|
||||
if ($digits === '') {
|
||||
return null;
|
||||
$phone = $this->normalizePhoneDigits((string) request()->query('live_phone', ''));
|
||||
if ($phone === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
||||
if (! $this->liveIncomingCall) {
|
||||
$this->liveIncomingCall = [
|
||||
'message_id' => (int) request()->query('live_message_id', 0),
|
||||
'phone' => $phone,
|
||||
'line' => (string) request()->query('live_note', ''),
|
||||
];
|
||||
}
|
||||
|
||||
$tail = strlen($phone) >= 7 ? substr($phone, -7) : $phone;
|
||||
if ($tail === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Soggetto::query()->where('telefono', 'like', '%' . $tail . '%')->first();
|
||||
$this->prefillTicketFromLiveCall($phone, (string) request()->query('live_note', ''));
|
||||
}
|
||||
|
||||
public function excerpt(?string $text, int $limit = 160): string
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
use App\Models\TicketAttachment;
|
||||
use App\Models\TicketIntervento;
|
||||
use App\Models\User;
|
||||
use App\Services\Support\TicketAttachmentUploadService;
|
||||
use App\Support\StabileContext;
|
||||
use BackedEnum;
|
||||
use Filament\Notifications\Notification;
|
||||
|
|
@ -218,16 +219,16 @@ public function caricaAllegati(): void
|
|||
continue;
|
||||
}
|
||||
|
||||
$path = $file->store('ticket-scheda/' . $ticket->id, 'public');
|
||||
$stored = app(TicketAttachmentUploadService::class)->store($file, 'ticket-scheda/' . $ticket->id);
|
||||
|
||||
TicketAttachment::query()->create([
|
||||
'ticket_id' => $ticket->id,
|
||||
'ticket_update_id' => null,
|
||||
'user_id' => Auth::id(),
|
||||
'file_path' => $path,
|
||||
'original_file_name' => (string) $file->getClientOriginalName(),
|
||||
'mime_type' => (string) $file->getClientMimeType(),
|
||||
'size' => (int) $file->getSize(),
|
||||
'file_path' => $stored['path'],
|
||||
'original_file_name' => $stored['original_name'],
|
||||
'mime_type' => $stored['mime'],
|
||||
'size' => $stored['size'],
|
||||
'description' => $this->descrizioneAllegati ?: 'Allegato da scheda ticket',
|
||||
]);
|
||||
|
||||
|
|
|
|||
79
app/Livewire/Filament/TopbarLiveCall.php
Normal file
79
app/Livewire/Filament/TopbarLiveCall.php
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Filament;
|
||||
|
||||
use App\Models\ChiamataPostIt;
|
||||
use App\Models\User;
|
||||
use App\Services\Cti\LiveIncomingCallService;
|
||||
use App\Support\StabileContext;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Component;
|
||||
|
||||
class TopbarLiveCall extends Component
|
||||
{
|
||||
/** @var array<string,mixed>|null */
|
||||
public ?array $liveIncomingCall = null;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->refreshLiveIncomingCall();
|
||||
}
|
||||
|
||||
public function refreshLiveIncomingCall(): void
|
||||
{
|
||||
$user = Auth::user();
|
||||
if (! $user instanceof User) {
|
||||
$this->liveIncomingCall = null;
|
||||
return;
|
||||
}
|
||||
|
||||
$this->liveIncomingCall = app(LiveIncomingCallService::class)->getForUser($user);
|
||||
}
|
||||
|
||||
public function createPostIt(): void
|
||||
{
|
||||
$user = Auth::user();
|
||||
if (! $user instanceof User || ! $this->liveIncomingCall) {
|
||||
return;
|
||||
}
|
||||
|
||||
$stabileId = StabileContext::resolveActiveStabileId($user);
|
||||
if (! $stabileId) {
|
||||
Notification::make()->title('Seleziona prima uno stabile attivo')->warning()->send();
|
||||
return;
|
||||
}
|
||||
|
||||
ChiamataPostIt::query()->create([
|
||||
'stabile_id' => (int) $stabileId,
|
||||
'creato_da_user_id' => (int) $user->id,
|
||||
'rubrica_id' => (int) ($this->liveIncomingCall['rubrica_id'] ?? 0) ?: null,
|
||||
'telefono' => (string) ($this->liveIncomingCall['phone'] ?? ''),
|
||||
'nome_chiamante' => (string) (($this->liveIncomingCall['rubrica_nome'] ?? '') ?: ('Chiamata ' . ($this->liveIncomingCall['phone'] ?? ''))),
|
||||
'direzione' => 'in_arrivo',
|
||||
'origine' => 'topbar_live_call',
|
||||
'origine_id' => (string) ($this->liveIncomingCall['message_id'] ?? ''),
|
||||
'oggetto' => 'Chiamata in arrivo da valutare',
|
||||
'nota' => (string) ($this->liveIncomingCall['line'] ?? ('Chiamata in ingresso da ' . ($this->liveIncomingCall['phone'] ?? ''))),
|
||||
'priorita' => 'Media',
|
||||
'stato' => 'post_it',
|
||||
'chiamata_il' => now(),
|
||||
]);
|
||||
|
||||
Notification::make()->title('Post-it creato dalla testata')->success()->send();
|
||||
}
|
||||
|
||||
public function openTicketMobile()
|
||||
{
|
||||
if (! $this->liveIncomingCall || empty($this->liveIncomingCall['ticket_mobile_url'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return redirect()->to((string) $this->liveIncomingCall['ticket_mobile_url']);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.filament.topbar-live-call');
|
||||
}
|
||||
}
|
||||
|
|
@ -55,6 +55,16 @@ public function panel(Panel $panel): Panel
|
|||
->visible(function () {
|
||||
$user = Auth::user();
|
||||
|
||||
return $user instanceof \App\Models\User
|
||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||
}),
|
||||
NavigationItem::make('Cellulare / WebApp')
|
||||
->group('NetGescon')
|
||||
->icon('heroicon-o-phone')
|
||||
->url(fn() => route('admin.mobile'))
|
||||
->visible(function () {
|
||||
$user = Auth::user();
|
||||
|
||||
return $user instanceof \App\Models\User
|
||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||
}),
|
||||
|
|
|
|||
134
app/Services/Cti/LiveIncomingCallService.php
Normal file
134
app/Services/Cti/LiveIncomingCallService.php
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Cti;
|
||||
|
||||
use App\Filament\Pages\Supporto\TicketMobile;
|
||||
use App\Models\CommunicationMessage;
|
||||
use App\Models\RubricaUniversale;
|
||||
use App\Models\Soggetto;
|
||||
use App\Models\User;
|
||||
|
||||
class LiveIncomingCallService
|
||||
{
|
||||
/**
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
public function getForUser(User $user): ?array
|
||||
{
|
||||
$userExtension = trim((string) ($user->pbx_extension ?? ''));
|
||||
$watchedExtensions = app(PbxRoutingService::class)->getWatchedExtensionsForUser($user);
|
||||
$popupRestricted = (bool) ($user->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 ($query) use ($watchedExtensions): void {
|
||||
$query->whereIn('target_extension', $watchedExtensions);
|
||||
})
|
||||
->latest('id')
|
||||
->first(['id', 'phone_number', 'target_extension', 'received_at', 'message_text', 'metadata']);
|
||||
|
||||
if (! $latest) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$phone = $this->normalizePhoneDigits((string) ($latest->phone_number ?? ''));
|
||||
if ($phone === '') {
|
||||
$phone = $this->normalizePhoneDigits((string) data_get($latest->metadata, 'smdr.dial_number', ''));
|
||||
}
|
||||
|
||||
if ($phone === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$rubrica = $this->matchRubricaByPhone($phone);
|
||||
$soggetto = $this->matchSoggetto($rubrica, $phone);
|
||||
|
||||
return [
|
||||
'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),
|
||||
'can_click_to_call' => (bool) ($user->pbx_click_to_call_enabled ?? false) && $userExtension !== '',
|
||||
'ticket_mobile_url' => TicketMobile::getUrl([
|
||||
'live_phone' => $phone,
|
||||
'live_message_id' => (int) $latest->id,
|
||||
'live_note' => (string) ($latest->message_text ?? ''),
|
||||
], panel: 'admin-filament'),
|
||||
'rubrica_url' => (int) ($rubrica?->id ?? 0) > 0
|
||||
? \App\Filament\Pages\Gescon\RubricaUniversaleScheda::getUrl(['record' => (int) $rubrica->id], panel: 'admin-filament')
|
||||
: null,
|
||||
'estratto_conto_url' => (int) ($soggetto?->id ?? 0) > 0
|
||||
? \App\Filament\Pages\Contabilita\EstrattoContoSoggetto::getUrl(['record' => (int) $soggetto->id], panel: 'admin-filament')
|
||||
: null,
|
||||
'post_it_redirect_url' => TicketMobile::getUrl([
|
||||
'live_phone' => $phone,
|
||||
'live_message_id' => (int) $latest->id,
|
||||
'live_note' => (string) ($latest->message_text ?? ''),
|
||||
'live_action' => 'post-it',
|
||||
], panel: 'admin-filament'),
|
||||
];
|
||||
}
|
||||
|
||||
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 ($query) use ($needle, $tailNeedle): void {
|
||||
$query->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();
|
||||
}
|
||||
}
|
||||
|
|
@ -90,6 +90,7 @@ public function getWatchedExtensionsForUser(User $user): array
|
|||
*/
|
||||
public function getStudioExtensionsForAmministratore(Amministratore $amministratore): array
|
||||
{
|
||||
$pbx = (array) (($amministratore->impostazioni ?? [])['pbx'] ?? []);
|
||||
$centralino = (array) (($amministratore->impostazioni ?? [])['centralino'] ?? []);
|
||||
|
||||
$extensions = [
|
||||
|
|
@ -100,6 +101,29 @@ public function getStudioExtensionsForAmministratore(Amministratore $amministrat
|
|||
$this->normalizeExtension((string) Arr::get($centralino, 'interno_gruppo_notte', '')),
|
||||
];
|
||||
|
||||
$watchExtensions = preg_split('/\s*,\s*/', (string) ($pbx['watch_extensions'] ?? ''), -1, PREG_SPLIT_NO_EMPTY) ?: [];
|
||||
foreach ($watchExtensions as $extension) {
|
||||
$extensions[] = $this->normalizeExtension((string) $extension);
|
||||
}
|
||||
|
||||
foreach ((array) ($pbx['response_groups'] ?? []) as $group) {
|
||||
if (! is_array($group)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$extensions[] = $this->normalizeExtension((string) ($group['extension'] ?? ''));
|
||||
$extensions[] = $this->normalizeExtension((string) ($group['target_extension'] ?? ''));
|
||||
}
|
||||
|
||||
foreach ((array) ($pbx['incoming_lines'] ?? []) as $line) {
|
||||
if (! is_array($line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$extensions[] = $this->normalizeExtension((string) ($line['route_group'] ?? ''));
|
||||
$extensions[] = $this->normalizeExtension((string) ($line['target_extension'] ?? ''));
|
||||
}
|
||||
|
||||
return array_values(array_unique(array_filter($extensions, fn($extension): bool => $extension !== '')));
|
||||
}
|
||||
|
||||
|
|
@ -118,6 +142,7 @@ public function findStudioContextByExtension(?string $extension): array
|
|||
}
|
||||
|
||||
foreach (Amministratore::query()->get(['id', 'impostazioni']) as $amministratore) {
|
||||
$pbx = (array) (($amministratore->impostazioni ?? [])['pbx'] ?? []);
|
||||
$centralino = (array) (($amministratore->impostazioni ?? [])['centralino'] ?? []);
|
||||
$map = [
|
||||
'interno_centralino' => ['studio_role' => 'centralino', 'studio_mode' => null],
|
||||
|
|
@ -138,6 +163,39 @@ public function findStudioContextByExtension(?string $extension): array
|
|||
'studio_mode' => $context['studio_mode'],
|
||||
];
|
||||
}
|
||||
|
||||
foreach ((array) ($pbx['response_groups'] ?? []) as $group) {
|
||||
if (! is_array($group)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->normalizeExtension((string) ($group['extension'] ?? '')) !== $normalized) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return [
|
||||
'amministratore_id' => (int) $amministratore->id,
|
||||
'studio_role' => 'gruppo',
|
||||
'studio_mode' => filled($group['mode'] ?? null) ? (string) $group['mode'] : null,
|
||||
];
|
||||
}
|
||||
|
||||
foreach ((array) ($pbx['incoming_lines'] ?? []) as $line) {
|
||||
if (! is_array($line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$routeExtension = $this->normalizeExtension((string) (($line['route_group'] ?? '') !== '' ? $line['route_group'] : ($line['target_extension'] ?? '')));
|
||||
if ($routeExtension !== $normalized) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return [
|
||||
'amministratore_id' => (int) $amministratore->id,
|
||||
'studio_role' => 'linea',
|
||||
'studio_mode' => filled($line['label'] ?? null) ? (string) $line['label'] : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
|
|
|
|||
119
app/Services/Support/TicketAttachmentUploadService.php
Normal file
119
app/Services/Support/TicketAttachmentUploadService.php
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Support;
|
||||
|
||||
use App\Models\TicketAttachment;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
|
||||
class TicketAttachmentUploadService
|
||||
{
|
||||
/**
|
||||
* @param UploadedFile|TemporaryUploadedFile $file
|
||||
* @return array{path:string,mime:string,size:int,original_name:string}
|
||||
*/
|
||||
public function store(object $file, string $directory): array
|
||||
{
|
||||
$path = $file->store($directory, 'public');
|
||||
|
||||
$originalName = method_exists($file, 'getClientOriginalName')
|
||||
? (string) $file->getClientOriginalName()
|
||||
: basename($path);
|
||||
|
||||
$mime = TicketAttachment::normalizeMimeType(
|
||||
method_exists($file, 'getClientMimeType') ? (string) $file->getClientMimeType() : '',
|
||||
$originalName,
|
||||
$path,
|
||||
);
|
||||
|
||||
if (str_starts_with($mime, 'image/')) {
|
||||
$this->optimizeImage($path, $mime);
|
||||
$mime = TicketAttachment::normalizeMimeType($mime, $originalName, $path);
|
||||
}
|
||||
|
||||
$size = 0;
|
||||
try {
|
||||
$size = (int) Storage::disk('public')->size($path);
|
||||
} catch (\Throwable) {
|
||||
$size = (int) (method_exists($file, 'getSize') ? $file->getSize() : 0);
|
||||
}
|
||||
|
||||
return [
|
||||
'path' => $path,
|
||||
'mime' => $mime,
|
||||
'size' => $size,
|
||||
'original_name' => $originalName,
|
||||
];
|
||||
}
|
||||
|
||||
private function optimizeImage(string $path, string $mime): void
|
||||
{
|
||||
$absolutePath = Storage::disk('public')->path($path);
|
||||
if (! is_file($absolutePath) || ! function_exists('getimagesize')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$size = @getimagesize($absolutePath);
|
||||
if (! is_array($size)) {
|
||||
return;
|
||||
}
|
||||
|
||||
[$width, $height] = $size;
|
||||
if ($width <= 0 || $height <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$maxWidth = 1600;
|
||||
$maxHeight = 1600;
|
||||
$ratio = min($maxWidth / $width, $maxHeight / $height, 1);
|
||||
$targetW = (int) max(1, round($width * $ratio));
|
||||
$targetH = (int) max(1, round($height * $ratio));
|
||||
|
||||
$source = $this->createImageResource($absolutePath, $mime);
|
||||
if (! $source) {
|
||||
return;
|
||||
}
|
||||
|
||||
$target = imagecreatetruecolor($targetW, $targetH);
|
||||
if ($target === false) {
|
||||
imagedestroy($source);
|
||||
return;
|
||||
}
|
||||
|
||||
if (in_array($mime, ['image/png', 'image/webp'], true)) {
|
||||
imagealphablending($target, false);
|
||||
imagesavealpha($target, true);
|
||||
$transparent = imagecolorallocatealpha($target, 255, 255, 255, 127);
|
||||
imagefilledrectangle($target, 0, 0, $targetW, $targetH, $transparent);
|
||||
}
|
||||
|
||||
imagecopyresampled($target, $source, 0, 0, 0, 0, $targetW, $targetH, $width, $height);
|
||||
|
||||
switch ($mime) {
|
||||
case 'image/png':
|
||||
imagepng($target, $absolutePath, 6);
|
||||
break;
|
||||
case 'image/webp':
|
||||
if (function_exists('imagewebp')) {
|
||||
imagewebp($target, $absolutePath, 82);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
imagejpeg($target, $absolutePath, 82);
|
||||
break;
|
||||
}
|
||||
|
||||
imagedestroy($target);
|
||||
imagedestroy($source);
|
||||
}
|
||||
|
||||
private function createImageResource(string $absolutePath, string $mime)
|
||||
{
|
||||
return match ($mime) {
|
||||
'image/png' => function_exists('imagecreatefrompng') ? @imagecreatefrompng($absolutePath) : false,
|
||||
'image/webp' => function_exists('imagecreatefromwebp') ? @imagecreatefromwebp($absolutePath) : false,
|
||||
default => function_exists('imagecreatefromjpeg') ? @imagecreatefromjpeg($absolutePath) : false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -35,7 +35,7 @@
|
|||
$haRiscaldamento = (bool) ($stabileAttivo?->riscaldamento_centralizzato ?? false);
|
||||
@endphp
|
||||
|
||||
<div class="flex items-center gap-2 ps-3">
|
||||
<div class="flex min-w-0 items-center gap-2 ps-3">
|
||||
<x-filament::dropdown placement="bottom-start">
|
||||
<x-slot name="trigger">
|
||||
<button type="button" class="flex items-center gap-2">
|
||||
|
|
@ -92,6 +92,8 @@
|
|||
</div>
|
||||
</x-filament::dropdown>
|
||||
|
||||
<livewire:filament.topbar-live-call />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="fi-icon-btn fi-color-gray"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,21 @@
|
|||
]"
|
||||
/>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-gradient-to-r from-slate-900 via-slate-800 to-emerald-800 p-4 text-white shadow-sm">
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<div class="text-sm font-semibold uppercase tracking-[0.18em] text-emerald-200">Cellulare e WebApp</div>
|
||||
<div class="mt-1 text-lg font-semibold">Accessi rapidi per chiamate, ticket e consultazione mobile</div>
|
||||
<div class="mt-1 text-sm text-slate-200">Metto in evidenza i collegamenti operativi che servono quando stai lavorando dal telefono o stai gestendo una chiamata in ingresso.</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="{{ route('admin.mobile') }}" class="inline-flex items-center rounded-md bg-emerald-500 px-3 py-2 text-sm font-medium text-slate-950 hover:bg-emerald-400">Hub Cellulare</a>
|
||||
<a href="{{ \App\Filament\Pages\Supporto\TicketMobile::getUrl(panel: 'admin-filament') }}" class="inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-medium text-slate-900 hover:bg-slate-100">Ticket Mobile</a>
|
||||
<a href="{{ route('condomino.tickets.mobile') }}" target="_blank" rel="noopener noreferrer" class="inline-flex items-center rounded-md border border-white/30 bg-white/10 px-3 py-2 text-sm font-medium text-white hover:bg-white/20">WebApp Condomino</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<livewire:gescon.rubrica-universale-table />
|
||||
</div>
|
||||
</x-filament-panels::page>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,136 @@
|
|||
@php($summary = $this->pbxSummary)
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-3">
|
||||
<div class="mb-2 text-sm font-semibold text-gray-700">Stato integrazione Panasonic / TAPI</div>
|
||||
<div class="mb-3 text-xs text-gray-500">Questa sezione tiene insieme configurazione PBX, gruppi di risposta, linee entranti e ultimi eventi CRM letti dal canale Panasonic.</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-3 md:grid-cols-4">
|
||||
<div class="rounded-md border border-slate-200 bg-slate-50 p-3">
|
||||
<div class="text-[11px] uppercase tracking-wide text-slate-500">Stato</div>
|
||||
<div class="mt-1 text-sm font-semibold text-slate-800">{{ $summary['label'] }}</div>
|
||||
<div class="mt-1 text-xs text-slate-500">{{ $summary['assessment'] }}</div>
|
||||
</div>
|
||||
<div class="rounded-md border border-slate-200 bg-slate-50 p-3">
|
||||
<div class="text-[11px] uppercase tracking-wide text-slate-500">Provider</div>
|
||||
<div class="mt-1 text-sm font-semibold text-slate-800">{{ $summary['provider'] }}</div>
|
||||
<div class="mt-1 text-xs text-slate-500">Canale: {{ $summary['channel'] }} · Bridge: {{ $summary['bridge_mode'] }}</div>
|
||||
</div>
|
||||
<div class="rounded-md border border-slate-200 bg-slate-50 p-3">
|
||||
<div class="text-[11px] uppercase tracking-wide text-slate-500">Eventi CRM</div>
|
||||
<div class="mt-1 text-sm font-semibold text-slate-800">{{ $summary['total_messages'] }}</div>
|
||||
<div class="mt-1 text-xs text-slate-500">Ultime 24h: {{ $summary['last_24h'] }} · Perse: {{ $summary['missed_calls'] }}</div>
|
||||
</div>
|
||||
<div class="rounded-md border border-slate-200 bg-slate-50 p-3">
|
||||
<div class="text-[11px] uppercase tracking-wide text-slate-500">Ultimo evento</div>
|
||||
<div class="mt-1 text-sm font-semibold text-slate-800">{{ $summary['last_event_type'] ?: 'Nessun evento' }}</div>
|
||||
<div class="mt-1 text-xs text-slate-500">{{ $summary['last_event_at'] ?: 'Nessun timestamp disponibile' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 text-xs text-gray-600">
|
||||
<span class="font-semibold">Interni osservati:</span>
|
||||
{{ count($summary['watched_extensions']) > 0 ? implode(', ', $summary['watched_extensions']) : 'nessuno configurato' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-3">
|
||||
<div class="mb-2 text-sm font-semibold text-gray-700">Matrice di instradamento</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full border-collapse border text-xs">
|
||||
<thead>
|
||||
<tr class="bg-slate-100 text-slate-700">
|
||||
<th class="border px-2 py-1.5 text-left">Tipo</th>
|
||||
<th class="border px-2 py-1.5 text-left">Voce</th>
|
||||
<th class="border px-2 py-1.5 text-left">Interno / numero</th>
|
||||
<th class="border px-2 py-1.5 text-left">Target</th>
|
||||
<th class="border px-2 py-1.5 text-left">Note</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($this->pbxRoutingRows as $row)
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="border px-2 py-1.5">{{ $row['type'] }}</td>
|
||||
<td class="border px-2 py-1.5">{{ $row['label'] }}</td>
|
||||
<td class="border px-2 py-1.5">{{ $row['extension'] }}</td>
|
||||
<td class="border px-2 py-1.5">{{ $row['target'] }}</td>
|
||||
<td class="border px-2 py-1.5">{{ $row['note'] }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-3">
|
||||
<div class="mb-2 text-sm font-semibold text-gray-700">Gruppi e linee configurate</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="mb-1 text-xs font-semibold text-gray-700">Gruppi di risposta</div>
|
||||
<div class="text-xs text-gray-600">
|
||||
@if(count($summary['response_groups']) > 0)
|
||||
@foreach($summary['response_groups'] as $row)
|
||||
<div>{{ $row['extension'] ?? '-' }} · {{ $row['label'] ?? 'Gruppo' }} · fallback {{ $row['target_extension'] ?? '-' }} · modalita {{ $row['mode'] ?? '-' }}</div>
|
||||
@endforeach
|
||||
@else
|
||||
<div>Nessun gruppo aggiuntivo configurato. Restano validi i campi studio 601 e 603.</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb-1 text-xs font-semibold text-gray-700">Linee entranti</div>
|
||||
<div class="text-xs text-gray-600">
|
||||
@if(count($summary['incoming_lines']) > 0)
|
||||
@foreach($summary['incoming_lines'] as $row)
|
||||
<div>{{ $row['number'] ?? '-' }} · {{ $row['label'] ?? 'Linea' }} · gruppo {{ $row['route_group'] ?? '-' }} · interno {{ $row['target_extension'] ?? '-' }}</div>
|
||||
@endforeach
|
||||
@else
|
||||
<div>Nessuna linea entrante mappata. Usa questa sezione per separare flussi clienti, fornitori e reperibilita.</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-3">
|
||||
<div class="mb-2 text-sm font-semibold text-gray-700">Ultimi eventi letti dal CRM</div>
|
||||
<div class="mb-3 text-xs text-gray-500">Se qui compaiono record, il bridge Panasonic sta gia portando nel CRM almeno una parte del traffico telefonico.</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full border-collapse border text-xs">
|
||||
<thead>
|
||||
<tr class="bg-slate-100 text-slate-700">
|
||||
<th class="border px-2 py-1.5 text-left">Quando</th>
|
||||
<th class="border px-2 py-1.5 text-left">Evento</th>
|
||||
<th class="border px-2 py-1.5 text-left">Direzione</th>
|
||||
<th class="border px-2 py-1.5 text-left">Telefono</th>
|
||||
<th class="border px-2 py-1.5 text-left">Interno</th>
|
||||
<th class="border px-2 py-1.5 text-left">Utente</th>
|
||||
<th class="border px-2 py-1.5 text-left">Ruolo</th>
|
||||
<th class="border px-2 py-1.5 text-left">Stato</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($this->pbxRecentMessages as $row)
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="border px-2 py-1.5">{{ $row['received_at'] }}</td>
|
||||
<td class="border px-2 py-1.5">{{ $row['event_type'] }}</td>
|
||||
<td class="border px-2 py-1.5">{{ $row['direction'] }}</td>
|
||||
<td class="border px-2 py-1.5">{{ $row['phone_number'] }}</td>
|
||||
<td class="border px-2 py-1.5">{{ $row['target_extension'] }}</td>
|
||||
<td class="border px-2 py-1.5">{{ $row['user'] }}</td>
|
||||
<td class="border px-2 py-1.5">{{ $row['studio_role'] }}{{ $row['studio_mode'] !== '-' ? ' / ' . $row['studio_mode'] : '' }}</td>
|
||||
<td class="border px-2 py-1.5">{{ $row['status'] }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="8" class="border px-2 py-3 text-center text-gray-500">Nessun evento Panasonic presente nel CRM per questo amministratore.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -58,6 +58,8 @@
|
|||
<div class="flex flex-wrap items-center gap-2">
|
||||
<a href="{{ $this->getDashboardFilamentUrl() }}" class="inline-flex items-center rounded-md bg-gray-900 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-700">Dashboard Filament</a>
|
||||
<a href="{{ $this->getRubricaFilamentUrl() }}" class="inline-flex items-center rounded-md bg-primary-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-primary-500">Rubrica Unica Filament</a>
|
||||
<a href="{{ $this->getAdminMobileHubUrl() }}" class="inline-flex items-center rounded-md bg-emerald-700 px-3 py-1.5 text-xs font-medium text-white hover:bg-emerald-600">Hub Cellulare / WebApp</a>
|
||||
<a href="{{ $this->getCondominoMobileTicketUrl() }}" class="inline-flex items-center rounded-md bg-amber-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-amber-500" target="_blank" rel="noopener noreferrer">WebApp Ticket Condomino</a>
|
||||
<a href="{{ $this->getTicketArchivioUrl() }}" class="inline-flex items-center rounded-md bg-indigo-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-indigo-500">Archivio Ticket Completo</a>
|
||||
</div>
|
||||
<div class="mt-3 grid grid-cols-2 gap-2 sm:grid-cols-4">
|
||||
|
|
@ -109,9 +111,19 @@
|
|||
<div class="mt-3 text-xs text-gray-500">Nessun contatto trovato con questa ricerca.</div>
|
||||
@endif
|
||||
|
||||
<div class="mt-4 rounded-lg border border-dashed p-3">
|
||||
<div class="mt-4 rounded-lg border border-dashed p-3" x-data="ticketMobilePreview()">
|
||||
<div class="text-sm font-semibold">Nuovo ticket rapido</div>
|
||||
<div class="mt-1 text-xs text-gray-500">Dopo aver selezionato il chiamante, compila e crea il ticket.</div>
|
||||
@if($errors->any())
|
||||
<div class="mt-3 rounded-lg border border-rose-200 bg-rose-50 px-3 py-2 text-xs text-rose-800">
|
||||
<div class="font-semibold">Il ticket non e stato inviato.</div>
|
||||
<ul class="mt-1 list-disc ps-4">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
<div class="mt-3 grid gap-3 md:grid-cols-2">
|
||||
<label class="block text-sm md:col-span-2">
|
||||
<span class="mb-1 block font-medium">Titolo</span>
|
||||
|
|
@ -167,21 +179,46 @@
|
|||
<div class="flex flex-wrap gap-2">
|
||||
<label class="inline-flex cursor-pointer items-center rounded-md bg-emerald-600 px-3 py-2 text-xs font-medium text-white hover:bg-emerald-500">
|
||||
Scatta foto
|
||||
<input type="file" wire:model="newTicketCameraShots" multiple accept="image/*" capture="environment" class="hidden" />
|
||||
<input type="file" wire:model="newTicketCameraShots" x-on:change="ingest($event, 'camera')" multiple accept="image/*" capture="environment" class="hidden" />
|
||||
</label>
|
||||
|
||||
<label class="inline-flex cursor-pointer items-center rounded-md bg-indigo-600 px-3 py-2 text-xs font-medium text-white hover:bg-indigo-500">
|
||||
Aggiungi allegati
|
||||
<input type="file" wire:model="newTicketAttachments" multiple accept=".pdf,.doc,.docx,.xls,.xlsx,.txt,image/*" class="hidden" />
|
||||
<input type="file" wire:model="newTicketAttachments" x-on:change="ingest($event, 'attachment')" multiple accept=".pdf,.doc,.docx,.xls,.xlsx,.txt,image/*" class="hidden" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-gray-500">Puoi caricare fino a 10 file totali (foto + documenti). Ogni file ha descrizione separata.</div>
|
||||
<div class="mt-1 text-xs text-gray-500">iPhone: su "Aggiungi allegati" scegli "Sfoglia" per aprire l'app File (iCloud Drive/On My iPhone).</div>
|
||||
<div wire:loading wire:target="newTicketCameraShots,newTicketAttachments" class="mt-2 text-xs font-medium text-amber-700">Caricamento in corso: l'anteprima locale e gia disponibile, sto sincronizzando i file al server.</div>
|
||||
</label>
|
||||
|
||||
<div class="md:col-span-2" x-show="previews.length > 0">
|
||||
<div class="mb-2 text-xs font-semibold text-emerald-700">Anteprima immediata sul telefono</div>
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<template x-for="item in previews" :key="item.key">
|
||||
<div class="rounded-xl border border-emerald-100 bg-emerald-50 p-3 text-xs shadow-sm">
|
||||
<div class="aspect-[4/3] rounded-lg border bg-white p-2">
|
||||
<template x-if="item.isImage && item.previewUrl">
|
||||
<img :src="item.previewUrl" :alt="item.name" class="h-full w-full rounded object-cover" />
|
||||
</template>
|
||||
<template x-if="!item.isImage || !item.previewUrl">
|
||||
<div class="flex h-full items-center justify-center text-center text-gray-500">
|
||||
<div>
|
||||
<div class="text-sm font-semibold" x-text="item.ext"></div>
|
||||
<div class="mt-1 text-[11px]" x-text="item.name"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="mt-2 font-medium" x-text="item.name"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(count($this->selectedUploads) > 0)
|
||||
<div class="md:col-span-2">
|
||||
<div class="mb-2 text-xs font-semibold text-gray-700">Anteprima allegati selezionati (stile catalogo)</div>
|
||||
<div class="mb-2 text-xs font-semibold text-gray-700">Anteprima allegati sincronizzati</div>
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
@foreach($this->selectedUploads as $upload)
|
||||
<div class="rounded-xl border bg-white p-3 text-xs shadow-sm">
|
||||
|
|
@ -212,7 +249,10 @@
|
|||
@endif
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<x-filament::button wire:click="creaTicketRapido">Crea ticket</x-filament::button>
|
||||
<x-filament::button wire:click="creaTicketRapido" wire:loading.attr="disabled" wire:target="creaTicketRapido,newTicketCameraShots,newTicketAttachments">
|
||||
<span wire:loading.remove wire:target="creaTicketRapido">Crea ticket</span>
|
||||
<span wire:loading wire:target="creaTicketRapido">Invio ticket...</span>
|
||||
</x-filament::button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -230,4 +270,28 @@
|
|||
Suggerimento: aggiungi questa pagina ai preferiti del browser sul telefono per usarla come webapp rapida.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function ticketMobilePreview() {
|
||||
return {
|
||||
previews: [],
|
||||
ingest(event, source) {
|
||||
const files = Array.from(event.target.files || []);
|
||||
files.forEach((file, index) => {
|
||||
const isImage = (file.type || '').startsWith('image/');
|
||||
const previewUrl = isImage ? URL.createObjectURL(file) : null;
|
||||
const extension = (file.name.split('.').pop() || 'FILE').toUpperCase();
|
||||
this.previews.unshift({
|
||||
key: `${source}-${file.name}-${file.size}-${index}`,
|
||||
name: file.name,
|
||||
isImage,
|
||||
previewUrl,
|
||||
ext: extension,
|
||||
});
|
||||
});
|
||||
this.previews = this.previews.slice(0, 10);
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
</x-filament-panels::page>
|
||||
|
|
|
|||
35
resources/views/livewire/filament/topbar-live-call.blade.php
Normal file
35
resources/views/livewire/filament/topbar-live-call.blade.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<div wire:poll.8s="refreshLiveIncomingCall" class="hidden xl:flex xl:min-w-[28rem] xl:flex-1 xl:justify-center">
|
||||
@if($liveIncomingCall)
|
||||
<div class="mx-3 flex w-full max-w-2xl items-center justify-between gap-3 rounded-xl border border-emerald-300 bg-emerald-50 px-4 py-2 shadow-sm">
|
||||
<div class="min-w-0">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-emerald-800">Chiamata in arrivo</div>
|
||||
<div class="truncate text-sm font-semibold text-emerald-950">
|
||||
{{ $liveIncomingCall['rubrica_nome'] ?: 'Numero non riconosciuto' }}
|
||||
</div>
|
||||
<div class="truncate text-xs text-emerald-800">
|
||||
{{ $liveIncomingCall['phone'] ?? '-' }}
|
||||
@if(!empty($liveIncomingCall['target_extension']))
|
||||
· int. {{ $liveIncomingCall['target_extension'] }}
|
||||
@endif
|
||||
@if(!empty($liveIncomingCall['received_at']))
|
||||
· {{ $liveIncomingCall['received_at'] }}
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 flex-wrap items-center gap-2">
|
||||
<button type="button" wire:click="createPostIt" class="inline-flex items-center rounded-md bg-emerald-700 px-3 py-1.5 text-xs font-medium text-white hover:bg-emerald-600">
|
||||
Post-it
|
||||
</button>
|
||||
<button type="button" wire:click="openTicketMobile" class="inline-flex items-center rounded-md bg-slate-900 px-3 py-1.5 text-xs font-medium text-white hover:bg-slate-800">
|
||||
Prendi nota
|
||||
</button>
|
||||
@if(!empty($liveIncomingCall['rubrica_url']))
|
||||
<a href="{{ $liveIncomingCall['rubrica_url'] }}" class="inline-flex items-center rounded-md border border-emerald-300 bg-white px-3 py-1.5 text-xs font-medium text-emerald-800 hover:bg-emerald-100">
|
||||
Rubrica
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
Loading…
Reference in New Issue
Block a user