feat: allinea day0 supporto ticket e import catasto
This commit is contained in:
parent
b9b9e999f7
commit
fd3904e0d2
|
|
@ -48,11 +48,11 @@ public function handle(): int
|
||||||
return self::SUCCESS;
|
return self::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
$applySafe = (bool) $this->option('apply-safe');
|
$applySafe = (bool) $this->option('apply-safe');
|
||||||
$updatedReferences = 0;
|
$updatedReferences = 0;
|
||||||
$createdRuoli = 0;
|
$createdRuoli = 0;
|
||||||
$ambiguous = 0;
|
$ambiguous = 0;
|
||||||
$manualReview = 0;
|
$manualReview = 0;
|
||||||
|
|
||||||
foreach ($rows as $rubrica) {
|
foreach ($rows as $rubrica) {
|
||||||
$analysis = $this->analyzeRubrica($rubrica);
|
$analysis = $this->analyzeRubrica($rubrica);
|
||||||
|
|
@ -88,9 +88,9 @@ public function handle(): int
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$applied = $this->applySafeFixes($rubrica, $analysis);
|
$applied = $this->applySafeFixes($rubrica, $analysis);
|
||||||
$updatedReferences += $applied['references_updated'];
|
$updatedReferences += $applied['references_updated'];
|
||||||
$createdRuoli += $applied['ruoli_created'];
|
$createdRuoli += $applied['ruoli_created'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->line('');
|
$this->line('');
|
||||||
|
|
@ -111,8 +111,8 @@ public function handle(): int
|
||||||
*/
|
*/
|
||||||
private function analyzeRubrica(RubricaUniversale $rubrica): array
|
private function analyzeRubrica(RubricaUniversale $rubrica): array
|
||||||
{
|
{
|
||||||
$legacy = $this->parseLegacyMeta((string) ($rubrica->note ?? ''));
|
$legacy = $this->parseLegacyMeta((string) ($rubrica->note ?? ''));
|
||||||
$stabile = $this->resolveStabile($legacy['cod_stabile']);
|
$stabile = $this->resolveStabile($legacy['cod_stabile']);
|
||||||
$existingRuoli = Schema::hasTable('rubrica_ruoli')
|
$existingRuoli = Schema::hasTable('rubrica_ruoli')
|
||||||
? DB::table('rubrica_ruoli')->where('rubrica_id', (int) $rubrica->id)->get()->all()
|
? DB::table('rubrica_ruoli')->where('rubrica_id', (int) $rubrica->id)->get()->all()
|
||||||
: [];
|
: [];
|
||||||
|
|
@ -133,18 +133,18 @@ private function analyzeRubrica(RubricaUniversale $rubrica): array
|
||||||
}
|
}
|
||||||
|
|
||||||
$requiresManualReview = $uniqueUnit !== null
|
$requiresManualReview = $uniqueUnit !== null
|
||||||
&& ! empty($stabile['id'])
|
&& ! empty($stabile['id'])
|
||||||
&& ! $uniqueUnit['safe_role_creation']
|
&& ! $uniqueUnit['safe_role_creation']
|
||||||
&& count($existingRuoli) === 0;
|
&& count($existingRuoli) === 0;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'legacy' => $legacy,
|
'legacy' => $legacy,
|
||||||
'stabile' => $stabile,
|
'stabile' => $stabile,
|
||||||
'stabile_label' => $this->buildStabileReference($stabile),
|
'stabile_label' => $this->buildStabileReference($stabile),
|
||||||
'existing_ruoli' => $existingRuoli,
|
'existing_ruoli' => $existingRuoli,
|
||||||
'candidate_units' => $candidateUnits,
|
'candidate_units' => $candidateUnits,
|
||||||
'unique_unit' => $uniqueUnit,
|
'unique_unit' => $uniqueUnit,
|
||||||
'persona_summary' => $this->summarizePersonas($personaMatches),
|
'persona_summary' => $this->summarizePersonas($personaMatches),
|
||||||
'requires_manual_review' => $requiresManualReview,
|
'requires_manual_review' => $requiresManualReview,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -155,19 +155,19 @@ private function analyzeRubrica(RubricaUniversale $rubrica): array
|
||||||
private function applySafeFixes(RubricaUniversale $rubrica, array $analysis): array
|
private function applySafeFixes(RubricaUniversale $rubrica, array $analysis): array
|
||||||
{
|
{
|
||||||
$referencesUpdated = 0;
|
$referencesUpdated = 0;
|
||||||
$ruoliCreated = 0;
|
$ruoliCreated = 0;
|
||||||
$stabile = $analysis['stabile'];
|
$stabile = $analysis['stabile'];
|
||||||
$uniqueUnit = $analysis['unique_unit'];
|
$uniqueUnit = $analysis['unique_unit'];
|
||||||
|
|
||||||
$dirty = false;
|
$dirty = false;
|
||||||
if (($rubrica->riferimento_stabile === null || trim((string) $rubrica->riferimento_stabile) === '') && ! empty($stabile['id'])) {
|
if (($rubrica->riferimento_stabile === null || trim((string) $rubrica->riferimento_stabile) === '') && ! empty($stabile['id'])) {
|
||||||
$rubrica->riferimento_stabile = $this->buildStabileReference($stabile);
|
$rubrica->riferimento_stabile = $this->buildStabileReference($stabile);
|
||||||
$dirty = true;
|
$dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (($rubrica->riferimento_unita === null || trim((string) $rubrica->riferimento_unita) === '') && $uniqueUnit !== null) {
|
if (($rubrica->riferimento_unita === null || trim((string) $rubrica->riferimento_unita) === '') && $uniqueUnit !== null) {
|
||||||
$rubrica->riferimento_unita = $uniqueUnit['unit_reference'];
|
$rubrica->riferimento_unita = $uniqueUnit['unit_reference'];
|
||||||
$dirty = true;
|
$dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($dirty) {
|
if ($dirty) {
|
||||||
|
|
@ -186,20 +186,20 @@ private function applySafeFixes(RubricaUniversale $rubrica, array $analysis): ar
|
||||||
) {
|
) {
|
||||||
DB::table('rubrica_ruoli')->updateOrInsert(
|
DB::table('rubrica_ruoli')->updateOrInsert(
|
||||||
[
|
[
|
||||||
'rubrica_id' => (int) $rubrica->id,
|
'rubrica_id' => (int) $rubrica->id,
|
||||||
'stabile_id' => (int) $stabile['id'],
|
'stabile_id' => (int) $stabile['id'],
|
||||||
'unita_immobiliare_id' => (int) $uniqueUnit['unit_id'],
|
'unita_immobiliare_id' => (int) $uniqueUnit['unit_id'],
|
||||||
'ruolo_standard' => $uniqueUnit['role_standard'],
|
'ruolo_standard' => $uniqueUnit['role_standard'],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'is_attivo' => true,
|
'is_attivo' => true,
|
||||||
'is_preferito' => true,
|
'is_preferito' => true,
|
||||||
'meta' => json_encode([
|
'meta' => json_encode([
|
||||||
'source' => $uniqueUnit['source'],
|
'source' => $uniqueUnit['source'],
|
||||||
'created_by' => 'gescon:qa-rubrica-legami',
|
'created_by' => 'gescon:qa-rubrica-legami',
|
||||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||||
'updated_at' => now(),
|
'updated_at' => now(),
|
||||||
'created_at' => now(),
|
'created_at' => now(),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -208,7 +208,7 @@ private function applySafeFixes(RubricaUniversale $rubrica, array $analysis): ar
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'references_updated' => $referencesUpdated,
|
'references_updated' => $referencesUpdated,
|
||||||
'ruoli_created' => $ruoliCreated,
|
'ruoli_created' => $ruoliCreated,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -219,8 +219,8 @@ private function parseLegacyMeta(string $note): array
|
||||||
{
|
{
|
||||||
$payload = [
|
$payload = [
|
||||||
'cod_stabile' => null,
|
'cod_stabile' => null,
|
||||||
'cod_cond' => null,
|
'cod_cond' => null,
|
||||||
'ruolo' => null,
|
'ruolo' => null,
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach (['cod_stabile', 'cod_cond', 'ruolo'] as $key) {
|
foreach (['cod_stabile', 'cod_cond', 'ruolo'] as $key) {
|
||||||
|
|
@ -238,13 +238,13 @@ private function parseLegacyMeta(string $note): array
|
||||||
private function resolveStabile(?string $legacyCode): array
|
private function resolveStabile(?string $legacyCode): array
|
||||||
{
|
{
|
||||||
$blank = ['id' => null, 'codice_stabile' => null, 'cod_stabile' => null, 'denominazione' => null];
|
$blank = ['id' => null, 'codice_stabile' => null, 'cod_stabile' => null, 'denominazione' => null];
|
||||||
$code = trim((string) $legacyCode);
|
$code = trim((string) $legacyCode);
|
||||||
if ($code === '') {
|
if ($code === '') {
|
||||||
return $blank;
|
return $blank;
|
||||||
}
|
}
|
||||||
|
|
||||||
$trimmed = ltrim($code, '0');
|
$trimmed = ltrim($code, '0');
|
||||||
$row = DB::table('stabili')
|
$row = DB::table('stabili')
|
||||||
->select(['id', 'codice_stabile', 'cod_stabile', 'denominazione'])
|
->select(['id', 'codice_stabile', 'cod_stabile', 'denominazione'])
|
||||||
->where(function ($query) use ($code, $trimmed): void {
|
->where(function ($query) use ($code, $trimmed): void {
|
||||||
$query->where('codice_stabile', $code)
|
$query->where('codice_stabile', $code)
|
||||||
|
|
@ -263,10 +263,10 @@ private function resolveStabile(?string $legacyCode): array
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => (int) $row->id,
|
'id' => (int) $row->id,
|
||||||
'codice_stabile' => $row->codice_stabile ? (string) $row->codice_stabile : null,
|
'codice_stabile' => $row->codice_stabile ? (string) $row->codice_stabile : null,
|
||||||
'cod_stabile' => $row->cod_stabile ? (string) $row->cod_stabile : null,
|
'cod_stabile' => $row->cod_stabile ? (string) $row->cod_stabile : null,
|
||||||
'denominazione' => $row->denominazione ? (string) $row->denominazione : null,
|
'denominazione' => $row->denominazione ? (string) $row->denominazione : null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -298,11 +298,11 @@ private function collectLegacyUnitCandidates(?int $stabileId, array $legacy): ar
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
$unitId = (int) $row->unit_id;
|
$unitId = (int) $row->unit_id;
|
||||||
$grouped[$unitId] ??= [
|
$grouped[$unitId] ??= [
|
||||||
'unit_id' => $unitId,
|
'unit_id' => $unitId,
|
||||||
'unit_label' => $this->buildUnitLabel($row),
|
'unit_label' => $this->buildUnitLabel($row),
|
||||||
'unit_reference' => $this->buildUnitReference($row),
|
'unit_reference' => $this->buildUnitReference($row),
|
||||||
'source' => 'legacy_local',
|
'source' => 'legacy_local',
|
||||||
'role_tokens' => [],
|
'role_tokens' => [],
|
||||||
'safe_role_creation' => true,
|
'safe_role_creation' => true,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -315,7 +315,7 @@ private function collectLegacyUnitCandidates(?int $stabileId, array $legacy): ar
|
||||||
return array_values(array_map(function (array $candidate) use ($legacy): array {
|
return array_values(array_map(function (array $candidate) use ($legacy): array {
|
||||||
$roleStandard = $this->mapRoleStandard($legacy['ruolo']);
|
$roleStandard = $this->mapRoleStandard($legacy['ruolo']);
|
||||||
if ($roleStandard === null) {
|
if ($roleStandard === null) {
|
||||||
$tokens = array_keys($candidate['role_tokens']);
|
$tokens = array_keys($candidate['role_tokens']);
|
||||||
$roleStandard = count($tokens) === 1 ? $tokens[0] : null;
|
$roleStandard = count($tokens) === 1 ? $tokens[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -337,7 +337,7 @@ private function collectPersonaRelationCandidates(array $personaMatches, ?int $s
|
||||||
}
|
}
|
||||||
|
|
||||||
$personaIds = array_values(array_unique(array_map(fn(array $row): int => (int) $row['id'], $personaMatches)));
|
$personaIds = array_values(array_unique(array_map(fn(array $row): int => (int) $row['id'], $personaMatches)));
|
||||||
$rows = DB::table('persone_unita_relazioni as pur')
|
$rows = DB::table('persone_unita_relazioni as pur')
|
||||||
->join('unita_immobiliari as ui', 'ui.id', '=', 'pur.unita_id')
|
->join('unita_immobiliari as ui', 'ui.id', '=', 'pur.unita_id')
|
||||||
->whereIn('pur.persona_id', $personaIds)
|
->whereIn('pur.persona_id', $personaIds)
|
||||||
->when($stabileId, fn($query) => $query->where('ui.stabile_id', $stabileId))
|
->when($stabileId, fn($query) => $query->where('ui.stabile_id', $stabileId))
|
||||||
|
|
@ -357,11 +357,11 @@ private function collectPersonaRelationCandidates(array $personaMatches, ?int $s
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
$unitId = (int) $row->unit_id;
|
$unitId = (int) $row->unit_id;
|
||||||
$grouped[$unitId] ??= [
|
$grouped[$unitId] ??= [
|
||||||
'unit_id' => $unitId,
|
'unit_id' => $unitId,
|
||||||
'unit_label' => $this->buildUnitLabel($row),
|
'unit_label' => $this->buildUnitLabel($row),
|
||||||
'unit_reference' => $this->buildUnitReference($row),
|
'unit_reference' => $this->buildUnitReference($row),
|
||||||
'source' => 'persona_relazione',
|
'source' => 'persona_relazione',
|
||||||
'role_tokens' => [],
|
'role_tokens' => [],
|
||||||
'safe_role_creation' => true,
|
'safe_role_creation' => true,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -400,8 +400,8 @@ private function collectImportUnitCandidates(RubricaUniversale $rubrica, ?int $s
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
$unit = $this->resolveUnitFromImportRow($stabileId, [
|
$unit = $this->resolveUnitFromImportRow($stabileId, [
|
||||||
'nom_cond' => (string) ($row->nom_cond ?? ''),
|
'nom_cond' => (string) ($row->nom_cond ?? ''),
|
||||||
'scala' => (string) ($row->scala ?? ''),
|
'scala' => (string) ($row->scala ?? ''),
|
||||||
'interno' => (string) ($row->interno ?? ''),
|
'interno' => (string) ($row->interno ?? ''),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($unit === null) {
|
if ($unit === null) {
|
||||||
|
|
@ -410,11 +410,11 @@ private function collectImportUnitCandidates(RubricaUniversale $rubrica, ?int $s
|
||||||
|
|
||||||
$roleFromNote = $this->mapRoleStandard($legacy['ruolo']);
|
$roleFromNote = $this->mapRoleStandard($legacy['ruolo']);
|
||||||
$candidates[] = [
|
$candidates[] = [
|
||||||
'unit_id' => (int) $unit->unit_id,
|
'unit_id' => (int) $unit->unit_id,
|
||||||
'unit_label' => $this->buildUnitLabel($unit),
|
'unit_label' => $this->buildUnitLabel($unit),
|
||||||
'unit_reference' => $this->buildUnitReference($unit),
|
'unit_reference' => $this->buildUnitReference($unit),
|
||||||
'source' => 'gescon_import',
|
'source' => 'gescon_import',
|
||||||
'role_standard' => $roleFromNote,
|
'role_standard' => $roleFromNote,
|
||||||
'safe_role_creation' => false,
|
'safe_role_creation' => false,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -490,8 +490,8 @@ private function mergeCandidates(array ...$candidateSets): array
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$merged[$unitId]['source'] .= '+' . $candidate['source'];
|
$merged[$unitId]['source'] .= '+' . $candidate['source'];
|
||||||
$merged[$unitId]['safe_role_creation'] = (bool) $merged[$unitId]['safe_role_creation'] || (bool) $candidate['safe_role_creation'];
|
$merged[$unitId]['safe_role_creation'] = (bool) $merged[$unitId]['safe_role_creation'] || (bool) $candidate['safe_role_creation'];
|
||||||
if (($merged[$unitId]['role_standard'] ?? null) === null && ($candidate['role_standard'] ?? null) !== null) {
|
if (($merged[$unitId]['role_standard'] ?? null) === null && ($candidate['role_standard'] ?? null) !== null) {
|
||||||
$merged[$unitId]['role_standard'] = $candidate['role_standard'];
|
$merged[$unitId]['role_standard'] = $candidate['role_standard'];
|
||||||
}
|
}
|
||||||
|
|
@ -509,7 +509,7 @@ private function resolveUnitFromImportRow(int $stabileId, array $row): ?object
|
||||||
->where('stabile_id', $stabileId)
|
->where('stabile_id', $stabileId)
|
||||||
->select(['id as unit_id', 'codice_unita', 'denominazione', 'scala', 'interno']);
|
->select(['id as unit_id', 'codice_unita', 'denominazione', 'scala', 'interno']);
|
||||||
|
|
||||||
$scala = trim((string) ($row['scala'] ?? ''));
|
$scala = trim((string) ($row['scala'] ?? ''));
|
||||||
$interno = trim((string) ($row['interno'] ?? ''));
|
$interno = trim((string) ($row['interno'] ?? ''));
|
||||||
|
|
||||||
if ($scala !== '' || $interno !== '') {
|
if ($scala !== '' || $interno !== '') {
|
||||||
|
|
@ -560,19 +560,19 @@ private function buildStabileReference(array $stabile): ?string
|
||||||
return $parts !== [] ? implode(' - ', $parts) : ('Stabile #' . (int) $stabile['id']);
|
return $parts !== [] ? implode(' - ', $parts) : ('Stabile #' . (int) $stabile['id']);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildUnitReference(object|array $row): string
|
private function buildUnitReference(object | array $row): string
|
||||||
{
|
{
|
||||||
$code = trim((string) data_get((array) $row, 'codice_unita', data_get($row, 'codice_unita', '')));
|
$code = trim((string) data_get((array) $row, 'codice_unita', data_get($row, 'codice_unita', '')));
|
||||||
$label = trim((string) data_get((array) $row, 'denominazione', data_get($row, 'denominazione', '')));
|
$label = trim((string) data_get((array) $row, 'denominazione', data_get($row, 'denominazione', '')));
|
||||||
|
|
||||||
return trim($code . ($label !== '' ? ' - ' . $label : ''));
|
return trim($code . ($label !== '' ? ' - ' . $label : ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildUnitLabel(object|array $row): string
|
private function buildUnitLabel(object | array $row): string
|
||||||
{
|
{
|
||||||
$code = trim((string) data_get((array) $row, 'codice_unita', data_get($row, 'codice_unita', '')));
|
$code = trim((string) data_get((array) $row, 'codice_unita', data_get($row, 'codice_unita', '')));
|
||||||
$scala = trim((string) data_get((array) $row, 'scala', data_get($row, 'scala', '')));
|
$scala = trim((string) data_get((array) $row, 'scala', data_get($row, 'scala', '')));
|
||||||
$interno = trim((string) data_get((array) $row, 'interno', data_get($row, 'interno', '')));
|
$interno = trim((string) data_get((array) $row, 'interno', data_get($row, 'interno', '')));
|
||||||
$denominazione = trim((string) data_get((array) $row, 'denominazione', data_get($row, 'denominazione', '')));
|
$denominazione = trim((string) data_get((array) $row, 'denominazione', data_get($row, 'denominazione', '')));
|
||||||
|
|
||||||
$bits = array_values(array_filter([$code, $scala, $interno, $denominazione]));
|
$bits = array_values(array_filter([$code, $scala, $interno, $denominazione]));
|
||||||
|
|
@ -624,7 +624,7 @@ private function normalizeDigits(string $value): string
|
||||||
|
|
||||||
private function normalizeText(string $value): string
|
private function normalizeText(string $value): string
|
||||||
{
|
{
|
||||||
$upper = mb_strtoupper(trim($value));
|
$upper = mb_strtoupper(trim($value));
|
||||||
$normalized = preg_replace('/[^A-Z0-9]+/u', '', $upper);
|
$normalized = preg_replace('/[^A-Z0-9]+/u', '', $upper);
|
||||||
|
|
||||||
return is_string($normalized) ? $normalized : '';
|
return is_string($normalized) ? $normalized : '';
|
||||||
|
|
|
||||||
|
|
@ -163,12 +163,15 @@ public function handle(): int
|
||||||
$lineCount++;
|
$lineCount++;
|
||||||
$this->line("[{$lineCount}] {$line}");
|
$this->line("[{$lineCount}] {$line}");
|
||||||
|
|
||||||
$phone = $parsed['phone_number'];
|
$phone = $parsed['phone_number'];
|
||||||
$direction = $parsed['direction'];
|
$direction = $parsed['direction'];
|
||||||
$receivedAt = $parsed['occurred_at'] ?? now();
|
$receivedAt = $parsed['occurred_at'] ?? now();
|
||||||
$durationSeconds = $parsed['duration_seconds'];
|
$durationSeconds = $parsed['duration_seconds'];
|
||||||
$targetExtension = (string) ($parsed['extension'] ?? '');
|
$targetExtension = (string) ($parsed['extension'] ?? '');
|
||||||
[$assignedUserId, $stabileId] = $this->resolveRouting($targetExtension);
|
$routing = $this->resolveRouting($parsed);
|
||||||
|
$assignedUserId = $routing['user_id'];
|
||||||
|
$stabileId = $routing['stabile_id'];
|
||||||
|
$amministratoreId = $routing['amministratore_id'];
|
||||||
|
|
||||||
Log::info('SMDR line received', [
|
Log::info('SMDR line received', [
|
||||||
'line' => $line,
|
'line' => $line,
|
||||||
|
|
@ -195,6 +198,9 @@ public function handle(): int
|
||||||
'source_port' => $port,
|
'source_port' => $port,
|
||||||
'assigned_user_id' => $assignedUserId,
|
'assigned_user_id' => $assignedUserId,
|
||||||
'stabile_id' => $stabileId,
|
'stabile_id' => $stabileId,
|
||||||
|
'amministratore_id'=> $amministratoreId,
|
||||||
|
'studio_role' => $routing['studio_role'],
|
||||||
|
'studio_mode' => $routing['studio_mode'],
|
||||||
'smdr' => [
|
'smdr' => [
|
||||||
'date' => $parsed['date'],
|
'date' => $parsed['date'],
|
||||||
'time' => $parsed['time'],
|
'time' => $parsed['time'],
|
||||||
|
|
@ -202,6 +208,9 @@ public function handle(): int
|
||||||
'call_phase' => $parsed['call_phase'],
|
'call_phase' => $parsed['call_phase'],
|
||||||
'co' => $parsed['co'],
|
'co' => $parsed['co'],
|
||||||
'co_line_number' => $parsed['co_line_number'],
|
'co_line_number' => $parsed['co_line_number'],
|
||||||
|
'line_label' => $routing['line_label'],
|
||||||
|
'route_group' => $routing['route_group'],
|
||||||
|
'target_extension' => $routing['target_extension'],
|
||||||
'dial_number_raw' => $parsed['dial_number_raw'],
|
'dial_number_raw' => $parsed['dial_number_raw'],
|
||||||
'dial_number' => $parsed['dial_number'],
|
'dial_number' => $parsed['dial_number'],
|
||||||
'ring_duration_raw' => $parsed['ring_duration_raw'],
|
'ring_duration_raw' => $parsed['ring_duration_raw'],
|
||||||
|
|
@ -232,16 +241,19 @@ public function handle(): int
|
||||||
CommunicationMessage::query()->create($communicationPayload);
|
CommunicationMessage::query()->create($communicationPayload);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($writePostIt && Schema::hasTable('chiamate_post_it')) {
|
if ($writePostIt && Schema::hasTable('chiamate_post_it') && $this->shouldCreateOperationalPostIt($parsed)) {
|
||||||
try {
|
try {
|
||||||
|
$lineSuffix = ! empty($parsed['co']) ? ' - linea ' . (string) $parsed['co'] : '';
|
||||||
|
|
||||||
ChiamataPostIt::query()->create([
|
ChiamataPostIt::query()->create([
|
||||||
'telefono' => $phone,
|
'telefono' => $phone,
|
||||||
'stabile_id' => $stabileId,
|
'stabile_id' => $stabileId,
|
||||||
'creato_da_user_id' => $assignedUserId,
|
'creato_da_user_id' => $assignedUserId,
|
||||||
'nome_chiamante' => 'SMDR ' . strtoupper($direction),
|
'nome_chiamante' => 'SMDR INBOUND',
|
||||||
'oggetto' => 'Chiamata ' . strtoupper($direction) . ' da SMDR',
|
'oggetto' => 'Chiamata in entrata da SMDR' . $lineSuffix,
|
||||||
'nota' => $line,
|
'nota' => $line,
|
||||||
'direzione' => $this->mapPostItDirection($direction, $durationSeconds),
|
'direzione' => 'in_arrivo',
|
||||||
|
'esito' => ($durationSeconds ?? 0) > 0 ? 'answered' : 'missed',
|
||||||
'origine' => 'smdr',
|
'origine' => 'smdr',
|
||||||
'origine_id' => $fingerprint,
|
'origine_id' => $fingerprint,
|
||||||
'durata_secondi' => $durationSeconds,
|
'durata_secondi' => $durationSeconds,
|
||||||
|
|
@ -482,16 +494,28 @@ private function mapPostItDirection(string $direction, ?int $durationSeconds): s
|
||||||
return 'in_uscita';
|
return 'in_uscita';
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolveRouting(string $extension): array
|
/** @param array<string,mixed> $parsed */
|
||||||
|
private function resolveRouting(array $parsed): array
|
||||||
{
|
{
|
||||||
$routing = app(PbxRoutingService::class)->resolveByExtension($extension);
|
return app(PbxRoutingService::class)->resolveBySmdr(
|
||||||
if (! $routing['user']) {
|
(string) ($parsed['extension'] ?? ''),
|
||||||
return [null, null];
|
(string) ($parsed['co'] ?? ''),
|
||||||
|
(string) ($parsed['direction'] ?? '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string,mixed> $parsed */
|
||||||
|
private function shouldCreateOperationalPostIt(array $parsed): bool
|
||||||
|
{
|
||||||
|
if (($parsed['direction'] ?? null) !== 'inbound') {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
$phone = preg_replace('/\D+/', '', (string) ($parsed['phone_number'] ?? '')) ?: '';
|
||||||
$routing['user_id'],
|
if ($phone === '' || strlen($phone) <= 4) {
|
||||||
$routing['stabile_id'],
|
return false;
|
||||||
];
|
}
|
||||||
|
|
||||||
|
return ! empty($parsed['co']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,17 @@ class RubricaUniversaleScheda extends Page
|
||||||
/** @var array<int, array<string, mixed>> */
|
/** @var array<int, array<string, mixed>> */
|
||||||
public array $stabili = [];
|
public array $stabili = [];
|
||||||
|
|
||||||
|
/** @var array<string, mixed> */
|
||||||
|
public array $collegamentoSuggerito = [];
|
||||||
|
|
||||||
|
public ?int $collegamentoStabileId = null;
|
||||||
|
|
||||||
|
public ?int $collegamentoUnitaId = null;
|
||||||
|
|
||||||
|
public string $collegamentoRuolo = 'altro';
|
||||||
|
|
||||||
|
public string $collegamentoRuoloCustom = '';
|
||||||
|
|
||||||
/** @var array<int, array<string, mixed>> */
|
/** @var array<int, array<string, mixed>> */
|
||||||
public array $contattiPrincipali = [];
|
public array $contattiPrincipali = [];
|
||||||
|
|
||||||
|
|
@ -303,10 +314,468 @@ public function mount(int | string $record): void
|
||||||
|
|
||||||
$this->loadEmailMultiple();
|
$this->loadEmailMultiple();
|
||||||
$this->fillInlineForm();
|
$this->fillInlineForm();
|
||||||
|
$this->hydrateCollegamentoWorkspace();
|
||||||
$this->hydrateFornitoriWorkspace();
|
$this->hydrateFornitoriWorkspace();
|
||||||
$this->hydrateStudioCollaboratoreWorkspace();
|
$this->hydrateStudioCollaboratoreWorkspace();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function hydrateCollegamentoWorkspace(): void
|
||||||
|
{
|
||||||
|
$suggestion = $this->resolveCollegamentoSuggerito();
|
||||||
|
|
||||||
|
$this->collegamentoSuggerito = $suggestion;
|
||||||
|
$this->collegamentoStabileId = ! empty($suggestion['stabile_id']) ? (int) $suggestion['stabile_id'] : null;
|
||||||
|
$this->collegamentoUnitaId = ! empty($suggestion['unita_id']) ? (int) $suggestion['unita_id'] : null;
|
||||||
|
$this->collegamentoRuolo = ! empty($suggestion['role_standard']) ? (string) $suggestion['role_standard'] : 'altro';
|
||||||
|
$this->collegamentoRuoloCustom = ! empty($suggestion['role_custom']) ? (string) $suggestion['role_custom'] : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collegaCollegamentoSuggerito(): void
|
||||||
|
{
|
||||||
|
if ($this->collegamentoSuggerito === []) {
|
||||||
|
Notification::make()
|
||||||
|
->title('Nessun collegamento suggerito disponibile')
|
||||||
|
->warning()
|
||||||
|
->send();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->persistCollegamentoRubrica(
|
||||||
|
(int) ($this->collegamentoSuggerito['stabile_id'] ?? 0),
|
||||||
|
! empty($this->collegamentoSuggerito['unita_id']) ? (int) $this->collegamentoSuggerito['unita_id'] : null,
|
||||||
|
(string) ($this->collegamentoSuggerito['role_standard'] ?? 'altro'),
|
||||||
|
(string) ($this->collegamentoSuggerito['role_custom'] ?? ''),
|
||||||
|
(string) ($this->collegamentoSuggerito['source'] ?? 'suggerimento_scheda')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function salvaCollegamentoManuale(): void
|
||||||
|
{
|
||||||
|
$data = validator([
|
||||||
|
'stabile_id' => $this->collegamentoStabileId,
|
||||||
|
'unita_immobiliare_id' => $this->collegamentoUnitaId,
|
||||||
|
'ruolo_standard' => $this->collegamentoRuolo,
|
||||||
|
'ruolo_custom' => $this->collegamentoRuoloCustom,
|
||||||
|
], [
|
||||||
|
'stabile_id' => ['required', 'integer'],
|
||||||
|
'unita_immobiliare_id' => ['nullable', 'integer'],
|
||||||
|
'ruolo_standard' => ['required', 'string', 'max:120'],
|
||||||
|
'ruolo_custom' => ['nullable', 'string', 'max:255'],
|
||||||
|
])->validate();
|
||||||
|
|
||||||
|
$this->persistCollegamentoRubrica(
|
||||||
|
(int) $data['stabile_id'],
|
||||||
|
! empty($data['unita_immobiliare_id']) ? (int) $data['unita_immobiliare_id'] : null,
|
||||||
|
(string) $data['ruolo_standard'],
|
||||||
|
(string) ($data['ruolo_custom'] ?? ''),
|
||||||
|
'manuale_scheda'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function persistCollegamentoRubrica(
|
||||||
|
int $stabileId,
|
||||||
|
?int $unitaId,
|
||||||
|
string $ruoloStandard,
|
||||||
|
string $ruoloCustom = '',
|
||||||
|
string $source = 'manuale_scheda'
|
||||||
|
): void {
|
||||||
|
$stabile = Stabile::query()->find($stabileId);
|
||||||
|
if (! $stabile) {
|
||||||
|
Notification::make()
|
||||||
|
->title('Stabile non trovato')
|
||||||
|
->warning()
|
||||||
|
->send();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = Auth::user();
|
||||||
|
$isSuperAdmin = $user instanceof User && method_exists($user, 'hasRole')
|
||||||
|
? (bool) $user->hasRole('super-admin')
|
||||||
|
: false;
|
||||||
|
|
||||||
|
if (! $isSuperAdmin && $this->adminId > 0 && (int) $stabile->amministratore_id !== $this->adminId) {
|
||||||
|
Notification::make()
|
||||||
|
->title('Stabile fuori dal tenant attivo')
|
||||||
|
->body('Il collegamento può essere creato solo sugli stabili dell\'amministratore corrente.')
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$unita = null;
|
||||||
|
if ($unitaId !== null && $unitaId > 0) {
|
||||||
|
$unita = UnitaImmobiliare::query()
|
||||||
|
->where('id', $unitaId)
|
||||||
|
->where('stabile_id', (int) $stabile->id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $unita) {
|
||||||
|
Notification::make()
|
||||||
|
->title('Unità non coerente con lo stabile')
|
||||||
|
->warning()
|
||||||
|
->send();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$ruoloStandard = trim($ruoloStandard) !== '' ? trim($ruoloStandard) : 'altro';
|
||||||
|
$ruoloCustom = trim($ruoloCustom);
|
||||||
|
|
||||||
|
$attributes = [
|
||||||
|
'rubrica_id' => (int) $this->rubrica->id,
|
||||||
|
'ruolo_standard' => $ruoloStandard,
|
||||||
|
'stabile_id' => (int) $stabile->id,
|
||||||
|
'unita_immobiliare_id' => $unita?->id,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($ruoloCustom !== '') {
|
||||||
|
$attributes['ruolo_custom'] = $ruoloCustom;
|
||||||
|
}
|
||||||
|
|
||||||
|
$existing = RubricaRuolo::query()
|
||||||
|
->where('rubrica_id', (int) $this->rubrica->id)
|
||||||
|
->where('ruolo_standard', $ruoloStandard)
|
||||||
|
->where('stabile_id', (int) $stabile->id)
|
||||||
|
->where('unita_immobiliare_id', $unita?->id)
|
||||||
|
->when($ruoloCustom !== '', fn(Builder $query) => $query->where('ruolo_custom', $ruoloCustom))
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'ruolo_custom' => $ruoloCustom !== '' ? $ruoloCustom : null,
|
||||||
|
'is_attivo' => true,
|
||||||
|
'is_preferito' => $existing ? (bool) ($existing->is_preferito ?? false) : count($this->ruoliAttivi) === 0,
|
||||||
|
'data_inizio' => $existing?->data_inizio ?: now()->toDateString(),
|
||||||
|
'meta' => array_filter([
|
||||||
|
'source' => $source,
|
||||||
|
'linked_from' => 'rubrica_scheda',
|
||||||
|
], fn($value) => $value !== null && $value !== ''),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
$existing->fill($payload);
|
||||||
|
$existing->save();
|
||||||
|
} else {
|
||||||
|
RubricaRuolo::query()->create(array_merge($attributes, $payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->rubrica->riferimento_stabile = $this->buildLocalStabileReference($stabile);
|
||||||
|
$this->rubrica->riferimento_unita = $unita ? $this->buildLocalUnitReference($unita) : $this->rubrica->riferimento_unita;
|
||||||
|
if (in_array($ruoloStandard, ['condomino', 'inquilino', 'fornitore', 'amministratore', 'altro'], true)) {
|
||||||
|
$this->rubrica->tipo_utenza_call = $ruoloStandard;
|
||||||
|
}
|
||||||
|
$this->rubrica->data_ultima_modifica = now()->toDateString();
|
||||||
|
$this->rubrica->modificato_da = Auth::id();
|
||||||
|
$this->rubrica->save();
|
||||||
|
|
||||||
|
$this->mount((int) $this->rubrica->id);
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title('Collegamento aggiornato')
|
||||||
|
->body('La scheda rubrica è ora agganciata a stabile e unità selezionati.')
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
private function resolveCollegamentoSuggerito(): array
|
||||||
|
{
|
||||||
|
$legacy = $this->parseCollegamentoLegacyMeta((string) ($this->rubrica->note ?? ''));
|
||||||
|
$stabile = $this->resolveCollegamentoStabile($legacy['cod_stabile'] ?? null, (string) ($this->rubrica->riferimento_stabile ?? ''));
|
||||||
|
if (empty($stabile['id'])) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$unit = $this->resolveCollegamentoUnita(
|
||||||
|
(int) $stabile['id'],
|
||||||
|
$legacy,
|
||||||
|
(string) ($this->rubrica->riferimento_unita ?? '')
|
||||||
|
);
|
||||||
|
|
||||||
|
return array_filter([
|
||||||
|
'stabile_id' => (int) $stabile['id'],
|
||||||
|
'stabile_label' => $this->buildCollegamentoStabileReference($stabile),
|
||||||
|
'unita_id' => (int) ($unit['unit_id'] ?? 0) ?: null,
|
||||||
|
'unita_label' => (string) ($unit['unit_label'] ?? ''),
|
||||||
|
'unita_reference' => (string) ($unit['unit_reference'] ?? ''),
|
||||||
|
'role_standard' => $this->mapCollegamentoRole($legacy['ruolo'] ?? $this->rubrica->tipo_utenza_call),
|
||||||
|
'role_custom' => null,
|
||||||
|
'source' => (string) ($unit['source'] ?? 'riferimento_stabile'),
|
||||||
|
'requires_review' => (bool) ($unit['source'] ?? false),
|
||||||
|
], fn($value) => $value !== null && $value !== '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array{cod_stabile:?string,cod_cond:?string,ruolo:?string} */
|
||||||
|
private function parseCollegamentoLegacyMeta(string $note): array
|
||||||
|
{
|
||||||
|
$payload = [
|
||||||
|
'cod_stabile' => null,
|
||||||
|
'cod_cond' => null,
|
||||||
|
'ruolo' => null,
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach (array_keys($payload) as $key) {
|
||||||
|
if (preg_match('/' . preg_quote($key, '/') . '=([^|\n\r]+)/i', $note, $matches) === 1) {
|
||||||
|
$payload[$key] = trim((string) ($matches[1] ?? '')) ?: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array{id:int|null,codice_stabile:?string,cod_stabile:?string,denominazione:?string} */
|
||||||
|
private function resolveCollegamentoStabile(?string $legacyCode, string $reference): array
|
||||||
|
{
|
||||||
|
$blank = ['id' => null, 'codice_stabile' => null, 'cod_stabile' => null, 'denominazione' => null];
|
||||||
|
|
||||||
|
foreach (array_filter([$legacyCode, ...$this->extractReferenceCandidates($reference)]) as $candidate) {
|
||||||
|
$code = trim((string) $candidate);
|
||||||
|
if ($code === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$trimmed = ltrim($code, '0');
|
||||||
|
$row = Stabile::query()
|
||||||
|
->when($this->adminId > 0, fn(Builder $query) => $query->where('amministratore_id', $this->adminId))
|
||||||
|
->where(function (Builder $query) use ($code, $trimmed): void {
|
||||||
|
$query->where('codice_stabile', $code)
|
||||||
|
->orWhere('cod_stabile', $code);
|
||||||
|
|
||||||
|
if ($trimmed !== '' && $trimmed !== $code) {
|
||||||
|
$query->orWhere('codice_stabile', $trimmed)
|
||||||
|
->orWhere('cod_stabile', $trimmed);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->orderBy('id')
|
||||||
|
->first(['id', 'codice_stabile', 'cod_stabile', 'denominazione']);
|
||||||
|
|
||||||
|
if ($row) {
|
||||||
|
return [
|
||||||
|
'id' => (int) $row->id,
|
||||||
|
'codice_stabile' => $row->codice_stabile ? (string) $row->codice_stabile : null,
|
||||||
|
'cod_stabile' => $row->cod_stabile ? (string) $row->cod_stabile : null,
|
||||||
|
'denominazione' => $row->denominazione ? (string) $row->denominazione : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = $this->extractReferenceName($reference);
|
||||||
|
if ($name !== '') {
|
||||||
|
$rows = Stabile::query()
|
||||||
|
->when($this->adminId > 0, fn(Builder $query) => $query->where('amministratore_id', $this->adminId))
|
||||||
|
->whereRaw("LOWER(COALESCE(denominazione, '')) = ?", [mb_strtolower($name)])
|
||||||
|
->limit(2)
|
||||||
|
->get(['id', 'codice_stabile', 'cod_stabile', 'denominazione']);
|
||||||
|
|
||||||
|
if ($rows->count() === 1) {
|
||||||
|
$row = $rows->first();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => (int) $row->id,
|
||||||
|
'codice_stabile' => $row->codice_stabile ? (string) $row->codice_stabile : null,
|
||||||
|
'cod_stabile' => $row->cod_stabile ? (string) $row->cod_stabile : null,
|
||||||
|
'denominazione' => $row->denominazione ? (string) $row->denominazione : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $blank;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
private function resolveCollegamentoUnita(int $stabileId, array $legacy, string $reference): array
|
||||||
|
{
|
||||||
|
$reference = trim($reference);
|
||||||
|
if ($reference !== '') {
|
||||||
|
$code = trim((string) Str::before($reference, ' - '));
|
||||||
|
if ($code !== '') {
|
||||||
|
$rows = UnitaImmobiliare::query()
|
||||||
|
->where('stabile_id', $stabileId)
|
||||||
|
->where('codice_unita', $code)
|
||||||
|
->limit(2)
|
||||||
|
->get(['id', 'codice_unita', 'denominazione', 'scala', 'interno']);
|
||||||
|
|
||||||
|
if ($rows->count() === 1) {
|
||||||
|
$row = $rows->first();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'unit_id' => (int) $row->id,
|
||||||
|
'unit_label' => $this->buildLocalUnitLabel($row),
|
||||||
|
'unit_reference' => $this->buildLocalUnitReference($row),
|
||||||
|
'source' => 'riferimento_unita',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($legacy['cod_stabile']) && ! empty($legacy['cod_cond']) && $this->hasGesconImportCondomin()) {
|
||||||
|
$rows = DB::connection('gescon_import')
|
||||||
|
->table('condomin')
|
||||||
|
->where('cod_stabile', trim((string) $legacy['cod_stabile']))
|
||||||
|
->where('cod_cond', trim((string) $legacy['cod_cond']))
|
||||||
|
->select(['nom_cond', 'scala', 'interno'])
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$candidates = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$unit = $this->resolveUnitFromImportRow($stabileId, [
|
||||||
|
'nom_cond' => (string) ($row->nom_cond ?? ''),
|
||||||
|
'scala' => (string) ($row->scala ?? ''),
|
||||||
|
'interno' => (string) ($row->interno ?? ''),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (! $unit) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidates[(int) $unit->unit_id] = [
|
||||||
|
'unit_id' => (int) $unit->unit_id,
|
||||||
|
'unit_label' => $this->buildLocalUnitLabel($unit),
|
||||||
|
'unit_reference' => $this->buildLocalUnitReference($unit),
|
||||||
|
'source' => 'gescon_import',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($candidates) === 1) {
|
||||||
|
return array_values($candidates)[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<int, string> */
|
||||||
|
private function extractReferenceCandidates(string $reference): array
|
||||||
|
{
|
||||||
|
$parts = preg_split('/\s*-\s*/', trim($reference)) ?: [];
|
||||||
|
|
||||||
|
return array_values(array_filter(array_map(static function (string $part): string {
|
||||||
|
$value = trim($part);
|
||||||
|
return preg_match('/^[0-9]+$/', $value) === 1 ? $value : '';
|
||||||
|
}, $parts)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extractReferenceName(string $reference): string
|
||||||
|
{
|
||||||
|
$parts = preg_split('/\s*-\s*/', trim($reference)) ?: [];
|
||||||
|
$parts = array_values(array_filter(array_map(static fn(string $part): string => trim($part), $parts), static fn(string $part): bool => $part !== '' && preg_match('/^[0-9]+$/', $part) !== 1));
|
||||||
|
|
||||||
|
return $parts !== [] ? (string) end($parts) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildLocalStabileReference(Stabile $stabile): string
|
||||||
|
{
|
||||||
|
$parts = array_values(array_filter([
|
||||||
|
trim((string) ($stabile->cod_stabile ?? '')),
|
||||||
|
trim((string) ($stabile->codice_stabile ?? '')),
|
||||||
|
trim((string) ($stabile->denominazione ?? '')),
|
||||||
|
]));
|
||||||
|
|
||||||
|
return $parts !== [] ? implode(' - ', $parts) : ('Stabile #' . (int) $stabile->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildCollegamentoStabileReference(array $stabile): string
|
||||||
|
{
|
||||||
|
$parts = array_values(array_filter([
|
||||||
|
trim((string) ($stabile['cod_stabile'] ?? '')),
|
||||||
|
trim((string) ($stabile['codice_stabile'] ?? '')),
|
||||||
|
trim((string) ($stabile['denominazione'] ?? '')),
|
||||||
|
]));
|
||||||
|
|
||||||
|
return $parts !== [] ? implode(' - ', $parts) : ('Stabile #' . (int) ($stabile['id'] ?? 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildLocalUnitReference(object $unit): string
|
||||||
|
{
|
||||||
|
$code = trim((string) data_get($unit, 'codice_unita', ''));
|
||||||
|
$label = trim((string) data_get($unit, 'denominazione', ''));
|
||||||
|
|
||||||
|
return trim($code . ($label !== '' ? ' - ' . $label : ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildLocalUnitLabel(object $unit): string
|
||||||
|
{
|
||||||
|
$bits = array_values(array_filter([
|
||||||
|
trim((string) data_get($unit, 'codice_unita', '')),
|
||||||
|
trim((string) data_get($unit, 'scala', '')),
|
||||||
|
trim((string) data_get($unit, 'interno', '')),
|
||||||
|
trim((string) data_get($unit, 'denominazione', '')),
|
||||||
|
]));
|
||||||
|
|
||||||
|
return implode(' / ', $bits);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mapCollegamentoRole(?string $value): ?string
|
||||||
|
{
|
||||||
|
$normalized = strtolower(trim((string) $value));
|
||||||
|
|
||||||
|
return match ($normalized) {
|
||||||
|
'c', 'condomino', 'proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario', 'titolare' => 'condomino',
|
||||||
|
'i', 'inquilino', 'conduttore', 'locatario' => 'inquilino',
|
||||||
|
'fornitore' => 'fornitore',
|
||||||
|
'amministratore' => 'amministratore',
|
||||||
|
'collaboratore' => 'collaboratore',
|
||||||
|
'altro' => 'altro',
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveUnitFromImportRow(int $stabileId, array $row): ?object
|
||||||
|
{
|
||||||
|
$query = DB::table('unita_immobiliari')
|
||||||
|
->where('stabile_id', $stabileId)
|
||||||
|
->select(['id as unit_id', 'codice_unita', 'denominazione', 'scala', 'interno']);
|
||||||
|
|
||||||
|
$scala = trim((string) ($row['scala'] ?? ''));
|
||||||
|
$interno = trim((string) ($row['interno'] ?? ''));
|
||||||
|
|
||||||
|
if ($scala !== '' || $interno !== '') {
|
||||||
|
$strict = (clone $query)
|
||||||
|
->when($scala !== '', fn($inner) => $inner->where('scala', $scala))
|
||||||
|
->when($interno !== '', fn($inner) => $inner->where('interno', $interno))
|
||||||
|
->get();
|
||||||
|
if ($strict->count() === 1) {
|
||||||
|
return $strict->first();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedName = $this->normalizeCollegamentoText((string) ($row['nom_cond'] ?? ''));
|
||||||
|
if ($normalizedName === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$matches = (clone $query)
|
||||||
|
->get()
|
||||||
|
->filter(function ($unit) use ($normalizedName): bool {
|
||||||
|
$haystack = $this->normalizeCollegamentoText((string) ($unit->denominazione ?? ''));
|
||||||
|
|
||||||
|
return $haystack !== '' && ($haystack === $normalizedName || str_contains($haystack, $normalizedName) || str_contains($normalizedName, $haystack));
|
||||||
|
})
|
||||||
|
->values();
|
||||||
|
|
||||||
|
return $matches->count() === 1 ? $matches->first() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hasGesconImportCondomin(): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return Schema::connection('gescon_import')->hasTable('condomin');
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeCollegamentoText(string $value): string
|
||||||
|
{
|
||||||
|
$upper = mb_strtoupper(trim($value));
|
||||||
|
$normalized = preg_replace('/[^A-Z0-9]+/u', '', $upper);
|
||||||
|
|
||||||
|
return is_string($normalized) ? $normalized : '';
|
||||||
|
}
|
||||||
|
|
||||||
private function applyLegacyIdentityDefaults(): void
|
private function applyLegacyIdentityDefaults(): void
|
||||||
{
|
{
|
||||||
$legacyIdentity = $this->extractLegacyIdentityFromNote($this->rubrica->note ?? null);
|
$legacyIdentity = $this->extractLegacyIdentityFromNote($this->rubrica->note ?? null);
|
||||||
|
|
@ -1570,7 +2039,7 @@ public function getUrlUnita(?int $unitaId): ?string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<string, string> */
|
/** @return array<string, string> */
|
||||||
protected static function getRuoliLabels(): array
|
public static function getRuoliLabels(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'super_admin' => 'Super admin',
|
'super_admin' => 'Super admin',
|
||||||
|
|
@ -1589,7 +2058,7 @@ protected static function getRuoliLabels(): array
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<int, string> */
|
/** @return array<int, string> */
|
||||||
protected function getStabiliOptions(): array
|
public function getStabiliOptions(): array
|
||||||
{
|
{
|
||||||
$query = Stabile::query();
|
$query = Stabile::query();
|
||||||
|
|
||||||
|
|
@ -1630,7 +2099,7 @@ protected function getTitoliOptions(): array
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<int, string> */
|
/** @return array<int, string> */
|
||||||
protected function getUnitaOptions($stabileId): array
|
public function getUnitaOptions($stabileId): array
|
||||||
{
|
{
|
||||||
$stabileId = (int) ($stabileId ?? 0);
|
$stabileId = (int) ($stabileId ?? 0);
|
||||||
if ($stabileId <= 0) {
|
if ($stabileId <= 0) {
|
||||||
|
|
|
||||||
|
|
@ -1315,20 +1315,20 @@ 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] ?? []);
|
$mapping = (array) ($mappings[(string) $user->id] ?? []);
|
||||||
$extensions = array_values(array_filter((array) ($mapping['extensions'] ?? []), fn($value): bool => is_string($value) && trim($value) !== ''));
|
$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) !== ''));
|
$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) !== ''));
|
$lines = array_values(array_filter((array) ($mapping['lines'] ?? []), fn($value): bool => is_string($value) && trim($value) !== ''));
|
||||||
|
|
||||||
if ($extensions === [] && filled($user->pbx_extension)) {
|
if ($extensions === [] && filled($user->pbx_extension)) {
|
||||||
$extensions = [(string) $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->collaboratorePbxExtensions[(int) $user->id] = implode(', ', $extensions);
|
||||||
$this->collaboratorePbxGroups[(int) $user->id] = implode(', ', $groups);
|
$this->collaboratorePbxGroups[(int) $user->id] = implode(', ', $groups);
|
||||||
$this->collaboratorePbxLines[(int) $user->id] = implode(', ', $lines);
|
$this->collaboratorePbxLines[(int) $user->id] = implode(', ', $lines);
|
||||||
$this->collaboratorePbxPopupEnabled[(int) $user->id] = (bool) ($user->pbx_popup_enabled ?? false);
|
$this->collaboratorePbxPopupEnabled[(int) $user->id] = (bool) ($user->pbx_popup_enabled ?? false);
|
||||||
$this->collaboratorePbxClickToCallEnabled[(int) $user->id] = (bool) ($user->pbx_click_to_call_enabled ?? false);
|
$this->collaboratorePbxClickToCallEnabled[(int) $user->id] = (bool) ($user->pbx_click_to_call_enabled ?? false);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
@ -1355,8 +1355,8 @@ public function getPbxObservedTokensProperty(): array
|
||||||
if (! Schema::hasTable('communication_messages')) {
|
if (! Schema::hasTable('communication_messages')) {
|
||||||
return [
|
return [
|
||||||
'extensions' => [],
|
'extensions' => [],
|
||||||
'groups' => [],
|
'groups' => [],
|
||||||
'lines' => [],
|
'lines' => [],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1366,8 +1366,8 @@ public function getPbxObservedTokensProperty(): array
|
||||||
->get(['target_extension', 'message_text', 'metadata']);
|
->get(['target_extension', 'message_text', 'metadata']);
|
||||||
|
|
||||||
$extensions = [];
|
$extensions = [];
|
||||||
$groups = [];
|
$groups = [];
|
||||||
$lines = [];
|
$lines = [];
|
||||||
|
|
||||||
foreach ($rows as $message) {
|
foreach ($rows as $message) {
|
||||||
$metadata = (array) ($message->metadata ?? []);
|
$metadata = (array) ($message->metadata ?? []);
|
||||||
|
|
@ -1392,8 +1392,8 @@ public function getPbxObservedTokensProperty(): array
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'extensions' => array_slice(array_values(array_unique($extensions)), 0, 24),
|
'extensions' => array_slice(array_values(array_unique($extensions)), 0, 24),
|
||||||
'groups' => array_slice(array_values(array_unique($groups)), 0, 24),
|
'groups' => array_slice(array_values(array_unique($groups)), 0, 24),
|
||||||
'lines' => array_slice(array_values(array_unique($lines)), 0, 24),
|
'lines' => array_slice(array_values(array_unique($lines)), 0, 24),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1617,7 +1617,7 @@ private function resolvePbxChannels(array $settings): array
|
||||||
|
|
||||||
return match ($channel) {
|
return match ($channel) {
|
||||||
'panasonic', 'panasonic_csta', 'csta' => ['panasonic_csta'],
|
'panasonic', 'panasonic_csta', 'csta' => ['panasonic_csta'],
|
||||||
'smdr' => ['smdr'],
|
'smdr' => ['smdr'],
|
||||||
default => ['panasonic_csta', 'smdr'],
|
default => ['panasonic_csta', 'smdr'],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -1662,26 +1662,26 @@ public function salvaInternoCollaboratore(int $userId): void
|
||||||
}
|
}
|
||||||
|
|
||||||
$extensions = $this->normalizePbxMappingInput((string) ($this->collaboratorePbxExtensions[$userId] ?? $this->collaboratorePbxExtension[$userId] ?? ''));
|
$extensions = $this->normalizePbxMappingInput((string) ($this->collaboratorePbxExtensions[$userId] ?? $this->collaboratorePbxExtension[$userId] ?? ''));
|
||||||
$groups = $this->normalizePbxMappingInput((string) ($this->collaboratorePbxGroups[$userId] ?? ''));
|
$groups = $this->normalizePbxMappingInput((string) ($this->collaboratorePbxGroups[$userId] ?? ''));
|
||||||
$lines = $this->normalizePbxMappingInput((string) ($this->collaboratorePbxLines[$userId] ?? ''));
|
$lines = $this->normalizePbxMappingInput((string) ($this->collaboratorePbxLines[$userId] ?? ''));
|
||||||
|
|
||||||
$user->pbx_extension = $extensions[0] ?? null;
|
$user->pbx_extension = $extensions[0] ?? null;
|
||||||
$user->pbx_popup_enabled = (bool) ($this->collaboratorePbxPopupEnabled[$userId] ?? false);
|
$user->pbx_popup_enabled = (bool) ($this->collaboratorePbxPopupEnabled[$userId] ?? false);
|
||||||
$user->pbx_click_to_call_enabled = (bool) ($this->collaboratorePbxClickToCallEnabled[$userId] ?? false);
|
$user->pbx_click_to_call_enabled = (bool) ($this->collaboratorePbxClickToCallEnabled[$userId] ?? false);
|
||||||
$user->save();
|
$user->save();
|
||||||
|
|
||||||
$impostazioni = $this->amministratore->impostazioni ?? [];
|
$impostazioni = $this->amministratore->impostazioni ?? [];
|
||||||
$pbx = (array) Arr::get($impostazioni, 'pbx', []);
|
$pbx = (array) Arr::get($impostazioni, 'pbx', []);
|
||||||
$mappings = (array) ($pbx['user_mappings'] ?? []);
|
$mappings = (array) ($pbx['user_mappings'] ?? []);
|
||||||
|
|
||||||
$mappings[(string) $userId] = [
|
$mappings[(string) $userId] = [
|
||||||
'extensions' => $extensions,
|
'extensions' => $extensions,
|
||||||
'groups' => $groups,
|
'groups' => $groups,
|
||||||
'lines' => $lines,
|
'lines' => $lines,
|
||||||
];
|
];
|
||||||
|
|
||||||
$pbx['user_mappings'] = $mappings;
|
$pbx['user_mappings'] = $mappings;
|
||||||
$impostazioni['pbx'] = $pbx;
|
$impostazioni['pbx'] = $pbx;
|
||||||
$this->amministratore->impostazioni = $impostazioni;
|
$this->amministratore->impostazioni = $impostazioni;
|
||||||
$this->amministratore->save();
|
$this->amministratore->save();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,20 +34,20 @@ class PostIt extends Page
|
||||||
|
|
||||||
protected string $view = 'filament.pages.strumenti.post-it';
|
protected string $view = 'filament.pages.strumenti.post-it';
|
||||||
|
|
||||||
public ?string $telefono = null;
|
public ?string $telefono = null;
|
||||||
public ?string $nomeChiamante = null;
|
public ?string $nomeChiamante = null;
|
||||||
public ?string $ricercaChiamante = null;
|
public ?string $ricercaChiamante = null;
|
||||||
public string $direzione = 'in_arrivo';
|
public string $direzione = 'in_arrivo';
|
||||||
public ?string $esito = null;
|
public ?string $esito = null;
|
||||||
public ?string $oggetto = null;
|
public ?string $oggetto = null;
|
||||||
public ?string $nota = null;
|
public ?string $nota = null;
|
||||||
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 $focusPostItId = null;
|
||||||
public ?int $selectedPostItId = null;
|
public ?int $selectedPostItId = null;
|
||||||
public string $assegnazioneTipo = 'operatore';
|
public string $assegnazioneTipo = 'operatore';
|
||||||
public ?int $assegnazioneUserId = null;
|
public ?int $assegnazioneUserId = null;
|
||||||
public ?int $assegnazioneFornitoreId = null;
|
public ?int $assegnazioneFornitoreId = null;
|
||||||
|
|
||||||
public ?int $selectedSuggerimentoId = null;
|
public ?int $selectedSuggerimentoId = null;
|
||||||
|
|
@ -61,8 +61,8 @@ public function mount(): void
|
||||||
$this->direzione = 'in_arrivo';
|
$this->direzione = 'in_arrivo';
|
||||||
$this->suggerimentiRubrica = collect();
|
$this->suggerimentiRubrica = collect();
|
||||||
|
|
||||||
$focus = (int) request()->query('focus_post_it', 0);
|
$focus = (int) request()->query('focus_post_it', 0);
|
||||||
$this->focusPostItId = $focus > 0 ? $focus : null;
|
$this->focusPostItId = $focus > 0 ? $focus : null;
|
||||||
$this->selectedPostItId = $this->focusPostItId;
|
$this->selectedPostItId = $this->focusPostItId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,14 +95,14 @@ public function assegnaPostIt(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->validate([
|
$this->validate([
|
||||||
'assegnazioneTipo' => ['required', 'in:operatore,collaboratore,fornitore'],
|
'assegnazioneTipo' => ['required', 'in:operatore,collaboratore,fornitore'],
|
||||||
'assegnazioneUserId' => ['nullable', 'integer', 'exists:users,id'],
|
'assegnazioneUserId' => ['nullable', 'integer', 'exists:users,id'],
|
||||||
'assegnazioneFornitoreId' => ['nullable', 'integer', 'exists:fornitori,id'],
|
'assegnazioneFornitoreId' => ['nullable', 'integer', 'exists:fornitori,id'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
'assegnazione_tipo' => $this->assegnazioneTipo,
|
'assegnazione_tipo' => $this->assegnazioneTipo,
|
||||||
'assegnato_a_user_id' => null,
|
'assegnato_a_user_id' => null,
|
||||||
'assegnato_a_fornitore_id' => null,
|
'assegnato_a_fornitore_id' => null,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -131,7 +131,7 @@ public function assegnaPostIt(): void
|
||||||
|
|
||||||
if ($this->assegnazioneTipo === 'fornitore') {
|
if ($this->assegnazioneTipo === 'fornitore') {
|
||||||
$ticketPayload['assegnato_a_fornitore_id'] = $this->assegnazioneFornitoreId;
|
$ticketPayload['assegnato_a_fornitore_id'] = $this->assegnazioneFornitoreId;
|
||||||
$ticketPayload['assegnato_a_user_id'] = null;
|
$ticketPayload['assegnato_a_user_id'] = null;
|
||||||
} else {
|
} else {
|
||||||
$ticketPayload['assegnato_a_user_id'] = $this->assegnazioneUserId;
|
$ticketPayload['assegnato_a_user_id'] = $this->assegnazioneUserId;
|
||||||
}
|
}
|
||||||
|
|
@ -142,7 +142,7 @@ public function assegnaPostIt(): void
|
||||||
|
|
||||||
if (method_exists($postIt->ticket, 'messages') && Schema::hasTable('ticket_messages')) {
|
if (method_exists($postIt->ticket, 'messages') && Schema::hasTable('ticket_messages')) {
|
||||||
$postIt->ticket->messages()->create([
|
$postIt->ticket->messages()->create([
|
||||||
'user_id' => Auth::id(),
|
'user_id' => Auth::id(),
|
||||||
'messaggio' => 'Assegnazione da Post-it: ' . $this->getSelectedAssignmentLabel(),
|
'messaggio' => 'Assegnazione da Post-it: ' . $this->getSelectedAssignmentLabel(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
@ -160,9 +160,9 @@ public function isPostItTableReady(): bool
|
||||||
public function isPostItAssignmentReady(): bool
|
public function isPostItAssignmentReady(): bool
|
||||||
{
|
{
|
||||||
return $this->isPostItTableReady()
|
return $this->isPostItTableReady()
|
||||||
&& Schema::hasColumn('chiamate_post_it', 'assegnazione_tipo')
|
&& Schema::hasColumn('chiamate_post_it', 'assegnazione_tipo')
|
||||||
&& Schema::hasColumn('chiamate_post_it', 'assegnato_a_user_id')
|
&& Schema::hasColumn('chiamate_post_it', 'assegnato_a_user_id')
|
||||||
&& Schema::hasColumn('chiamate_post_it', 'assegnato_a_fornitore_id');
|
&& Schema::hasColumn('chiamate_post_it', 'assegnato_a_fornitore_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updatedRicercaChiamante(?string $value): void
|
public function updatedRicercaChiamante(?string $value): void
|
||||||
|
|
@ -340,12 +340,12 @@ 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_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,
|
'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(),
|
||||||
'stato' => 'Aperto',
|
'stato' => 'Aperto',
|
||||||
'priorita' => $postIt->priorita,
|
'priorita' => $postIt->priorita,
|
||||||
|
|
@ -538,17 +538,17 @@ private function loadSelectedPostItAssignment(): void
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->assegnazioneTipo = (string) ($postIt->assegnazione_tipo ?: 'operatore');
|
$this->assegnazioneTipo = (string) ($postIt->assegnazione_tipo ?: 'operatore');
|
||||||
$this->assegnazioneUserId = (int) ($postIt->assegnato_a_user_id ?? 0) ?: null;
|
$this->assegnazioneUserId = (int) ($postIt->assegnato_a_user_id ?? 0) ?: null;
|
||||||
$this->assegnazioneFornitoreId = (int) ($postIt->assegnato_a_fornitore_id ?? 0) ?: null;
|
$this->assegnazioneFornitoreId = (int) ($postIt->assegnato_a_fornitore_id ?? 0) ?: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getSelectedAssignmentLabel(): string
|
private function getSelectedAssignmentLabel(): string
|
||||||
{
|
{
|
||||||
return match ($this->assegnazioneTipo) {
|
return match ($this->assegnazioneTipo) {
|
||||||
'fornitore' => 'fornitore #' . (int) ($this->assegnazioneFornitoreId ?? 0),
|
'fornitore' => 'fornitore #' . (int) ($this->assegnazioneFornitoreId ?? 0),
|
||||||
'collaboratore' => 'collaboratore #' . (int) ($this->assegnazioneUserId ?? 0),
|
'collaboratore' => 'collaboratore #' . (int) ($this->assegnazioneUserId ?? 0),
|
||||||
default => 'operatore #' . (int) ($this->assegnazioneUserId ?? 0),
|
default => 'operatore #' . (int) ($this->assegnazioneUserId ?? 0),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
use Illuminate\Database\QueryException;
|
use Illuminate\Database\QueryException;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use UnitEnum;
|
use UnitEnum;
|
||||||
|
|
||||||
class PostItGestione extends Page
|
class PostItGestione extends Page
|
||||||
|
|
@ -122,9 +123,9 @@ public function convertiInTicket(int $postItId): void
|
||||||
'aperto_da_user_id' => Auth::id(),
|
'aperto_da_user_id' => Auth::id(),
|
||||||
'titolo' => $postIt->oggetto ?: ('Chiamata da ' . ($postIt->nome_chiamante ?: 'Contatto sconosciuto')),
|
'titolo' => $postIt->oggetto ?: ('Chiamata da ' . ($postIt->nome_chiamante ?: 'Contatto sconosciuto')),
|
||||||
'descrizione' => $this->buildTicketDescriptionFromPostIt($postItId, $postIt),
|
'descrizione' => $this->buildTicketDescriptionFromPostIt($postItId, $postIt),
|
||||||
'data_apertura' => now(),
|
'data_apertura' => now(),
|
||||||
'stato' => 'Aperto',
|
'stato' => 'Aperto',
|
||||||
'priorita' => $postIt->priorita,
|
'priorita' => $postIt->priorita,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$postIt->ticket_id = $ticket->id;
|
$postIt->ticket_id = $ticket->id;
|
||||||
|
|
@ -245,10 +246,19 @@ public function getRecentiProperty()
|
||||||
public function getRecentiVisibiliProperty()
|
public function getRecentiVisibiliProperty()
|
||||||
{
|
{
|
||||||
return $this->recenti
|
return $this->recenti
|
||||||
->filter(fn(ChiamataPostIt $postIt): bool => ! $this->isInternalPostIt($postIt))
|
->filter(fn(ChiamataPostIt $postIt): bool => $this->shouldShowPostIt($postIt))
|
||||||
->values();
|
->values();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function shouldShowPostIt(ChiamataPostIt $postIt): bool
|
||||||
|
{
|
||||||
|
if ($this->isInternalPostIt($postIt)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ! $this->isLegacyFilteredSmdrPostIt($postIt);
|
||||||
|
}
|
||||||
|
|
||||||
public function getRecentiApertiVisibiliProperty()
|
public function getRecentiApertiVisibiliProperty()
|
||||||
{
|
{
|
||||||
return $this->recentiVisibili
|
return $this->recentiVisibili
|
||||||
|
|
@ -275,27 +285,91 @@ public function getRecentiChiusiRaggruppatiProperty()
|
||||||
|
|
||||||
public function openGroupModal(string $modalKey, string $mode = 'open'): void
|
public function openGroupModal(string $modalKey, string $mode = 'open'): void
|
||||||
{
|
{
|
||||||
$this->selectedGroupModalKey = $modalKey;
|
$this->selectedGroupModalKey = $modalKey;
|
||||||
$this->selectedGroupModalMode = in_array($mode, ['open', 'closed'], true) ? $mode : 'open';
|
$this->selectedGroupModalMode = in_array($mode, ['open', 'closed'], true) ? $mode : 'open';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function closeGroupModal(): void
|
public function closeGroupModal(): void
|
||||||
{
|
{
|
||||||
$this->selectedGroupModalKey = null;
|
$this->selectedGroupModalKey = null;
|
||||||
$this->selectedGroupModalMode = 'open';
|
$this->selectedGroupModalMode = 'open';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function createRubricaDraftFromGroup(string $modalKey, string $mode = 'open'): void
|
||||||
|
{
|
||||||
|
$group = $this->findGroupedCollectionByModalKey($modalKey, $mode);
|
||||||
|
if (! is_array($group)) {
|
||||||
|
Notification::make()->title('Gruppo chiamate non trovato')->warning()->send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$phoneDigits = preg_replace('/\D+/', '', (string) ($group['phone'] ?? '')) ?: '';
|
||||||
|
if (! $this->canCreateRubricaFromPhone($phoneDigits)) {
|
||||||
|
Notification::make()
|
||||||
|
->title('Numero non adatto alla rubrica')
|
||||||
|
->body('Per creare una nuova anagrafica serve un numero esterno valido, non un interno breve.')
|
||||||
|
->warning()
|
||||||
|
->send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$existingId = $this->resolveRubricaIdByPhone($phoneDigits);
|
||||||
|
if ($existingId) {
|
||||||
|
$this->redirect(RubricaUniversaleScheda::getUrl(['record' => $existingId], panel: 'admin-filament'), navigate: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$identity = $this->buildRubricaDraftIdentity((string) ($group['caller_label'] ?? ''), $phoneDigits);
|
||||||
|
$user = Auth::user();
|
||||||
|
$adminId = (int) ($user?->amministratore?->id ?? 0);
|
||||||
|
|
||||||
|
$rubrica = RubricaUniversale::query()->create([
|
||||||
|
'amministratore_id' => $adminId > 0 ? $adminId : null,
|
||||||
|
'nome' => $identity['nome'],
|
||||||
|
'cognome' => $identity['cognome'],
|
||||||
|
'ragione_sociale' => $identity['ragione_sociale'],
|
||||||
|
'tipo_contatto' => $identity['tipo_contatto'],
|
||||||
|
'telefono_cellulare' => $this->isLikelyMobilePhone($phoneDigits) ? $phoneDigits : null,
|
||||||
|
'telefono_ufficio' => $this->isLikelyMobilePhone($phoneDigits) ? null : $phoneDigits,
|
||||||
|
'categoria' => 'altro',
|
||||||
|
'stato' => 'attivo',
|
||||||
|
'data_inserimento' => now()->toDateString(),
|
||||||
|
'data_ultima_modifica'=> now()->toDateString(),
|
||||||
|
'creato_da' => Auth::id(),
|
||||||
|
'modificato_da' => Auth::id(),
|
||||||
|
'note_segreteria' => 'Bozza creata da Lavagna chiamate per gestire richiamo e identificazione del contatto.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$itemIds = collect($group['items'])
|
||||||
|
->map(fn(ChiamataPostIt $item): int => (int) $item->id)
|
||||||
|
->filter(fn(int $id): bool => $id > 0)
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
if ($itemIds !== []) {
|
||||||
|
ChiamataPostIt::query()
|
||||||
|
->whereIn('id', $itemIds)
|
||||||
|
->update([
|
||||||
|
'rubrica_id' => (int) $rubrica->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->rubricaPhoneCache = [];
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title('Bozza anagrafica creata')
|
||||||
|
->body('Nuovo contatto collegato alla lavagna chiamate. Completa ora la scheda rubrica.')
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
|
||||||
|
$this->redirect(RubricaUniversaleScheda::getUrl(['record' => (int) $rubrica->id], panel: 'admin-filament'), navigate: true);
|
||||||
|
}
|
||||||
|
|
||||||
public function getSelectedGroupModalProperty(): ?array
|
public function getSelectedGroupModalProperty(): ?array
|
||||||
{
|
{
|
||||||
if ($this->selectedGroupModalKey === null) {
|
return $this->selectedGroupModalKey !== null
|
||||||
return null;
|
? $this->findGroupedCollectionByModalKey($this->selectedGroupModalKey, $this->selectedGroupModalMode)
|
||||||
}
|
: null;
|
||||||
|
|
||||||
$groups = $this->selectedGroupModalMode === 'closed'
|
|
||||||
? $this->recentiChiusiRaggruppati
|
|
||||||
: $this->recentiRaggruppati;
|
|
||||||
|
|
||||||
return $groups->firstWhere('modal_key', $this->selectedGroupModalKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSelectedGroupModalDaysProperty()
|
public function getSelectedGroupModalDaysProperty()
|
||||||
|
|
@ -315,7 +389,7 @@ public function getSelectedGroupModalDaysProperty()
|
||||||
->values();
|
->values();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'day' => $day,
|
'day' => $day,
|
||||||
'items' => $sorted,
|
'items' => $sorted,
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
|
|
@ -431,10 +505,10 @@ public function creaPostItDaMessaggio(int $messageId): void
|
||||||
$nominativo = $this->getTecnicoNominativo($message);
|
$nominativo = $this->getTecnicoNominativo($message);
|
||||||
$lineLabel = $this->getTecnicoLineaLabel($message);
|
$lineLabel = $this->getTecnicoLineaLabel($message);
|
||||||
$direction = match (strtolower(trim((string) $message->direction))) {
|
$direction = match (strtolower(trim((string) $message->direction))) {
|
||||||
'inbound' => 'Chiamata ricevuta',
|
'inbound' => 'Chiamata ricevuta',
|
||||||
'outbound' => 'Chiamata effettuata',
|
'outbound' => 'Chiamata effettuata',
|
||||||
'internal' => 'Chiamata interna',
|
'internal' => 'Chiamata interna',
|
||||||
default => 'Evento telefonico',
|
default => 'Evento telefonico',
|
||||||
};
|
};
|
||||||
|
|
||||||
$oggetto = $direction;
|
$oggetto = $direction;
|
||||||
|
|
@ -633,6 +707,10 @@ private function isInternalPostIt(ChiamataPostIt $postIt): bool
|
||||||
}
|
}
|
||||||
|
|
||||||
$phone = preg_replace('/\D+/', '', (string) ($postIt->telefono ?? ''));
|
$phone = preg_replace('/\D+/', '', (string) ($postIt->telefono ?? ''));
|
||||||
|
if (is_string($phone) && $this->isShortInternalNumber($phone)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (is_string($phone) && $phone !== '' && $this->isKnownPbxExtension($phone)) {
|
if (is_string($phone) && $phone !== '' && $this->isKnownPbxExtension($phone)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -640,6 +718,20 @@ private function isInternalPostIt(ChiamataPostIt $postIt): bool
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function isLegacyFilteredSmdrPostIt(ChiamataPostIt $postIt): bool
|
||||||
|
{
|
||||||
|
if ((string) ($postIt->origine ?? '') !== 'smdr') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw = strtoupper(trim((string) ($postIt->nota ?? '')));
|
||||||
|
if ($raw === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ! str_contains($raw, '<I>');
|
||||||
|
}
|
||||||
|
|
||||||
private function resolveMessageFromPostIt(ChiamataPostIt $postIt): ?CommunicationMessage
|
private function resolveMessageFromPostIt(ChiamataPostIt $postIt): ?CommunicationMessage
|
||||||
{
|
{
|
||||||
$originId = (int) ($postIt->origine_id ?? 0);
|
$originId = (int) ($postIt->origine_id ?? 0);
|
||||||
|
|
@ -651,7 +743,7 @@ private function resolveMessageFromPostIt(ChiamataPostIt $postIt): ?Communicatio
|
||||||
return $this->postItMessageCache[$originId];
|
return $this->postItMessageCache[$originId];
|
||||||
}
|
}
|
||||||
|
|
||||||
$message = CommunicationMessage::query()->find($originId);
|
$message = CommunicationMessage::query()->find($originId);
|
||||||
$this->postItMessageCache[$originId] = $message;
|
$this->postItMessageCache[$originId] = $message;
|
||||||
|
|
||||||
return $message;
|
return $message;
|
||||||
|
|
@ -659,7 +751,9 @@ private function resolveMessageFromPostIt(ChiamataPostIt $postIt): ?Communicatio
|
||||||
|
|
||||||
private function normalizeGroupedPhone(string $phone): string
|
private function normalizeGroupedPhone(string $phone): string
|
||||||
{
|
{
|
||||||
return preg_replace('/\D+/', '', $phone) ?: '';
|
$digits = preg_replace('/\D+/', '', $phone) ?: '';
|
||||||
|
|
||||||
|
return $this->isShortInternalNumber($digits) ? '' : $digits;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolveGroupedRubricaId(ChiamataPostIt $postIt): ?int
|
private function resolveGroupedRubricaId(ChiamataPostIt $postIt): ?int
|
||||||
|
|
@ -739,15 +833,15 @@ private function buildGroupedCollection($items)
|
||||||
}
|
}
|
||||||
|
|
||||||
$groups[$groupKey] = [
|
$groups[$groupKey] = [
|
||||||
'key' => $groupKey,
|
'key' => $groupKey,
|
||||||
'modal_key' => md5($groupKey),
|
'modal_key' => md5($groupKey),
|
||||||
'items' => [$postIt],
|
'items' => [$postIt],
|
||||||
'count' => 1,
|
'count' => 1,
|
||||||
'latest' => $postIt,
|
'latest' => $postIt,
|
||||||
'caller_label' => $postIt->nome_chiamante ?: 'Chiamante non identificato',
|
'caller_label' => $postIt->nome_chiamante ?: 'Chiamante non identificato',
|
||||||
'phone' => (string) ($postIt->telefono ?? ''),
|
'phone' => (string) ($postIt->telefono ?? ''),
|
||||||
'first_at' => optional($postIt->chiamata_il)->format('d/m/Y H:i') ?: '-',
|
'first_at' => optional($postIt->chiamata_il)->format('d/m/Y H:i') ?: '-',
|
||||||
'last_at' => optional($postIt->chiamata_il)->format('d/m/Y H:i') ?: '-',
|
'last_at' => optional($postIt->chiamata_il)->format('d/m/Y H:i') ?: '-',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -758,15 +852,21 @@ private function buildGroupedCollection($items)
|
||||||
});
|
});
|
||||||
|
|
||||||
$group['latest'] = $this->resolveLatestGroupedPostIt($group['items']);
|
$group['latest'] = $this->resolveLatestGroupedPostIt($group['items']);
|
||||||
$oldest = collect($group['items'])
|
$group['primary_post_it'] = $this->resolvePrimaryGroupedPostIt($group['items']);
|
||||||
|
$oldest = collect($group['items'])
|
||||||
->sortBy(fn(ChiamataPostIt $item) => $item->chiamata_il?->getTimestamp() ?? 0)
|
->sortBy(fn(ChiamataPostIt $item) => $item->chiamata_il?->getTimestamp() ?? 0)
|
||||||
->first();
|
->first();
|
||||||
$group['first_at'] = optional($oldest?->chiamata_il)->format('d/m/Y H:i') ?: '-';
|
$group['first_at'] = optional($oldest?->chiamata_il)->format('d/m/Y H:i') ?: '-';
|
||||||
$group['last_at'] = optional($group['latest']->chiamata_il)->format('d/m/Y H:i') ?: '-';
|
$group['last_at'] = optional($group['latest']->chiamata_il)->format('d/m/Y H:i') ?: '-';
|
||||||
$group['caller_label'] = $this->resolveGroupedCallerLabel($group['items']);
|
$group['caller_label'] = $this->resolveGroupedCallerLabel($group['items']);
|
||||||
$group['phone'] = $this->resolveGroupedPhoneLabel($group['items']);
|
$group['phone'] = $this->resolveGroupedPhoneLabel($group['items']);
|
||||||
|
$group['rubrica_url'] = $this->getRubricaUrlByPhone((string) $group['phone']);
|
||||||
|
$group['answered_count']= collect($group['items'])->filter(fn(ChiamataPostIt $item): bool => $this->isAnsweredPostIt($item))->count();
|
||||||
|
$group['missed_count'] = collect($group['items'])->filter(fn(ChiamataPostIt $item): bool => $this->isMissedPostIt($item))->count();
|
||||||
|
$group['can_create_rubrica'] = ! $group['rubrica_url'] && $this->canCreateRubricaFromPhone((string) $group['phone']);
|
||||||
|
$group['primary_action_label'] = $group['answered_count'] > 0 ? 'Apri Post-it' : 'Apri richiamo';
|
||||||
$group['visible_items'] = collect($group['items'])->take(4)->values()->all();
|
$group['visible_items'] = collect($group['items'])->take(4)->values()->all();
|
||||||
$group['hidden_count'] = max(0, count($group['items']) - count($group['visible_items']));
|
$group['hidden_count'] = max(0, count($group['items']) - count($group['visible_items']));
|
||||||
|
|
||||||
return $group;
|
return $group;
|
||||||
})
|
})
|
||||||
|
|
@ -911,6 +1011,10 @@ private function resolveRubricaIdByPhone(?string $phone): ?int
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->isShortInternalNumber($digits)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->isKnownPbxExtension($digits)) {
|
if ($this->isKnownPbxExtension($digits)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -967,6 +1071,134 @@ private function isKnownPbxExtension(string $digits): bool
|
||||||
return User::query()->where('pbx_extension', $digits)->exists();
|
return User::query()->where('pbx_extension', $digits)->exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function isShortInternalNumber(string $digits): bool
|
||||||
|
{
|
||||||
|
return $digits !== '' && strlen($digits) <= 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function canCreateRubricaFromPhone(?string $phone): bool
|
||||||
|
{
|
||||||
|
$digits = preg_replace('/\D+/', '', (string) $phone) ?: '';
|
||||||
|
|
||||||
|
return $digits !== '' && ! $this->isShortInternalNumber($digits);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isLikelyMobilePhone(string $digits): bool
|
||||||
|
{
|
||||||
|
return str_starts_with($digits, '3') && strlen($digits) >= 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizePostItOutcome(?string $outcome): string
|
||||||
|
{
|
||||||
|
return Str::lower(trim((string) $outcome));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isAnsweredPostIt(ChiamataPostIt $postIt): bool
|
||||||
|
{
|
||||||
|
$outcome = $this->normalizePostItOutcome($postIt->esito);
|
||||||
|
|
||||||
|
if (($postIt->durata_secondi ?? 0) > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return in_array($outcome, ['answered', 'risposta', 'gestita', 'connected', 'ok'], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isMissedPostIt(ChiamataPostIt $postIt): bool
|
||||||
|
{
|
||||||
|
$outcome = $this->normalizePostItOutcome($postIt->esito);
|
||||||
|
|
||||||
|
if (in_array($outcome, ['persa', 'missed', 'no_answer', 'unanswered', 'non_risposta'], true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $postIt->direzione === 'in_arrivo' && ! $this->isAnsweredPostIt($postIt) && (int) ($postIt->durata_secondi ?? 0) === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPostItCallStateMeta(ChiamataPostIt $postIt): array
|
||||||
|
{
|
||||||
|
if ($this->isMissedPostIt($postIt)) {
|
||||||
|
return [
|
||||||
|
'label' => 'Mancata risposta',
|
||||||
|
'class' => 'bg-rose-100 text-rose-800',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->isAnsweredPostIt($postIt)) {
|
||||||
|
return [
|
||||||
|
'label' => (string) $postIt->direzione === 'in_uscita' ? 'Richiamo effettuato' : 'Risposta registrata',
|
||||||
|
'class' => 'bg-emerald-100 text-emerald-800',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'label' => (string) $postIt->direzione === 'in_uscita' ? 'Richiamo da fare' : 'Da verificare',
|
||||||
|
'class' => 'bg-amber-100 text-amber-900',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<int,ChiamataPostIt> $items */
|
||||||
|
private function resolvePrimaryGroupedPostIt(array $items): ChiamataPostIt
|
||||||
|
{
|
||||||
|
$answered = collect($items)
|
||||||
|
->filter(fn(ChiamataPostIt $item): bool => $this->isAnsweredPostIt($item))
|
||||||
|
->sortByDesc(fn(ChiamataPostIt $item) => $item->chiamata_il?->getTimestamp() ?? 0)
|
||||||
|
->values();
|
||||||
|
|
||||||
|
if ($answered->isNotEmpty()) {
|
||||||
|
return $answered->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->resolveLatestGroupedPostIt($items);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findGroupedCollectionByModalKey(string $modalKey, string $mode = 'open'): ?array
|
||||||
|
{
|
||||||
|
$groups = $mode === 'closed'
|
||||||
|
? $this->recentiChiusiRaggruppati
|
||||||
|
: $this->recentiRaggruppati;
|
||||||
|
|
||||||
|
$group = $groups->firstWhere('modal_key', $modalKey);
|
||||||
|
|
||||||
|
return is_array($group) ? $group : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildRubricaDraftIdentity(string $label, string $phoneDigits): array
|
||||||
|
{
|
||||||
|
$cleanLabel = trim(preg_replace('/\s+/', ' ', $label) ?? '');
|
||||||
|
$cleanLabel = $cleanLabel !== '' && $cleanLabel !== 'Chiamante non identificato'
|
||||||
|
? $cleanLabel
|
||||||
|
: ('Nuovo contatto ' . $phoneDigits);
|
||||||
|
|
||||||
|
if (str_contains($cleanLabel, '/') || preg_match('/\b(srl|sas|spa|snc|studio|condominio|associazione|impresa)\b/i', $cleanLabel) === 1) {
|
||||||
|
return [
|
||||||
|
'tipo_contatto' => 'persona_giuridica',
|
||||||
|
'nome' => null,
|
||||||
|
'cognome' => null,
|
||||||
|
'ragione_sociale' => $cleanLabel,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = preg_split('/\s+/', $cleanLabel) ?: [];
|
||||||
|
if (count($parts) >= 2) {
|
||||||
|
$nome = array_shift($parts);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'tipo_contatto' => 'persona_fisica',
|
||||||
|
'nome' => $nome,
|
||||||
|
'cognome' => implode(' ', $parts),
|
||||||
|
'ragione_sociale' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'tipo_contatto' => 'persona_fisica',
|
||||||
|
'nome' => $cleanLabel,
|
||||||
|
'cognome' => null,
|
||||||
|
'ragione_sociale' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
private function isManagedStudioNumberMessage(CommunicationMessage $message): bool
|
private function isManagedStudioNumberMessage(CommunicationMessage $message): bool
|
||||||
{
|
{
|
||||||
$phoneDigits = preg_replace('/\D+/', '', (string) ($message->phone_number ?: data_get($message->metadata, 'smdr.dial_number', '')));
|
$phoneDigits = preg_replace('/\D+/', '', (string) ($message->phone_number ?: data_get($message->metadata, 'smdr.dial_number', '')));
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
|
|
||||||
use App\Models\Amministratore;
|
use App\Models\Amministratore;
|
||||||
use App\Models\Stabile;
|
use App\Models\Stabile;
|
||||||
|
use App\Models\SupportBugRegistry;
|
||||||
|
use App\Models\SupportUpdateEntry;
|
||||||
use App\Support\StabileContext;
|
use App\Support\StabileContext;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
|
|
@ -12,6 +14,7 @@
|
||||||
use Illuminate\Support\Facades\File;
|
use Illuminate\Support\Facades\File;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
use Illuminate\Support\Facades\Process;
|
use Illuminate\Support\Facades\Process;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
use UnitEnum;
|
use UnitEnum;
|
||||||
|
|
@ -148,12 +151,62 @@ class Modifiche extends Page
|
||||||
/** @var array<int,array{hash:string,short_hash:string,author:string,date:string,message:string}> */
|
/** @var array<int,array{hash:string,short_hash:string,author:string,date:string,message:string}> */
|
||||||
public array $latestCommits = [];
|
public array $latestCommits = [];
|
||||||
|
|
||||||
|
/** @var array<int,array{hash:string,short_hash:string,author:string,date:string,message:string}> */
|
||||||
|
public array $pendingGitCommits = [];
|
||||||
|
|
||||||
|
public int $pendingGitCommitCount = 0;
|
||||||
|
|
||||||
|
/** @var array<int,array{label:string,path:string,area:string}> */
|
||||||
|
public array $pendingGitChangedAreas = [];
|
||||||
|
|
||||||
/** @var array<int,array{page:string,path:string,commit:string,message:string}> */
|
/** @var array<int,array{page:string,path:string,commit:string,message:string}> */
|
||||||
public array $recentFilamentPages = [];
|
public array $recentFilamentPages = [];
|
||||||
|
|
||||||
/** @var array<int,string> */
|
/** @var array<int,string> */
|
||||||
public array $functionalHighlights = [];
|
public array $functionalHighlights = [];
|
||||||
|
|
||||||
|
/** @var array<int,array{id:int,release_key:string,category:string,audience:string,title:string,summary:string,impact:string,page_label:string,page_class:string,related_path:string,related_commit:string,recorded_at:string,is_highlighted:bool,page_url:?string}> */
|
||||||
|
public array $supportUpdateEntries = [];
|
||||||
|
|
||||||
|
public int $supportUpdateVisibleCount = 0;
|
||||||
|
|
||||||
|
public int $supportImplementationCount = 0;
|
||||||
|
|
||||||
|
public int $supportBugfixCount = 0;
|
||||||
|
|
||||||
|
public int $supportDebugCount = 0;
|
||||||
|
|
||||||
|
public ?int $supportUpdateFormId = null;
|
||||||
|
|
||||||
|
public string $supportUpdateFormCategory = 'implementazione';
|
||||||
|
|
||||||
|
public string $supportUpdateFormAudience = 'utenti';
|
||||||
|
|
||||||
|
public string $supportUpdateFormTitle = '';
|
||||||
|
|
||||||
|
public string $supportUpdateFormSummary = '';
|
||||||
|
|
||||||
|
public string $supportUpdateFormImpact = '';
|
||||||
|
|
||||||
|
public string $supportUpdateFormPageLabel = '';
|
||||||
|
|
||||||
|
public string $supportUpdateFormPageClass = '';
|
||||||
|
|
||||||
|
public string $supportUpdateFormRelatedPath = '';
|
||||||
|
|
||||||
|
public string $supportUpdateFormRelatedCommit = '';
|
||||||
|
|
||||||
|
public string $supportUpdateFormRecordedAt = '';
|
||||||
|
|
||||||
|
public bool $supportUpdateFormVisible = true;
|
||||||
|
|
||||||
|
public bool $supportUpdateFormHighlighted = false;
|
||||||
|
|
||||||
|
public ?string $pendingGitPreviewIssue = null;
|
||||||
|
|
||||||
|
/** @var array<string,mixed>|null */
|
||||||
|
public ?array $lastNotificationSanitization = null;
|
||||||
|
|
||||||
/** @var array<int,array{key:string,title:string,description:string,path:string,exists:bool,html:string}> */
|
/** @var array<int,array{key:string,title:string,description:string,path:string,exists:bool,html:string}> */
|
||||||
public array $manualSections = [];
|
public array $manualSections = [];
|
||||||
|
|
||||||
|
|
@ -173,26 +226,25 @@ public static function canAccess(): bool
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function canRunUpdate(): bool
|
|
||||||
{
|
|
||||||
$user = Auth::user();
|
|
||||||
|
|
||||||
return $user && method_exists($user, 'hasAnyRole')
|
|
||||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function reloadDashboardData(): void
|
public function reloadDashboardData(): void
|
||||||
{
|
{
|
||||||
$this->loadVersionInfo();
|
$this->loadVersionInfo();
|
||||||
$this->loadLatestCommits();
|
$this->loadLatestCommits();
|
||||||
$this->loadCommitNotes();
|
$this->loadCommitNotes();
|
||||||
$this->loadRuntimeErrors();
|
|
||||||
$this->loadRecentFilamentPages();
|
$this->loadRecentFilamentPages();
|
||||||
$this->loadFunctionalHighlights();
|
$this->loadFunctionalHighlights();
|
||||||
$this->loadManualSections();
|
$this->loadManualSections();
|
||||||
$this->loadDevelopmentSnapshot();
|
$this->loadDevelopmentSnapshot();
|
||||||
$this->loadGitWorkspaceStatus();
|
$this->loadGitWorkspaceStatus();
|
||||||
|
$this->loadPendingGitPreview();
|
||||||
|
$this->loadSupportUpdateEntries();
|
||||||
|
$this->loadRuntimeErrors();
|
||||||
$this->loadLatestBackupSummary();
|
$this->loadLatestBackupSummary();
|
||||||
|
$this->loadLastNotificationSanitization();
|
||||||
|
|
||||||
|
if ($this->supportUpdateFormRecordedAt === '') {
|
||||||
|
$this->resetSupportUpdateForm();
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->selectedCommitHash === null && isset($this->latestCommits[0]['hash'])) {
|
if ($this->selectedCommitHash === null && isset($this->latestCommits[0]['hash'])) {
|
||||||
$this->selectedCommitHash = (string) $this->latestCommits[0]['hash'];
|
$this->selectedCommitHash = (string) $this->latestCommits[0]['hash'];
|
||||||
|
|
@ -571,6 +623,98 @@ private function loadLatestCommits(): void
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function loadPendingGitPreview(): void
|
||||||
|
{
|
||||||
|
$this->pendingGitCommits = [];
|
||||||
|
$this->pendingGitCommitCount = 0;
|
||||||
|
$this->pendingGitChangedAreas = [];
|
||||||
|
$this->pendingGitPreviewIssue = null;
|
||||||
|
|
||||||
|
$remote = trim($this->gitRemote);
|
||||||
|
$branch = trim($this->gitBranch);
|
||||||
|
|
||||||
|
if ($remote === '' || $branch === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fetch = Process::path(base_path())
|
||||||
|
->timeout(120)
|
||||||
|
->run(['git', 'fetch', $remote, $branch, '--quiet']);
|
||||||
|
|
||||||
|
if (! $fetch->successful()) {
|
||||||
|
$this->pendingGitPreviewIssue = 'Impossibile aggiornare l anteprima remota da Gitea. Il conteggio mostrato potrebbe essere non aggiornato.';
|
||||||
|
}
|
||||||
|
|
||||||
|
$range = 'HEAD..refs/remotes/' . $remote . '/' . $branch;
|
||||||
|
$count = $this->runGitProcess(['rev-list', '--count', $range]);
|
||||||
|
|
||||||
|
if (! is_string($count) || ! ctype_digit(trim($count))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->pendingGitCommitCount = (int) trim($count);
|
||||||
|
if ($this->pendingGitCommitCount <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$format = '%H|%h|%an|%ad|%s';
|
||||||
|
$command = 'git log -n 12 --date=format:"%d/%m/%Y %H:%M" --pretty=format:"' . $format . '" ' . escapeshellarg($range);
|
||||||
|
$result = Process::path(base_path())->run(['bash', '-lc', $command]);
|
||||||
|
|
||||||
|
if ($result->successful()) {
|
||||||
|
$lines = preg_split('/\r\n|\r|\n/', trim($result->output())) ?: [];
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$parts = explode('|', $line, 5);
|
||||||
|
if (count($parts) !== 5) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->pendingGitCommits[] = [
|
||||||
|
'hash' => (string) $parts[0],
|
||||||
|
'short_hash' => (string) $parts[1],
|
||||||
|
'author' => (string) $parts[2],
|
||||||
|
'date' => (string) $parts[3],
|
||||||
|
'message' => (string) $parts[4],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$changed = $this->runGitProcess([
|
||||||
|
'diff',
|
||||||
|
'--name-only',
|
||||||
|
$range,
|
||||||
|
'--',
|
||||||
|
'app/Filament/Pages',
|
||||||
|
'resources/views/filament/pages',
|
||||||
|
'app/Console/Commands',
|
||||||
|
'docs/support',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (! is_string($changed) || trim($changed) === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$seen = [];
|
||||||
|
$lines = preg_split('/\r\n|\r|\n/', trim($changed)) ?: [];
|
||||||
|
foreach ($lines as $path) {
|
||||||
|
$path = trim((string) $path);
|
||||||
|
if ($path === '' || isset($seen[$path])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$seen[$path] = true;
|
||||||
|
$this->pendingGitChangedAreas[] = [
|
||||||
|
'label' => $this->humanizePagePath($path),
|
||||||
|
'path' => $path,
|
||||||
|
'area' => $this->resolveChangedAreaLabel($path),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (count($this->pendingGitChangedAreas) >= 16) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function startUpdateJob(bool $fallback): void
|
private function startUpdateJob(bool $fallback): void
|
||||||
{
|
{
|
||||||
$this->registerPlannedUpdateSummary();
|
$this->registerPlannedUpdateSummary();
|
||||||
|
|
@ -924,6 +1068,77 @@ private function loadGitWorkspaceStatus(): void
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function loadSupportUpdateEntries(): void
|
||||||
|
{
|
||||||
|
$this->supportUpdateEntries = [];
|
||||||
|
$this->supportUpdateVisibleCount = 0;
|
||||||
|
$this->supportImplementationCount = 0;
|
||||||
|
$this->supportBugfixCount = 0;
|
||||||
|
$this->supportDebugCount = 0;
|
||||||
|
|
||||||
|
if (! $this->isSupportUpdateRegistryReady()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->syncStructuredUpdateRegistry();
|
||||||
|
|
||||||
|
$visibleEntries = SupportUpdateEntry::query()
|
||||||
|
->where('is_visible', true)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$this->supportUpdateVisibleCount = $visibleEntries->count();
|
||||||
|
$this->supportImplementationCount = $visibleEntries->where('category', 'implementazione')->count();
|
||||||
|
$this->supportBugfixCount = $visibleEntries->where('category', 'bugfix')->count();
|
||||||
|
$this->supportDebugCount = $visibleEntries->where('category', 'debug')->count();
|
||||||
|
|
||||||
|
$entries = SupportUpdateEntry::query()
|
||||||
|
->where('is_visible', true)
|
||||||
|
->orderByDesc('is_highlighted')
|
||||||
|
->orderByDesc('recorded_at')
|
||||||
|
->orderByDesc('id')
|
||||||
|
->limit(24)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($entries as $entry) {
|
||||||
|
$category = (string) $entry->category;
|
||||||
|
|
||||||
|
$this->supportUpdateEntries[] = [
|
||||||
|
'id' => (int) $entry->id,
|
||||||
|
'release_key' => (string) $entry->release_key,
|
||||||
|
'category' => $category,
|
||||||
|
'audience' => (string) $entry->audience,
|
||||||
|
'title' => (string) $entry->title,
|
||||||
|
'summary' => (string) $entry->summary,
|
||||||
|
'impact' => (string) ($entry->impact ?? ''),
|
||||||
|
'page_label' => (string) ($entry->page_label ?? ''),
|
||||||
|
'page_class' => (string) ($entry->page_class ?? ''),
|
||||||
|
'related_path' => (string) ($entry->related_path ?? ''),
|
||||||
|
'related_commit' => (string) ($entry->related_commit ?? ''),
|
||||||
|
'recorded_at' => (string) ($entry->recorded_at ?? ''),
|
||||||
|
'is_visible' => (bool) $entry->is_visible,
|
||||||
|
'is_highlighted' => (bool) $entry->is_highlighted,
|
||||||
|
'page_url' => $this->resolveSupportUpdatePageUrl((string) ($entry->page_class ?? '')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loadLastNotificationSanitization(): void
|
||||||
|
{
|
||||||
|
$this->lastNotificationSanitization = null;
|
||||||
|
|
||||||
|
$path = storage_path('app/support/filament-notification-sanitize.json');
|
||||||
|
if (! is_file($path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode((string) @file_get_contents($path), true);
|
||||||
|
if (! is_array($decoded)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->lastNotificationSanitization = $decoded;
|
||||||
|
}
|
||||||
|
|
||||||
private function loadDevelopmentSnapshot(): void
|
private function loadDevelopmentSnapshot(): void
|
||||||
{
|
{
|
||||||
$generatedPath = storage_path('app/support/generated/CONTINUITA-SVILUPPO-SNAPSHOT.md');
|
$generatedPath = storage_path('app/support/generated/CONTINUITA-SVILUPPO-SNAPSHOT.md');
|
||||||
|
|
@ -987,6 +1202,172 @@ private function humanizePagePath(string $path): string
|
||||||
return trim($name);
|
return trim($name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function resolveChangedAreaLabel(string $path): string
|
||||||
|
{
|
||||||
|
if (str_starts_with($path, 'app/Filament/Pages/')) {
|
||||||
|
return 'pagina Filament';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($path, 'resources/views/filament/pages/')) {
|
||||||
|
return 'view pagina';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($path, 'app/Console/Commands/')) {
|
||||||
|
return 'comando applicativo';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($path, 'docs/support/')) {
|
||||||
|
return 'documentazione supporto';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'area applicativa';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isSupportUpdateRegistryReady(): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return Schema::hasTable('support_update_entries');
|
||||||
|
} catch (Throwable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isSupportBugRegistryReady(): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return Schema::hasTable('support_bug_registries');
|
||||||
|
} catch (Throwable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function syncStructuredUpdateRegistry(): void
|
||||||
|
{
|
||||||
|
foreach ($this->getStructuredUpdateDefinitions() as $row) {
|
||||||
|
$exists = SupportUpdateEntry::query()
|
||||||
|
->where('release_key', (string) $row['release_key'])
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if ($exists) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$row['updated_at'] = now();
|
||||||
|
$row['created_at'] = now();
|
||||||
|
SupportUpdateEntry::query()->create($row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<int,array<string,mixed>> */
|
||||||
|
private function getStructuredUpdateDefinitions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'release_key' => 'post-it-assegnazione-operativa',
|
||||||
|
'category' => 'implementazione',
|
||||||
|
'audience' => 'utenti',
|
||||||
|
'title' => 'Assegnazione Post-it a operatore, collaboratore o fornitore',
|
||||||
|
'summary' => 'La scheda Post-it registra ora l assegnazione operativa direttamente in UI e propaga il riferimento anche al ticket collegato.',
|
||||||
|
'impact' => 'Riduce passaggi manuali e mantiene il responsabile collegato al promemoria anche dopo la conversione in ticket.',
|
||||||
|
'page_label' => 'Strumenti > Post-it',
|
||||||
|
'page_class' => \App\Filament\Pages\Strumenti\PostIt::class,
|
||||||
|
'related_path' => 'app/Filament/Pages/Strumenti/PostIt.php',
|
||||||
|
'related_commit' => '236aef5',
|
||||||
|
'is_visible' => true,
|
||||||
|
'is_highlighted' => true,
|
||||||
|
'recorded_at' => '2026-04-04 09:30',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'release_key' => 'lavagna-chiamate-filtra-interni',
|
||||||
|
'category' => 'bugfix',
|
||||||
|
'audience' => 'utenti',
|
||||||
|
'title' => 'Lavagna chiamate e storico Post-it ripuliti dai numeri interni',
|
||||||
|
'summary' => 'Le chiamate interne restano nel flusso tecnico SMDR ma non inquinano piu la vista operativa della lavagna e dello storico.',
|
||||||
|
'impact' => 'Gli operatori vedono solo chiamate lavorabili e la vista tecnica conserva tutto l import PBX completo.',
|
||||||
|
'page_label' => 'Strumenti > Post-it Gestione',
|
||||||
|
'page_class' => \App\Filament\Pages\Strumenti\PostItGestione::class,
|
||||||
|
'related_path' => 'app/Filament/Pages/Strumenti/PostItGestione.php',
|
||||||
|
'related_commit' => 'ab7b804',
|
||||||
|
'is_visible' => true,
|
||||||
|
'is_highlighted' => true,
|
||||||
|
'recorded_at' => '2026-04-04 10:00',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'release_key' => 'lavagna-archivio-modal-raggruppata',
|
||||||
|
'category' => 'implementazione',
|
||||||
|
'audience' => 'utenti',
|
||||||
|
'title' => 'Lavagna gialla con archivio chiusi e dettaglio storico per giorno',
|
||||||
|
'summary' => 'Le chiamate raggruppate sono state spostate in una tab dedicata con box uniformi, archivio dei chiusi e modal storico per giornate.',
|
||||||
|
'impact' => 'La lettura diventa piu vicina alla bacheca reale e lo storico resta consultabile senza perdere il collegamento con rubrica e nominativo.',
|
||||||
|
'page_label' => 'Strumenti > Post-it Gestione',
|
||||||
|
'page_class' => \App\Filament\Pages\Strumenti\PostItGestione::class,
|
||||||
|
'related_path' => 'resources/views/filament/pages/strumenti/post-it-gestione.blade.php',
|
||||||
|
'related_commit' => 'b9b9e99',
|
||||||
|
'is_visible' => true,
|
||||||
|
'is_highlighted' => true,
|
||||||
|
'recorded_at' => '2026-04-04 10:20',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'release_key' => 'pbx-mappature-osservate-collaboratori',
|
||||||
|
'category' => 'implementazione',
|
||||||
|
'audience' => 'supporto',
|
||||||
|
'title' => 'Scheda amministratore con mapping PBX osservato per collaboratori',
|
||||||
|
'summary' => 'La configurazione PBX/TAPI accumula token osservati e permette mappature multiple per interni, gruppi, linee e flag click to call/popup.',
|
||||||
|
'impact' => 'Riduce configurazioni cieche e rende il PBX piu neutro rispetto al fornitore, mantenendo traccia operativa dei valori visti.',
|
||||||
|
'page_label' => 'Impostazioni > Scheda Amministratore',
|
||||||
|
'page_class' => \App\Filament\Pages\Impostazioni\SchedaAmministratore::class,
|
||||||
|
'related_path' => 'app/Filament/Pages/Impostazioni/SchedaAmministratore.php',
|
||||||
|
'related_commit' => '236aef5',
|
||||||
|
'is_visible' => true,
|
||||||
|
'is_highlighted' => false,
|
||||||
|
'recorded_at' => '2026-04-04 10:35',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'release_key' => 'sync-git-rubrica-qa-sicura',
|
||||||
|
'category' => 'debug',
|
||||||
|
'audience' => 'supporto',
|
||||||
|
'title' => 'Sync Git da Gitea con ricucitura sicura dei legami rubrica',
|
||||||
|
'summary' => 'La sync UI prova anche gescon:qa-rubrica-legami --apply-safe, cosi il riallineamento staging resta dentro il flusso applicativo.',
|
||||||
|
'impact' => 'Riduce dipendenza dal terminale e abbassa il rischio di dimenticare la ricostruzione minima della rubrica dopo il pull.',
|
||||||
|
'page_label' => 'Supporto > Modifiche',
|
||||||
|
'page_class' => self::class,
|
||||||
|
'related_path' => 'app/Console/Commands/NetgesconGitSyncCommand.php',
|
||||||
|
'related_commit' => '236aef5',
|
||||||
|
'is_visible' => true,
|
||||||
|
'is_highlighted' => false,
|
||||||
|
'recorded_at' => '2026-04-04 10:50',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveSupportUpdatePageUrl(string $pageClass): ?string
|
||||||
|
{
|
||||||
|
if ($pageClass === '' || ! class_exists($pageClass) || ! method_exists($pageClass, 'getUrl')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return $pageClass::getUrl();
|
||||||
|
} catch (Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildSupportUpdateReleaseKey(string $title): string
|
||||||
|
{
|
||||||
|
$base = Str::slug(Str::limit($title, 80, ''), '-');
|
||||||
|
$base = $base !== '' ? $base : 'support-update';
|
||||||
|
$key = $base;
|
||||||
|
$i = 1;
|
||||||
|
|
||||||
|
while (SupportUpdateEntry::query()->where('release_key', $key)->exists()) {
|
||||||
|
$i++;
|
||||||
|
$key = $base . '-' . $i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $key;
|
||||||
|
}
|
||||||
|
|
||||||
public function setActiveTab(string $tab): void
|
public function setActiveTab(string $tab): void
|
||||||
{
|
{
|
||||||
if (! in_array($tab, ['manuale', 'errori', 'commit', 'migliorie'], true)) {
|
if (! in_array($tab, ['manuale', 'errori', 'commit', 'migliorie'], true)) {
|
||||||
|
|
@ -996,6 +1377,130 @@ public function setActiveTab(string $tab): void
|
||||||
$this->activeTab = $tab;
|
$this->activeTab = $tab;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function resetSupportUpdateForm(): void
|
||||||
|
{
|
||||||
|
$this->supportUpdateFormId = null;
|
||||||
|
$this->supportUpdateFormCategory = 'implementazione';
|
||||||
|
$this->supportUpdateFormAudience = 'utenti';
|
||||||
|
$this->supportUpdateFormTitle = '';
|
||||||
|
$this->supportUpdateFormSummary = '';
|
||||||
|
$this->supportUpdateFormImpact = '';
|
||||||
|
$this->supportUpdateFormPageLabel = '';
|
||||||
|
$this->supportUpdateFormPageClass = '';
|
||||||
|
$this->supportUpdateFormRelatedPath = '';
|
||||||
|
$this->supportUpdateFormRelatedCommit = '';
|
||||||
|
$this->supportUpdateFormRecordedAt = now()->format('Y-m-d H:i');
|
||||||
|
$this->supportUpdateFormVisible = true;
|
||||||
|
$this->supportUpdateFormHighlighted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function editSupportUpdateEntry(int $entryId): void
|
||||||
|
{
|
||||||
|
if (! $this->isSupportUpdateRegistryReady()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$entry = SupportUpdateEntry::query()->find($entryId);
|
||||||
|
if (! $entry instanceof SupportUpdateEntry) {
|
||||||
|
Notification::make()->title('Voce registro non trovata')->warning()->send();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->supportUpdateFormId = (int) $entry->id;
|
||||||
|
$this->supportUpdateFormCategory = (string) $entry->category;
|
||||||
|
$this->supportUpdateFormAudience = (string) $entry->audience;
|
||||||
|
$this->supportUpdateFormTitle = (string) $entry->title;
|
||||||
|
$this->supportUpdateFormSummary = (string) $entry->summary;
|
||||||
|
$this->supportUpdateFormImpact = (string) ($entry->impact ?? '');
|
||||||
|
$this->supportUpdateFormPageLabel = (string) ($entry->page_label ?? '');
|
||||||
|
$this->supportUpdateFormPageClass = (string) ($entry->page_class ?? '');
|
||||||
|
$this->supportUpdateFormRelatedPath = (string) ($entry->related_path ?? '');
|
||||||
|
$this->supportUpdateFormRelatedCommit = (string) ($entry->related_commit ?? '');
|
||||||
|
$this->supportUpdateFormRecordedAt = (string) ($entry->recorded_at ?: now()->format('Y-m-d H:i'));
|
||||||
|
$this->supportUpdateFormVisible = (bool) $entry->is_visible;
|
||||||
|
$this->supportUpdateFormHighlighted = (bool) $entry->is_highlighted;
|
||||||
|
|
||||||
|
Notification::make()->title('Voce caricata nel form')->success()->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveSupportUpdateEntry(): void
|
||||||
|
{
|
||||||
|
if (! $this->isSupportUpdateRegistryReady()) {
|
||||||
|
Notification::make()->title('Registro DB non pronto')->warning()->send();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$title = trim($this->supportUpdateFormTitle);
|
||||||
|
$summary = trim($this->supportUpdateFormSummary);
|
||||||
|
$recordedAt = mb_substr(trim($this->supportUpdateFormRecordedAt), 0, 40);
|
||||||
|
$relatedCommit = mb_substr(trim($this->supportUpdateFormRelatedCommit), 0, 40);
|
||||||
|
|
||||||
|
if ($title === '' || $summary === '') {
|
||||||
|
Notification::make()->title('Titolo e riepilogo sono obbligatori')->warning()->send();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$category = in_array($this->supportUpdateFormCategory, ['implementazione', 'bugfix', 'debug'], true)
|
||||||
|
? $this->supportUpdateFormCategory
|
||||||
|
: 'implementazione';
|
||||||
|
$audience = in_array($this->supportUpdateFormAudience, ['utenti', 'supporto'], true)
|
||||||
|
? $this->supportUpdateFormAudience
|
||||||
|
: 'utenti';
|
||||||
|
|
||||||
|
$entry = $this->supportUpdateFormId !== null
|
||||||
|
? SupportUpdateEntry::query()->find($this->supportUpdateFormId)
|
||||||
|
: new SupportUpdateEntry();
|
||||||
|
|
||||||
|
if (! $entry instanceof SupportUpdateEntry) {
|
||||||
|
$entry = new SupportUpdateEntry();
|
||||||
|
}
|
||||||
|
|
||||||
|
$releaseKey = $entry->exists
|
||||||
|
? (string) $entry->release_key
|
||||||
|
: $this->buildSupportUpdateReleaseKey($title);
|
||||||
|
|
||||||
|
$entry->fill([
|
||||||
|
'release_key' => $releaseKey,
|
||||||
|
'category' => $category,
|
||||||
|
'audience' => $audience,
|
||||||
|
'title' => $title,
|
||||||
|
'summary' => $summary,
|
||||||
|
'impact' => trim($this->supportUpdateFormImpact),
|
||||||
|
'page_label' => trim($this->supportUpdateFormPageLabel),
|
||||||
|
'page_class' => trim($this->supportUpdateFormPageClass),
|
||||||
|
'related_path' => trim($this->supportUpdateFormRelatedPath),
|
||||||
|
'related_commit' => $relatedCommit,
|
||||||
|
'recorded_at' => $recordedAt !== '' ? $recordedAt : now()->format('Y-m-d H:i'),
|
||||||
|
'is_visible' => $this->supportUpdateFormVisible,
|
||||||
|
'is_highlighted' => $this->supportUpdateFormHighlighted,
|
||||||
|
]);
|
||||||
|
$entry->save();
|
||||||
|
|
||||||
|
$this->loadSupportUpdateEntries();
|
||||||
|
$this->resetSupportUpdateForm();
|
||||||
|
|
||||||
|
Notification::make()->title('Voce registro salvata')->success()->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toggleSupportUpdateVisibility(int $entryId): void
|
||||||
|
{
|
||||||
|
if (! $this->isSupportUpdateRegistryReady()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$entry = SupportUpdateEntry::query()->find($entryId);
|
||||||
|
if (! $entry instanceof SupportUpdateEntry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$entry->is_visible = ! (bool) $entry->is_visible;
|
||||||
|
$entry->save();
|
||||||
|
$this->loadSupportUpdateEntries();
|
||||||
|
}
|
||||||
|
|
||||||
private function startGitSyncJob(): void
|
private function startGitSyncJob(): void
|
||||||
{
|
{
|
||||||
$jobId = now()->format('YmdHis') . '-' . Str::lower(Str::random(6));
|
$jobId = now()->format('YmdHis') . '-' . Str::lower(Str::random(6));
|
||||||
|
|
@ -1525,6 +2030,40 @@ private function loadBugRegistry(): void
|
||||||
$this->bugRegistry = [];
|
$this->bugRegistry = [];
|
||||||
$this->nextBugId = 1;
|
$this->nextBugId = 1;
|
||||||
|
|
||||||
|
if ($this->isSupportBugRegistryReady()) {
|
||||||
|
$rows = SupportBugRegistry::query()
|
||||||
|
->orderBy('bug_id')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$fingerprint = (string) $row->fingerprint;
|
||||||
|
if ($fingerprint === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = (int) $row->bug_id;
|
||||||
|
if ($id <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->bugRegistry[$fingerprint] = [
|
||||||
|
'id' => $id,
|
||||||
|
'status' => (string) ($row->status ?: 'open'),
|
||||||
|
'first_seen' => (string) ($row->first_seen ?: '-'),
|
||||||
|
'last_seen' => (string) ($row->last_seen ?: '-'),
|
||||||
|
'resolved_at' => $row->resolved_at !== null ? (string) $row->resolved_at : null,
|
||||||
|
'resolution_note' => (string) ($row->resolution_note ?? ''),
|
||||||
|
'title' => (string) ($row->title ?? ''),
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->nextBugId = max($this->nextBugId, $id + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->bugRegistry !== []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$path = storage_path('app/support/bug-tracker.json');
|
$path = storage_path('app/support/bug-tracker.json');
|
||||||
if (! is_file($path)) {
|
if (! is_file($path)) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -1569,6 +2108,36 @@ private function loadBugRegistry(): void
|
||||||
|
|
||||||
private function persistBugRegistry(): void
|
private function persistBugRegistry(): void
|
||||||
{
|
{
|
||||||
|
if ($this->isSupportBugRegistryReady()) {
|
||||||
|
$rows = [];
|
||||||
|
foreach ($this->bugRegistry as $fingerprint => $meta) {
|
||||||
|
if (! is_string($fingerprint) || ! is_array($meta)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows[] = [
|
||||||
|
'fingerprint' => $fingerprint,
|
||||||
|
'bug_id' => (int) ($meta['id'] ?? 0),
|
||||||
|
'status' => (string) ($meta['status'] ?? 'open'),
|
||||||
|
'title' => (string) ($meta['title'] ?? ''),
|
||||||
|
'first_seen' => (string) ($meta['first_seen'] ?? '-'),
|
||||||
|
'last_seen' => (string) ($meta['last_seen'] ?? '-'),
|
||||||
|
'resolved_at' => isset($meta['resolved_at']) && is_string($meta['resolved_at']) ? $meta['resolved_at'] : null,
|
||||||
|
'resolution_note' => (string) ($meta['resolution_note'] ?? ''),
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($rows !== []) {
|
||||||
|
SupportBugRegistry::query()->upsert(
|
||||||
|
$rows,
|
||||||
|
['fingerprint'],
|
||||||
|
['bug_id', 'status', 'title', 'first_seen', 'last_seen', 'resolved_at', 'resolution_note', 'updated_at']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$path = storage_path('app/support/bug-tracker.json');
|
$path = storage_path('app/support/bug-tracker.json');
|
||||||
$dir = dirname($path);
|
$dir = dirname($path);
|
||||||
if (! is_dir($dir)) {
|
if (! is_dir($dir)) {
|
||||||
|
|
@ -1586,6 +2155,41 @@ private function persistBugRegistry(): void
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return array<string,mixed>|null */
|
||||||
|
public function getHighlightedBugProperty(): ?array
|
||||||
|
{
|
||||||
|
if ($this->selectedBugFingerprint !== null) {
|
||||||
|
foreach ($this->runtimeErrors as $row) {
|
||||||
|
if (($row['fingerprint'] ?? null) === $this->selectedBugFingerprint) {
|
||||||
|
return $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->runtimeErrors as $row) {
|
||||||
|
if (($row['bug_code'] ?? '') === 'BUG-0033') {
|
||||||
|
return $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->runtimeErrors[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHighlightedBugHintProperty(): ?string
|
||||||
|
{
|
||||||
|
$row = $this->highlightedBug;
|
||||||
|
if (! is_array($row)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = Str::lower((string) ($row['message'] ?? ''));
|
||||||
|
if (str_contains($message, 'filament\\notifications\\collection') || str_contains($message, 'argument #1 ($notification) must be of type array, int given')) {
|
||||||
|
return 'Causa probabile: payload notifiche Filament corrotto in sessione. Il bootstrap applicativo ora filtra i valori non-array prima che Livewire li rilegga.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private function loadCommitNotes(): void
|
private function loadCommitNotes(): void
|
||||||
{
|
{
|
||||||
$this->commitNotes = [];
|
$this->commitNotes = [];
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ class TicketGestione extends Page
|
||||||
|
|
||||||
protected string $view = 'filament.pages.supporto.ticket-gestione';
|
protected string $view = 'filament.pages.supporto.ticket-gestione';
|
||||||
|
|
||||||
public string $status = 'open';
|
public string $status = 'all';
|
||||||
|
|
||||||
public string $activeTab = 'elenco';
|
public string $activeTab = 'elenco';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ class TicketMobile extends Page
|
||||||
|
|
||||||
protected string $view = 'filament.pages.supporto.ticket-mobile';
|
protected string $view = 'filament.pages.supporto.ticket-mobile';
|
||||||
|
|
||||||
public string $status = 'open';
|
public string $status = 'all';
|
||||||
|
|
||||||
public string $callerSearch = '';
|
public string $callerSearch = '';
|
||||||
|
|
||||||
|
|
@ -603,7 +603,7 @@ public function creaTicketRapido(): void
|
||||||
'priorita' => $this->newTicketPriorita,
|
'priorita' => $this->newTicketPriorita,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$savedAttachments = $this->salvaAllegatiTicket($ticket, $user->id);
|
$savedAttachments = $this->salvaAllegatiTicket($ticket, $user->id);
|
||||||
|
|
||||||
Notification::make()
|
Notification::make()
|
||||||
->title('Ticket creato')
|
->title('Ticket creato')
|
||||||
|
|
@ -615,14 +615,14 @@ public function creaTicketRapido(): void
|
||||||
->success()
|
->success()
|
||||||
->send();
|
->send();
|
||||||
|
|
||||||
$this->newTicketTitolo = null;
|
$this->newTicketTitolo = null;
|
||||||
$this->newTicketDescrizione = null;
|
$this->newTicketDescrizione = null;
|
||||||
$this->newTicketCallNote = null;
|
$this->newTicketCallNote = null;
|
||||||
$this->newTicketLuogo = null;
|
$this->newTicketLuogo = null;
|
||||||
$this->newTicketPriorita = 'Media';
|
$this->newTicketPriorita = 'Media';
|
||||||
$this->newTicketCategoriaId = null;
|
$this->newTicketCategoriaId = null;
|
||||||
$this->newTicketTipoIntervento = 'altro';
|
$this->newTicketTipoIntervento = 'altro';
|
||||||
$this->newTicketFotoNote = null;
|
$this->newTicketFotoNote = null;
|
||||||
$this->resetDraftUploadState();
|
$this->resetDraftUploadState();
|
||||||
$this->refreshData();
|
$this->refreshData();
|
||||||
}
|
}
|
||||||
|
|
@ -846,6 +846,16 @@ private function appendPendingUploads(string $source): void
|
||||||
$this->syncAttachmentDescriptionSlots();
|
$this->syncAttachmentDescriptionSlots();
|
||||||
|
|
||||||
$discarded = count($pending) - count($accepted);
|
$discarded = count($pending) - count($accepted);
|
||||||
|
if (count($accepted) > 0) {
|
||||||
|
$queueTotal = count($this->newTicketCameraShots) + count($this->newTicketAttachments);
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title('File accodati alla bozza ticket')
|
||||||
|
->body(count($accepted) . ' file aggiunti. Totale attuale in coda: ' . $queueTotal . '.')
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
if ($discarded > 0) {
|
if ($discarded > 0) {
|
||||||
Notification::make()
|
Notification::make()
|
||||||
->title('Coda allegati ridotta')
|
->title('Coda allegati ridotta')
|
||||||
|
|
@ -964,10 +974,10 @@ public function getTicketCallContextProperty(): array
|
||||||
'phone' => $phone,
|
'phone' => $phone,
|
||||||
'target_extension' => ! empty($this->liveIncomingCall['target_extension']) ? (string) $this->liveIncomingCall['target_extension'] : null,
|
'target_extension' => ! empty($this->liveIncomingCall['target_extension']) ? (string) $this->liveIncomingCall['target_extension'] : null,
|
||||||
'received_at' => ! empty($this->liveIncomingCall['received_at']) ? (string) $this->liveIncomingCall['received_at'] : null,
|
'received_at' => ! empty($this->liveIncomingCall['received_at']) ? (string) $this->liveIncomingCall['received_at'] : null,
|
||||||
'caller_name' => $caller ? (string) ($caller->nome_completo ?: $caller->ragione_sociale ?: '') : (! empty($this->liveIncomingCall['rubrica_nome']) ? (string) $this->liveIncomingCall['rubrica_nome'] : null),
|
'caller_name' => $caller ? (string) ($caller->nome_completo ?: $caller->ragione_sociale ?: ''): (! empty($this->liveIncomingCall['rubrica_nome']) ? (string) $this->liveIncomingCall['rubrica_nome'] : null),
|
||||||
'caller_profile' => $caller ? ((string) ($caller->tipo_utenza_call ?: '') ?: null) : null,
|
'caller_profile' => $caller ? ((string) ($caller->tipo_utenza_call ?: '') ?: null): null,
|
||||||
'riferimento_stabile' => $caller ? ((string) ($caller->riferimento_stabile ?: '') ?: null) : null,
|
'riferimento_stabile' => $caller ? ((string) ($caller->riferimento_stabile ?: '') ?: null): null,
|
||||||
'riferimento_unita' => $caller ? ((string) ($caller->riferimento_unita ?: '') ?: null) : null,
|
'riferimento_unita' => $caller ? ((string) ($caller->riferimento_unita ?: '') ?: null): null,
|
||||||
'stabile_label' => $caller ? $this->getCallerStabileLabel((int) $caller->id) : null,
|
'stabile_label' => $caller ? $this->getCallerStabileLabel((int) $caller->id) : null,
|
||||||
'call_note' => $this->newTicketCallNote,
|
'call_note' => $this->newTicketCallNote,
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,11 @@ class ChiamataPostIt extends Model
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'chiamata_il' => 'datetime',
|
'chiamata_il' => 'datetime',
|
||||||
'chiusa_il' => 'datetime',
|
'chiusa_il' => 'datetime',
|
||||||
'durata_secondi' => 'integer',
|
'durata_secondi' => 'integer',
|
||||||
'created_at' => 'datetime',
|
'created_at' => 'datetime',
|
||||||
'updated_at' => 'datetime',
|
'updated_at' => 'datetime',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function rubrica()
|
public function rubrica()
|
||||||
|
|
|
||||||
18
app/Models/SupportBugRegistry.php
Normal file
18
app/Models/SupportBugRegistry.php
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class SupportBugRegistry extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'fingerprint',
|
||||||
|
'bug_id',
|
||||||
|
'status',
|
||||||
|
'title',
|
||||||
|
'first_seen',
|
||||||
|
'last_seen',
|
||||||
|
'resolved_at',
|
||||||
|
'resolution_note',
|
||||||
|
];
|
||||||
|
}
|
||||||
28
app/Models/SupportUpdateEntry.php
Normal file
28
app/Models/SupportUpdateEntry.php
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class SupportUpdateEntry extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'release_key',
|
||||||
|
'category',
|
||||||
|
'audience',
|
||||||
|
'title',
|
||||||
|
'summary',
|
||||||
|
'impact',
|
||||||
|
'page_label',
|
||||||
|
'page_class',
|
||||||
|
'related_path',
|
||||||
|
'related_commit',
|
||||||
|
'is_visible',
|
||||||
|
'is_highlighted',
|
||||||
|
'recorded_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_visible' => 'boolean',
|
||||||
|
'is_highlighted' => 'boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Session\Session;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
use Illuminate\Support\Facades\URL;
|
use Illuminate\Support\Facades\URL;
|
||||||
use Illuminate\Support\Facades\View;
|
use Illuminate\Support\Facades\View;
|
||||||
|
|
@ -45,6 +46,8 @@ public function boot(): void
|
||||||
URL::forceScheme('https');
|
URL::forceScheme('https');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->sanitizeFilamentNotificationSessionPayload();
|
||||||
|
|
||||||
// View Composer per la sidebar
|
// View Composer per la sidebar
|
||||||
View::composer([
|
View::composer([
|
||||||
'components.menu.sidebar',
|
'components.menu.sidebar',
|
||||||
|
|
@ -95,4 +98,48 @@ function userSetting($key, $default = null)
|
||||||
// Silenzia in bootstrap (es. durante installazione senza DB)
|
// Silenzia in bootstrap (es. durante installazione senza DB)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function sanitizeFilamentNotificationSessionPayload(): void
|
||||||
|
{
|
||||||
|
if ($this->app->runningInConsole() || ! $this->app->bound('session.store')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
/** @var Session $session */
|
||||||
|
$session = $this->app->make('session.store');
|
||||||
|
$raw = $session->get('filament.notifications');
|
||||||
|
|
||||||
|
if ($raw === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! is_array($raw)) {
|
||||||
|
$session->forget('filament.notifications');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized = array_values(array_filter($raw, static fn(mixed $notification): bool => is_array($notification)));
|
||||||
|
|
||||||
|
if ($normalized !== $raw) {
|
||||||
|
$session->put('filament.notifications', $normalized);
|
||||||
|
$path = storage_path('app/support/filament-notification-sanitize.json');
|
||||||
|
$dir = dirname($path);
|
||||||
|
|
||||||
|
if (! is_dir($dir)) {
|
||||||
|
@mkdir($dir, 0775, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@file_put_contents($path, json_encode([
|
||||||
|
'timestamp' => now()->toIso8601String(),
|
||||||
|
'removed_items' => max(0, count($raw) - count($normalized)),
|
||||||
|
'remaining_items' => count($normalized),
|
||||||
|
'reason' => 'Filament notifications payload contained non-array items.',
|
||||||
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// Evita che una sessione corrotta blocchi il bootstrap dell'applicazione.
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ public function getForUser(User $user): ?array
|
||||||
$watchedExtensions = app(PbxRoutingService::class)->getWatchedExtensionsForUser($user);
|
$watchedExtensions = app(PbxRoutingService::class)->getWatchedExtensionsForUser($user);
|
||||||
$popupRestricted = (bool) ($user->pbx_popup_enabled ?? false) && count($watchedExtensions) > 0;
|
$popupRestricted = (bool) ($user->pbx_popup_enabled ?? false) && count($watchedExtensions) > 0;
|
||||||
|
|
||||||
$latest = CommunicationMessage::query()
|
$candidates = CommunicationMessage::query()
|
||||||
->whereIn('channel', ['smdr', 'panasonic_csta'])
|
->whereIn('channel', ['smdr', 'panasonic_csta'])
|
||||||
->where('direction', 'inbound')
|
->where('direction', 'inbound')
|
||||||
->whereNotNull('received_at')
|
->whereNotNull('received_at')
|
||||||
|
|
@ -37,8 +37,34 @@ public function getForUser(User $user): ?array
|
||||||
->when($popupRestricted, function ($query) use ($watchedExtensions): void {
|
->when($popupRestricted, function ($query) use ($watchedExtensions): void {
|
||||||
$query->whereIn('target_extension', $watchedExtensions);
|
$query->whereIn('target_extension', $watchedExtensions);
|
||||||
})
|
})
|
||||||
->latest('id')
|
->latest('received_at')
|
||||||
->first(['id', 'phone_number', 'target_extension', 'received_at', 'message_text', 'metadata']);
|
->limit(30)
|
||||||
|
->get(['id', 'channel', 'status', 'phone_number', 'target_extension', 'received_at', 'message_text', 'metadata']);
|
||||||
|
|
||||||
|
$latest = $candidates
|
||||||
|
->sortByDesc(function (CommunicationMessage $message): int {
|
||||||
|
$metadata = is_array($message->metadata) ? $message->metadata : [];
|
||||||
|
$eventType = strtolower(trim((string) ($metadata['event_type'] ?? '')));
|
||||||
|
$endedAt = trim((string) ($metadata['ended_at'] ?? ''));
|
||||||
|
$isActiveCsta = (string) $message->channel === 'panasonic_csta'
|
||||||
|
&& (string) ($message->status ?? '') !== 'completed'
|
||||||
|
&& $endedAt === ''
|
||||||
|
&& ! in_array($eventType, ['connectioncleared', 'cleared', 'released'], true);
|
||||||
|
|
||||||
|
if ($isActiveCsta) {
|
||||||
|
return 5000000000 + (int) ($message->received_at?->getTimestamp() ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$isRecentCsta = (string) $message->channel === 'panasonic_csta'
|
||||||
|
&& (int) ($message->received_at?->getTimestamp() ?? 0) >= now()->subSeconds(45)->getTimestamp();
|
||||||
|
|
||||||
|
if ($isRecentCsta) {
|
||||||
|
return 4000000000 + (int) ($message->received_at?->getTimestamp() ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1000000000 + (int) ($message->received_at?->getTimestamp() ?? 0);
|
||||||
|
})
|
||||||
|
->first();
|
||||||
|
|
||||||
if (! $latest) {
|
if (! $latest) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,13 @@ public function normalizeExtension(?string $extension): string
|
||||||
return is_string($digits) ? $digits : '';
|
return is_string($digits) ? $digits : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function normalizeLineNumber(?string $line): string
|
||||||
|
{
|
||||||
|
$digits = preg_replace('/\D+/', '', (string) $line);
|
||||||
|
|
||||||
|
return is_string($digits) ? $digits : '';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{extension:string,user_id:int|null,stabile_id:int|null,user:?User,amministratore_id:int|null,studio_role:?string,studio_mode:?string}
|
* @return array{extension:string,user_id:int|null,stabile_id:int|null,user:?User,amministratore_id:int|null,studio_role:?string,studio_mode:?string}
|
||||||
*/
|
*/
|
||||||
|
|
@ -65,6 +72,117 @@ public function resolveByExtension(?string $extension): array
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{line:string,user_id:int|null,stabile_id:int|null,user:?User,amministratore_id:int|null,studio_role:?string,studio_mode:?string,target_extension:string,route_group:string,line_label:?string}
|
||||||
|
*/
|
||||||
|
public function resolveByIncomingLine(?string $line): array
|
||||||
|
{
|
||||||
|
$normalized = $this->normalizeLineNumber($line);
|
||||||
|
|
||||||
|
$empty = [
|
||||||
|
'line' => $normalized,
|
||||||
|
'user_id' => null,
|
||||||
|
'stabile_id' => null,
|
||||||
|
'user' => null,
|
||||||
|
'amministratore_id' => null,
|
||||||
|
'studio_role' => null,
|
||||||
|
'studio_mode' => null,
|
||||||
|
'target_extension' => '',
|
||||||
|
'route_group' => '',
|
||||||
|
'line_label' => null,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($normalized === '') {
|
||||||
|
return $empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Amministratore::query()->get(['id', 'impostazioni']) as $amministratore) {
|
||||||
|
$incomingLines = array_values(array_filter((array) (($amministratore->impostazioni ?? [])['pbx']['incoming_lines'] ?? []), fn($row): bool => is_array($row)));
|
||||||
|
|
||||||
|
foreach ($incomingLines as $lineRow) {
|
||||||
|
if (! $this->matchesLineNumber((string) ($lineRow['number'] ?? ''), $normalized)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetExtension = $this->normalizeExtension((string) ($lineRow['target_extension'] ?? ''));
|
||||||
|
$routeGroup = $this->normalizeExtension((string) ($lineRow['route_group'] ?? ''));
|
||||||
|
$targetRouting = $targetExtension !== ''
|
||||||
|
? $this->resolveByExtension($targetExtension)
|
||||||
|
: ($routeGroup !== '' ? $this->resolveByExtension($routeGroup) : [
|
||||||
|
'user_id' => null,
|
||||||
|
'stabile_id' => null,
|
||||||
|
'user' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (($targetRouting['user'] ?? null) === null) {
|
||||||
|
$mappedUser = $this->findUserByLineMapping($amministratore, $normalized);
|
||||||
|
if ($mappedUser instanceof User) {
|
||||||
|
$targetRouting = [
|
||||||
|
'user_id' => (int) $mappedUser->id,
|
||||||
|
'stabile_id' => method_exists($mappedUser, 'stabiliAssegnati') ? ((int) ($mappedUser->stabiliAssegnati()->value('stabili.id') ?? 0) ?: null) : null,
|
||||||
|
'user' => $mappedUser,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'line' => $normalized,
|
||||||
|
'user_id' => (int) ($targetRouting['user_id'] ?? 0) ?: null,
|
||||||
|
'stabile_id' => (int) ($targetRouting['stabile_id'] ?? 0) ?: null,
|
||||||
|
'user' => $targetRouting['user'] ?? null,
|
||||||
|
'amministratore_id' => (int) $amministratore->id,
|
||||||
|
'studio_role' => 'linea',
|
||||||
|
'studio_mode' => filled($lineRow['label'] ?? null) ? (string) $lineRow['label'] : null,
|
||||||
|
'target_extension' => $targetExtension,
|
||||||
|
'route_group' => $routeGroup,
|
||||||
|
'line_label' => filled($lineRow['label'] ?? null) ? (string) $lineRow['label'] : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{extension:string,line:string,user_id:int|null,stabile_id:int|null,user:?User,amministratore_id:int|null,studio_role:?string,studio_mode:?string,target_extension:string,route_group:string,line_label:?string}
|
||||||
|
*/
|
||||||
|
public function resolveBySmdr(?string $extension, ?string $line, ?string $direction): array
|
||||||
|
{
|
||||||
|
$normalizedDirection = strtolower(trim((string) $direction));
|
||||||
|
$extensionRouting = $this->resolveByExtension($extension);
|
||||||
|
$lineRouting = $this->resolveByIncomingLine($line);
|
||||||
|
|
||||||
|
if ($normalizedDirection === 'inbound' && ($lineRouting['amministratore_id'] ?? null) !== null) {
|
||||||
|
return [
|
||||||
|
'extension' => $extensionRouting['extension'],
|
||||||
|
'line' => $lineRouting['line'],
|
||||||
|
'user_id' => $lineRouting['user_id'] ?? $extensionRouting['user_id'],
|
||||||
|
'stabile_id' => $lineRouting['stabile_id'] ?? $extensionRouting['stabile_id'],
|
||||||
|
'user' => $lineRouting['user'] ?? $extensionRouting['user'],
|
||||||
|
'amministratore_id' => $lineRouting['amministratore_id'],
|
||||||
|
'studio_role' => $lineRouting['studio_role'],
|
||||||
|
'studio_mode' => $lineRouting['studio_mode'],
|
||||||
|
'target_extension' => $lineRouting['target_extension'],
|
||||||
|
'route_group' => $lineRouting['route_group'],
|
||||||
|
'line_label' => $lineRouting['line_label'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'extension' => $extensionRouting['extension'],
|
||||||
|
'line' => $lineRouting['line'],
|
||||||
|
'user_id' => $extensionRouting['user_id'],
|
||||||
|
'stabile_id' => $extensionRouting['stabile_id'],
|
||||||
|
'user' => $extensionRouting['user'],
|
||||||
|
'amministratore_id' => $extensionRouting['amministratore_id'] ?? $lineRouting['amministratore_id'],
|
||||||
|
'studio_role' => $extensionRouting['studio_role'],
|
||||||
|
'studio_mode' => $extensionRouting['studio_mode'] ?? $lineRouting['studio_mode'],
|
||||||
|
'target_extension' => '',
|
||||||
|
'route_group' => '',
|
||||||
|
'line_label' => $lineRouting['line_label'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<int,string>
|
* @return array<int,string>
|
||||||
*/
|
*/
|
||||||
|
|
@ -230,4 +348,39 @@ private function resolveAmministratoreIdForUser(User $user): ?int
|
||||||
|
|
||||||
return $amministratore ? (int) $amministratore->id : null;
|
return $amministratore ? (int) $amministratore->id : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function matchesLineNumber(string $configuredLine, string $incomingLine): bool
|
||||||
|
{
|
||||||
|
$configured = $this->normalizeLineNumber($configuredLine);
|
||||||
|
if ($configured === '' || $incomingLine === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $configured === $incomingLine || ltrim($configured, '0') === ltrim($incomingLine, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findUserByLineMapping(Amministratore $amministratore, string $line): ?User
|
||||||
|
{
|
||||||
|
$mappings = (array) (($amministratore->impostazioni ?? [])['pbx']['user_mappings'] ?? []);
|
||||||
|
|
||||||
|
foreach ($mappings as $userId => $mapping) {
|
||||||
|
if (! is_array($mapping)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = array_values(array_filter((array) ($mapping['lines'] ?? []), fn($value): bool => is_string($value) && trim($value) !== ''));
|
||||||
|
foreach ($lines as $configuredLine) {
|
||||||
|
if (! $this->matchesLineNumber($configuredLine, $line)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = User::query()->find((int) $userId);
|
||||||
|
if ($user instanceof User) {
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ public function enrich(string $legacyCode, string $basePath = '/mnt/gescon-archi
|
||||||
// Catasto fields mapping heuristic (legacy → logical field)
|
// Catasto fields mapping heuristic (legacy → logical field)
|
||||||
$mapCatasto = [
|
$mapCatasto = [
|
||||||
'foglio' => ['foglio', 'ac_foglio', 'foglio_catasto'],
|
'foglio' => ['foglio', 'ac_foglio', 'foglio_catasto'],
|
||||||
'particella' => ['particella', 'ac_particella', 'mappale', 'catasto_particella'],
|
'particella' => ['particella', 'ac_particella', 'ac_partic1', 'ac_partic_1', 'ac_particella1', 'ac_particella_1', 'mappale', 'catasto_particella'],
|
||||||
'subalterno' => ['sub', 'subalterno', 'ac_sub', 'ac_subalterno'],
|
'subalterno' => ['sub', 'subalterno', 'ac_sub', 'ac_subalterno'],
|
||||||
'sezione' => ['sezione', 'ac_sezione', 'sez'],
|
'sezione' => ['sezione', 'ac_sezione', 'sez'],
|
||||||
'categoria' => ['categoria_catastale', 'categoria', 'cat_categoria'],
|
'categoria' => ['categoria_catastale', 'categoria', 'cat_categoria'],
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,9 @@
|
||||||
use App\Console\Commands\GesconImportFattureAdeCsv;
|
use App\Console\Commands\GesconImportFattureAdeCsv;
|
||||||
use App\Console\Commands\GesconImportFornitoriLegacyMdb;
|
use App\Console\Commands\GesconImportFornitoriLegacyMdb;
|
||||||
use App\Console\Commands\GesconMaterializePersoneRelazioniCommand;
|
use App\Console\Commands\GesconMaterializePersoneRelazioniCommand;
|
||||||
use App\Console\Commands\GesconQaRubricaLegamiCommand;
|
|
||||||
use App\Console\Commands\GesconQaContatori;
|
use App\Console\Commands\GesconQaContatori;
|
||||||
use App\Console\Commands\GesconQaEmissione;
|
use App\Console\Commands\GesconQaEmissione;
|
||||||
|
use App\Console\Commands\GesconQaRubricaLegamiCommand;
|
||||||
use App\Console\Commands\GesconResyncStabili;
|
use App\Console\Commands\GesconResyncStabili;
|
||||||
use App\Console\Commands\GoogleImportKeepNotesCommand;
|
use App\Console\Commands\GoogleImportKeepNotesCommand;
|
||||||
use App\Console\Commands\GooglePushRubricaContactsCommand;
|
use App\Console\Commands\GooglePushRubricaContactsCommand;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
<?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::create('support_update_entries', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('release_key')->unique();
|
||||||
|
$table->string('category', 32)->index();
|
||||||
|
$table->string('audience', 32)->default('supporto')->index();
|
||||||
|
$table->string('title');
|
||||||
|
$table->text('summary');
|
||||||
|
$table->text('impact')->nullable();
|
||||||
|
$table->string('page_label')->nullable();
|
||||||
|
$table->string('page_class')->nullable();
|
||||||
|
$table->string('related_path')->nullable();
|
||||||
|
$table->string('related_commit', 40)->nullable()->index();
|
||||||
|
$table->boolean('is_visible')->default(true)->index();
|
||||||
|
$table->boolean('is_highlighted')->default(false);
|
||||||
|
$table->string('recorded_at', 40)->nullable()->index();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('support_bug_registries', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('fingerprint', 64)->unique();
|
||||||
|
$table->unsignedInteger('bug_id')->unique();
|
||||||
|
$table->string('status', 20)->default('open')->index();
|
||||||
|
$table->string('title')->nullable();
|
||||||
|
$table->string('first_seen', 40)->nullable();
|
||||||
|
$table->string('last_seen', 40)->nullable()->index();
|
||||||
|
$table->string('resolved_at', 40)->nullable();
|
||||||
|
$table->text('resolution_note')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('support_bug_registries');
|
||||||
|
Schema::dropIfExists('support_update_entries');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -175,7 +175,7 @@ ### Sequenza pratica consigliata
|
||||||
- porta `33333`
|
- porta `33333`
|
||||||
- nessun trasporto bloccato su `USB:PC` se il collegamento e IP
|
- nessun trasporto bloccato su `USB:PC` se il collegamento e IP
|
||||||
|
|
||||||
4. verifica che il centralino abbia licenza CTI attiva lato TSP/CSTA
|
1. verifica che il centralino abbia licenza CTI attiva lato TSP/CSTA
|
||||||
2. salva la configurazione e riavvia il tool Panasonic se richiesto
|
2. salva la configurazione e riavvia il tool Panasonic se richiesto
|
||||||
3. rilancia `get-netgescon-panasonic-tapi-info.ps1`
|
3. rilancia `get-netgescon-panasonic-tapi-info.ps1`
|
||||||
4. conferma che compaiano interni reali come `201`, `205`, `206`
|
4. conferma che compaiano interni reali come `201`, `205`, `206`
|
||||||
|
|
|
||||||
|
|
@ -469,6 +469,87 @@
|
||||||
<x-slot name="heading">Collegamenti</x-slot>
|
<x-slot name="heading">Collegamenti</x-slot>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
|
@if(empty($stabili) && !empty($collegamentoSuggerito))
|
||||||
|
<div class="rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900">
|
||||||
|
<div class="font-semibold">Collegamento riconosciuto ma non ancora agganciato</div>
|
||||||
|
<div class="mt-1 text-xs text-amber-800">
|
||||||
|
La scheda contiene riferimenti a uno stabile coerente, ma manca ancora il legame strutturato che popola questo box.
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 grid gap-2 text-xs md:grid-cols-3">
|
||||||
|
<div>
|
||||||
|
<div class="font-semibold text-amber-900">Stabile candidato</div>
|
||||||
|
<div>{{ $collegamentoSuggerito['stabile_label'] ?? '—' }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="font-semibold text-amber-900">Unità candidata</div>
|
||||||
|
<div>{{ $collegamentoSuggerito['unita_reference'] ?? 'Non rilevata' }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="font-semibold text-amber-900">Ruolo proposto</div>
|
||||||
|
<div>{{ $this::getRuoliLabels()[$collegamentoSuggerito['role_standard'] ?? 'altro'] ?? ($collegamentoSuggerito['role_standard'] ?? 'Altro') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 flex flex-wrap gap-2">
|
||||||
|
<x-filament::button type="button" color="warning" wire:click="collegaCollegamentoSuggerito">
|
||||||
|
Aggancia collegamento suggerito
|
||||||
|
</x-filament::button>
|
||||||
|
<x-filament::button type="button" color="gray" wire:click="mountAction('ruoli_legami')">
|
||||||
|
Apri editor completo ruoli / legami
|
||||||
|
</x-filament::button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="rounded-lg border border-slate-200 bg-slate-50 p-4">
|
||||||
|
<div class="text-sm font-semibold text-slate-900">Collegamento rapido a stabile / unità</div>
|
||||||
|
<div class="mt-1 text-xs text-slate-600">
|
||||||
|
Usa questo blocco per agganciare manualmente una nuova anagrafica a uno stabile del tenant attivo oppure per completare un record importato che oggi ha solo riferimenti testuali.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 grid gap-3 md:grid-cols-3">
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs font-semibold text-slate-700">Stabile</label>
|
||||||
|
<select wire:model.live="collegamentoStabileId" class="w-full rounded-md border-gray-300 text-sm">
|
||||||
|
<option value="">Seleziona stabile</option>
|
||||||
|
@foreach($this->getStabiliOptions() as $stabileId => $stabileLabel)
|
||||||
|
<option value="{{ (int) $stabileId }}">{{ $stabileLabel }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs font-semibold text-slate-700">Unità</label>
|
||||||
|
<select wire:model.live="collegamentoUnitaId" class="w-full rounded-md border-gray-300 text-sm">
|
||||||
|
<option value="">Nessuna unità specifica</option>
|
||||||
|
@foreach($this->getUnitaOptions($collegamentoStabileId) as $unitaId => $unitaLabel)
|
||||||
|
<option value="{{ (int) $unitaId }}">{{ $unitaLabel }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs font-semibold text-slate-700">Ruolo</label>
|
||||||
|
<select wire:model.live="collegamentoRuolo" class="w-full rounded-md border-gray-300 text-sm">
|
||||||
|
@foreach($this::getRuoliLabels() as $ruoloKey => $ruoloLabel)
|
||||||
|
<option value="{{ $ruoloKey }}">{{ $ruoloLabel }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 grid gap-3 md:grid-cols-[1fr_auto]">
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs font-semibold text-slate-700">Ruolo custom</label>
|
||||||
|
<input type="text" wire:model.defer="collegamentoRuoloCustom" class="w-full rounded-md border-gray-300 text-sm" placeholder="Opzionale" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-end">
|
||||||
|
<x-filament::button type="button" color="primary" wire:click="salvaCollegamentoManuale">
|
||||||
|
Salva collegamento
|
||||||
|
</x-filament::button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm text-gray-500">Fornitori (ambiente attivo)</div>
|
<div class="text-sm text-gray-500">Fornitori (ambiente attivo)</div>
|
||||||
@if(count($fornitori) > 0)
|
@if(count($fornitori) > 0)
|
||||||
|
|
|
||||||
|
|
@ -131,9 +131,11 @@
|
||||||
- {{ $gruppo['phone'] }}
|
- {{ $gruppo['phone'] }}
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@php($rubricaUrlGrouped = $this->getRubricaUrlByPhone((string) $gruppo['phone']))
|
@php($rubricaUrlGrouped = $gruppo['rubrica_url'])
|
||||||
@if($rubricaUrlGrouped)
|
@if($rubricaUrlGrouped)
|
||||||
<a href="{{ $rubricaUrlGrouped }}" class="mt-1 inline-flex text-xs font-medium text-indigo-700 hover:underline">Apri scheda rubrica</a>
|
<a href="{{ $rubricaUrlGrouped }}" class="mt-1 inline-flex text-xs font-medium text-indigo-700 hover:underline">Apri scheda rubrica</a>
|
||||||
|
@elseif($gruppo['can_create_rubrica'])
|
||||||
|
<button type="button" wire:click="createRubricaDraftFromGroup('{{ $gruppo['modal_key'] }}', 'open')" class="mt-1 inline-flex text-xs font-medium text-indigo-700 hover:underline">Crea nuova anagrafica</button>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<span class="rounded-full bg-amber-200 px-2.5 py-1 text-xs font-semibold text-amber-900">{{ $gruppo['count'] }} chiamate</span>
|
<span class="rounded-full bg-amber-200 px-2.5 py-1 text-xs font-semibold text-amber-900">{{ $gruppo['count'] }} chiamate</span>
|
||||||
|
|
@ -143,15 +145,21 @@
|
||||||
Aperto dal {{ $gruppo['first_at'] }} al {{ $gruppo['last_at'] }}
|
Aperto dal {{ $gruppo['first_at'] }} al {{ $gruppo['last_at'] }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 flex flex-wrap gap-2 text-[11px]">
|
||||||
|
<span class="rounded-full bg-emerald-100 px-2 py-1 font-semibold text-emerald-800">{{ (int) $gruppo['answered_count'] }} risposte</span>
|
||||||
|
<span class="rounded-full bg-rose-100 px-2 py-1 font-semibold text-rose-800">{{ (int) $gruppo['missed_count'] }} mancate risposte</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex-1 space-y-2 border-t border-amber-300 pt-3">
|
<div class="mt-4 flex-1 space-y-2 border-t border-amber-300 pt-3">
|
||||||
@foreach($gruppo['visible_items'] as $item)
|
@foreach($gruppo['visible_items'] as $item)
|
||||||
|
@php($callState = $this->getPostItCallStateMeta($item))
|
||||||
<div class="rounded-xl border border-amber-300 bg-amber-50/70 p-2.5">
|
<div class="rounded-xl border border-amber-300 bg-amber-50/70 p-2.5">
|
||||||
<div class="flex items-start justify-between gap-3">
|
<div class="flex items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs font-medium text-amber-950">{{ $item->oggetto ?: 'Senza oggetto' }}</div>
|
<div class="text-xs font-medium text-amber-950">{{ $item->oggetto ?: 'Senza oggetto' }}</div>
|
||||||
<div class="text-[11px] text-amber-800">{{ optional($item->chiamata_il)->format('d/m/Y H:i') }}</div>
|
<div class="text-[11px] text-amber-800">{{ optional($item->chiamata_il)->format('d/m/Y H:i') }}</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="rounded-md bg-white/70 px-2 py-1 text-[11px] text-amber-900">{{ $item->stato }}</span>
|
<span class="rounded-md px-2 py-1 text-[11px] font-semibold {{ $callState['class'] }}">{{ $callState['label'] }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if($item->stabile)
|
@if($item->stabile)
|
||||||
|
|
@ -169,7 +177,10 @@
|
||||||
|
|
||||||
<div class="mt-4 flex flex-wrap gap-2">
|
<div class="mt-4 flex flex-wrap gap-2">
|
||||||
<x-filament::button size="sm" color="warning" wire:click="openGroupModal('{{ $gruppo['modal_key'] }}', 'open')">Vedi dettaglio</x-filament::button>
|
<x-filament::button size="sm" color="warning" wire:click="openGroupModal('{{ $gruppo['modal_key'] }}', 'open')">Vedi dettaglio</x-filament::button>
|
||||||
<a href="{{ $this->getPostItPageUrl((int) $box->id) }}" class="inline-flex items-center rounded-md bg-amber-700 px-3 py-1.5 text-xs font-medium text-white hover:bg-amber-600">Apri Post-it</a>
|
<a href="{{ $this->getPostItPageUrl((int) $gruppo['primary_post_it']->id) }}" class="inline-flex items-center rounded-md bg-amber-700 px-3 py-1.5 text-xs font-medium text-white hover:bg-amber-600">{{ $gruppo['primary_action_label'] }}</a>
|
||||||
|
@if($gruppo['can_create_rubrica'])
|
||||||
|
<x-filament::button size="sm" color="info" wire:click="createRubricaDraftFromGroup('{{ $gruppo['modal_key'] }}', 'open')">Nuova anagrafica</x-filament::button>
|
||||||
|
@endif
|
||||||
@if($box->ticket_id)
|
@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 border-amber-400 bg-white/70 px-3 py-1.5 text-xs text-amber-900">Apri ticket</a>
|
<a href="{{ \App\Filament\Pages\Supporto\TicketGestione::getUrl(panel: 'admin-filament') }}#ticket-{{ (int) $box->ticket_id }}" class="inline-flex items-center rounded-md border border-amber-400 bg-white/70 px-3 py-1.5 text-xs text-amber-900">Apri ticket</a>
|
||||||
@endif
|
@endif
|
||||||
|
|
@ -392,8 +403,19 @@
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm font-semibold text-slate-900">{{ $this->selectedGroupModal['caller_label'] }}@if($this->selectedGroupModal['phone'] !== '') - {{ $this->selectedGroupModal['phone'] }}@endif</div>
|
<div class="text-sm font-semibold text-slate-900">{{ $this->selectedGroupModal['caller_label'] }}@if($this->selectedGroupModal['phone'] !== '') - {{ $this->selectedGroupModal['phone'] }}@endif</div>
|
||||||
<div class="text-xs text-slate-500">{{ $this->selectedGroupModal['count'] }} chiamate totali · {{ $selectedGroupModalMode === 'closed' ? 'archivio chiusi' : 'lavagna aperti' }}</div>
|
<div class="text-xs text-slate-500">{{ $this->selectedGroupModal['count'] }} chiamate totali · {{ $selectedGroupModalMode === 'closed' ? 'archivio chiusi' : 'lavagna aperti' }}</div>
|
||||||
|
<div class="mt-1 flex flex-wrap gap-2 text-[11px]">
|
||||||
|
<span class="rounded-full bg-emerald-100 px-2 py-1 font-semibold text-emerald-800">{{ (int) $this->selectedGroupModal['answered_count'] }} risposte</span>
|
||||||
|
<span class="rounded-full bg-rose-100 px-2 py-1 font-semibold text-rose-800">{{ (int) $this->selectedGroupModal['missed_count'] }} mancate risposte</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
@if(($this->selectedGroupModal['rubrica_url'] ?? null))
|
||||||
|
<a href="{{ $this->selectedGroupModal['rubrica_url'] }}" class="inline-flex items-center rounded-md border px-3 py-1.5 text-xs text-indigo-700">Apri scheda rubrica</a>
|
||||||
|
@elseif($this->selectedGroupModal['can_create_rubrica'])
|
||||||
|
<x-filament::button size="sm" color="info" wire:click="createRubricaDraftFromGroup('{{ $this->selectedGroupModal['modal_key'] }}', '{{ $selectedGroupModalMode }}')">Crea anagrafica</x-filament::button>
|
||||||
|
@endif
|
||||||
|
<button type="button" wire:click="closeGroupModal" class="rounded-md bg-slate-100 px-3 py-1.5 text-xs text-slate-700 hover:bg-slate-200">Chiudi</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" wire:click="closeGroupModal" class="rounded-md bg-slate-100 px-3 py-1.5 text-xs text-slate-700 hover:bg-slate-200">Chiudi</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="max-h-[70vh] overflow-y-auto p-5">
|
<div class="max-h-[70vh] overflow-y-auto p-5">
|
||||||
|
|
@ -403,13 +425,14 @@
|
||||||
<div class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-500">{{ $day['day'] }}</div>
|
<div class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-500">{{ $day['day'] }}</div>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
@foreach($day['items'] as $item)
|
@foreach($day['items'] as $item)
|
||||||
|
@php($callState = $this->getPostItCallStateMeta($item))
|
||||||
<div class="rounded-xl border bg-slate-50 p-3">
|
<div class="rounded-xl border bg-slate-50 p-3">
|
||||||
<div class="flex items-start justify-between gap-3">
|
<div class="flex items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm font-medium text-slate-900">{{ $item->oggetto ?: 'Senza oggetto' }}</div>
|
<div class="text-sm font-medium text-slate-900">{{ $item->oggetto ?: 'Senza oggetto' }}</div>
|
||||||
<div class="text-xs text-slate-600">{{ optional($item->chiamata_il)->format('d/m/Y H:i') }}</div>
|
<div class="text-xs text-slate-600">{{ optional($item->chiamata_il)->format('d/m/Y H:i') }}</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="rounded-md bg-white px-2 py-1 text-[11px] text-slate-700">{{ $item->stato }}</span>
|
<span class="rounded-md px-2 py-1 text-[11px] font-semibold {{ $callState['class'] }}">{{ $callState['label'] }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if($item->stabile)
|
@if($item->stabile)
|
||||||
|
|
@ -417,6 +440,10 @@
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<div class="mt-2 whitespace-pre-wrap text-xs text-slate-800">{{ $item->nota }}</div>
|
<div class="mt-2 whitespace-pre-wrap text-xs text-slate-800">{{ $item->nota }}</div>
|
||||||
|
|
||||||
|
<div class="mt-3 flex flex-wrap gap-2">
|
||||||
|
<a href="{{ $this->getPostItPageUrl((int) $item->id) }}" class="inline-flex items-center rounded-md border px-3 py-1 text-[11px] text-slate-700">{{ $this->isAnsweredPostIt($item) ? 'Apri Post-it' : 'Apri richiamo' }}</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -169,28 +169,63 @@ class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium {{ $t
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-lg border bg-white p-3">
|
<div class="rounded-lg border bg-white p-3">
|
||||||
<div class="text-xs font-semibold text-gray-700">Aggiornamenti inclusi nel riallineamento Git</div>
|
<div class="flex flex-wrap items-start justify-between gap-2">
|
||||||
|
<div>
|
||||||
|
<div class="text-xs font-semibold text-gray-700">Anteprima prima della sync Git</div>
|
||||||
|
<div class="mt-1 text-[11px] text-gray-500">Conteggio commit remoti, aree toccate e registro persistente delle modifiche gia strutturate.</div>
|
||||||
|
</div>
|
||||||
|
<x-filament::button size="sm" color="gray" wire:click="reloadDashboardData">
|
||||||
|
Ricalcola anteprima
|
||||||
|
</x-filament::button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 grid gap-2 sm:grid-cols-4">
|
||||||
|
<div class="rounded-lg border bg-slate-50 p-2">
|
||||||
|
<div class="text-[10px] uppercase tracking-wide text-slate-500">Commit in arrivo</div>
|
||||||
|
<div class="mt-1 text-lg font-semibold text-slate-800">{{ (int) $this->pendingGitCommitCount }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg border bg-slate-50 p-2">
|
||||||
|
<div class="text-[10px] uppercase tracking-wide text-slate-500">Registro DB visibile</div>
|
||||||
|
<div class="mt-1 text-lg font-semibold text-slate-800">{{ (int) $this->supportUpdateVisibleCount }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg border bg-slate-50 p-2">
|
||||||
|
<div class="text-[10px] uppercase tracking-wide text-slate-500">Implementazioni</div>
|
||||||
|
<div class="mt-1 text-lg font-semibold text-emerald-700">{{ (int) $this->supportImplementationCount }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg border bg-slate-50 p-2">
|
||||||
|
<div class="text-[10px] uppercase tracking-wide text-slate-500">Fix + debug</div>
|
||||||
|
<div class="mt-1 text-lg font-semibold text-amber-700">{{ (int) ($this->supportBugfixCount + $this->supportDebugCount) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-2 grid gap-3 lg:grid-cols-2">
|
<div class="mt-2 grid gap-3 lg:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-1 text-[11px] font-semibold text-slate-700">Ultimi commit</div>
|
<div class="mb-1 text-[11px] font-semibold text-slate-700">Commit remoti non ancora allineati</div>
|
||||||
|
@if(filled($this->pendingGitPreviewIssue))
|
||||||
|
<div class="mb-2 rounded border border-amber-300 bg-amber-50 px-2 py-1 text-[10px] text-amber-900">{{ $this->pendingGitPreviewIssue }}</div>
|
||||||
|
@endif
|
||||||
<ul class="space-y-1 text-[11px] text-gray-700">
|
<ul class="space-y-1 text-[11px] text-gray-700">
|
||||||
@forelse(array_slice($this->latestCommits, 0, 5) as $commit)
|
@forelse($this->pendingGitCommits as $commit)
|
||||||
<li class="rounded border bg-slate-50 px-2 py-1">
|
<li class="rounded border bg-slate-50 px-2 py-1">
|
||||||
<span class="font-mono text-[10px] text-slate-500">{{ $commit['short_hash'] }}</span>
|
<span class="font-mono text-[10px] text-slate-500">{{ $commit['short_hash'] }}</span>
|
||||||
<span class="ml-1">{{ $commit['message'] }}</span>
|
<span class="ml-1">{{ $commit['message'] }}</span>
|
||||||
|
<div class="mt-0.5 text-[10px] text-slate-500">{{ $commit['date'] }} · {{ $commit['author'] }}</div>
|
||||||
</li>
|
</li>
|
||||||
@empty
|
@empty
|
||||||
<li class="text-gray-500">Nessun commit disponibile.</li>
|
<li class="rounded border border-emerald-200 bg-emerald-50 px-2 py-1 text-emerald-800">Il nodo risulta gia allineato al branch remoto.</li>
|
||||||
@endforelse
|
@endforelse
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-1 text-[11px] font-semibold text-slate-700">Migliorie/fix rilevati</div>
|
<div class="mb-1 text-[11px] font-semibold text-slate-700">Pagine e aree toccate dai commit in arrivo</div>
|
||||||
<ul class="list-disc space-y-1 pl-4 text-[11px] text-gray-700">
|
<ul class="space-y-1 text-[11px] text-gray-700">
|
||||||
@forelse(array_slice($this->functionalHighlights, 0, 6) as $item)
|
@forelse($this->pendingGitChangedAreas as $item)
|
||||||
<li>{{ $item }}</li>
|
<li class="rounded border bg-slate-50 px-2 py-1">
|
||||||
|
<div class="font-medium text-slate-800">{{ $item['label'] }}</div>
|
||||||
|
<div class="text-[10px] text-slate-500">{{ $item['area'] }} · {{ $item['path'] }}</div>
|
||||||
|
</li>
|
||||||
@empty
|
@empty
|
||||||
<li>Nessuna miglioria estratta automaticamente.</li>
|
<li class="text-gray-500">Nessuna area Filament/supporto modificata nei commit remoti in attesa.</li>
|
||||||
@endforelse
|
@endforelse
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -198,6 +233,171 @@ class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium {{ $t
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 rounded-lg border bg-white p-3">
|
||||||
|
<div class="mb-2 flex flex-wrap items-start justify-between gap-2">
|
||||||
|
<div>
|
||||||
|
<div class="text-xs font-semibold text-gray-700">Registro aggiornamenti persistente in DB</div>
|
||||||
|
<div class="mt-1 text-[11px] text-gray-500">Implementazioni, fix e debug registrati in tabella applicativa con pagina impattata e commit di riferimento.</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded bg-slate-100 px-2 py-1 text-[10px] font-semibold text-slate-700">
|
||||||
|
{{ (int) $this->supportUpdateVisibleCount }} voci visibili
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3 grid gap-3 lg:grid-cols-[0.85fr_1.15fr]">
|
||||||
|
<div class="rounded-lg border bg-slate-50 p-3">
|
||||||
|
<div class="mb-2 flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div class="text-[11px] font-semibold text-slate-800">Editor voci aggiornamento</div>
|
||||||
|
<x-filament::button size="sm" color="gray" wire:click="resetSupportUpdateForm">
|
||||||
|
Nuova voce
|
||||||
|
</x-filament::button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-2 sm:grid-cols-2">
|
||||||
|
<label class="block">
|
||||||
|
<span class="mb-1 block text-[11px] font-medium">Categoria</span>
|
||||||
|
<select wire:model="supportUpdateFormCategory" class="w-full rounded-md border-gray-300 text-xs">
|
||||||
|
<option value="implementazione">implementazione</option>
|
||||||
|
<option value="bugfix">bugfix</option>
|
||||||
|
<option value="debug">debug</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="block">
|
||||||
|
<span class="mb-1 block text-[11px] font-medium">Audience</span>
|
||||||
|
<select wire:model="supportUpdateFormAudience" class="w-full rounded-md border-gray-300 text-xs">
|
||||||
|
<option value="utenti">utenti</option>
|
||||||
|
<option value="supporto">supporto</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="mt-2 block">
|
||||||
|
<span class="mb-1 block text-[11px] font-medium">Titolo</span>
|
||||||
|
<input type="text" wire:model.defer="supportUpdateFormTitle" class="w-full rounded-md border-gray-300 text-xs" placeholder="Es: Lavagna chiamate con archivio chiusi" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="mt-2 block">
|
||||||
|
<span class="mb-1 block text-[11px] font-medium">Riepilogo</span>
|
||||||
|
<textarea wire:model.defer="supportUpdateFormSummary" rows="3" class="w-full rounded-md border-gray-300 text-xs" placeholder="Cosa cambia davvero per chi aggiorna o usa la funzione"></textarea>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="mt-2 block">
|
||||||
|
<span class="mb-1 block text-[11px] font-medium">Impatto operativo</span>
|
||||||
|
<textarea wire:model.defer="supportUpdateFormImpact" rows="2" class="w-full rounded-md border-gray-300 text-xs" placeholder="Perche e importante prima della sync staging"></textarea>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="mt-2 grid gap-2 sm:grid-cols-2">
|
||||||
|
<label class="block">
|
||||||
|
<span class="mb-1 block text-[11px] font-medium">Pagina UI</span>
|
||||||
|
<input type="text" wire:model.defer="supportUpdateFormPageLabel" class="w-full rounded-md border-gray-300 text-xs" placeholder="Es: Strumenti > Post-it Gestione" />
|
||||||
|
</label>
|
||||||
|
<label class="block">
|
||||||
|
<span class="mb-1 block text-[11px] font-medium">Quando registrata</span>
|
||||||
|
<input type="text" wire:model.defer="supportUpdateFormRecordedAt" class="w-full rounded-md border-gray-300 text-xs" placeholder="2026-04-04 11:00" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 grid gap-2 sm:grid-cols-2">
|
||||||
|
<label class="block">
|
||||||
|
<span class="mb-1 block text-[11px] font-medium">Classe pagina</span>
|
||||||
|
<input type="text" wire:model.defer="supportUpdateFormPageClass" class="w-full rounded-md border-gray-300 text-xs" placeholder="App\Filament\Pages\..." />
|
||||||
|
</label>
|
||||||
|
<label class="block">
|
||||||
|
<span class="mb-1 block text-[11px] font-medium">Path collegato</span>
|
||||||
|
<input type="text" wire:model.defer="supportUpdateFormRelatedPath" class="w-full rounded-md border-gray-300 text-xs" placeholder="app/Filament/Pages/..." />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="mt-2 block">
|
||||||
|
<span class="mb-1 block text-[11px] font-medium">Commit collegato</span>
|
||||||
|
<input type="text" wire:model.defer="supportUpdateFormRelatedCommit" class="w-full rounded-md border-gray-300 text-xs" placeholder="short hash o commit completo" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="mt-2 flex flex-wrap gap-3 text-[11px]">
|
||||||
|
<label class="inline-flex items-center gap-2 rounded-md border bg-white px-2 py-1.5">
|
||||||
|
<input type="checkbox" wire:model="supportUpdateFormVisible" class="rounded border-gray-300" />
|
||||||
|
Visibile in elenco
|
||||||
|
</label>
|
||||||
|
<label class="inline-flex items-center gap-2 rounded-md border bg-white px-2 py-1.5">
|
||||||
|
<input type="checkbox" wire:model="supportUpdateFormHighlighted" class="rounded border-gray-300" />
|
||||||
|
Metti in evidenza
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 flex flex-wrap gap-2">
|
||||||
|
<x-filament::button size="sm" wire:click="saveSupportUpdateEntry">
|
||||||
|
Salva voce registro
|
||||||
|
</x-filament::button>
|
||||||
|
<x-filament::button size="sm" color="gray" wire:click="resetSupportUpdateForm">
|
||||||
|
Reset form
|
||||||
|
</x-filament::button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg border bg-slate-50 p-3">
|
||||||
|
<div class="text-[11px] font-semibold text-slate-800">Come usarlo prima di Git e staging</div>
|
||||||
|
<ol class="mt-2 list-decimal space-y-1 pl-4 text-[11px] text-slate-700">
|
||||||
|
<li>registra qui implementazioni, fix o debug dell ultimo blocco di lavoro</li>
|
||||||
|
<li>collega, se la conosci, la pagina UI e il commit previsto o gia creato</li>
|
||||||
|
<li>usa il conteggio commit remoti per controllare se lo staging e davvero indietro</li>
|
||||||
|
<li>poi fai il push Git e usa questa stessa tab per la sync da Gitea</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
@if($this->supportUpdateFormId)
|
||||||
|
<div class="mt-3 rounded border border-amber-300 bg-amber-50 px-3 py-2 text-[11px] text-amber-900">
|
||||||
|
Stai modificando la voce ID {{ $this->supportUpdateFormId }}. Se vuoi inserirne una nuova, premi “Nuova voce”.
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-2 lg:grid-cols-2">
|
||||||
|
@forelse(array_slice($this->supportUpdateEntries, 0, 8) as $entry)
|
||||||
|
<div class="rounded-lg border {{ $entry['is_highlighted'] ? 'border-amber-300 bg-amber-50' : 'bg-slate-50' }} p-3">
|
||||||
|
<div class="flex flex-wrap items-start justify-between gap-2">
|
||||||
|
<div class="font-semibold text-slate-800">{{ $entry['title'] }}</div>
|
||||||
|
<span class="rounded px-1.5 py-0.5 text-[10px] font-semibold uppercase {{ $entry['category'] === 'implementazione' ? 'bg-emerald-100 text-emerald-800' : ($entry['category'] === 'bugfix' ? 'bg-amber-100 text-amber-900' : 'bg-sky-100 text-sky-800') }}">{{ $entry['category'] }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 text-[11px] text-slate-700">{{ $entry['summary'] }}</div>
|
||||||
|
@if($entry['impact'] !== '')
|
||||||
|
<div class="mt-2 rounded border border-slate-200 bg-white px-2 py-1 text-[10px] text-slate-700">{{ $entry['impact'] }}</div>
|
||||||
|
@endif
|
||||||
|
<div class="mt-2 flex flex-wrap gap-x-3 gap-y-1 text-[10px] text-slate-500">
|
||||||
|
@if($entry['page_label'] !== '')
|
||||||
|
<span>Pagina: {{ $entry['page_label'] }}</span>
|
||||||
|
@endif
|
||||||
|
@if($entry['related_commit'] !== '')
|
||||||
|
<span>Commit: <span class="font-mono">{{ $entry['related_commit'] }}</span></span>
|
||||||
|
@endif
|
||||||
|
@if($entry['recorded_at'] !== '')
|
||||||
|
<span>Registrato: {{ $entry['recorded_at'] }}</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@if($entry['related_path'] !== '' || $entry['page_url'])
|
||||||
|
<div class="mt-2 flex flex-wrap gap-2 text-[10px]">
|
||||||
|
@if($entry['page_url'])
|
||||||
|
<a href="{{ $entry['page_url'] }}" class="text-primary-600 hover:underline">Apri pagina</a>
|
||||||
|
@endif
|
||||||
|
@if($entry['related_path'] !== '')
|
||||||
|
<span class="rounded bg-white px-1.5 py-0.5 font-mono text-slate-600">{{ $entry['related_path'] }}</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
<div class="mt-2 flex flex-wrap gap-2">
|
||||||
|
<x-filament::button size="xs" color="gray" wire:click="editSupportUpdateEntry({{ (int) $entry['id'] }})">
|
||||||
|
Modifica
|
||||||
|
</x-filament::button>
|
||||||
|
<x-filament::button size="xs" color="{{ $entry['is_visible'] ? 'warning' : 'success' }}" wire:click="toggleSupportUpdateVisibility({{ (int) $entry['id'] }})">
|
||||||
|
{{ $entry['is_visible'] ? 'Nascondi' : 'Mostra' }}
|
||||||
|
</x-filament::button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<div class="rounded border border-dashed px-3 py-4 text-[11px] text-gray-500 lg:col-span-2">Registro DB non ancora disponibile: esegui le migration del nuovo support registry su questo nodo.</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-3 lg:grid-cols-[1.15fr_0.85fr]">
|
<div class="grid gap-3 lg:grid-cols-[1.15fr_0.85fr]">
|
||||||
<div class="rounded-lg border bg-gray-50 p-3 lg:col-span-2">
|
<div class="rounded-lg border bg-gray-50 p-3 lg:col-span-2">
|
||||||
<div class="mb-2 text-xs font-semibold text-gray-700">Aggiornamento staging da Gitea</div>
|
<div class="mb-2 text-xs font-semibold text-gray-700">Aggiornamento staging da Gitea</div>
|
||||||
|
|
@ -528,19 +728,64 @@ class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium {{ $t
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-lg border bg-gray-50 p-3">
|
@if($this->highlightedBug)
|
||||||
<div class="mb-2 text-[11px] font-semibold text-gray-700">Nota risoluzione BUG</div>
|
<div class="rounded-lg border border-amber-300 bg-amber-50 p-3">
|
||||||
<div class="mb-2 text-[11px] text-gray-500">
|
<div class="flex flex-wrap items-start justify-between gap-2">
|
||||||
BUG selezionato: <span class="font-mono font-semibold">{{ $this->selectedBugFingerprint ? 'selezionato' : 'nessuno' }}</span>
|
<div>
|
||||||
|
<div class="text-[11px] font-semibold text-amber-900">BUG in evidenza</div>
|
||||||
|
<div class="mt-1 text-lg font-semibold text-slate-900">{{ $this->highlightedBug['bug_code'] }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-2 text-[10px]">
|
||||||
|
<span class="rounded bg-white px-2 py-1 font-semibold text-slate-700">Occorrenze {{ (int) ($this->highlightedBug['occurrences'] ?? 1) }}</span>
|
||||||
|
<span class="rounded px-2 py-1 font-semibold {{ ($this->highlightedBug['bug_status'] ?? 'open') === 'resolved' ? 'bg-emerald-100 text-emerald-800' : 'bg-rose-100 text-rose-800' }}">{{ ($this->highlightedBug['bug_status'] ?? 'open') === 'resolved' ? 'risolto' : 'aperto' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 rounded border border-amber-200 bg-white px-3 py-2">
|
||||||
|
<div class="text-[11px] font-semibold text-slate-700">Messaggio</div>
|
||||||
|
<div class="mt-1 text-[11px] text-slate-800">{{ $this->highlightedBug['message'] }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 grid gap-2 lg:grid-cols-2">
|
||||||
|
<div class="rounded border border-amber-200 bg-white px-3 py-2">
|
||||||
|
<div class="text-[11px] font-semibold text-slate-700">Contesto</div>
|
||||||
|
<div class="mt-1 whitespace-pre-wrap break-words text-[10px] text-slate-700">{{ $this->highlightedBug['context'] !== '' ? $this->highlightedBug['context'] : '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded border border-amber-200 bg-white px-3 py-2">
|
||||||
|
<div class="text-[11px] font-semibold text-slate-700">Nota risoluzione</div>
|
||||||
|
<label class="mt-1 block">
|
||||||
|
<textarea wire:model.defer="bugResolutionNote" rows="5" class="w-full rounded-md border-gray-300 text-xs" placeholder="Es: risolto con fix parser X, migration Y, deploy Z"></textarea>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(filled($this->highlightedBugHint))
|
||||||
|
<div class="mt-2 rounded border border-sky-200 bg-sky-50 px-3 py-2 text-[11px] text-sky-900">{{ $this->highlightedBugHint }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($this->lastNotificationSanitization)
|
||||||
|
<div class="mt-2 rounded border border-slate-200 bg-white px-3 py-2 text-[11px] text-slate-700">
|
||||||
|
Ultima sanificazione notifiche Filament: <span class="font-semibold">{{ $this->lastNotificationSanitization['timestamp'] ?? '-' }}</span>
|
||||||
|
· rimossi <span class="font-semibold">{{ (int) ($this->lastNotificationSanitization['removed_items'] ?? 0) }}</span>
|
||||||
|
· rimasti <span class="font-semibold">{{ (int) ($this->lastNotificationSanitization['remaining_items'] ?? 0) }}</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="mt-3 flex flex-wrap gap-2">
|
||||||
|
<x-filament::button size="sm" color="gray" wire:click="selectBugForResolution('{{ $this->highlightedBug['fingerprint'] }}')">Seleziona BUG</x-filament::button>
|
||||||
|
<x-filament::button size="sm" color="primary" wire:click="saveBugResolutionNote">Salva nota risoluzione</x-filament::button>
|
||||||
|
@if(($this->highlightedBug['bug_status'] ?? 'open') === 'resolved')
|
||||||
|
<x-filament::button size="sm" color="warning" wire:click="reopenBug('{{ $this->highlightedBug['fingerprint'] }}')">Riapri</x-filament::button>
|
||||||
|
@else
|
||||||
|
<x-filament::button size="sm" color="success" wire:click="markBugResolved('{{ $this->highlightedBug['fingerprint'] }}')">Segna risolto</x-filament::button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label class="block">
|
@else
|
||||||
<span class="mb-1 block text-[11px] font-medium">Nota</span>
|
<div class="rounded-lg border border-dashed bg-gray-50 p-3 text-[11px] text-gray-500">
|
||||||
<textarea wire:model.defer="bugResolutionNote" rows="4" class="w-full rounded-md border-gray-300 text-xs" placeholder="Es: risolto con fix parser X, migration Y, deploy Z"></textarea>
|
Nessun BUG da mettere in evidenza.
|
||||||
</label>
|
|
||||||
<div class="mt-2">
|
|
||||||
<x-filament::button size="sm" color="primary" wire:click="saveBugResolutionNote">Salva nota risoluzione</x-filament::button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
|
|
@ -561,7 +806,7 @@ class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium {{ $t
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@forelse($this->runtimeErrors as $row)
|
@forelse($this->runtimeErrors as $row)
|
||||||
<tr class="hover:bg-slate-50">
|
<tr class="hover:bg-slate-50 {{ ($row['bug_code'] ?? '') === 'BUG-0033' ? 'bg-amber-50' : '' }} {{ ($row['fingerprint'] ?? '') === ($this->selectedBugFingerprint ?? '') ? 'ring-1 ring-primary-300' : '' }}">
|
||||||
<td class="border px-2 py-1.5 font-mono font-semibold">{{ $row['bug_code'] }}</td>
|
<td class="border px-2 py-1.5 font-mono font-semibold">{{ $row['bug_code'] }}</td>
|
||||||
<td class="border px-2 py-1.5">
|
<td class="border px-2 py-1.5">
|
||||||
@if(($row['bug_status'] ?? 'open') === 'resolved')
|
@if(($row['bug_status'] ?? 'open') === 'resolved')
|
||||||
|
|
@ -573,7 +818,9 @@ class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium {{ $t
|
||||||
<td class="border px-2 py-1.5 whitespace-nowrap">{{ $row['timestamp'] }}</td>
|
<td class="border px-2 py-1.5 whitespace-nowrap">{{ $row['timestamp'] }}</td>
|
||||||
<td class="border px-2 py-1.5">{{ $row['source'] ?? '-' }}</td>
|
<td class="border px-2 py-1.5">{{ $row['source'] ?? '-' }}</td>
|
||||||
<td class="border px-2 py-1.5">{{ $row['type'] }}</td>
|
<td class="border px-2 py-1.5">{{ $row['type'] }}</td>
|
||||||
<td class="border px-2 py-1.5">{{ (int) ($row['occurrences'] ?? 1) }}</td>
|
<td class="border px-2 py-1.5">
|
||||||
|
<span class="rounded {{ (int) ($row['occurrences'] ?? 1) >= 20 ? 'bg-amber-100 text-amber-900' : 'bg-slate-100 text-slate-700' }} px-1.5 py-0.5 font-semibold">{{ (int) ($row['occurrences'] ?? 1) }}</span>
|
||||||
|
</td>
|
||||||
<td class="border px-2 py-1.5">{{ $row['message'] }}</td>
|
<td class="border px-2 py-1.5">{{ $row['message'] }}</td>
|
||||||
<td class="border px-2 py-1.5">
|
<td class="border px-2 py-1.5">
|
||||||
<div class="max-w-[420px] break-words text-[10px] text-gray-700">{{ $row['context'] !== '' ? \Illuminate\Support\Str::limit($row['context'], 220) : '-' }}</div>
|
<div class="max-w-[420px] break-words text-[10px] text-gray-700">{{ $row['context'] !== '' ? \Illuminate\Support\Str::limit($row['context'], 220) : '-' }}</div>
|
||||||
|
|
@ -585,7 +832,7 @@ class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium {{ $t
|
||||||
@endif
|
@endif
|
||||||
</td>
|
</td>
|
||||||
<td class="border px-2 py-1.5">
|
<td class="border px-2 py-1.5">
|
||||||
<x-filament::button size="xs" color="gray" type="button" wire:click="selectBugForResolution('{{ $row['fingerprint'] }}')">Nota</x-filament::button>
|
<x-filament::button size="xs" color="gray" type="button" wire:click="selectBugForResolution('{{ $row['fingerprint'] }}')">Apri nota</x-filament::button>
|
||||||
@if(($row['bug_status'] ?? 'open') === 'resolved')
|
@if(($row['bug_status'] ?? 'open') === 'resolved')
|
||||||
<x-filament::button size="xs" color="warning" type="button" wire:click="reopenBug('{{ $row['fingerprint'] }}')">Riapri</x-filament::button>
|
<x-filament::button size="xs" color="warning" type="button" wire:click="reopenBug('{{ $row['fingerprint'] }}')">Riapri</x-filament::button>
|
||||||
@else
|
@else
|
||||||
|
|
|
||||||
|
|
@ -246,6 +246,11 @@
|
||||||
|
|
||||||
<label class="block text-sm md:col-span-2">
|
<label class="block text-sm md:col-span-2">
|
||||||
<span class="mb-2 block font-medium">Acquisizione foto e allegati</span>
|
<span class="mb-2 block font-medium">Acquisizione foto e allegati</span>
|
||||||
|
@if(count($this->selectedUploads) > 0)
|
||||||
|
<div class="mb-2 inline-flex items-center rounded-full bg-emerald-100 px-3 py-1 text-[11px] font-semibold text-emerald-800">
|
||||||
|
{{ count($this->selectedUploads) }} file gi\u00e0 in coda per questo ticket
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<label class="inline-flex cursor-pointer items-center rounded-md bg-emerald-600 px-3 py-2 text-xs font-medium text-white hover:bg-emerald-500">
|
<label class="inline-flex cursor-pointer items-center rounded-md bg-emerald-600 px-3 py-2 text-xs font-medium text-white hover:bg-emerald-500">
|
||||||
Scatta foto
|
Scatta foto
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<div wire:poll.8s="refreshLiveIncomingCall" class="hidden lg:flex lg:min-w-[24rem] lg:flex-1 lg:justify-center">
|
<div wire:poll.3s="refreshLiveIncomingCall" class="hidden lg:flex lg:min-w-[24rem] lg:flex-1 lg:justify-center">
|
||||||
@if($liveIncomingCall)
|
@if($liveIncomingCall)
|
||||||
<div class="mx-3 flex w-full max-w-2xl items-center justify-between gap-3 rounded-xl border border-emerald-300 bg-emerald-50 px-4 py-2 shadow-sm">
|
<div class="mx-3 flex w-full max-w-2xl items-center justify-between gap-3 rounded-xl border border-emerald-300 bg-emerald-50 px-4 py-2 shadow-sm">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user