Fix post-it live flow and legacy imports
This commit is contained in:
parent
8c10069a71
commit
cd5d004561
|
|
@ -817,7 +817,7 @@ private function normalizePhone(?string $phone): ?string
|
|||
* Recupera identità tenant da altre righe condomin della stessa unità, quando la riga corrente è incompleta.
|
||||
* @return array{cf:?string,email:?string,telefono:?string,cellulare:?string}
|
||||
*/
|
||||
private function lookupTenantIdentityFromStaging(string $conn, string $codStabile, string $scala, string $interno, mixed $piano, mixed $tenantNameHint): array
|
||||
private function lookupTenantIdentityFromStaging(string $conn, string $codStabile, ?string $scala, ?string $interno, mixed $piano, mixed $tenantNameHint): array
|
||||
{
|
||||
try {
|
||||
if (! Schema::connection($conn)->hasTable('condomin')) {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ class ImportLegacyFornitoriTagsCommand extends Command
|
|||
|
||||
protected $description = 'Importa e normalizza i tag fornitori dalla colonna legacy descrizione (gescon_import.fornitori).';
|
||||
|
||||
private bool $hasLegacyDescrizioneColumn = false;
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
if (! config('database.connections.gescon_import')) {
|
||||
|
|
@ -22,30 +24,38 @@ public function handle(): int
|
|||
}
|
||||
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
$this->hasLegacyDescrizioneColumn = Schema::hasColumn('fornitori', 'legacy_descrizione');
|
||||
$filterIds = collect((array) $this->option('fornitore-id'))
|
||||
->map(fn($v) => (int) $v)
|
||||
->filter(fn(int $v): bool => $v > 0)
|
||||
->values()
|
||||
->all();
|
||||
|
||||
[$legacyTable, $descrColumn] = $this->resolveLegacySource();
|
||||
if ($legacyTable === null || $descrColumn === null) {
|
||||
$source = $this->resolveLegacySource();
|
||||
if ($source === null) {
|
||||
$this->warn('Sorgente legacy non trovata: manca tabella/colonna descrizione su gescon_import.');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$legacyRows = DB::connection('gescon_import')
|
||||
->table($legacyTable)
|
||||
->table($source['table'])
|
||||
->select([
|
||||
'id',
|
||||
'cod_forn',
|
||||
'ragione_sociale',
|
||||
'partita_iva',
|
||||
'codice_fiscale',
|
||||
DB::raw($descrColumn . ' as descrizione_legacy'),
|
||||
$this->selectColumnOrNull($source['id_column'] ?? null, 'id'),
|
||||
$this->selectColumnOrNull($source['code_column'] ?? null, 'cod_forn'),
|
||||
$this->selectCoalescedColumnOrNull($source['name_columns'] ?? [], 'ragione_sociale'),
|
||||
$this->selectColumnOrNull($source['piva_column'] ?? null, 'partita_iva'),
|
||||
$this->selectColumnOrNull($source['cf_column'] ?? null, 'codice_fiscale'),
|
||||
$this->selectColumnOrNull($source['description_column'] ?? null, 'descrizione_legacy'),
|
||||
$this->selectColumnOrNull($source['notes_column'] ?? null, 'note_legacy'),
|
||||
])
|
||||
->whereNotNull($descrColumn)
|
||||
->where($descrColumn, '<>', '')
|
||||
->where(function ($query) use ($source): void {
|
||||
foreach (($source['text_columns'] ?? []) as $column) {
|
||||
$query->orWhere(function ($inner) use ($column): void {
|
||||
$inner->whereNotNull($column)
|
||||
->where($column, '<>', '');
|
||||
});
|
||||
}
|
||||
})
|
||||
->get();
|
||||
|
||||
if ($legacyRows->isEmpty()) {
|
||||
|
|
@ -67,7 +77,8 @@ public function handle(): int
|
|||
continue;
|
||||
}
|
||||
|
||||
$incoming = $this->extractTags((string) ($legacy->descrizione_legacy ?? ''));
|
||||
$legacyDescrizione = $this->buildLegacyDescription($legacy);
|
||||
$incoming = $this->extractTags($legacyDescrizione);
|
||||
if (count($incoming) === 0) {
|
||||
$skipped++;
|
||||
continue;
|
||||
|
|
@ -77,7 +88,6 @@ public function handle(): int
|
|||
$merged = array_values(array_unique(array_merge($current, $incoming)));
|
||||
sort($merged, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
|
||||
$legacyDescrizione = trim((string) ($legacy->descrizione_legacy ?? ''));
|
||||
$newTagsValue = implode(', ', $merged);
|
||||
$changed = ((string) ($fornitore->tags ?? '')) !== $newTagsValue
|
||||
|| ((string) ($fornitore->legacy_descrizione ?? '')) !== $legacyDescrizione;
|
||||
|
|
@ -93,8 +103,10 @@ public function handle(): int
|
|||
continue;
|
||||
}
|
||||
|
||||
$fornitore->tags = $newTagsValue;
|
||||
$fornitore->legacy_descrizione = $legacyDescrizione !== '' ? $legacyDescrizione : null;
|
||||
$fornitore->tags = $newTagsValue;
|
||||
if ($this->hasLegacyDescrizioneColumn) {
|
||||
$fornitore->legacy_descrizione = $legacyDescrizione !== '' ? $legacyDescrizione : null;
|
||||
}
|
||||
$fornitore->save();
|
||||
$updated++;
|
||||
}
|
||||
|
|
@ -105,24 +117,129 @@ public function handle(): int
|
|||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function resolveLegacySource(): array
|
||||
private function resolveLegacySource(): ?array
|
||||
{
|
||||
$tables = ['fornitori', 'fornitori_gescon'];
|
||||
$columns = ['descrizione', 'descr', 'note'];
|
||||
$sources = [
|
||||
[
|
||||
'table' => 'mdb_fornitori',
|
||||
'id_column' => 'id_fornitore',
|
||||
'code_column' => 'cod_forn',
|
||||
'name_columns' => ['denominazione', 'ragione_sociale', 'cognome', 'nome'],
|
||||
'piva_column' => 'p_iva',
|
||||
'cf_column' => 'cod_fisc',
|
||||
'description_column' => 'descrizione',
|
||||
'notes_column' => 'note',
|
||||
'text_columns' => ['descrizione', 'note'],
|
||||
],
|
||||
[
|
||||
'table' => 'fornitori',
|
||||
'id_column' => 'id',
|
||||
'code_column' => 'cod_forn',
|
||||
'name_columns' => ['ragione_sociale', 'cognome', 'nome'],
|
||||
'piva_column' => 'partita_iva',
|
||||
'cf_column' => 'codice_fiscale',
|
||||
'description_column' => 'descrizione',
|
||||
'notes_column' => 'note',
|
||||
'text_columns' => ['descrizione', 'note'],
|
||||
],
|
||||
[
|
||||
'table' => 'fornitori_gescon',
|
||||
'id_column' => 'id',
|
||||
'code_column' => 'cod_forn',
|
||||
'name_columns' => ['ragione_sociale'],
|
||||
'piva_column' => 'partita_iva',
|
||||
'cf_column' => 'codice_fiscale',
|
||||
'description_column' => 'descr',
|
||||
'notes_column' => 'note',
|
||||
'text_columns' => ['descr', 'note'],
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($tables as $table) {
|
||||
if (! Schema::connection('gescon_import')->hasTable($table)) {
|
||||
foreach ($sources as $source) {
|
||||
if (! Schema::connection('gescon_import')->hasTable($source['table'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($columns as $column) {
|
||||
if (Schema::connection('gescon_import')->hasColumn($table, $column)) {
|
||||
return [$table, $column];
|
||||
$hasColumn = fn(string $column): bool => Schema::connection('gescon_import')->hasColumn($source['table'], $column);
|
||||
|
||||
$source['id_column'] = isset($source['id_column']) && $hasColumn($source['id_column'])
|
||||
? $source['id_column']
|
||||
: null;
|
||||
$source['code_column'] = isset($source['code_column']) && $hasColumn($source['code_column'])
|
||||
? $source['code_column']
|
||||
: null;
|
||||
$source['piva_column'] = isset($source['piva_column']) && $hasColumn($source['piva_column'])
|
||||
? $source['piva_column']
|
||||
: null;
|
||||
$source['cf_column'] = isset($source['cf_column']) && $hasColumn($source['cf_column'])
|
||||
? $source['cf_column']
|
||||
: null;
|
||||
$source['name_columns'] = array_values(array_filter(
|
||||
$source['name_columns'] ?? [],
|
||||
fn(string $column): bool => $hasColumn($column)
|
||||
));
|
||||
|
||||
$availableTextColumns = [];
|
||||
foreach (($source['text_columns'] ?? []) as $column) {
|
||||
if ($hasColumn($column)) {
|
||||
$availableTextColumns[] = $column;
|
||||
}
|
||||
}
|
||||
|
||||
if ($availableTextColumns === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$source['text_columns'] = $availableTextColumns;
|
||||
$source['description_column'] = in_array((string) ($source['description_column'] ?? ''), $availableTextColumns, true)
|
||||
? $source['description_column']
|
||||
: null;
|
||||
$source['notes_column'] = in_array((string) ($source['notes_column'] ?? ''), $availableTextColumns, true)
|
||||
? $source['notes_column']
|
||||
: null;
|
||||
|
||||
return $source;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function selectColumnOrNull(?string $column, string $alias): \Illuminate\Database\Query\Expression
|
||||
{
|
||||
if ($column === null) {
|
||||
return DB::raw('NULL as ' . $alias);
|
||||
}
|
||||
|
||||
return DB::raw($column . ' as ' . $alias);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $columns
|
||||
*/
|
||||
private function selectCoalescedColumnOrNull(array $columns, string $alias): \Illuminate\Database\Query\Expression
|
||||
{
|
||||
$usable = array_values(array_filter($columns, fn(string $column): bool => $column !== ''));
|
||||
if ($usable === []) {
|
||||
return DB::raw('NULL as ' . $alias);
|
||||
}
|
||||
|
||||
$segments = array_map(fn(string $column): string => 'NULLIF(TRIM(' . $column . '), \'\')', $usable);
|
||||
|
||||
return DB::raw('COALESCE(' . implode(', ', $segments) . ') as ' . $alias);
|
||||
}
|
||||
|
||||
private function buildLegacyDescription(object $legacy): string
|
||||
{
|
||||
$parts = [];
|
||||
|
||||
foreach (['descrizione_legacy', 'note_legacy'] as $field) {
|
||||
$value = trim((string) ($legacy->{$field} ?? ''));
|
||||
if ($value !== '') {
|
||||
$parts[] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return [null, null];
|
||||
return implode(' | ', array_values(array_unique($parts)));
|
||||
}
|
||||
|
||||
private function resolveLocalFornitore(object $legacy): ?Fornitore
|
||||
|
|
@ -165,6 +282,15 @@ private function resolveLocalFornitore(object $legacy): ?Fornitore
|
|||
return Fornitore::query()->where('ragione_sociale', $ragione)->first();
|
||||
}
|
||||
|
||||
$normalizedRagione = $this->normalizeComparableString($ragione);
|
||||
if ($normalizedRagione !== '') {
|
||||
return Fornitore::query()
|
||||
->get(['id', 'ragione_sociale'])
|
||||
->first(function (Fornitore $fornitore) use ($normalizedRagione): bool {
|
||||
return $this->normalizeComparableString((string) ($fornitore->ragione_sociale ?? '')) === $normalizedRagione;
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -179,7 +305,7 @@ private function extractTags(string $input): array
|
|||
}
|
||||
|
||||
$parts = preg_split('/[,;|\n\r\/]+/', $input) ?: [];
|
||||
$tags = [];
|
||||
$tags = $this->extractKeywordTags($input);
|
||||
|
||||
foreach ($parts as $part) {
|
||||
$normalized = $this->canonicalizeTag($part);
|
||||
|
|
@ -195,14 +321,93 @@ private function extractTags(string $input): array
|
|||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function extractKeywordTags(string $input): array
|
||||
{
|
||||
$haystack = mb_strtolower(trim($input));
|
||||
if ($haystack === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$keywords = [
|
||||
'idr' => 'idraulico',
|
||||
'termoidraul' => 'termoidraulico',
|
||||
'elettric' => 'elettricista',
|
||||
'energia elet' => 'energia elettrica',
|
||||
'ascens' => 'ascensorista',
|
||||
'puliz' => 'pulizie',
|
||||
'giardin' => 'giardiniere',
|
||||
'spurgh' => 'spurgo',
|
||||
'edil' => 'edile',
|
||||
'murator' => 'edile',
|
||||
'fabbro' => 'fabbro',
|
||||
'ferrament' => 'ferramenta',
|
||||
'serrament' => 'serramenti',
|
||||
'portier' => 'portierato',
|
||||
'citofon' => 'citofonia',
|
||||
'videosorv' => 'videosorveglianza',
|
||||
'sicurezz' => 'sicurezza',
|
||||
'antincend' => 'antincendio',
|
||||
'estintor' => 'antincendio',
|
||||
'calda' => 'caldaia',
|
||||
'riscald' => 'riscaldamento',
|
||||
'condizion' => 'condizionamento',
|
||||
'gas' => 'gas',
|
||||
'acqua' => 'acqua',
|
||||
'fogna' => 'fognatura',
|
||||
'antenna' => 'antenna',
|
||||
'telecom' => 'telefonia',
|
||||
'pec' => 'pec',
|
||||
'aruba' => 'pec',
|
||||
'posta' => 'postali',
|
||||
'raccomand' => 'postali',
|
||||
'spediz' => 'spedizioni',
|
||||
'infest' => 'disinfestazione',
|
||||
'amminist' => 'amministrazione',
|
||||
'assicur' => 'assicurazione',
|
||||
'avvocat' => 'legale',
|
||||
'legale' => 'legale',
|
||||
'commercial' => 'commercialista',
|
||||
'geometr' => 'geometra',
|
||||
'privacy' => 'privacy',
|
||||
'timbri' => 'timbri',
|
||||
'targhe' => 'targhe',
|
||||
'paghe' => 'paghe contributi',
|
||||
'contribut' => 'paghe contributi',
|
||||
'utenz' => 'utenze',
|
||||
];
|
||||
|
||||
$tags = [];
|
||||
foreach ($keywords as $needle => $canonical) {
|
||||
if (str_contains($haystack, $needle)) {
|
||||
$tags[] = $canonical;
|
||||
}
|
||||
}
|
||||
|
||||
$tags = array_values(array_unique($tags));
|
||||
sort($tags, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
private function canonicalizeTag(string $raw): ?string
|
||||
{
|
||||
$clean = trim(mb_strtolower($raw));
|
||||
$clean = trim(mb_strtolower($raw), " \t\n\r\0\x0B-_,.;:");
|
||||
$clean = preg_replace('/\s+/', ' ', $clean) ?? '';
|
||||
if ($clean === '' || mb_strlen($clean) < 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (str_contains($clean, '@') || preg_match('/\d{2,}/', $clean)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preg_match('/\b(tel|telefono|fax|cell|cellulare|pec|email|codice cliente|parlare con|sig\.?|sig\.ra)\b/u', $clean)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$map = [
|
||||
'idr' => 'idraulico',
|
||||
'idraul' => 'idraulico',
|
||||
|
|
@ -213,14 +418,31 @@ private function canonicalizeTag(string $raw): ?string
|
|||
'giardin' => 'giardiniere',
|
||||
'assicur' => 'assicurazione',
|
||||
'manut' => 'manutenzione',
|
||||
'murator' => 'edile',
|
||||
'privacy' => 'privacy',
|
||||
'ferrament'=> 'ferramenta',
|
||||
];
|
||||
|
||||
foreach ($map as $needle => $canonical) {
|
||||
if (str_starts_with($clean, $needle)) {
|
||||
if (str_contains($clean, $needle)) {
|
||||
return $canonical;
|
||||
}
|
||||
}
|
||||
|
||||
$lettersOnly = preg_replace('/[^[:alpha:] ]+/u', '', $clean) ?? '';
|
||||
$wordCount = count(array_filter(explode(' ', $lettersOnly), fn(string $word): bool => $word !== ''));
|
||||
if (mb_strlen(trim($lettersOnly)) < 3 || $wordCount > 3 || mb_strlen($clean) > 28) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $clean;
|
||||
}
|
||||
|
||||
private function normalizeComparableString(string $value): string
|
||||
{
|
||||
$normalized = mb_strtolower(trim($value));
|
||||
$normalized = preg_replace('/[^[:alnum:]]+/u', '', $normalized) ?? '';
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ public function mount(int | string $record): void
|
|||
}
|
||||
|
||||
$this->rubrica = RubricaUniversale::query()->findOrFail((int) $record);
|
||||
$this->applyLegacyIdentityDefaults();
|
||||
|
||||
$activeStabile = StabileContext::getActiveStabile($user);
|
||||
$adminId = (int) ($activeStabile?->amministratore_id ?: 0);
|
||||
|
|
@ -301,6 +302,17 @@ public function mount(int | string $record): void
|
|||
$this->hydrateStudioCollaboratoreWorkspace();
|
||||
}
|
||||
|
||||
private function applyLegacyIdentityDefaults(): void
|
||||
{
|
||||
$legacyIdentity = $this->extractLegacyIdentityFromNote($this->rubrica->note ?? null);
|
||||
|
||||
if (! $this->rubrica->titolo_id && ! empty($legacyIdentity['titolo_id'])) {
|
||||
$this->rubrica->titolo_id = (int) $legacyIdentity['titolo_id'];
|
||||
$this->rubrica->saveQuietly();
|
||||
$this->rubrica->refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public function startInlineEdit(): void
|
||||
{
|
||||
$this->fillInlineForm();
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ class PostItGestione extends Page
|
|||
|
||||
protected string $view = 'filament.pages.strumenti.post-it-gestione';
|
||||
|
||||
public ?int $focusPostItId = null;
|
||||
|
||||
public string $activeTab = 'storico';
|
||||
|
||||
public string $tecnicoCallView = 'esterne';
|
||||
|
|
@ -53,6 +55,16 @@ class PostItGestione extends Page
|
|||
/** @var array<string, array{id:int|null,name:string|null}> */
|
||||
private array $pbxExtensionCache = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$focus = (int) request()->query('focus_post_it', 0);
|
||||
$this->focusPostItId = $focus > 0 ? $focus : null;
|
||||
|
||||
if (request()->query('tab') === 'storico') {
|
||||
$this->activeTab = 'storico';
|
||||
}
|
||||
}
|
||||
|
||||
public function getPostItInserimentoUrl(): string
|
||||
{
|
||||
return PostIt::getUrl(panel: 'admin-filament');
|
||||
|
|
@ -196,11 +208,16 @@ public function getRecentiProperty()
|
|||
}
|
||||
|
||||
try {
|
||||
return ChiamataPostIt::query()
|
||||
$query = ChiamataPostIt::query()
|
||||
->with(['rubrica', 'stabile', 'ticket', 'creatoDa'])
|
||||
->orderByDesc('chiamata_il')
|
||||
->limit(50)
|
||||
->get();
|
||||
->orderByDesc('id');
|
||||
|
||||
if ($this->focusPostItId) {
|
||||
$query->orderByRaw('CASE WHEN id = ? THEN 0 ELSE 1 END', [$this->focusPostItId]);
|
||||
}
|
||||
|
||||
return $query->limit(50)->get();
|
||||
} catch (QueryException) {
|
||||
return collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
use App\Filament\Pages\Contabilita\EstrattoContoSoggetto;
|
||||
use App\Filament\Pages\Gescon\RubricaUniversaleScheda;
|
||||
use App\Filament\Pages\Strumenti\PostItGestione;
|
||||
use App\Models\CategoriaTicket;
|
||||
use App\Models\ChiamataPostIt;
|
||||
use App\Models\PbxClickToCallRequest;
|
||||
|
|
@ -68,9 +69,17 @@ class TicketMobile extends Page
|
|||
/** @var array<int, mixed> */
|
||||
public array $newTicketCameraShots = [];
|
||||
|
||||
/** @var array<int, mixed> */
|
||||
public array $pendingTicketAttachments = [];
|
||||
|
||||
/** @var array<int, mixed> */
|
||||
public array $pendingTicketCameraShots = [];
|
||||
|
||||
/** @var array<int, string> */
|
||||
public array $newTicketAttachmentDescriptions = [];
|
||||
|
||||
public string $newTicketDraftProtocol = '';
|
||||
|
||||
public ?string $newTicketFotoNote = null;
|
||||
|
||||
/** @var \Illuminate\Support\Collection<int, Ticket> */
|
||||
|
|
@ -122,6 +131,7 @@ public function mount(): void
|
|||
$this->callerMatches = collect();
|
||||
$this->newTicketPriorita = 'Media';
|
||||
$this->newTicketTipoIntervento = 'altro';
|
||||
$this->resetDraftUploadState();
|
||||
$this->loadCategorieTicketOptions();
|
||||
|
||||
if (request()->query('status') === 'all') {
|
||||
|
|
@ -160,7 +170,7 @@ public function refreshLiveCallBanner(): void
|
|||
return;
|
||||
}
|
||||
|
||||
$this->liveIncomingCall = app(LiveIncomingCallService::class)->getForUser($authUser);
|
||||
$this->liveIncomingCall = app(LiveIncomingCallService::class)->getForUser($authUser);
|
||||
$this->liveCallCanClickToCall = (bool) ($this->liveIncomingCall['can_click_to_call'] ?? false);
|
||||
}
|
||||
|
||||
|
|
@ -188,7 +198,7 @@ public function creaPostItDaChiamataInIngresso(): void
|
|||
$line = (string) ($this->liveIncomingCall['line'] ?? '');
|
||||
$this->prefillTicketFromLiveCall($phone, $line);
|
||||
|
||||
ChiamataPostIt::query()->create([
|
||||
$postIt = ChiamataPostIt::query()->create([
|
||||
'stabile_id' => (int) $stabileId,
|
||||
'creato_da_user_id' => (int) $user->id,
|
||||
'rubrica_id' => (int) ($this->selectedCallerId ?? 0) ?: null,
|
||||
|
|
@ -208,6 +218,14 @@ public function creaPostItDaChiamataInIngresso(): void
|
|||
->title('Post-it creato dalla chiamata in arrivo')
|
||||
->success()
|
||||
->send();
|
||||
|
||||
$this->redirect(
|
||||
PostItGestione::getUrl([
|
||||
'tab' => 'storico',
|
||||
'focus_post_it' => (int) $postIt->id,
|
||||
], panel: 'admin-filament'),
|
||||
navigate: true,
|
||||
);
|
||||
}
|
||||
|
||||
public function usaChiamataInIngresso(): void
|
||||
|
|
@ -365,15 +383,15 @@ private function loadTickets(): void
|
|||
return;
|
||||
}
|
||||
|
||||
$stabileId = StabileContext::resolveActiveStabileId($user);
|
||||
if (! $stabileId) {
|
||||
$stabileIds = $this->resolveTicketScopeStabileIds($this->status === 'all');
|
||||
if ($stabileIds === []) {
|
||||
$this->tickets = collect();
|
||||
return;
|
||||
}
|
||||
|
||||
$query = Ticket::query()
|
||||
->with(['categoriaTicket'])
|
||||
->where('stabile_id', $stabileId);
|
||||
->whereIn('stabile_id', $stabileIds);
|
||||
|
||||
if ($this->status === 'open') {
|
||||
$query->whereIn('stato', ['Aperto', 'Preso in Carico', 'In Lavorazione']);
|
||||
|
|
@ -395,13 +413,13 @@ private function loadCounters(): void
|
|||
return;
|
||||
}
|
||||
|
||||
$stabileId = StabileContext::resolveActiveStabileId($user);
|
||||
if (! $stabileId) {
|
||||
$stabileIds = $this->resolveTicketScopeStabileIds($this->status === 'all');
|
||||
if ($stabileIds === []) {
|
||||
$this->ticketCounters = ['open' => 0, 'urgent' => 0, 'closed' => 0, 'all' => 0];
|
||||
return;
|
||||
}
|
||||
|
||||
$base = Ticket::query()->where('stabile_id', $stabileId);
|
||||
$base = Ticket::query()->whereIn('stabile_id', $stabileIds);
|
||||
|
||||
$this->ticketCounters = [
|
||||
'open' => (clone $base)->whereIn('stato', ['Aperto', 'Preso in Carico', 'In Lavorazione'])->count(),
|
||||
|
|
@ -484,22 +502,22 @@ public function creaTicketRapido(): void
|
|||
'newTicketAttachmentDescriptions' => ['nullable', 'array'],
|
||||
'newTicketAttachmentDescriptions.*' => ['nullable', 'string', 'max:255'],
|
||||
], [
|
||||
'newTicketTitolo.required' => 'Inserisci il titolo del ticket.',
|
||||
'newTicketTitolo.max' => 'Il titolo puo contenere al massimo 255 caratteri.',
|
||||
'newTicketDescrizione.required' => 'Inserisci una descrizione del problema.',
|
||||
'newTicketLuogo.max' => 'Il luogo intervento puo contenere al massimo 255 caratteri.',
|
||||
'newTicketPriorita.in' => 'Seleziona una priorita valida.',
|
||||
'newTicketCategoriaId.exists' => 'La categoria selezionata non e valida.',
|
||||
'newTicketTipoIntervento.max' => 'Il tipo intervento selezionato non e valido.',
|
||||
'newTicketFotoNote.max' => 'La nota generale foto puo contenere al massimo 2000 caratteri.',
|
||||
'newTicketCameraShots.max' => 'Puoi caricare al massimo 10 foto per volta.',
|
||||
'newTicketCameraShots.*.file' => 'Ogni foto deve essere un file valido.',
|
||||
'newTicketCameraShots.*.max' => 'Ogni foto deve pesare al massimo 10 MB.',
|
||||
'newTicketCameraShots.*.mimes' => 'Le foto devono essere JPG, PNG o WEBP.',
|
||||
'newTicketAttachments.max' => 'Puoi caricare al massimo 10 allegati per volta.',
|
||||
'newTicketAttachments.*.file' => 'Ogni allegato deve essere un file valido.',
|
||||
'newTicketAttachments.*.max' => 'Ogni allegato deve pesare al massimo 10 MB.',
|
||||
'newTicketAttachments.*.mimes' => 'Gli allegati devono essere immagini, PDF o documenti Office/testo.',
|
||||
'newTicketTitolo.required' => 'Inserisci il titolo del ticket.',
|
||||
'newTicketTitolo.max' => 'Il titolo puo contenere al massimo 255 caratteri.',
|
||||
'newTicketDescrizione.required' => 'Inserisci una descrizione del problema.',
|
||||
'newTicketLuogo.max' => 'Il luogo intervento puo contenere al massimo 255 caratteri.',
|
||||
'newTicketPriorita.in' => 'Seleziona una priorita valida.',
|
||||
'newTicketCategoriaId.exists' => 'La categoria selezionata non e valida.',
|
||||
'newTicketTipoIntervento.max' => 'Il tipo intervento selezionato non e valido.',
|
||||
'newTicketFotoNote.max' => 'La nota generale foto puo contenere al massimo 2000 caratteri.',
|
||||
'newTicketCameraShots.max' => 'Puoi caricare al massimo 10 foto per volta.',
|
||||
'newTicketCameraShots.*.file' => 'Ogni foto deve essere un file valido.',
|
||||
'newTicketCameraShots.*.max' => 'Ogni foto deve pesare al massimo 10 MB.',
|
||||
'newTicketCameraShots.*.mimes' => 'Le foto devono essere JPG, PNG o WEBP.',
|
||||
'newTicketAttachments.max' => 'Puoi caricare al massimo 10 allegati per volta.',
|
||||
'newTicketAttachments.*.file' => 'Ogni allegato deve essere un file valido.',
|
||||
'newTicketAttachments.*.max' => 'Ogni allegato deve pesare al massimo 10 MB.',
|
||||
'newTicketAttachments.*.mimes' => 'Gli allegati devono essere immagini, PDF o documenti Office/testo.',
|
||||
'newTicketAttachmentDescriptions.*.max' => 'Ogni descrizione allegato puo contenere al massimo 255 caratteri.',
|
||||
], [
|
||||
'newTicketTitolo' => 'titolo',
|
||||
|
|
@ -595,27 +613,25 @@ public function creaTicketRapido(): void
|
|||
->success()
|
||||
->send();
|
||||
|
||||
$this->newTicketTitolo = null;
|
||||
$this->newTicketDescrizione = null;
|
||||
$this->newTicketLuogo = null;
|
||||
$this->newTicketPriorita = 'Media';
|
||||
$this->newTicketCategoriaId = null;
|
||||
$this->newTicketTipoIntervento = 'altro';
|
||||
$this->newTicketCameraShots = [];
|
||||
$this->newTicketAttachments = [];
|
||||
$this->newTicketAttachmentDescriptions = [];
|
||||
$this->newTicketFotoNote = null;
|
||||
$this->newTicketTitolo = null;
|
||||
$this->newTicketDescrizione = null;
|
||||
$this->newTicketLuogo = null;
|
||||
$this->newTicketPriorita = 'Media';
|
||||
$this->newTicketCategoriaId = null;
|
||||
$this->newTicketTipoIntervento = 'altro';
|
||||
$this->newTicketFotoNote = null;
|
||||
$this->resetDraftUploadState();
|
||||
$this->refreshData();
|
||||
}
|
||||
|
||||
public function updatedNewTicketCameraShots(): void
|
||||
public function updatedPendingTicketCameraShots(): void
|
||||
{
|
||||
$this->syncAttachmentDescriptionSlots();
|
||||
$this->appendPendingUploads('camera');
|
||||
}
|
||||
|
||||
public function updatedNewTicketAttachments(): void
|
||||
public function updatedPendingTicketAttachments(): void
|
||||
{
|
||||
$this->syncAttachmentDescriptionSlots();
|
||||
$this->appendPendingUploads('attachment');
|
||||
}
|
||||
|
||||
public function removeNewTicketFile(int $index): void
|
||||
|
|
@ -635,7 +651,7 @@ public function removeNewTicketFile(int $index): void
|
|||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{index:int,name:string,mime:string,is_image:bool,preview_url:?string,description:string}>
|
||||
* @return array<int, array{index:int,name:string,original_name:string,protocol:string,mime:string,is_image:bool,preview_url:?string,description:string}>
|
||||
*/
|
||||
public function getSelectedUploadsProperty(): array
|
||||
{
|
||||
|
|
@ -647,9 +663,10 @@ public function getSelectedUploadsProperty(): array
|
|||
continue;
|
||||
}
|
||||
|
||||
$name = (string) (method_exists($file, 'getClientOriginalName') ? $file->getClientOriginalName() : ('file-' . $index));
|
||||
$mime = (string) (method_exists($file, 'getClientMimeType') ? $file->getClientMimeType() : 'application/octet-stream');
|
||||
$isImage = str_starts_with($mime, 'image/');
|
||||
$name = (string) (method_exists($file, 'getClientOriginalName') ? $file->getClientOriginalName() : ('file-' . $index));
|
||||
$mime = (string) (method_exists($file, 'getClientMimeType') ? $file->getClientMimeType() : 'application/octet-stream');
|
||||
$isImage = str_starts_with($mime, 'image/');
|
||||
$protocol = $this->buildDraftProtocolCode((int) $index, $isImage);
|
||||
|
||||
$previewUrl = null;
|
||||
if ($isImage && method_exists($file, 'temporaryUrl')) {
|
||||
|
|
@ -661,12 +678,14 @@ public function getSelectedUploadsProperty(): array
|
|||
}
|
||||
|
||||
$rows[] = [
|
||||
'index' => (int) $index,
|
||||
'name' => $name,
|
||||
'mime' => $mime,
|
||||
'is_image' => $isImage,
|
||||
'preview_url' => $previewUrl,
|
||||
'description' => (string) ($this->newTicketAttachmentDescriptions[$index] ?? ''),
|
||||
'index' => (int) $index,
|
||||
'name' => $this->buildDraftUploadDisplayName((int) $index, $name, $isImage),
|
||||
'original_name' => $name,
|
||||
'protocol' => $protocol,
|
||||
'mime' => $mime,
|
||||
'is_image' => $isImage,
|
||||
'preview_url' => $previewUrl,
|
||||
'description' => (string) ($this->newTicketAttachmentDescriptions[$index] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -702,7 +721,11 @@ private function salvaAllegatiTicket(Ticket $ticket, int $userId): int
|
|||
}
|
||||
|
||||
try {
|
||||
$stored = app(TicketAttachmentUploadService::class)->store($file, 'ticket-mobile/' . $ticket->id);
|
||||
$displayName = $this->buildStoredUploadDisplayName($ticket, (int) $index, $file);
|
||||
$stored = app(TicketAttachmentUploadService::class)->store($file, 'ticket-mobile/' . $ticket->id, [
|
||||
'stored_basename' => pathinfo($displayName, PATHINFO_FILENAME),
|
||||
'display_name' => $displayName,
|
||||
]);
|
||||
|
||||
TicketAttachment::query()->create([
|
||||
'ticket_id' => (int) $ticket->id,
|
||||
|
|
@ -712,7 +735,12 @@ private function salvaAllegatiTicket(Ticket $ticket, int $userId): int
|
|||
'original_file_name' => $stored['original_name'],
|
||||
'mime_type' => $stored['mime'],
|
||||
'size' => $stored['size'],
|
||||
'description' => $this->resolveAttachmentDescription((int) $index),
|
||||
'description' => $this->resolveAttachmentDescription(
|
||||
(int) $index,
|
||||
is_array($stored['metadata'] ?? null) ? $stored['metadata'] : [],
|
||||
$stored['original_name'] ?? null,
|
||||
$stored['source_original_name'] ?? null,
|
||||
),
|
||||
]);
|
||||
|
||||
$saved++;
|
||||
|
|
@ -739,18 +767,152 @@ private function syncAttachmentDescriptionSlots(): void
|
|||
$this->newTicketAttachmentDescriptions = $next;
|
||||
}
|
||||
|
||||
private function resolveAttachmentDescription(int $index): string
|
||||
private function resolveAttachmentDescription(int $index, array $metadata = [], ?string $displayName = null, ?string $sourceOriginalName = null): string
|
||||
{
|
||||
$parts = [];
|
||||
$specific = trim((string) ($this->newTicketAttachmentDescriptions[$index] ?? ''));
|
||||
if ($specific !== '') {
|
||||
return $specific;
|
||||
$parts[] = $specific;
|
||||
} elseif (filled($this->newTicketFotoNote)) {
|
||||
$parts[] = 'Nota foto: ' . trim((string) $this->newTicketFotoNote);
|
||||
} else {
|
||||
$parts[] = 'Allegato da Ticket Mobile';
|
||||
}
|
||||
|
||||
if (filled($this->newTicketFotoNote)) {
|
||||
return 'Nota foto: ' . trim((string) $this->newTicketFotoNote);
|
||||
if (filled($displayName)) {
|
||||
$parts[] = 'File: ' . trim((string) $displayName);
|
||||
}
|
||||
|
||||
return 'Allegato da Ticket Mobile';
|
||||
$sourceName = trim((string) $sourceOriginalName);
|
||||
if ($sourceName !== '' && $sourceName !== trim((string) $displayName)) {
|
||||
$parts[] = 'Orig: ' . $sourceName;
|
||||
}
|
||||
|
||||
$exifDate = trim((string) ($metadata['exif_datetime'] ?? ''));
|
||||
if ($exifDate !== '') {
|
||||
$parts[] = 'EXIF: ' . $exifDate;
|
||||
}
|
||||
|
||||
$device = trim((string) ($metadata['device'] ?? ''));
|
||||
if ($device !== '') {
|
||||
$parts[] = 'Device: ' . $device;
|
||||
}
|
||||
|
||||
$lat = $metadata['gps_decimal']['lat'] ?? null;
|
||||
$lng = $metadata['gps_decimal']['lng'] ?? null;
|
||||
if (is_numeric($lat) && is_numeric($lng)) {
|
||||
$parts[] = 'GPS: ' . number_format((float) $lat, 5, '.', '') . ',' . number_format((float) $lng, 5, '.', '');
|
||||
|
||||
$mapsUrl = trim((string) ($metadata['google_maps_url'] ?? ''));
|
||||
if ($mapsUrl !== '') {
|
||||
$parts[] = 'Maps: ' . $mapsUrl;
|
||||
}
|
||||
}
|
||||
|
||||
return Str::limit(implode(' | ', array_filter($parts)), 255);
|
||||
}
|
||||
|
||||
private function appendPendingUploads(string $source): void
|
||||
{
|
||||
$pendingProperty = $source === 'camera' ? 'pendingTicketCameraShots' : 'pendingTicketAttachments';
|
||||
$targetProperty = $source === 'camera' ? 'newTicketCameraShots' : 'newTicketAttachments';
|
||||
$pending = array_values(array_filter($this->{$pendingProperty}, fn($file) => is_object($file)));
|
||||
|
||||
if ($pending === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$currentTotal = count($this->newTicketCameraShots) + count($this->newTicketAttachments);
|
||||
$available = max(0, 10 - $currentTotal);
|
||||
|
||||
if ($available <= 0) {
|
||||
$this->{$pendingProperty} = [];
|
||||
|
||||
Notification::make()
|
||||
->title('Limite allegati raggiunto')
|
||||
->body('Puoi tenere in coda al massimo 10 file totali per ticket.')
|
||||
->warning()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$accepted = array_slice($pending, 0, $available);
|
||||
$this->{$targetProperty} = array_values(array_merge($this->{$targetProperty}, $accepted));
|
||||
$this->{$pendingProperty} = [];
|
||||
$this->syncAttachmentDescriptionSlots();
|
||||
|
||||
$discarded = count($pending) - count($accepted);
|
||||
if ($discarded > 0) {
|
||||
Notification::make()
|
||||
->title('Coda allegati ridotta')
|
||||
->body('Sono stati accodati solo i primi ' . count($accepted) . ' file disponibili: limite massimo 10.')
|
||||
->warning()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
||||
private function resetDraftUploadState(): void
|
||||
{
|
||||
$this->newTicketCameraShots = [];
|
||||
$this->newTicketAttachments = [];
|
||||
$this->pendingTicketCameraShots = [];
|
||||
$this->pendingTicketAttachments = [];
|
||||
$this->newTicketAttachmentDescriptions = [];
|
||||
$this->newTicketDraftProtocol = 'TM-' . now()->format('Ymd-His');
|
||||
}
|
||||
|
||||
private function buildDraftProtocolCode(int $index, bool $isImage): string
|
||||
{
|
||||
return $this->newTicketDraftProtocol . '-' . ($isImage ? 'F' : 'A') . str_pad((string) ($index + 1), 2, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
private function buildDraftUploadDisplayName(int $index, string $originalName, bool $isImage): string
|
||||
{
|
||||
$extension = strtolower((string) pathinfo($originalName, PATHINFO_EXTENSION));
|
||||
|
||||
return $this->buildDraftProtocolCode($index, $isImage) . ($extension !== '' ? '.' . $extension : '');
|
||||
}
|
||||
|
||||
private function buildStoredUploadDisplayName(Ticket $ticket, int $index, object $file): string
|
||||
{
|
||||
$originalName = (string) (method_exists($file, 'getClientOriginalName') ? $file->getClientOriginalName() : ('file-' . $index));
|
||||
$extension = strtolower((string) pathinfo($originalName, PATHINFO_EXTENSION));
|
||||
$mime = (string) (method_exists($file, 'getClientMimeType') ? $file->getClientMimeType() : 'application/octet-stream');
|
||||
$isImage = str_starts_with($mime, 'image/');
|
||||
$baseName = 'ticket-' . str_pad((string) $ticket->id, 6, '0', STR_PAD_LEFT) . '-' . ($isImage ? 'foto-' : 'all-') . str_pad((string) ($index + 1), 2, '0', STR_PAD_LEFT);
|
||||
|
||||
return $baseName . ($extension !== '' ? '.' . $extension : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, int>
|
||||
*/
|
||||
private function resolveTicketScopeStabileIds(bool $includeAllAccessible = false): array
|
||||
{
|
||||
$user = Auth::user();
|
||||
if (! $user instanceof User) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$accessibleIds = StabileContext::accessibleStabili($user)
|
||||
->pluck('id')
|
||||
->map(fn($value) => (int) $value)
|
||||
->filter(fn(int $value) => $value > 0)
|
||||
->values()
|
||||
->all();
|
||||
|
||||
if ($accessibleIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($includeAllAccessible) {
|
||||
return $accessibleIds;
|
||||
}
|
||||
|
||||
$activeId = StabileContext::resolveActiveStabileId($user);
|
||||
|
||||
return $activeId ? [$activeId] : [(int) $accessibleIds[0]];
|
||||
}
|
||||
|
||||
private function prefillTicketFromLiveCall(string $phone, string $line): void
|
||||
|
|
@ -822,14 +984,14 @@ private function aggiornaStatoTicket(int $ticketId, string $nuovoStato, bool $as
|
|||
return;
|
||||
}
|
||||
|
||||
$stabileId = StabileContext::resolveActiveStabileId($user);
|
||||
if (! $stabileId) {
|
||||
$stabileIds = $this->resolveTicketScopeStabileIds(true);
|
||||
if ($stabileIds === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ticket = Ticket::query()
|
||||
->where('id', $ticketId)
|
||||
->where('stabile_id', $stabileId)
|
||||
->whereIn('stabile_id', $stabileIds)
|
||||
->first();
|
||||
|
||||
if (! $ticket) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Filament;
|
||||
|
||||
use App\Filament\Pages\Strumenti\PostItGestione;
|
||||
use App\Models\ChiamataPostIt;
|
||||
use App\Models\User;
|
||||
use App\Services\Cti\LiveIncomingCallService;
|
||||
|
|
@ -44,7 +44,7 @@ public function createPostIt(): void
|
|||
return;
|
||||
}
|
||||
|
||||
ChiamataPostIt::query()->create([
|
||||
$postIt = ChiamataPostIt::query()->create([
|
||||
'stabile_id' => (int) $stabileId,
|
||||
'creato_da_user_id' => (int) $user->id,
|
||||
'rubrica_id' => (int) ($this->liveIncomingCall['rubrica_id'] ?? 0) ?: null,
|
||||
|
|
@ -61,6 +61,14 @@ public function createPostIt(): void
|
|||
]);
|
||||
|
||||
Notification::make()->title('Post-it creato dalla testata')->success()->send();
|
||||
|
||||
$this->redirect(
|
||||
PostItGestione::getUrl([
|
||||
'tab' => 'storico',
|
||||
'focus_post_it' => (int) $postIt->id,
|
||||
], panel: 'admin-filament'),
|
||||
navigate: true,
|
||||
);
|
||||
}
|
||||
|
||||
public function openTicketMobile()
|
||||
|
|
@ -76,4 +84,4 @@ public function render()
|
|||
{
|
||||
return view('livewire.filament.topbar-live-call');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,9 +15,12 @@
|
|||
<button type="button" wire:click="$set('activeTab', 'storico')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $activeTab === 'storico' ? 'bg-indigo-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">
|
||||
Storico Post-it
|
||||
</button>
|
||||
<button type="button" wire:click="$set('activeTab', 'tecnico')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $activeTab === 'tecnico' ? 'bg-indigo-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">
|
||||
<button type="button" wire:click="$set('activeTab', 'smdr')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $activeTab === 'smdr' ? 'bg-indigo-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">
|
||||
Chiamate Tecniche SMDR
|
||||
</button>
|
||||
<button type="button" wire:click="$set('activeTab', 'csta')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $activeTab === 'csta' ? 'bg-indigo-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">
|
||||
CTI / CSTA e Click-to-Call
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -32,10 +35,16 @@
|
|||
|
||||
<div class="space-y-3">
|
||||
@forelse($this->recenti as $riga)
|
||||
<div class="rounded-lg border p-3">
|
||||
<div @class([
|
||||
'rounded-lg border p-3',
|
||||
'border-emerald-400 bg-emerald-50/60 ring-1 ring-emerald-200' => (int) ($this->focusPostItId ?? 0) === (int) $riga->id,
|
||||
])>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div>
|
||||
<div class="font-medium">{{ $riga->oggetto ?: 'Senza oggetto' }}</div>
|
||||
@if((int) ($this->focusPostItId ?? 0) === (int) $riga->id)
|
||||
<div class="mt-1 text-[11px] font-semibold uppercase tracking-wide text-emerald-700">Post-it appena creato</div>
|
||||
@endif
|
||||
<div class="text-sm text-gray-600">
|
||||
{{ $riga->nome_chiamante ?: 'Chiamante non identificato' }}
|
||||
@if($riga->telefono)
|
||||
|
|
@ -93,10 +102,59 @@
|
|||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
@elseif(in_array($activeTab, ['smdr', 'csta'], true))
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<h2 class="mb-1 text-base font-semibold">Elenco tecnico chiamate (1 riga = 1 evento)</h2>
|
||||
<p class="mb-3 text-xs text-gray-500">Qui vedi in colonne i dati disponibili dal centralino: interno, linea/trunk, numero, durata, costo, utente assegnato, stabile, raw line, ecc.</p>
|
||||
@php($isCstaTab = $activeTab === 'csta')
|
||||
<h2 class="mb-1 text-base font-semibold">{{ $isCstaTab ? 'Elenco tecnico CTI / CSTA' : 'Elenco tecnico chiamate SMDR' }}</h2>
|
||||
<p class="mb-3 text-xs text-gray-500">
|
||||
{{ $isCstaTab
|
||||
? 'Vista test PBX per eventi CSTA, provider, interno coinvolto e richieste click-to-call. Il provider viene letto dai metadati così il cruscotto resta riusabile anche con FreePBX o altri centralini.'
|
||||
: 'Qui vedi in colonne i dati disponibili dal centralino via SMDR: interno, linea/trunk, numero, durata, costo, utente assegnato, stabile e raw line.' }}
|
||||
</p>
|
||||
|
||||
@if($isCstaTab)
|
||||
<div class="mb-4 rounded-lg border border-blue-200 bg-blue-50 p-3 text-xs text-blue-900">
|
||||
<div class="font-semibold">Controllo chiamate PBX</div>
|
||||
<div class="mt-1">Da questo tab puoi verificare gli eventi CSTA e registrare richieste click-to-call verso il PBX dell'interno assegnato. La UI non e vincolata a Panasonic: il provider e esposto come metadato.</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 rounded-lg border border-slate-200 bg-slate-50 p-3">
|
||||
<div class="mb-2 flex items-center justify-between gap-2">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-slate-600">Ultime richieste click-to-call</div>
|
||||
<div class="text-[11px] text-slate-500">Fonte: coda locale PBX</div>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full border text-[11px]">
|
||||
<thead class="bg-white">
|
||||
<tr>
|
||||
<th class="border px-2 py-1 text-left">Richiesta</th>
|
||||
<th class="border px-2 py-1 text-left">Interno</th>
|
||||
<th class="border px-2 py-1 text-left">Numero</th>
|
||||
<th class="border px-2 py-1 text-left">Stato</th>
|
||||
<th class="border px-2 py-1 text-left">Provider</th>
|
||||
<th class="border px-2 py-1 text-left">Quando</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($this->recentClickToCallRequests as $req)
|
||||
<tr>
|
||||
<td class="border px-2 py-1">#{{ $req->id }}</td>
|
||||
<td class="border px-2 py-1">{{ $req->source_extension ?: '-' }}</td>
|
||||
<td class="border px-2 py-1">{{ $req->target_number ?: '-' }}</td>
|
||||
<td class="border px-2 py-1">{{ $req->status ?: '-' }}</td>
|
||||
<td class="border px-2 py-1">{{ strtoupper(str_replace('_', ' ', (string) data_get($req->metadata, 'provider', data_get($req->metadata, 'source_channel', '-')))) }}</td>
|
||||
<td class="border px-2 py-1">{{ optional($req->requested_at)->format('d/m/Y H:i:s') ?: optional($req->created_at)->format('d/m/Y H:i:s') }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="border px-2 py-3 text-center text-slate-500">Nessuna richiesta click-to-call registrata per il contesto utente corrente.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mb-4 grid grid-cols-1 gap-3 rounded-lg border border-slate-200 bg-slate-50 p-3 md:grid-cols-4">
|
||||
<div>
|
||||
|
|
@ -137,10 +195,11 @@
|
|||
<tr>
|
||||
<th class="border px-2 py-1 text-left">ID</th>
|
||||
<th class="border px-2 py-1 text-left">Data/Ora</th>
|
||||
<th class="border px-2 py-1 text-left">Provider</th>
|
||||
<th class="border px-2 py-1 text-left">Direzione</th>
|
||||
<th class="border px-2 py-1 text-left">Ambito</th>
|
||||
<th class="border px-2 py-1 text-left">Interno</th>
|
||||
<th class="border px-2 py-1 text-left">Linea/CO</th>
|
||||
<th class="border px-2 py-1 text-left">Linea / Evento</th>
|
||||
<th class="border px-2 py-1 text-left">Numero</th>
|
||||
<th class="border px-2 py-1 text-left">Nominativo</th>
|
||||
<th class="border px-2 py-1 text-left">Durata</th>
|
||||
|
|
@ -162,10 +221,11 @@
|
|||
<tr>
|
||||
<td class="border px-2 py-1">{{ $m->id }}</td>
|
||||
<td class="border px-2 py-1">{{ optional($m->received_at)->format('d/m/Y H:i:s') }}</td>
|
||||
<td class="border px-2 py-1">{{ $this->getTecnicoProviderLabel($m) }}</td>
|
||||
<td class="border px-2 py-1">{{ $m->direction }}</td>
|
||||
<td class="border px-2 py-1">{{ $this->getTecnicoScopeLabel($m) }}</td>
|
||||
<td class="border px-2 py-1">{{ $this->getTecnicoInternoLabel($m) }}</td>
|
||||
<td class="border px-2 py-1">{{ (string) ($smdr['co'] ?? '-') }}</td>
|
||||
<td class="border px-2 py-1">{{ $this->getTecnicoLineaLabel($m) }}</td>
|
||||
<td class="border px-2 py-1">{{ (string) ($m->phone_number ?: ($smdr['dial_number'] ?? '-')) }}</td>
|
||||
<td class="border px-2 py-1">{{ $rubricaNomeTech ?: '-' }}</td>
|
||||
<td class="border px-2 py-1">{{ $duration !== '' ? $duration : '-' }}</td>
|
||||
|
|
@ -179,6 +239,9 @@
|
|||
@if($rubricaUrlTech && ! $this->isInternalSmdrMessage($m))
|
||||
<a href="{{ $rubricaUrlTech }}" class="inline-flex items-center rounded-md border px-2 py-1 text-[11px] text-indigo-700 hover:bg-indigo-50">Apri Rubrica</a>
|
||||
@endif
|
||||
@if($activeTab === 'csta')
|
||||
<x-filament::button size="xs" color="info" wire:click="richiediClickToCallDaMessaggio({{ (int) $m->id }})">Click-to-call</x-filament::button>
|
||||
@endif
|
||||
@if(!$m->post_it_id)
|
||||
<x-filament::button size="xs" wire:click="creaPostItDaMessaggio({{ (int) $m->id }})">Crea Post-it</x-filament::button>
|
||||
@else
|
||||
|
|
@ -188,7 +251,7 @@
|
|||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="15" class="border px-2 py-3 text-center text-gray-500">Nessun evento SMDR disponibile.</td>
|
||||
<td colspan="16" class="border px-2 py-3 text-center text-gray-500">{{ $isCstaTab ? 'Nessun evento CSTA disponibile.' : 'Nessun evento SMDR disponibile.' }}</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user