feat: consolida post-it pbx e continuita operativa
This commit is contained in:
parent
72661559df
commit
236aef53cd
|
|
@ -11,6 +11,10 @@ ## [Unreleased]
|
||||||
- Added live-call CRM improvements in Ticket Mobile and Post-it Gestione: cleaner call context, operator note before conversion, clearer SMDR/CSTA labels, and line/group filtering.
|
- Added live-call CRM improvements in Ticket Mobile and Post-it Gestione: cleaner call context, operator note before conversion, clearer SMDR/CSTA labels, and line/group filtering.
|
||||||
- Extended PBX day0 preset coverage to include main/admin lines `0001` and `0003`, with aligned studio watchlist for broader staging monitoring.
|
- Extended PBX day0 preset coverage to include main/admin lines `0001` and `0003`, with aligned studio watchlist for broader staging monitoring.
|
||||||
- Documented current CTI staging status and Windows restart procedure so the deployment trail remains in-repo even if the active chat is interrupted.
|
- Documented current CTI staging status and Windows restart procedure so the deployment trail remains in-repo even if the active chat is interrupted.
|
||||||
|
- Added grouped Post-it call boxes, direct Post-it opening from grouped history, and explicit Post-it assignment to operatore, fornitore, or collaboratore.
|
||||||
|
- Extended Scheda Amministratore PBX/TAPI with observed tokens, multi-line collaborator mapping, and inline PBX flag editing.
|
||||||
|
- Included safe rubrica-link QA repair in the Git-first staging sync flow from Supporto > Modifiche.
|
||||||
|
- Documented the modular coworking direction of NetGescon and the first server-distribution path for isolated debug nodes.
|
||||||
|
|
||||||
## [0.1.0] - 2026-01-17
|
## [0.1.0] - 2026-01-17
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -290,6 +290,15 @@ public function handle(): int
|
||||||
return self::FAILURE;
|
return self::FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->writeProgress(80, 'Ricucitura legami rubrica CRM', 'running');
|
||||||
|
$rubricaQaExit = 0;
|
||||||
|
try {
|
||||||
|
$rubricaQaExit = Artisan::call('gescon:qa-rubrica-legami', ['--apply-safe' => true]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$rubricaQaExit = 1;
|
||||||
|
$this->warn('Ricucitura legami rubrica non completata: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
$this->writeProgress(82, 'Pulizia cache applicativa', 'running');
|
$this->writeProgress(82, 'Pulizia cache applicativa', 'running');
|
||||||
$this->callSilently('optimize:clear');
|
$this->callSilently('optimize:clear');
|
||||||
|
|
||||||
|
|
@ -312,6 +321,7 @@ public function handle(): int
|
||||||
'repo_path' => $repoPath,
|
'repo_path' => $repoPath,
|
||||||
'deploy_path' => $deployPath,
|
'deploy_path' => $deployPath,
|
||||||
'mode' => $deploySync ? 'external-repo-rsync' : 'in-place-git',
|
'mode' => $deploySync ? 'external-repo-rsync' : 'in-place-git',
|
||||||
|
'rubrica_legami_qa' => $rubricaQaExit === 0 ? 'completed' : 'warning',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->info('Sync Git completata con successo.');
|
$this->info('Sync Git completata con successo.');
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,21 @@ class SchedaAmministratore extends Page implements HasForms
|
||||||
/** @var array<int,string> */
|
/** @var array<int,string> */
|
||||||
public array $collaboratorePbxExtension = [];
|
public array $collaboratorePbxExtension = [];
|
||||||
|
|
||||||
|
/** @var array<int,string> */
|
||||||
|
public array $collaboratorePbxExtensions = [];
|
||||||
|
|
||||||
|
/** @var array<int,string> */
|
||||||
|
public array $collaboratorePbxGroups = [];
|
||||||
|
|
||||||
|
/** @var array<int,string> */
|
||||||
|
public array $collaboratorePbxLines = [];
|
||||||
|
|
||||||
|
/** @var array<int,bool> */
|
||||||
|
public array $collaboratorePbxPopupEnabled = [];
|
||||||
|
|
||||||
|
/** @var array<int,bool> */
|
||||||
|
public array $collaboratorePbxClickToCallEnabled = [];
|
||||||
|
|
||||||
public Amministratore $amministratore;
|
public Amministratore $amministratore;
|
||||||
|
|
||||||
public static function canAccess(): bool
|
public static function canAccess(): bool
|
||||||
|
|
@ -1284,6 +1299,8 @@ public function getAccessoAmministratoreRowsProperty(): array
|
||||||
*/
|
*/
|
||||||
public function getCollaboratoriCentralinoRowsProperty(): array
|
public function getCollaboratoriCentralinoRowsProperty(): array
|
||||||
{
|
{
|
||||||
|
$mappings = (array) Arr::get($this->amministratore->impostazioni ?? [], 'pbx.user_mappings', []);
|
||||||
|
|
||||||
$query = User::query()
|
$query = User::query()
|
||||||
->with('roles')
|
->with('roles')
|
||||||
->whereHas('roles', function ($q): void {
|
->whereHas('roles', function ($q): void {
|
||||||
|
|
@ -1298,7 +1315,21 @@ public function getCollaboratoriCentralinoRowsProperty(): array
|
||||||
return $query
|
return $query
|
||||||
->get()
|
->get()
|
||||||
->map(function (User $user): array {
|
->map(function (User $user): array {
|
||||||
|
$mapping = (array) ($mappings[(string) $user->id] ?? []);
|
||||||
|
$extensions = array_values(array_filter((array) ($mapping['extensions'] ?? []), fn($value): bool => is_string($value) && trim($value) !== ''));
|
||||||
|
$groups = array_values(array_filter((array) ($mapping['groups'] ?? []), fn($value): bool => is_string($value) && trim($value) !== ''));
|
||||||
|
$lines = array_values(array_filter((array) ($mapping['lines'] ?? []), fn($value): bool => is_string($value) && trim($value) !== ''));
|
||||||
|
|
||||||
|
if ($extensions === [] && filled($user->pbx_extension)) {
|
||||||
|
$extensions = [(string) $user->pbx_extension];
|
||||||
|
}
|
||||||
|
|
||||||
$this->collaboratorePbxExtension[(int) $user->id] = (string) ($user->pbx_extension ?? '');
|
$this->collaboratorePbxExtension[(int) $user->id] = (string) ($user->pbx_extension ?? '');
|
||||||
|
$this->collaboratorePbxExtensions[(int) $user->id] = implode(', ', $extensions);
|
||||||
|
$this->collaboratorePbxGroups[(int) $user->id] = implode(', ', $groups);
|
||||||
|
$this->collaboratorePbxLines[(int) $user->id] = implode(', ', $lines);
|
||||||
|
$this->collaboratorePbxPopupEnabled[(int) $user->id] = (bool) ($user->pbx_popup_enabled ?? false);
|
||||||
|
$this->collaboratorePbxClickToCallEnabled[(int) $user->id] = (bool) ($user->pbx_click_to_call_enabled ?? false);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'user_id' => (int) $user->id,
|
'user_id' => (int) $user->id,
|
||||||
|
|
@ -1306,6 +1337,9 @@ public function getCollaboratoriCentralinoRowsProperty(): array
|
||||||
'email' => (string) $user->email,
|
'email' => (string) $user->email,
|
||||||
'ruoli' => $user->roles->pluck('name')->values()->all(),
|
'ruoli' => $user->roles->pluck('name')->values()->all(),
|
||||||
'pbx_extension' => (string) ($user->pbx_extension ?? ''),
|
'pbx_extension' => (string) ($user->pbx_extension ?? ''),
|
||||||
|
'pbx_extensions' => $this->collaboratorePbxExtensions[(int) $user->id],
|
||||||
|
'pbx_groups' => $this->collaboratorePbxGroups[(int) $user->id],
|
||||||
|
'pbx_lines' => $this->collaboratorePbxLines[(int) $user->id],
|
||||||
'pbx_popup_enabled' => (bool) ($user->pbx_popup_enabled ?? false),
|
'pbx_popup_enabled' => (bool) ($user->pbx_popup_enabled ?? false),
|
||||||
'pbx_click_to_call_enabled' => (bool) ($user->pbx_click_to_call_enabled ?? false),
|
'pbx_click_to_call_enabled' => (bool) ($user->pbx_click_to_call_enabled ?? false),
|
||||||
];
|
];
|
||||||
|
|
@ -1313,6 +1347,56 @@ public function getCollaboratoriCentralinoRowsProperty(): array
|
||||||
->all();
|
->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{extensions:array<int,string>,groups:array<int,string>,lines:array<int,string>}
|
||||||
|
*/
|
||||||
|
public function getPbxObservedTokensProperty(): array
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('communication_messages')) {
|
||||||
|
return [
|
||||||
|
'extensions' => [],
|
||||||
|
'groups' => [],
|
||||||
|
'lines' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = $this->pbxRawMessagesQuery()
|
||||||
|
->latest('id')
|
||||||
|
->limit(300)
|
||||||
|
->get(['target_extension', 'message_text', 'metadata']);
|
||||||
|
|
||||||
|
$extensions = [];
|
||||||
|
$groups = [];
|
||||||
|
$lines = [];
|
||||||
|
|
||||||
|
foreach ($rows as $message) {
|
||||||
|
$metadata = (array) ($message->metadata ?? []);
|
||||||
|
|
||||||
|
$this->collectPbxTokens($extensions, [
|
||||||
|
(string) ($message->target_extension ?? ''),
|
||||||
|
(string) data_get($metadata, 'called_extension', ''),
|
||||||
|
(string) data_get($metadata, 'source_extension', ''),
|
||||||
|
], '/\b(?:EXT\d+|\d{2,5})\b/i');
|
||||||
|
|
||||||
|
$this->collectPbxTokens($groups, [
|
||||||
|
(string) data_get($metadata, 'message_text', ''),
|
||||||
|
(string) ($message->message_text ?? ''),
|
||||||
|
], '/\bGRP\d+\b/i');
|
||||||
|
|
||||||
|
$this->collectPbxTokens($lines, [
|
||||||
|
(string) data_get($metadata, 'smdr.co', ''),
|
||||||
|
(string) data_get($metadata, 'line', ''),
|
||||||
|
(string) data_get($metadata, 'did', ''),
|
||||||
|
], '/\b(?:\d{3,6}|EXT\d+|GRP\d+)\b/i');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'extensions' => array_slice(array_values(array_unique($extensions)), 0, 24),
|
||||||
|
'groups' => array_slice(array_values(array_unique($groups)), 0, 24),
|
||||||
|
'lines' => array_slice(array_values(array_unique($lines)), 0, 24),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @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}
|
* @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}
|
||||||
*/
|
*/
|
||||||
|
|
@ -1577,16 +1661,78 @@ public function salvaInternoCollaboratore(int $userId): void
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$extension = trim((string) ($this->collaboratorePbxExtension[$userId] ?? ''));
|
$extensions = $this->normalizePbxMappingInput((string) ($this->collaboratorePbxExtensions[$userId] ?? $this->collaboratorePbxExtension[$userId] ?? ''));
|
||||||
$user->pbx_extension = $extension !== '' ? $extension : null;
|
$groups = $this->normalizePbxMappingInput((string) ($this->collaboratorePbxGroups[$userId] ?? ''));
|
||||||
|
$lines = $this->normalizePbxMappingInput((string) ($this->collaboratorePbxLines[$userId] ?? ''));
|
||||||
|
|
||||||
|
$user->pbx_extension = $extensions[0] ?? null;
|
||||||
|
$user->pbx_popup_enabled = (bool) ($this->collaboratorePbxPopupEnabled[$userId] ?? false);
|
||||||
|
$user->pbx_click_to_call_enabled = (bool) ($this->collaboratorePbxClickToCallEnabled[$userId] ?? false);
|
||||||
$user->save();
|
$user->save();
|
||||||
|
|
||||||
|
$impostazioni = $this->amministratore->impostazioni ?? [];
|
||||||
|
$pbx = (array) Arr::get($impostazioni, 'pbx', []);
|
||||||
|
$mappings = (array) ($pbx['user_mappings'] ?? []);
|
||||||
|
|
||||||
|
$mappings[(string) $userId] = [
|
||||||
|
'extensions' => $extensions,
|
||||||
|
'groups' => $groups,
|
||||||
|
'lines' => $lines,
|
||||||
|
];
|
||||||
|
|
||||||
|
$pbx['user_mappings'] = $mappings;
|
||||||
|
$impostazioni['pbx'] = $pbx;
|
||||||
|
$this->amministratore->impostazioni = $impostazioni;
|
||||||
|
$this->amministratore->save();
|
||||||
|
|
||||||
Notification::make()
|
Notification::make()
|
||||||
->title('Interno collaboratore aggiornato')
|
->title('Mappatura PBX collaboratore aggiornata')
|
||||||
->success()
|
->success()
|
||||||
->send();
|
->send();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int,string> $target
|
||||||
|
* @param array<int,string> $values
|
||||||
|
*/
|
||||||
|
private function collectPbxTokens(array &$target, array $values, string $pattern): void
|
||||||
|
{
|
||||||
|
foreach ($values as $value) {
|
||||||
|
if ($value === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match_all($pattern, strtoupper($value), $matches) > 0) {
|
||||||
|
foreach ((array) ($matches[0] ?? []) as $match) {
|
||||||
|
$token = trim((string) $match);
|
||||||
|
if ($token !== '') {
|
||||||
|
$target[] = $token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = trim(strtoupper($value));
|
||||||
|
if ($token !== '') {
|
||||||
|
$target[] = $token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int,string>
|
||||||
|
*/
|
||||||
|
private function normalizePbxMappingInput(string $value): array
|
||||||
|
{
|
||||||
|
$tokens = preg_split('/[\s,;]+/', strtoupper(trim($value)), -1, PREG_SPLIT_NO_EMPTY) ?: [];
|
||||||
|
|
||||||
|
return array_values(array_unique(array_filter(array_map(function ($token): string {
|
||||||
|
$clean = preg_replace('/[^A-Z0-9_-]/', '', (string) $token) ?? '';
|
||||||
|
return trim($clean);
|
||||||
|
}, $tokens), fn(string $token): bool => $token !== '')));
|
||||||
|
}
|
||||||
|
|
||||||
public function abilitaAccessoFornitore(int $dipendenteId): void
|
public function abilitaAccessoFornitore(int $dipendenteId): void
|
||||||
{
|
{
|
||||||
$dipendente = FornitoreDipendente::query()
|
$dipendente = FornitoreDipendente::query()
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,13 @@
|
||||||
namespace App\Filament\Pages\Strumenti;
|
namespace App\Filament\Pages\Strumenti;
|
||||||
|
|
||||||
use App\Models\ChiamataPostIt;
|
use App\Models\ChiamataPostIt;
|
||||||
|
use App\Models\Fornitore;
|
||||||
use App\Models\RubricaUniversale;
|
use App\Models\RubricaUniversale;
|
||||||
use App\Models\Stabile;
|
use App\Models\Stabile;
|
||||||
use App\Models\Ticket;
|
use App\Models\Ticket;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Support\PhoneNumber;
|
use App\Support\PhoneNumber;
|
||||||
|
use App\Support\StabileContext;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Pages\Page;
|
use Filament\Pages\Page;
|
||||||
|
|
@ -42,6 +44,11 @@ class PostIt extends Page
|
||||||
public string $priorita = 'Media';
|
public string $priorita = 'Media';
|
||||||
public ?int $rubricaId = null;
|
public ?int $rubricaId = null;
|
||||||
public ?int $stabileId = null;
|
public ?int $stabileId = null;
|
||||||
|
public ?int $focusPostItId = null;
|
||||||
|
public ?int $selectedPostItId = null;
|
||||||
|
public string $assegnazioneTipo = 'operatore';
|
||||||
|
public ?int $assegnazioneUserId = null;
|
||||||
|
public ?int $assegnazioneFornitoreId = null;
|
||||||
|
|
||||||
public ?int $selectedSuggerimentoId = null;
|
public ?int $selectedSuggerimentoId = null;
|
||||||
|
|
||||||
|
|
@ -53,6 +60,10 @@ public function mount(): void
|
||||||
$this->priorita = 'Media';
|
$this->priorita = 'Media';
|
||||||
$this->direzione = 'in_arrivo';
|
$this->direzione = 'in_arrivo';
|
||||||
$this->suggerimentiRubrica = collect();
|
$this->suggerimentiRubrica = collect();
|
||||||
|
|
||||||
|
$focus = (int) request()->query('focus_post_it', 0);
|
||||||
|
$this->focusPostItId = $focus > 0 ? $focus : null;
|
||||||
|
$this->selectedPostItId = $this->focusPostItId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getGestionePostItUrl(): string
|
public function getGestionePostItUrl(): string
|
||||||
|
|
@ -65,11 +76,95 @@ public function getGestioneTicketUrl(): string
|
||||||
return \App\Filament\Pages\Supporto\TicketGestione::getUrl(panel: 'admin-filament');
|
return \App\Filament\Pages\Supporto\TicketGestione::getUrl(panel: 'admin-filament');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function selectPostIt(int $postItId): void
|
||||||
|
{
|
||||||
|
$this->selectedPostItId = $postItId;
|
||||||
|
$this->loadSelectedPostItAssignment();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function assegnaPostIt(): void
|
||||||
|
{
|
||||||
|
if (! $this->isPostItTableReady()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$postIt = $this->selectedPostIt;
|
||||||
|
if (! $postIt) {
|
||||||
|
Notification::make()->title('Seleziona prima un Post-it')->warning()->send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->validate([
|
||||||
|
'assegnazioneTipo' => ['required', 'in:operatore,collaboratore,fornitore'],
|
||||||
|
'assegnazioneUserId' => ['nullable', 'integer', 'exists:users,id'],
|
||||||
|
'assegnazioneFornitoreId' => ['nullable', 'integer', 'exists:fornitori,id'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'assegnazione_tipo' => $this->assegnazioneTipo,
|
||||||
|
'assegnato_a_user_id' => null,
|
||||||
|
'assegnato_a_fornitore_id' => null,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (in_array($this->assegnazioneTipo, ['operatore', 'collaboratore'], true)) {
|
||||||
|
if (! $this->assegnazioneUserId) {
|
||||||
|
Notification::make()->title('Seleziona un utente')->warning()->send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload['assegnato_a_user_id'] = $this->assegnazioneUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->assegnazioneTipo === 'fornitore') {
|
||||||
|
if (! $this->assegnazioneFornitoreId) {
|
||||||
|
Notification::make()->title('Seleziona un fornitore')->warning()->send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload['assegnato_a_fornitore_id'] = $this->assegnazioneFornitoreId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$postIt->update($payload);
|
||||||
|
|
||||||
|
if ($postIt->ticket) {
|
||||||
|
$ticketPayload = [];
|
||||||
|
|
||||||
|
if ($this->assegnazioneTipo === 'fornitore') {
|
||||||
|
$ticketPayload['assegnato_a_fornitore_id'] = $this->assegnazioneFornitoreId;
|
||||||
|
$ticketPayload['assegnato_a_user_id'] = null;
|
||||||
|
} else {
|
||||||
|
$ticketPayload['assegnato_a_user_id'] = $this->assegnazioneUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ticketPayload !== []) {
|
||||||
|
$postIt->ticket->update($ticketPayload);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method_exists($postIt->ticket, 'messages') && Schema::hasTable('ticket_messages')) {
|
||||||
|
$postIt->ticket->messages()->create([
|
||||||
|
'user_id' => Auth::id(),
|
||||||
|
'messaggio' => 'Assegnazione da Post-it: ' . $this->getSelectedAssignmentLabel(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Notification::make()->title('Assegnazione Post-it aggiornata')->success()->send();
|
||||||
|
$this->selectedPostItId = (int) $postIt->id;
|
||||||
|
}
|
||||||
|
|
||||||
public function isPostItTableReady(): bool
|
public function isPostItTableReady(): bool
|
||||||
{
|
{
|
||||||
return Schema::hasTable('chiamate_post_it');
|
return Schema::hasTable('chiamate_post_it');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isPostItAssignmentReady(): bool
|
||||||
|
{
|
||||||
|
return $this->isPostItTableReady()
|
||||||
|
&& Schema::hasColumn('chiamate_post_it', 'assegnazione_tipo')
|
||||||
|
&& Schema::hasColumn('chiamate_post_it', 'assegnato_a_user_id')
|
||||||
|
&& Schema::hasColumn('chiamate_post_it', 'assegnato_a_fornitore_id');
|
||||||
|
}
|
||||||
|
|
||||||
public function updatedRicercaChiamante(?string $value): void
|
public function updatedRicercaChiamante(?string $value): void
|
||||||
{
|
{
|
||||||
$query = trim((string) $value);
|
$query = trim((string) $value);
|
||||||
|
|
@ -247,6 +342,8 @@ public function convertiInTicket(int $postItId): void
|
||||||
$ticket = Ticket::query()->create([
|
$ticket = Ticket::query()->create([
|
||||||
'stabile_id' => $postIt->stabile_id,
|
'stabile_id' => $postIt->stabile_id,
|
||||||
'aperto_da_user_id' => Auth::id(),
|
'aperto_da_user_id' => Auth::id(),
|
||||||
|
'assegnato_a_user_id' => $postIt->assegnazione_tipo !== 'fornitore' ? $postIt->assegnato_a_user_id : null,
|
||||||
|
'assegnato_a_fornitore_id' => $postIt->assegnazione_tipo === 'fornitore' ? $postIt->assegnato_a_fornitore_id : null,
|
||||||
'titolo' => $postIt->oggetto ?: ('Chiamata da ' . ($postIt->nome_chiamante ?: 'Contatto sconosciuto')),
|
'titolo' => $postIt->oggetto ?: ('Chiamata da ' . ($postIt->nome_chiamante ?: 'Contatto sconosciuto')),
|
||||||
'descrizione' => trim(($postIt->nota ?? '') . "\n\nRiferimento chiamata: #{$postIt->id}"),
|
'descrizione' => trim(($postIt->nota ?? '') . "\n\nRiferimento chiamata: #{$postIt->id}"),
|
||||||
'data_apertura' => now(),
|
'data_apertura' => now(),
|
||||||
|
|
@ -292,16 +389,81 @@ public function getRecentiProperty()
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return ChiamataPostIt::query()
|
$items = ChiamataPostIt::query()
|
||||||
->with(['rubrica', 'stabile', 'ticket', 'creatoDa'])
|
->with(['rubrica', 'stabile', 'ticket', 'creatoDa', 'assegnatoAUser:id,name', 'assegnatoAFornitore:id,ragione_sociale,nome,cognome'])
|
||||||
->orderByDesc('chiamata_il')
|
->orderByDesc('chiamata_il')
|
||||||
->limit(25)
|
->limit(25)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
|
if (! $this->selectedPostItId && $items->isNotEmpty()) {
|
||||||
|
$this->selectedPostItId = (int) ($this->focusPostItId ?: $items->first()->id);
|
||||||
|
$this->loadSelectedPostItAssignment();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $items;
|
||||||
} catch (QueryException) {
|
} catch (QueryException) {
|
||||||
return collect();
|
return collect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getSelectedPostItProperty(): ?ChiamataPostIt
|
||||||
|
{
|
||||||
|
if (! $this->isPostItTableReady() || ! $this->selectedPostItId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ChiamataPostIt::query()
|
||||||
|
->with(['stabile', 'ticket', 'assegnatoAUser:id,name', 'assegnatoAFornitore:id,ragione_sociale,nome,cognome'])
|
||||||
|
->find($this->selectedPostItId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOperatoriOptionsProperty(): array
|
||||||
|
{
|
||||||
|
return User::query()
|
||||||
|
->role(['super-admin', 'admin', 'amministratore'])
|
||||||
|
->orderBy('name')
|
||||||
|
->limit(100)
|
||||||
|
->pluck('name', 'id')
|
||||||
|
->map(fn($name): string => (string) $name)
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCollaboratoriOptionsProperty(): array
|
||||||
|
{
|
||||||
|
$adminId = $this->resolveCurrentAmministratoreId();
|
||||||
|
|
||||||
|
return User::query()
|
||||||
|
->whereHas('roles', function ($q): void {
|
||||||
|
$q->where('name', 'collaboratore');
|
||||||
|
})
|
||||||
|
->when($adminId > 0, function ($query) use ($adminId): void {
|
||||||
|
$query->whereHas('stabiliAssegnati', function ($inner) use ($adminId): void {
|
||||||
|
$inner->where('amministratore_id', $adminId);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->orderBy('name')
|
||||||
|
->limit(150)
|
||||||
|
->pluck('name', 'id')
|
||||||
|
->map(fn($name): string => (string) $name)
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFornitoriOptionsProperty(): array
|
||||||
|
{
|
||||||
|
$adminId = $this->resolveCurrentAmministratoreId();
|
||||||
|
|
||||||
|
return Fornitore::query()
|
||||||
|
->when($adminId > 0, fn($query) => $query->where('amministratore_id', $adminId))
|
||||||
|
->orderByRaw("COALESCE(ragione_sociale, CONCAT(COALESCE(nome, ''), ' ', COALESCE(cognome, '')))")
|
||||||
|
->limit(150)
|
||||||
|
->get(['id', 'ragione_sociale', 'nome', 'cognome'])
|
||||||
|
->mapWithKeys(function (Fornitore $fornitore): array {
|
||||||
|
$label = trim((string) ($fornitore->ragione_sociale ?: (($fornitore->nome ?? '') . ' ' . ($fornitore->cognome ?? ''))));
|
||||||
|
return [(int) $fornitore->id => ($label !== '' ? $label : ('Fornitore #' . $fornitore->id))];
|
||||||
|
})
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
private function creaTicketDirettoDaForm(): void
|
private function creaTicketDirettoDaForm(): void
|
||||||
{
|
{
|
||||||
$this->validate([
|
$this->validate([
|
||||||
|
|
@ -369,6 +531,43 @@ private function createPostItRecord(): ChiamataPostIt
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function loadSelectedPostItAssignment(): void
|
||||||
|
{
|
||||||
|
$postIt = $this->selectedPostIt;
|
||||||
|
if (! $postIt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assegnazioneTipo = (string) ($postIt->assegnazione_tipo ?: 'operatore');
|
||||||
|
$this->assegnazioneUserId = (int) ($postIt->assegnato_a_user_id ?? 0) ?: null;
|
||||||
|
$this->assegnazioneFornitoreId = (int) ($postIt->assegnato_a_fornitore_id ?? 0) ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getSelectedAssignmentLabel(): string
|
||||||
|
{
|
||||||
|
return match ($this->assegnazioneTipo) {
|
||||||
|
'fornitore' => 'fornitore #' . (int) ($this->assegnazioneFornitoreId ?? 0),
|
||||||
|
'collaboratore' => 'collaboratore #' . (int) ($this->assegnazioneUserId ?? 0),
|
||||||
|
default => 'operatore #' . (int) ($this->assegnazioneUserId ?? 0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveCurrentAmministratoreId(): int
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
if (! $user instanceof User) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->amministratore) {
|
||||||
|
return (int) $user->amministratore->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stabile = StabileContext::getActiveStabile($user);
|
||||||
|
|
||||||
|
return (int) ($stabile?->amministratore_id ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
private function resetFormState(): void
|
private function resetFormState(): void
|
||||||
{
|
{
|
||||||
$this->telefono = null;
|
$this->telefono = null;
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,9 @@ class PostItGestione extends Page
|
||||||
/** @var array<string, array{id:int|null,name:string|null}> */
|
/** @var array<string, array{id:int|null,name:string|null}> */
|
||||||
private array $pbxExtensionCache = [];
|
private array $pbxExtensionCache = [];
|
||||||
|
|
||||||
|
/** @var array<int, CommunicationMessage|null> */
|
||||||
|
private array $postItMessageCache = [];
|
||||||
|
|
||||||
public function mount(): void
|
public function mount(): void
|
||||||
{
|
{
|
||||||
$focus = (int) request()->query('focus_post_it', 0);
|
$focus = (int) request()->query('focus_post_it', 0);
|
||||||
|
|
@ -75,6 +78,11 @@ public function getPostItInserimentoUrl(): string
|
||||||
return PostIt::getUrl(panel: 'admin-filament');
|
return PostIt::getUrl(panel: 'admin-filament');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getPostItPageUrl(int $postItId): string
|
||||||
|
{
|
||||||
|
return PostIt::getUrl(panel: 'admin-filament') . '?focus_post_it=' . $postItId;
|
||||||
|
}
|
||||||
|
|
||||||
public function convertiInTicket(int $postItId): void
|
public function convertiInTicket(int $postItId): void
|
||||||
{
|
{
|
||||||
if (! $this->isPostItTableReady()) {
|
if (! $this->isPostItTableReady()) {
|
||||||
|
|
@ -216,7 +224,7 @@ public function getRecentiProperty()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$query = ChiamataPostIt::query()
|
$query = ChiamataPostIt::query()
|
||||||
->with(['rubrica', 'stabile', 'ticket', 'creatoDa'])
|
->with(['rubrica', 'stabile', 'ticket', 'creatoDa', 'assegnatoAUser:id,name', 'assegnatoAFornitore:id,ragione_sociale,nome,cognome'])
|
||||||
->orderByDesc('chiamata_il')
|
->orderByDesc('chiamata_il')
|
||||||
->orderByDesc('id');
|
->orderByDesc('id');
|
||||||
|
|
||||||
|
|
@ -230,6 +238,41 @@ public function getRecentiProperty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getRecentiRaggruppatiProperty()
|
||||||
|
{
|
||||||
|
$groups = [];
|
||||||
|
|
||||||
|
foreach ($this->recenti as $postIt) {
|
||||||
|
if ($this->isInternalPostIt($postIt)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$groupKey = $this->buildGroupedPostItKey($postIt);
|
||||||
|
$lastIndex = count($groups) - 1;
|
||||||
|
|
||||||
|
if ($lastIndex >= 0 && $groups[$lastIndex]['key'] === $groupKey) {
|
||||||
|
$groups[$lastIndex]['items'][] = $postIt;
|
||||||
|
$groups[$lastIndex]['count']++;
|
||||||
|
$groups[$lastIndex]['last_at'] = optional($postIt->chiamata_il)->format('d/m/Y H:i') ?: '-';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$groups[] = [
|
||||||
|
'key' => $groupKey,
|
||||||
|
'items' => [$postIt],
|
||||||
|
'count' => 1,
|
||||||
|
'latest' => $postIt,
|
||||||
|
'caller_label' => $postIt->nome_chiamante ?: 'Chiamante non identificato',
|
||||||
|
'phone' => (string) ($postIt->telefono ?? ''),
|
||||||
|
'route_label' => $this->resolvePostItRouteLabel($postIt),
|
||||||
|
'first_at' => optional($postIt->chiamata_il)->format('d/m/Y H:i') ?: '-',
|
||||||
|
'last_at' => optional($postIt->chiamata_il)->format('d/m/Y H:i') ?: '-',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return collect($groups);
|
||||||
|
}
|
||||||
|
|
||||||
public function getChiamateTecnicheProperty()
|
public function getChiamateTecnicheProperty()
|
||||||
{
|
{
|
||||||
if (! Schema::hasTable('communication_messages')) {
|
if (! Schema::hasTable('communication_messages')) {
|
||||||
|
|
@ -518,6 +561,59 @@ private function buildTicketDescriptionFromPostIt(int $postItId, ChiamataPostIt
|
||||||
return implode("\n\n", $parts);
|
return implode("\n\n", $parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildGroupedPostItKey(ChiamataPostIt $postIt): string
|
||||||
|
{
|
||||||
|
$phone = preg_replace('/\D+/', '', (string) ($postIt->telefono ?? '')) ?: '';
|
||||||
|
$caller = mb_strtolower(trim((string) ($postIt->nome_chiamante ?? '')));
|
||||||
|
$route = mb_strtolower($this->resolvePostItRouteLabel($postIt));
|
||||||
|
|
||||||
|
return implode('|', [$phone, $caller, $route, (string) $postIt->direzione]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolvePostItRouteLabel(ChiamataPostIt $postIt): string
|
||||||
|
{
|
||||||
|
$message = $this->resolveMessageFromPostIt($postIt);
|
||||||
|
if ($message instanceof CommunicationMessage) {
|
||||||
|
$line = trim((string) ($message->target_extension ?: data_get($message->metadata, 'smdr.co', '')));
|
||||||
|
if ($line !== '') {
|
||||||
|
return $line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/linea\s+([^\n]+)/i', (string) ($postIt->oggetto ?? ''), $matches) === 1) {
|
||||||
|
return trim((string) $matches[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isInternalPostIt(ChiamataPostIt $postIt): bool
|
||||||
|
{
|
||||||
|
$message = $this->resolveMessageFromPostIt($postIt);
|
||||||
|
if ($message instanceof CommunicationMessage) {
|
||||||
|
return $this->isInternalSmdrMessage($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveMessageFromPostIt(ChiamataPostIt $postIt): ?CommunicationMessage
|
||||||
|
{
|
||||||
|
$originId = (int) ($postIt->origine_id ?? 0);
|
||||||
|
if ($originId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists($originId, $this->postItMessageCache)) {
|
||||||
|
return $this->postItMessageCache[$originId];
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = CommunicationMessage::query()->find($originId);
|
||||||
|
$this->postItMessageCache[$originId] = $message;
|
||||||
|
|
||||||
|
return $message;
|
||||||
|
}
|
||||||
|
|
||||||
public function richiediClickToCallDaMessaggio(int $messageId): void
|
public function richiediClickToCallDaMessaggio(int $messageId): void
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
|
||||||
|
|
@ -1096,6 +1096,18 @@ private function loadManualSections(): void
|
||||||
'description' => 'Regole pratiche per aggiornare documentazione, note commit e materiali utili al riaggancio delle prossime sessioni.',
|
'description' => 'Regole pratiche per aggiornare documentazione, note commit e materiali utili al riaggancio delle prossime sessioni.',
|
||||||
'path' => 'docs/support/CONTINUITA-SVILUPPO-AGENT.md',
|
'path' => 'docs/support/CONTINUITA-SVILUPPO-AGENT.md',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'key' => 'coworking-moduli',
|
||||||
|
'title' => 'Architettura ruoli, moduli e coworking',
|
||||||
|
'description' => 'Direzione architetturale di NetGescon come spazio unico abilitato per ruoli multipli e moduli attivabili sulla stessa base dominio.',
|
||||||
|
'path' => 'docs/support/ARCHITETTURA-RUOLI-MODULI-COWORKING.md',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'server-debug',
|
||||||
|
'title' => 'Distribuzione server e debug separato',
|
||||||
|
'description' => 'Primo piano pratico per portare NetGescon su nodi separati dalla rete locale e stabilizzare deploy, collaudo e diagnostica.',
|
||||||
|
'path' => 'docs/support/DISTRIBUZIONE-SERVER-DEBUG.md',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($documents as $document) {
|
foreach ($documents as $document) {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@ class ChiamataPostIt extends Model
|
||||||
'rubrica_id',
|
'rubrica_id',
|
||||||
'ticket_id',
|
'ticket_id',
|
||||||
'creato_da_user_id',
|
'creato_da_user_id',
|
||||||
|
'assegnato_a_user_id',
|
||||||
|
'assegnato_a_fornitore_id',
|
||||||
|
'assegnazione_tipo',
|
||||||
'telefono',
|
'telefono',
|
||||||
'nome_chiamante',
|
'nome_chiamante',
|
||||||
'direzione',
|
'direzione',
|
||||||
|
|
@ -57,4 +60,14 @@ public function creatoDa()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(User::class, 'creato_da_user_id');
|
return $this->belongsTo(User::class, 'creato_da_user_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function assegnatoAUser()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'assegnato_a_user_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function assegnatoAFornitore()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Fornitore::class, 'assegnato_a_fornitore_id');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('chiamate_post_it', function (Blueprint $table): void {
|
||||||
|
if (! Schema::hasColumn('chiamate_post_it', 'assegnato_a_user_id')) {
|
||||||
|
$table->foreignId('assegnato_a_user_id')->nullable()->after('creato_da_user_id')->constrained('users')->nullOnDelete();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::hasColumn('chiamate_post_it', 'assegnato_a_fornitore_id')) {
|
||||||
|
$table->foreignId('assegnato_a_fornitore_id')->nullable()->after('assegnato_a_user_id')->constrained('fornitori')->nullOnDelete();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::hasColumn('chiamate_post_it', 'assegnazione_tipo')) {
|
||||||
|
$table->string('assegnazione_tipo', 32)->nullable()->after('assegnato_a_fornitore_id');
|
||||||
|
$table->index(['assegnazione_tipo', 'assegnato_a_user_id'], 'chiamate_post_it_assign_user_idx');
|
||||||
|
$table->index(['assegnazione_tipo', 'assegnato_a_fornitore_id'], 'chiamate_post_it_assign_supplier_idx');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('chiamate_post_it', function (Blueprint $table): void {
|
||||||
|
if (Schema::hasColumn('chiamate_post_it', 'assegnazione_tipo')) {
|
||||||
|
try {
|
||||||
|
$table->dropIndex('chiamate_post_it_assign_user_idx');
|
||||||
|
} catch (Throwable) {
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$table->dropIndex('chiamate_post_it_assign_supplier_idx');
|
||||||
|
} catch (Throwable) {
|
||||||
|
}
|
||||||
|
|
||||||
|
$table->dropColumn('assegnazione_tipo');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Schema::hasColumn('chiamate_post_it', 'assegnato_a_fornitore_id')) {
|
||||||
|
try {
|
||||||
|
$table->dropConstrainedForeignId('assegnato_a_fornitore_id');
|
||||||
|
} catch (Throwable) {
|
||||||
|
$table->dropColumn('assegnato_a_fornitore_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Schema::hasColumn('chiamate_post_it', 'assegnato_a_user_id')) {
|
||||||
|
try {
|
||||||
|
$table->dropConstrainedForeignId('assegnato_a_user_id');
|
||||||
|
} catch (Throwable) {
|
||||||
|
$table->dropColumn('assegnato_a_user_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -31,10 +31,23 @@ ## UI e comando predisposto
|
||||||
1. fa fetch dal repository Gitea
|
1. fa fetch dal repository Gitea
|
||||||
2. allinea il nodo al branch richiesto
|
2. allinea il nodo al branch richiesto
|
||||||
3. rigenera il changelog Git automatico per la pagina supporto
|
3. rigenera il changelog Git automatico per la pagina supporto
|
||||||
4. salva esito e diagnostica in `storage/app/support/generated/git-sync-last.json`
|
4. lancia anche la ricucitura sicura dei legami CRM rubrica se il comando QA e disponibile
|
||||||
|
5. salva esito e diagnostica in `storage/app/support/generated/git-sync-last.json`
|
||||||
|
|
||||||
Quindi la UI non e un percorso separato: e il front-end controllato del flusso Git ufficiale.
|
Quindi la UI non e un percorso separato: e il front-end controllato del flusso Git ufficiale.
|
||||||
|
|
||||||
|
## Ricucitura CRM inclusa nella UI
|
||||||
|
|
||||||
|
La procedura `Supporto > Modifiche` non richiede piu che l operatore esegua a mano il comando QA rubrica da terminale.
|
||||||
|
|
||||||
|
Durante il sync Git, il backend prova anche a eseguire:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan gescon:qa-rubrica-legami --apply-safe
|
||||||
|
```
|
||||||
|
|
||||||
|
In questo modo la riallocazione dei legami sicuri tra rubrica, stabile e unita rientra nel normale aggiornamento UI di staging.
|
||||||
|
|
||||||
## Cosa stiamo aggiornando in questo momento
|
## Cosa stiamo aggiornando in questo momento
|
||||||
|
|
||||||
### 1. Supporto > Modifiche come pannello operativo reale
|
### 1. Supporto > Modifiche come pannello operativo reale
|
||||||
|
|
|
||||||
145
docs/support/ARCHITETTURA-RUOLI-MODULI-COWORKING.md
Normal file
145
docs/support/ARCHITETTURA-RUOLI-MODULI-COWORKING.md
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
# Architettura ruoli, moduli e coworking
|
||||||
|
|
||||||
|
## Obiettivo
|
||||||
|
|
||||||
|
NetGescon non va piu letto come un applicativo monoruolo dedicato solo all amministrazione condominiale.
|
||||||
|
|
||||||
|
La direzione corretta e questa:
|
||||||
|
|
||||||
|
1. una base dominio unica e coerente
|
||||||
|
2. moduli attivabili sopra la stessa base dati
|
||||||
|
3. ruoli e abilitazioni che decidono cosa una persona puo fare
|
||||||
|
4. stessa persona abilitabile su piu funzioni operative
|
||||||
|
|
||||||
|
In pratica: uno spazio unico di coworking operativo, non una somma di mini-app scollegate.
|
||||||
|
|
||||||
|
## Base dominio da tenere stabile
|
||||||
|
|
||||||
|
La base comune resta:
|
||||||
|
|
||||||
|
- anagrafica unica dei nominativi
|
||||||
|
- stabili, unita immobiliari, relazioni e ruoli
|
||||||
|
- movimenti, banca, ripartizione spese, piano dei conti
|
||||||
|
- contabilita in partita doppia
|
||||||
|
- ticket, allegati, documenti e storico operativo
|
||||||
|
|
||||||
|
Questa base non va duplicata per mestiere o per modulo.
|
||||||
|
|
||||||
|
## Cosa cambia davvero tra un uso e l altro
|
||||||
|
|
||||||
|
Quello che cambia non e il dato di partenza, ma:
|
||||||
|
|
||||||
|
- il ruolo
|
||||||
|
- la procedura disponibile
|
||||||
|
- il modulo abilitato
|
||||||
|
- il livello di permesso sul tenant, sullo stabile o sul progetto
|
||||||
|
|
||||||
|
Esempi reali della stessa persona:
|
||||||
|
|
||||||
|
- supporto informatico
|
||||||
|
- sviluppo software
|
||||||
|
- revisione contabile
|
||||||
|
- collaboratore dell amministratore
|
||||||
|
- gestione PBX / CTI
|
||||||
|
- supervisione di ticket e fornitori
|
||||||
|
|
||||||
|
## Regola architetturale
|
||||||
|
|
||||||
|
NetGescon deve diventare:
|
||||||
|
|
||||||
|
- domain-first
|
||||||
|
- permission-first
|
||||||
|
- module-first
|
||||||
|
|
||||||
|
Non deve diventare:
|
||||||
|
|
||||||
|
- menu-first
|
||||||
|
- pagina-first
|
||||||
|
- ruolo singolo hard-coded
|
||||||
|
|
||||||
|
## Moduli principali gia visibili o in formazione
|
||||||
|
|
||||||
|
- Condominio e anagrafica
|
||||||
|
- Contabilita e banca
|
||||||
|
- Ripartizioni e tabelle millesimali
|
||||||
|
- Ticket e Post-it operativi
|
||||||
|
- CTI / PBX / click-to-call
|
||||||
|
- Import legacy Gescon
|
||||||
|
- Revisione contabile
|
||||||
|
- Storage tenant-aware e archivi
|
||||||
|
- Distribuzione server e manutenzione nodo
|
||||||
|
|
||||||
|
## Effetto pratico sulla UI
|
||||||
|
|
||||||
|
La UI deve progressivamente convergere verso:
|
||||||
|
|
||||||
|
1. stessa base record
|
||||||
|
2. azioni visibili solo se abilitate
|
||||||
|
3. moduli attivabili senza biforcare il prodotto
|
||||||
|
4. procedure specialistiche innestate sul contesto corretto
|
||||||
|
|
||||||
|
Esempio:
|
||||||
|
|
||||||
|
- una chiamata entra nel CRM
|
||||||
|
- produce un Post-it
|
||||||
|
- il Post-it puo essere assegnato a operatore, collaboratore o fornitore
|
||||||
|
- se serve diventa ticket
|
||||||
|
- il ticket impatta documenti, fornitore, stabile, contabilita o revisione
|
||||||
|
|
||||||
|
Sempre sullo stesso grafo dati.
|
||||||
|
|
||||||
|
## Modello di abilitazione consigliato
|
||||||
|
|
||||||
|
Tre livelli distinti:
|
||||||
|
|
||||||
|
### 1. Ruolo base
|
||||||
|
|
||||||
|
Definisce il perimetro generale:
|
||||||
|
|
||||||
|
- amministratore
|
||||||
|
- collaboratore
|
||||||
|
- fornitore
|
||||||
|
- revisore
|
||||||
|
- tecnico
|
||||||
|
- sviluppatore / ops
|
||||||
|
|
||||||
|
### 2. Capacita operative
|
||||||
|
|
||||||
|
Definiscono le azioni concrete:
|
||||||
|
|
||||||
|
- vedere contabilità
|
||||||
|
- modificare ripartizioni
|
||||||
|
- usare CTI
|
||||||
|
- lanciare sync staging
|
||||||
|
- aprire ticket
|
||||||
|
- assegnare Post-it
|
||||||
|
- gestire fornitori
|
||||||
|
|
||||||
|
### 3. Ambito
|
||||||
|
|
||||||
|
Definisce dove valgono:
|
||||||
|
|
||||||
|
- tenant / amministratore
|
||||||
|
- stabile
|
||||||
|
- progetto
|
||||||
|
- modulo
|
||||||
|
|
||||||
|
## Conseguenza tecnica
|
||||||
|
|
||||||
|
Ogni nuova funzione dovrebbe chiedersi prima:
|
||||||
|
|
||||||
|
1. su quale base dominio si appoggia
|
||||||
|
2. quale modulo la ospita
|
||||||
|
3. quale permission la abilita
|
||||||
|
4. su quale ambito agisce
|
||||||
|
|
||||||
|
Se una funzione non risponde a queste quattro domande, rischia di diventare una pagina isolata e fragile.
|
||||||
|
|
||||||
|
## Decisione attuale
|
||||||
|
|
||||||
|
La direzione da seguire nelle prossime sessioni e:
|
||||||
|
|
||||||
|
1. consolidare i moduli gia emersi invece di moltiplicare pagine parallele
|
||||||
|
2. portare la gestione operativa sempre piu vicino alle procedure abilitate
|
||||||
|
3. usare la pagina `Supporto > Modifiche` come ponte fra sviluppo, documentazione e deploy
|
||||||
|
4. preparare la distribuzione di NetGescon su nodi separati per ambienti di debug e collaudo
|
||||||
|
|
@ -65,3 +65,19 @@ ## Direzione architetturale attuale
|
||||||
- staging deve potersi riallineare da interfaccia web senza richiedere terminale all operatore
|
- staging deve potersi riallineare da interfaccia web senza richiedere terminale all operatore
|
||||||
- CTI/PBX va tenuto aperto a provider diversi, senza chiudersi su Panasonic
|
- CTI/PBX va tenuto aperto a provider diversi, senza chiudersi su Panasonic
|
||||||
- le pagine gestionali devono seguire il contesto condiviso topbar per stabile e anno
|
- le pagine gestionali devono seguire il contesto condiviso topbar per stabile e anno
|
||||||
|
|
||||||
|
## Stato utile da ricordare dopo questo slice
|
||||||
|
|
||||||
|
- `Supporto > Modifiche` e il punto da cui l operatore deve poter riallineare staging e far partire anche la ricucitura rubrica sicura.
|
||||||
|
- `Strumenti > Post-it Gestione` mostra ora un livello operativo raggruppato per richiami consecutivi, non solo righe singole.
|
||||||
|
- `Strumenti > Post-it` sta diventando il punto unico dove inserire, prendere in carico e indirizzare il Post-it verso operatore, collaboratore o fornitore.
|
||||||
|
- `Impostazioni > Scheda Amministratore > PBX / TAPI` e gia il posto giusto per accumulare segnali PBX osservati e mapparli alle persone reali dello studio.
|
||||||
|
- la direzione architetturale e `coworking unico + moduli abilitabili + ruoli multipli sulla stessa base dominio`
|
||||||
|
- il prossimo fronte infrastrutturale non e una rivoluzione immediata a microservizi, ma la distribuzione controllata su nodi separati per debug e collaudo fuori rete locale
|
||||||
|
|
||||||
|
## File nuovi o da leggere per ripartire
|
||||||
|
|
||||||
|
- `docs/support/ARCHITETTURA-RUOLI-MODULI-COWORKING.md`
|
||||||
|
- `docs/support/DISTRIBUZIONE-SERVER-DEBUG.md`
|
||||||
|
- `docs/support/CRM-ANAGRAFICA-LEGAMI.md`
|
||||||
|
- `docs/support/AGGIORNAMENTI-STAGING-GIT.md`
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,10 @@ ## Ordine corretto della pipeline
|
||||||
|
|
||||||
## Nuovo comando QA mirato
|
## Nuovo comando QA mirato
|
||||||
|
|
||||||
|
Per l operatore la via ordinaria non deve essere il terminale: il flusso corretto passa dalla UI `Supporto > Modifiche`, che durante il sync Git prova anche la ricucitura sicura dei legami rubrica.
|
||||||
|
|
||||||
|
Il comando resta comunque disponibile per diagnosi tecniche o ambienti di sviluppo.
|
||||||
|
|
||||||
Per analizzare casi singoli senza modificare nulla:
|
Per analizzare casi singoli senza modificare nulla:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
107
docs/support/DISTRIBUZIONE-SERVER-DEBUG.md
Normal file
107
docs/support/DISTRIBUZIONE-SERVER-DEBUG.md
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
# Distribuzione server e debug separato
|
||||||
|
|
||||||
|
## Obiettivo
|
||||||
|
|
||||||
|
Portare NetGescon fuori dalla sola rete locale e iniziare a lavorare con nodi separati per:
|
||||||
|
|
||||||
|
- deploy piu stabili
|
||||||
|
- collaudo fuori macchina sviluppo
|
||||||
|
- debug su ambienti isolati
|
||||||
|
- verifica reale di update, restore e sincronizzazioni
|
||||||
|
|
||||||
|
## Punto di partenza
|
||||||
|
|
||||||
|
Oggi il flusso piu stabile e:
|
||||||
|
|
||||||
|
1. sviluppo nel workspace Day0
|
||||||
|
2. commit e push su Gitea
|
||||||
|
3. sync UI da `Supporto > Modifiche`
|
||||||
|
4. verifica su staging
|
||||||
|
|
||||||
|
Questo flusso va mantenuto anche quando iniziamo a distribuire NetGescon su altri server.
|
||||||
|
|
||||||
|
## Strategia consigliata
|
||||||
|
|
||||||
|
### Fase 1. Nodo staging separato e disciplinato
|
||||||
|
|
||||||
|
Obiettivo:
|
||||||
|
|
||||||
|
- continuare a usare staging come nodo intermedio obbligatorio
|
||||||
|
- verificare che update, migrazioni e ricuciture dati passino sempre dalla procedura Git-first
|
||||||
|
|
||||||
|
Vincoli:
|
||||||
|
|
||||||
|
- niente modifiche manuali non tracciate sul nodo
|
||||||
|
- niente deploy paralleli fuori procedura
|
||||||
|
|
||||||
|
### Fase 2. Nodo debug esterno alla rete locale
|
||||||
|
|
||||||
|
Obiettivo:
|
||||||
|
|
||||||
|
- avere una macchina separata dalla rete di sviluppo
|
||||||
|
- usare dati di test controllati
|
||||||
|
- validare comportamento applicativo, login, PBX, allegati, storage, update
|
||||||
|
|
||||||
|
Uso consigliato:
|
||||||
|
|
||||||
|
- test deployment
|
||||||
|
- test restore
|
||||||
|
- test debug multi-macchina
|
||||||
|
- verifiche su watcher o bridge che non devono vivere solo sul PC principale
|
||||||
|
|
||||||
|
### Fase 3. Pacchetto installabile ripetibile
|
||||||
|
|
||||||
|
Obiettivo:
|
||||||
|
|
||||||
|
- trasformare NetGescon in un installabile coerente
|
||||||
|
- poter allestire un nuovo nodo senza ricostruire a mano script, env e procedure
|
||||||
|
|
||||||
|
Minimo indispensabile:
|
||||||
|
|
||||||
|
- checklist server
|
||||||
|
- script bootstrap
|
||||||
|
- procedura aggiornamento
|
||||||
|
- procedura backup e restore
|
||||||
|
- note di rete e storage
|
||||||
|
|
||||||
|
## Architettura iniziale consigliata
|
||||||
|
|
||||||
|
Per il primo ciclo non serve un parco complesso di macchine.
|
||||||
|
|
||||||
|
Bastano tre ruoli chiari:
|
||||||
|
|
||||||
|
1. workspace autorevole Day0
|
||||||
|
2. staging operativo aggiornato da Gitea
|
||||||
|
3. debug node separato per prove e collaudi fuori rete locale
|
||||||
|
|
||||||
|
## Cosa validare sul debug node
|
||||||
|
|
||||||
|
- installazione applicativa pulita
|
||||||
|
- migrazioni e bootstrap
|
||||||
|
- login e permessi ruoli
|
||||||
|
- documentazione in `Supporto > Modifiche`
|
||||||
|
- procedura Git sync e pacchetto aggiornamento
|
||||||
|
- Post-it, ticket e CRM telefonico
|
||||||
|
- storage tenant-aware e documenti sensibili
|
||||||
|
- eventuali watcher PBX o bridge esterni
|
||||||
|
|
||||||
|
## Regola pratica per il deploy
|
||||||
|
|
||||||
|
Ogni nodo deve poter essere ricostruito da materiale nel repository.
|
||||||
|
|
||||||
|
Quindi il repo deve contenere almeno:
|
||||||
|
|
||||||
|
- manuale operativo
|
||||||
|
- flusso update staging
|
||||||
|
- architettura ruoli/moduli
|
||||||
|
- note continuita agent
|
||||||
|
- script di update e di sync gia usati dalla UI
|
||||||
|
|
||||||
|
## Decisione attuale
|
||||||
|
|
||||||
|
Per il prossimo passo concreto:
|
||||||
|
|
||||||
|
1. completare il commit del slice corrente
|
||||||
|
2. aggiornare staging via Git-first
|
||||||
|
3. preparare una checklist tecnica per il primo nodo debug esterno
|
||||||
|
4. poi estrarre un bootstrap server riusabile, senza dipendere dalla macchina locale
|
||||||
|
|
@ -35,6 +35,34 @@ ## Effetto pratico
|
||||||
|
|
||||||
## Implementazioni recenti da tenere come riferimento
|
## Implementazioni recenti da tenere come riferimento
|
||||||
|
|
||||||
|
### 0. Direzione di prodotto: NetGescon come spazio unico a ruoli e moduli
|
||||||
|
|
||||||
|
- La direzione attuale non e piu quella di un gestionale chiuso solo per l amministratore condominiale.
|
||||||
|
- NetGescon va trattato come uno spazio operativo unico dove la differenza non la fa la macchina o il menu fisso, ma l abilitazione concessa a ruolo, persona o tenant.
|
||||||
|
- Lo stesso utente puo lavorare su piu assi: assistenza informatica, sviluppo software, revisione contabile, collaboratore amministrativo, gestione PBX, supporto fornitori.
|
||||||
|
- La base dominio resta stabile: unita immobiliari, anagrafica unica, spese, banca, piano dei conti, ripartizioni, partita doppia.
|
||||||
|
- Sopra questa base si attivano moduli o funzioni: CTI/PBX, ticketing, revisione, import legacy, storage tenant-aware, distribuzione server, fornitori, allegati, analytics.
|
||||||
|
|
||||||
|
Effetto pratico:
|
||||||
|
|
||||||
|
- [P] l evoluzione corretta e modulo-first e permission-first, non pagina-first
|
||||||
|
- [P] lo stesso record dominio deve poter essere usato in procedure diverse senza duplicare dati o UI separate per mestiere
|
||||||
|
- [U] una persona puo lavorare nello stesso ambiente con ruoli multipli senza cambiare prodotto
|
||||||
|
|
||||||
|
### 0-bis. Slice appena chiuso su CRM telefonico e instradamento umano
|
||||||
|
|
||||||
|
- In `strumenti/post-it-gestione` le chiamate recenti hanno ora anche box raggruppati per chiamante/numero/linea consecutivi.
|
||||||
|
- Le chiamate interne sono escluse dal raggruppamento operativo, cosi la vista serve davvero per il CRM e non per il rumore tecnico.
|
||||||
|
- Ogni box puo aprire direttamente il Post-it corrispondente.
|
||||||
|
- In `strumenti/post-it` il Post-it puo essere assegnato esplicitamente a operatore, collaboratore o fornitore.
|
||||||
|
- In `impostazioni/scheda-amministratore` il tab PBX/TAPI accumula i token osservati e permette di associare piu linee, gruppi ed extension allo stesso collaboratore.
|
||||||
|
|
||||||
|
Effetto pratico:
|
||||||
|
|
||||||
|
- [U] meno duplicazione nei richiami ripetuti
|
||||||
|
- [U] piu facile decidere chi prende in carico una chiamata o un ticket derivato
|
||||||
|
- [P] il PBX viene trattato come sorgente di segnali osservati, non come struttura rigida hard-coded sul fornitore attuale
|
||||||
|
|
||||||
### 1. CTI / CSTA / click-to-call piu aperti lato PBX
|
### 1. CTI / CSTA / click-to-call piu aperti lato PBX
|
||||||
|
|
||||||
- Separato il tab tecnico SMDR dal tab `CTI / CSTA e Click-to-Call` in `strumenti/post-it-gestione`.
|
- Separato il tab tecnico SMDR dal tab `CTI / CSTA e Click-to-Call` in `strumenti/post-it-gestione`.
|
||||||
|
|
@ -96,3 +124,5 @@ ## Prossimi passi consigliati
|
||||||
1. usare questa sezione come fonte primaria per le prossime implementazioni CTI e contabilita
|
1. usare questa sezione come fonte primaria per le prossime implementazioni CTI e contabilita
|
||||||
2. automatizzare anche la scrittura di snapshot di sviluppo basati sui commit e sulle note operative
|
2. automatizzare anche la scrittura di snapshot di sviluppo basati sui commit e sulle note operative
|
||||||
3. aggiungere progressivamente how-to piu mirati, senza spargere file duplicati
|
3. aggiungere progressivamente how-to piu mirati, senza spargere file duplicati
|
||||||
|
4. spostare le prossime decisioni su ruoli/moduli nel documento architetturale dedicato `docs/support/ARCHITETTURA-RUOLI-MODULI-COWORKING.md`
|
||||||
|
5. preparare il primo nodo esterno di debug/deploy seguendo `docs/support/DISTRIBUZIONE-SERVER-DEBUG.md`
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
@php($summary = $this->pbxSummary)
|
@php($summary = $this->pbxSummary)
|
||||||
|
@php($observed = $this->pbxObservedTokens)
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="rounded-lg border border-gray-200 bg-white p-3">
|
<div class="rounded-lg border border-gray-200 bg-white p-3">
|
||||||
|
|
@ -94,6 +95,100 @@
|
||||||
</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">Token PBX osservati dal CRM</div>
|
||||||
|
<div class="mb-3 text-xs text-gray-500">Il CRM accumula automaticamente gruppi, interni e linee letti dagli eventi telefonici. Usa questi valori per mappare chi gestisce cosa, senza vincolarti a Panasonic.</div>
|
||||||
|
|
||||||
|
<div class="grid gap-4 md:grid-cols-3">
|
||||||
|
<div>
|
||||||
|
<div class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-600">Interni / extension</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
@forelse($observed['extensions'] as $token)
|
||||||
|
<span class="rounded-full bg-slate-100 px-2.5 py-1 text-xs text-slate-700">{{ $token }}</span>
|
||||||
|
@empty
|
||||||
|
<span class="text-xs text-slate-500">Nessun interno osservato.</span>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-600">Gruppi</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
@forelse($observed['groups'] as $token)
|
||||||
|
<span class="rounded-full bg-blue-100 px-2.5 py-1 text-xs text-blue-800">{{ $token }}</span>
|
||||||
|
@empty
|
||||||
|
<span class="text-xs text-slate-500">Nessun gruppo osservato.</span>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-600">Linee / DID</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
@forelse($observed['lines'] as $token)
|
||||||
|
<span class="rounded-full bg-emerald-100 px-2.5 py-1 text-xs text-emerald-800">{{ $token }}</span>
|
||||||
|
@empty
|
||||||
|
<span class="text-xs text-slate-500">Nessuna linea osservata.</span>
|
||||||
|
@endforelse
|
||||||
|
</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">Chi gestisce interni, linee e gruppi</div>
|
||||||
|
<div class="mb-3 text-xs text-gray-500">Ogni nominativo puo gestire piu interni, piu linee e piu gruppi. I flag PBX restano modificabili inline con check rapidi.</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">Collaboratore</th>
|
||||||
|
<th class="border px-2 py-1.5 text-left">Extension gestite</th>
|
||||||
|
<th class="border px-2 py-1.5 text-left">Gruppi gestiti</th>
|
||||||
|
<th class="border px-2 py-1.5 text-left">Linee / DID</th>
|
||||||
|
<th class="border px-2 py-1.5 text-left">Flags</th>
|
||||||
|
<th class="border px-2 py-1.5 text-left">Azioni</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse($this->collaboratoriCentralinoRows as $row)
|
||||||
|
<tr class="hover:bg-slate-50">
|
||||||
|
<td class="border px-2 py-1.5">
|
||||||
|
<div class="font-medium">{{ $row['name'] }}</div>
|
||||||
|
<div class="text-[11px] text-slate-500">{{ $row['email'] }}</div>
|
||||||
|
</td>
|
||||||
|
<td class="border px-2 py-1.5">
|
||||||
|
<input type="text" wire:model.defer="collaboratorePbxExtensions.{{ (int) $row['user_id'] }}" class="w-40 rounded-md border-gray-300 text-xs" placeholder="EXT00208, 208" />
|
||||||
|
</td>
|
||||||
|
<td class="border px-2 py-1.5">
|
||||||
|
<input type="text" wire:model.defer="collaboratorePbxGroups.{{ (int) $row['user_id'] }}" class="w-36 rounded-md border-gray-300 text-xs" placeholder="GRP00601, GRP00603" />
|
||||||
|
</td>
|
||||||
|
<td class="border px-2 py-1.5">
|
||||||
|
<input type="text" wire:model.defer="collaboratorePbxLines.{{ (int) $row['user_id'] }}" class="w-36 rounded-md border-gray-300 text-xs" placeholder="0003, 0008" />
|
||||||
|
</td>
|
||||||
|
<td class="border px-2 py-1.5">
|
||||||
|
<label class="mr-3 inline-flex items-center gap-1 text-[11px]">
|
||||||
|
<input type="checkbox" wire:model.defer="collaboratorePbxPopupEnabled.{{ (int) $row['user_id'] }}" class="rounded border-gray-300" />
|
||||||
|
popup
|
||||||
|
</label>
|
||||||
|
<label class="inline-flex items-center gap-1 text-[11px]">
|
||||||
|
<input type="checkbox" wire:model.defer="collaboratorePbxClickToCallEnabled.{{ (int) $row['user_id'] }}" class="rounded border-gray-300" />
|
||||||
|
click2call
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
<td class="border px-2 py-1.5">
|
||||||
|
<x-filament::button size="xs" color="gray" type="button" wire:click="salvaInternoCollaboratore({{ (int) $row['user_id'] }})">Salva</x-filament::button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="border px-2 py-3 text-center text-gray-500">Nessun collaboratore assegnato agli stabili di questo amministratore.</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="rounded-lg border border-gray-200 bg-white p-3">
|
<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-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, almeno uno dei canali telefonici attivi sta gia portando traffico nel CRM. In modalita hybrid puoi vedere sia eventi Panasonic sia righe SMDR.</div>
|
<div class="mb-3 text-xs text-gray-500">Se qui compaiono record, almeno uno dei canali telefonici attivi sta gia portando traffico nel CRM. In modalita hybrid puoi vedere sia eventi Panasonic sia righe SMDR.</div>
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,60 @@
|
||||||
<div class="rounded-xl border bg-white p-4">
|
<div class="rounded-xl border bg-white p-4">
|
||||||
<h2 class="mb-3 text-base font-semibold">Storico chiamate recenti</h2>
|
<h2 class="mb-3 text-base font-semibold">Storico chiamate recenti</h2>
|
||||||
|
|
||||||
|
<div class="mb-5">
|
||||||
|
<div class="mb-2 flex items-center justify-between gap-2">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-semibold text-slate-800">Box chiamate raggruppate</div>
|
||||||
|
<div class="text-xs text-slate-500">Le comunicazioni interne non compaiono qui. Le chiamate consecutive dello stesso numero restano nello stesso box finche non interviene un altro chiamante.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||||
|
@forelse($this->recentiRaggruppati as $gruppo)
|
||||||
|
@php($box = $gruppo['latest'])
|
||||||
|
<div class="rounded-xl border border-slate-200 bg-slate-50 p-4">
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<div class="font-semibold text-slate-900">{{ $gruppo['caller_label'] }}</div>
|
||||||
|
<div class="text-sm text-slate-600">
|
||||||
|
{{ $gruppo['phone'] !== '' ? $gruppo['phone'] : 'Numero non disponibile' }}
|
||||||
|
@if($gruppo['route_label'] !== '-')
|
||||||
|
· verso {{ $gruppo['route_label'] }}
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="rounded-full bg-indigo-100 px-2.5 py-1 text-xs font-semibold text-indigo-800">{{ $gruppo['count'] }} tentativi</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 text-xs text-slate-500">{{ $gruppo['last_at'] }}{{ $gruppo['count'] > 1 ? ' · gruppo consecutivo' : '' }}</div>
|
||||||
|
<div class="mt-2 text-sm text-slate-700">{{ $box->oggetto ?: 'Senza oggetto' }}</div>
|
||||||
|
|
||||||
|
<div class="mt-3 flex flex-wrap gap-2 text-[11px]">
|
||||||
|
<span class="rounded-md bg-white px-2 py-1 text-slate-700">{{ $box->stato }}</span>
|
||||||
|
@if($box->assegnazione_tipo)
|
||||||
|
<span class="rounded-md bg-amber-100 px-2 py-1 text-amber-800">{{ ucfirst(str_replace('_', ' ', (string) $box->assegnazione_tipo)) }}</span>
|
||||||
|
@endif
|
||||||
|
@if($box->assegnatoAUser)
|
||||||
|
<span class="rounded-md bg-emerald-100 px-2 py-1 text-emerald-800">{{ $box->assegnatoAUser->name }}</span>
|
||||||
|
@endif
|
||||||
|
@if($box->assegnatoAFornitore)
|
||||||
|
<span class="rounded-md bg-emerald-100 px-2 py-1 text-emerald-800">{{ $box->assegnatoAFornitore->ragione_sociale ?: trim(($box->assegnatoAFornitore->nome ?? '') . ' ' . ($box->assegnatoAFornitore->cognome ?? '')) }}</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex flex-wrap gap-2">
|
||||||
|
<a href="{{ $this->getPostItPageUrl((int) $box->id) }}" class="inline-flex items-center rounded-md bg-indigo-700 px-3 py-1.5 text-xs font-medium text-white hover:bg-indigo-600">Apri Post-it</a>
|
||||||
|
@if($box->ticket_id)
|
||||||
|
<a href="{{ \App\Filament\Pages\Supporto\TicketGestione::getUrl(panel: 'admin-filament') }}#ticket-{{ (int) $box->ticket_id }}" class="inline-flex items-center rounded-md border px-3 py-1.5 text-xs">Apri ticket</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<div class="rounded-lg border border-dashed p-4 text-sm text-gray-500 md:col-span-2 xl:col-span-3">Nessuna chiamata esterna raggruppabile disponibile.</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
@forelse($this->recenti as $riga)
|
@forelse($this->recenti as $riga)
|
||||||
<div @class([
|
<div @class([
|
||||||
|
|
|
||||||
|
|
@ -108,5 +108,131 @@
|
||||||
<x-filament::button color="success" wire:click="salvaEConvertiInTicket">Salva e crea ticket</x-filament::button>
|
<x-filament::button color="success" wire:click="salvaEConvertiInTicket">Salva e crea ticket</x-filament::button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if($this->isPostItTableReady())
|
||||||
|
<div class="grid gap-6 lg:grid-cols-[minmax(0,1.4fr)_minmax(320px,1fr)]">
|
||||||
|
<div class="rounded-xl border bg-white p-4">
|
||||||
|
<div class="mb-3 flex items-center justify-between gap-2">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-base font-semibold">Post-it recenti</h2>
|
||||||
|
<p class="text-sm text-gray-500">Seleziona un Post-it per gestirlo o assegnarlo direttamente.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
@forelse($this->recenti as $riga)
|
||||||
|
<button type="button" wire:click="selectPostIt({{ (int) $riga->id }})" class="block w-full rounded-lg border p-3 text-left {{ (int) $selectedPostItId === (int) $riga->id ? 'border-emerald-400 bg-emerald-50 ring-1 ring-emerald-200' : 'hover:bg-gray-50' }}">
|
||||||
|
<div class="flex flex-wrap items-start justify-between gap-2">
|
||||||
|
<div>
|
||||||
|
<div class="font-medium">{{ $riga->oggetto ?: 'Senza oggetto' }}</div>
|
||||||
|
<div class="text-sm text-gray-600">
|
||||||
|
{{ $riga->nome_chiamante ?: 'Chiamante non identificato' }}
|
||||||
|
@if($riga->telefono)
|
||||||
|
· {{ $riga->telefono }}
|
||||||
|
@endif
|
||||||
|
@if($riga->stabile)
|
||||||
|
· {{ $riga->stabile->denominazione ?: ('Stabile #' . $riga->stabile->id) }}
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500">{{ optional($riga->chiamata_il)->format('d/m/Y H:i') }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 flex flex-wrap gap-2 text-[11px]">
|
||||||
|
<span class="rounded-md bg-slate-100 px-2 py-1 text-slate-700">{{ $riga->stato }}</span>
|
||||||
|
<span class="rounded-md bg-blue-100 px-2 py-1 text-blue-800">{{ $riga->priorita }}</span>
|
||||||
|
@if($riga->assegnazione_tipo)
|
||||||
|
<span class="rounded-md bg-amber-100 px-2 py-1 text-amber-800">Assegnato a {{ $riga->assegnazione_tipo }}</span>
|
||||||
|
@endif
|
||||||
|
@if($riga->assegnatoAUser)
|
||||||
|
<span class="rounded-md bg-emerald-100 px-2 py-1 text-emerald-800">{{ $riga->assegnatoAUser->name }}</span>
|
||||||
|
@endif
|
||||||
|
@if($riga->assegnatoAFornitore)
|
||||||
|
<span class="rounded-md bg-emerald-100 px-2 py-1 text-emerald-800">{{ $riga->assegnatoAFornitore->ragione_sociale ?: trim(($riga->assegnatoAFornitore->nome ?? '') . ' ' . ($riga->assegnatoAFornitore->cognome ?? '')) }}</span>
|
||||||
|
@endif
|
||||||
|
@if($riga->ticket_id)
|
||||||
|
<span class="rounded-md bg-indigo-100 px-2 py-1 text-indigo-800">Ticket #{{ (int) $riga->ticket_id }}</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
@empty
|
||||||
|
<div class="rounded-lg border border-dashed p-4 text-sm text-gray-500">Nessun Post-it recente.</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border bg-white p-4">
|
||||||
|
<h2 class="text-base font-semibold">Assegnazione Post-it</h2>
|
||||||
|
@if(! $this->isPostItAssignmentReady())
|
||||||
|
<div class="mt-3 rounded-lg border border-amber-300 bg-amber-50 p-3 text-sm text-amber-800">Assegnazione disponibile dopo l'applicazione della nuova migrazione Post-it nel normale flusso di aggiornamento.</div>
|
||||||
|
@elseif($this->selectedPostIt)
|
||||||
|
<div class="mt-3 rounded-lg border bg-slate-50 p-3 text-sm text-slate-700">
|
||||||
|
<div class="font-medium">Post-it #{{ (int) $this->selectedPostIt->id }} · {{ $this->selectedPostIt->oggetto ?: 'Senza oggetto' }}</div>
|
||||||
|
<div class="mt-1 text-xs text-slate-500">
|
||||||
|
{{ $this->selectedPostIt->nome_chiamante ?: 'Chiamante non identificato' }}
|
||||||
|
@if($this->selectedPostIt->telefono)
|
||||||
|
· {{ $this->selectedPostIt->telefono }}
|
||||||
|
@endif
|
||||||
|
@if($this->selectedPostIt->ticket_id)
|
||||||
|
· Ticket #{{ (int) $this->selectedPostIt->ticket_id }}
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 space-y-4">
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Tipo assegnazione</span>
|
||||||
|
<select wire:model.live="assegnazioneTipo" class="w-full rounded-lg border-gray-300">
|
||||||
|
<option value="operatore">Operatore</option>
|
||||||
|
<option value="fornitore">Fornitore</option>
|
||||||
|
<option value="collaboratore">Collaboratore dello studio</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
@if($assegnazioneTipo === 'operatore')
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Operatore</span>
|
||||||
|
<select wire:model="assegnazioneUserId" class="w-full rounded-lg border-gray-300">
|
||||||
|
<option value="">Seleziona operatore</option>
|
||||||
|
@foreach($this->operatoriOptions as $id => $label)
|
||||||
|
<option value="{{ $id }}">{{ $label }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
@elseif($assegnazioneTipo === 'collaboratore')
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Collaboratore</span>
|
||||||
|
<select wire:model="assegnazioneUserId" class="w-full rounded-lg border-gray-300">
|
||||||
|
<option value="">Seleziona collaboratore</option>
|
||||||
|
@foreach($this->collaboratoriOptions as $id => $label)
|
||||||
|
<option value="{{ $id }}">{{ $label }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
@else
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Fornitore</span>
|
||||||
|
<select wire:model="assegnazioneFornitoreId" class="w-full rounded-lg border-gray-300">
|
||||||
|
<option value="">Seleziona fornitore</option>
|
||||||
|
@foreach($this->fornitoriOptions as $id => $label)
|
||||||
|
<option value="{{ $id }}">{{ $label }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<x-filament::button wire:click="assegnaPostIt">Salva assegnazione</x-filament::button>
|
||||||
|
@if($this->selectedPostIt->ticket_id)
|
||||||
|
<a href="{{ \App\Filament\Pages\Supporto\TicketGestione::getUrl(panel: 'admin-filament') }}#ticket-{{ (int) $this->selectedPostIt->ticket_id }}" class="inline-flex items-center rounded-md border px-3 py-2 text-sm">Apri ticket collegato</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="mt-3 rounded-lg border border-dashed p-4 text-sm text-gray-500">Seleziona un Post-it recente per assegnarlo.</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</x-filament-panels::page>
|
</x-filament-panels::page>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user