Consolida moduli operativi e sblocca update staging
This commit is contained in:
parent
a6dbc5d696
commit
1a468fdd78
|
|
@ -114,7 +114,7 @@ public function handle(): int
|
||||||
$codCondRaw = trim((string) ($row->cod_cond ?? ''));
|
$codCondRaw = trim((string) ($row->cod_cond ?? ''));
|
||||||
|
|
||||||
// Dedup: se E_lostesso_Di è valorizzato, usa il record master come riferimento per chiavi/identità.
|
// Dedup: se E_lostesso_Di è valorizzato, usa il record master come riferimento per chiavi/identità.
|
||||||
$masterId = (int) ($row->E_lostesso_Di ?? 0);
|
$masterId = (int) ($row->E_lostesso_Di ?? $row->e_lostesso_di ?? 0);
|
||||||
$masterRow = null;
|
$masterRow = null;
|
||||||
if ($masterId > 0) {
|
if ($masterId > 0) {
|
||||||
if (isset($condominById[$masterId])) {
|
if (isset($condominById[$masterId])) {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,5 +1,4 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
use App\Models\GestioneContabile;
|
use App\Models\GestioneContabile;
|
||||||
|
|
@ -21,23 +20,24 @@ class ImportGestioniFromLegacy extends Command
|
||||||
|
|
||||||
public function handle(): int
|
public function handle(): int
|
||||||
{
|
{
|
||||||
$stabileCode = (string)$this->option('stabile');
|
$stabileCode = (string) $this->option('stabile');
|
||||||
if (!$stabileCode) {
|
if (! $stabileCode) {
|
||||||
$this->error('Specificare --stabile=0000');
|
$this->error('Specificare --stabile=0000');
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
$stabileCode = str_pad($stabileCode, 4, '0', STR_PAD_LEFT);
|
$stabileCode = str_pad($stabileCode, 4, '0', STR_PAD_LEFT);
|
||||||
$root = rtrim((string)$this->option('root') ?: '/mnt/gescon-archives/gescon', '/');
|
$root = rtrim((string) $this->option('root') ?: '/mnt/gescon-archives/gescon', '/');
|
||||||
$tenant = (string)$this->option('tenant') ?: 'default';
|
$tenant = (string) $this->option('tenant') ?: 'default';
|
||||||
$dry = (bool)$this->option('dry-run');
|
$dry = (bool) $this->option('dry-run');
|
||||||
|
|
||||||
// Heuristics per master MDB (Stabili)
|
// Heuristics per master MDB (Stabili)
|
||||||
$stabiliCandidates = [];
|
$stabiliCandidates = [];
|
||||||
if ($this->option('stabili-mdb')) {
|
if ($this->option('stabili-mdb')) {
|
||||||
$stabiliCandidates[] = (string)$this->option('stabili-mdb');
|
$stabiliCandidates[] = (string) $this->option('stabili-mdb');
|
||||||
}
|
}
|
||||||
foreach (['cartellestabili_e_anni.mdb', 'Stabili.mdb', 'stabili.mdb'] as $n) {
|
foreach (['Stabili.mdb', 'stabili.mdb', 'cartellestabili_e_anni.mdb'] as $n) {
|
||||||
$stabiliCandidates[] = $root . '/' . $n;
|
$stabiliCandidates[] = $root . '/' . $n;
|
||||||
|
$stabiliCandidates[] = $root . '/dbc/' . $n;
|
||||||
}
|
}
|
||||||
$stabiliMdb = null;
|
$stabiliMdb = null;
|
||||||
foreach ($stabiliCandidates as $cand) {
|
foreach ($stabiliCandidates as $cand) {
|
||||||
|
|
@ -46,36 +46,42 @@ public function handle(): int
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!$stabiliMdb) {
|
if (! $stabiliMdb) {
|
||||||
$this->warn('Master MDB Stabili non trovato; continuerò usando convenzione directory.');
|
$this->warn('Master MDB Stabili non trovato; continuerò usando convenzione directory.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1) Leggi mesi da Stabili (ORD_RATA_*, RIS_RATA_*) e risolvi directory
|
// 1) Leggi mesi da Stabili (ORD_RATA_*, RIS_RATA_*) e risolvi directory
|
||||||
$ordMesi = [];
|
$ordMesi = [];
|
||||||
$risMesi = [];
|
$risMesi = [];
|
||||||
$stableDir = $root . '/' . $stabileCode;
|
$stableDir = $root . '/' . $stabileCode;
|
||||||
if ($stabiliMdb) {
|
if ($stabiliMdb) {
|
||||||
[$row, $headers] = $this->findRowInMdb($stabiliMdb, ['cod', 'codice', 'cod_stabile', 'stabile'], $stabileCode);
|
[$row, $headers] = $this->findRowInMdb($stabiliMdb, ['cod', 'codice', 'cod_stabile', 'stabile', 'nome_directory', 'codice_directory', 'internet_cod_stab'], $stabileCode);
|
||||||
if ($row) {
|
if ($row) {
|
||||||
$hmap = array_change_key_case($headers, CASE_LOWER);
|
$hmap = array_change_key_case($headers, CASE_LOWER);
|
||||||
// Estrai mesi
|
// Estrai mesi
|
||||||
foreach ($hmap as $col => $idx) {
|
foreach ($hmap as $col => $idx) {
|
||||||
if (str_starts_with($col, 'ord_rata_')) {
|
if (str_starts_with($col, 'ord_rata_')) {
|
||||||
$num = (int)preg_replace('/[^0-9]/', '', substr($col, strlen('ord_rata_')));
|
$num = (int) preg_replace('/[^0-9]/', '', substr($col, strlen('ord_rata_')));
|
||||||
if ($num >= 1 && $num <= 12) {
|
if ($num >= 1 && $num <= 12) {
|
||||||
if (self::truthy($row[$idx] ?? null)) $ordMesi[] = $num;
|
if (self::truthy($row[$idx] ?? null)) {
|
||||||
|
$ordMesi[] = $num;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
} elseif (str_starts_with($col, 'ris_rata_')) {
|
} elseif (str_starts_with($col, 'ris_rata_')) {
|
||||||
$num = (int)preg_replace('/[^0-9]/', '', substr($col, strlen('ris_rata_')));
|
$num = (int) preg_replace('/[^0-9]/', '', substr($col, strlen('ris_rata_')));
|
||||||
if ($num >= 1 && $num <= 12) {
|
if ($num >= 1 && $num <= 12) {
|
||||||
if (self::truthy($row[$idx] ?? null)) $risMesi[] = $num;
|
if (self::truthy($row[$idx] ?? null)) {
|
||||||
|
$risMesi[] = $num;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Directory se presente (campi possibili)
|
// Directory se presente (campi possibili)
|
||||||
foreach (['directory', 'dir', 'cartella', 'path', 'folder', 'dir_stabile'] as $k) {
|
foreach (['directory', 'dir', 'cartella', 'path', 'folder', 'dir_stabile', 'nome_directory', 'codice_directory', 'internet_cod_stab'] as $k) {
|
||||||
if (isset($hmap[$k])) {
|
if (isset($hmap[$k])) {
|
||||||
$val = trim((string)($row[$hmap[$k]] ?? ''));
|
$val = trim((string) ($row[$hmap[$k]] ?? ''));
|
||||||
if ($val) {
|
if ($val) {
|
||||||
$stableDir = $root . '/' . trim($val, '/');
|
$stableDir = $root . '/' . trim($val, '/');
|
||||||
break;
|
break;
|
||||||
|
|
@ -85,36 +91,39 @@ public function handle(): int
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_dir($stableDir)) {
|
if (! is_dir($stableDir)) {
|
||||||
$this->warn("Directory stabile non esiste: {$stableDir}; userò {$root}/{$stabileCode}");
|
$this->warn("Directory stabile non esiste: {$stableDir}; userò {$root}/{$stabileCode}");
|
||||||
$stableDir = $root . '/' . $stabileCode;
|
$stableDir = $root . '/' . $stabileCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Determina anni gestione da generale_stabile.mdb o fallback
|
// 2) Determina anni gestione da generale_stabile.mdb o fallback
|
||||||
$generale = $stableDir . '/generale_stabile.mdb';
|
$generale = $stableDir . '/generale_stabile.mdb';
|
||||||
$anni = [];
|
$anni = [];
|
||||||
if (is_file($generale)) {
|
if (is_file($generale)) {
|
||||||
$anni = $this->extractAnniFromGenerale($generale);
|
$anni = $this->extractAnniFromGenerale($generale);
|
||||||
}
|
}
|
||||||
if (!$anni) {
|
if (! $anni) {
|
||||||
// Fallback: prova da singolo_anno.mdb (incassi anno_rif)
|
// Fallback: prova da singolo_anno.mdb (incassi anno_rif)
|
||||||
foreach (glob($stableDir . '/*/singolo_anno.mdb') as $mdb) {
|
foreach (glob($stableDir . '/*/singolo_anno.mdb') as $mdb) {
|
||||||
$anni = array_values(array_unique(array_merge($anni, $this->extractAnniFromSingoloAnno($mdb))));
|
$anni = array_values(array_unique(array_merge($anni, $this->extractAnniFromSingoloAnno($mdb))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sort($anni);
|
sort($anni);
|
||||||
if (!$anni) {
|
if (! $anni) {
|
||||||
$this->warn('Nessun anno trovato; userò anno corrente.');
|
$this->warn('Nessun anno trovato; userò anno corrente.');
|
||||||
$anni = [(int)date('Y')];
|
$anni = [(int) date('Y')];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2b) Fallback mesi per anno da singolo_anno.mdb (tabella rate)
|
// 2b) Fallback mesi per anno da singolo_anno.mdb (tabella rate)
|
||||||
$mesiPerAnno = [];
|
$mesiPerAnno = [];
|
||||||
if (!$ordMesi || !$risMesi) {
|
if (! $ordMesi || ! $risMesi) {
|
||||||
foreach (glob($stableDir . '/*/singolo_anno.mdb') as $mdb) {
|
foreach (glob($stableDir . '/*/singolo_anno.mdb') as $mdb) {
|
||||||
$estratti = $this->extractMesiPerAnnoFromSingoloAnno($mdb);
|
$estratti = $this->extractMesiPerAnnoFromSingoloAnno($mdb);
|
||||||
foreach ($estratti as $y => $tipi) {
|
foreach ($estratti as $y => $tipi) {
|
||||||
if (!isset($mesiPerAnno[$y])) $mesiPerAnno[$y] = ['O' => [], 'R' => []];
|
if (! isset($mesiPerAnno[$y])) {
|
||||||
|
$mesiPerAnno[$y] = ['O' => [], 'R' => []];
|
||||||
|
}
|
||||||
|
|
||||||
$mesiPerAnno[$y]['O'] = array_values(array_unique(array_merge($mesiPerAnno[$y]['O'], $tipi['O'] ?? [])));
|
$mesiPerAnno[$y]['O'] = array_values(array_unique(array_merge($mesiPerAnno[$y]['O'], $tipi['O'] ?? [])));
|
||||||
$mesiPerAnno[$y]['R'] = array_values(array_unique(array_merge($mesiPerAnno[$y]['R'], $tipi['R'] ?? [])));
|
$mesiPerAnno[$y]['R'] = array_values(array_unique(array_merge($mesiPerAnno[$y]['R'], $tipi['R'] ?? [])));
|
||||||
}
|
}
|
||||||
|
|
@ -139,12 +148,12 @@ public function handle(): int
|
||||||
$updated = 0;
|
$updated = 0;
|
||||||
// Recupera Stabile record
|
// Recupera Stabile record
|
||||||
$stabileModel = Stabile::where('codice_stabile', $stabileCode)->first();
|
$stabileModel = Stabile::where('codice_stabile', $stabileCode)->first();
|
||||||
$stabileId = $stabileModel?->id;
|
$stabileId = $stabileModel?->id;
|
||||||
$forceUpdate = (bool)$this->option('force-update-months');
|
$forceUpdate = (bool) $this->option('force-update-months');
|
||||||
foreach ($anni as $anno) {
|
foreach ($anni as $anno) {
|
||||||
foreach (['ordinaria' => $ordMesi, 'riscaldamento' => $risMesi] as $tipo => $mesi) {
|
foreach (['ordinaria' => $ordMesi, 'riscaldamento' => $risMesi] as $tipo => $mesi) {
|
||||||
// Se mesi vuoti dal master, prova fallback per-anno
|
// Se mesi vuoti dal master, prova fallback per-anno
|
||||||
if (!$mesi) {
|
if (! $mesi) {
|
||||||
$mesi = $tipo === 'ordinaria'
|
$mesi = $tipo === 'ordinaria'
|
||||||
? ($mesiPerAnno[$anno]['O'] ?? [])
|
? ($mesiPerAnno[$anno]['O'] ?? [])
|
||||||
: ($mesiPerAnno[$anno]['R'] ?? []);
|
: ($mesiPerAnno[$anno]['R'] ?? []);
|
||||||
|
|
@ -156,11 +165,11 @@ public function handle(): int
|
||||||
->first();
|
->first();
|
||||||
if ($exists) {
|
if ($exists) {
|
||||||
// Aggiorna mesi se forzato o vuoti
|
// Aggiorna mesi se forzato o vuoti
|
||||||
$campo = $tipo === 'ordinaria' ? 'mesi_rate_ordinaria' : 'mesi_rate_riscaldamento';
|
$campo = $tipo === 'ordinaria' ? 'mesi_rate_ordinaria' : 'mesi_rate_riscaldamento';
|
||||||
$valEsist = (array)($exists->{$campo} ?? []);
|
$valEsist = (array) ($exists->{$campo} ?? []);
|
||||||
if ($forceUpdate || (empty($valEsist) && !empty($mesi))) {
|
if ($forceUpdate || (empty($valEsist) && ! empty($mesi))) {
|
||||||
$exists->{$campo} = array_values(array_unique(array_map('intval', $mesi)));
|
$exists->{$campo} = array_values(array_unique(array_map('intval', $mesi)));
|
||||||
if (!$dry) {
|
if (! $dry) {
|
||||||
$exists->save();
|
$exists->save();
|
||||||
}
|
}
|
||||||
$updated++;
|
$updated++;
|
||||||
|
|
@ -169,15 +178,15 @@ public function handle(): int
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$gestione = new GestioneContabile();
|
$gestione = new GestioneContabile();
|
||||||
$gestione->tenant_id = $tenant;
|
$gestione->tenant_id = $tenant;
|
||||||
$gestione->stabile_id = $stabileId;
|
$gestione->stabile_id = $stabileId;
|
||||||
$gestione->anno_gestione = $anno;
|
$gestione->anno_gestione = $anno;
|
||||||
$gestione->tipo_gestione = $tipo;
|
$gestione->tipo_gestione = $tipo;
|
||||||
$gestione->denominazione = ucfirst($tipo) . ' ' . $anno;
|
$gestione->denominazione = ucfirst($tipo) . ' ' . $anno;
|
||||||
$gestione->data_inizio = sprintf('%04d-01-01', $anno);
|
$gestione->data_inizio = sprintf('%04d-01-01', $anno);
|
||||||
$gestione->data_fine = sprintf('%04d-12-31', $anno);
|
$gestione->data_fine = sprintf('%04d-12-31', $anno);
|
||||||
$gestione->stato = 'aperta';
|
$gestione->stato = 'aperta';
|
||||||
$gestione->protocollo_prefix = strtoupper(substr($tipo, 0, 1)) . $anno;
|
$gestione->protocollo_prefix = strtoupper(substr($tipo, 0, 1)) . $anno;
|
||||||
$gestione->ultimo_protocollo = 0;
|
$gestione->ultimo_protocollo = 0;
|
||||||
if ($tipo === 'ordinaria') {
|
if ($tipo === 'ordinaria') {
|
||||||
|
|
@ -186,9 +195,9 @@ public function handle(): int
|
||||||
if ($tipo === 'riscaldamento') {
|
if ($tipo === 'riscaldamento') {
|
||||||
$gestione->mesi_rate_riscaldamento = array_values(array_unique(array_map('intval', $mesi)));
|
$gestione->mesi_rate_riscaldamento = array_values(array_unique(array_map('intval', $mesi)));
|
||||||
}
|
}
|
||||||
$gestione->gestione_attiva = true;
|
$gestione->gestione_attiva = true;
|
||||||
$gestione->codice_archivio_legacy = $stableDir;
|
$gestione->codice_archivio_legacy = $stableDir;
|
||||||
if (!$dry) {
|
if (! $dry) {
|
||||||
$gestione->save();
|
$gestione->save();
|
||||||
}
|
}
|
||||||
$created++;
|
$created++;
|
||||||
|
|
@ -201,8 +210,8 @@ public function handle(): int
|
||||||
|
|
||||||
private static function truthy($v): bool
|
private static function truthy($v): bool
|
||||||
{
|
{
|
||||||
$s = strtolower(trim((string)$v));
|
$s = strtolower(trim((string) $v));
|
||||||
return $s !== '' && !in_array($s, ['0', 'no', 'false', 'off', 'n'], true);
|
return $s !== '' && ! in_array($s, ['0', 'no', 'false', 'off', 'n'], true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -212,23 +221,35 @@ private static function truthy($v): bool
|
||||||
private function findRowInMdb(string $mdbPath, array $keys, string $value): array
|
private function findRowInMdb(string $mdbPath, array $keys, string $value): array
|
||||||
{
|
{
|
||||||
$table = $this->resolveTable($mdbPath, ['stabili', 'Stabili', 'STABILI']);
|
$table = $this->resolveTable($mdbPath, ['stabili', 'Stabili', 'STABILI']);
|
||||||
if (!$table) return [null, []];
|
if (! $table) {
|
||||||
|
return [null, []];
|
||||||
|
}
|
||||||
|
|
||||||
$csv = $this->mdbExport($mdbPath, $table);
|
$csv = $this->mdbExport($mdbPath, $table);
|
||||||
if ($csv === null) return [null, []];
|
if ($csv === null) {
|
||||||
|
return [null, []];
|
||||||
|
}
|
||||||
|
|
||||||
$fh = fopen('php://temp', 'r+');
|
$fh = fopen('php://temp', 'r+');
|
||||||
fwrite($fh, $csv);
|
fwrite($fh, $csv);
|
||||||
rewind($fh);
|
rewind($fh);
|
||||||
$header = fgetcsv($fh);
|
$header = fgetcsv($fh);
|
||||||
if (!$header) return [null, []];
|
if (! $header) {
|
||||||
|
return [null, []];
|
||||||
|
}
|
||||||
|
|
||||||
$hmap = [];
|
$hmap = [];
|
||||||
foreach ($header as $i => $h) {
|
foreach ($header as $i => $h) {
|
||||||
$hmap[strtolower(trim((string)$h))] = $i;
|
$hmap[strtolower(trim((string) $h))] = $i;
|
||||||
}
|
}
|
||||||
while (($row = fgetcsv($fh)) !== false) {
|
while (($row = fgetcsv($fh)) !== false) {
|
||||||
foreach ($keys as $k) {
|
foreach ($keys as $k) {
|
||||||
$i = $hmap[strtolower($k)] ?? null;
|
$i = $hmap[strtolower($k)] ?? null;
|
||||||
if ($i === null) continue;
|
if ($i === null) {
|
||||||
if (str_pad(trim((string)($row[$i] ?? '')), 4, '0', STR_PAD_LEFT) === $value) {
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_pad(trim((string) ($row[$i] ?? '')), 4, '0', STR_PAD_LEFT) === $value) {
|
||||||
return [$row, $hmap];
|
return [$row, $hmap];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -240,25 +261,34 @@ private function extractAnniFromGenerale(string $mdbPath): array
|
||||||
{
|
{
|
||||||
$years = [];
|
$years = [];
|
||||||
$table = $this->resolveTable($mdbPath, ['gestioni', 'gestione', 'esercizi', 'Esercizi', 'Anni']);
|
$table = $this->resolveTable($mdbPath, ['gestioni', 'gestione', 'esercizi', 'Esercizi', 'Anni']);
|
||||||
if (!$table) return $years;
|
if (! $table) {
|
||||||
|
return $years;
|
||||||
|
}
|
||||||
|
|
||||||
$csv = $this->mdbExport($mdbPath, $table);
|
$csv = $this->mdbExport($mdbPath, $table);
|
||||||
if ($csv === null) return $years;
|
if ($csv === null) {
|
||||||
|
return $years;
|
||||||
|
}
|
||||||
|
|
||||||
$fh = fopen('php://temp', 'r+');
|
$fh = fopen('php://temp', 'r+');
|
||||||
fwrite($fh, $csv);
|
fwrite($fh, $csv);
|
||||||
rewind($fh);
|
rewind($fh);
|
||||||
$header = fgetcsv($fh);
|
$header = fgetcsv($fh);
|
||||||
if (!$header) return $years;
|
if (! $header) {
|
||||||
|
return $years;
|
||||||
|
}
|
||||||
|
|
||||||
$hmap = [];
|
$hmap = [];
|
||||||
foreach ($header as $i => $h) {
|
foreach ($header as $i => $h) {
|
||||||
$hmap[strtolower(trim((string)$h))] = $i;
|
$hmap[strtolower(trim((string) $h))] = $i;
|
||||||
}
|
}
|
||||||
while (($row = fgetcsv($fh)) !== false) {
|
while (($row = fgetcsv($fh)) !== false) {
|
||||||
$y = null;
|
$y = null;
|
||||||
foreach (['anno', 'esercizio', 'str_anno'] as $k) {
|
foreach (['anno', 'esercizio', 'str_anno'] as $k) {
|
||||||
if (isset($hmap[$k])) {
|
if (isset($hmap[$k])) {
|
||||||
$val = trim((string)($row[$hmap[$k]] ?? ''));
|
$val = trim((string) ($row[$hmap[$k]] ?? ''));
|
||||||
if ($val !== '' && is_numeric($val)) {
|
if ($val !== '' && is_numeric($val)) {
|
||||||
$y = (int)$val;
|
$y = (int) $val;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -266,9 +296,9 @@ private function extractAnniFromGenerale(string $mdbPath): array
|
||||||
if ($y === null) {
|
if ($y === null) {
|
||||||
foreach (['data_inizio', 'inizio', 'dal'] as $k) {
|
foreach (['data_inizio', 'inizio', 'dal'] as $k) {
|
||||||
if (isset($hmap[$k])) {
|
if (isset($hmap[$k])) {
|
||||||
$val = trim((string)($row[$hmap[$k]] ?? ''));
|
$val = trim((string) ($row[$hmap[$k]] ?? ''));
|
||||||
if ($val && preg_match('/^(\d{4})-/', $val, $m)) {
|
if ($val && preg_match('/^(\d{4})-/', $val, $m)) {
|
||||||
$y = (int)$m[1];
|
$y = (int) $m[1];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -284,33 +314,42 @@ private function extractAnniFromGenerale(string $mdbPath): array
|
||||||
private function extractAnniFromSingoloAnno(string $mdbPath): array
|
private function extractAnniFromSingoloAnno(string $mdbPath): array
|
||||||
{
|
{
|
||||||
$table = $this->resolveTable($mdbPath, ['incassi', 'Incassi']);
|
$table = $this->resolveTable($mdbPath, ['incassi', 'Incassi']);
|
||||||
if (!$table) return [];
|
if (! $table) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
$csv = $this->mdbExport($mdbPath, $table);
|
$csv = $this->mdbExport($mdbPath, $table);
|
||||||
if ($csv === null) return [];
|
if ($csv === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
$fh = fopen('php://temp', 'r+');
|
$fh = fopen('php://temp', 'r+');
|
||||||
fwrite($fh, $csv);
|
fwrite($fh, $csv);
|
||||||
rewind($fh);
|
rewind($fh);
|
||||||
$header = fgetcsv($fh);
|
$header = fgetcsv($fh);
|
||||||
if (!$header) return [];
|
if (! $header) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
$hmap = [];
|
$hmap = [];
|
||||||
foreach ($header as $i => $h) {
|
foreach ($header as $i => $h) {
|
||||||
$hmap[strtolower(trim((string)$h))] = $i;
|
$hmap[strtolower(trim((string) $h))] = $i;
|
||||||
}
|
}
|
||||||
$years = [];
|
$years = [];
|
||||||
while (($row = fgetcsv($fh)) !== false) {
|
while (($row = fgetcsv($fh)) !== false) {
|
||||||
$ar = $hmap['anno_rif'] ?? null;
|
$ar = $hmap['anno_rif'] ?? null;
|
||||||
if ($ar !== null) {
|
if ($ar !== null) {
|
||||||
$val = trim((string)($row[$ar] ?? ''));
|
$val = trim((string) ($row[$ar] ?? ''));
|
||||||
if ($val && is_numeric($val)) {
|
if ($val && is_numeric($val)) {
|
||||||
$years[] = (int)$val;
|
$years[] = (int) $val;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$dt = $hmap['dt_empag'] ?? null;
|
$dt = $hmap['dt_empag'] ?? null;
|
||||||
if ($dt !== null) {
|
if ($dt !== null) {
|
||||||
$val = trim((string)($row[$dt] ?? ''));
|
$val = trim((string) ($row[$dt] ?? ''));
|
||||||
if ($val && preg_match('/^(\d{4})-/', $val, $m)) {
|
if ($val && preg_match('/^(\d{4})-/', $val, $m)) {
|
||||||
$years[] = (int)$m[1];
|
$years[] = (int) $m[1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -319,19 +358,28 @@ private function extractAnniFromSingoloAnno(string $mdbPath): array
|
||||||
|
|
||||||
private function resolveTable(string $mdbPath, array $candidates): ?string
|
private function resolveTable(string $mdbPath, array $candidates): ?string
|
||||||
{
|
{
|
||||||
$bin = trim((string)@shell_exec('command -v mdb-tables')) ?: '/usr/bin/mdb-tables';
|
$bin = trim((string) @shell_exec('command -v mdb-tables')) ?: '/usr/bin/mdb-tables';
|
||||||
$list = @shell_exec(escapeshellcmd($bin) . ' -1 ' . escapeshellarg($mdbPath));
|
$list = @shell_exec(escapeshellcmd($bin) . ' -1 ' . escapeshellarg($mdbPath));
|
||||||
if (!$list) return null;
|
if (! $list) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
$names = array_filter(array_map('trim', explode("\n", $list)));
|
$names = array_filter(array_map('trim', explode("\n", $list)));
|
||||||
foreach ($candidates as $c) {
|
foreach ($candidates as $c) {
|
||||||
foreach ($names as $n) {
|
foreach ($names as $n) {
|
||||||
if (strcasecmp($n, $c) === 0) return $n;
|
if (strcasecmp($n, $c) === 0) {
|
||||||
|
return $n;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// fallback: first contains substring
|
// fallback: first contains substring
|
||||||
foreach ($candidates as $c) {
|
foreach ($candidates as $c) {
|
||||||
foreach ($names as $n) {
|
foreach ($names as $n) {
|
||||||
if (stripos($n, $c) !== false) return $n;
|
if (stripos($n, $c) !== false) {
|
||||||
|
return $n;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -339,51 +387,69 @@ private function resolveTable(string $mdbPath, array $candidates): ?string
|
||||||
|
|
||||||
private function mdbExport(string $mdbPath, string $table): ?string
|
private function mdbExport(string $mdbPath, string $table): ?string
|
||||||
{
|
{
|
||||||
$bin = trim((string)@shell_exec('command -v mdb-export')) ?: '/usr/bin/mdb-export';
|
$bin = trim((string) @shell_exec('command -v mdb-export')) ?: '/usr/bin/mdb-export';
|
||||||
$p = new Process([$bin, '-D', '%Y-%m-%d', $mdbPath, $table]);
|
$p = new Process([$bin, '-D', '%Y-%m-%d', $mdbPath, $table]);
|
||||||
$p->setTimeout(120);
|
$p->setTimeout(120);
|
||||||
$p->run();
|
$p->run();
|
||||||
if (!$p->isSuccessful()) return null;
|
if (! $p->isSuccessful()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return $p->getOutput();
|
return $p->getOutput();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Estrae mesi per anno dalla tabella rate del singolo_anno.mdb: ritorna [anno=>['O'=>[mesi],'R'=>[mesi]]] */
|
/** Estrae mesi per anno dalla tabella rate del singolo_anno.mdb: ritorna [anno=>['O'=>[mesi],'R'=>[mesi]]] */
|
||||||
private function extractMesiPerAnnoFromSingoloAnno(string $mdbPath): array
|
private function extractMesiPerAnnoFromSingoloAnno(string $mdbPath): array
|
||||||
{
|
{
|
||||||
$out = [];
|
$out = [];
|
||||||
$table = $this->resolveTable($mdbPath, ['rate', 'Rate']);
|
$table = $this->resolveTable($mdbPath, ['rate', 'Rate']);
|
||||||
if (!$table) return $out;
|
if (! $table) {
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
$csv = $this->mdbExport($mdbPath, $table);
|
$csv = $this->mdbExport($mdbPath, $table);
|
||||||
if ($csv === null) return $out;
|
if ($csv === null) {
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
$fh = fopen('php://temp', 'r+');
|
$fh = fopen('php://temp', 'r+');
|
||||||
fwrite($fh, $csv);
|
fwrite($fh, $csv);
|
||||||
rewind($fh);
|
rewind($fh);
|
||||||
$header = fgetcsv($fh);
|
$header = fgetcsv($fh);
|
||||||
if (!$header) return $out;
|
if (! $header) {
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
$hmap = [];
|
$hmap = [];
|
||||||
foreach ($header as $i => $h) {
|
foreach ($header as $i => $h) {
|
||||||
$hmap[strtolower(trim((string)$h))] = $i;
|
$hmap[strtolower(trim((string) $h))] = $i;
|
||||||
}
|
}
|
||||||
while (($row = fgetcsv($fh)) !== false) {
|
while (($row = fgetcsv($fh)) !== false) {
|
||||||
$tipo = null;
|
$tipo = null;
|
||||||
if (isset($hmap['o_r_s'])) {
|
if (isset($hmap['o_r_s'])) {
|
||||||
$tipo = strtoupper(trim((string)($row[$hmap['o_r_s']] ?? '')));
|
$tipo = strtoupper(trim((string) ($row[$hmap['o_r_s']] ?? '')));
|
||||||
}
|
}
|
||||||
if (!in_array($tipo, ['O', 'R'], true)) continue;
|
if (! in_array($tipo, ['O', 'R'], true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$mese = null;
|
$mese = null;
|
||||||
if (isset($hmap['n_mese'])) {
|
if (isset($hmap['n_mese'])) {
|
||||||
$val = trim((string)($row[$hmap['n_mese']] ?? ''));
|
$val = trim((string) ($row[$hmap['n_mese']] ?? ''));
|
||||||
if ($val !== '' && is_numeric($val)) {
|
if ($val !== '' && is_numeric($val)) {
|
||||||
$mese = max(1, min(12, (int)$val));
|
$mese = max(1, min(12, (int) $val));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($mese === null) continue;
|
if ($mese === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$anno = null;
|
$anno = null;
|
||||||
foreach (['str_anno'] as $k) {
|
foreach (['str_anno'] as $k) {
|
||||||
if (isset($hmap[$k])) {
|
if (isset($hmap[$k])) {
|
||||||
$s = trim((string)($row[$hmap[$k]] ?? ''));
|
$s = trim((string) ($row[$hmap[$k]] ?? ''));
|
||||||
if ($s && preg_match('/(\d{4})/', $s, $m)) {
|
if ($s && preg_match('/(\d{4})/', $s, $m)) {
|
||||||
$anno = (int)$m[1];
|
$anno = (int) $m[1];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -391,16 +457,19 @@ private function extractMesiPerAnnoFromSingoloAnno(string $mdbPath): array
|
||||||
if ($anno === null) {
|
if ($anno === null) {
|
||||||
foreach (['dt_empag', 'dt1_da', 'data_emissione'] as $k) {
|
foreach (['dt_empag', 'dt1_da', 'data_emissione'] as $k) {
|
||||||
if (isset($hmap[$k])) {
|
if (isset($hmap[$k])) {
|
||||||
$s = trim((string)($row[$hmap[$k]] ?? ''));
|
$s = trim((string) ($row[$hmap[$k]] ?? ''));
|
||||||
if ($s && preg_match('/^(\d{4})-/', $s, $m)) {
|
if ($s && preg_match('/^(\d{4})-/', $s, $m)) {
|
||||||
$anno = (int)$m[1];
|
$anno = (int) $m[1];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($anno === null) continue;
|
if ($anno === null) {
|
||||||
if (!isset($out[$anno])) {
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! isset($out[$anno])) {
|
||||||
$out[$anno] = ['O' => [], 'R' => []];
|
$out[$anno] = ['O' => [], 'R' => []];
|
||||||
}
|
}
|
||||||
if ($tipo === 'O') {
|
if ($tipo === 'O') {
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Symfony\Component\Process\Process;
|
use Symfony\Component\Process\Process;
|
||||||
|
|
||||||
class LoadGesconMdbToStaging extends Command
|
class LoadGesconMdbToStaging extends Command
|
||||||
{
|
{
|
||||||
protected $signature = 'gescon:load-mdb
|
protected $signature = 'gescon:load-mdb
|
||||||
{--mdb= : Percorso al file singolo_anno.mdb}
|
{--mdb= : Percorso al file singolo_anno.mdb}
|
||||||
{--table=condomin : Nome tabella MDB da caricare (es. condomin|condomini)}
|
{--table=condomin : Nome tabella MDB da caricare (es. condomin|condomini)}
|
||||||
{--stabile= : Codice stabile (es. 0021) per popolamento cod_stabile}
|
{--stabile= : Codice stabile (es. 0021) per popolamento cod_stabile}
|
||||||
|
|
@ -48,46 +47,61 @@ private function ensureAnagCasseTable(): void
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function ensureCondominColumns(): void
|
||||||
|
{
|
||||||
|
if (! Schema::connection('gescon_import')->hasTable('condomin')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alcuni ambienti hanno gia' la tabella al limite della row size.
|
||||||
|
// I campi storici opzionali vanno gestiti come best-effort a valle,
|
||||||
|
// senza forzare ALTER TABLE durante il reload della staging.
|
||||||
|
}
|
||||||
|
|
||||||
public function handle(): int
|
public function handle(): int
|
||||||
{
|
{
|
||||||
$mdb = $this->option('mdb');
|
$mdb = $this->option('mdb');
|
||||||
$table = $this->option('table') ?: 'condomin';
|
$table = $this->option('table') ?: 'condomin';
|
||||||
$limit = $this->option('limit') ? (int)$this->option('limit') : null;
|
$limit = $this->option('limit') ? (int) $this->option('limit') : null;
|
||||||
$stabile = $this->option('stabile');
|
$stabile = $this->option('stabile');
|
||||||
$legacyYear = $this->option('legacy-year');
|
$legacyYear = $this->option('legacy-year');
|
||||||
if (!$mdb || !is_file($mdb)) {
|
if (! $mdb || ! is_file($mdb)) {
|
||||||
$this->error('Specifica --mdb con path valido al file .mdb');
|
$this->error('Specifica --mdb con path valido al file .mdb');
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
// Consenti alias comuni per tabella di staging
|
// Consenti alias comuni per tabella di staging
|
||||||
$stagingTable = $table;
|
$stagingTable = $table;
|
||||||
$tableLower = strtolower((string) $table);
|
$tableLower = strtolower((string) $table);
|
||||||
if (in_array($tableLower, ['anag_casse', 'anagr_casse', 'anagcasse', 'anagrcasse'], true)) {
|
if (in_array($tableLower, ['anag_casse', 'anagr_casse', 'anagcasse', 'anagrcasse'], true)) {
|
||||||
$stagingTable = 'anag_casse';
|
$stagingTable = 'anag_casse';
|
||||||
$this->ensureAnagCasseTable();
|
$this->ensureAnagCasseTable();
|
||||||
}
|
}
|
||||||
if (!Schema::connection('gescon_import')->hasTable($stagingTable)) {
|
if (in_array($tableLower, ['condomin', 'condomini'], true)) {
|
||||||
|
$stagingTable = 'condomin';
|
||||||
|
$this->ensureCondominColumns();
|
||||||
|
}
|
||||||
|
if (! Schema::connection('gescon_import')->hasTable($stagingTable)) {
|
||||||
$aliases = [
|
$aliases = [
|
||||||
'condomini' => 'condomin',
|
'condomini' => 'condomin',
|
||||||
'condomin' => 'condomini',
|
'condomin' => 'condomini',
|
||||||
'anag_casse' => 'anag_casse',
|
'anag_casse' => 'anag_casse',
|
||||||
'Anag_casse' => 'anag_casse',
|
'Anag_casse' => 'anag_casse',
|
||||||
'Anagr_casse' => 'anag_casse',
|
'Anagr_casse' => 'anag_casse',
|
||||||
'anagr_casse' => 'anag_casse',
|
'anagr_casse' => 'anag_casse',
|
||||||
'operazioni' => 'operazioni',
|
'operazioni' => 'operazioni',
|
||||||
'incassi' => 'incassi',
|
'incassi' => 'incassi',
|
||||||
'rate' => 'rate',
|
'rate' => 'rate',
|
||||||
'fornitori' => 'mdb_fornitori',
|
'fornitori' => 'mdb_fornitori',
|
||||||
'Fornitori' => 'mdb_fornitori',
|
'Fornitori' => 'mdb_fornitori',
|
||||||
'tabelle' => 'tabelle_millesimali',
|
'tabelle' => 'tabelle_millesimali',
|
||||||
'TABELLE' => 'tabelle_millesimali',
|
'TABELLE' => 'tabelle_millesimali',
|
||||||
'Fraz_gen' => 'fraz_gen',
|
'Fraz_gen' => 'fraz_gen',
|
||||||
'fraz_gen' => 'fraz_gen',
|
'fraz_gen' => 'fraz_gen',
|
||||||
'fraz_dett' => 'fraz_dett',
|
'fraz_dett' => 'fraz_dett',
|
||||||
'emes_det' => Schema::connection('gescon_import')->hasTable('rate_emissioni_dettaglio')
|
'emes_det' => Schema::connection('gescon_import')->hasTable('rate_emissioni_dettaglio')
|
||||||
? 'rate_emissioni_dettaglio'
|
? 'rate_emissioni_dettaglio'
|
||||||
: 'rate_dettaglio_gescon',
|
: 'rate_dettaglio_gescon',
|
||||||
'emes_gen' => Schema::connection('gescon_import')->hasTable('rate_emissioni')
|
'emes_gen' => Schema::connection('gescon_import')->hasTable('rate_emissioni')
|
||||||
? 'rate_emissioni'
|
? 'rate_emissioni'
|
||||||
: 'rate_generale_gescon',
|
: 'rate_generale_gescon',
|
||||||
];
|
];
|
||||||
|
|
@ -97,7 +111,7 @@ public function handle(): int
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!Schema::connection('gescon_import')->hasTable($stagingTable)) {
|
if (! Schema::connection('gescon_import')->hasTable($stagingTable)) {
|
||||||
$this->error("Tabella di staging inesistente: {$table}");
|
$this->error("Tabella di staging inesistente: {$table}");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
@ -112,15 +126,15 @@ public function handle(): int
|
||||||
if ($stabile && Schema::hasTable('amministratori')) {
|
if ($stabile && Schema::hasTable('amministratori')) {
|
||||||
$amm = DB::table('amministratori')->first();
|
$amm = DB::table('amministratori')->first();
|
||||||
if ($amm) {
|
if ($amm) {
|
||||||
$base = getenv('HOME') ?: '/home/' . trim((string)shell_exec('whoami'));
|
$base = getenv('HOME') ?: '/home/' . trim((string) shell_exec('whoami'));
|
||||||
$root = rtrim($base, '/') . '/netgescon';
|
$root = rtrim($base, '/') . '/netgescon';
|
||||||
$code = $amm->codice_amministratore ?? $amm->codice_univoco ?? ('AMM' . str_pad((string)$amm->id, 4, '0', STR_PAD_LEFT));
|
$code = $amm->codice_amministratore ?? $amm->codice_univoco ?? ('AMM' . str_pad((string) $amm->id, 4, '0', STR_PAD_LEFT));
|
||||||
$adminDir = $root . '/' . $code;
|
$adminDir = $root . '/' . $code;
|
||||||
if (empty($amm->cartella_dati)) {
|
if (empty($amm->cartella_dati)) {
|
||||||
@mkdir($adminDir, 0755, true);
|
@mkdir($adminDir, 0755, true);
|
||||||
DB::table('amministratori')->where('id', $amm->id)->update([
|
DB::table('amministratori')->where('id', $amm->id)->update([
|
||||||
'cartella_dati' => $adminDir,
|
'cartella_dati' => $adminDir,
|
||||||
'updated_at' => now(),
|
'updated_at' => now(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -131,8 +145,8 @@ public function handle(): int
|
||||||
// Nome tabella MDB corretto (case sensitive)
|
// Nome tabella MDB corretto (case sensitive)
|
||||||
$tableEffective = $table;
|
$tableEffective = $table;
|
||||||
try {
|
try {
|
||||||
$binTables = trim((string)shell_exec('command -v mdb-tables')) ?: '/usr/bin/mdb-tables';
|
$binTables = trim((string) shell_exec('command -v mdb-tables')) ?: '/usr/bin/mdb-tables';
|
||||||
$list = @shell_exec(escapeshellcmd($binTables) . ' -1 ' . escapeshellarg($mdb));
|
$list = @shell_exec(escapeshellcmd($binTables) . ' -1 ' . escapeshellarg($mdb));
|
||||||
if ($list) {
|
if ($list) {
|
||||||
foreach (array_filter(array_map('trim', explode("\n", $list))) as $nm) {
|
foreach (array_filter(array_map('trim', explode("\n", $list))) as $nm) {
|
||||||
if (strcasecmp($nm, $table) === 0) {
|
if (strcasecmp($nm, $table) === 0) {
|
||||||
|
|
@ -187,16 +201,16 @@ public function handle(): int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Estrai CSV con header
|
// Estrai CSV con header
|
||||||
$binExport = trim((string)shell_exec('command -v mdb-export')) ?: '/usr/bin/mdb-export';
|
$binExport = trim((string) shell_exec('command -v mdb-export')) ?: '/usr/bin/mdb-export';
|
||||||
$process = new Process([$binExport, '-D', '%Y-%m-%d', $mdb, $tableEffective]);
|
$process = new Process([$binExport, '-D', '%Y-%m-%d', $mdb, $tableEffective]);
|
||||||
$process->setTimeout(120);
|
$process->setTimeout(120);
|
||||||
$process->run();
|
$process->run();
|
||||||
if (!$process->isSuccessful()) {
|
if (! $process->isSuccessful()) {
|
||||||
$this->error('mdb-export fallito: ' . $process->getErrorOutput());
|
$this->error('mdb-export fallito: ' . $process->getErrorOutput());
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
$csv = $process->getOutput();
|
$csv = $process->getOutput();
|
||||||
if (!$csv) {
|
if (! $csv) {
|
||||||
$this->warn('Nessun dato esportato.');
|
$this->warn('Nessun dato esportato.');
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -205,12 +219,12 @@ public function handle(): int
|
||||||
fwrite($fh, $csv);
|
fwrite($fh, $csv);
|
||||||
rewind($fh);
|
rewind($fh);
|
||||||
$rawHeader = fgetcsv($fh);
|
$rawHeader = fgetcsv($fh);
|
||||||
if (!$rawHeader) {
|
if (! $rawHeader) {
|
||||||
$this->warn('Header CSV non letto.');
|
$this->warn('Header CSV non letto.');
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
$header = array_map(fn($h) => strtolower(trim((string)$h)), $rawHeader);
|
$header = array_map(fn($h) => strtolower(trim((string) $h)), $rawHeader);
|
||||||
$cols = Schema::connection('gescon_import')->getColumnListing($stagingTable);
|
$cols = Schema::connection('gescon_import')->getColumnListing($stagingTable);
|
||||||
$colMap = [];
|
$colMap = [];
|
||||||
foreach ($cols as $c) {
|
foreach ($cols as $c) {
|
||||||
$colMap[strtolower($c)] = $c;
|
$colMap[strtolower($c)] = $c;
|
||||||
|
|
@ -218,21 +232,39 @@ public function handle(): int
|
||||||
$hasColumn = fn(string $c) => in_array($c, $cols, true);
|
$hasColumn = fn(string $c) => in_array($c, $cols, true);
|
||||||
|
|
||||||
$normalizeDate = function ($v) {
|
$normalizeDate = function ($v) {
|
||||||
if ($v === null || $v === '') return null;
|
if ($v === null || $v === '') {
|
||||||
$s = trim((string)$v);
|
return null;
|
||||||
if (preg_match('/^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$/', $s)) return $s;
|
}
|
||||||
|
|
||||||
|
$s = trim((string) $v);
|
||||||
|
if (preg_match('/^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$/', $s)) {
|
||||||
|
return $s;
|
||||||
|
}
|
||||||
|
|
||||||
$dt = \DateTime::createFromFormat('m/d/y H:i:s', $s) ?: \DateTime::createFromFormat('d/m/y H:i:s', $s);
|
$dt = \DateTime::createFromFormat('m/d/y H:i:s', $s) ?: \DateTime::createFromFormat('d/m/y H:i:s', $s);
|
||||||
if ($dt) return $dt->format('Y-m-d H:i:s');
|
if ($dt) {
|
||||||
|
return $dt->format('Y-m-d H:i:s');
|
||||||
|
}
|
||||||
|
|
||||||
$dt = \DateTime::createFromFormat('m/d/Y', $s) ?: \DateTime::createFromFormat('d/m/Y', $s);
|
$dt = \DateTime::createFromFormat('m/d/Y', $s) ?: \DateTime::createFromFormat('d/m/Y', $s);
|
||||||
if ($dt) return $dt->format('Y-m-d');
|
if ($dt) {
|
||||||
|
return $dt->format('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
$ts = @strtotime($s);
|
$ts = @strtotime($s);
|
||||||
return $ts ? date('Y-m-d H:i:s', $ts) : null;
|
return $ts ? date('Y-m-d H:i:s', $ts) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
$normalizeNumber = function ($v) {
|
$normalizeNumber = function ($v) {
|
||||||
if ($v === null) return null;
|
if ($v === null) {
|
||||||
$s = trim((string)$v);
|
return null;
|
||||||
if ($s === '' || $s === '- - -' || $s === '---') return null;
|
}
|
||||||
|
|
||||||
|
$s = trim((string) $v);
|
||||||
|
if ($s === '' || $s === '- - -' || $s === '---') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
$s = str_replace(["\r", "\n", "\t"], '', $s);
|
$s = str_replace(["\r", "\n", "\t"], '', $s);
|
||||||
$s = str_replace(' ', '', $s);
|
$s = str_replace(' ', '', $s);
|
||||||
$s = str_replace('.', '', $s);
|
$s = str_replace('.', '', $s);
|
||||||
|
|
@ -240,12 +272,18 @@ public function handle(): int
|
||||||
return is_numeric($s) ? (float) $s : null;
|
return is_numeric($s) ? (float) $s : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
$count = 0;
|
$count = 0;
|
||||||
$failCount = 0;
|
$failCount = 0;
|
||||||
$firstError = null;
|
$firstError = null;
|
||||||
while (($row = fgetcsv($fh)) !== false) {
|
while (($row = fgetcsv($fh)) !== false) {
|
||||||
if ($row === [null]) continue;
|
if ($row === [null]) {
|
||||||
if ($limit && $count >= $limit) break;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($limit && $count >= $limit) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
$assoc = [];
|
$assoc = [];
|
||||||
foreach ($header as $i => $col) {
|
foreach ($header as $i => $col) {
|
||||||
$assoc[$col] = $row[$i] ?? null;
|
$assoc[$col] = $row[$i] ?? null;
|
||||||
|
|
@ -257,12 +295,17 @@ public function handle(): int
|
||||||
$filtered[$colMap[$key]] = $v;
|
$filtered[$colMap[$key]] = $v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($stabile && $hasColumn('cod_stabile')) $filtered['cod_stabile'] = $stabile;
|
if ($stabile && $hasColumn('cod_stabile')) {
|
||||||
if ($legacyYear && $hasColumn('legacy_year')) $filtered['legacy_year'] = (string)$legacyYear;
|
$filtered['cod_stabile'] = $stabile;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($legacyYear && $hasColumn('legacy_year')) {
|
||||||
|
$filtered['legacy_year'] = (string) $legacyYear;
|
||||||
|
}
|
||||||
|
|
||||||
if (strtolower($stagingTable) === 'mdb_fornitori') {
|
if (strtolower($stagingTable) === 'mdb_fornitori') {
|
||||||
if ($hasColumn('legacy_key') && empty($filtered['legacy_key'])) {
|
if ($hasColumn('legacy_key') && empty($filtered['legacy_key'])) {
|
||||||
$filtered['legacy_key'] = trim((string)($assoc['cod_forn'] ?? $assoc['id_fornitore'] ?? ''));
|
$filtered['legacy_key'] = trim((string) ($assoc['cod_forn'] ?? $assoc['id_fornitore'] ?? ''));
|
||||||
}
|
}
|
||||||
if ($hasColumn('row_hash') && empty($filtered['row_hash'])) {
|
if ($hasColumn('row_hash') && empty($filtered['row_hash'])) {
|
||||||
$seed = json_encode([
|
$seed = json_encode([
|
||||||
|
|
@ -309,7 +352,7 @@ public function handle(): int
|
||||||
$filtered['indirizzo_corrispondenza'] = trim((string) ($assoc['ind'] ?? $assoc['indirizzo_corrispondenza'] ?? null)) ?: null;
|
$filtered['indirizzo_corrispondenza'] = trim((string) ($assoc['ind'] ?? $assoc['indirizzo_corrispondenza'] ?? null)) ?: null;
|
||||||
}
|
}
|
||||||
if ($hasColumn('inquilino') && empty($filtered['inquilino'])) {
|
if ($hasColumn('inquilino') && empty($filtered['inquilino'])) {
|
||||||
$filtered['inquilino'] = !empty($assoc['inquil_nome']) ? 1 : 0;
|
$filtered['inquilino'] = ! empty($assoc['inquil_nome']) ? 1 : 0;
|
||||||
}
|
}
|
||||||
if ($hasColumn('proprietario') && empty($filtered['proprietario'])) {
|
if ($hasColumn('proprietario') && empty($filtered['proprietario'])) {
|
||||||
$filtered['proprietario'] = 1;
|
$filtered['proprietario'] = 1;
|
||||||
|
|
@ -359,15 +402,15 @@ public function handle(): int
|
||||||
$filtered['totale_inquilini'] = $normalizeNumber($assoc['tot_inq'] ?? $assoc['totale_inquilini'] ?? null);
|
$filtered['totale_inquilini'] = $normalizeNumber($assoc['tot_inq'] ?? $assoc['totale_inquilini'] ?? null);
|
||||||
}
|
}
|
||||||
if ($hasColumn('selezionato')) {
|
if ($hasColumn('selezionato')) {
|
||||||
$raw = $assoc['selezionato'] ?? $filtered['selezionato'] ?? null;
|
$raw = $assoc['selezionato'] ?? $filtered['selezionato'] ?? null;
|
||||||
$filtered['selezionato'] = is_numeric($raw) ? (int) $raw : 0;
|
$filtered['selezionato'] = is_numeric($raw) ? (int) $raw : 0;
|
||||||
}
|
}
|
||||||
if ($hasColumn('nord')) {
|
if ($hasColumn('nord')) {
|
||||||
$raw = $assoc['nord'] ?? $filtered['nord'] ?? null;
|
$raw = $assoc['nord'] ?? $filtered['nord'] ?? null;
|
||||||
$filtered['nord'] = is_numeric($raw) ? (int) $raw : 0;
|
$filtered['nord'] = is_numeric($raw) ? (int) $raw : 0;
|
||||||
}
|
}
|
||||||
if ($hasColumn('is_detrazione') && empty($filtered['is_detrazione'])) {
|
if ($hasColumn('is_detrazione') && empty($filtered['is_detrazione'])) {
|
||||||
$raw = $assoc['is_detr'] ?? $assoc['is_detrazione'] ?? null;
|
$raw = $assoc['is_detr'] ?? $assoc['is_detrazione'] ?? null;
|
||||||
$filtered['is_detrazione'] = is_numeric($raw) ? (int) $raw : 0;
|
$filtered['is_detrazione'] = is_numeric($raw) ? (int) $raw : 0;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -397,24 +440,24 @@ public function handle(): int
|
||||||
$filtered['id'] = null;
|
$filtered['id'] = null;
|
||||||
}
|
}
|
||||||
if ($hasColumn('unico')) {
|
if ($hasColumn('unico')) {
|
||||||
$raw = $assoc['unico'] ?? $filtered['unico'] ?? null;
|
$raw = $assoc['unico'] ?? $filtered['unico'] ?? null;
|
||||||
$num = is_numeric($raw) ? (int)$raw : 0;
|
$num = is_numeric($raw) ? (int) $raw : 0;
|
||||||
$filtered['unico'] = $num > 0 ? 1 : 0;
|
$filtered['unico'] = $num > 0 ? 1 : 0;
|
||||||
}
|
}
|
||||||
if ($hasColumn('n_stra')) {
|
if ($hasColumn('n_stra')) {
|
||||||
$raw = $assoc['n_stra'] ?? $filtered['n_stra'] ?? 0;
|
$raw = $assoc['n_stra'] ?? $filtered['n_stra'] ?? 0;
|
||||||
$filtered['n_stra'] = is_numeric($raw) ? (int)$raw : 0;
|
$filtered['n_stra'] = is_numeric($raw) ? (int) $raw : 0;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'cre_deb_preced':
|
case 'cre_deb_preced':
|
||||||
if ($hasColumn('incluso')) {
|
if ($hasColumn('incluso')) {
|
||||||
$raw = $assoc['incluso'] ?? $filtered['incluso'] ?? null;
|
$raw = $assoc['incluso'] ?? $filtered['incluso'] ?? null;
|
||||||
$val = is_numeric($raw) ? (int)$raw : (in_array(strtoupper(trim((string)$raw)), ['S', 'Y', 'SI', 'YES', '1'], true) ? 1 : 0);
|
$val = is_numeric($raw) ? (int) $raw : (in_array(strtoupper(trim((string) $raw)), ['S', 'Y', 'SI', 'YES', '1'], true) ? 1 : 0);
|
||||||
$filtered['incluso'] = $val ? 1 : 0;
|
$filtered['incluso'] = $val ? 1 : 0;
|
||||||
}
|
}
|
||||||
if ($hasColumn('n_stra')) {
|
if ($hasColumn('n_stra')) {
|
||||||
$raw = $assoc['n_stra'] ?? $filtered['n_stra'] ?? 0;
|
$raw = $assoc['n_stra'] ?? $filtered['n_stra'] ?? 0;
|
||||||
$filtered['n_stra'] = is_numeric($raw) ? (int)$raw : 0;
|
$filtered['n_stra'] = is_numeric($raw) ? (int) $raw : 0;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'straordinarie':
|
case 'straordinarie':
|
||||||
|
|
@ -424,15 +467,15 @@ public function handle(): int
|
||||||
break;
|
break;
|
||||||
case 'rate':
|
case 'rate':
|
||||||
// cod_cond (id del condomino in Gescon)
|
// cod_cond (id del condomino in Gescon)
|
||||||
if ($hasColumn('cod_cond') && !isset($filtered['cod_cond'])) {
|
if ($hasColumn('cod_cond') && ! isset($filtered['cod_cond'])) {
|
||||||
$filtered['cod_cond'] = (string)($assoc['id_condomino'] ?? $assoc['cod_cond'] ?? '');
|
$filtered['cod_cond'] = (string) ($assoc['id_condomino'] ?? $assoc['cod_cond'] ?? '');
|
||||||
}
|
}
|
||||||
// Deriva anno e mese per emissione
|
// Deriva anno e mese per emissione
|
||||||
$year = null;
|
$year = null;
|
||||||
$month = null;
|
$month = null;
|
||||||
if (!empty($assoc['str_anno'])) {
|
if (! empty($assoc['str_anno'])) {
|
||||||
if (preg_match('/(\d{4})/', (string)$assoc['str_anno'], $m)) {
|
if (preg_match('/(\d{4})/', (string) $assoc['str_anno'], $m)) {
|
||||||
$year = (int)$m[1];
|
$year = (int) $m[1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($year === null) {
|
if ($year === null) {
|
||||||
|
|
@ -442,97 +485,176 @@ public function handle(): int
|
||||||
if ($ds) {
|
if ($ds) {
|
||||||
$ts = strtotime($ds);
|
$ts = strtotime($ds);
|
||||||
if ($ts) {
|
if ($ts) {
|
||||||
$year = (int)date('Y', $ts);
|
$year = (int) date('Y', $ts);
|
||||||
$month = (int)date('m', $ts);
|
$month = (int) date('m', $ts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!empty($assoc['n_mese']) && is_numeric($assoc['n_mese'])) {
|
if (! empty($assoc['n_mese']) && is_numeric($assoc['n_mese'])) {
|
||||||
$month = max(1, min(12, (int)$assoc['n_mese']));
|
$month = max(1, min(12, (int) $assoc['n_mese']));
|
||||||
}
|
}
|
||||||
if ($hasColumn('n_emissione') && !isset($filtered['n_emissione'])) {
|
if ($hasColumn('n_emissione') && ! isset($filtered['n_emissione'])) {
|
||||||
$filtered['n_emissione'] = $year ? ($year * 100 + ($month ?: 1)) : 0;
|
$filtered['n_emissione'] = $year ? ($year * 100 + ($month ?: 1)): 0;
|
||||||
}
|
}
|
||||||
// Date emissione/scadenza
|
// Date emissione/scadenza
|
||||||
if ($hasColumn('data_emissione')) {
|
if ($hasColumn('data_emissione')) {
|
||||||
$de = $normalizeDate($assoc['dt1_da'] ?? ($assoc['dt_empag'] ?? null));
|
$de = $normalizeDate($assoc['dt1_da'] ?? ($assoc['dt_empag'] ?? null));
|
||||||
if (!$de && $year) {
|
if (! $de && $year) {
|
||||||
$de = sprintf('%04d-%02d-01', $year, $month ?: 1);
|
$de = sprintf('%04d-%02d-01', $year, $month ?: 1);
|
||||||
}
|
}
|
||||||
$filtered['data_emissione'] = $de ? substr($de, 0, 10) : null;
|
$filtered['data_emissione'] = $de ? substr($de, 0, 10) : null;
|
||||||
}
|
}
|
||||||
if ($hasColumn('data_scadenza')) {
|
if ($hasColumn('data_scadenza')) {
|
||||||
$ds = $normalizeDate($assoc['dt2_a'] ?? null);
|
$ds = $normalizeDate($assoc['dt2_a'] ?? null);
|
||||||
if (!$ds && $year) {
|
if (! $ds && $year) {
|
||||||
$ds = sprintf('%04d-%02d-28', $year, $month ?: 1);
|
$ds = sprintf('%04d-%02d-28', $year, $month ?: 1);
|
||||||
}
|
}
|
||||||
$filtered['data_scadenza'] = $ds ? substr($ds, 0, 10) : null;
|
$filtered['data_scadenza'] = $ds ? substr($ds, 0, 10) : null;
|
||||||
}
|
}
|
||||||
// Importi
|
// Importi
|
||||||
$impR = $assoc['importo_dovuto_euro'] ?? $assoc['importo_euro'] ?? $assoc['importo'] ?? 0;
|
$impR = $assoc['importo_dovuto_euro'] ?? $assoc['importo_euro'] ?? $assoc['importo'] ?? 0;
|
||||||
if ($hasColumn('importo_euro') && !isset($filtered['importo_euro'])) $filtered['importo_euro'] = is_numeric($impR) ? (float)$impR : 0;
|
if ($hasColumn('importo_euro') && ! isset($filtered['importo_euro'])) {
|
||||||
if ($hasColumn('importo') && !isset($filtered['importo'])) $filtered['importo'] = is_numeric($impR) ? (float)$impR : 0;
|
$filtered['importo_euro'] = is_numeric($impR) ? (float) $impR : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasColumn('importo') && ! isset($filtered['importo'])) {
|
||||||
|
$filtered['importo'] = is_numeric($impR) ? (float) $impR : 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Tipo rata (o/r/s)
|
// Tipo rata (o/r/s)
|
||||||
if ($hasColumn('tipo_rata') && !isset($filtered['tipo_rata']) && !empty($assoc['o_r_s'])) {
|
if ($hasColumn('tipo_rata') && ! isset($filtered['tipo_rata']) && ! empty($assoc['o_r_s'])) {
|
||||||
$t = strtolower(trim((string)$assoc['o_r_s']));
|
$t = strtolower(trim((string) $assoc['o_r_s']));
|
||||||
$filtered['tipo_rata'] = in_array($t, ['o', 'r', 's'], true) ? strtoupper($t) : null;
|
$filtered['tipo_rata'] = in_array($t, ['o', 'r', 's'], true) ? strtoupper($t) : null;
|
||||||
}
|
}
|
||||||
// Causale/Note
|
// Causale/Note
|
||||||
if ($hasColumn('causale') && !isset($filtered['causale']) && isset($assoc['descrizione'])) $filtered['causale'] = (string)$assoc['descrizione'];
|
if ($hasColumn('causale') && ! isset($filtered['causale']) && isset($assoc['descrizione'])) {
|
||||||
if ($hasColumn('note') && !isset($filtered['note']) && isset($assoc['descrizione'])) $filtered['note'] = (string)$assoc['descrizione'];
|
$filtered['causale'] = (string) $assoc['descrizione'];
|
||||||
if ($hasColumn('pagata') && !isset($filtered['pagata'])) $filtered['pagata'] = 0;
|
}
|
||||||
|
|
||||||
|
if ($hasColumn('note') && ! isset($filtered['note']) && isset($assoc['descrizione'])) {
|
||||||
|
$filtered['note'] = (string) $assoc['descrizione'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasColumn('pagata') && ! isset($filtered['pagata'])) {
|
||||||
|
$filtered['pagata'] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'incassi':
|
case 'incassi':
|
||||||
if ($hasColumn('cod_cond') && isset($assoc['cod_cond'])) $filtered['cod_cond'] = (string)$assoc['cod_cond'];
|
if ($hasColumn('cod_cond') && isset($assoc['cod_cond'])) {
|
||||||
if ($hasColumn('cond_inq')) $filtered['cond_inq'] = (string)($assoc['cond_inq'] ?? $assoc['cond_inquil'] ?? '');
|
$filtered['cod_cond'] = (string) $assoc['cod_cond'];
|
||||||
if ($hasColumn('o_r_s')) $filtered['o_r_s'] = (string)($assoc['o_r_s'] ?? '');
|
}
|
||||||
if ($hasColumn('anno_rif')) $filtered['anno_rif'] = (string)($assoc['anno_rif'] ?? '');
|
|
||||||
if ($hasColumn('n_mese')) $filtered['n_mese'] = is_numeric($assoc['n_mese'] ?? null) ? (int)$assoc['n_mese'] : null;
|
if ($hasColumn('cond_inq')) {
|
||||||
if ($hasColumn('n_stra')) $filtered['n_stra'] = is_numeric($assoc['n_stra'] ?? null) ? (int)$assoc['n_stra'] : null;
|
$filtered['cond_inq'] = (string) ($assoc['cond_inq'] ?? $assoc['cond_inquil'] ?? '');
|
||||||
if ($hasColumn('descrizione') && isset($assoc['descrizione'])) $filtered['descrizione'] = (string)$assoc['descrizione'];
|
}
|
||||||
if ($hasColumn('cod_cassa') && isset($assoc['cod_cassa'])) $filtered['cod_cassa'] = (string)$assoc['cod_cassa'];
|
|
||||||
if ($hasColumn('dt_empag')) $filtered['dt_empag'] = $normalizeDate($assoc['dt_empag'] ?? null);
|
if ($hasColumn('o_r_s')) {
|
||||||
|
$filtered['o_r_s'] = (string) ($assoc['o_r_s'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasColumn('anno_rif')) {
|
||||||
|
$filtered['anno_rif'] = (string) ($assoc['anno_rif'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasColumn('n_mese')) {
|
||||||
|
$filtered['n_mese'] = is_numeric($assoc['n_mese'] ?? null) ? (int) $assoc['n_mese'] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasColumn('n_stra')) {
|
||||||
|
$filtered['n_stra'] = is_numeric($assoc['n_stra'] ?? null) ? (int) $assoc['n_stra'] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasColumn('descrizione') && isset($assoc['descrizione'])) {
|
||||||
|
$filtered['descrizione'] = (string) $assoc['descrizione'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasColumn('cod_cassa') && isset($assoc['cod_cassa'])) {
|
||||||
|
$filtered['cod_cassa'] = (string) $assoc['cod_cassa'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasColumn('dt_empag')) {
|
||||||
|
$filtered['dt_empag'] = $normalizeDate($assoc['dt_empag'] ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
if ($hasColumn('importo_pagato_euro')) {
|
if ($hasColumn('importo_pagato_euro')) {
|
||||||
$imp = $assoc['importo_pagato_euro'] ?? $assoc['importo_euro'] ?? $assoc['importo'] ?? null;
|
$imp = $assoc['importo_pagato_euro'] ?? $assoc['importo_euro'] ?? $assoc['importo'] ?? null;
|
||||||
$filtered['importo_pagato_euro'] = $normalizeNumber($imp) ?? 0;
|
$filtered['importo_pagato_euro'] = $normalizeNumber($imp) ?? 0;
|
||||||
}
|
}
|
||||||
if ($hasColumn('data_incasso')) {
|
if ($hasColumn('data_incasso')) {
|
||||||
$di = $normalizeDate($assoc['dt_empag'] ?? ($filtered['data_incasso'] ?? null));
|
$di = $normalizeDate($assoc['dt_empag'] ?? ($filtered['data_incasso'] ?? null));
|
||||||
if (!$di) {
|
if (! $di) {
|
||||||
// Fallback: costruisci data da anno_rif e n_mese
|
// Fallback: costruisci data da anno_rif e n_mese
|
||||||
$year = null;
|
$year = null;
|
||||||
if (!empty($assoc['anno_rif']) && is_numeric($assoc['anno_rif'])) $year = (int)$assoc['anno_rif'];
|
if (! empty($assoc['anno_rif']) && is_numeric($assoc['anno_rif'])) {
|
||||||
elseif (!empty($assoc['anno_ricev']) && is_numeric($assoc['anno_ricev'])) $year = (int)$assoc['anno_ricev'];
|
$year = (int) $assoc['anno_rif'];
|
||||||
|
} elseif (! empty($assoc['anno_ricev']) && is_numeric($assoc['anno_ricev'])) {
|
||||||
|
$year = (int) $assoc['anno_ricev'];
|
||||||
|
}
|
||||||
|
|
||||||
$month = null;
|
$month = null;
|
||||||
if (!empty($assoc['n_mese']) && is_numeric($assoc['n_mese'])) $month = max(1, min(12, (int)$assoc['n_mese']));
|
if (! empty($assoc['n_mese']) && is_numeric($assoc['n_mese'])) {
|
||||||
|
$month = max(1, min(12, (int) $assoc['n_mese']));
|
||||||
|
}
|
||||||
|
|
||||||
if ($year) {
|
if ($year) {
|
||||||
$m = $month ?: 1;
|
$m = $month ?: 1;
|
||||||
$day = 15;
|
$day = 15;
|
||||||
$di = sprintf('%04d-%02d-%02d', $year, $m, $day);
|
$di = sprintf('%04d-%02d-%02d', $year, $m, $day);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$filtered['data_incasso'] = $di ? substr($di, 0, 10) : null;
|
$filtered['data_incasso'] = $di ? substr($di, 0, 10) : null;
|
||||||
}
|
}
|
||||||
// Deriva anno da header legacy se la tabella incassi di staging lo prevede
|
// Deriva anno da header legacy se la tabella incassi di staging lo prevede
|
||||||
if ($hasColumn('anno') && !isset($filtered['anno'])) {
|
if ($hasColumn('anno') && ! isset($filtered['anno'])) {
|
||||||
$y = null;
|
$y = null;
|
||||||
if (!empty($assoc['anno_rif']) && is_numeric($assoc['anno_rif'])) $y = (int)$assoc['anno_rif'];
|
if (! empty($assoc['anno_rif']) && is_numeric($assoc['anno_rif'])) {
|
||||||
elseif (!empty($assoc['dt_empag'])) {
|
$y = (int) $assoc['anno_rif'];
|
||||||
|
} elseif (! empty($assoc['dt_empag'])) {
|
||||||
$ts = @strtotime($assoc['dt_empag']);
|
$ts = @strtotime($assoc['dt_empag']);
|
||||||
if ($ts) $y = (int)date('Y', $ts);
|
if ($ts) {
|
||||||
|
$y = (int) date('Y', $ts);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
if ($y !== null) $filtered['anno'] = $y;
|
if ($y !== null) {
|
||||||
|
$filtered['anno'] = $y;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
$impI = $assoc['importo_pagato_euro'] ?? $assoc['importo_pagato'] ?? $assoc['importo_euro'] ?? $assoc['importo'] ?? 0;
|
$impI = $assoc['importo_pagato_euro'] ?? $assoc['importo_pagato'] ?? $assoc['importo_euro'] ?? $assoc['importo'] ?? 0;
|
||||||
if ($hasColumn('importo_euro') && !isset($filtered['importo_euro'])) $filtered['importo_euro'] = is_numeric($impI) ? (float)$impI : 0;
|
if ($hasColumn('importo_euro') && ! isset($filtered['importo_euro'])) {
|
||||||
if ($hasColumn('importo') && !isset($filtered['importo'])) $filtered['importo'] = is_numeric($impI) ? (float)$impI : 0;
|
$filtered['importo_euro'] = is_numeric($impI) ? (float) $impI : 0;
|
||||||
if ($hasColumn('modalita_pagamento') && !isset($filtered['modalita_pagamento']) && isset($assoc['cod_cassa'])) $filtered['modalita_pagamento'] = (string)$assoc['cod_cassa'];
|
}
|
||||||
if ($hasColumn('riferimento_pagamento') && !isset($filtered['riferimento_pagamento']) && isset($assoc['n_riferimento'])) $filtered['riferimento_pagamento'] = (string)$assoc['n_riferimento'];
|
|
||||||
if ($hasColumn('n_rata_riferimento') && !isset($filtered['n_rata_riferimento']) && isset($assoc['n_mese']) && is_numeric($assoc['n_mese'])) $filtered['n_rata_riferimento'] = (int)$assoc['n_mese'];
|
if ($hasColumn('importo') && ! isset($filtered['importo'])) {
|
||||||
if ($hasColumn('riconciliato') && !isset($filtered['riconciliato'])) $filtered['riconciliato'] = 0;
|
$filtered['importo'] = is_numeric($impI) ? (float) $impI : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasColumn('modalita_pagamento') && ! isset($filtered['modalita_pagamento']) && isset($assoc['cod_cassa'])) {
|
||||||
|
$filtered['modalita_pagamento'] = (string) $assoc['cod_cassa'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasColumn('riferimento_pagamento') && ! isset($filtered['riferimento_pagamento']) && isset($assoc['n_riferimento'])) {
|
||||||
|
$filtered['riferimento_pagamento'] = (string) $assoc['n_riferimento'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasColumn('n_rata_riferimento') && ! isset($filtered['n_rata_riferimento']) && isset($assoc['n_mese']) && is_numeric($assoc['n_mese'])) {
|
||||||
|
$filtered['n_rata_riferimento'] = (int) $assoc['n_mese'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasColumn('riconciliato') && ! isset($filtered['riconciliato'])) {
|
||||||
|
$filtered['riconciliato'] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'operazioni':
|
case 'operazioni':
|
||||||
foreach (['dt_spe', 'dt_fat'] as $dc) if ($hasColumn($dc) && array_key_exists($dc, $filtered)) $filtered[$dc] = $normalizeDate($filtered[$dc]);
|
foreach (['dt_spe', 'dt_fat'] as $dc) {
|
||||||
|
if ($hasColumn($dc) && array_key_exists($dc, $filtered)) {
|
||||||
|
$filtered[$dc] = $normalizeDate($filtered[$dc]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'voc_spe':
|
case 'voc_spe':
|
||||||
// Evita collisioni multi-anno sugli ID
|
// Evita collisioni multi-anno sugli ID
|
||||||
|
|
@ -540,7 +662,10 @@ public function handle(): int
|
||||||
unset($filtered['id_vocspe']);
|
unset($filtered['id_vocspe']);
|
||||||
}
|
}
|
||||||
foreach (['perc_proprietario', 'perc_inquilino', 'imp_propr', 'imp_inquil', 'importo', 'importo_euro', 'preventivo', 'preventivo_euro', 'consuntivo', 'consuntivo_euro', 'abit_detr36'] as $nf) {
|
foreach (['perc_proprietario', 'perc_inquilino', 'imp_propr', 'imp_inquil', 'importo', 'importo_euro', 'preventivo', 'preventivo_euro', 'consuntivo', 'consuntivo_euro', 'abit_detr36'] as $nf) {
|
||||||
if ($hasColumn($nf) && (!isset($filtered[$nf]) || $filtered[$nf] === '')) $filtered[$nf] = 0;
|
if ($hasColumn($nf) && (! isset($filtered[$nf]) || $filtered[$nf] === '')) {
|
||||||
|
$filtered[$nf] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'rate_emissioni_dettaglio':
|
case 'rate_emissioni_dettaglio':
|
||||||
|
|
@ -719,7 +844,10 @@ public function handle(): int
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($filtered as $k => $v) {
|
foreach ($filtered as $k => $v) {
|
||||||
if ($v === '') $filtered[$k] = null;
|
if ($v === '') {
|
||||||
|
$filtered[$k] = null;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -741,11 +869,17 @@ public function handle(): int
|
||||||
// Se manca data_incasso ed è richiesta, prova fallback con anno_rif/n_mese
|
// Se manca data_incasso ed è richiesta, prova fallback con anno_rif/n_mese
|
||||||
if ($hasColumn('data_incasso') && (empty($filtered['data_incasso']))) {
|
if ($hasColumn('data_incasso') && (empty($filtered['data_incasso']))) {
|
||||||
$year = null;
|
$year = null;
|
||||||
if (isset($assoc['anno_rif']) && is_numeric($assoc['anno_rif'])) $year = (int)$assoc['anno_rif'];
|
if (isset($assoc['anno_rif']) && is_numeric($assoc['anno_rif'])) {
|
||||||
|
$year = (int) $assoc['anno_rif'];
|
||||||
|
}
|
||||||
|
|
||||||
$month = null;
|
$month = null;
|
||||||
if (isset($assoc['n_mese']) && is_numeric($assoc['n_mese'])) $month = max(1, min(12, (int)$assoc['n_mese']));
|
if (isset($assoc['n_mese']) && is_numeric($assoc['n_mese'])) {
|
||||||
|
$month = max(1, min(12, (int) $assoc['n_mese']));
|
||||||
|
}
|
||||||
|
|
||||||
if ($year) {
|
if ($year) {
|
||||||
$m = $month ?: 1;
|
$m = $month ?: 1;
|
||||||
$filtered['data_incasso'] = sprintf('%04d-%02d-15', $year, $m);
|
$filtered['data_incasso'] = sprintf('%04d-%02d-15', $year, $m);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Auth;
|
namespace App\Filament\Auth;
|
||||||
|
|
||||||
|
use App\Filament\Pages\Fornitore\TicketOperativi;
|
||||||
use Filament\Auth\Http\Responses\Contracts\LoginResponse;
|
use Filament\Auth\Http\Responses\Contracts\LoginResponse;
|
||||||
use Filament\Auth\Pages\Login as BaseLogin;
|
use Filament\Auth\Pages\Login as BaseLogin;
|
||||||
use Filament\Facades\Filament;
|
use Filament\Facades\Filament;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class Login extends BaseLogin
|
class Login extends BaseLogin
|
||||||
|
|
@ -15,6 +16,12 @@ class Login extends BaseLogin
|
||||||
*/
|
*/
|
||||||
protected function getRedirectUrl(): string
|
protected function getRedirectUrl(): string
|
||||||
{
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
if ($user instanceof \App\Models\User && $user->hasRole('fornitore')) {
|
||||||
|
return TicketOperativi::getUrl(panel: 'admin-filament');
|
||||||
|
}
|
||||||
|
|
||||||
return '/admin-filament';
|
return '/admin-filament';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -24,9 +31,9 @@ public function authenticate(): ?LoginResponse
|
||||||
|
|
||||||
if ($response) {
|
if ($response) {
|
||||||
Log::info('Filament login forced redirect (backup4)', [
|
Log::info('Filament login forced redirect (backup4)', [
|
||||||
'panel_id' => Filament::getCurrentPanel()?->getId(),
|
'panel_id' => Filament::getCurrentPanel()?->getId(),
|
||||||
'filament_guard_check' => Filament::auth()->check(),
|
'filament_guard_check' => Filament::auth()->check(),
|
||||||
'filament_user_id' => Filament::auth()->id(),
|
'filament_user_id' => Filament::auth()->id(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->redirect($this->getRedirectUrl());
|
$this->redirect($this->getRedirectUrl());
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,11 @@
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Forms\Components\DatePicker;
|
use Filament\Forms\Components\DatePicker;
|
||||||
|
use Filament\Forms\Components\FileUpload;
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Forms\Components\Textarea;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Pages\Page;
|
use Filament\Pages\Page;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Concerns\InteractsWithTable;
|
use Filament\Tables\Concerns\InteractsWithTable;
|
||||||
|
|
@ -290,6 +293,24 @@ public function table(Table $table): Table
|
||||||
->dateTime('d/m/Y H:i')
|
->dateTime('d/m/Y H:i')
|
||||||
->toggleable(isToggledHiddenByDefault: true),
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
|
||||||
|
TextColumn::make('deadline_lettura_at')
|
||||||
|
->label('Deadline campagna')
|
||||||
|
->dateTime('d/m/Y H:i')
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
|
||||||
|
TextColumn::make('sollecito_inviato_at')
|
||||||
|
->label('Sollecito')
|
||||||
|
->dateTime('d/m/Y H:i')
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
|
||||||
|
TextColumn::make('rilevatore_tipo')
|
||||||
|
->label('Rilevatore')
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
|
||||||
|
TextColumn::make('lettura_precedente_valore')
|
||||||
|
->label('Lettura precedente')
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
|
||||||
TextColumn::make('riferimento_acquisizione')->label('Riferimento')->toggleable(isToggledHiddenByDefault: true),
|
TextColumn::make('riferimento_acquisizione')->label('Riferimento')->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
|
||||||
TextColumn::make('archivio_documentale_path')
|
TextColumn::make('archivio_documentale_path')
|
||||||
|
|
@ -324,6 +345,40 @@ public function table(Table $table): Table
|
||||||
$this->archivioScope = $this->archivioScope === 'all' ? 'active' : 'all';
|
$this->archivioScope = $this->archivioScope === 'all' ? 'active' : 'all';
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
Action::make('programmaCampagnaAcqua')
|
||||||
|
->label('Campagna letture acqua')
|
||||||
|
->icon('heroicon-o-megaphone')
|
||||||
|
->color('primary')
|
||||||
|
->form([
|
||||||
|
Select::make('stabile_servizio_id')
|
||||||
|
->label('Servizio acqua')
|
||||||
|
->options(fn() => $this->getServiziOptionsByTipo('acqua'))
|
||||||
|
->searchable()
|
||||||
|
->required(),
|
||||||
|
DatePicker::make('deadline_lettura_at')
|
||||||
|
->label('Scadenza invio lettura')
|
||||||
|
->native(false)
|
||||||
|
->required(),
|
||||||
|
Textarea::make('messaggio_template')
|
||||||
|
->label('Messaggio base')
|
||||||
|
->default('Inviare la lettura del contatore acqua entro la data indicata allegando foto del contatore.')
|
||||||
|
->rows(3),
|
||||||
|
])
|
||||||
|
->action(function (array $data): void {
|
||||||
|
$created = $this->scheduleWaterCampaign($data);
|
||||||
|
Notification::make()->title('Campagna letture impostata')->body('Righe aggiornate o create: ' . $created)->success()->send();
|
||||||
|
}),
|
||||||
|
|
||||||
|
Action::make('sollecitaScadute')
|
||||||
|
->label('Sollecita scadute')
|
||||||
|
->icon('heroicon-o-bell-alert')
|
||||||
|
->color('warning')
|
||||||
|
->requiresConfirmation()
|
||||||
|
->action(function (): void {
|
||||||
|
$count = $this->markOverdueWaterReminders();
|
||||||
|
Notification::make()->title('Solleciti aggiornati')->body('Righe sollecitate: ' . $count)->success()->send();
|
||||||
|
}),
|
||||||
|
|
||||||
Action::make('create')
|
Action::make('create')
|
||||||
->label('Nuova')
|
->label('Nuova')
|
||||||
->icon('heroicon-o-plus')
|
->icon('heroicon-o-plus')
|
||||||
|
|
@ -376,7 +431,25 @@ public function table(Table $table): Table
|
||||||
TextInput::make('riferimento_acquisizione')->label('Riferimento acquisizione')->maxLength(191),
|
TextInput::make('riferimento_acquisizione')->label('Riferimento acquisizione')->maxLength(191),
|
||||||
DatePicker::make('richiesta_lettura_inviata_at')->label('Richiesta inviata il')->native(false),
|
DatePicker::make('richiesta_lettura_inviata_at')->label('Richiesta inviata il')->native(false),
|
||||||
DatePicker::make('prossima_lettura_scadenza_at')->label('Prossima scadenza lettura')->native(false),
|
DatePicker::make('prossima_lettura_scadenza_at')->label('Prossima scadenza lettura')->native(false),
|
||||||
|
DatePicker::make('deadline_lettura_at')->label('Deadline lettura')->native(false),
|
||||||
|
Select::make('rilevatore_tipo')
|
||||||
|
->label('Rilevatore')
|
||||||
|
->options([
|
||||||
|
'condomino' => 'Condomino',
|
||||||
|
'inquilino' => 'Inquilino',
|
||||||
|
'letturista' => 'Letturista',
|
||||||
|
'amministrazione' => 'Amministrazione',
|
||||||
|
]),
|
||||||
|
TextInput::make('rilevatore_nome')->label('Nome rilevatore')->maxLength(191),
|
||||||
|
FileUpload::make('lettura_foto_upload')
|
||||||
|
->label('Foto contatore')
|
||||||
|
->image()
|
||||||
|
->imageEditor(),
|
||||||
|
FileUpload::make('lettura_precedente_foto_upload')
|
||||||
|
->label('Foto contatore precedente')
|
||||||
|
->image(),
|
||||||
TextInput::make('archivio_documentale_path')->label('Path archivio documentale')->maxLength(500),
|
TextInput::make('archivio_documentale_path')->label('Path archivio documentale')->maxLength(500),
|
||||||
|
TextInput::make('lettura_precedente_valore')->label('Lettura precedente')->numeric(),
|
||||||
TextInput::make('lettura_inizio')->label('Lettura inizio')->numeric(),
|
TextInput::make('lettura_inizio')->label('Lettura inizio')->numeric(),
|
||||||
TextInput::make('lettura_fine')->label('Lettura fine')->numeric(),
|
TextInput::make('lettura_fine')->label('Lettura fine')->numeric(),
|
||||||
TextInput::make('consumo_valore')->label('Consumo valore')->numeric(),
|
TextInput::make('consumo_valore')->label('Consumo valore')->numeric(),
|
||||||
|
|
@ -394,7 +467,7 @@ public function table(Table $table): Table
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
StabileServizioLettura::query()->create([
|
$record = StabileServizioLettura::query()->create([
|
||||||
'stabile_id' => (int) $stabileId,
|
'stabile_id' => (int) $stabileId,
|
||||||
'stabile_servizio_id' => (int) ($data['stabile_servizio_id'] ?? 0),
|
'stabile_servizio_id' => (int) ($data['stabile_servizio_id'] ?? 0),
|
||||||
'fornitore_id' => (int) ($data['fornitore_id'] ?? 0) ?: null,
|
'fornitore_id' => (int) ($data['fornitore_id'] ?? 0) ?: null,
|
||||||
|
|
@ -410,7 +483,11 @@ public function table(Table $table): Table
|
||||||
'riferimento_acquisizione' => trim((string) ($data['riferimento_acquisizione'] ?? '')) ?: null,
|
'riferimento_acquisizione' => trim((string) ($data['riferimento_acquisizione'] ?? '')) ?: null,
|
||||||
'richiesta_lettura_inviata_at' => $data['richiesta_lettura_inviata_at'] ?? null,
|
'richiesta_lettura_inviata_at' => $data['richiesta_lettura_inviata_at'] ?? null,
|
||||||
'prossima_lettura_scadenza_at' => $data['prossima_lettura_scadenza_at'] ?? null,
|
'prossima_lettura_scadenza_at' => $data['prossima_lettura_scadenza_at'] ?? null,
|
||||||
|
'deadline_lettura_at' => $data['deadline_lettura_at'] ?? null,
|
||||||
|
'rilevatore_tipo' => trim((string) ($data['rilevatore_tipo'] ?? '')) ?: null,
|
||||||
|
'rilevatore_nome' => trim((string) ($data['rilevatore_nome'] ?? '')) ?: null,
|
||||||
'archivio_documentale_path' => trim((string) ($data['archivio_documentale_path'] ?? '')) ?: null,
|
'archivio_documentale_path' => trim((string) ($data['archivio_documentale_path'] ?? '')) ?: null,
|
||||||
|
'lettura_precedente_valore' => $data['lettura_precedente_valore'] ?? null,
|
||||||
'lettura_inizio' => $data['lettura_inizio'] ?? null,
|
'lettura_inizio' => $data['lettura_inizio'] ?? null,
|
||||||
'lettura_fine' => $data['lettura_fine'] ?? null,
|
'lettura_fine' => $data['lettura_fine'] ?? null,
|
||||||
'consumo_valore' => $data['consumo_valore'] ?? null,
|
'consumo_valore' => $data['consumo_valore'] ?? null,
|
||||||
|
|
@ -418,6 +495,9 @@ public function table(Table $table): Table
|
||||||
'importo_totale' => $data['importo_totale'] ?? null,
|
'importo_totale' => $data['importo_totale'] ?? null,
|
||||||
'created_by' => (int) $user->id,
|
'created_by' => (int) $user->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$this->hydrateReadingWithPreviousData($record);
|
||||||
|
$this->storeReadingUploads($record, $data);
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
->actions([
|
->actions([
|
||||||
|
|
@ -458,7 +538,25 @@ public function table(Table $table): Table
|
||||||
'pdf_ocr' => 'PDF/OCR',
|
'pdf_ocr' => 'PDF/OCR',
|
||||||
]),
|
]),
|
||||||
TextInput::make('riferimento_acquisizione')->label('Riferimento acquisizione')->maxLength(191),
|
TextInput::make('riferimento_acquisizione')->label('Riferimento acquisizione')->maxLength(191),
|
||||||
|
DatePicker::make('deadline_lettura_at')->label('Deadline lettura')->native(false),
|
||||||
|
Select::make('rilevatore_tipo')
|
||||||
|
->label('Rilevatore')
|
||||||
|
->options([
|
||||||
|
'condomino' => 'Condomino',
|
||||||
|
'inquilino' => 'Inquilino',
|
||||||
|
'letturista' => 'Letturista',
|
||||||
|
'amministrazione' => 'Amministrazione',
|
||||||
|
]),
|
||||||
|
TextInput::make('rilevatore_nome')->label('Nome rilevatore')->maxLength(191),
|
||||||
|
FileUpload::make('lettura_foto_upload')
|
||||||
|
->label('Foto contatore')
|
||||||
|
->image()
|
||||||
|
->imageEditor(),
|
||||||
|
FileUpload::make('lettura_precedente_foto_upload')
|
||||||
|
->label('Foto contatore precedente')
|
||||||
|
->image(),
|
||||||
TextInput::make('lettura_inizio')->label('Lettura inizio')->numeric(),
|
TextInput::make('lettura_inizio')->label('Lettura inizio')->numeric(),
|
||||||
|
TextInput::make('lettura_precedente_valore')->label('Lettura precedente')->numeric(),
|
||||||
TextInput::make('lettura_fine')->label('Lettura fine')->numeric(),
|
TextInput::make('lettura_fine')->label('Lettura fine')->numeric(),
|
||||||
TextInput::make('consumo_valore')->label('Consumo valore')->numeric(),
|
TextInput::make('consumo_valore')->label('Consumo valore')->numeric(),
|
||||||
TextInput::make('consumo_unita')->label('Unità')->maxLength(16),
|
TextInput::make('consumo_unita')->label('Unità')->maxLength(16),
|
||||||
|
|
@ -479,7 +577,11 @@ public function table(Table $table): Table
|
||||||
'riferimento_acquisizione' => (string) ($record->riferimento_acquisizione ?? ''),
|
'riferimento_acquisizione' => (string) ($record->riferimento_acquisizione ?? ''),
|
||||||
'richiesta_lettura_inviata_at' => $record->richiesta_lettura_inviata_at,
|
'richiesta_lettura_inviata_at' => $record->richiesta_lettura_inviata_at,
|
||||||
'prossima_lettura_scadenza_at' => $record->prossima_lettura_scadenza_at,
|
'prossima_lettura_scadenza_at' => $record->prossima_lettura_scadenza_at,
|
||||||
|
'deadline_lettura_at' => $record->deadline_lettura_at,
|
||||||
|
'rilevatore_tipo' => (string) ($record->rilevatore_tipo ?? ''),
|
||||||
|
'rilevatore_nome' => (string) ($record->rilevatore_nome ?? ''),
|
||||||
'archivio_documentale_path' => (string) ($record->archivio_documentale_path ?? ''),
|
'archivio_documentale_path' => (string) ($record->archivio_documentale_path ?? ''),
|
||||||
|
'lettura_precedente_valore' => $record->lettura_precedente_valore,
|
||||||
'lettura_inizio' => $record->lettura_inizio,
|
'lettura_inizio' => $record->lettura_inizio,
|
||||||
'lettura_fine' => $record->lettura_fine,
|
'lettura_fine' => $record->lettura_fine,
|
||||||
'consumo_valore' => $record->consumo_valore,
|
'consumo_valore' => $record->consumo_valore,
|
||||||
|
|
@ -502,7 +604,11 @@ public function table(Table $table): Table
|
||||||
'riferimento_acquisizione' => trim((string) ($data['riferimento_acquisizione'] ?? '')) ?: null,
|
'riferimento_acquisizione' => trim((string) ($data['riferimento_acquisizione'] ?? '')) ?: null,
|
||||||
'richiesta_lettura_inviata_at' => $data['richiesta_lettura_inviata_at'] ?? null,
|
'richiesta_lettura_inviata_at' => $data['richiesta_lettura_inviata_at'] ?? null,
|
||||||
'prossima_lettura_scadenza_at' => $data['prossima_lettura_scadenza_at'] ?? null,
|
'prossima_lettura_scadenza_at' => $data['prossima_lettura_scadenza_at'] ?? null,
|
||||||
|
'deadline_lettura_at' => $data['deadline_lettura_at'] ?? null,
|
||||||
|
'rilevatore_tipo' => trim((string) ($data['rilevatore_tipo'] ?? '')) ?: null,
|
||||||
|
'rilevatore_nome' => trim((string) ($data['rilevatore_nome'] ?? '')) ?: null,
|
||||||
'archivio_documentale_path' => trim((string) ($data['archivio_documentale_path'] ?? '')) ?: null,
|
'archivio_documentale_path' => trim((string) ($data['archivio_documentale_path'] ?? '')) ?: null,
|
||||||
|
'lettura_precedente_valore' => $data['lettura_precedente_valore'] ?? null,
|
||||||
'lettura_inizio' => $data['lettura_inizio'] ?? null,
|
'lettura_inizio' => $data['lettura_inizio'] ?? null,
|
||||||
'lettura_fine' => $data['lettura_fine'] ?? null,
|
'lettura_fine' => $data['lettura_fine'] ?? null,
|
||||||
'consumo_valore' => $data['consumo_valore'] ?? null,
|
'consumo_valore' => $data['consumo_valore'] ?? null,
|
||||||
|
|
@ -510,6 +616,8 @@ public function table(Table $table): Table
|
||||||
'importo_totale' => $data['importo_totale'] ?? null,
|
'importo_totale' => $data['importo_totale'] ?? null,
|
||||||
]);
|
]);
|
||||||
$record->save();
|
$record->save();
|
||||||
|
$this->hydrateReadingWithPreviousData($record);
|
||||||
|
$this->storeReadingUploads($record, $data);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Action::make('delete')
|
Action::make('delete')
|
||||||
|
|
@ -670,4 +778,244 @@ private function getUnitaOptions(): array
|
||||||
})
|
})
|
||||||
->all();
|
->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function getServiziOptionsByTipo(string $tipo): array
|
||||||
|
{
|
||||||
|
return StabileServizio::query()
|
||||||
|
->where('tipo', $tipo)
|
||||||
|
->whereIn('id', array_keys($this->getServiziOptions()))
|
||||||
|
->orderBy('nome')
|
||||||
|
->get(['id', 'nome', 'contatore_matricola'])
|
||||||
|
->mapWithKeys(function (StabileServizio $servizio): array {
|
||||||
|
$label = trim((string) ($servizio->nome ?? '')) ?: ('Servizio #' . (int) $servizio->id);
|
||||||
|
if (trim((string) ($servizio->contatore_matricola ?? '')) !== '') {
|
||||||
|
$label .= ' (' . trim((string) $servizio->contatore_matricola) . ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
return [(int) $servizio->id => $label];
|
||||||
|
})
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function scheduleWaterCampaign(array $data): int
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
$stabileId = $user instanceof User ? (int) StabileContext::resolveActiveStabileId($user) : 0;
|
||||||
|
$servizioId = (int) ($data['stabile_servizio_id'] ?? 0);
|
||||||
|
if ($stabileId <= 0 || $servizioId <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$deadline = $data['deadline_lettura_at'] ?? null;
|
||||||
|
$message = trim((string) ($data['messaggio_template'] ?? ''));
|
||||||
|
$count = 0;
|
||||||
|
|
||||||
|
$unitaIds = UnitaImmobiliare::query()
|
||||||
|
->where('stabile_id', $stabileId)
|
||||||
|
->pluck('id');
|
||||||
|
|
||||||
|
foreach ($unitaIds as $unitaId) {
|
||||||
|
$row = StabileServizioLettura::query()
|
||||||
|
->where('stabile_id', $stabileId)
|
||||||
|
->where('stabile_servizio_id', $servizioId)
|
||||||
|
->where('unita_immobiliare_id', (int) $unitaId)
|
||||||
|
->whereIn('workflow_stato', ['da_richiedere', 'richiesta_inviata', 'richiesta_sollecitata'])
|
||||||
|
->latest('id')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $row instanceof StabileServizioLettura) {
|
||||||
|
$row = StabileServizioLettura::query()->create([
|
||||||
|
'stabile_id' => $stabileId,
|
||||||
|
'stabile_servizio_id' => $servizioId,
|
||||||
|
'unita_immobiliare_id' => (int) $unitaId,
|
||||||
|
'workflow_stato' => 'richiesta_inviata',
|
||||||
|
'canale_acquisizione' => 'manuale',
|
||||||
|
'richiesta_lettura_inviata_at' => now(),
|
||||||
|
'deadline_lettura_at' => $deadline,
|
||||||
|
'raw' => ['campagna_acqua' => ['messaggio' => $message, 'creata_da' => Auth::id()]],
|
||||||
|
'created_by' => Auth::id(),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$raw = is_array($row->raw ?? null) ? $row->raw : [];
|
||||||
|
$raw['campagna_acqua'] = [
|
||||||
|
'messaggio' => $message,
|
||||||
|
'aggiornata_da' => Auth::id(),
|
||||||
|
'updated_at' => now()->toIso8601String(),
|
||||||
|
];
|
||||||
|
$row->workflow_stato = 'richiesta_inviata';
|
||||||
|
$row->richiesta_lettura_inviata_at = $row->richiesta_lettura_inviata_at ?? now();
|
||||||
|
$row->deadline_lettura_at = $deadline;
|
||||||
|
$row->raw = $raw;
|
||||||
|
$row->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function markOverdueWaterReminders(): int
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
$stabileId = $user instanceof User ? (int) StabileContext::resolveActiveStabileId($user) : 0;
|
||||||
|
if ($stabileId <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = StabileServizioLettura::query()
|
||||||
|
->where('stabile_id', $stabileId)
|
||||||
|
->whereHas('servizio', fn(Builder $q) => $q->where('tipo', 'acqua'))
|
||||||
|
->whereNotNull('deadline_lettura_at')
|
||||||
|
->where('deadline_lettura_at', '<', now())
|
||||||
|
->where(function (Builder $q): void {
|
||||||
|
$q->whereNull('lettura_fine')->orWhereNull('consumo_valore');
|
||||||
|
})
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$raw = is_array($row->raw ?? null) ? $row->raw : [];
|
||||||
|
$campagna = is_array($raw['campagna_acqua'] ?? null) ? $raw['campagna_acqua'] : [];
|
||||||
|
$campagna['solleciti'] = (int) ($campagna['solleciti'] ?? 0) + 1;
|
||||||
|
$campagna['ultimo_sollecito_at'] = now()->toIso8601String();
|
||||||
|
$raw['campagna_acqua'] = $campagna;
|
||||||
|
|
||||||
|
$row->sollecito_inviato_at = now();
|
||||||
|
$row->workflow_stato = 'richiesta_sollecitata';
|
||||||
|
$row->raw = $raw;
|
||||||
|
$row->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rows->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hydrateReadingWithPreviousData(StabileServizioLettura $record): void
|
||||||
|
{
|
||||||
|
if ($record->unita_immobiliare_id === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$previous = StabileServizioLettura::query()
|
||||||
|
->where('stabile_servizio_id', (int) $record->stabile_servizio_id)
|
||||||
|
->where('unita_immobiliare_id', (int) $record->unita_immobiliare_id)
|
||||||
|
->where('id', '!=', (int) $record->id)
|
||||||
|
->latest('periodo_al')
|
||||||
|
->latest('id')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $previous instanceof StabileServizioLettura) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$changed = false;
|
||||||
|
if ($record->lettura_precedente_valore === null && $previous->lettura_fine !== null) {
|
||||||
|
$record->lettura_precedente_valore = $previous->lettura_fine;
|
||||||
|
$changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $record->lettura_precedente_foto_path && $previous->lettura_foto_path) {
|
||||||
|
$record->lettura_precedente_foto_path = $previous->lettura_foto_path;
|
||||||
|
$changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($changed) {
|
||||||
|
$record->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function storeReadingUploads(StabileServizioLettura $record, array $data): void
|
||||||
|
{
|
||||||
|
$updated = false;
|
||||||
|
|
||||||
|
foreach ([
|
||||||
|
'lettura_foto_upload' => ['field' => 'lettura_foto_path', 'prefix' => 'lettura'],
|
||||||
|
'lettura_precedente_foto_upload' => ['field' => 'lettura_precedente_foto_path', 'prefix' => 'precedente'],
|
||||||
|
] as $input => $meta) {
|
||||||
|
$upload = $data[$input] ?? null;
|
||||||
|
$stored = $this->storeReadingPhoto($upload, $record, (string) $meta['prefix']);
|
||||||
|
if (! is_array($stored)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$record->{$meta['field']} = $stored['path'];
|
||||||
|
|
||||||
|
$raw = is_array($record->raw ?? null) ? $record->raw : [];
|
||||||
|
$raw[$input . '_metadata'] = $stored['metadata'];
|
||||||
|
if ($input === 'lettura_foto_upload') {
|
||||||
|
$record->lettura_foto_original_name = (string) ($stored['original_name'] ?? '');
|
||||||
|
$record->lettura_foto_metadata = $stored['metadata'];
|
||||||
|
}
|
||||||
|
$record->raw = $raw;
|
||||||
|
$updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($updated) {
|
||||||
|
$record->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function storeReadingPhoto($upload, StabileServizioLettura $record, string $prefix): ?array
|
||||||
|
{
|
||||||
|
if (! is_object($upload) || ! method_exists($upload, 'storeAs')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ext = strtolower((string) ($upload->getClientOriginalExtension() ?: 'jpg'));
|
||||||
|
$unit = (int) ($record->unita_immobiliare_id ?? 0);
|
||||||
|
$filename = implode('-', array_filter([
|
||||||
|
'stabile' . (int) $record->stabile_id,
|
||||||
|
'servizio' . (int) $record->stabile_servizio_id,
|
||||||
|
$unit > 0 ? 'ui' . $unit : null,
|
||||||
|
$prefix,
|
||||||
|
now()->format('YmdHis'),
|
||||||
|
])) . '.' . $ext;
|
||||||
|
|
||||||
|
$path = $upload->storeAs('letture-servizi/foto', $filename, 'public');
|
||||||
|
return [
|
||||||
|
'path' => $path,
|
||||||
|
'original_name' => (string) $upload->getClientOriginalName(),
|
||||||
|
'metadata' => $this->extractImageMetadata($path),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extractImageMetadata(string $path): array
|
||||||
|
{
|
||||||
|
$absolutePath = \Illuminate\Support\Facades\Storage::disk('public')->path($path);
|
||||||
|
$metadata = [
|
||||||
|
'path' => $path,
|
||||||
|
'size' => is_file($absolutePath) ? filesize($absolutePath) : null,
|
||||||
|
];
|
||||||
|
|
||||||
|
$imageSize = @getimagesize($absolutePath);
|
||||||
|
if (is_array($imageSize)) {
|
||||||
|
$metadata['width'] = $imageSize[0] ?? null;
|
||||||
|
$metadata['height'] = $imageSize[1] ?? null;
|
||||||
|
$metadata['mime'] = $imageSize['mime'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (function_exists('exif_read_data')) {
|
||||||
|
try {
|
||||||
|
$exif = @exif_read_data($absolutePath);
|
||||||
|
if (is_array($exif)) {
|
||||||
|
$metadata['exif_datetime'] = $exif['DateTimeOriginal'] ?? ($exif['DateTime'] ?? null);
|
||||||
|
$metadata['gps'] = [
|
||||||
|
'lat' => $exif['GPSLatitude'] ?? null,
|
||||||
|
'lat_ref' => $exif['GPSLatitudeRef'] ?? null,
|
||||||
|
'lng' => $exif['GPSLongitude'] ?? null,
|
||||||
|
'lng_ref' => $exif['GPSLongitudeRef'] ?? null,
|
||||||
|
];
|
||||||
|
$metadata['device'] = trim(implode(' ', array_filter([
|
||||||
|
$exif['Make'] ?? null,
|
||||||
|
$exif['Model'] ?? null,
|
||||||
|
])));
|
||||||
|
}
|
||||||
|
} catch (\Throwable) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,25 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Pages\Condomini;
|
namespace App\Filament\Pages\Condomini;
|
||||||
|
|
||||||
|
use App\Models\InsuranceClaim;
|
||||||
|
use App\Models\InsurancePolicy;
|
||||||
|
use App\Models\RubricaUniversale;
|
||||||
use App\Models\Stabile as StabileModel;
|
use App\Models\Stabile as StabileModel;
|
||||||
|
use App\Models\Ticket;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Support\StabileContext;
|
use App\Support\StabileContext;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Pages\Page;
|
use Filament\Pages\Page;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Livewire\WithFileUploads;
|
||||||
use UnitEnum;
|
use UnitEnum;
|
||||||
|
|
||||||
class StabilePage extends Page
|
class StabilePage extends Page
|
||||||
{
|
{
|
||||||
|
use WithFileUploads;
|
||||||
|
|
||||||
protected static ?string $navigationLabel = 'Stabile';
|
protected static ?string $navigationLabel = 'Stabile';
|
||||||
|
|
||||||
protected static ?string $title = 'Stabile';
|
protected static ?string $title = 'Stabile';
|
||||||
|
|
@ -35,7 +42,7 @@ class StabilePage extends Page
|
||||||
|
|
||||||
/** @var array{ordinaria: array<int>, riscaldamento: array<int>, straordinaria: array<int>} */
|
/** @var array{ordinaria: array<int>, riscaldamento: array<int>, straordinaria: array<int>} */
|
||||||
public array $periodiEmissione = [
|
public array $periodiEmissione = [
|
||||||
'ordinaria' => [],
|
'ordinaria' => [],
|
||||||
'riscaldamento' => [],
|
'riscaldamento' => [],
|
||||||
'straordinaria' => [],
|
'straordinaria' => [],
|
||||||
];
|
];
|
||||||
|
|
@ -60,11 +67,36 @@ class StabilePage extends Page
|
||||||
/** @var array<int, bool> */
|
/** @var array<int, bool> */
|
||||||
public array $checklistFineAnnoState = [];
|
public array $checklistFineAnnoState = [];
|
||||||
|
|
||||||
|
public ?int $selectedInsurancePolicyId = null;
|
||||||
|
|
||||||
|
public array $insurancePolicyForm = [
|
||||||
|
'broker_rubrica_id' => null,
|
||||||
|
'company_rubrica_id' => null,
|
||||||
|
'policy_name' => '',
|
||||||
|
'policy_number' => '',
|
||||||
|
'policy_reference' => '',
|
||||||
|
'status' => 'attiva',
|
||||||
|
'started_at' => null,
|
||||||
|
'expires_at' => null,
|
||||||
|
'renewal_at' => null,
|
||||||
|
'annual_amount' => null,
|
||||||
|
'payment_split' => '',
|
||||||
|
'payment_schedule_notes' => '',
|
||||||
|
'coverage_summary' => '',
|
||||||
|
'special_conditions' => '',
|
||||||
|
'claim_form_template' => '',
|
||||||
|
'notes' => '',
|
||||||
|
];
|
||||||
|
|
||||||
|
public $insurancePolicyDocumentUpload = null;
|
||||||
|
|
||||||
|
public $insuranceSignatureUpload = null;
|
||||||
|
|
||||||
public static function canAccess(): bool
|
public static function canAccess(): bool
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
return $user instanceof User
|
return $user instanceof User
|
||||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mount(): void
|
public function mount(): void
|
||||||
|
|
@ -99,19 +131,27 @@ public function mount(): void
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$config = (array) ($this->stabile->configurazione_avanzata ?? []);
|
$config = (array) ($this->stabile->configurazione_avanzata ?? []);
|
||||||
$this->mostraRiscaldamento = (bool) ($config['mostra_riscaldamento'] ?? false);
|
|
||||||
|
|
||||||
$this->gestioneOrdinariaId = isset($config['gestione_ordinaria_id']) ? (int) $config['gestione_ordinaria_id'] : null;
|
$this->gestioneOrdinariaId = isset($config['gestione_ordinaria_id']) ? (int) $config['gestione_ordinaria_id'] : null;
|
||||||
$this->checklistFineAnnoState = is_array($config['checklist_fine_anno_state'] ?? null)
|
$this->checklistFineAnnoState = is_array($config['checklist_fine_anno_state'] ?? null)
|
||||||
? $config['checklist_fine_anno_state']
|
? $config['checklist_fine_anno_state']
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
$periodi = (array) ($config['periodi_emissione'] ?? []);
|
$periodi = (array) ($config['periodi_emissione'] ?? []);
|
||||||
|
$fallbackOrdinaria = $this->normalizeMesi((array) ($this->stabile->rate_ordinarie_mesi ?? []));
|
||||||
|
if ($fallbackOrdinaria === []) {
|
||||||
|
$fallbackOrdinaria = $this->extractLegacyRateMonthsFromConfig($config, 'ord');
|
||||||
|
}
|
||||||
|
$fallbackRiscaldamento = $this->normalizeMesi((array) ($this->stabile->rate_riscaldamento_mesi ?? []));
|
||||||
|
if ($fallbackRiscaldamento === []) {
|
||||||
|
$fallbackRiscaldamento = $this->extractLegacyRateMonthsFromConfig($config, 'ris');
|
||||||
|
}
|
||||||
$this->periodiEmissione = [
|
$this->periodiEmissione = [
|
||||||
'ordinaria' => $this->normalizeMesi((array) ($periodi['ordinaria'] ?? [])),
|
'ordinaria' => $this->normalizeMesi((array) ($periodi['ordinaria'] ?? [])) ?: $fallbackOrdinaria,
|
||||||
'riscaldamento' => $this->normalizeMesi((array) ($periodi['riscaldamento'] ?? [])),
|
'riscaldamento' => $this->normalizeMesi((array) ($periodi['riscaldamento'] ?? [])) ?: $fallbackRiscaldamento,
|
||||||
'straordinaria' => $this->normalizeMesi((array) ($periodi['straordinaria'] ?? [])),
|
'straordinaria' => $this->normalizeMesi((array) ($periodi['straordinaria'] ?? [])),
|
||||||
];
|
];
|
||||||
|
$this->mostraRiscaldamento = (bool) ($config['mostra_riscaldamento'] ?? ($this->periodiEmissione['riscaldamento'] !== []));
|
||||||
|
|
||||||
$gestioneSelezionataId = request()->integer('gestione_id');
|
$gestioneSelezionataId = request()->integer('gestione_id');
|
||||||
if ($gestioneSelezionataId > 0) {
|
if ($gestioneSelezionataId > 0) {
|
||||||
|
|
@ -139,30 +179,32 @@ public function mount(): void
|
||||||
public function tabs(): array
|
public function tabs(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'dati-generali' => 'Dati generali',
|
'dati-generali' => 'Dati generali',
|
||||||
'unita-immobiliari' => 'Unità / Locali tecnici',
|
'unita-immobiliari' => 'Unità / Locali tecnici',
|
||||||
'palazzine' => 'Palazzine',
|
'palazzine' => 'Palazzine',
|
||||||
'tabelle-millesimali' => 'Tabelle millesimali',
|
'assicurazioni' => 'Assicurazioni',
|
||||||
'rate-emesse' => 'Rate emesse',
|
'tabelle-millesimali' => 'Tabelle millesimali',
|
||||||
'gestioni' => 'Gestioni',
|
'rate-emesse' => 'Rate emesse',
|
||||||
|
'gestioni' => 'Gestioni',
|
||||||
'gestione-documentale' => 'Gestione documentale',
|
'gestione-documentale' => 'Gestione documentale',
|
||||||
'documenti-collegati' => 'Documenti collegati',
|
'documenti-collegati' => 'Documenti collegati',
|
||||||
'dati-bancari' => 'Banche e casse',
|
'dati-bancari' => 'Banche e casse',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function tabView(): string
|
public function tabView(): string
|
||||||
{
|
{
|
||||||
return match ($this->tab) {
|
return match ($this->tab) {
|
||||||
'rate-emesse' => 'admin.stabili.tabs.rate-emesse',
|
'rate-emesse' => 'admin.stabili.tabs.rate-emesse',
|
||||||
'palazzine' => 'admin.stabili.tabs.palazzine',
|
'palazzine' => 'admin.stabili.tabs.palazzine',
|
||||||
'dati-bancari' => 'filament.pages.condomini.tabs.dati-bancari',
|
'assicurazioni' => 'filament.pages.condomini.tabs.assicurazioni',
|
||||||
'tabelle-millesimali' => 'filament.pages.condomini.tabelle-millesimali-tab',
|
'dati-bancari' => 'filament.pages.condomini.tabs.dati-bancari',
|
||||||
'gestioni' => 'admin.stabili.tabs.gestioni',
|
'tabelle-millesimali' => 'filament.pages.condomini.tabelle-millesimali-tab',
|
||||||
'unita-immobiliari' => 'admin.stabili.tabs.unita-immobiliari',
|
'gestioni' => 'admin.stabili.tabs.gestioni',
|
||||||
|
'unita-immobiliari' => 'admin.stabili.tabs.unita-immobiliari',
|
||||||
'gestione-documentale' => 'admin.stabili.tabs.gestione-documentale',
|
'gestione-documentale' => 'admin.stabili.tabs.gestione-documentale',
|
||||||
'documenti-collegati' => 'admin.stabili.tabs.documenti-collegati',
|
'documenti-collegati' => 'admin.stabili.tabs.documenti-collegati',
|
||||||
default => 'admin.stabili.tabs.dati-generali',
|
default => 'admin.stabili.tabs.dati-generali',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -172,8 +214,8 @@ public function toggleMostraRiscaldamento(): void
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$config = (array) ($this->stabile->configurazione_avanzata ?? []);
|
$config = (array) ($this->stabile->configurazione_avanzata ?? []);
|
||||||
$value = ! (bool) ($config['mostra_riscaldamento'] ?? false);
|
$value = ! (bool) ($config['mostra_riscaldamento'] ?? false);
|
||||||
$config['mostra_riscaldamento'] = $value;
|
$config['mostra_riscaldamento'] = $value;
|
||||||
|
|
||||||
$this->stabile->configurazione_avanzata = $config;
|
$this->stabile->configurazione_avanzata = $config;
|
||||||
|
|
@ -190,9 +232,9 @@ public function savePeriodiEmissione(): void
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$config = (array) ($this->stabile->configurazione_avanzata ?? []);
|
$config = (array) ($this->stabile->configurazione_avanzata ?? []);
|
||||||
$config['periodi_emissione'] = [
|
$config['periodi_emissione'] = [
|
||||||
'ordinaria' => $this->normalizeMesi((array) ($this->periodiEmissione['ordinaria'] ?? [])),
|
'ordinaria' => $this->normalizeMesi((array) ($this->periodiEmissione['ordinaria'] ?? [])),
|
||||||
'riscaldamento' => $this->normalizeMesi((array) ($this->periodiEmissione['riscaldamento'] ?? [])),
|
'riscaldamento' => $this->normalizeMesi((array) ($this->periodiEmissione['riscaldamento'] ?? [])),
|
||||||
'straordinaria' => $this->normalizeMesi((array) ($this->periodiEmissione['straordinaria'] ?? [])),
|
'straordinaria' => $this->normalizeMesi((array) ($this->periodiEmissione['straordinaria'] ?? [])),
|
||||||
];
|
];
|
||||||
|
|
@ -220,23 +262,23 @@ public function saveGestioneSelezionata(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
$gestione->data_inizio = $this->gestioneDataInizio ?: null;
|
$gestione->data_inizio = $this->gestioneDataInizio ?: null;
|
||||||
$gestione->data_fine = $this->gestioneDataFine ?: null;
|
$gestione->data_fine = $this->gestioneDataFine ?: null;
|
||||||
|
|
||||||
$mesi = $this->normalizeMesi($this->gestioneMesi);
|
$mesi = $this->normalizeMesi($this->gestioneMesi);
|
||||||
|
|
||||||
$field = match ($gestione->tipo_gestione) {
|
$field = match ($gestione->tipo_gestione) {
|
||||||
'ordinaria' => 'mesi_rate_ordinaria',
|
'ordinaria' => 'mesi_rate_ordinaria',
|
||||||
'riscaldamento' => 'mesi_rate_riscaldamento',
|
'riscaldamento' => 'mesi_rate_riscaldamento',
|
||||||
'straordinaria' => 'mesi_rate_straordinaria',
|
'straordinaria' => 'mesi_rate_straordinaria',
|
||||||
default => 'mesi_rate_ordinaria',
|
default => 'mesi_rate_ordinaria',
|
||||||
};
|
};
|
||||||
|
|
||||||
$gestione->{$field} = $mesi;
|
$gestione->{$field} = $mesi;
|
||||||
|
|
||||||
if ($gestione->tipo_gestione === 'straordinaria') {
|
if ($gestione->tipo_gestione === 'straordinaria') {
|
||||||
$piano = (array) ($gestione->piano_straordinario ?? []);
|
$piano = (array) ($gestione->piano_straordinario ?? []);
|
||||||
$piano['numero_rate'] = $this->gestioneNumeroRate;
|
$piano['numero_rate'] = $this->gestioneNumeroRate;
|
||||||
$piano['cadenza'] = $this->gestioneCadenza;
|
$piano['cadenza'] = $this->gestioneCadenza;
|
||||||
$gestione->piano_straordinario = $piano;
|
$gestione->piano_straordinario = $piano;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -254,9 +296,9 @@ public function saveChecklistFineAnno(): void
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$config = (array) ($this->stabile->configurazione_avanzata ?? []);
|
$config = (array) ($this->stabile->configurazione_avanzata ?? []);
|
||||||
$config['gestione_ordinaria_id'] = $this->gestioneOrdinariaId;
|
$config['gestione_ordinaria_id'] = $this->gestioneOrdinariaId;
|
||||||
$config['checklist_fine_anno_state'] = $this->checklistFineAnnoState;
|
$config['checklist_fine_anno_state'] = $this->checklistFineAnnoState;
|
||||||
$this->stabile->configurazione_avanzata = $config;
|
$this->stabile->configurazione_avanzata = $config;
|
||||||
$this->stabile->save();
|
$this->stabile->save();
|
||||||
|
|
||||||
|
|
@ -282,22 +324,22 @@ private function loadGestioneSelezionata(): void
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->gestioneSelezionataId = $gestione->id;
|
$this->gestioneSelezionataId = $gestione->id;
|
||||||
$this->gestioneSelezionataTipo = $gestione->tipo_gestione;
|
$this->gestioneSelezionataTipo = $gestione->tipo_gestione;
|
||||||
$this->gestioneDataInizio = $gestione->data_inizio?->format('Y-m-d');
|
$this->gestioneDataInizio = $gestione->data_inizio?->format('Y-m-d');
|
||||||
$this->gestioneDataFine = $gestione->data_fine?->format('Y-m-d');
|
$this->gestioneDataFine = $gestione->data_fine?->format('Y-m-d');
|
||||||
|
|
||||||
$mesi = match ($gestione->tipo_gestione) {
|
$mesi = match ($gestione->tipo_gestione) {
|
||||||
'ordinaria' => (array) ($gestione->mesi_rate_ordinaria ?? []),
|
'ordinaria' => (array) ($gestione->mesi_rate_ordinaria ?? []),
|
||||||
'riscaldamento' => (array) ($gestione->mesi_rate_riscaldamento ?? []),
|
'riscaldamento' => (array) ($gestione->mesi_rate_riscaldamento ?? []),
|
||||||
'straordinaria' => (array) ($gestione->mesi_rate_straordinaria ?? []),
|
'straordinaria' => (array) ($gestione->mesi_rate_straordinaria ?? []),
|
||||||
default => [],
|
default => [],
|
||||||
};
|
};
|
||||||
$this->gestioneMesi = $this->normalizeMesi($mesi);
|
$this->gestioneMesi = $this->normalizeMesi($mesi);
|
||||||
|
|
||||||
$piano = (array) ($gestione->piano_straordinario ?? []);
|
$piano = (array) ($gestione->piano_straordinario ?? []);
|
||||||
$this->gestioneNumeroRate = isset($piano['numero_rate']) ? (int) $piano['numero_rate'] : null;
|
$this->gestioneNumeroRate = isset($piano['numero_rate']) ? (int) $piano['numero_rate'] : null;
|
||||||
$this->gestioneCadenza = $piano['cadenza'] ?? null;
|
$this->gestioneCadenza = $piano['cadenza'] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param array<int, int|string> $mesi */
|
/** @param array<int, int|string> $mesi */
|
||||||
|
|
@ -307,4 +349,266 @@ private function normalizeMesi(array $mesi): array
|
||||||
sort($mesi);
|
sort($mesi);
|
||||||
return $mesi;
|
return $mesi;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \Illuminate\Database\Eloquent\Collection<int, InsurancePolicy>
|
||||||
|
*/
|
||||||
|
public function getInsurancePoliciesProperty()
|
||||||
|
{
|
||||||
|
if (! $this->stabile instanceof StabileModel) {
|
||||||
|
return InsurancePolicy::query()->whereRaw('1 = 0')->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
return InsurancePolicy::query()
|
||||||
|
->with(['brokerRubrica', 'companyRubrica'])
|
||||||
|
->where('stabile_id', (int) $this->stabile->id)
|
||||||
|
->orderByDesc('renewal_at')
|
||||||
|
->orderByDesc('expires_at')
|
||||||
|
->orderByDesc('id')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \Illuminate\Database\Eloquent\Collection<int, InsuranceClaim>
|
||||||
|
*/
|
||||||
|
public function getInsuranceClaimsRowsProperty()
|
||||||
|
{
|
||||||
|
if (! $this->stabile instanceof StabileModel) {
|
||||||
|
return InsuranceClaim::query()->whereRaw('1 = 0')->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
return InsuranceClaim::query()
|
||||||
|
->with(['ticket', 'insurancePolicy'])
|
||||||
|
->where('stabile_id', (int) $this->stabile->id)
|
||||||
|
->when(
|
||||||
|
(int) ($this->selectedInsurancePolicyId ?? 0) > 0,
|
||||||
|
fn($query) => $query->where('insurance_policy_id', (int) $this->selectedInsurancePolicyId)
|
||||||
|
)
|
||||||
|
->latest('opened_at')
|
||||||
|
->latest('id')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public function getInsuranceRubricaOptionsProperty(): array
|
||||||
|
{
|
||||||
|
return RubricaUniversale::query()
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->whereIn('categoria', ['assicurazione', 'fornitore', 'altro'])
|
||||||
|
->orderBy('ragione_sociale')
|
||||||
|
->orderBy('cognome')
|
||||||
|
->orderBy('nome')
|
||||||
|
->limit(300)
|
||||||
|
->get(['id', 'ragione_sociale', 'nome', 'cognome'])
|
||||||
|
->mapWithKeys(function (RubricaUniversale $rubrica): array {
|
||||||
|
$label = trim((string) ($rubrica->ragione_sociale ?: trim(($rubrica->nome ?? '') . ' ' . ($rubrica->cognome ?? ''))));
|
||||||
|
return [(int) $rubrica->id => ($label !== '' ? $label : ('Rubrica #' . (int) $rubrica->id))];
|
||||||
|
})
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function selectInsurancePolicy(int $policyId): void
|
||||||
|
{
|
||||||
|
$policy = $this->insurancePolicies->firstWhere('id', $policyId);
|
||||||
|
if (! $policy instanceof InsurancePolicy) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->selectedInsurancePolicyId = (int) $policy->id;
|
||||||
|
$this->insurancePolicyForm = [
|
||||||
|
'broker_rubrica_id' => $policy->broker_rubrica_id,
|
||||||
|
'company_rubrica_id' => $policy->company_rubrica_id,
|
||||||
|
'policy_name' => (string) ($policy->policy_name ?? ''),
|
||||||
|
'policy_number' => (string) ($policy->policy_number ?? ''),
|
||||||
|
'policy_reference' => (string) ($policy->policy_reference ?? ''),
|
||||||
|
'status' => (string) ($policy->status ?? 'attiva'),
|
||||||
|
'started_at' => optional($policy->started_at)->format('Y-m-d'),
|
||||||
|
'expires_at' => optional($policy->expires_at)->format('Y-m-d'),
|
||||||
|
'renewal_at' => optional($policy->renewal_at)->format('Y-m-d'),
|
||||||
|
'annual_amount' => $policy->annual_amount,
|
||||||
|
'payment_split' => (string) ($policy->payment_split ?? ''),
|
||||||
|
'payment_schedule_notes' => (string) ($policy->payment_schedule_notes ?? ''),
|
||||||
|
'coverage_summary' => (string) ($policy->coverage_summary ?? ''),
|
||||||
|
'special_conditions' => (string) ($policy->special_conditions ?? ''),
|
||||||
|
'claim_form_template' => (string) ($policy->claim_form_template ?? ''),
|
||||||
|
'notes' => (string) ($policy->notes ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resetInsurancePolicyForm(): void
|
||||||
|
{
|
||||||
|
$this->selectedInsurancePolicyId = null;
|
||||||
|
$this->insurancePolicyForm = [
|
||||||
|
'broker_rubrica_id' => null,
|
||||||
|
'company_rubrica_id' => null,
|
||||||
|
'policy_name' => '',
|
||||||
|
'policy_number' => '',
|
||||||
|
'policy_reference' => '',
|
||||||
|
'status' => 'attiva',
|
||||||
|
'started_at' => null,
|
||||||
|
'expires_at' => null,
|
||||||
|
'renewal_at' => null,
|
||||||
|
'annual_amount' => null,
|
||||||
|
'payment_split' => '',
|
||||||
|
'payment_schedule_notes' => '',
|
||||||
|
'coverage_summary' => '',
|
||||||
|
'special_conditions' => '',
|
||||||
|
'claim_form_template' => '',
|
||||||
|
'notes' => '',
|
||||||
|
];
|
||||||
|
$this->insurancePolicyDocumentUpload = null;
|
||||||
|
$this->insuranceSignatureUpload = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveInsurancePolicy(): void
|
||||||
|
{
|
||||||
|
if (! $this->stabile instanceof StabileModel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = validator($this->insurancePolicyForm, [
|
||||||
|
'broker_rubrica_id' => ['nullable', 'integer', 'exists:rubrica_universale,id'],
|
||||||
|
'company_rubrica_id' => ['nullable', 'integer', 'exists:rubrica_universale,id'],
|
||||||
|
'policy_name' => ['nullable', 'string', 'max:255'],
|
||||||
|
'policy_number' => ['nullable', 'string', 'max:255'],
|
||||||
|
'policy_reference' => ['nullable', 'string', 'max:255'],
|
||||||
|
'status' => ['required', 'string', 'max:50'],
|
||||||
|
'started_at' => ['nullable', 'date'],
|
||||||
|
'expires_at' => ['nullable', 'date'],
|
||||||
|
'renewal_at' => ['nullable', 'date'],
|
||||||
|
'annual_amount' => ['nullable', 'numeric'],
|
||||||
|
'payment_split' => ['nullable', 'string', 'max:255'],
|
||||||
|
'payment_schedule_notes' => ['nullable', 'string', 'max:5000'],
|
||||||
|
'coverage_summary' => ['nullable', 'string', 'max:10000'],
|
||||||
|
'special_conditions' => ['nullable', 'string', 'max:20000'],
|
||||||
|
'claim_form_template' => ['nullable', 'string', 'max:20000'],
|
||||||
|
'notes' => ['nullable', 'string', 'max:10000'],
|
||||||
|
])->validate();
|
||||||
|
|
||||||
|
$policy = $this->selectedInsurancePolicyId
|
||||||
|
? InsurancePolicy::query()->where('stabile_id', (int) $this->stabile->id)->find($this->selectedInsurancePolicyId)
|
||||||
|
: new InsurancePolicy();
|
||||||
|
|
||||||
|
if (! $policy instanceof InsurancePolicy) {
|
||||||
|
$policy = new InsurancePolicy();
|
||||||
|
}
|
||||||
|
|
||||||
|
$policy->fill([
|
||||||
|
'stabile_id' => (int) $this->stabile->id,
|
||||||
|
'broker_rubrica_id' => (int) ($data['broker_rubrica_id'] ?? 0) ?: null,
|
||||||
|
'company_rubrica_id' => (int) ($data['company_rubrica_id'] ?? 0) ?: null,
|
||||||
|
'policy_name' => $this->cleanInsuranceNullable($data['policy_name'] ?? null),
|
||||||
|
'policy_number' => $this->cleanInsuranceNullable($data['policy_number'] ?? null),
|
||||||
|
'policy_reference' => $this->cleanInsuranceNullable($data['policy_reference'] ?? null),
|
||||||
|
'status' => $this->cleanInsuranceNullable($data['status'] ?? 'attiva') ?: 'attiva',
|
||||||
|
'started_at' => $data['started_at'] ?? null,
|
||||||
|
'expires_at' => $data['expires_at'] ?? null,
|
||||||
|
'renewal_at' => $data['renewal_at'] ?? null,
|
||||||
|
'annual_amount' => $data['annual_amount'] ?? null,
|
||||||
|
'payment_split' => $this->cleanInsuranceNullable($data['payment_split'] ?? null),
|
||||||
|
'payment_schedule_notes' => $this->cleanInsuranceNullable($data['payment_schedule_notes'] ?? null),
|
||||||
|
'coverage_summary' => $this->cleanInsuranceNullable($data['coverage_summary'] ?? null),
|
||||||
|
'special_conditions' => $this->cleanInsuranceNullable($data['special_conditions'] ?? null),
|
||||||
|
'claim_form_template' => $this->cleanInsuranceNullable($data['claim_form_template'] ?? null),
|
||||||
|
'notes' => $this->cleanInsuranceNullable($data['notes'] ?? null),
|
||||||
|
'created_by' => Auth::id(),
|
||||||
|
]);
|
||||||
|
$policy->save();
|
||||||
|
|
||||||
|
$this->storeInsuranceUploads($policy);
|
||||||
|
$this->selectedInsurancePolicyId = (int) $policy->id;
|
||||||
|
|
||||||
|
Notification::make()->title('Polizza assicurativa salvata')->success()->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteInsurancePolicy(int $policyId): void
|
||||||
|
{
|
||||||
|
if (! $this->stabile instanceof StabileModel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$policy = InsurancePolicy::query()->where('stabile_id', (int) $this->stabile->id)->find($policyId);
|
||||||
|
if (! $policy instanceof InsurancePolicy) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$policy->delete();
|
||||||
|
|
||||||
|
if ((int) ($this->selectedInsurancePolicyId ?? 0) === (int) $policyId) {
|
||||||
|
$this->resetInsurancePolicyForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
Notification::make()->title('Polizza assicurativa eliminata')->success()->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getInsuranceDocumentUrl(InsurancePolicy $policy): ?string
|
||||||
|
{
|
||||||
|
$path = trim((string) ($policy->policy_document_path ?? ''));
|
||||||
|
return $path !== '' ? Storage::disk('public')->url($path) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getInsuranceSignatureUrl(InsurancePolicy $policy): ?string
|
||||||
|
{
|
||||||
|
$path = trim((string) ($policy->signature_image_path ?? ''));
|
||||||
|
return $path !== '' ? Storage::disk('public')->url($path) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getInsuranceTicketUrl(?Ticket $ticket): ?string
|
||||||
|
{
|
||||||
|
if (! $ticket instanceof Ticket) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return \App\Filament\Pages\Supporto\TicketGestione::getUrl(['ticket' => (int) $ticket->id, 'tab' => 'scheda'], panel: 'admin-filament');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function storeInsuranceUploads(InsurancePolicy $policy): void
|
||||||
|
{
|
||||||
|
if (is_object($this->insurancePolicyDocumentUpload) && method_exists($this->insurancePolicyDocumentUpload, 'storeAs')) {
|
||||||
|
$ext = strtolower((string) ($this->insurancePolicyDocumentUpload->getClientOriginalExtension() ?: 'pdf'));
|
||||||
|
$fileName = 'stabile-' . (int) $policy->stabile_id . '-polizza-' . (int) $policy->id . '-documento.' . $ext;
|
||||||
|
$path = $this->insurancePolicyDocumentUpload->storeAs('assicurazioni/polizze', $fileName, 'public');
|
||||||
|
$policy->policy_document_path = $path;
|
||||||
|
$policy->policy_document_name = (string) $this->insurancePolicyDocumentUpload->getClientOriginalName();
|
||||||
|
$policy->policy_document_mime = (string) $this->insurancePolicyDocumentUpload->getClientMimeType();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_object($this->insuranceSignatureUpload) && method_exists($this->insuranceSignatureUpload, 'storeAs')) {
|
||||||
|
$ext = strtolower((string) ($this->insuranceSignatureUpload->getClientOriginalExtension() ?: 'png'));
|
||||||
|
$fileName = 'stabile-' . (int) $policy->stabile_id . '-polizza-' . (int) $policy->id . '-firma.' . $ext;
|
||||||
|
$policy->signature_image_path = $this->insuranceSignatureUpload->storeAs('assicurazioni/firme', $fileName, 'public');
|
||||||
|
}
|
||||||
|
|
||||||
|
$policy->save();
|
||||||
|
$this->insurancePolicyDocumentUpload = null;
|
||||||
|
$this->insuranceSignatureUpload = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function cleanInsuranceNullable($value): ?string
|
||||||
|
{
|
||||||
|
$value = trim((string) $value);
|
||||||
|
return $value !== '' ? $value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extractLegacyRateMonthsFromConfig(array $config, string $section): array
|
||||||
|
{
|
||||||
|
$flags = (array) data_get($config, 'rata_flags.' . $section, []);
|
||||||
|
$months = [];
|
||||||
|
|
||||||
|
foreach (range(1, 12) as $month) {
|
||||||
|
$value = $flags[$section . '_rata_' . $month] ?? null;
|
||||||
|
if ($value === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized = strtolower(trim((string) $value));
|
||||||
|
if ($normalized !== '' && ! in_array($normalized, ['0', 'false', 'no', 'off', 'n'], true)) {
|
||||||
|
$months[] = $month;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->normalizeMesi($months);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Pages\Condomini;
|
namespace App\Filament\Pages\Condomini;
|
||||||
|
|
||||||
use App\Models\Stabile;
|
use App\Models\Stabile;
|
||||||
|
|
@ -45,7 +44,7 @@ public static function canAccess(): bool
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
return $user instanceof User
|
return $user instanceof User
|
||||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mount(): void
|
public function mount(): void
|
||||||
|
|
@ -66,15 +65,15 @@ public function mount(): void
|
||||||
$posta = (array) Arr::get($stabile->configurazione_avanzata ?? [], 'posta', []);
|
$posta = (array) Arr::get($stabile->configurazione_avanzata ?? [], 'posta', []);
|
||||||
|
|
||||||
$this->getSchema('form')?->fill([
|
$this->getSchema('form')?->fill([
|
||||||
'pec_condominio' => (string) ($stabile->pec_condominio ?? ''),
|
'pec_condominio' => (string) ($stabile->pec_condominio ?? ''),
|
||||||
'pec_amministratore' => (string) ($stabile->pec_amministratore ?? ''),
|
'pec_amministratore' => (string) ($stabile->pec_amministratore ?? ''),
|
||||||
'posta' => array_merge([
|
'posta' => array_merge([
|
||||||
'caselle' => [],
|
'caselle' => [],
|
||||||
'acquisizione' => [
|
'acquisizione' => [
|
||||||
'salva_documenti' => true,
|
'salva_documenti' => true,
|
||||||
'salva_contabilita' => true,
|
'salva_contabilita' => true,
|
||||||
'mittenti_assicurazione' => '',
|
'mittenti_assicurazione' => '',
|
||||||
'descrizione_default' => 'Documento acquisito da casella stabile',
|
'descrizione_default' => 'Documento acquisito da casella stabile',
|
||||||
],
|
],
|
||||||
], $posta),
|
], $posta),
|
||||||
]);
|
]);
|
||||||
|
|
@ -112,8 +111,8 @@ public function form(Schema $schema): Schema
|
||||||
->label('Tipo casella')
|
->label('Tipo casella')
|
||||||
->options([
|
->options([
|
||||||
'gmail' => 'Gmail / Google Workspace',
|
'gmail' => 'Gmail / Google Workspace',
|
||||||
'imap' => 'IMAP ordinaria',
|
'imap' => 'IMAP ordinaria',
|
||||||
'pec' => 'PEC via IMAP',
|
'pec' => 'PEC via IMAP',
|
||||||
])
|
])
|
||||||
->default('imap')
|
->default('imap')
|
||||||
->required(),
|
->required(),
|
||||||
|
|
@ -134,8 +133,8 @@ public function form(Schema $schema): Schema
|
||||||
Select::make('encryption')
|
Select::make('encryption')
|
||||||
->label('Cifratura')
|
->label('Cifratura')
|
||||||
->options([
|
->options([
|
||||||
'ssl' => 'SSL',
|
'ssl' => 'SSL',
|
||||||
'tls' => 'TLS',
|
'tls' => 'TLS',
|
||||||
'none' => 'Nessuna',
|
'none' => 'Nessuna',
|
||||||
])
|
])
|
||||||
->default('ssl'),
|
->default('ssl'),
|
||||||
|
|
@ -187,12 +186,12 @@ public function save(): void
|
||||||
{
|
{
|
||||||
abort_unless($this->stabile instanceof Stabile, 404);
|
abort_unless($this->stabile instanceof Stabile, 404);
|
||||||
|
|
||||||
$state = is_array($this->data ?? null) ? $this->data : [];
|
$state = is_array($this->data ?? null) ? $this->data : [];
|
||||||
$config = (array) ($this->stabile->configurazione_avanzata ?? []);
|
$config = (array) ($this->stabile->configurazione_avanzata ?? []);
|
||||||
$config['posta'] = is_array($state['posta'] ?? null) ? $state['posta'] : [];
|
$config['posta'] = is_array($state['posta'] ?? null) ? $state['posta'] : [];
|
||||||
|
|
||||||
$this->stabile->pec_condominio = trim((string) ($state['pec_condominio'] ?? '')) ?: null;
|
$this->stabile->pec_condominio = trim((string) ($state['pec_condominio'] ?? '')) ?: null;
|
||||||
$this->stabile->pec_amministratore = trim((string) ($state['pec_amministratore'] ?? '')) ?: null;
|
$this->stabile->pec_amministratore = trim((string) ($state['pec_amministratore'] ?? '')) ?: null;
|
||||||
$this->stabile->configurazione_avanzata = $config;
|
$this->stabile->configurazione_avanzata = $config;
|
||||||
$this->stabile->save();
|
$this->stabile->save();
|
||||||
|
|
||||||
|
|
@ -201,4 +200,4 @@ public function save(): void
|
||||||
->success()
|
->success()
|
||||||
->send();
|
->send();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
177
app/Filament/Pages/Fiscale/FiscaleDashboard.php
Normal file
177
app/Filament/Pages/Fiscale/FiscaleDashboard.php
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
<?php
|
||||||
|
namespace App\Filament\Pages\Fiscale;
|
||||||
|
|
||||||
|
use App\Filament\Pages\Contabilita\RegistroRitenuteAccontoArchivio;
|
||||||
|
use App\Models\AdempimentoFiscale;
|
||||||
|
use App\Models\RegistroRitenuteAcconto;
|
||||||
|
use App\Models\Stabile;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Support\StabileContext;
|
||||||
|
use BackedEnum;
|
||||||
|
use Filament\Pages\Page;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use UnitEnum;
|
||||||
|
|
||||||
|
class FiscaleDashboard extends Page
|
||||||
|
{
|
||||||
|
protected static ?string $navigationLabel = 'Cruscotto Fiscale';
|
||||||
|
|
||||||
|
protected static ?string $title = 'Cruscotto fiscale';
|
||||||
|
|
||||||
|
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-receipt-percent';
|
||||||
|
|
||||||
|
protected static UnitEnum|string|null $navigationGroup = 'Fiscale';
|
||||||
|
|
||||||
|
protected static ?int $navigationSort = 1;
|
||||||
|
|
||||||
|
protected static ?string $slug = 'fiscale/cruscotto';
|
||||||
|
|
||||||
|
protected string $view = 'filament.pages.fiscale.cruscotto';
|
||||||
|
|
||||||
|
public ?Stabile $stabileAttivo = null;
|
||||||
|
|
||||||
|
public int $annoFiscale;
|
||||||
|
|
||||||
|
public array $summary = [];
|
||||||
|
|
||||||
|
public Collection $adempimentiInScadenza;
|
||||||
|
|
||||||
|
public array $templates = [];
|
||||||
|
|
||||||
|
public static function canAccess(): bool
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
if (! $user instanceof User) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->hasAnyRole(['super-admin', 'admin'])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user->hasAnyRole(['amministratore', 'collaboratore']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mount(): void
|
||||||
|
{
|
||||||
|
$this->annoFiscale = (int) now()->format('Y');
|
||||||
|
$this->adempimentiInScadenza = collect();
|
||||||
|
|
||||||
|
$user = Auth::user();
|
||||||
|
if (! $user instanceof User) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$activeId = StabileContext::resolveActiveStabileId($user);
|
||||||
|
if (! $activeId) {
|
||||||
|
$this->stabileAttivo = null;
|
||||||
|
$this->summary = $this->emptySummary();
|
||||||
|
$this->templates = $this->defaultTemplates();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->stabileAttivo = StabileContext::accessibleStabili($user)->firstWhere('id', $activeId);
|
||||||
|
$this->summary = $this->buildSummary($activeId, $this->annoFiscale);
|
||||||
|
$this->adempimentiInScadenza = AdempimentoFiscale::query()
|
||||||
|
->where('stabile_id', $activeId)
|
||||||
|
->orderByRaw('CASE WHEN scadenza IS NULL THEN 1 ELSE 0 END')
|
||||||
|
->orderBy('scadenza')
|
||||||
|
->limit(12)
|
||||||
|
->get();
|
||||||
|
$this->templates = $this->defaultTemplates();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRegistroRaUrl(): string
|
||||||
|
{
|
||||||
|
return RegistroRitenuteAccontoArchivio::getUrl(panel: 'admin-filament');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function emptySummary(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'ritenute_count' => 0,
|
||||||
|
'ritenute_da_versare' => 0,
|
||||||
|
'detrazioni_count' => 0,
|
||||||
|
'straordinarie_fiscali' => 0,
|
||||||
|
'adempimenti_count' => 0,
|
||||||
|
'adempimenti_aperti' => 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildSummary(int $stabileId, int $annoFiscale): array
|
||||||
|
{
|
||||||
|
$summary = $this->emptySummary();
|
||||||
|
|
||||||
|
$summary['ritenute_count'] = RegistroRitenuteAcconto::query()
|
||||||
|
->whereHas('gestione', fn($q) => $q->where('stabile_id', $stabileId))
|
||||||
|
->where('anno_dichiarazione', $annoFiscale)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$summary['ritenute_da_versare'] = RegistroRitenuteAcconto::query()
|
||||||
|
->whereHas('gestione', fn($q) => $q->where('stabile_id', $stabileId))
|
||||||
|
->where('anno_dichiarazione', $annoFiscale)
|
||||||
|
->whereIn('stato_versamento', ['da_versare', 'scaduto', 'ritardo'])
|
||||||
|
->count();
|
||||||
|
|
||||||
|
if (Schema::hasTable('detrazioni_fiscali_dom')) {
|
||||||
|
$summary['detrazioni_count'] = DB::table('detrazioni_fiscali_dom as dfd')
|
||||||
|
->leftJoin('gestioni_contabili as gc', 'gc.id', '=', 'dfd.gestione_id')
|
||||||
|
->where(function ($q) use ($stabileId): void {
|
||||||
|
$q->where('gc.stabile_id', $stabileId);
|
||||||
|
|
||||||
|
if (Schema::hasColumn('detrazioni_fiscali_dom', 'stabile_id')) {
|
||||||
|
$q->orWhere('dfd.stabile_id', $stabileId);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->where(function ($q) use ($annoFiscale): void {
|
||||||
|
$q->whereYear('dfd.created_at', $annoFiscale);
|
||||||
|
|
||||||
|
if (Schema::hasColumn('detrazioni_fiscali_dom', 'anno_fiscale')) {
|
||||||
|
$q->orWhere('dfd.anno_fiscale', $annoFiscale);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Schema::hasColumn('detrazioni_fiscali_dom', 'legacy_rif')) {
|
||||||
|
$q->orWhere('dfd.legacy_rif', 'like', $annoFiscale . '%');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Schema::hasTable('lavori_straordinari')) {
|
||||||
|
$summary['straordinarie_fiscali'] = DB::table('lavori_straordinari as ls')
|
||||||
|
->join('gestioni_contabili as gc', 'gc.id', '=', 'ls.gestione_id')
|
||||||
|
->where('gc.stabile_id', $stabileId)
|
||||||
|
->where('gc.anno_gestione', $annoFiscale)
|
||||||
|
->whereNotNull('ls.gestione_id')
|
||||||
|
->where('ls.note_json', 'like', '%"is_fiscal_declaration":true%')
|
||||||
|
->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
$summary['adempimenti_count'] = AdempimentoFiscale::query()
|
||||||
|
->where('stabile_id', $stabileId)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$summary['adempimenti_aperti'] = AdempimentoFiscale::query()
|
||||||
|
->where('stabile_id', $stabileId)
|
||||||
|
->whereNotIn('stato', ['completato', 'annullato'])
|
||||||
|
->count();
|
||||||
|
|
||||||
|
return $summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function defaultTemplates(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['tipo' => 'ritenuta_acconto', 'titolo' => 'Ritenute d\'acconto', 'descrizione' => 'Versamenti, F24, CU e quadratura come sostituto d\'imposta.'],
|
||||||
|
['tipo' => 'certificazione_detrazione', 'titolo' => 'Certificazioni detrazioni fiscali', 'descrizione' => 'Invio certificazioni e archivio documentale per lavori agevolati.'],
|
||||||
|
['tipo' => 'quadro_ac', 'titolo' => 'Quadro AC amministratore', 'descrizione' => 'Raccolta dati e controllo annuale dei fornitori e compensi rilevanti.'],
|
||||||
|
['tipo' => 'dichiarazione_770', 'titolo' => 'Modello 770', 'descrizione' => 'Preparazione dichiarazione, quadratura ritenute e storico invii.'],
|
||||||
|
['tipo' => 'rendita_condominiale', 'titolo' => 'Rendite condominiali e affitti', 'descrizione' => 'Monitoraggio rendite, contratti, certificazioni e imponibili collegati.'],
|
||||||
|
['tipo' => 'compenso_amministratore', 'titolo' => 'Compensi amministratore', 'descrizione' => 'Preventivo, rinnovi annuali e voci addebitabili per servizi e pratiche.'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
use App\Filament\Pages\Fornitore\Concerns\ResolvesOperatoreContext;
|
use App\Filament\Pages\Fornitore\Concerns\ResolvesOperatoreContext;
|
||||||
use App\Models\Fornitore;
|
use App\Models\Fornitore;
|
||||||
use App\Models\FornitoreDipendente;
|
use App\Models\FornitoreDipendente;
|
||||||
|
use App\Models\RubricaUniversale;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
|
|
@ -36,14 +37,46 @@ class Collaboratori extends Page
|
||||||
|
|
||||||
public bool $missingAdminContext = false;
|
public bool $missingAdminContext = false;
|
||||||
|
|
||||||
|
public string $activeTab = 'nuovo';
|
||||||
|
|
||||||
public string $nome = '';
|
public string $nome = '';
|
||||||
|
|
||||||
public string $cognome = '';
|
public string $cognome = '';
|
||||||
|
|
||||||
|
public string $tipoContatto = 'persona_fisica';
|
||||||
|
|
||||||
|
public string $ragioneSociale = '';
|
||||||
|
|
||||||
|
public string $codiceFiscale = '';
|
||||||
|
|
||||||
|
public string $partitaIva = '';
|
||||||
|
|
||||||
|
public string $pec = '';
|
||||||
|
|
||||||
|
public string $sesso = '';
|
||||||
|
|
||||||
|
public string $dataNascita = '';
|
||||||
|
|
||||||
|
public string $luogoNascita = '';
|
||||||
|
|
||||||
|
public string $provinciaNascita = '';
|
||||||
|
|
||||||
public string $email = '';
|
public string $email = '';
|
||||||
|
|
||||||
public string $telefono = '';
|
public string $telefono = '';
|
||||||
|
|
||||||
|
public string $indirizzo = '';
|
||||||
|
|
||||||
|
public string $civico = '';
|
||||||
|
|
||||||
|
public string $cap = '';
|
||||||
|
|
||||||
|
public string $citta = '';
|
||||||
|
|
||||||
|
public string $provincia = '';
|
||||||
|
|
||||||
|
public string $nazione = 'Italia';
|
||||||
|
|
||||||
public string $collaboratoreTipo = FornitoreDipendente::TIPO_INTERNO;
|
public string $collaboratoreTipo = FornitoreDipendente::TIPO_INTERNO;
|
||||||
|
|
||||||
public string $fornitoreSearch = '';
|
public string $fornitoreSearch = '';
|
||||||
|
|
@ -84,6 +117,7 @@ public function mount(): void
|
||||||
$this->fornitore = $fornitore;
|
$this->fornitore = $fornitore;
|
||||||
$this->refreshFornitoreOptions();
|
$this->refreshFornitoreOptions();
|
||||||
$this->refreshRows();
|
$this->refreshRows();
|
||||||
|
$this->activeTab = $this->rows !== [] ? 'elenco' : 'nuovo';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updatedCollaboratoreTipo(): void
|
public function updatedCollaboratoreTipo(): void
|
||||||
|
|
@ -110,10 +144,25 @@ public function createCollaboratore(): void
|
||||||
|
|
||||||
$validated = $this->validate([
|
$validated = $this->validate([
|
||||||
'collaboratoreTipo' => ['required', 'string', 'in:' . implode(',', [FornitoreDipendente::TIPO_INTERNO, FornitoreDipendente::TIPO_FORNITORE_ESTERNO])],
|
'collaboratoreTipo' => ['required', 'string', 'in:' . implode(',', [FornitoreDipendente::TIPO_INTERNO, FornitoreDipendente::TIPO_FORNITORE_ESTERNO])],
|
||||||
|
'tipoContatto' => ['nullable', 'string', 'in:persona_fisica,persona_giuridica'],
|
||||||
'nome' => ['nullable', 'string', 'max:100'],
|
'nome' => ['nullable', 'string', 'max:100'],
|
||||||
'cognome' => ['nullable', 'string', 'max:100'],
|
'cognome' => ['nullable', 'string', 'max:100'],
|
||||||
|
'ragioneSociale' => ['nullable', 'string', 'max:255'],
|
||||||
|
'codiceFiscale' => ['nullable', 'string', 'max:50'],
|
||||||
|
'partitaIva' => ['nullable', 'string', 'max:50'],
|
||||||
|
'pec' => ['nullable', 'string', 'max:255'],
|
||||||
|
'sesso' => ['nullable', 'string', 'max:20'],
|
||||||
|
'dataNascita' => ['nullable', 'date'],
|
||||||
|
'luogoNascita' => ['nullable', 'string', 'max:255'],
|
||||||
|
'provinciaNascita' => ['nullable', 'string', 'max:10'],
|
||||||
'email' => ['nullable', 'email', 'max:255'],
|
'email' => ['nullable', 'email', 'max:255'],
|
||||||
'telefono' => ['nullable', 'string', 'max:50'],
|
'telefono' => ['nullable', 'string', 'max:50'],
|
||||||
|
'indirizzo' => ['nullable', 'string', 'max:255'],
|
||||||
|
'civico' => ['nullable', 'string', 'max:20'],
|
||||||
|
'cap' => ['nullable', 'string', 'max:20'],
|
||||||
|
'citta' => ['nullable', 'string', 'max:255'],
|
||||||
|
'provincia' => ['nullable', 'string', 'max:10'],
|
||||||
|
'nazione' => ['nullable', 'string', 'max:100'],
|
||||||
'note' => ['nullable', 'string', 'max:2000'],
|
'note' => ['nullable', 'string', 'max:2000'],
|
||||||
'createUserAccess' => ['nullable', 'boolean'],
|
'createUserAccess' => ['nullable', 'boolean'],
|
||||||
'accessPassword' => ['nullable', 'string', 'min:8', 'max:100'],
|
'accessPassword' => ['nullable', 'string', 'min:8', 'max:100'],
|
||||||
|
|
@ -121,10 +170,21 @@ public function createCollaboratore(): void
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$tipo = (string) $validated['collaboratoreTipo'];
|
$tipo = (string) $validated['collaboratoreTipo'];
|
||||||
|
$tipoContatto = (string) ($validated['tipoContatto'] ?? 'persona_fisica');
|
||||||
|
|
||||||
if ($tipo === FornitoreDipendente::TIPO_INTERNO && trim((string) ($validated['nome'] ?? '')) === '') {
|
if ($tipo === FornitoreDipendente::TIPO_INTERNO) {
|
||||||
Notification::make()->title('Inserisci il nome del collaboratore')->warning()->send();
|
$hasPersonalName = trim((string) ($validated['nome'] ?? '')) !== '';
|
||||||
return;
|
$hasCompanyName = trim((string) ($validated['ragioneSociale'] ?? '')) !== '';
|
||||||
|
|
||||||
|
if ($tipoContatto === 'persona_giuridica' && ! $hasCompanyName) {
|
||||||
|
Notification::make()->title('Inserisci la ragione sociale del collaboratore')->warning()->send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($tipoContatto !== 'persona_giuridica' && ! $hasPersonalName) {
|
||||||
|
Notification::make()->title('Inserisci il nome del collaboratore')->warning()->send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$fornitoreEsterno = null;
|
$fornitoreEsterno = null;
|
||||||
|
|
@ -170,8 +230,10 @@ public function createCollaboratore(): void
|
||||||
$dipendente->tipo_collaboratore = $tipo;
|
$dipendente->tipo_collaboratore = $tipo;
|
||||||
$dipendente->nome = $tipo === FornitoreDipendente::TIPO_FORNITORE_ESTERNO
|
$dipendente->nome = $tipo === FornitoreDipendente::TIPO_FORNITORE_ESTERNO
|
||||||
? (string) ($fornitoreEsterno?->ragione_sociale ?: $fornitoreEsterno?->nome ?: 'Subfornitore')
|
? (string) ($fornitoreEsterno?->ragione_sociale ?: $fornitoreEsterno?->nome ?: 'Subfornitore')
|
||||||
: trim((string) $validated['nome']);
|
: ($tipoContatto === 'persona_giuridica'
|
||||||
$dipendente->cognome = $tipo === FornitoreDipendente::TIPO_FORNITORE_ESTERNO
|
? trim((string) ($validated['ragioneSociale'] ?? ''))
|
||||||
|
: trim((string) $validated['nome']));
|
||||||
|
$dipendente->cognome = $tipo === FornitoreDipendente::TIPO_FORNITORE_ESTERNO || $tipoContatto === 'persona_giuridica'
|
||||||
? null
|
? null
|
||||||
: (trim((string) ($validated['cognome'] ?? '')) ?: null);
|
: (trim((string) ($validated['cognome'] ?? '')) ?: null);
|
||||||
$dipendente->email = $email !== '' ? $email : null;
|
$dipendente->email = $email !== '' ? $email : null;
|
||||||
|
|
@ -215,10 +277,15 @@ public function createCollaboratore(): void
|
||||||
$dipendente->user_id = (int) $user->id;
|
$dipendente->user_id = (int) $user->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($tipo === FornitoreDipendente::TIPO_INTERNO) {
|
||||||
|
$dipendente->rubrica_id = $this->upsertRubricaInterna($validated, $dipendente);
|
||||||
|
}
|
||||||
|
|
||||||
$dipendente->save();
|
$dipendente->save();
|
||||||
|
|
||||||
$this->resetForm();
|
$this->resetForm();
|
||||||
$this->refreshRows();
|
$this->refreshRows();
|
||||||
|
$this->activeTab = 'elenco';
|
||||||
|
|
||||||
$notification = Notification::make()->title('Collaboratore salvato')->success();
|
$notification = Notification::make()->title('Collaboratore salvato')->success();
|
||||||
if ($createdPassword !== null && $email !== '') {
|
if ($createdPassword !== null && $email !== '') {
|
||||||
|
|
@ -388,6 +455,7 @@ protected function refreshRows(): void
|
||||||
'note' => (string) ($dipendente->note ?? ''),
|
'note' => (string) ($dipendente->note ?? ''),
|
||||||
'attivo' => (bool) $dipendente->attivo,
|
'attivo' => (bool) $dipendente->attivo,
|
||||||
'user_id' => $dipendente->user_id ? (int) $dipendente->user_id : null,
|
'user_id' => $dipendente->user_id ? (int) $dipendente->user_id : null,
|
||||||
|
'rubrica_id' => $dipendente->rubrica_id ? (int) $dipendente->rubrica_id : null,
|
||||||
'tipo' => (string) $dipendente->tipo_collaboratore,
|
'tipo' => (string) $dipendente->tipo_collaboratore,
|
||||||
'tipo_label' => (string) $dipendente->tipo_label,
|
'tipo_label' => (string) $dipendente->tipo_label,
|
||||||
'fornitore_esterno_nome' => (string) ($dipendente->fornitoreEsterno?->ragione_sociale ?: trim((string) (($dipendente->fornitoreEsterno?->nome ?? '') . ' ' . ($dipendente->fornitoreEsterno?->cognome ?? '')))),
|
'fornitore_esterno_nome' => (string) ($dipendente->fornitoreEsterno?->ragione_sociale ?: trim((string) (($dipendente->fornitoreEsterno?->nome ?? '') . ' ' . ($dipendente->fornitoreEsterno?->cognome ?? '')))),
|
||||||
|
|
@ -447,10 +515,25 @@ protected function resolveFornitoreEsterno(int $fornitoreId): ?Fornitore
|
||||||
|
|
||||||
protected function resetForm(): void
|
protected function resetForm(): void
|
||||||
{
|
{
|
||||||
|
$this->tipoContatto = 'persona_fisica';
|
||||||
$this->nome = '';
|
$this->nome = '';
|
||||||
$this->cognome = '';
|
$this->cognome = '';
|
||||||
|
$this->ragioneSociale = '';
|
||||||
|
$this->codiceFiscale = '';
|
||||||
|
$this->partitaIva = '';
|
||||||
|
$this->pec = '';
|
||||||
|
$this->sesso = '';
|
||||||
|
$this->dataNascita = '';
|
||||||
|
$this->luogoNascita = '';
|
||||||
|
$this->provinciaNascita = '';
|
||||||
$this->email = '';
|
$this->email = '';
|
||||||
$this->telefono = '';
|
$this->telefono = '';
|
||||||
|
$this->indirizzo = '';
|
||||||
|
$this->civico = '';
|
||||||
|
$this->cap = '';
|
||||||
|
$this->citta = '';
|
||||||
|
$this->provincia = '';
|
||||||
|
$this->nazione = 'Italia';
|
||||||
$this->collaboratoreTipo = FornitoreDipendente::TIPO_INTERNO;
|
$this->collaboratoreTipo = FornitoreDipendente::TIPO_INTERNO;
|
||||||
$this->fornitoreSearch = '';
|
$this->fornitoreSearch = '';
|
||||||
$this->fornitoreEsternoId = null;
|
$this->fornitoreEsternoId = null;
|
||||||
|
|
@ -459,4 +542,60 @@ protected function resetForm(): void
|
||||||
$this->accessPassword = '';
|
$this->accessPassword = '';
|
||||||
$this->refreshFornitoreOptions();
|
$this->refreshFornitoreOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function upsertRubricaInterna(array $validated, FornitoreDipendente $dipendente): int
|
||||||
|
{
|
||||||
|
$email = trim((string) ($validated['email'] ?? ''));
|
||||||
|
$rubrica = null;
|
||||||
|
|
||||||
|
if ($email !== '') {
|
||||||
|
$rubrica = RubricaUniversale::query()
|
||||||
|
->whereRaw('LOWER(email) = ?', [mb_strtolower($email)])
|
||||||
|
->where('categoria', 'fornitore')
|
||||||
|
->orderByDesc('id')
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $rubrica instanceof RubricaUniversale) {
|
||||||
|
$rubrica = new RubricaUniversale();
|
||||||
|
$rubrica->data_inserimento = now()->toDateString();
|
||||||
|
$rubrica->creato_da = Auth::id();
|
||||||
|
}
|
||||||
|
|
||||||
|
$tipoContatto = (string) ($validated['tipoContatto'] ?? 'persona_fisica');
|
||||||
|
$telefono = trim((string) ($validated['telefono'] ?? ''));
|
||||||
|
$ragioneSociale = trim((string) ($validated['ragioneSociale'] ?? ''));
|
||||||
|
|
||||||
|
$rubrica->titolo_id = null;
|
||||||
|
$rubrica->nome = $tipoContatto === 'persona_giuridica' ? null : (trim((string) ($validated['nome'] ?? '')) ?: null);
|
||||||
|
$rubrica->cognome = $tipoContatto === 'persona_giuridica' ? null : (trim((string) ($validated['cognome'] ?? '')) ?: null);
|
||||||
|
$rubrica->ragione_sociale = $tipoContatto === 'persona_giuridica' ? ($ragioneSociale ?: null) : null;
|
||||||
|
$rubrica->tipo_contatto = $tipoContatto;
|
||||||
|
$rubrica->codice_fiscale = trim((string) ($validated['codiceFiscale'] ?? '')) ?: null;
|
||||||
|
$rubrica->partita_iva = trim((string) ($validated['partitaIva'] ?? '')) ?: null;
|
||||||
|
$rubrica->sesso = trim((string) ($validated['sesso'] ?? '')) ?: null;
|
||||||
|
$rubrica->data_nascita = trim((string) ($validated['dataNascita'] ?? '')) ?: null;
|
||||||
|
$rubrica->luogo_nascita = trim((string) ($validated['luogoNascita'] ?? '')) ?: null;
|
||||||
|
$rubrica->provincia_nascita = trim((string) ($validated['provinciaNascita'] ?? '')) ?: null;
|
||||||
|
$rubrica->indirizzo = trim((string) ($validated['indirizzo'] ?? '')) ?: null;
|
||||||
|
$rubrica->civico = trim((string) ($validated['civico'] ?? '')) ?: null;
|
||||||
|
$rubrica->cap = trim((string) ($validated['cap'] ?? '')) ?: null;
|
||||||
|
$rubrica->citta = trim((string) ($validated['citta'] ?? '')) ?: null;
|
||||||
|
$rubrica->provincia = trim((string) ($validated['provincia'] ?? '')) ?: null;
|
||||||
|
$rubrica->nazione = trim((string) ($validated['nazione'] ?? '')) ?: 'Italia';
|
||||||
|
$rubrica->telefono_ufficio = $tipoContatto === 'persona_giuridica' ? ($telefono ?: null) : null;
|
||||||
|
$rubrica->telefono_cellulare = $telefono ?: null;
|
||||||
|
$rubrica->email = $email !== '' ? $email : null;
|
||||||
|
$rubrica->pec = trim((string) ($validated['pec'] ?? '')) ?: null;
|
||||||
|
$rubrica->note = trim((string) ($validated['note'] ?? '')) ?: null;
|
||||||
|
$rubrica->categoria = 'fornitore';
|
||||||
|
$rubrica->stato = 'attivo';
|
||||||
|
$rubrica->data_ultima_modifica = now()->toDateString();
|
||||||
|
$rubrica->modificato_da = Auth::id();
|
||||||
|
$rubrica->save();
|
||||||
|
|
||||||
|
$dipendente->rubrica_id = (int) $rubrica->id;
|
||||||
|
|
||||||
|
return (int) $rubrica->id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,10 +40,10 @@ class Contabilita extends Page
|
||||||
|
|
||||||
/** @var array{aperte:int,pagate:int,registrate:int,totale_aperto:float,totale_registrato:float} */
|
/** @var array{aperte:int,pagate:int,registrate:int,totale_aperto:float,totale_registrato:float} */
|
||||||
public array $stats = [
|
public array $stats = [
|
||||||
'aperte' => 0,
|
'aperte' => 0,
|
||||||
'pagate' => 0,
|
'pagate' => 0,
|
||||||
'registrate' => 0,
|
'registrate' => 0,
|
||||||
'totale_aperto' => 0.0,
|
'totale_aperto' => 0.0,
|
||||||
'totale_registrato' => 0.0,
|
'totale_registrato' => 0.0,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -58,7 +58,7 @@ public static function canAccess(): bool
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
return $user instanceof User
|
return $user instanceof User
|
||||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'fornitore']);
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'fornitore']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mount(): void
|
public function mount(): void
|
||||||
|
|
@ -70,7 +70,7 @@ public function mount(): void
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->fornitore = $fornitore;
|
$this->fornitore = $fornitore;
|
||||||
$this->fornitoreLabel = $this->getFornitoreLabel($fornitore);
|
$this->fornitoreLabel = $this->getFornitoreLabel($fornitore);
|
||||||
$this->loadData();
|
$this->loadData();
|
||||||
}
|
}
|
||||||
|
|
@ -107,14 +107,14 @@ protected function loadData(): void
|
||||||
->limit(40)
|
->limit(40)
|
||||||
->get()
|
->get()
|
||||||
->map(fn(TicketIntervento $intervento) => [
|
->map(fn(TicketIntervento $intervento) => [
|
||||||
'ticket_id' => (int) $intervento->ticket_id,
|
'ticket_id' => (int) $intervento->ticket_id,
|
||||||
'intervento_id' => (int) $intervento->id,
|
'intervento_id' => (int) $intervento->id,
|
||||||
'stato' => (string) $intervento->stato,
|
'stato' => (string) $intervento->stato,
|
||||||
'stabile' => (string) ($intervento->ticket?->stabile?->denominazione ?? '-'),
|
'stabile' => (string) ($intervento->ticket?->stabile?->denominazione ?? '-'),
|
||||||
'amministratore' => (string) ($intervento->ticket?->stabile?->amministratore?->denominazione_studio ?? '-'),
|
'amministratore' => (string) ($intervento->ticket?->stabile?->amministratore?->denominazione_studio ?? '-'),
|
||||||
'titolo' => (string) ($intervento->ticket?->titolo ?? '-'),
|
'titolo' => (string) ($intervento->ticket?->titolo ?? '-'),
|
||||||
'aggiornato' => optional($intervento->updated_at)->format('d/m/Y H:i') ?: '-',
|
'aggiornato' => optional($intervento->updated_at)->format('d/m/Y H:i') ?: '-',
|
||||||
'url' => TicketInterventoScheda::getUrl(['record' => (int) $intervento->id], panel: 'admin-filament'),
|
'url' => TicketInterventoScheda::getUrl(['record' => (int) $intervento->id], panel: 'admin-filament'),
|
||||||
])
|
])
|
||||||
->all();
|
->all();
|
||||||
|
|
||||||
|
|
@ -134,25 +134,25 @@ protected function loadData(): void
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$this->stats = [
|
$this->stats = [
|
||||||
'aperte' => $fatture->whereIn('stato', ['inserito', 'contabilizzato'])->count(),
|
'aperte' => $fatture->whereIn('stato', ['inserito', 'contabilizzato'])->count(),
|
||||||
'pagate' => $fatture->where('stato', 'pagato')->count(),
|
'pagate' => $fatture->where('stato', 'pagato')->count(),
|
||||||
'registrate' => $fatture->count(),
|
'registrate' => $fatture->count(),
|
||||||
'totale_aperto' => (float) $fatture->whereIn('stato', ['inserito', 'contabilizzato'])->sum('netto_da_pagare'),
|
'totale_aperto' => (float) $fatture->whereIn('stato', ['inserito', 'contabilizzato'])->sum('netto_da_pagare'),
|
||||||
'totale_registrato' => (float) $fatture->sum('totale'),
|
'totale_registrato' => (float) $fatture->sum('totale'),
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->fattureRows = $fatture
|
$this->fattureRows = $fatture
|
||||||
->map(fn(FatturaFornitore $fattura) => [
|
->map(fn(FatturaFornitore $fattura) => [
|
||||||
'id' => (int) $fattura->id,
|
'id' => (int) $fattura->id,
|
||||||
'data_documento' => optional($fattura->data_documento)->format('d/m/Y') ?: '-',
|
'data_documento' => optional($fattura->data_documento)->format('d/m/Y') ?: '-',
|
||||||
'numero_documento' => (string) ($fattura->numero_documento ?? '-'),
|
'numero_documento' => (string) ($fattura->numero_documento ?? '-'),
|
||||||
'stato' => (string) ($fattura->stato ?? 'inserito'),
|
'stato' => (string) ($fattura->stato ?? 'inserito'),
|
||||||
'stabile' => (string) ($fattura->stabile?->denominazione ?? '-'),
|
'stabile' => (string) ($fattura->stabile?->denominazione ?? '-'),
|
||||||
'amministratore' => (string) ($fattura->stabile?->amministratore?->denominazione_studio ?? '-'),
|
'amministratore' => (string) ($fattura->stabile?->amministratore?->denominazione_studio ?? '-'),
|
||||||
'descrizione' => (string) ($fattura->descrizione ?? ''),
|
'descrizione' => (string) ($fattura->descrizione ?? ''),
|
||||||
'totale' => (float) ($fattura->totale ?? 0),
|
'totale' => (float) ($fattura->totale ?? 0),
|
||||||
'netto' => (float) ($fattura->netto_da_pagare ?? 0),
|
'netto' => (float) ($fattura->netto_da_pagare ?? 0),
|
||||||
])
|
])
|
||||||
->all();
|
->all();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
use App\Filament\Pages\Fornitore\Concerns\ResolvesOperatoreContext;
|
use App\Filament\Pages\Fornitore\Concerns\ResolvesOperatoreContext;
|
||||||
use App\Models\Fornitore;
|
use App\Models\Fornitore;
|
||||||
use App\Models\FornitoreDipendente;
|
use App\Models\FornitoreDipendente;
|
||||||
|
use App\Models\Ticket;
|
||||||
use App\Models\TicketAttachment;
|
use App\Models\TicketAttachment;
|
||||||
use App\Models\TicketIntervento;
|
use App\Models\TicketIntervento;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
|
@ -38,11 +39,14 @@ class TicketInterventoScheda extends Page
|
||||||
|
|
||||||
public ?FornitoreDipendente $dipendente = null;
|
public ?FornitoreDipendente $dipendente = null;
|
||||||
|
|
||||||
/** @var array{contatto:string,telefono:string,problema:string} */
|
/** @var array{contatto:string,telefono:string,email:string,problema:string,sorgente:string,riferimento:string} */
|
||||||
public array $caller = [
|
public array $caller = [
|
||||||
'contatto' => '-',
|
'contatto' => '-',
|
||||||
'telefono' => '',
|
'telefono' => '',
|
||||||
|
'email' => '',
|
||||||
'problema' => '',
|
'problema' => '',
|
||||||
|
'sorgente' => '',
|
||||||
|
'riferimento' => '',
|
||||||
];
|
];
|
||||||
|
|
||||||
/** @var array<int, array<string, mixed>> */
|
/** @var array<int, array<string, mixed>> */
|
||||||
|
|
@ -342,6 +346,8 @@ protected function reloadIntervento(): void
|
||||||
{
|
{
|
||||||
$this->intervento->load([
|
$this->intervento->load([
|
||||||
'ticket.stabile',
|
'ticket.stabile',
|
||||||
|
'ticket.unitaImmobiliare',
|
||||||
|
'ticket.soggettoRichiedente',
|
||||||
'ticket.messages.user',
|
'ticket.messages.user',
|
||||||
'ticket.attachments.user',
|
'ticket.attachments.user',
|
||||||
'ticket.apertoDaUser',
|
'ticket.apertoDaUser',
|
||||||
|
|
@ -353,6 +359,8 @@ protected function reloadIntervento(): void
|
||||||
$this->intervento->refresh();
|
$this->intervento->refresh();
|
||||||
$this->intervento->load([
|
$this->intervento->load([
|
||||||
'ticket.stabile',
|
'ticket.stabile',
|
||||||
|
'ticket.unitaImmobiliare',
|
||||||
|
'ticket.soggettoRichiedente',
|
||||||
'ticket.messages.user',
|
'ticket.messages.user',
|
||||||
'ticket.attachments.user',
|
'ticket.attachments.user',
|
||||||
'ticket.apertoDaUser',
|
'ticket.apertoDaUser',
|
||||||
|
|
@ -361,7 +369,7 @@ protected function reloadIntervento(): void
|
||||||
'eseguitoDaDipendente',
|
'eseguitoDaDipendente',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->caller = $this->extractCallerData((string) ($this->intervento->ticket?->descrizione ?? ''));
|
$this->caller = $this->resolveCallerData($this->intervento->ticket);
|
||||||
$this->storicoRows = TicketIntervento::query()
|
$this->storicoRows = TicketIntervento::query()
|
||||||
->with(['ticket'])
|
->with(['ticket'])
|
||||||
->where('fornitore_id', (int) $this->fornitore->id)
|
->where('fornitore_id', (int) $this->fornitore->id)
|
||||||
|
|
@ -401,4 +409,78 @@ protected function reloadIntervento(): void
|
||||||
$this->articoliUtilizzati = array_pad((array) ($this->intervento->articoli_utilizzati ?? []), 3, '');
|
$this->articoliUtilizzati = array_pad((array) ($this->intervento->articoli_utilizzati ?? []), 3, '');
|
||||||
$this->qrToken = '';
|
$this->qrToken = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{contatto:string,telefono:string,email:string,problema:string,sorgente:string,riferimento:string}
|
||||||
|
*/
|
||||||
|
protected function resolveCallerData(?Ticket $ticket): array
|
||||||
|
{
|
||||||
|
$parsed = $this->extractCallerData((string) ($ticket?->descrizione ?? ''));
|
||||||
|
|
||||||
|
$caller = [
|
||||||
|
'contatto' => $parsed['contatto'],
|
||||||
|
'telefono' => $parsed['telefono'],
|
||||||
|
'email' => '',
|
||||||
|
'problema' => $parsed['problema'] !== '' ? $parsed['problema'] : (string) ($ticket?->titolo ?? ''),
|
||||||
|
'sorgente' => 'Descrizione ticket',
|
||||||
|
'riferimento' => $this->buildCallerReference($ticket),
|
||||||
|
];
|
||||||
|
|
||||||
|
$soggetto = $ticket?->soggettoRichiedente;
|
||||||
|
if ($soggetto) {
|
||||||
|
$label = trim((string) ($soggetto->ragione_sociale ?: trim(($soggetto->nome ?? '') . ' ' . ($soggetto->cognome ?? ''))));
|
||||||
|
if ($label !== '') {
|
||||||
|
$caller['contatto'] = $label;
|
||||||
|
}
|
||||||
|
|
||||||
|
$caller['telefono'] = trim((string) ($soggetto->telefono ?? '')) ?: $caller['telefono'];
|
||||||
|
$caller['email'] = trim((string) ($soggetto->email ?? ''));
|
||||||
|
$caller['sorgente'] = 'Richiedente collegato al ticket';
|
||||||
|
|
||||||
|
return $caller;
|
||||||
|
}
|
||||||
|
|
||||||
|
$openedBy = $ticket?->apertoDaUser;
|
||||||
|
if ($openedBy instanceof User) {
|
||||||
|
$caller['contatto'] = trim((string) ($openedBy->name ?? '')) ?: $caller['contatto'];
|
||||||
|
$caller['email'] = trim((string) ($openedBy->email ?? ''));
|
||||||
|
$caller['sorgente'] = 'Utente che ha aperto il ticket';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $caller;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildCallerReference(?Ticket $ticket): string
|
||||||
|
{
|
||||||
|
if (! $ticket instanceof Ticket) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = [];
|
||||||
|
|
||||||
|
$stabile = $ticket->stabile;
|
||||||
|
if ($stabile) {
|
||||||
|
$denominazione = trim((string) ($stabile->denominazione ?? ''));
|
||||||
|
$indirizzo = trim(implode(' ', array_filter([
|
||||||
|
$stabile->indirizzo ?? null,
|
||||||
|
$stabile->cap ?? null,
|
||||||
|
$stabile->citta ?? null,
|
||||||
|
])));
|
||||||
|
|
||||||
|
if ($denominazione !== '') {
|
||||||
|
$parts[] = $denominazione;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($indirizzo !== '') {
|
||||||
|
$parts[] = $indirizzo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$luogoIntervento = trim((string) ($ticket->luogo_intervento ?? ''));
|
||||||
|
if ($luogoIntervento !== '') {
|
||||||
|
$parts[] = 'Luogo: ' . $luogoIntervento;
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(' · ', array_unique($parts));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Pages\Gescon;
|
namespace App\Filament\Pages\Gescon;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
|
@ -31,6 +30,6 @@ public static function canAccess(): bool
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
return $user instanceof User
|
return $user instanceof User
|
||||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
namespace App\Filament\Pages\Gescon;
|
namespace App\Filament\Pages\Gescon;
|
||||||
|
|
||||||
|
use App\Console\Commands\ImportLegacyFornitoriTagsCommand;
|
||||||
use App\Models\Fornitore;
|
use App\Models\Fornitore;
|
||||||
use App\Models\FornitoreDipendente;
|
use App\Models\FornitoreDipendente;
|
||||||
use App\Models\RubricaUniversale;
|
use App\Models\RubricaUniversale;
|
||||||
|
|
@ -11,6 +12,7 @@
|
||||||
use Filament\Pages\Page;
|
use Filament\Pages\Page;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
use Illuminate\Support\Facades\Artisan;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
|
@ -327,6 +329,49 @@ public function pulisciRicerca(): void
|
||||||
$this->activeTab = 'elenco';
|
$this->activeTab = 'elenco';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function importaTagLegacy(bool $soloSelezionato = false, bool $dryRun = false): void
|
||||||
|
{
|
||||||
|
if (! Schema::hasColumn('fornitori', 'tags')) {
|
||||||
|
Notification::make()->title('Colonna tags non presente')->warning()->send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = [];
|
||||||
|
if ($dryRun) {
|
||||||
|
$params['--dry-run'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($soloSelezionato) {
|
||||||
|
$fornitoreId = (int) ($this->selectedFornitoreId ?? 0);
|
||||||
|
if ($fornitoreId <= 0) {
|
||||||
|
Notification::make()->title('Seleziona prima un fornitore')->warning()->send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$params['--fornitore-id'] = [$fornitoreId];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Artisan::call(ImportLegacyFornitoriTagsCommand::class, $params);
|
||||||
|
$output = trim(Artisan::output());
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Notification::make()->title('Riallineamento tag legacy fallito')->body($e->getMessage())->danger()->send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((int) ($this->selectedFornitoreId ?? 0) > 0) {
|
||||||
|
$this->hydrateSchedaFromSelected();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->searchFornitoreMatches();
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title($dryRun ? 'Anteprima riallineamento completata' : 'Riallineamento tag legacy completato')
|
||||||
|
->body($output !== '' ? $output : 'Operazione eseguita.')
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
public function removeSearchFilter(string $token): void
|
public function removeSearchFilter(string $token): void
|
||||||
{
|
{
|
||||||
$tokens = array_values(array_filter(
|
$tokens = array_values(array_filter(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Pages\Gescon;
|
namespace App\Filament\Pages\Gescon;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
|
@ -29,6 +28,6 @@ public static function canAccess(): bool
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
return $user instanceof User
|
return $user instanceof User
|
||||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ public static function canAccess(): bool
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
return $user instanceof User
|
return $user instanceof User
|
||||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ?string $mastrinoTabella = null;
|
public ?string $mastrinoTabella = null;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Pages\Gescon;
|
namespace App\Filament\Pages\Gescon;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
|
@ -31,6 +30,6 @@ public static function canAccess(): bool
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
return $user instanceof User
|
return $user instanceof User
|
||||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,7 @@ public function table(Table $table): Table
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($tokens as $token) {
|
foreach ($tokens as $token) {
|
||||||
$like = '%' . mb_strtolower($token) . '%';
|
$like = '%' . mb_strtolower($token) . '%';
|
||||||
$digits = preg_replace('/\D+/', '', $token) ?: '';
|
$digits = preg_replace('/\D+/', '', $token) ?: '';
|
||||||
$query->where(function (Builder $q) use ($like, $digits) {
|
$query->where(function (Builder $q) use ($like, $digits) {
|
||||||
$q->whereRaw("LOWER(COALESCE(ragione_sociale, '')) LIKE ?", [$like])
|
$q->whereRaw("LOWER(COALESCE(ragione_sociale, '')) LIKE ?", [$like])
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,25 @@ class RubricaUniversaleScheda extends Page
|
||||||
/** @var array<int,string> */
|
/** @var array<int,string> */
|
||||||
public array $dipendentePbxExtension = [];
|
public array $dipendentePbxExtension = [];
|
||||||
|
|
||||||
|
/** @var array<int,string> */
|
||||||
|
public array $studioCollaboratoreStabileIds = [];
|
||||||
|
|
||||||
|
/** @var array<int,string> */
|
||||||
|
public array $studioCollaboratoreStabileOptions = [];
|
||||||
|
|
||||||
|
public ?int $studioCollaboratoreUserId = null;
|
||||||
|
|
||||||
|
public ?string $studioCollaboratoreUserEmail = null;
|
||||||
|
|
||||||
|
public ?string $studioCollaboratoreUserName = null;
|
||||||
|
|
||||||
|
public ?string $studioCollaboratoreGeneratedPassword = null;
|
||||||
|
|
||||||
|
public string $studioCollaboratorePbxExtension = '';
|
||||||
|
|
||||||
|
/** @var array<int,string> */
|
||||||
|
public array $studioCollaboratoreRoles = [];
|
||||||
|
|
||||||
/** @var array<string,mixed> */
|
/** @var array<string,mixed> */
|
||||||
public array $inlineForm = [];
|
public array $inlineForm = [];
|
||||||
|
|
||||||
|
|
@ -125,6 +144,17 @@ public function mount(int | string $record): void
|
||||||
|
|
||||||
$this->adminStabileIds = $stabiliAdminIds;
|
$this->adminStabileIds = $stabiliAdminIds;
|
||||||
|
|
||||||
|
$this->studioCollaboratoreStabileOptions = Stabile::query()
|
||||||
|
->when($adminId > 0, fn(Builder $q) => $q->where('amministratore_id', $adminId))
|
||||||
|
->when($adminId <= 0 && ! empty($stabiliAdminIds), fn(Builder $q) => $q->whereIn('id', $stabiliAdminIds))
|
||||||
|
->orderBy('codice_stabile')
|
||||||
|
->orderBy('denominazione')
|
||||||
|
->get(['id', 'codice_stabile', 'denominazione'])
|
||||||
|
->mapWithKeys(fn(Stabile $stabile) => [
|
||||||
|
(int) $stabile->id => trim((string) ($stabile->codice_stabile ?: ('#' . $stabile->id)) . ' - ' . (string) ($stabile->denominazione ?: 'Stabile')),
|
||||||
|
])
|
||||||
|
->all();
|
||||||
|
|
||||||
$this->initArchivioNavigation();
|
$this->initArchivioNavigation();
|
||||||
|
|
||||||
$this->fornitori = Fornitore::query()
|
$this->fornitori = Fornitore::query()
|
||||||
|
|
@ -268,6 +298,7 @@ public function mount(int | string $record): void
|
||||||
$this->hydrateEmailMultiple();
|
$this->hydrateEmailMultiple();
|
||||||
$this->fillInlineForm();
|
$this->fillInlineForm();
|
||||||
$this->hydrateFornitoriWorkspace();
|
$this->hydrateFornitoriWorkspace();
|
||||||
|
$this->hydrateStudioCollaboratoreWorkspace();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function startInlineEdit(): void
|
public function startInlineEdit(): void
|
||||||
|
|
@ -1751,4 +1782,155 @@ public function salvaInternoDipendente(int $dipendenteId): void
|
||||||
$this->hydrateFornitoriWorkspace();
|
$this->hydrateFornitoriWorkspace();
|
||||||
Notification::make()->title('Interno PBX aggiornato')->success()->send();
|
Notification::make()->title('Interno PBX aggiornato')->success()->send();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function abilitaAccessoCollaboratoreStudio(): void
|
||||||
|
{
|
||||||
|
$email = mb_strtolower(trim((string) ($this->rubrica->email ?? '')));
|
||||||
|
if ($email === '') {
|
||||||
|
Notification::make()
|
||||||
|
->title('Email rubrica mancante')
|
||||||
|
->warning()
|
||||||
|
->body('Per creare o collegare il collaboratore di studio serve un indirizzo email nella rubrica.')
|
||||||
|
->send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = trim((string) ($this->rubrica->nome_completo ?: $this->rubrica->ragione_sociale ?: 'Collaboratore Studio'));
|
||||||
|
|
||||||
|
$user = User::query()->whereRaw('LOWER(email) = ?', [$email])->first();
|
||||||
|
if (! $user) {
|
||||||
|
$tmpPwd = Str::random(12);
|
||||||
|
$user = User::query()->create([
|
||||||
|
'name' => $name,
|
||||||
|
'email' => $email,
|
||||||
|
'password' => Hash::make($tmpPwd),
|
||||||
|
'email_verified_at' => now(),
|
||||||
|
'is_active' => true,
|
||||||
|
'registration_status' => 'approved',
|
||||||
|
]);
|
||||||
|
$this->studioCollaboratoreGeneratedPassword = $tmpPwd;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->assignRole('collaboratore');
|
||||||
|
|
||||||
|
$extension = trim($this->studioCollaboratorePbxExtension);
|
||||||
|
if (Schema::hasColumn('users', 'pbx_extension')) {
|
||||||
|
$user->pbx_extension = $extension !== '' ? $extension : null;
|
||||||
|
$user->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->syncStudioCollaboratoreStabili($user);
|
||||||
|
$this->hydrateStudioCollaboratoreWorkspace();
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title('Collaboratore di studio abilitato')
|
||||||
|
->success()
|
||||||
|
->body('Utente collegato alla rubrica e pronto per la gestione centralino/stabili.')
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function salvaCollaboratoreStudio(): void
|
||||||
|
{
|
||||||
|
$userId = (int) ($this->studioCollaboratoreUserId ?? 0);
|
||||||
|
if ($userId <= 0) {
|
||||||
|
Notification::make()
|
||||||
|
->title('Collaboratore non ancora abilitato')
|
||||||
|
->warning()
|
||||||
|
->body('Crea o collega prima l\'utente collaboratore da questa rubrica.')
|
||||||
|
->send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = User::query()->find($userId);
|
||||||
|
if (! $user) {
|
||||||
|
Notification::make()->title('Utente collaboratore non trovato')->danger()->send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Schema::hasColumn('users', 'pbx_extension')) {
|
||||||
|
$extension = trim($this->studioCollaboratorePbxExtension);
|
||||||
|
$user->pbx_extension = $extension !== '' ? $extension : null;
|
||||||
|
$user->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->syncStudioCollaboratoreStabili($user);
|
||||||
|
$this->hydrateStudioCollaboratoreWorkspace();
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title('Collaboratore studio aggiornato')
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hydrateStudioCollaboratoreWorkspace(): void
|
||||||
|
{
|
||||||
|
$this->studioCollaboratoreUserId = null;
|
||||||
|
$this->studioCollaboratoreUserEmail = trim((string) ($this->rubrica->email ?? '')) ?: null;
|
||||||
|
$this->studioCollaboratoreUserName = trim((string) ($this->rubrica->nome_completo ?: $this->rubrica->ragione_sociale ?: '')) ?: null;
|
||||||
|
$this->studioCollaboratorePbxExtension = '';
|
||||||
|
$this->studioCollaboratoreRoles = [];
|
||||||
|
$this->studioCollaboratoreGeneratedPassword = $this->studioCollaboratoreGeneratedPassword ?: null;
|
||||||
|
|
||||||
|
$defaultStabili = array_values(array_unique(array_filter(array_map(
|
||||||
|
fn(array $row): int => (int) ($row['id'] ?? 0),
|
||||||
|
$this->stabili
|
||||||
|
))));
|
||||||
|
|
||||||
|
if ($defaultStabili === [] && count($this->adminStabileIds) === 1) {
|
||||||
|
$defaultStabili = $this->adminStabileIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
$email = $this->studioCollaboratoreUserEmail;
|
||||||
|
if ($email === null) {
|
||||||
|
$this->studioCollaboratoreStabileIds = array_map('strval', $defaultStabili);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = User::query()->with('roles')->whereRaw('LOWER(email) = ?', [mb_strtolower($email)])->first();
|
||||||
|
if (! $user) {
|
||||||
|
$this->studioCollaboratoreStabileIds = array_map('strval', $defaultStabili);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->studioCollaboratoreUserId = (int) $user->id;
|
||||||
|
$this->studioCollaboratoreUserName = (string) $user->name;
|
||||||
|
$this->studioCollaboratorePbxExtension = (string) ($user->pbx_extension ?? '');
|
||||||
|
$this->studioCollaboratoreRoles = $user->roles->pluck('name')->values()->all();
|
||||||
|
|
||||||
|
$assignedIds = method_exists($user, 'stabiliAssegnati')
|
||||||
|
? $user->stabiliAssegnati()->pluck('stabili.id')->map(fn($v) => (int) $v)->all()
|
||||||
|
: [];
|
||||||
|
|
||||||
|
$assignedForAdmin = ! empty($this->adminStabileIds)
|
||||||
|
? array_values(array_intersect($assignedIds, $this->adminStabileIds))
|
||||||
|
: $assignedIds;
|
||||||
|
|
||||||
|
$this->studioCollaboratoreStabileIds = array_map('strval', $assignedForAdmin !== [] ? $assignedForAdmin : $defaultStabili);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function syncStudioCollaboratoreStabili(User $user): void
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('collaboratore_stabile') || ! method_exists($user, 'stabiliAssegnati')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$requested = [];
|
||||||
|
foreach ($this->studioCollaboratoreStabileIds as $value) {
|
||||||
|
if (is_numeric($value)) {
|
||||||
|
$requested[] = (int) $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$requested = array_values(array_unique($requested));
|
||||||
|
if (! empty($this->adminStabileIds)) {
|
||||||
|
$requested = array_values(array_intersect($requested, $this->adminStabileIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentIds = $user->stabiliAssegnati()->pluck('stabili.id')->map(fn($v) => (int) $v)->all();
|
||||||
|
$outsideAdminIds = ! empty($this->adminStabileIds)
|
||||||
|
? array_values(array_diff($currentIds, $this->adminStabileIds))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
$user->stabiliAssegnati()->sync(array_values(array_unique(array_merge($outsideAdminIds, $requested))));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
use App\Models\Stabile;
|
use App\Models\Stabile;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\GesconImport\EssentialImportService;
|
use App\Services\GesconImport\EssentialImportService;
|
||||||
|
use App\Services\Stabili\StabileTransferService;
|
||||||
use App\Support\StabileContext;
|
use App\Support\StabileContext;
|
||||||
use App\Filament\Pages\Condomini\CruscottoStabile;
|
use App\Filament\Pages\Condomini\CruscottoStabile;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
|
|
@ -22,10 +23,13 @@
|
||||||
use Illuminate\Support\Facades\Schema as SchemaFacade;
|
use Illuminate\Support\Facades\Schema as SchemaFacade;
|
||||||
use Filament\Forms\Components\Checkbox;
|
use Filament\Forms\Components\Checkbox;
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Forms\Components\Textarea;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use App\Models\Amministratore;
|
use App\Models\Amministratore;
|
||||||
use App\Models\Palazzina;
|
use App\Models\Palazzina;
|
||||||
|
use App\Models\StabileAmministratoreTransfer;
|
||||||
use App\Models\UnitaImmobiliare;
|
use App\Models\UnitaImmobiliare;
|
||||||
use App\Models\Persona;
|
use App\Models\Persona;
|
||||||
use App\Models\PersonaUnitaRelazione;
|
use App\Models\PersonaUnitaRelazione;
|
||||||
|
|
@ -389,10 +393,10 @@ public function table(Table $table): Table
|
||||||
->toggleable(),
|
->toggleable(),
|
||||||
|
|
||||||
TextColumn::make('cod_stabile')
|
TextColumn::make('cod_stabile')
|
||||||
->label('Codice operatore')
|
->label('Codice mnemonico')
|
||||||
->searchable()
|
->searchable()
|
||||||
->sortable()
|
->sortable()
|
||||||
->toggleable(isToggledHiddenByDefault: true),
|
->toggleable(),
|
||||||
|
|
||||||
TextColumn::make('denominazione')
|
TextColumn::make('denominazione')
|
||||||
->label('Denominazione')
|
->label('Denominazione')
|
||||||
|
|
@ -400,6 +404,25 @@ public function table(Table $table): Table
|
||||||
->sortable()
|
->sortable()
|
||||||
->wrap(),
|
->wrap(),
|
||||||
|
|
||||||
|
TextColumn::make('amministratore_corrente')
|
||||||
|
->label('Amministratore')
|
||||||
|
->getStateUsing(function (Stabile $record): string {
|
||||||
|
$amministratore = $record->amministratore;
|
||||||
|
if (! $amministratore instanceof Amministratore) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
$label = trim((string) ($amministratore->denominazione_studio ?: $amministratore->nome_completo));
|
||||||
|
if ($label === '') {
|
||||||
|
$label = 'Amministratore';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $label . ' (ID ' . (int) $amministratore->id . ')';
|
||||||
|
})
|
||||||
|
->searchable(false)
|
||||||
|
->toggleable()
|
||||||
|
->wrap(),
|
||||||
|
|
||||||
TextColumn::make('indirizzo_completo')
|
TextColumn::make('indirizzo_completo')
|
||||||
->label('Indirizzo')
|
->label('Indirizzo')
|
||||||
->toggleable()
|
->toggleable()
|
||||||
|
|
@ -558,7 +581,7 @@ public function table(Table $table): Table
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Action::make('trasferisci')
|
Action::make('trasferisci')
|
||||||
->label('Trasferisci')
|
->label('Cambia amministratore')
|
||||||
->icon('heroicon-o-arrow-right-circle')
|
->icon('heroicon-o-arrow-right-circle')
|
||||||
->visible(function (): bool {
|
->visible(function (): bool {
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
@ -584,12 +607,19 @@ public function table(Table $table): Table
|
||||||
})
|
})
|
||||||
->searchable()
|
->searchable()
|
||||||
->required(),
|
->required(),
|
||||||
|
Textarea::make('reason')
|
||||||
|
->label('Motivazione')
|
||||||
|
->helperText('Traccia il motivo del passaggio: riallineamento, presa in carico interna, correzione operativa, ecc.')
|
||||||
|
->rows(3)
|
||||||
|
->required()
|
||||||
|
->maxLength(5000),
|
||||||
Checkbox::make('also_rubrica')
|
Checkbox::make('also_rubrica')
|
||||||
->label('Aggiorna anche Rubrica collegata (se presente)')
|
->label('Aggiorna anche Rubrica collegata (se presente)')
|
||||||
->default(true),
|
->default(true),
|
||||||
])
|
])
|
||||||
->requiresConfirmation()
|
->requiresConfirmation()
|
||||||
->action(function (Stabile $record, array $data): void {
|
->action(function (Stabile $record, array $data): void {
|
||||||
|
$user = Auth::user();
|
||||||
$toId = (int) ($data['to_amministratore_id'] ?? 0);
|
$toId = (int) ($data['to_amministratore_id'] ?? 0);
|
||||||
if ($toId <= 0) {
|
if ($toId <= 0) {
|
||||||
Notification::make()->title('Amministratore non valido')->danger()->send();
|
Notification::make()->title('Amministratore non valido')->danger()->send();
|
||||||
|
|
@ -606,31 +636,51 @@ public function table(Table $table): Table
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
DB::transaction(function () use ($record, $toId, $dest, $data): void {
|
$transfer = app(StabileTransferService::class)->transfer(
|
||||||
$record->amministratore_id = $toId;
|
stabile: $record,
|
||||||
$record->save();
|
destination: $dest,
|
||||||
|
actor: $user instanceof User ? $user : null,
|
||||||
if (! empty($data['also_rubrica']) && (int) ($record->rubrica_id ?? 0) > 0) {
|
alsoRubrica: (bool) ($data['also_rubrica'] ?? true),
|
||||||
if (SchemaFacade::hasTable('rubrica_universale') && SchemaFacade::hasColumn('rubrica_universale', 'amministratore_id')) {
|
reason: trim((string) ($data['reason'] ?? '')),
|
||||||
DB::table('rubrica_universale')
|
source: 'portal-superadmin',
|
||||||
->where('id', (int) $record->rubrica_id)
|
ipAddress: request()->ip(),
|
||||||
->update(['amministratore_id' => $toId, 'updated_at' => now()]);
|
meta: [
|
||||||
}
|
'panel' => 'admin-filament',
|
||||||
}
|
'action' => 'stabili-archivio-trasferisci',
|
||||||
|
],
|
||||||
// Assicura cartelle archivio nel nuovo tenant.
|
);
|
||||||
$dest->provisionArchiveIfMissing();
|
|
||||||
$record->refresh();
|
|
||||||
$record->provisionArchiveIfMissing();
|
|
||||||
});
|
|
||||||
|
|
||||||
Notification::make()
|
Notification::make()
|
||||||
->title('Trasferimento completato')
|
->title('Trasferimento completato')
|
||||||
->body('Nuovo amministratore: ' . ($dest->codice_univoco ?: ('ID ' . $dest->id)))
|
->body('Nuovo amministratore: ' . ($dest->codice_univoco ?: ('ID ' . $dest->id)) . ' · storico #' . (int) $transfer->id)
|
||||||
->success()
|
->success()
|
||||||
->send();
|
->send();
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
Action::make('storico_trasferimenti')
|
||||||
|
->label('Storico passaggi')
|
||||||
|
->icon('heroicon-o-clock')
|
||||||
|
->visible(function (): bool {
|
||||||
|
$user = Auth::user();
|
||||||
|
return $user instanceof User && $user->hasAnyRole(['super-admin', 'admin']);
|
||||||
|
})
|
||||||
|
->modalHeading('Storico passaggi amministratore')
|
||||||
|
->modalSubmitAction(false)
|
||||||
|
->modalCancelActionLabel('Chiudi')
|
||||||
|
->modalContent(function (Stabile $record): View {
|
||||||
|
$transfers = StabileAmministratoreTransfer::query()
|
||||||
|
->with(['fromAmministratore', 'toAmministratore', 'changedByUser'])
|
||||||
|
->where('stabile_id', (int) $record->id)
|
||||||
|
->latest('id')
|
||||||
|
->limit(25)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('filament.pages.gescon.partials.stabile-transfer-history', [
|
||||||
|
'stabile' => $record,
|
||||||
|
'transfers' => $transfers,
|
||||||
|
]);
|
||||||
|
}),
|
||||||
|
|
||||||
Action::make('entra')
|
Action::make('entra')
|
||||||
->label('Entra')
|
->label('Entra')
|
||||||
->icon('heroicon-o-arrow-right')
|
->icon('heroicon-o-arrow-right')
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Pages\Gescon;
|
namespace App\Filament\Pages\Gescon;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
|
@ -31,6 +30,6 @@ public static function canAccess(): bool
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
return $user instanceof User
|
return $user instanceof User
|
||||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Pages\Gescon;
|
namespace App\Filament\Pages\Gescon;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
|
@ -31,6 +30,6 @@ public static function canAccess(): bool
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
return $user instanceof User
|
return $user instanceof User
|
||||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -380,8 +380,8 @@ public function form(Schema $schema): Schema
|
||||||
->label('Tipo')
|
->label('Tipo')
|
||||||
->options([
|
->options([
|
||||||
'gmail' => 'Gmail / Google Workspace',
|
'gmail' => 'Gmail / Google Workspace',
|
||||||
'imap' => 'IMAP ordinaria',
|
'imap' => 'IMAP ordinaria',
|
||||||
'pec' => 'PEC via IMAP',
|
'pec' => 'PEC via IMAP',
|
||||||
])
|
])
|
||||||
->default('imap')
|
->default('imap')
|
||||||
->required(),
|
->required(),
|
||||||
|
|
@ -402,8 +402,8 @@ public function form(Schema $schema): Schema
|
||||||
Select::make('encryption')
|
Select::make('encryption')
|
||||||
->label('Cifratura')
|
->label('Cifratura')
|
||||||
->options([
|
->options([
|
||||||
'ssl' => 'SSL',
|
'ssl' => 'SSL',
|
||||||
'tls' => 'TLS',
|
'tls' => 'TLS',
|
||||||
'none' => 'Nessuna',
|
'none' => 'Nessuna',
|
||||||
])
|
])
|
||||||
->default('ssl'),
|
->default('ssl'),
|
||||||
|
|
@ -442,6 +442,13 @@ public function form(Schema $schema): Schema
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
|
Section::make('Operativita posta / Google')
|
||||||
|
->schema([
|
||||||
|
Placeholder::make('mail_ops_panel')
|
||||||
|
->hiddenLabel()
|
||||||
|
->content(fn() => view('filament.pages.impostazioni.partials.scheda-amministratore-posta-operativita')),
|
||||||
|
]),
|
||||||
|
|
||||||
Section::make('WhatsApp Cloud API (Meta)')
|
Section::make('WhatsApp Cloud API (Meta)')
|
||||||
->columns(2)
|
->columns(2)
|
||||||
->schema([
|
->schema([
|
||||||
|
|
@ -681,6 +688,13 @@ public function form(Schema $schema): Schema
|
||||||
->default('gdrive:NetGesconBackups/prod/data-incremental')
|
->default('gdrive:NetGesconBackups/prod/data-incremental')
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
|
Section::make('Azioni operative')
|
||||||
|
->schema([
|
||||||
|
Placeholder::make('ops_distribution_panel')
|
||||||
|
->hiddenLabel()
|
||||||
|
->content(fn() => view('filament.pages.impostazioni.partials.scheda-amministratore-distribuzione-backup')),
|
||||||
|
]),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
Tab::make('Operativita / Accessi')
|
Tab::make('Operativita / Accessi')
|
||||||
|
|
@ -890,6 +904,16 @@ public function runGoogleDriveCheck(): void
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function runGoogleOAuthReadinessCheck(): void
|
||||||
|
{
|
||||||
|
$this->runOpsProcess(
|
||||||
|
['bash', 'scripts/ops/netgescon-google-check.sh'],
|
||||||
|
'Controllo Google OAuth completato',
|
||||||
|
'Controllo Google OAuth fallito',
|
||||||
|
120,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function connectGoogle(): void
|
public function connectGoogle(): void
|
||||||
{
|
{
|
||||||
$google = Arr::get($this->amministratore->impostazioni ?? [], 'google', []);
|
$google = Arr::get($this->amministratore->impostazioni ?? [], 'google', []);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Pages\Strumenti;
|
namespace App\Filament\Pages\Strumenti;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
|
@ -29,6 +28,6 @@ public static function canAccess(): bool
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
return $user instanceof User
|
return $user instanceof User
|
||||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,12 +35,23 @@ class PostItGestione extends Page
|
||||||
|
|
||||||
public string $activeTab = 'storico';
|
public string $activeTab = 'storico';
|
||||||
|
|
||||||
|
public string $tecnicoCallView = 'esterne';
|
||||||
|
|
||||||
|
public string $tecnicoDirectionFilter = 'tutte';
|
||||||
|
|
||||||
|
public string $tecnicoSearch = '';
|
||||||
|
|
||||||
|
public string $tecnicoScopeFilter = 'tutti';
|
||||||
|
|
||||||
/** @var array<int,string> */
|
/** @var array<int,string> */
|
||||||
public array $riaperturaNote = [];
|
public array $riaperturaNote = [];
|
||||||
|
|
||||||
/** @var array<string, int|null> */
|
/** @var array<string, int|null> */
|
||||||
private array $rubricaPhoneCache = [];
|
private array $rubricaPhoneCache = [];
|
||||||
|
|
||||||
|
/** @var array<string, array{id:int|null,name:string|null}> */
|
||||||
|
private array $pbxExtensionCache = [];
|
||||||
|
|
||||||
public function getPostItInserimentoUrl(): string
|
public function getPostItInserimentoUrl(): string
|
||||||
{
|
{
|
||||||
return PostIt::getUrl(panel: 'admin-filament');
|
return PostIt::getUrl(panel: 'admin-filament');
|
||||||
|
|
@ -201,12 +212,40 @@ public function getChiamateTecnicheProperty()
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return CommunicationMessage::query()
|
$query = CommunicationMessage::query()
|
||||||
->with(['assignedUser:id,name', 'stabile:id,denominazione'])
|
->with(['assignedUser:id,name', 'stabile:id,denominazione'])
|
||||||
->where('channel', 'smdr')
|
->where('channel', 'smdr')
|
||||||
->latest('id')
|
->latest('id');
|
||||||
|
|
||||||
|
if ($this->tecnicoCallView === 'interne') {
|
||||||
|
$query->where('direction', 'internal');
|
||||||
|
} elseif ($this->tecnicoCallView === 'esterne') {
|
||||||
|
$query->where('direction', '!=', 'internal');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($this->tecnicoDirectionFilter, ['inbound', 'outbound', 'internal'], true)) {
|
||||||
|
$query->where('direction', $this->tecnicoDirectionFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
$search = trim($this->tecnicoSearch);
|
||||||
|
if ($search !== '') {
|
||||||
|
$query->where(function ($q) use ($search): void {
|
||||||
|
$q->where('phone_number', 'like', '%' . $search . '%')
|
||||||
|
->orWhere('target_extension', 'like', '%' . $search . '%')
|
||||||
|
->orWhere('message_text', 'like', '%' . $search . '%')
|
||||||
|
->orWhere('sender_name', 'like', '%' . $search . '%');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = $query
|
||||||
->limit(250)
|
->limit(250)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
|
if (in_array($this->tecnicoScopeFilter, ['studio', 'esterno', 'interno'], true)) {
|
||||||
|
$items = $items->filter(fn(CommunicationMessage $message): bool => $this->getTecnicoScope($message) === $this->tecnicoScopeFilter)->values();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $items;
|
||||||
} catch (QueryException) {
|
} catch (QueryException) {
|
||||||
return collect();
|
return collect();
|
||||||
}
|
}
|
||||||
|
|
@ -302,6 +341,98 @@ public function getRubricaNomeByPhone(?string $phone): ?string
|
||||||
return $nome !== '' ? $nome : null;
|
return $nome !== '' ? $nome : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getTecnicoNominativo(CommunicationMessage $message): ?string
|
||||||
|
{
|
||||||
|
$extension = $this->extractExtensionFromMessage($message);
|
||||||
|
if ($this->isInternalSmdrMessage($message)) {
|
||||||
|
return $this->getCollaboratoreNomeByExtension($extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
$phone = (string) ($message->phone_number ?: data_get($message->metadata, 'smdr.dial_number', ''));
|
||||||
|
|
||||||
|
return $this->getRubricaNomeByPhone($phone);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCollaboratoreNomeByExtension(?string $extension): ?string
|
||||||
|
{
|
||||||
|
$normalized = $this->normalizeExtension($extension);
|
||||||
|
if ($normalized === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists($normalized, $this->pbxExtensionCache)) {
|
||||||
|
return $this->pbxExtensionCache[$normalized]['name'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::hasColumn('users', 'pbx_extension')) {
|
||||||
|
$this->pbxExtensionCache[$normalized] = ['id' => null, 'name' => null];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = User::query()
|
||||||
|
->select(['id', 'name', 'pbx_extension'])
|
||||||
|
->where('pbx_extension', $normalized)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$result = [
|
||||||
|
'id' => $user ? (int) $user->id : null,
|
||||||
|
'name' => $user ? trim((string) $user->name) : null,
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->pbxExtensionCache[$normalized] = $result;
|
||||||
|
|
||||||
|
return $result['name'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isInternalSmdrMessage(CommunicationMessage $message): bool
|
||||||
|
{
|
||||||
|
if ((string) $message->direction === 'internal') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$extension = $this->extractExtensionFromMessage($message);
|
||||||
|
$phone = preg_replace('/\D+/', '', (string) ($message->phone_number ?: data_get($message->metadata, 'smdr.dial_number', '')));
|
||||||
|
|
||||||
|
return $extension !== null && $phone !== '' && $phone === $extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTecnicoInternoLabel(CommunicationMessage $message): string
|
||||||
|
{
|
||||||
|
$extension = $this->extractExtensionFromMessage($message);
|
||||||
|
if ($extension === null) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = $this->getCollaboratoreNomeByExtension($extension);
|
||||||
|
if ($name === null) {
|
||||||
|
return $extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $extension . ' - ' . $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTecnicoScope(CommunicationMessage $message): string
|
||||||
|
{
|
||||||
|
if ($this->isInternalSmdrMessage($message)) {
|
||||||
|
return 'interno';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->isManagedStudioNumberMessage($message)) {
|
||||||
|
return 'studio';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'esterno';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTecnicoScopeLabel(CommunicationMessage $message): string
|
||||||
|
{
|
||||||
|
return match ($this->getTecnicoScope($message)) {
|
||||||
|
'interno' => 'Interno collaboratore',
|
||||||
|
'studio' => 'Numero studio gestito',
|
||||||
|
default => 'Numero esterno',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private function resolveRubricaIdByPhone(?string $phone): ?int
|
private function resolveRubricaIdByPhone(?string $phone): ?int
|
||||||
{
|
{
|
||||||
$digits = preg_replace('/\D+/', '', (string) $phone);
|
$digits = preg_replace('/\D+/', '', (string) $phone);
|
||||||
|
|
@ -309,6 +440,10 @@ private function resolveRubricaIdByPhone(?string $phone): ?int
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->isKnownPbxExtension($digits)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (array_key_exists($digits, $this->rubricaPhoneCache)) {
|
if (array_key_exists($digits, $this->rubricaPhoneCache)) {
|
||||||
return $this->rubricaPhoneCache[$digits];
|
return $this->rubricaPhoneCache[$digits];
|
||||||
}
|
}
|
||||||
|
|
@ -338,6 +473,61 @@ private function resolveRubricaIdByPhone(?string $phone): ?int
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function extractExtensionFromMessage(CommunicationMessage $message): ?string
|
||||||
|
{
|
||||||
|
$extension = (string) ($message->target_extension ?: data_get($message->metadata, 'smdr.extension', ''));
|
||||||
|
|
||||||
|
return $this->normalizeExtension($extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeExtension(?string $extension): ?string
|
||||||
|
{
|
||||||
|
$digits = preg_replace('/\D+/', '', (string) $extension);
|
||||||
|
|
||||||
|
return is_string($digits) && $digits !== '' ? $digits : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isKnownPbxExtension(string $digits): bool
|
||||||
|
{
|
||||||
|
if (strlen($digits) > 6 || ! Schema::hasColumn('users', 'pbx_extension')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return User::query()->where('pbx_extension', $digits)->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isManagedStudioNumberMessage(CommunicationMessage $message): bool
|
||||||
|
{
|
||||||
|
$phoneDigits = preg_replace('/\D+/', '', (string) ($message->phone_number ?: data_get($message->metadata, 'smdr.dial_number', '')));
|
||||||
|
if (! is_string($phoneDigits) || $phoneDigits === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return in_array($phoneDigits, $this->getManagedStudioNumbers(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<int,string> */
|
||||||
|
private function getManagedStudioNumbers(): array
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
if (! $user) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$impostazioni = (array) (($user->amministratore?->impostazioni ?? []));
|
||||||
|
$centralino = (array) ($impostazioni['centralino'] ?? []);
|
||||||
|
|
||||||
|
$numbers = [];
|
||||||
|
foreach (['numero_principale', 'numero_backup', 'numero_emergenza'] as $key) {
|
||||||
|
$digits = preg_replace('/\D+/', '', (string) ($centralino[$key] ?? ''));
|
||||||
|
if (is_string($digits) && $digits !== '') {
|
||||||
|
$numbers[] = $digits;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($numbers));
|
||||||
|
}
|
||||||
|
|
||||||
public static function canAccess(): bool
|
public static function canAccess(): bool
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
|
||||||
|
|
@ -284,10 +284,17 @@ public function runMaintenanceViewRebuild(): void
|
||||||
*/
|
*/
|
||||||
public function getUpdatePlannedStepsProperty(): array
|
public function getUpdatePlannedStepsProperty(): array
|
||||||
{
|
{
|
||||||
|
$requireDrive = $this->shouldRequireDrivePreupdateBackup();
|
||||||
|
$driveEnabled = $this->shouldAttemptDrivePreupdateBackup();
|
||||||
|
|
||||||
$steps = [
|
$steps = [
|
||||||
'Verifica canale update: ' . $this->updateChannel,
|
'Verifica canale update: ' . $this->updateChannel,
|
||||||
'Backup pre-update automatico (snapshot differenziale + indice record)',
|
'Backup pre-update automatico (snapshot differenziale + indice record)',
|
||||||
'Upload backup su Google Drive obbligatorio prima dell update',
|
$requireDrive
|
||||||
|
? 'Upload backup su Google Drive obbligatorio prima dell update'
|
||||||
|
: ($driveEnabled
|
||||||
|
? 'Upload backup su Google Drive tentato se configurato sul sito corrente'
|
||||||
|
: 'Upload backup su Google Drive non obbligatorio per questo nodo'),
|
||||||
'Esecuzione in modalita ' . ($this->updateDryRun ? 'dry-run (nessuna scrittura)' : 'apply (aggiornamento reale)'),
|
'Esecuzione in modalita ' . ($this->updateDryRun ? 'dry-run (nessuna scrittura)' : 'apply (aggiornamento reale)'),
|
||||||
'Flag force: ' . ($this->updateForce ? 'abilitato' : 'disabilitato'),
|
'Flag force: ' . ($this->updateForce ? 'abilitato' : 'disabilitato'),
|
||||||
'Check endpoint update consigliato prima del lancio',
|
'Check endpoint update consigliato prima del lancio',
|
||||||
|
|
@ -444,7 +451,10 @@ private function startUpdateJob(bool $fallback): void
|
||||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||||
|
|
||||||
$adminId = $this->resolveAmministratoreId();
|
$adminId = $this->resolveAmministratoreId();
|
||||||
if ($adminId <= 0) {
|
$requireDrive = $this->shouldRequireDrivePreupdateBackup();
|
||||||
|
$driveEnabled = $this->shouldAttemptDrivePreupdateBackup();
|
||||||
|
|
||||||
|
if ($requireDrive && $adminId <= 0) {
|
||||||
$this->updateInProgress = false;
|
$this->updateInProgress = false;
|
||||||
$this->updateProgressStatus = 'failed';
|
$this->updateProgressStatus = 'failed';
|
||||||
$this->updateProgressPercent = 100;
|
$this->updateProgressPercent = 100;
|
||||||
|
|
@ -463,13 +473,19 @@ private function startUpdateJob(bool $fallback): void
|
||||||
}
|
}
|
||||||
|
|
||||||
$backupParams = [
|
$backupParams = [
|
||||||
'--differential' => true,
|
'--differential' => true,
|
||||||
'--drive' => true,
|
'--tag' => 'update-' . $jobId,
|
||||||
'--require-drive' => true,
|
|
||||||
'--admin-id' => (string) $adminId,
|
|
||||||
'--tag' => 'update-' . $jobId,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if ($driveEnabled && $adminId > 0) {
|
||||||
|
$backupParams['--drive'] = true;
|
||||||
|
$backupParams['--admin-id'] = (string) $adminId;
|
||||||
|
|
||||||
|
if ($requireDrive) {
|
||||||
|
$backupParams['--require-drive'] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$backupExit = Artisan::call('netgescon:preupdate-backup', $backupParams);
|
$backupExit = Artisan::call('netgescon:preupdate-backup', $backupParams);
|
||||||
$backupOut = trim((string) Artisan::output());
|
$backupOut = trim((string) Artisan::output());
|
||||||
|
|
@ -574,6 +590,36 @@ private function resolveAmministratoreId(): int
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function shouldAttemptDrivePreupdateBackup(): bool
|
||||||
|
{
|
||||||
|
if (! (bool) config('distribution.preupdate_drive_enabled', false)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$adminId = $this->resolveAmministratoreId();
|
||||||
|
if ($adminId <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$admin = Amministratore::query()->find($adminId);
|
||||||
|
if (! $admin instanceof Amministratore) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$google = (array) data_get($admin->impostazioni ?? [], 'google', []);
|
||||||
|
$oauth = (array) data_get($google, 'oauth', []);
|
||||||
|
|
||||||
|
$accessToken = trim((string) ($oauth['access_token'] ?? ''));
|
||||||
|
$refreshToken = trim((string) ($oauth['refresh_token'] ?? ''));
|
||||||
|
|
||||||
|
return $accessToken !== '' || $refreshToken !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function shouldRequireDrivePreupdateBackup(): bool
|
||||||
|
{
|
||||||
|
return (bool) config('distribution.preupdate_require_drive', false);
|
||||||
|
}
|
||||||
|
|
||||||
private function registerPlannedUpdateSummary(): void
|
private function registerPlannedUpdateSummary(): void
|
||||||
{
|
{
|
||||||
$summary = implode(' | ', $this->getUpdatePlannedStepsProperty());
|
$summary = implode(' | ', $this->getUpdatePlannedStepsProperty());
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
use App\Models\CategoriaTicket;
|
use App\Models\CategoriaTicket;
|
||||||
use App\Models\Fornitore;
|
use App\Models\Fornitore;
|
||||||
use App\Models\FornitoreDipendente;
|
use App\Models\FornitoreDipendente;
|
||||||
|
use App\Models\InsuranceClaim;
|
||||||
|
use App\Models\InsurancePolicy;
|
||||||
use App\Models\Ticket;
|
use App\Models\Ticket;
|
||||||
use App\Models\TicketAttachment;
|
use App\Models\TicketAttachment;
|
||||||
use App\Models\TicketIntervento;
|
use App\Models\TicketIntervento;
|
||||||
|
|
@ -48,8 +50,18 @@ class TicketGestione extends Page
|
||||||
|
|
||||||
public ?int $fornitoreId = null;
|
public ?int $fornitoreId = null;
|
||||||
|
|
||||||
|
public string $fornitoreSearch = '';
|
||||||
|
|
||||||
public ?string $noteAssegnazione = null;
|
public ?string $noteAssegnazione = null;
|
||||||
|
|
||||||
|
public ?string $insurancePolicyReference = null;
|
||||||
|
|
||||||
|
public ?int $insurancePolicyId = null;
|
||||||
|
|
||||||
|
public ?string $insuranceClaimNumber = null;
|
||||||
|
|
||||||
|
public ?string $insuranceNotes = null;
|
||||||
|
|
||||||
/** @var array<int,mixed> */
|
/** @var array<int,mixed> */
|
||||||
public array $nuoviAllegati = [];
|
public array $nuoviAllegati = [];
|
||||||
|
|
||||||
|
|
@ -69,8 +81,8 @@ class TicketGestione extends Page
|
||||||
/** @var array<int,array{id:int,nome:string,descrizione:string}> */
|
/** @var array<int,array{id:int,nome:string,descrizione:string}> */
|
||||||
public array $categorieOptions = [];
|
public array $categorieOptions = [];
|
||||||
|
|
||||||
/** @var array<int,array{id:int,nome:string}> */
|
/** @var array<int,array<string,mixed>> */
|
||||||
public array $fornitoriOptions = [];
|
public array $fornitoreMatches = [];
|
||||||
|
|
||||||
/** @var array<int,array<string,mixed>> */
|
/** @var array<int,array<string,mixed>> */
|
||||||
public array $fornitoriAttiviRows = [];
|
public array $fornitoriAttiviRows = [];
|
||||||
|
|
@ -111,7 +123,6 @@ public function mount(): void
|
||||||
$this->status = 'all';
|
$this->status = 'all';
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->loadFornitoriOptions();
|
|
||||||
$this->loadCategorieTicketOptions();
|
$this->loadCategorieTicketOptions();
|
||||||
$this->refreshFornitoriAttiviRows();
|
$this->refreshFornitoriAttiviRows();
|
||||||
if ($this->selectedTicket) {
|
if ($this->selectedTicket) {
|
||||||
|
|
@ -192,11 +203,17 @@ public function updatedStatus(): void
|
||||||
$this->refreshData();
|
$this->refreshData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function updatedFornitoreSearch(): void
|
||||||
|
{
|
||||||
|
$this->searchFornitori();
|
||||||
|
}
|
||||||
|
|
||||||
public function refreshData(): void
|
public function refreshData(): void
|
||||||
{
|
{
|
||||||
$this->loadCounters();
|
$this->loadCounters();
|
||||||
$this->loadTickets();
|
$this->loadTickets();
|
||||||
$this->refreshFornitoriAttiviRows();
|
$this->refreshFornitoriAttiviRows();
|
||||||
|
$this->syncSelectedTicketState();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getTicketInserimentoUrl(): string
|
public function getTicketInserimentoUrl(): string
|
||||||
|
|
@ -211,6 +228,7 @@ public function apriScheda(int $ticketId): void
|
||||||
|
|
||||||
$ticket = $this->selectedTicket;
|
$ticket = $this->selectedTicket;
|
||||||
$this->fornitoreId = $ticket ? ((int) ($ticket->assegnato_a_fornitore_id ?? 0) ?: null): null;
|
$this->fornitoreId = $ticket ? ((int) ($ticket->assegnato_a_fornitore_id ?? 0) ?: null): null;
|
||||||
|
$this->syncSelectedTicketState();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function apriElenco(): void
|
public function apriElenco(): void
|
||||||
|
|
@ -220,7 +238,7 @@ public function apriElenco(): void
|
||||||
|
|
||||||
public function getFornitoreOperativoUrl(?int $fornitoreId = null): string
|
public function getFornitoreOperativoUrl(?int $fornitoreId = null): string
|
||||||
{
|
{
|
||||||
$base = route('fornitore.tickets.index');
|
$base = \App\Filament\Pages\Fornitore\TicketOperativi::getUrl(panel: 'admin-filament');
|
||||||
if ($fornitoreId) {
|
if ($fornitoreId) {
|
||||||
return $base . '?fornitore=' . $fornitoreId;
|
return $base . '?fornitore=' . $fornitoreId;
|
||||||
}
|
}
|
||||||
|
|
@ -293,6 +311,7 @@ public function getSelectedTicketProperty(): ?Ticket
|
||||||
'stabile:id,denominazione',
|
'stabile:id,denominazione',
|
||||||
'categoriaTicket:id,nome',
|
'categoriaTicket:id,nome',
|
||||||
'assegnatoAFornitore:id,ragione_sociale,nome,cognome',
|
'assegnatoAFornitore:id,ragione_sociale,nome,cognome',
|
||||||
|
'insuranceClaim',
|
||||||
'attachments.user:id,name',
|
'attachments.user:id,name',
|
||||||
'messages.user:id,name',
|
'messages.user:id,name',
|
||||||
'interventi.fornitore:id,ragione_sociale,nome,cognome',
|
'interventi.fornitore:id,ragione_sociale,nome,cognome',
|
||||||
|
|
@ -351,6 +370,31 @@ public function assegnaFornitore(): void
|
||||||
$this->refreshData();
|
$this->refreshData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function selezionaFornitore(int $fornitoreId): void
|
||||||
|
{
|
||||||
|
$match = collect($this->fornitoreMatches)->firstWhere('id', $fornitoreId);
|
||||||
|
|
||||||
|
if (! is_array($match)) {
|
||||||
|
$fornitore = $this->resolveFornitoriBaseQuery()->find($fornitoreId);
|
||||||
|
if (! $fornitore instanceof Fornitore) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$match = $this->mapFornitoreSearchRow($fornitore);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->fornitoreId = (int) $match['id'];
|
||||||
|
$this->fornitoreSearch = (string) $match['nome'];
|
||||||
|
$this->fornitoreMatches = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resetFornitoreSelection(): void
|
||||||
|
{
|
||||||
|
$this->fornitoreId = null;
|
||||||
|
$this->fornitoreSearch = '';
|
||||||
|
$this->fornitoreMatches = [];
|
||||||
|
}
|
||||||
|
|
||||||
public function aggiungiNotaInterna(): void
|
public function aggiungiNotaInterna(): void
|
||||||
{
|
{
|
||||||
$ticket = $this->selectedTicket;
|
$ticket = $this->selectedTicket;
|
||||||
|
|
@ -377,6 +421,50 @@ public function aggiungiNotaInterna(): void
|
||||||
Notification::make()->title('Nota aggiunta')->success()->send();
|
Notification::make()->title('Nota aggiunta')->success()->send();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function salvaSinistroAssicurativo(): void
|
||||||
|
{
|
||||||
|
$ticket = $this->selectedTicket;
|
||||||
|
if (! $ticket) {
|
||||||
|
Notification::make()->title('Ticket non trovato')->danger()->send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->validate([
|
||||||
|
'insurancePolicyId' => ['nullable', 'integer', 'exists:insurance_policies,id'],
|
||||||
|
'insurancePolicyReference' => ['nullable', 'string', 'max:255'],
|
||||||
|
'insuranceClaimNumber' => ['nullable', 'string', 'max:255'],
|
||||||
|
'insuranceNotes' => ['nullable', 'string', 'max:4000'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$claim = InsuranceClaim::query()->updateOrCreate(
|
||||||
|
['ticket_id' => (int) $ticket->id],
|
||||||
|
[
|
||||||
|
'insurance_policy_id' => (int) ($this->insurancePolicyId ?? 0) ?: null,
|
||||||
|
'stabile_id' => (int) $ticket->stabile_id,
|
||||||
|
'policy_reference' => filled($this->insurancePolicyReference) ? trim((string) $this->insurancePolicyReference) : null,
|
||||||
|
'claim_number' => filled($this->insuranceClaimNumber) ? trim((string) $this->insuranceClaimNumber) : null,
|
||||||
|
'status' => 'aperta',
|
||||||
|
'opened_at' => $ticket->insuranceClaim?->opened_at ?? now(),
|
||||||
|
'notes' => filled($this->insuranceNotes) ? trim((string) $this->insuranceNotes) : null,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$ticket->messages()->create([
|
||||||
|
'user_id' => Auth::id(),
|
||||||
|
'messaggio' => 'Apertura sinistro assicurativo collegata al ticket. Riferimento sinistro: ' . ($claim->claim_number ?: 'da definire'),
|
||||||
|
'canale' => 'assicurazione',
|
||||||
|
'direzione' => 'outbound',
|
||||||
|
'inviato_il' => now(),
|
||||||
|
'metadata' => [
|
||||||
|
'insurance_claim_id' => (int) $claim->id,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->refreshData();
|
||||||
|
|
||||||
|
Notification::make()->title('Sinistro assicurativo aggiornato')->success()->send();
|
||||||
|
}
|
||||||
|
|
||||||
public function caricaAllegati(): void
|
public function caricaAllegati(): void
|
||||||
{
|
{
|
||||||
$ticket = $this->selectedTicket;
|
$ticket = $this->selectedTicket;
|
||||||
|
|
@ -754,18 +842,83 @@ private function abilitaAccessoDipendente(FornitoreDipendente $dipendente): void
|
||||||
Notification::make()->title('Accesso fornitore abilitato')->body($body)->success()->send();
|
Notification::make()->title('Accesso fornitore abilitato')->body($body)->success()->send();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function loadFornitoriOptions(): void
|
private function searchFornitori(): void
|
||||||
|
{
|
||||||
|
$raw = trim($this->fornitoreSearch);
|
||||||
|
|
||||||
|
if ($raw === '') {
|
||||||
|
if ($this->fornitoreId) {
|
||||||
|
$selected = $this->resolveFornitoriBaseQuery()->find((int) $this->fornitoreId);
|
||||||
|
$this->fornitoreMatches = $selected instanceof Fornitore ? [$this->mapFornitoreSearchRow($selected)] : [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->fornitoreMatches = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$digits = preg_replace('/\D+/', '', $raw) ?: '';
|
||||||
|
$needleText = '%' . mb_strtolower($raw) . '%';
|
||||||
|
$needleDigits = '%' . $digits . '%';
|
||||||
|
$canonicalNeedles = $this->normalizeSearchTags($raw);
|
||||||
|
|
||||||
|
$matches = $this->resolveFornitoriBaseQuery()
|
||||||
|
->where(function ($query) use ($needleText, $needleDigits, $digits, $canonicalNeedles): void {
|
||||||
|
$query->orWhereRaw("LOWER(COALESCE(ragione_sociale, '')) LIKE ?", [$needleText])
|
||||||
|
->orWhereRaw("LOWER(COALESCE(nome, '')) LIKE ?", [$needleText])
|
||||||
|
->orWhereRaw("LOWER(COALESCE(cognome, '')) LIKE ?", [$needleText])
|
||||||
|
->orWhereRaw("LOWER(COALESCE(email, '')) LIKE ?", [$needleText])
|
||||||
|
->orWhereRaw("LOWER(COALESCE(tags, '')) LIKE ?", [$needleText])
|
||||||
|
->orWhereRaw("LOWER(COALESCE(note, '')) LIKE ?", [$needleText]);
|
||||||
|
|
||||||
|
foreach ($canonicalNeedles as $canonicalNeedle) {
|
||||||
|
$query->orWhereRaw("LOWER(COALESCE(tags, '')) LIKE ?", ['%' . $canonicalNeedle . '%']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($digits !== '') {
|
||||||
|
$query->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$needleDigits])
|
||||||
|
->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(cellulare, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$needleDigits]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->limit(60)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$this->fornitoreMatches = $matches
|
||||||
|
->map(function (Fornitore $fornitore) use ($raw, $digits, $canonicalNeedles): array {
|
||||||
|
$row = $this->mapFornitoreSearchRow($fornitore);
|
||||||
|
$row['score'] = $this->scoreFornitoreMatch($row, $raw, $digits, $canonicalNeedles);
|
||||||
|
|
||||||
|
return $row;
|
||||||
|
})
|
||||||
|
->sortByDesc('score')
|
||||||
|
->take(12)
|
||||||
|
->values()
|
||||||
|
->map(function (array $row): array {
|
||||||
|
unset($row['score']);
|
||||||
|
|
||||||
|
return $row;
|
||||||
|
})
|
||||||
|
->all();
|
||||||
|
|
||||||
|
if ($this->fornitoreId && ! collect($this->fornitoreMatches)->contains('id', $this->fornitoreId)) {
|
||||||
|
$selected = $this->resolveFornitoriBaseQuery()->find((int) $this->fornitoreId);
|
||||||
|
if ($selected instanceof Fornitore) {
|
||||||
|
array_unshift($this->fornitoreMatches, $this->mapFornitoreSearchRow($selected));
|
||||||
|
$this->fornitoreMatches = array_slice($this->fornitoreMatches, 0, 12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveFornitoriBaseQuery()
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
if (! $user instanceof User) {
|
if (! $user instanceof User) {
|
||||||
$this->fornitoriOptions = [];
|
return Fornitore::query()->whereRaw('1 = 0');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$stabileId = StabileContext::resolveActiveStabileId($user);
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
||||||
if (! $stabileId) {
|
if (! $stabileId) {
|
||||||
$this->fornitoriOptions = [];
|
return Fornitore::query()->whereRaw('1 = 0');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$stabile = \App\Models\Stabile::query()->find($stabileId);
|
$stabile = \App\Models\Stabile::query()->find($stabileId);
|
||||||
|
|
@ -773,18 +926,134 @@ private function loadFornitoriOptions(): void
|
||||||
|
|
||||||
$query = Fornitore::query()->orderBy('ragione_sociale')->orderBy('cognome')->orderBy('nome');
|
$query = Fornitore::query()->orderBy('ragione_sociale')->orderBy('cognome')->orderBy('nome');
|
||||||
if ($adminId > 0) {
|
if ($adminId > 0) {
|
||||||
$query->where(function ($q) use ($adminId): void {
|
$query->where(function ($builder) use ($adminId): void {
|
||||||
$q->where('amministratore_id', $adminId)->orWhereNull('amministratore_id');
|
$builder->where('amministratore_id', $adminId)->orWhereNull('amministratore_id');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->fornitoriOptions = $query->limit(400)->get()->map(function (Fornitore $f): array {
|
return $query;
|
||||||
$label = trim((string) ($f->ragione_sociale ?: trim(($f->nome ?? '') . ' ' . ($f->cognome ?? ''))));
|
}
|
||||||
return [
|
|
||||||
'id' => (int) $f->id,
|
/**
|
||||||
'nome' => $label !== '' ? $label : ('Fornitore #' . $f->id),
|
* @return array<string,mixed>
|
||||||
];
|
*/
|
||||||
})->all();
|
private function mapFornitoreSearchRow(Fornitore $fornitore): array
|
||||||
|
{
|
||||||
|
$tags = $this->splitFornitoreTags((string) ($fornitore->tags ?? ''));
|
||||||
|
$label = trim((string) ($fornitore->ragione_sociale ?: trim(($fornitore->nome ?? '') . ' ' . ($fornitore->cognome ?? ''))));
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => (int) $fornitore->id,
|
||||||
|
'nome' => $label !== '' ? $label : ('Fornitore #' . $fornitore->id),
|
||||||
|
'email' => (string) ($fornitore->email ?? ''),
|
||||||
|
'telefono' => (string) ($fornitore->telefono ?: $fornitore->cellulare),
|
||||||
|
'tags' => $tags,
|
||||||
|
'tags_label' => $tags !== [] ? implode(', ', $tags) : '',
|
||||||
|
'note' => Str::limit(trim((string) ($fornitore->note ?? '')), 120),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,mixed> $row
|
||||||
|
* @param array<int,string> $canonicalNeedles
|
||||||
|
*/
|
||||||
|
private function scoreFornitoreMatch(array $row, string $raw, string $digits, array $canonicalNeedles): int
|
||||||
|
{
|
||||||
|
$score = (int) (($row['id'] ?? 0) === (int) ($this->fornitoreId ?? 0) ? 1000 : 0);
|
||||||
|
$haystack = mb_strtolower(implode(' ', array_filter([
|
||||||
|
(string) ($row['nome'] ?? ''),
|
||||||
|
(string) ($row['email'] ?? ''),
|
||||||
|
(string) ($row['telefono'] ?? ''),
|
||||||
|
(string) ($row['tags_label'] ?? ''),
|
||||||
|
(string) ($row['note'] ?? ''),
|
||||||
|
])));
|
||||||
|
|
||||||
|
if ($raw !== '' && str_contains($haystack, mb_strtolower($raw))) {
|
||||||
|
$score += 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($digits !== '') {
|
||||||
|
$phoneDigits = preg_replace('/\D+/', '', (string) ($row['telefono'] ?? '')) ?: '';
|
||||||
|
if ($phoneDigits !== '' && str_contains($phoneDigits, $digits)) {
|
||||||
|
$score += 45;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$rowTags = array_map(fn(string $tag): string => mb_strtolower($tag), (array) ($row['tags'] ?? []));
|
||||||
|
foreach ($canonicalNeedles as $canonicalNeedle) {
|
||||||
|
foreach ($rowTags as $rowTag) {
|
||||||
|
if ($rowTag === $canonicalNeedle) {
|
||||||
|
$score += 90;
|
||||||
|
} elseif (str_contains($rowTag, $canonicalNeedle) || str_contains($canonicalNeedle, $rowTag)) {
|
||||||
|
$score += 55;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $score;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int,string>
|
||||||
|
*/
|
||||||
|
private function normalizeSearchTags(string $input): array
|
||||||
|
{
|
||||||
|
$tags = [];
|
||||||
|
foreach ($this->splitFornitoreTags($input) as $tag) {
|
||||||
|
$normalized = $this->canonicalizeFornitoreTag($tag);
|
||||||
|
if ($normalized !== null) {
|
||||||
|
$tags[] = $normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($tags));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int,string>
|
||||||
|
*/
|
||||||
|
private function splitFornitoreTags(string $value): array
|
||||||
|
{
|
||||||
|
$parts = preg_split('/[,;|\n\r\/]+/', $value) ?: [];
|
||||||
|
|
||||||
|
return array_values(array_filter(array_map(function (string $part): string {
|
||||||
|
$clean = trim($part);
|
||||||
|
$clean = preg_replace('/\s+/', ' ', $clean) ?? '';
|
||||||
|
|
||||||
|
return $clean;
|
||||||
|
}, $parts), fn(string $part): bool => $part !== ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function canonicalizeFornitoreTag(string $raw): ?string
|
||||||
|
{
|
||||||
|
$clean = trim(mb_strtolower($raw));
|
||||||
|
if ($clean === '' || mb_strlen($clean) < 3) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$map = [
|
||||||
|
'idr' => 'idraulico',
|
||||||
|
'idraul' => 'idraulico',
|
||||||
|
'elett' => 'elettricista',
|
||||||
|
'elettric' => 'elettricista',
|
||||||
|
'ascens' => 'ascensorista',
|
||||||
|
'puliz' => 'pulizie',
|
||||||
|
'giardin' => 'giardiniere',
|
||||||
|
'assicur' => 'assicurazione',
|
||||||
|
'manut' => 'manutenzione',
|
||||||
|
'spurgh' => 'spurghi',
|
||||||
|
'fogn' => 'spurghi',
|
||||||
|
'serr' => 'serrature',
|
||||||
|
'cald' => 'caldaia',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($map as $prefix => $normalized) {
|
||||||
|
if (str_starts_with($clean, $prefix)) {
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $clean;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function loadCategorieTicketOptions(): void
|
private function loadCategorieTicketOptions(): void
|
||||||
|
|
@ -825,6 +1094,66 @@ public function getTipoInterventoLabel(Ticket $ticket): string
|
||||||
return 'N/D';
|
return 'N/D';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function syncSelectedTicketState(): void
|
||||||
|
{
|
||||||
|
$ticket = $this->selectedTicket;
|
||||||
|
|
||||||
|
if (! $ticket) {
|
||||||
|
$this->fornitoreSearch = '';
|
||||||
|
$this->fornitoreMatches = [];
|
||||||
|
$this->insurancePolicyId = null;
|
||||||
|
$this->insurancePolicyReference = null;
|
||||||
|
$this->insuranceClaimNumber = null;
|
||||||
|
$this->insuranceNotes = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->fornitoreId = (int) ($ticket->assegnato_a_fornitore_id ?? 0) ?: null;
|
||||||
|
if ($this->fornitoreId) {
|
||||||
|
$selected = $this->resolveFornitoriBaseQuery()->find((int) $this->fornitoreId);
|
||||||
|
if ($selected instanceof Fornitore) {
|
||||||
|
$this->fornitoreSearch = $this->mapFornitoreSearchRow($selected)['nome'];
|
||||||
|
$this->fornitoreMatches = [$this->mapFornitoreSearchRow($selected)];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->fornitoreSearch = '';
|
||||||
|
$this->fornitoreMatches = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->insurancePolicyId = (int) ($ticket->insuranceClaim?->insurance_policy_id ?? 0) ?: null;
|
||||||
|
$this->insurancePolicyReference = (string) ($ticket->insuranceClaim?->policy_reference ?? '');
|
||||||
|
$this->insuranceClaimNumber = (string) ($ticket->insuranceClaim?->claim_number ?? '');
|
||||||
|
$this->insuranceNotes = (string) ($ticket->insuranceClaim?->notes ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public function getInsurancePolicyOptionsProperty(): array
|
||||||
|
{
|
||||||
|
$ticket = $this->selectedTicket;
|
||||||
|
if (! $ticket || ! class_exists(InsurancePolicy::class)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return InsurancePolicy::query()
|
||||||
|
->where('stabile_id', (int) $ticket->stabile_id)
|
||||||
|
->orderByDesc('renewal_at')
|
||||||
|
->orderByDesc('expires_at')
|
||||||
|
->orderBy('policy_number')
|
||||||
|
->get()
|
||||||
|
->mapWithKeys(function (InsurancePolicy $policy): array {
|
||||||
|
$label = trim(implode(' · ', array_filter([
|
||||||
|
$policy->display_name,
|
||||||
|
$policy->policy_number,
|
||||||
|
$policy->expires_at?->format('d/m/Y'),
|
||||||
|
])));
|
||||||
|
|
||||||
|
return [(int) $policy->id => ($label !== '' ? $label : ('Polizza #' . (int) $policy->id))];
|
||||||
|
})
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
private function loadCounters(): void
|
private function loadCounters(): void
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
|
||||||
|
|
@ -353,6 +353,11 @@ public function getTicketArchivioUrl(): string
|
||||||
return TicketGestione::getUrl(panel: 'admin-filament', parameters: ['status' => 'all']);
|
return TicketGestione::getUrl(panel: 'admin-filament', parameters: ['status' => 'all']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getHelpUrl(): string
|
||||||
|
{
|
||||||
|
return TicketMobileHelp::getUrl(panel: 'admin-filament');
|
||||||
|
}
|
||||||
|
|
||||||
public function getTicketDettaglioUrl(int $ticketId): string
|
public function getTicketDettaglioUrl(int $ticketId): string
|
||||||
{
|
{
|
||||||
return TicketScheda::getUrl(panel: 'admin-filament') . '?ticket=' . $ticketId;
|
return TicketScheda::getUrl(panel: 'admin-filament') . '?ticket=' . $ticketId;
|
||||||
|
|
|
||||||
41
app/Filament/Pages/Supporto/TicketMobileHelp.php
Normal file
41
app/Filament/Pages/Supporto/TicketMobileHelp.php
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?php
|
||||||
|
namespace App\Filament\Pages\Supporto;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use BackedEnum;
|
||||||
|
use Filament\Pages\Page;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use UnitEnum;
|
||||||
|
|
||||||
|
class TicketMobileHelp extends Page
|
||||||
|
{
|
||||||
|
protected static ?string $title = 'Help Ticket Mobile';
|
||||||
|
|
||||||
|
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-question-mark-circle';
|
||||||
|
|
||||||
|
protected static UnitEnum|string|null $navigationGroup = 'Supporto';
|
||||||
|
|
||||||
|
protected static ?string $slug = 'supporto/ticket-mobile-help';
|
||||||
|
|
||||||
|
protected static bool $shouldRegisterNavigation = false;
|
||||||
|
|
||||||
|
protected string $view = 'filament.pages.supporto.ticket-mobile-help';
|
||||||
|
|
||||||
|
public static function canAccess(): bool
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
return $user instanceof User
|
||||||
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBackUrl(): string
|
||||||
|
{
|
||||||
|
return TicketMobile::getUrl(panel: 'admin-filament');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getGestioneUrl(): string
|
||||||
|
{
|
||||||
|
return TicketGestione::getUrl(panel: 'admin-filament');
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,5 +1,4 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Pages;
|
namespace App\Filament\Pages;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
|
@ -29,14 +28,14 @@ public static function canAccess(): bool
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
return $user instanceof User
|
return $user instanceof User
|
||||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mount(): void
|
public function mount(): void
|
||||||
{
|
{
|
||||||
// Manteniamo la route storica ma riportiamo alla pagina unica.
|
// Manteniamo la route storica ma riportiamo alla pagina unica.
|
||||||
$unitaId = request()->query('unita_id');
|
$unitaId = request()->query('unita_id');
|
||||||
$url = UnitaImmobiliarePage::getUrl(panel: 'admin-filament');
|
$url = UnitaImmobiliarePage::getUrl(panel: 'admin-filament');
|
||||||
if (is_numeric($unitaId)) {
|
if (is_numeric($unitaId)) {
|
||||||
$url .= '?unita_id=' . (int) $unitaId;
|
$url .= '?unita_id=' . (int) $unitaId;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Pages;
|
namespace App\Filament\Pages;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
|
@ -29,14 +28,14 @@ public static function canAccess(): bool
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
return $user instanceof User
|
return $user instanceof User
|
||||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mount(): void
|
public function mount(): void
|
||||||
{
|
{
|
||||||
// Manteniamo la route storica ma riportiamo alla pagina unica.
|
// Manteniamo la route storica ma riportiamo alla pagina unica.
|
||||||
$unitaId = request()->query('unita_id');
|
$unitaId = request()->query('unita_id');
|
||||||
$url = UnitaImmobiliarePage::getUrl(panel: 'admin-filament');
|
$url = UnitaImmobiliarePage::getUrl(panel: 'admin-filament');
|
||||||
if (is_numeric($unitaId)) {
|
if (is_numeric($unitaId)) {
|
||||||
$url .= '?unita_id=' . (int) $unitaId;
|
$url .= '?unita_id=' . (int) $unitaId;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\RubricaUniversale;
|
use App\Models\RubricaUniversale;
|
||||||
|
use App\Models\Stabile;
|
||||||
use App\Models\Ticket;
|
use App\Models\Ticket;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
@ -33,6 +34,14 @@ public function index(Request $request)
|
||||||
->limit(25)
|
->limit(25)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
|
$stabili = Stabile::query()
|
||||||
|
->when($adminId, function ($q) use ($adminId): void {
|
||||||
|
$q->where('amministratore_id', $adminId);
|
||||||
|
})
|
||||||
|
->orderBy('denominazione')
|
||||||
|
->limit(12)
|
||||||
|
->get(['id', 'denominazione', 'indirizzo', 'citta', 'stato']);
|
||||||
|
|
||||||
$callerMatches = collect();
|
$callerMatches = collect();
|
||||||
if ($phoneQuery !== '') {
|
if ($phoneQuery !== '') {
|
||||||
$digits = preg_replace('/\D+/', '', $phoneQuery) ?: '';
|
$digits = preg_replace('/\D+/', '', $phoneQuery) ?: '';
|
||||||
|
|
@ -51,6 +60,7 @@ public function index(Request $request)
|
||||||
|
|
||||||
return view('admin.mobile.hub', [
|
return view('admin.mobile.hub', [
|
||||||
'tickets' => $tickets,
|
'tickets' => $tickets,
|
||||||
|
'stabili' => $stabili,
|
||||||
'status' => $status,
|
'status' => $status,
|
||||||
'phoneQuery' => $phoneQuery,
|
'phoneQuery' => $phoneQuery,
|
||||||
'callerMatches' => $callerMatches,
|
'callerMatches' => $callerMatches,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Fornitore;
|
||||||
|
use App\Models\FornitoreDipendente;
|
||||||
use App\Models\TicketAttachment;
|
use App\Models\TicketAttachment;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Support\StabileContext;
|
use App\Support\StabileContext;
|
||||||
|
|
@ -18,7 +20,7 @@ public function show(TicketAttachment $attachment): BinaryFileResponse
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore'])) {
|
if (! $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore', 'fornitore'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,9 +29,13 @@ public function show(TicketAttachment $attachment): BinaryFileResponse
|
||||||
abort(404);
|
abort(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
$stabileId = StabileContext::resolveActiveStabileId($user);
|
if ($user->hasRole('fornitore')) {
|
||||||
if (! $stabileId || (int) $ticket->stabile_id !== (int) $stabileId) {
|
$this->authorizeSupplierAttachment($user, $attachment);
|
||||||
abort(403);
|
} else {
|
||||||
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
||||||
|
if (! $stabileId || (int) $ticket->stabile_id !== (int) $stabileId) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$disk = Storage::disk('public');
|
$disk = Storage::disk('public');
|
||||||
|
|
@ -47,4 +53,73 @@ public function show(TicketAttachment $attachment): BinaryFileResponse
|
||||||
'Content-Disposition' => 'inline; filename="' . addslashes((string) ($attachment->original_file_name ?: basename($path))) . '"',
|
'Content-Disposition' => 'inline; filename="' . addslashes((string) ($attachment->original_file_name ?: basename($path))) . '"',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function authorizeSupplierAttachment(User $user, TicketAttachment $attachment): void
|
||||||
|
{
|
||||||
|
$fornitoreIds = [];
|
||||||
|
$currentSupplier = $this->resolveCurrentUserSupplier($user);
|
||||||
|
|
||||||
|
if ($currentSupplier instanceof Fornitore) {
|
||||||
|
$fornitoreIds[] = (int) $currentSupplier->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dipendente = $this->resolveCollaboratoreForUser($user, $currentSupplier?->id);
|
||||||
|
if ($dipendente instanceof FornitoreDipendente) {
|
||||||
|
$fornitoreIds[] = (int) $dipendente->fornitore_id;
|
||||||
|
|
||||||
|
if ((int) ($dipendente->fornitore_esterno_id ?? 0) > 0) {
|
||||||
|
$fornitoreIds[] = (int) $dipendente->fornitore_esterno_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$fornitoreIds = array_values(array_unique(array_filter($fornitoreIds)));
|
||||||
|
abort_if($fornitoreIds === [], 403);
|
||||||
|
|
||||||
|
$ticket = $attachment->ticket;
|
||||||
|
abort_unless($ticket, 404);
|
||||||
|
|
||||||
|
if (in_array((int) ($ticket->assegnato_a_fornitore_id ?? 0), $fornitoreIds, true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasIntervento = $ticket->interventi()
|
||||||
|
->whereIn('fornitore_id', $fornitoreIds)
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
abort_unless($hasIntervento, 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function resolveCurrentUserSupplier(User $user): ?Fornitore
|
||||||
|
{
|
||||||
|
$email = mb_strtolower(trim((string) $user->email));
|
||||||
|
if ($email === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Fornitore::query()
|
||||||
|
->whereRaw('LOWER(email) = ?', [$email])
|
||||||
|
->withCount(['ticketInterventi', 'dipendenti'])
|
||||||
|
->orderByDesc('ticket_interventi_count')
|
||||||
|
->orderByDesc('dipendenti_count')
|
||||||
|
->orderByDesc('id')
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function resolveCollaboratoreForUser(User $user, ?int $currentSupplierId = null): ?FornitoreDipendente
|
||||||
|
{
|
||||||
|
return FornitoreDipendente::query()
|
||||||
|
->where('attivo', true)
|
||||||
|
->where(function ($builder) use ($user, $currentSupplierId): void {
|
||||||
|
$builder->where('user_id', (int) $user->id)
|
||||||
|
->orWhereRaw('LOWER(email) = ?', [mb_strtolower((string) $user->email)]);
|
||||||
|
|
||||||
|
if (($currentSupplierId ?? 0) > 0) {
|
||||||
|
$builder->orWhere('fornitore_esterno_id', (int) $currentSupplierId);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->orderByRaw('CASE WHEN user_id = ? THEN 0 ELSE 1 END', [(int) $user->id])
|
||||||
|
->orderByRaw('CASE WHEN fornitore_esterno_id IS NULL THEN 1 ELSE 0 END')
|
||||||
|
->orderByDesc('id')
|
||||||
|
->first();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\CategoriaTicket;
|
use App\Models\CategoriaTicket;
|
||||||
|
use App\Models\CommunicationMessage;
|
||||||
use App\Models\Documento;
|
use App\Models\Documento;
|
||||||
use App\Models\Fornitore;
|
use App\Models\Fornitore;
|
||||||
use App\Models\InsuranceClaim;
|
use App\Models\InsuranceClaim;
|
||||||
|
|
@ -197,6 +198,26 @@ public function storeEmailMessage(Request $request, Ticket $ticket)
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
CommunicationMessage::query()->create([
|
||||||
|
'channel' => 'email',
|
||||||
|
'direction' => 'inbound',
|
||||||
|
'external_message_id' => $externalId !== '' ? $externalId : null,
|
||||||
|
'stabile_id' => $ticket->stabile_id,
|
||||||
|
'sender_name' => $mittente !== '' ? $mittente : null,
|
||||||
|
'message_text' => trim((string) ($validated['messaggio'] ?? '')) ?: ($oggetto !== '' ? $oggetto : 'Email collegata al ticket #' . $ticket->id),
|
||||||
|
'ticket_id' => (int) $ticket->id,
|
||||||
|
'status' => 'linked_to_ticket',
|
||||||
|
'received_at' => $inviatoIl,
|
||||||
|
'metadata' => [
|
||||||
|
'source' => 'admin_ticket_eml_import',
|
||||||
|
'email_mittente' => $mittente !== '' ? $mittente : null,
|
||||||
|
'email_destinatario' => $destinatario !== '' ? $destinatario : null,
|
||||||
|
'eml_documento_id' => (int) $documento->id,
|
||||||
|
'original_file_name' => $file->getClientOriginalName(),
|
||||||
|
'stored_path' => $path,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
return redirect()->route('admin.tickets.show', $ticket)
|
return redirect()->route('admin.tickets.show', $ticket)
|
||||||
->with('success', 'Email archiviata e collegata al ticket.');
|
->with('success', 'Email archiviata e collegata al ticket.');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
389
app/Http/Controllers/Condomino/AnagrafeController.php
Normal file
389
app/Http/Controllers/Condomino/AnagrafeController.php
Normal file
|
|
@ -0,0 +1,389 @@
|
||||||
|
<?php
|
||||||
|
namespace App\Http\Controllers\Condomino;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Condomino\Concerns\ResolvesCondominoAccess;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Persona;
|
||||||
|
use App\Models\PersonaAudit;
|
||||||
|
use App\Models\PersonaEmailMultipla;
|
||||||
|
use App\Models\UnitaRecapitoServizio;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\Comunicazioni\RecapitiServizioResolver;
|
||||||
|
use Dompdf\Dompdf;
|
||||||
|
use Dompdf\Options;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class AnagrafeController extends Controller
|
||||||
|
{
|
||||||
|
use ResolvesCondominoAccess;
|
||||||
|
|
||||||
|
public function __construct(private readonly RecapitiServizioResolver $resolver)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(): View
|
||||||
|
{
|
||||||
|
/** @var User|null $user */
|
||||||
|
$user = Auth::user();
|
||||||
|
$unitaImmobiliari = $this->resolveUserUnita($user)->values();
|
||||||
|
$persona = $this->resolvePortalPersona($user, $unitaImmobiliari);
|
||||||
|
$serviceLabels = UnitaRecapitoServizio::serviceLabels();
|
||||||
|
|
||||||
|
return view('condomino.anagrafe.show', [
|
||||||
|
'persona' => $persona,
|
||||||
|
'unitaImmobiliari' => $unitaImmobiliari,
|
||||||
|
'serviceLabels' => $serviceLabels,
|
||||||
|
'emailRows' => $this->buildEmailRows($persona),
|
||||||
|
'serviceContactMatrix' => $this->buildServiceContactMatrix($persona, $unitaImmobiliari),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
/** @var User|null $user */
|
||||||
|
$user = Auth::user();
|
||||||
|
$unitaImmobiliari = $this->resolveUserUnita($user)->values();
|
||||||
|
$persona = $this->resolvePortalPersona($user, $unitaImmobiliari);
|
||||||
|
|
||||||
|
if (! $persona instanceof Persona) {
|
||||||
|
return back()->withErrors([
|
||||||
|
'anagrafe' => 'Non e stato possibile collegare con certezza il tuo profilo a una scheda anagrafica digitale.',
|
||||||
|
])->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'cognome' => 'nullable|string|max:100',
|
||||||
|
'nome' => 'nullable|string|max:100',
|
||||||
|
'codice_fiscale' => 'nullable|string|max:16',
|
||||||
|
'partita_iva' => 'nullable|string|max:16',
|
||||||
|
'data_nascita' => 'nullable|date',
|
||||||
|
'residenza_via' => 'nullable|string|max:255',
|
||||||
|
'telefono_principale' => 'nullable|string|max:50',
|
||||||
|
'email_principale' => 'nullable|email|max:255',
|
||||||
|
'email_pec' => 'nullable|email|max:255',
|
||||||
|
'whatsapp' => 'nullable|string|max:50',
|
||||||
|
'lingua_preferita' => 'nullable|string|max:10',
|
||||||
|
'modalita_comunicazione_preferita' => 'nullable|in:email,sms,whatsapp,telefono,posta',
|
||||||
|
'consenso_privacy' => 'nullable|boolean',
|
||||||
|
'note' => 'nullable|string|max:4000',
|
||||||
|
'email_multiple' => 'nullable|array',
|
||||||
|
'email_multiple.*.id' => 'nullable|integer',
|
||||||
|
'email_multiple.*.email' => 'nullable|email|max:255',
|
||||||
|
'email_multiple.*.tipo_email' => 'nullable|string|max:50',
|
||||||
|
'email_multiple.*.attiva' => 'nullable|boolean',
|
||||||
|
'service_contacts' => 'nullable|array',
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::transaction(function () use ($validated, $persona, $unitaImmobiliari): void {
|
||||||
|
$this->updatePersona($persona, $validated);
|
||||||
|
$this->syncAdditionalEmails($persona, (array) ($validated['email_multiple'] ?? []));
|
||||||
|
$this->syncServiceContacts($persona, $unitaImmobiliari, (array) ($validated['service_contacts'] ?? []));
|
||||||
|
});
|
||||||
|
|
||||||
|
return redirect()->route('condomino.anagrafe.show')
|
||||||
|
->with('success', 'Scheda anagrafica aggiornata. Le modifiche sono tracciate con data, utente e IP.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function downloadModulo(): Response
|
||||||
|
{
|
||||||
|
/** @var User|null $user */
|
||||||
|
$user = Auth::user();
|
||||||
|
$unitaImmobiliari = $this->resolveUserUnita($user)->values();
|
||||||
|
$persona = $this->resolvePortalPersona($user, $unitaImmobiliari);
|
||||||
|
|
||||||
|
$html = view('condomino.anagrafe.modulo-pdf', [
|
||||||
|
'persona' => $persona,
|
||||||
|
'utente' => $user,
|
||||||
|
'unitaImmobiliari' => $unitaImmobiliari,
|
||||||
|
'serviceLabels' => UnitaRecapitoServizio::serviceLabels(),
|
||||||
|
'emailRows' => $this->buildEmailRows($persona),
|
||||||
|
'serviceContactMatrix' => $this->buildServiceContactMatrix($persona, $unitaImmobiliari),
|
||||||
|
'generatedAt' => now(),
|
||||||
|
])->render();
|
||||||
|
|
||||||
|
$options = new Options();
|
||||||
|
$options->set('isRemoteEnabled', true);
|
||||||
|
$options->set('defaultFont', 'DejaVu Sans');
|
||||||
|
|
||||||
|
$dompdf = new Dompdf($options);
|
||||||
|
$dompdf->setPaper('A4', 'portrait');
|
||||||
|
$dompdf->loadHtml($html, 'UTF-8');
|
||||||
|
$dompdf->render();
|
||||||
|
|
||||||
|
return response($dompdf->output(), 200, [
|
||||||
|
'Content-Type' => 'application/pdf',
|
||||||
|
'Content-Disposition' => 'attachment; filename="modulo-anagrafe-condominiale.pdf"',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function updatePersona(Persona $persona, array $validated): void
|
||||||
|
{
|
||||||
|
$fields = [
|
||||||
|
'cognome',
|
||||||
|
'nome',
|
||||||
|
'codice_fiscale',
|
||||||
|
'partita_iva',
|
||||||
|
'data_nascita',
|
||||||
|
'residenza_via',
|
||||||
|
'telefono_principale',
|
||||||
|
'email_principale',
|
||||||
|
'email_pec',
|
||||||
|
'whatsapp',
|
||||||
|
'lingua_preferita',
|
||||||
|
'modalita_comunicazione_preferita',
|
||||||
|
'note',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
$newValue = $validated[$field] ?? null;
|
||||||
|
$oldValue = $persona->getAttribute($field);
|
||||||
|
|
||||||
|
if ((string) $oldValue === (string) $newValue) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
PersonaAudit::registraModifica($persona->id, $field, $oldValue, $newValue);
|
||||||
|
$persona->setAttribute($field, $newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
$consensoPrivacy = (bool) ($validated['consenso_privacy'] ?? false);
|
||||||
|
if ((bool) $persona->consenso_privacy !== $consensoPrivacy) {
|
||||||
|
PersonaAudit::registraModifica($persona->id, 'consenso_privacy', $persona->consenso_privacy ? '1' : '0', $consensoPrivacy ? '1' : '0');
|
||||||
|
$persona->consenso_privacy = $consensoPrivacy;
|
||||||
|
$persona->data_consenso_privacy = $consensoPrivacy ? now() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$persona->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function syncAdditionalEmails(Persona $persona, array $rows): void
|
||||||
|
{
|
||||||
|
$before = $persona->emailMultiple()
|
||||||
|
->orderBy('id')
|
||||||
|
->get(['email', 'tipo_email', 'attiva'])
|
||||||
|
->map(fn(PersonaEmailMultipla $row): array=> [
|
||||||
|
'email' => mb_strtolower(trim((string) $row->email)),
|
||||||
|
'tipo_email' => (string) $row->tipo_email,
|
||||||
|
'attiva' => (bool) $row->attiva,
|
||||||
|
])
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$keepIds = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$email = mb_strtolower(trim((string) ($row['email'] ?? '')));
|
||||||
|
if ($email === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attrs = [
|
||||||
|
'email' => $email,
|
||||||
|
'tipo_email' => trim((string) ($row['tipo_email'] ?? 'secondaria')) ?: 'secondaria',
|
||||||
|
'attiva' => (bool) ($row['attiva'] ?? true),
|
||||||
|
];
|
||||||
|
|
||||||
|
$id = (int) ($row['id'] ?? 0);
|
||||||
|
if ($id > 0) {
|
||||||
|
$existing = $persona->emailMultiple()->whereKey($id)->first();
|
||||||
|
if ($existing instanceof PersonaEmailMultipla) {
|
||||||
|
$existing->fill($attrs);
|
||||||
|
$existing->save();
|
||||||
|
$keepIds[] = (int) $existing->id;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$created = $persona->emailMultiple()->create($attrs);
|
||||||
|
$keepIds[] = (int) $created->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$deleteQuery = $persona->emailMultiple();
|
||||||
|
if ($keepIds !== []) {
|
||||||
|
$deleteQuery->whereNotIn('id', $keepIds);
|
||||||
|
}
|
||||||
|
$deleteQuery->delete();
|
||||||
|
|
||||||
|
$after = $persona->emailMultiple()
|
||||||
|
->orderBy('id')
|
||||||
|
->get(['email', 'tipo_email', 'attiva'])
|
||||||
|
->map(fn(PersonaEmailMultipla $row): array=> [
|
||||||
|
'email' => mb_strtolower(trim((string) $row->email)),
|
||||||
|
'tipo_email' => (string) $row->tipo_email,
|
||||||
|
'attiva' => (bool) $row->attiva,
|
||||||
|
])
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
if (json_encode($before) !== json_encode($after)) {
|
||||||
|
PersonaAudit::registraModifica($persona->id, 'email_multiple', json_encode($before), json_encode($after));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function syncServiceContacts(Persona $persona, Collection $unitaImmobiliari, array $matrix): void
|
||||||
|
{
|
||||||
|
$allowedUnitIds = $unitaImmobiliari->pluck('id')->map(fn($id) => (int) $id)->all();
|
||||||
|
|
||||||
|
foreach ($allowedUnitIds as $unitaId) {
|
||||||
|
$serviceRows = (array) ($matrix[$unitaId] ?? []);
|
||||||
|
|
||||||
|
foreach (array_keys(UnitaRecapitoServizio::serviceLabels()) as $serviceType) {
|
||||||
|
$email = mb_strtolower(trim((string) ($serviceRows[$serviceType] ?? '')));
|
||||||
|
$query = UnitaRecapitoServizio::query()
|
||||||
|
->where('unita_id', $unitaId)
|
||||||
|
->where('persona_id', (int) $persona->id)
|
||||||
|
->where('tipo_servizio', $serviceType);
|
||||||
|
|
||||||
|
$before = $query->orderBy('id')->pluck('email')->map(fn($value) => mb_strtolower(trim((string) $value)))->values()->all();
|
||||||
|
|
||||||
|
if ($email === '') {
|
||||||
|
$query->delete();
|
||||||
|
$after = [];
|
||||||
|
} else {
|
||||||
|
$record = $query->orderBy('id')->first();
|
||||||
|
if (! $record instanceof UnitaRecapitoServizio) {
|
||||||
|
$record = new UnitaRecapitoServizio([
|
||||||
|
'unita_id' => $unitaId,
|
||||||
|
'persona_id' => (int) $persona->id,
|
||||||
|
'tipo_servizio' => $serviceType,
|
||||||
|
'ordine' => 1,
|
||||||
|
'is_default' => true,
|
||||||
|
'attivo' => true,
|
||||||
|
'sorgente' => 'portale_condomino',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$record->email = $email;
|
||||||
|
$record->etichetta = UnitaRecapitoServizio::serviceLabels()[$serviceType];
|
||||||
|
$record->last_confirmed_at = now();
|
||||||
|
$record->save();
|
||||||
|
|
||||||
|
UnitaRecapitoServizio::query()
|
||||||
|
->where('unita_id', $unitaId)
|
||||||
|
->where('persona_id', (int) $persona->id)
|
||||||
|
->where('tipo_servizio', $serviceType)
|
||||||
|
->where('id', '!=', (int) $record->id)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
$after = [$email];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json_encode($before) !== json_encode($after)) {
|
||||||
|
PersonaAudit::registraModifica(
|
||||||
|
$persona->id,
|
||||||
|
'unita_recapito:' . $unitaId . ':' . $serviceType,
|
||||||
|
json_encode($before),
|
||||||
|
json_encode($after)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
private function buildEmailRows(?Persona $persona): array
|
||||||
|
{
|
||||||
|
$rows = [];
|
||||||
|
if ($persona instanceof Persona) {
|
||||||
|
$rows = $persona->emailMultiple()
|
||||||
|
->orderByDesc('attiva')
|
||||||
|
->orderBy('tipo_email')
|
||||||
|
->orderBy('email')
|
||||||
|
->get()
|
||||||
|
->map(fn(PersonaEmailMultipla $row): array=> [
|
||||||
|
'id' => (int) $row->id,
|
||||||
|
'email' => (string) $row->email,
|
||||||
|
'tipo_email' => (string) ($row->tipo_email ?: 'secondaria'),
|
||||||
|
'attiva' => (bool) $row->attiva,
|
||||||
|
])
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
while (count($rows) < 4) {
|
||||||
|
$rows[] = [
|
||||||
|
'id' => null,
|
||||||
|
'email' => '',
|
||||||
|
'tipo_email' => 'secondaria',
|
||||||
|
'attiva' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array<string, array<string, mixed>>>
|
||||||
|
*/
|
||||||
|
private function buildServiceContactMatrix(?Persona $persona, Collection $unitaImmobiliari): array
|
||||||
|
{
|
||||||
|
$matrix = [];
|
||||||
|
$labels = UnitaRecapitoServizio::serviceLabels();
|
||||||
|
|
||||||
|
$storedRows = collect();
|
||||||
|
if ($persona instanceof Persona) {
|
||||||
|
$storedRows = UnitaRecapitoServizio::query()
|
||||||
|
->where('persona_id', (int) $persona->id)
|
||||||
|
->whereIn('unita_id', $unitaImmobiliari->pluck('id')->all())
|
||||||
|
->get()
|
||||||
|
->groupBy(['unita_id', 'tipo_servizio']);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($unitaImmobiliari as $unita) {
|
||||||
|
$unitRows = [];
|
||||||
|
|
||||||
|
foreach ($labels as $serviceType => $label) {
|
||||||
|
$stored = data_get($storedRows, $unita->id . '.' . $serviceType . '.0.email');
|
||||||
|
$resolved = $this->resolver->resolveForUnita($unita, $serviceType);
|
||||||
|
|
||||||
|
$unitRows[$serviceType] = [
|
||||||
|
'label' => $label,
|
||||||
|
'value' => (string) ($stored ?? ''),
|
||||||
|
'resolved' => $resolved,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$matrix[(int) $unita->id] = $unitRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $matrix;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolvePortalPersona(?User $user, Collection $unitaImmobiliari): ?Persona
|
||||||
|
{
|
||||||
|
if (! $user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$email = mb_strtolower(trim((string) $user->email));
|
||||||
|
if ($email === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$unitIds = $unitaImmobiliari->pluck('id')->map(fn($id) => (int) $id)->all();
|
||||||
|
|
||||||
|
$baseQuery = Persona::query()->with(['emailMultiple', 'relazioniUnitaAttive.unitaImmobiliare.stabile']);
|
||||||
|
if ($unitIds !== []) {
|
||||||
|
$baseQuery->whereHas('relazioniUnitaAttive', fn($query) => $query->whereIn('unita_id', $unitIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
$primaryMatch = (clone $baseQuery)
|
||||||
|
->whereRaw('LOWER(email_principale) = ?', [$email])
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($primaryMatch instanceof Persona) {
|
||||||
|
return $primaryMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (clone $baseQuery)
|
||||||
|
->whereHas('emailMultiple', fn($query) => $query->whereRaw('LOWER(email) = ?', [$email])->where('attiva', true))
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
<?php
|
||||||
|
namespace App\Http\Controllers\Condomino\Concerns;
|
||||||
|
|
||||||
|
use App\Models\UnitaImmobiliare;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
trait ResolvesCondominoAccess
|
||||||
|
{
|
||||||
|
protected function resolveSoggettoId(?User $user): ?int
|
||||||
|
{
|
||||||
|
if (! $user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($user->soggetto?->id_soggetto) && is_numeric($user->soggetto->id_soggetto)) {
|
||||||
|
return (int) $user->soggetto->id_soggetto;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($user->soggetto?->id) && is_numeric($user->soggetto->id)) {
|
||||||
|
return (int) $user->soggetto->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function resolveUserUnita(?User $user): Collection
|
||||||
|
{
|
||||||
|
if (! $user) {
|
||||||
|
return collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
$soggettoId = $this->resolveSoggettoId($user);
|
||||||
|
|
||||||
|
$query = UnitaImmobiliare::query()
|
||||||
|
->with(['stabile', 'soggetti'])
|
||||||
|
->where(function ($q) use ($soggettoId, $user): void {
|
||||||
|
if ($soggettoId) {
|
||||||
|
$q->whereHas('soggetti', function ($qp) use ($soggettoId): void {
|
||||||
|
$qp->where('soggetti.id', $soggettoId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$email = trim((string) $user->email);
|
||||||
|
if ($email !== '') {
|
||||||
|
$q->orWhereIn('id', function ($sub) use ($email): void {
|
||||||
|
$sub->select('ai.unita_immobiliare_id')
|
||||||
|
->from('affitti_immobili as ai')
|
||||||
|
->join('rubrica_universale as ru', 'ru.id', '=', 'ai.rubrica_inquilino_id')
|
||||||
|
->whereNotNull('ai.unita_immobiliare_id')
|
||||||
|
->whereRaw('LOWER(ru.email) = ?', [mb_strtolower($email)]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return $query->orderBy('stabile_id')->orderBy('scala')->orderBy('interno')->get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,41 +1,38 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers\Condomino;
|
namespace App\Http\Controllers\Condomino;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Condomino\Concerns\ResolvesCondominoAccess;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Ticket;
|
|
||||||
use App\Models\Rata;
|
|
||||||
use App\Models\Documento;
|
use App\Models\Documento;
|
||||||
use App\Models\UnitaImmobiliare;
|
use App\Models\Ticket;
|
||||||
use App\Models\Proprieta;
|
use App\Models\UnitaRecapitoServizio;
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Carbon\Carbon;
|
|
||||||
|
|
||||||
class DashboardController extends Controller
|
class DashboardController extends Controller
|
||||||
{
|
{
|
||||||
|
use ResolvesCondominoAccess;
|
||||||
|
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
// Trova le unità immobiliari associate all'utente
|
$soggettoId = $this->resolveSoggettoId($user);
|
||||||
$unitaImmobiliari = UnitaImmobiliare::whereHas('proprieta', function($q) use ($user) {
|
$unitaImmobiliari = $this->resolveUserUnita($user);
|
||||||
$q->where('soggetto_id', $user->soggetto->id_soggetto ?? null);
|
$stabiliIds = $unitaImmobiliari->pluck('stabile_id')->filter()->unique();
|
||||||
})->with(['stabile', 'proprieta.soggetto'])->get();
|
|
||||||
|
|
||||||
// Statistiche principali
|
// Statistiche principali
|
||||||
$stats = [
|
$stats = [
|
||||||
'unita_possedute' => $unitaImmobiliari->count(),
|
'unita_possedute' => $unitaImmobiliari->count(),
|
||||||
'ticket_aperti' => Ticket::where('soggetto_richiedente_id', $user->soggetto->id_soggetto ?? null)
|
'ticket_aperti' => Ticket::where('soggetto_richiedente_id', $soggettoId)
|
||||||
->whereIn('stato', ['Aperto', 'Preso in Carico', 'In Lavorazione'])->count(),
|
->whereIn('stato', ['Aperto', 'Preso in Carico', 'In Lavorazione'])->count(),
|
||||||
'rate_scadute' => 0, // Implementeremo quando avremo le rate
|
'rate_scadute' => 0, // Implementeremo quando avremo le rate
|
||||||
'documenti_disponibili' => Documento::whereHasMorph('documentable', ['App\Models\Stabile'], function($q) use ($unitaImmobiliari) {
|
'documenti_disponibili' => Documento::whereHasMorph('documentable', ['App\Models\Stabile'], function ($q) use ($stabiliIds) {
|
||||||
$q->whereIn('id_stabile', $unitaImmobiliari->pluck('stabile_id'));
|
$q->whereIn('id', $stabiliIds);
|
||||||
})->count(),
|
})->count(),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Ticket recenti
|
// Ticket recenti
|
||||||
$ticketRecenti = Ticket::where('soggetto_richiedente_id', $user->soggetto->id_soggetto ?? null)
|
$ticketRecenti = Ticket::where('soggetto_richiedente_id', $soggettoId)
|
||||||
->with(['stabile', 'categoriaTicket'])
|
->with(['stabile', 'categoriaTicket'])
|
||||||
->orderBy('created_at', 'desc')
|
->orderBy('created_at', 'desc')
|
||||||
->take(5)
|
->take(5)
|
||||||
|
|
@ -45,16 +42,22 @@ public function index()
|
||||||
$rateInScadenza = collect();
|
$rateInScadenza = collect();
|
||||||
|
|
||||||
// Ultimi documenti
|
// Ultimi documenti
|
||||||
$ultimiDocumenti = Documento::whereHasMorph('documentable', ['App\Models\Stabile'], function($q) use ($unitaImmobiliari) {
|
$ultimiDocumenti = Documento::whereHasMorph('documentable', ['App\Models\Stabile'], function ($q) use ($stabiliIds) {
|
||||||
$q->whereIn('id_stabile', $unitaImmobiliari->pluck('stabile_id'));
|
$q->whereIn('id', $stabiliIds);
|
||||||
})->orderBy('created_at', 'desc')->take(5)->get();
|
})->orderBy('created_at', 'desc')->take(5)->get();
|
||||||
|
|
||||||
|
$serviceContactsCount = UnitaRecapitoServizio::query()
|
||||||
|
->whereIn('unita_id', $unitaImmobiliari->pluck('id')->all())
|
||||||
|
->where('attivo', true)
|
||||||
|
->count();
|
||||||
|
|
||||||
return view('condomino.dashboard', compact(
|
return view('condomino.dashboard', compact(
|
||||||
'stats',
|
'stats',
|
||||||
'unitaImmobiliari',
|
'unitaImmobiliari',
|
||||||
'ticketRecenti',
|
'ticketRecenti',
|
||||||
'rateInScadenza',
|
'rateInScadenza',
|
||||||
'ultimiDocumenti'
|
'ultimiDocumenti',
|
||||||
|
'serviceContactsCount'
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,45 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers\Condomino;
|
namespace App\Http\Controllers\Condomino;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Condomino\Concerns\ResolvesCondominoAccess;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Documento;
|
use App\Models\Documento;
|
||||||
use App\Models\UnitaImmobiliare;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
class DocumentoController extends Controller
|
class DocumentoController extends Controller
|
||||||
{
|
{
|
||||||
|
use ResolvesCondominoAccess;
|
||||||
|
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
// Trova gli stabili delle unità dell'utente
|
|
||||||
$stabiliIds = UnitaImmobiliare::whereHas('proprieta', function($q) use ($user) {
|
|
||||||
$q->where('soggetto_id', $user->soggetto->id_soggetto ?? null);
|
|
||||||
})->pluck('stabile_id')->unique();
|
|
||||||
|
|
||||||
$query = Documento::whereHasMorph('documentable', ['App\Models\Stabile'], function($q) use ($stabiliIds) {
|
// Trova gli stabili delle unità dell'utente
|
||||||
$q->whereIn('id_stabile', $stabiliIds);
|
$stabiliIds = $this->resolveUserUnita($user)->pluck('stabile_id')->filter()->unique();
|
||||||
|
|
||||||
|
$query = Documento::whereHasMorph('documentable', ['App\Models\Stabile'], function ($q) use ($stabiliIds) {
|
||||||
|
$q->whereIn('id', $stabiliIds);
|
||||||
})->with('documentable');
|
})->with('documentable');
|
||||||
|
|
||||||
// Filtri
|
// Filtri
|
||||||
if ($request->filled('tipo_documento')) {
|
if ($request->filled('tipo_documento')) {
|
||||||
$query->where('tipo_documento', $request->tipo_documento);
|
$query->where('tipo_documento', $request->tipo_documento);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('search')) {
|
if ($request->filled('search')) {
|
||||||
$query->where(function($q) use ($request) {
|
$query->where(function ($q) use ($request) {
|
||||||
$q->where('nome_file', 'like', '%' . $request->search . '%')
|
$q->where('nome_file', 'like', '%' . $request->search . '%')
|
||||||
->orWhere('descrizione', 'like', '%' . $request->search . '%');
|
->orWhere('descrizione', 'like', '%' . $request->search . '%');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$documenti = $query->orderBy('created_at', 'desc')->paginate(20);
|
$documenti = $query->orderBy('created_at', 'desc')->paginate(20);
|
||||||
|
|
||||||
// Tipi documento per filtro
|
// Tipi documento per filtro
|
||||||
$tipiDocumento = Documento::whereHasMorph('documentable', ['App\Models\Stabile'], function($q) use ($stabiliIds) {
|
$tipiDocumento = Documento::whereHasMorph('documentable', ['App\Models\Stabile'], function ($q) use ($stabiliIds) {
|
||||||
$q->whereIn('id_stabile', $stabiliIds);
|
$q->whereIn('id', $stabiliIds);
|
||||||
})->distinct()->pluck('tipo_documento')->filter();
|
})->distinct()->pluck('tipo_documento')->filter();
|
||||||
|
|
||||||
return view('condomino.documenti.index', compact('documenti', 'tipiDocumento'));
|
return view('condomino.documenti.index', compact('documenti', 'tipiDocumento'));
|
||||||
|
|
@ -49,22 +48,20 @@ public function index(Request $request)
|
||||||
public function download(Documento $documento)
|
public function download(Documento $documento)
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
// Verifica accesso
|
// Verifica accesso
|
||||||
$stabiliIds = UnitaImmobiliare::whereHas('proprieta', function($q) use ($user) {
|
$stabiliIds = $this->resolveUserUnita($user)->pluck('stabile_id')->filter()->unique();
|
||||||
$q->where('soggetto_id', $user->soggetto->id_soggetto ?? null);
|
|
||||||
})->pluck('stabile_id')->unique();
|
|
||||||
|
|
||||||
if ($documento->documentable_type === 'App\Models\Stabile') {
|
if ($documento->documentable_type === 'App\Models\Stabile') {
|
||||||
if (!$stabiliIds->contains($documento->documentable_id)) {
|
if (! $stabiliIds->contains($documento->documentable_id)) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Storage::disk('public')->exists($documento->path_file)) {
|
if (! Storage::disk('public')->exists($documento->path_file)) {
|
||||||
abort(404, 'File non trovato');
|
abort(404, 'File non trovato');
|
||||||
}
|
}
|
||||||
|
|
||||||
return Storage::disk('public')->download($documento->path_file, $documento->nome_file);
|
return Storage::disk('public')->download($documento->path_file, $documento->nome_file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,21 @@
|
||||||
<?php
|
<?php
|
||||||
namespace App\Http\Controllers\Condomino;
|
namespace App\Http\Controllers\Condomino;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Condomino\Concerns\ResolvesCondominoAccess;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\StabileServizio;
|
use App\Models\StabileServizio;
|
||||||
use App\Models\StabileServizioLettura;
|
use App\Models\StabileServizioLettura;
|
||||||
use App\Models\UnitaImmobiliare;
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
class LetturaAcquaController extends Controller
|
class LetturaAcquaController extends Controller
|
||||||
{
|
{
|
||||||
|
use ResolvesCondominoAccess;
|
||||||
|
|
||||||
public function index(Request $request): View
|
public function index(Request $request): View
|
||||||
{
|
{
|
||||||
/** @var User|null $user */
|
/** @var User|null $user */
|
||||||
|
|
@ -149,41 +150,4 @@ public function store(Request $request): RedirectResponse
|
||||||
return redirect()->route('condomino.letture-acqua.index')
|
return redirect()->route('condomino.letture-acqua.index')
|
||||||
->with('success', 'Autolettura registrata correttamente.');
|
->with('success', 'Autolettura registrata correttamente.');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolveUserUnita(?User $user): Collection
|
|
||||||
{
|
|
||||||
if (! $user) {
|
|
||||||
return collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
$soggettoId = null;
|
|
||||||
if (isset($user->soggetto?->id_soggetto) && is_numeric($user->soggetto->id_soggetto)) {
|
|
||||||
$soggettoId = (int) $user->soggetto->id_soggetto;
|
|
||||||
} elseif (isset($user->soggetto?->id) && is_numeric($user->soggetto->id)) {
|
|
||||||
$soggettoId = (int) $user->soggetto->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
$query = UnitaImmobiliare::query()
|
|
||||||
->with('stabile')
|
|
||||||
->where(function ($q) use ($soggettoId, $user): void {
|
|
||||||
if ($soggettoId) {
|
|
||||||
$q->whereHas('proprieta', function ($qp) use ($soggettoId): void {
|
|
||||||
$qp->where('soggetto_id', $soggettoId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$email = trim((string) $user->email);
|
|
||||||
if ($email !== '') {
|
|
||||||
$q->orWhereIn('id', function ($sub) use ($email): void {
|
|
||||||
$sub->select('ai.unita_immobiliare_id')
|
|
||||||
->from('affitti_immobili as ai')
|
|
||||||
->join('rubrica_universale as ru', 'ru.id', '=', 'ai.rubrica_inquilino_id')
|
|
||||||
->whereNotNull('ai.unita_immobiliare_id')
|
|
||||||
->whereRaw('LOWER(ru.email) = ?', [mb_strtolower($email)]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return $query->orderBy('stabile_id')->orderBy('scala')->orderBy('interno')->get();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
<?php
|
<?php
|
||||||
namespace App\Http\Controllers\Condomino;
|
namespace App\Http\Controllers\Condomino;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Condomino\Concerns\ResolvesCondominoAccess;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\CategoriaTicket;
|
use App\Models\CategoriaTicket;
|
||||||
use App\Models\Ticket;
|
use App\Models\Ticket;
|
||||||
use App\Models\TicketMessage;
|
use App\Models\TicketMessage;
|
||||||
use App\Models\UnitaImmobiliare;
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
class TicketController extends Controller
|
class TicketController extends Controller
|
||||||
{
|
{
|
||||||
|
use ResolvesCondominoAccess;
|
||||||
|
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
@ -127,52 +128,4 @@ public function show(Ticket $ticket)
|
||||||
return view('condomino.tickets.show', compact('ticket'));
|
return view('condomino.tickets.show', compact('ticket'));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolveSoggettoId(?User $user): ?int
|
|
||||||
{
|
|
||||||
if (! $user) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($user->soggetto?->id_soggetto) && is_numeric($user->soggetto->id_soggetto)) {
|
|
||||||
return (int) $user->soggetto->id_soggetto;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($user->soggetto?->id) && is_numeric($user->soggetto->id)) {
|
|
||||||
return (int) $user->soggetto->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function resolveUserUnita(?User $user): Collection
|
|
||||||
{
|
|
||||||
if (! $user) {
|
|
||||||
return collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
$soggettoId = $this->resolveSoggettoId($user);
|
|
||||||
|
|
||||||
$query = UnitaImmobiliare::query()
|
|
||||||
->with('stabile')
|
|
||||||
->where(function ($q) use ($soggettoId, $user): void {
|
|
||||||
if ($soggettoId) {
|
|
||||||
$q->whereHas('proprieta', function ($qp) use ($soggettoId): void {
|
|
||||||
$qp->where('soggetto_id', $soggettoId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$email = trim((string) $user->email);
|
|
||||||
if ($email !== '') {
|
|
||||||
$q->orWhereIn('id', function ($sub) use ($email): void {
|
|
||||||
$sub->select('ai.unita_immobiliare_id')
|
|
||||||
->from('affitti_immobili as ai')
|
|
||||||
->join('rubrica_universale as ru', 'ru.id', '=', 'ai.rubrica_inquilino_id')
|
|
||||||
->whereNotNull('ai.unita_immobiliare_id')
|
|
||||||
->whereRaw('LOWER(ru.email) = ?', [mb_strtolower($email)]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return $query->orderBy('stabile_id')->orderBy('scala')->orderBy('interno')->get();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,24 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers\Condomino;
|
namespace App\Http\Controllers\Condomino;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Condomino\Concerns\ResolvesCondominoAccess;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\UnitaImmobiliare;
|
|
||||||
use App\Models\RichiestaModifica;
|
use App\Models\RichiestaModifica;
|
||||||
|
use App\Models\UnitaImmobiliare;
|
||||||
|
use App\Models\UnitaRecapitoServizio;
|
||||||
|
use App\Services\Comunicazioni\RecapitiServizioResolver;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
class UnitaController extends Controller
|
class UnitaController extends Controller
|
||||||
{
|
{
|
||||||
|
use ResolvesCondominoAccess;
|
||||||
|
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
$unitaImmobiliari = UnitaImmobiliare::whereHas('proprieta', function($q) use ($user) {
|
$unitaImmobiliari = $this->resolveUserUnita($user);
|
||||||
$q->where('soggetto_id', $user->soggetto->id_soggetto ?? null);
|
|
||||||
})->with(['stabile', 'proprieta.soggetto'])->get();
|
|
||||||
|
|
||||||
return view('condomino.unita.index', compact('unitaImmobiliari'));
|
return view('condomino.unita.index', compact('unitaImmobiliari'));
|
||||||
}
|
}
|
||||||
|
|
@ -24,50 +26,63 @@ public function index()
|
||||||
public function show(UnitaImmobiliare $unitaImmobiliare)
|
public function show(UnitaImmobiliare $unitaImmobiliare)
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
// Verifica accesso
|
// Verifica accesso
|
||||||
$hasAccess = $unitaImmobiliare->proprieta()
|
$hasAccess = $this->resolveUserUnita($user)
|
||||||
->where('soggetto_id', $user->soggetto->id_soggetto ?? null)
|
->pluck('id')
|
||||||
->exists();
|
->contains((int) $unitaImmobiliare->id);
|
||||||
|
|
||||||
if (!$hasAccess) {
|
if (! $hasAccess) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
$unitaImmobiliare->load(['stabile', 'proprieta.soggetto']);
|
$unitaImmobiliare->load(['stabile', 'soggetti', 'relazioniPersoneAttive.persona.emailMultiple', 'unitaRecapitiServizio']);
|
||||||
|
|
||||||
return view('condomino.unita.show', compact('unitaImmobiliare'));
|
$serviceLabels = UnitaRecapitoServizio::serviceLabels();
|
||||||
|
$serviceRecipients = [];
|
||||||
|
$resolver = app(RecapitiServizioResolver::class);
|
||||||
|
foreach (array_keys($serviceLabels) as $serviceType) {
|
||||||
|
$serviceRecipients[$serviceType] = $resolver->resolveForUnita($unitaImmobiliare, $serviceType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('condomino.unita.show', compact('unitaImmobiliare', 'serviceLabels', 'serviceRecipients'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function richiestaModifica(Request $request, UnitaImmobiliare $unitaImmobiliare)
|
public function richiestaModifica(Request $request, UnitaImmobiliare $unitaImmobiliare)
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
// Verifica accesso
|
// Verifica accesso
|
||||||
$hasAccess = $unitaImmobiliare->proprieta()
|
$hasAccess = $this->resolveUserUnita($user)
|
||||||
->where('soggetto_id', $user->soggetto->id_soggetto ?? null)
|
->pluck('id')
|
||||||
->exists();
|
->contains((int) $unitaImmobiliare->id);
|
||||||
|
|
||||||
if (!$hasAccess) {
|
if (! $hasAccess) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$soggettoId = $this->resolveSoggettoId($user);
|
||||||
|
|
||||||
|
if (! $soggettoId) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'tipo_modifica' => 'required|in:anagrafica,catastale,proprieta',
|
'tipo_modifica' => 'required|in:anagrafica,catastale,proprieta,catastale_servizio',
|
||||||
'descrizione' => 'required|string',
|
'descrizione' => 'required|string',
|
||||||
'dati_proposti' => 'required|array',
|
'dati_proposti' => 'required|array',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
RichiestaModifica::create([
|
RichiestaModifica::create([
|
||||||
'unita_immobiliare_id' => $unitaImmobiliare->id_unita,
|
'unita_immobiliare_id' => $unitaImmobiliare->id,
|
||||||
'soggetto_richiedente_id' => $user->soggetto->id_soggetto,
|
'soggetto_richiedente_id' => $soggettoId,
|
||||||
'tipo_modifica' => $request->tipo_modifica,
|
'tipo_modifica' => $request->tipo_modifica,
|
||||||
'descrizione' => $request->descrizione,
|
'descrizione' => $request->descrizione,
|
||||||
'dati_attuali' => $unitaImmobiliare->toArray(),
|
'dati_attuali' => $unitaImmobiliare->toArray(),
|
||||||
'dati_proposti' => $request->dati_proposti,
|
'dati_proposti' => $request->dati_proposti,
|
||||||
'stato' => 'in_attesa',
|
'stato' => 'in_attesa',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return back()->with('success', 'Richiesta di modifica inviata con successo.');
|
return back()->with('success', 'Richiesta di modifica inviata con successo.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,18 +18,12 @@ public function index()
|
||||||
{
|
{
|
||||||
$fornitore = $this->resolveFornitoreForUser();
|
$fornitore = $this->resolveFornitoreForUser();
|
||||||
|
|
||||||
$dipendenti = FornitoreDipendente::query()
|
return redirect()->to(
|
||||||
->with('user')
|
\App\Filament\Pages\Fornitore\Collaboratori::getUrl(
|
||||||
->where('fornitore_id', $fornitore->id)
|
['fornitore' => (int) $fornitore->id],
|
||||||
->orderBy('attivo', 'desc')
|
panel: 'admin-filament'
|
||||||
->orderBy('cognome')
|
)
|
||||||
->orderBy('nome')
|
);
|
||||||
->paginate(20);
|
|
||||||
|
|
||||||
return view('fornitore.dipendenti.index', [
|
|
||||||
'fornitore' => $fornitore,
|
|
||||||
'dipendenti' => $dipendenti,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(Request $request): RedirectResponse
|
public function store(Request $request): RedirectResponse
|
||||||
|
|
@ -43,6 +37,7 @@ public function store(Request $request): RedirectResponse
|
||||||
'telefono' => ['nullable', 'string', 'max:50'],
|
'telefono' => ['nullable', 'string', 'max:50'],
|
||||||
'note' => ['nullable', 'string', 'max:2000'],
|
'note' => ['nullable', 'string', 'max:2000'],
|
||||||
'create_user_access' => ['nullable', 'boolean'],
|
'create_user_access' => ['nullable', 'boolean'],
|
||||||
|
'access_password' => ['nullable', 'string', 'min:8', 'max:100'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$email = trim((string) ($validated['email'] ?? ''));
|
$email = trim((string) ($validated['email'] ?? ''));
|
||||||
|
|
@ -67,16 +62,23 @@ public function store(Request $request): RedirectResponse
|
||||||
$dipendente->created_by_user_id = Auth::id();
|
$dipendente->created_by_user_id = Auth::id();
|
||||||
$dipendente->updated_by_user_id = Auth::id();
|
$dipendente->updated_by_user_id = Auth::id();
|
||||||
|
|
||||||
|
$accessPassword = trim((string) ($validated['access_password'] ?? ''));
|
||||||
|
$createdPassword = null;
|
||||||
|
|
||||||
if ((bool) ($validated['create_user_access'] ?? false) && $email !== '') {
|
if ((bool) ($validated['create_user_access'] ?? false) && $email !== '') {
|
||||||
$user = User::query()->whereRaw('LOWER(email) = ?', [mb_strtolower($email)])->first();
|
$user = User::query()->whereRaw('LOWER(email) = ?', [mb_strtolower($email)])->first();
|
||||||
if (! $user) {
|
if (! $user) {
|
||||||
$generatedPassword = Str::random(12);
|
$createdPassword = $accessPassword !== '' ? $accessPassword : Str::random(12);
|
||||||
$user = User::query()->create([
|
$user = User::query()->create([
|
||||||
'name' => trim($dipendente->nome . ' ' . ($dipendente->cognome ?? '')),
|
'name' => trim($dipendente->nome . ' ' . ($dipendente->cognome ?? '')),
|
||||||
'email' => $email,
|
'email' => $email,
|
||||||
'password' => Hash::make($generatedPassword),
|
'password' => Hash::make($createdPassword),
|
||||||
'email_verified_at' => now(),
|
'email_verified_at' => now(),
|
||||||
]);
|
]);
|
||||||
|
} elseif ($accessPassword !== '') {
|
||||||
|
$user->password = Hash::make($accessPassword);
|
||||||
|
$user->save();
|
||||||
|
$createdPassword = $accessPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
$role = Role::query()->where('name', 'fornitore')->first();
|
$role = Role::query()->where('name', 'fornitore')->first();
|
||||||
|
|
@ -89,8 +91,17 @@ public function store(Request $request): RedirectResponse
|
||||||
|
|
||||||
$dipendente->save();
|
$dipendente->save();
|
||||||
|
|
||||||
return redirect()->route('fornitore.dipendenti.index')
|
$redirect = redirect()->route('fornitore.dipendenti.index')
|
||||||
->with('success', 'Dipendente creato correttamente.');
|
->with('success', 'Dipendente creato correttamente.');
|
||||||
|
|
||||||
|
if ($createdPassword !== null && $email !== '') {
|
||||||
|
$redirect->with('access_credentials', [
|
||||||
|
'email' => $email,
|
||||||
|
'password' => $createdPassword,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $redirect;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(Request $request, FornitoreDipendente $dipendente): RedirectResponse
|
public function update(Request $request, FornitoreDipendente $dipendente): RedirectResponse
|
||||||
|
|
@ -136,17 +147,35 @@ private function resolveFornitoreForUser(): Fornitore
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
$fornitore = Fornitore::query()
|
$fornitore = null;
|
||||||
->where('email', (string) ($user?->email ?? ''))
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if (! $fornitore && $user) {
|
if ($user) {
|
||||||
$link = FornitoreDipendente::query()
|
$email = mb_strtolower(trim((string) $user->email));
|
||||||
->where('user_id', (int) $user->id)
|
|
||||||
->where('attivo', true)
|
if ($email !== '') {
|
||||||
->first();
|
$fornitore = Fornitore::query()
|
||||||
if ($link) {
|
->whereRaw('LOWER(email) = ?', [$email])
|
||||||
$fornitore = Fornitore::query()->find((int) $link->fornitore_id);
|
->withCount(['ticketInterventi', 'dipendenti'])
|
||||||
|
->orderByDesc('ticket_interventi_count')
|
||||||
|
->orderByDesc('dipendenti_count')
|
||||||
|
->orderByDesc('id')
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $fornitore) {
|
||||||
|
$link = FornitoreDipendente::query()
|
||||||
|
->where('attivo', true)
|
||||||
|
->where(function ($q) use ($user): void {
|
||||||
|
$q->where('user_id', (int) $user->id)
|
||||||
|
->orWhereRaw('LOWER(email) = ?', [mb_strtolower((string) $user->email)]);
|
||||||
|
})
|
||||||
|
->orderByRaw('CASE WHEN user_id = ? THEN 0 ELSE 1 END', [(int) $user->id])
|
||||||
|
->orderByDesc('id')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($link) {
|
||||||
|
$fornitore = Fornitore::query()->find((int) $link->fornitore_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,85 +14,81 @@ class TicketOperativoController extends Controller
|
||||||
{
|
{
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
[$fornitore] = $this->resolveOperatoreContext();
|
[$fornitore, $dipendente] = $this->resolveOperatoreContext();
|
||||||
|
|
||||||
|
$url = \App\Filament\Pages\Fornitore\TicketOperativi::getUrl(
|
||||||
|
['fornitore' => (int) $fornitore->id],
|
||||||
|
panel: 'admin-filament'
|
||||||
|
);
|
||||||
|
|
||||||
$stato = (string) $request->query('stato', 'aperti');
|
$stato = (string) $request->query('stato', 'aperti');
|
||||||
$query = TicketIntervento::query()
|
if ($stato !== '') {
|
||||||
->with(['ticket.stabile'])
|
$url .= '&stato=' . urlencode($stato);
|
||||||
->where('fornitore_id', $fornitore->id)
|
|
||||||
->orderByDesc('created_at');
|
|
||||||
|
|
||||||
if ($stato === 'chiusi') {
|
|
||||||
$query->whereIn('stato', ['chiuso', 'fatturato']);
|
|
||||||
} elseif ($stato === 'fatturabili') {
|
|
||||||
$query->whereIn('stato', ['fatturabile', 'fatturato']);
|
|
||||||
} else {
|
|
||||||
$query->whereNotIn('stato', ['chiuso']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$interventi = $query->paginate(15)->withQueryString();
|
return redirect()->to($url);
|
||||||
|
|
||||||
$rows = collect($interventi->items())
|
|
||||||
->map(fn(TicketIntervento $intervento): array=> $this->buildInterventoRow($intervento));
|
|
||||||
|
|
||||||
$fatturabili = TicketIntervento::query()
|
|
||||||
->where('fornitore_id', $fornitore->id)
|
|
||||||
->whereIn('stato', ['fatturabile', 'fatturato'])
|
|
||||||
->orderByDesc('updated_at')
|
|
||||||
->limit(100)
|
|
||||||
->get();
|
|
||||||
|
|
||||||
return view('fornitore.tickets.index', [
|
|
||||||
'fornitore' => $fornitore,
|
|
||||||
'interventi' => $interventi,
|
|
||||||
'rows' => $rows,
|
|
||||||
'stato' => $stato,
|
|
||||||
'fatturabili' => $fatturabili,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function show(TicketIntervento $intervento)
|
public function show(TicketIntervento $intervento)
|
||||||
|
{
|
||||||
|
[$fornitore, $dipendente] = $this->resolveOperatoreContext();
|
||||||
|
$this->authorizeIntervento($intervento, $fornitore);
|
||||||
|
$this->authorizeDipendenteIntervento($intervento, $dipendente);
|
||||||
|
|
||||||
|
return redirect()->to(
|
||||||
|
\App\Filament\Pages\Fornitore\TicketInterventoScheda::getUrl(
|
||||||
|
['record' => (int) $intervento->id],
|
||||||
|
panel: 'admin-filament'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function assign(Request $request, TicketIntervento $intervento)
|
||||||
{
|
{
|
||||||
[$fornitore] = $this->resolveOperatoreContext();
|
[$fornitore] = $this->resolveOperatoreContext();
|
||||||
$this->authorizeIntervento($intervento, $fornitore);
|
$this->authorizeIntervento($intervento, $fornitore);
|
||||||
|
|
||||||
$intervento->load([
|
$validated = $request->validate([
|
||||||
'ticket.stabile',
|
'dipendente_id' => 'nullable|integer',
|
||||||
'ticket.messages.user',
|
|
||||||
'ticket.attachments.user',
|
|
||||||
'ticket.apertoDaUser',
|
|
||||||
'ticket.assegnatoAUser',
|
|
||||||
'ticket.assegnatoAFornitore',
|
|
||||||
'eseguitoDaDipendente',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$storico = TicketIntervento::query()
|
$dipendenteId = (int) ($validated['dipendente_id'] ?? 0);
|
||||||
->with(['ticket'])
|
$dipendente = null;
|
||||||
->where('fornitore_id', $fornitore->id)
|
|
||||||
->where('id', '!=', $intervento->id)
|
|
||||||
->when(
|
|
||||||
(int) ($intervento->ticket?->soggetto_richiedente_id ?? 0) > 0,
|
|
||||||
fn($q) => $q->whereHas('ticket', fn($t) => $t->where('soggetto_richiedente_id', (int) $intervento->ticket->soggetto_richiedente_id)),
|
|
||||||
fn($q) => $q->whereHas('ticket', fn($t) => $t->where('stabile_id', (int) ($intervento->ticket?->stabile_id ?? 0)))
|
|
||||||
)
|
|
||||||
->latest('id')
|
|
||||||
->limit(12)
|
|
||||||
->get();
|
|
||||||
|
|
||||||
$caller = $this->extractCallerData((string) ($intervento->ticket?->descrizione ?? ''));
|
if ($dipendenteId > 0) {
|
||||||
|
$dipendente = FornitoreDipendente::query()
|
||||||
|
->where('fornitore_id', (int) $fornitore->id)
|
||||||
|
->where('attivo', true)
|
||||||
|
->find($dipendenteId);
|
||||||
|
|
||||||
return view('fornitore.tickets.show', [
|
abort_unless($dipendente instanceof FornitoreDipendente, 404, 'Dipendente non valido per questo fornitore.');
|
||||||
'fornitore' => $fornitore,
|
}
|
||||||
'intervento' => $intervento,
|
|
||||||
'storico' => $storico,
|
$intervento->eseguito_da_dipendente_id = $dipendente?->id;
|
||||||
'caller' => $caller,
|
$intervento->save();
|
||||||
|
|
||||||
|
$stamp = now()->format('d/m/Y H:i');
|
||||||
|
$message = $dipendente
|
||||||
|
? '[' . $stamp . '] Intervento assegnato al dipendente fornitore: ' . $dipendente->nome_completo . '.'
|
||||||
|
: '[' . $stamp . '] Assegnazione dipendente rimossa: intervento riportato al coordinamento del fornitore.';
|
||||||
|
|
||||||
|
$intervento->ticket->messages()->create([
|
||||||
|
'user_id' => Auth::id(),
|
||||||
|
'messaggio' => $message,
|
||||||
|
'canale' => 'interno',
|
||||||
|
'direzione' => 'inbound',
|
||||||
|
'inviato_il' => now(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('fornitore.tickets.show', $intervento)
|
||||||
|
->with('success', $dipendente ? 'Intervento assegnato al dipendente selezionato.' : 'Assegnazione dipendente rimossa.');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function start(TicketIntervento $intervento)
|
public function start(TicketIntervento $intervento)
|
||||||
{
|
{
|
||||||
[$fornitore, $dipendente] = $this->resolveOperatoreContext();
|
[$fornitore, $dipendente] = $this->resolveOperatoreContext();
|
||||||
$this->authorizeIntervento($intervento, $fornitore);
|
$this->authorizeIntervento($intervento, $fornitore);
|
||||||
|
$this->authorizeDipendenteIntervento($intervento, $dipendente);
|
||||||
|
|
||||||
$intervento->update([
|
$intervento->update([
|
||||||
'stato' => 'in_corso',
|
'stato' => 'in_corso',
|
||||||
|
|
@ -111,6 +107,7 @@ public function complete(Request $request, TicketIntervento $intervento)
|
||||||
{
|
{
|
||||||
[$fornitore, $dipendente] = $this->resolveOperatoreContext();
|
[$fornitore, $dipendente] = $this->resolveOperatoreContext();
|
||||||
$this->authorizeIntervento($intervento, $fornitore);
|
$this->authorizeIntervento($intervento, $fornitore);
|
||||||
|
$this->authorizeDipendenteIntervento($intervento, $dipendente);
|
||||||
|
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'rapporto_fornitore' => 'required|string|max:5000',
|
'rapporto_fornitore' => 'required|string|max:5000',
|
||||||
|
|
@ -243,8 +240,9 @@ public function complete(Request $request, TicketIntervento $intervento)
|
||||||
|
|
||||||
public function sollecito(TicketIntervento $intervento)
|
public function sollecito(TicketIntervento $intervento)
|
||||||
{
|
{
|
||||||
[$fornitore] = $this->resolveOperatoreContext();
|
[$fornitore, $dipendente] = $this->resolveOperatoreContext();
|
||||||
$this->authorizeIntervento($intervento, $fornitore);
|
$this->authorizeIntervento($intervento, $fornitore);
|
||||||
|
$this->authorizeDipendenteIntervento($intervento, $dipendente);
|
||||||
|
|
||||||
$stamp = now()->format('d/m/Y H:i');
|
$stamp = now()->format('d/m/Y H:i');
|
||||||
$intervento->ticket->messages()->create([
|
$intervento->ticket->messages()->create([
|
||||||
|
|
@ -283,24 +281,37 @@ private function resolveOperatoreContext(): array
|
||||||
}
|
}
|
||||||
|
|
||||||
$dipendente = null;
|
$dipendente = null;
|
||||||
$fornitore = Fornitore::query()
|
$fornitore = null;
|
||||||
->where('email', (string) $user->email)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if (! $fornitore && $user) {
|
if ($user) {
|
||||||
$dipendente = FornitoreDipendente::query()
|
$email = mb_strtolower(trim((string) $user->email));
|
||||||
->where('attivo', true)
|
|
||||||
->where(function ($q) use ($user): void {
|
|
||||||
$q->where('user_id', (int) $user->id)
|
|
||||||
->orWhereRaw('LOWER(email) = ?', [mb_strtolower((string) $user->email)]);
|
|
||||||
})
|
|
||||||
->orderByDesc('id')
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if ($dipendente) {
|
if ($email !== '') {
|
||||||
$fornitore = Fornitore::query()->find((int) $dipendente->fornitore_id);
|
$fornitore = Fornitore::query()
|
||||||
$dipendente->ultimo_accesso_at = now();
|
->whereRaw('LOWER(email) = ?', [$email])
|
||||||
$dipendente->save();
|
->withCount(['ticketInterventi', 'dipendenti'])
|
||||||
|
->orderByDesc('ticket_interventi_count')
|
||||||
|
->orderByDesc('dipendenti_count')
|
||||||
|
->orderByDesc('id')
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $fornitore) {
|
||||||
|
$dipendente = FornitoreDipendente::query()
|
||||||
|
->where('attivo', true)
|
||||||
|
->where(function ($q) use ($user): void {
|
||||||
|
$q->where('user_id', (int) $user->id)
|
||||||
|
->orWhereRaw('LOWER(email) = ?', [mb_strtolower((string) $user->email)]);
|
||||||
|
})
|
||||||
|
->orderByRaw('CASE WHEN user_id = ? THEN 0 ELSE 1 END', [(int) $user->id])
|
||||||
|
->orderByDesc('id')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($dipendente) {
|
||||||
|
$fornitore = Fornitore::query()->find((int) $dipendente->fornitore_id);
|
||||||
|
$dipendente->ultimo_accesso_at = now();
|
||||||
|
$dipendente->save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -314,6 +325,16 @@ private function authorizeIntervento(TicketIntervento $intervento, Fornitore $fo
|
||||||
abort_unless((int) $intervento->fornitore_id === (int) $fornitore->id, 403);
|
abort_unless((int) $intervento->fornitore_id === (int) $fornitore->id, 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function authorizeDipendenteIntervento(TicketIntervento $intervento, ?FornitoreDipendente $dipendente): void
|
||||||
|
{
|
||||||
|
if (! $dipendente instanceof FornitoreDipendente) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$assignedDipendenteId = (int) ($intervento->eseguito_da_dipendente_id ?? 0);
|
||||||
|
abort_if($assignedDipendenteId > 0 && $assignedDipendenteId !== (int) $dipendente->id, 403, 'Intervento assegnato a un altro operatore del fornitore.');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{ingresso:string,contatto:string,telefono:string,problema:string,apparato:string}
|
* @return array{ingresso:string,contatto:string,telefono:string,problema:string,apparato:string}
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use App\Filament\Pages\Fornitore\TicketOperativi;
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
|
||||||
class RedirectIfAuthenticated
|
class RedirectIfAuthenticated
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
@ -22,20 +20,21 @@ public function handle(Request $request, Closure $next, string ...$guards): Resp
|
||||||
|
|
||||||
foreach ($guards as $guard) {
|
foreach ($guards as $guard) {
|
||||||
if (Auth::guard($guard)->check()) { // Se l'utente è autenticato...
|
if (Auth::guard($guard)->check()) { // Se l'utente è autenticato...
|
||||||
$user = Auth::user(); // ...recupera l'utente
|
$user = Auth::user(); // ...recupera l'utente
|
||||||
|
|
||||||
// Reindirizza l'utente alla dashboard specifica del suo ruolo
|
// Reindirizza l'utente alla dashboard specifica del suo ruolo
|
||||||
if ($user->hasRole('super-admin')) { return redirect()->route('superadmin.dashboard'); }
|
if ($user->hasRole('super-admin')) {return redirect('/admin-filament');}
|
||||||
if ($user->hasRole(['admin', 'amministratore'])) { return redirect()->route('admin.dashboard'); }
|
if ($user->hasRole(['admin', 'amministratore', 'collaboratore'])) {return redirect('/admin-filament');}
|
||||||
if ($user->hasRole('condomino')) { return redirect()->route('condomino.dashboard'); }
|
if ($user->hasRole('fornitore')) {return redirect()->to(TicketOperativi::getUrl(panel: 'admin-filament'));}
|
||||||
|
if ($user->hasRole('condomino')) {return redirect()->route('condomino.dashboard');}
|
||||||
|
|
||||||
// Fallback per utenti autenticati senza un ruolo specifico o con un ruolo non gestito
|
// Fallback per utenti autenticati senza un ruolo specifico o con un ruolo non gestito
|
||||||
return redirect()->route('dashboard');
|
return redirect()->route('dashboard');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Se l'utente non è autenticato, continua con la richiesta
|
// Se l'utente non è autenticato, continua con la richiesta
|
||||||
// Questo permette di accedere alle rotte pubbliche come login, registrazione, ecc
|
// Questo permette di accedere alle rotte pubbliche come login, registrazione, ecc
|
||||||
|
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
42
app/Models/AdempimentoFiscale.php
Normal file
42
app/Models/AdempimentoFiscale.php
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class AdempimentoFiscale extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'adempimenti_fiscali';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'tenant_id',
|
||||||
|
'stabile_id',
|
||||||
|
'gestione_id',
|
||||||
|
'tipo',
|
||||||
|
'anno_fiscale',
|
||||||
|
'titolo',
|
||||||
|
'descrizione',
|
||||||
|
'stato',
|
||||||
|
'scadenza',
|
||||||
|
'data_completamento',
|
||||||
|
'periodicita',
|
||||||
|
'metadati',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'anno_fiscale' => 'integer',
|
||||||
|
'scadenza' => 'date',
|
||||||
|
'data_completamento' => 'date',
|
||||||
|
'metadati' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function stabile(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Stabile::class, 'stabile_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function gestione(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(GestioneContabile::class, 'gestione_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ class InsuranceClaim extends Model
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'ticket_id',
|
'ticket_id',
|
||||||
'stabile_id',
|
'stabile_id',
|
||||||
|
'insurance_policy_id',
|
||||||
'policy_reference',
|
'policy_reference',
|
||||||
'claim_number',
|
'claim_number',
|
||||||
'status',
|
'status',
|
||||||
|
|
@ -29,6 +30,11 @@ public function ticket()
|
||||||
return $this->belongsTo(Ticket::class, 'ticket_id');
|
return $this->belongsTo(Ticket::class, 'ticket_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function insurancePolicy()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(InsurancePolicy::class, 'insurance_policy_id');
|
||||||
|
}
|
||||||
|
|
||||||
public function communicationMessages()
|
public function communicationMessages()
|
||||||
{
|
{
|
||||||
return $this->hasMany(CommunicationMessage::class, 'insurance_claim_id');
|
return $this->hasMany(CommunicationMessage::class, 'insurance_claim_id');
|
||||||
|
|
|
||||||
79
app/Models/InsurancePolicy.php
Normal file
79
app/Models/InsurancePolicy.php
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
<?php
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class InsurancePolicy extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'stabile_id',
|
||||||
|
'broker_rubrica_id',
|
||||||
|
'company_rubrica_id',
|
||||||
|
'policy_name',
|
||||||
|
'policy_number',
|
||||||
|
'policy_reference',
|
||||||
|
'status',
|
||||||
|
'started_at',
|
||||||
|
'expires_at',
|
||||||
|
'renewal_at',
|
||||||
|
'annual_amount',
|
||||||
|
'payment_split',
|
||||||
|
'payment_schedule_notes',
|
||||||
|
'coverage_summary',
|
||||||
|
'special_conditions',
|
||||||
|
'claim_form_template',
|
||||||
|
'policy_document_path',
|
||||||
|
'policy_document_name',
|
||||||
|
'policy_document_mime',
|
||||||
|
'signature_image_path',
|
||||||
|
'notes',
|
||||||
|
'metadata',
|
||||||
|
'created_by',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'started_at' => 'date',
|
||||||
|
'expires_at' => 'date',
|
||||||
|
'renewal_at' => 'date',
|
||||||
|
'annual_amount' => 'decimal:2',
|
||||||
|
'metadata' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function stabile()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Stabile::class, 'stabile_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function brokerRubrica()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(RubricaUniversale::class, 'broker_rubrica_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function companyRubrica()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(RubricaUniversale::class, 'company_rubrica_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function claims()
|
||||||
|
{
|
||||||
|
return $this->hasMany(InsuranceClaim::class, 'insurance_policy_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDisplayNameAttribute(): string
|
||||||
|
{
|
||||||
|
$label = trim((string) ($this->policy_name ?? ''));
|
||||||
|
if ($label !== '') {
|
||||||
|
return $label;
|
||||||
|
}
|
||||||
|
|
||||||
|
$label = trim((string) ($this->policy_number ?? ''));
|
||||||
|
if ($label !== '') {
|
||||||
|
return $label;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Polizza #' . (int) $this->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
class Persona extends Model
|
class Persona extends Model
|
||||||
{
|
{
|
||||||
|
|
@ -39,15 +38,15 @@ class Persona extends Model
|
||||||
'codice_fiscale_societa',
|
'codice_fiscale_societa',
|
||||||
'partita_iva_societa',
|
'partita_iva_societa',
|
||||||
'note',
|
'note',
|
||||||
'attivo'
|
'attivo',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'data_nascita' => 'date',
|
'data_nascita' => 'date',
|
||||||
'data_consenso_privacy' => 'datetime',
|
'data_consenso_privacy' => 'datetime',
|
||||||
'consenso_privacy' => 'boolean',
|
'consenso_privacy' => 'boolean',
|
||||||
'consenso_marketing' => 'boolean',
|
'consenso_marketing' => 'boolean',
|
||||||
'attivo' => 'boolean'
|
'attivo' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -65,7 +64,7 @@ public function unitaImmobiliari(): BelongsToMany
|
||||||
'riceve_comunicazioni',
|
'riceve_comunicazioni',
|
||||||
'riceve_convocazioni',
|
'riceve_convocazioni',
|
||||||
'vota_assemblea',
|
'vota_assemblea',
|
||||||
'note_relazione'
|
'note_relazione',
|
||||||
])
|
])
|
||||||
->withTimestamps()
|
->withTimestamps()
|
||||||
->wherePivot('attivo', true);
|
->wherePivot('attivo', true);
|
||||||
|
|
@ -95,6 +94,11 @@ public function emailMultiple(): HasMany
|
||||||
return $this->hasMany(PersonaEmailMultipla::class);
|
return $this->hasMany(PersonaEmailMultipla::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function unitaRecapitiServizio(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(UnitaRecapitoServizio::class, 'persona_id');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Società di cui è rappresentante legale
|
* Società di cui è rappresentante legale
|
||||||
*/
|
*/
|
||||||
|
|
@ -226,7 +230,7 @@ public static function verificaDuplicati($datiPersona, $escludiId = null)
|
||||||
$duplicati = [];
|
$duplicati = [];
|
||||||
|
|
||||||
// 1️⃣ CONTROLLO CODICE FISCALE (Critico)
|
// 1️⃣ CONTROLLO CODICE FISCALE (Critico)
|
||||||
if (!empty($datiPersona['codice_fiscale'])) {
|
if (! empty($datiPersona['codice_fiscale'])) {
|
||||||
$query = static::where('codice_fiscale', $datiPersona['codice_fiscale']);
|
$query = static::where('codice_fiscale', $datiPersona['codice_fiscale']);
|
||||||
if ($escludiId) {
|
if ($escludiId) {
|
||||||
$query->where('id', '!=', $escludiId);
|
$query->where('id', '!=', $escludiId);
|
||||||
|
|
@ -234,34 +238,34 @@ public static function verificaDuplicati($datiPersona, $escludiId = null)
|
||||||
|
|
||||||
if ($existing = $query->first()) {
|
if ($existing = $query->first()) {
|
||||||
$duplicati[] = [
|
$duplicati[] = [
|
||||||
'tipo' => 'CRITICO',
|
'tipo' => 'CRITICO',
|
||||||
'campo' => 'Codice Fiscale',
|
'campo' => 'Codice Fiscale',
|
||||||
'persona_esistente' => $existing,
|
'persona_esistente' => $existing,
|
||||||
'azione' => 'BLOCCA_O_MERGE'
|
'azione' => 'BLOCCA_O_MERGE',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2️⃣ CONTROLLO TELEFONO PRINCIPALE (Critico)
|
// 2️⃣ CONTROLLO TELEFONO PRINCIPALE (Critico)
|
||||||
if (!empty($datiPersona['telefono_principale'])) {
|
if (! empty($datiPersona['telefono_principale'])) {
|
||||||
$telefono = preg_replace('/[^\d+]/', '', $datiPersona['telefono_principale']);
|
$telefono = preg_replace('/[^\d+]/', '', $datiPersona['telefono_principale']);
|
||||||
$query = static::where('telefono_principale', $telefono);
|
$query = static::where('telefono_principale', $telefono);
|
||||||
if ($escludiId) {
|
if ($escludiId) {
|
||||||
$query->where('id', '!=', $escludiId);
|
$query->where('id', '!=', $escludiId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($existing = $query->first()) {
|
if ($existing = $query->first()) {
|
||||||
$duplicati[] = [
|
$duplicati[] = [
|
||||||
'tipo' => 'CRITICO',
|
'tipo' => 'CRITICO',
|
||||||
'campo' => 'Telefono Principale',
|
'campo' => 'Telefono Principale',
|
||||||
'persona_esistente' => $existing,
|
'persona_esistente' => $existing,
|
||||||
'azione' => 'BLOCCA_O_MERGE'
|
'azione' => 'BLOCCA_O_MERGE',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3️⃣ CONTROLLO EMAIL (Warning)
|
// 3️⃣ CONTROLLO EMAIL (Warning)
|
||||||
if (!empty($datiPersona['email_principale'])) {
|
if (! empty($datiPersona['email_principale'])) {
|
||||||
$query = static::where('email_principale', $datiPersona['email_principale']);
|
$query = static::where('email_principale', $datiPersona['email_principale']);
|
||||||
if ($escludiId) {
|
if ($escludiId) {
|
||||||
$query->where('id', '!=', $escludiId);
|
$query->where('id', '!=', $escludiId);
|
||||||
|
|
@ -269,16 +273,16 @@ public static function verificaDuplicati($datiPersona, $escludiId = null)
|
||||||
|
|
||||||
if ($existing = $query->first()) {
|
if ($existing = $query->first()) {
|
||||||
$duplicati[] = [
|
$duplicati[] = [
|
||||||
'tipo' => 'WARNING',
|
'tipo' => 'WARNING',
|
||||||
'campo' => 'Email Principale',
|
'campo' => 'Email Principale',
|
||||||
'persona_esistente' => $existing,
|
'persona_esistente' => $existing,
|
||||||
'azione' => 'AVVISA'
|
'azione' => 'AVVISA',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4️⃣ CONTROLLO OMONIMIA (Warning)
|
// 4️⃣ CONTROLLO OMONIMIA (Warning)
|
||||||
if (!empty($datiPersona['cognome']) && !empty($datiPersona['nome']) && !empty($datiPersona['data_nascita'])) {
|
if (! empty($datiPersona['cognome']) && ! empty($datiPersona['nome']) && ! empty($datiPersona['data_nascita'])) {
|
||||||
$query = static::where('cognome', $datiPersona['cognome'])
|
$query = static::where('cognome', $datiPersona['cognome'])
|
||||||
->where('nome', $datiPersona['nome'])
|
->where('nome', $datiPersona['nome'])
|
||||||
->where('data_nascita', $datiPersona['data_nascita']);
|
->where('data_nascita', $datiPersona['data_nascita']);
|
||||||
|
|
@ -288,10 +292,10 @@ public static function verificaDuplicati($datiPersona, $escludiId = null)
|
||||||
|
|
||||||
if ($existing = $query->first()) {
|
if ($existing = $query->first()) {
|
||||||
$duplicati[] = [
|
$duplicati[] = [
|
||||||
'tipo' => 'WARNING',
|
'tipo' => 'WARNING',
|
||||||
'campo' => 'Omonimia Completa',
|
'campo' => 'Omonimia Completa',
|
||||||
'persona_esistente' => $existing,
|
'persona_esistente' => $existing,
|
||||||
'azione' => 'VERIFICA_MANUALE'
|
'azione' => 'VERIFICA_MANUALE',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -315,7 +319,7 @@ protected static function boot()
|
||||||
|
|
||||||
// Audit delle modifiche
|
// Audit delle modifiche
|
||||||
static::updating(function ($persona) {
|
static::updating(function ($persona) {
|
||||||
$changes = $persona->getChanges();
|
$changes = $persona->getChanges();
|
||||||
$original = $persona->getOriginal();
|
$original = $persona->getOriginal();
|
||||||
|
|
||||||
foreach ($changes as $campo => $nuovoValore) {
|
foreach ($changes as $campo => $nuovoValore) {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use App\Models\RubricaUniversale;
|
use App\Models\RubricaUniversale;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
|
|
||||||
class Stabile extends Model
|
class Stabile extends Model
|
||||||
{
|
{
|
||||||
|
|
@ -163,6 +165,16 @@ public function amministratore()
|
||||||
return $this->belongsTo(Amministratore::class, 'amministratore_id', 'id');
|
return $this->belongsTo(Amministratore::class, 'amministratore_id', 'id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function amministratoreTransfers(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(StabileAmministratoreTransfer::class, 'stabile_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function latestAmministratoreTransfer(): HasOne
|
||||||
|
{
|
||||||
|
return $this->hasOne(StabileAmministratoreTransfer::class, 'stabile_id', 'id')->latestOfMany();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rubrica universale collegata (opzionale)
|
* Rubrica universale collegata (opzionale)
|
||||||
*/
|
*/
|
||||||
|
|
@ -187,6 +199,14 @@ public function tickets()
|
||||||
return $this->hasMany(Ticket::class, 'stabile_id', 'id');
|
return $this->hasMany(Ticket::class, 'stabile_id', 'id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function insurancePolicies()
|
||||||
|
{
|
||||||
|
return $this->hasMany(InsurancePolicy::class, 'stabile_id', 'id')
|
||||||
|
->orderByDesc('renewal_at')
|
||||||
|
->orderByDesc('expires_at')
|
||||||
|
->orderByDesc('id');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relazione con RipartizioneSpese
|
* Relazione con RipartizioneSpese
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
54
app/Models/StabileAmministratoreTransfer.php
Normal file
54
app/Models/StabileAmministratoreTransfer.php
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class StabileAmministratoreTransfer extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'stabile_amministratore_transfers';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'stabile_id',
|
||||||
|
'from_amministratore_id',
|
||||||
|
'to_amministratore_id',
|
||||||
|
'changed_by_user_id',
|
||||||
|
'changed_by_name',
|
||||||
|
'source',
|
||||||
|
'reason',
|
||||||
|
'also_rubrica',
|
||||||
|
'ip_address',
|
||||||
|
'meta',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'also_rubrica' => 'boolean',
|
||||||
|
'meta' => 'array',
|
||||||
|
'created_at' => 'datetime',
|
||||||
|
'updated_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function stabile(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Stabile::class, 'stabile_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fromAmministratore(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Amministratore::class, 'from_amministratore_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toAmministratore(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Amministratore::class, 'to_amministratore_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function changedByUser(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'changed_by_user_id', 'id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,10 +25,21 @@ class StabileServizioLettura extends Model
|
||||||
'protocollo_numero',
|
'protocollo_numero',
|
||||||
'richiesta_lettura_inviata_at',
|
'richiesta_lettura_inviata_at',
|
||||||
'prossima_lettura_scadenza_at',
|
'prossima_lettura_scadenza_at',
|
||||||
|
'deadline_lettura_at',
|
||||||
|
'sollecito_inviato_at',
|
||||||
|
'rilevatore_tipo',
|
||||||
|
'rilevatore_nome',
|
||||||
'archivio_documentale_path',
|
'archivio_documentale_path',
|
||||||
'archivio_documentale_sha256',
|
'archivio_documentale_sha256',
|
||||||
|
'lettura_precedente_valore',
|
||||||
'lettura_inizio',
|
'lettura_inizio',
|
||||||
'lettura_fine',
|
'lettura_fine',
|
||||||
|
'lettura_foto_path',
|
||||||
|
'lettura_foto_original_name',
|
||||||
|
'lettura_foto_metadata',
|
||||||
|
'lettura_precedente_foto_path',
|
||||||
|
'lettura_ocr_valore',
|
||||||
|
'lettura_ocr_confidenza',
|
||||||
'consumo_valore',
|
'consumo_valore',
|
||||||
'consumo_unita',
|
'consumo_unita',
|
||||||
'importo_totale',
|
'importo_totale',
|
||||||
|
|
@ -41,8 +52,13 @@ class StabileServizioLettura extends Model
|
||||||
'periodo_al' => 'date',
|
'periodo_al' => 'date',
|
||||||
'richiesta_lettura_inviata_at' => 'datetime',
|
'richiesta_lettura_inviata_at' => 'datetime',
|
||||||
'prossima_lettura_scadenza_at' => 'datetime',
|
'prossima_lettura_scadenza_at' => 'datetime',
|
||||||
|
'deadline_lettura_at' => 'datetime',
|
||||||
|
'sollecito_inviato_at' => 'datetime',
|
||||||
|
'lettura_precedente_valore' => 'decimal:3',
|
||||||
'lettura_inizio' => 'decimal:3',
|
'lettura_inizio' => 'decimal:3',
|
||||||
'lettura_fine' => 'decimal:3',
|
'lettura_fine' => 'decimal:3',
|
||||||
|
'lettura_foto_metadata' => 'array',
|
||||||
|
'lettura_ocr_confidenza' => 'decimal:2',
|
||||||
'consumo_valore' => 'decimal:3',
|
'consumo_valore' => 'decimal:3',
|
||||||
'importo_totale' => 'decimal:2',
|
'importo_totale' => 'decimal:2',
|
||||||
'raw' => 'array',
|
'raw' => 'array',
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Models\RubricaRuolo;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use App\Models\RubricaRuolo;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
class UnitaImmobiliare extends Model
|
class UnitaImmobiliare extends Model
|
||||||
{
|
{
|
||||||
|
|
@ -125,31 +124,31 @@ class UnitaImmobiliare extends Model
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'consistenza' => 'decimal:2',
|
'consistenza' => 'decimal:2',
|
||||||
'rendita_catastale' => 'decimal:2',
|
'rendita_catastale' => 'decimal:2',
|
||||||
'superficie_commerciale' => 'decimal:2',
|
'superficie_commerciale' => 'decimal:2',
|
||||||
'superficie_calpestabile' => 'decimal:2',
|
'superficie_calpestabile' => 'decimal:2',
|
||||||
'superficie_balconi' => 'decimal:2',
|
'superficie_balconi' => 'decimal:2',
|
||||||
'superficie_giardino_privato' => 'decimal:2',
|
'superficie_giardino_privato' => 'decimal:2',
|
||||||
'ha_balconi' => 'boolean',
|
'ha_balconi' => 'boolean',
|
||||||
'ha_giardino_privato' => 'boolean',
|
'ha_giardino_privato' => 'boolean',
|
||||||
'ha_posto_auto_assegnato' => 'boolean',
|
'ha_posto_auto_assegnato' => 'boolean',
|
||||||
'ha_cantina_assegnata' => 'boolean',
|
'ha_cantina_assegnata' => 'boolean',
|
||||||
'ha_soffitta_assegnata' => 'boolean',
|
'ha_soffitta_assegnata' => 'boolean',
|
||||||
'ha_climatizzazione' => 'boolean',
|
'ha_climatizzazione' => 'boolean',
|
||||||
'ha_impianto_gas' => 'boolean',
|
'ha_impianto_gas' => 'boolean',
|
||||||
'ha_cambio_destinazione_uso' => 'boolean',
|
'ha_cambio_destinazione_uso' => 'boolean',
|
||||||
'millesimi_generali' => 'decimal:3',
|
'millesimi_generali' => 'decimal:3',
|
||||||
'millesimi_riscaldamento' => 'decimal:3',
|
'millesimi_riscaldamento' => 'decimal:3',
|
||||||
'millesimi_acqua' => 'decimal:3',
|
'millesimi_acqua' => 'decimal:3',
|
||||||
'millesimi_ascensore' => 'decimal:3',
|
'millesimi_ascensore' => 'decimal:3',
|
||||||
'millesimi_scale' => 'decimal:3',
|
'millesimi_scale' => 'decimal:3',
|
||||||
'valore_commerciale_stimato' => 'decimal:2',
|
'valore_commerciale_stimato' => 'decimal:2',
|
||||||
'canone_locazione_mensile' => 'decimal:2',
|
'canone_locazione_mensile' => 'decimal:2',
|
||||||
'data_scadenza_autorizzazioni' => 'date',
|
'data_scadenza_autorizzazioni' => 'date',
|
||||||
'data_ultima_valutazione' => 'date',
|
'data_ultima_valutazione' => 'date',
|
||||||
'attiva' => 'boolean',
|
'attiva' => 'boolean',
|
||||||
'unita_demo' => 'boolean',
|
'unita_demo' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -225,6 +224,23 @@ public function rubricaRuoliAttivi(): HasMany
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function relazioniPersone(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(PersonaUnitaRelazione::class, 'unita_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function relazioniPersoneAttive(): HasMany
|
||||||
|
{
|
||||||
|
return $this->relazioniPersone()->where('attivo', true)->where(function ($query) {
|
||||||
|
$query->whereNull('data_fine')->orWhere('data_fine', '>=', now()->toDateString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function unitaRecapitiServizio(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(UnitaRecapitoServizio::class, 'unita_id');
|
||||||
|
}
|
||||||
|
|
||||||
// Scope methods
|
// Scope methods
|
||||||
public function scopeAttive($query)
|
public function scopeAttive($query)
|
||||||
{
|
{
|
||||||
|
|
@ -322,27 +338,27 @@ public function getUnitaVicine()
|
||||||
// Unità sovrastante
|
// Unità sovrastante
|
||||||
if ($sovrastante = $this->getUnitaSovrastante()) {
|
if ($sovrastante = $this->getUnitaSovrastante()) {
|
||||||
$vicine->push([
|
$vicine->push([
|
||||||
'unita' => $sovrastante,
|
'unita' => $sovrastante,
|
||||||
'relazione' => 'sovrastante',
|
'relazione' => 'sovrastante',
|
||||||
'descrizione' => 'Piano superiore'
|
'descrizione' => 'Piano superiore',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unità sottostante
|
// Unità sottostante
|
||||||
if ($sottostante = $this->getUnitaSottostante()) {
|
if ($sottostante = $this->getUnitaSottostante()) {
|
||||||
$vicine->push([
|
$vicine->push([
|
||||||
'unita' => $sottostante,
|
'unita' => $sottostante,
|
||||||
'relazione' => 'sottostante',
|
'relazione' => 'sottostante',
|
||||||
'descrizione' => 'Piano inferiore'
|
'descrizione' => 'Piano inferiore',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unità laterali
|
// Unità laterali
|
||||||
foreach ($this->getUnitaLaterali() as $laterale) {
|
foreach ($this->getUnitaLaterali() as $laterale) {
|
||||||
$vicine->push([
|
$vicine->push([
|
||||||
'unita' => $laterale,
|
'unita' => $laterale,
|
||||||
'relazione' => 'laterale',
|
'relazione' => 'laterale',
|
||||||
'descrizione' => 'Stesso piano'
|
'descrizione' => 'Stesso piano',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -373,7 +389,7 @@ public function isLibera()
|
||||||
public function hasAutorizzazioniScadute()
|
public function hasAutorizzazioniScadute()
|
||||||
{
|
{
|
||||||
return $this->data_scadenza_autorizzazioni &&
|
return $this->data_scadenza_autorizzazioni &&
|
||||||
$this->data_scadenza_autorizzazioni->isPast();
|
$this->data_scadenza_autorizzazioni->isPast();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
67
app/Models/UnitaRecapitoServizio.php
Normal file
67
app/Models/UnitaRecapitoServizio.php
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
<?php
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class UnitaRecapitoServizio extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'unita_recapiti_servizio';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'unita_id',
|
||||||
|
'persona_id',
|
||||||
|
'tipo_servizio',
|
||||||
|
'email',
|
||||||
|
'etichetta',
|
||||||
|
'note',
|
||||||
|
'ordine',
|
||||||
|
'is_default',
|
||||||
|
'attivo',
|
||||||
|
'sorgente',
|
||||||
|
'last_confirmed_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'ordine' => 'integer',
|
||||||
|
'is_default' => 'boolean',
|
||||||
|
'attivo' => 'boolean',
|
||||||
|
'last_confirmed_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
public static function serviceLabels(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'comunicazioni' => 'Comunicazioni generali',
|
||||||
|
'convocazioni' => 'Convocazioni e verbali',
|
||||||
|
'ticket' => 'Ticket e segnalazioni',
|
||||||
|
'solleciti' => 'Solleciti e scadenze',
|
||||||
|
'contabilita' => 'Contabilita e rate',
|
||||||
|
'amministrazione' => 'Amministrazione',
|
||||||
|
'anagrafe' => 'Anagrafe condominiale',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function unitaImmobiliare(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(UnitaImmobiliare::class, 'unita_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function persona(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Persona::class, 'persona_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeAttivi($query)
|
||||||
|
{
|
||||||
|
return $query->where('attivo', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopePerServizio($query, string $tipoServizio)
|
||||||
|
{
|
||||||
|
return $query->where('tipo_servizio', $tipoServizio);
|
||||||
|
}
|
||||||
|
}
|
||||||
110
app/Services/Comunicazioni/RecapitiServizioResolver.php
Normal file
110
app/Services/Comunicazioni/RecapitiServizioResolver.php
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
<?php
|
||||||
|
namespace App\Services\Comunicazioni;
|
||||||
|
|
||||||
|
use App\Models\Persona;
|
||||||
|
use App\Models\PersonaEmailMultipla;
|
||||||
|
use App\Models\PersonaUnitaRelazione;
|
||||||
|
use App\Models\UnitaImmobiliare;
|
||||||
|
use App\Models\UnitaRecapitoServizio;
|
||||||
|
|
||||||
|
class RecapitiServizioResolver
|
||||||
|
{
|
||||||
|
public function resolveForUnita(UnitaImmobiliare $unita, string $tipoServizio): array
|
||||||
|
{
|
||||||
|
$labels = UnitaRecapitoServizio::serviceLabels();
|
||||||
|
if (! array_key_exists($tipoServizio, $labels)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$unita->loadMissing([
|
||||||
|
'unitaRecapitiServizio.persona',
|
||||||
|
'relazioniPersoneAttive.persona.emailMultiple',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$resolved = [];
|
||||||
|
|
||||||
|
foreach ($unita->unitaRecapitiServizio as $row) {
|
||||||
|
if (! $row->attivo || $row->tipo_servizio !== $tipoServizio) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolved[$this->normalizeEmailKey($row->email)] = [
|
||||||
|
'email' => mb_strtolower(trim((string) $row->email)),
|
||||||
|
'source' => 'override_unita',
|
||||||
|
'label' => $row->etichetta ?: $labels[$tipoServizio],
|
||||||
|
'persona' => $row->persona?->nome_completo,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($resolved !== []) {
|
||||||
|
return array_values($resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($unita->relazioniPersoneAttive as $relazione) {
|
||||||
|
if (! $this->relationAllowsService($relazione, $tipoServizio)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$persona = $relazione->persona;
|
||||||
|
if (! $persona instanceof Persona) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->emailsForPersona($persona) as $email) {
|
||||||
|
$key = $this->normalizeEmailKey($email);
|
||||||
|
if ($key === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolved[$key] = [
|
||||||
|
'email' => $key,
|
||||||
|
'source' => 'persona',
|
||||||
|
'label' => $labels[$tipoServizio],
|
||||||
|
'persona' => $persona->nome_completo,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values($resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function relationAllowsService(PersonaUnitaRelazione $relazione, string $tipoServizio): bool
|
||||||
|
{
|
||||||
|
return match ($tipoServizio) {
|
||||||
|
'convocazioni' => (bool) $relazione->riceve_convocazioni,
|
||||||
|
'solleciti', 'contabilita', 'comunicazioni', 'ticket', 'amministrazione', 'anagrafe' => (bool) $relazione->riceve_comunicazioni,
|
||||||
|
default => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function emailsForPersona(Persona $persona): array
|
||||||
|
{
|
||||||
|
$emails = [];
|
||||||
|
|
||||||
|
$primary = mb_strtolower(trim((string) $persona->email_principale));
|
||||||
|
if ($primary !== '') {
|
||||||
|
$emails[] = $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($persona->emailMultiple as $row) {
|
||||||
|
if (! $row instanceof PersonaEmailMultipla || ! $row->attiva) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$email = mb_strtolower(trim((string) $row->email));
|
||||||
|
if ($email !== '') {
|
||||||
|
$emails[] = $email;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($emails));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeEmailKey(?string $email): string
|
||||||
|
{
|
||||||
|
return mb_strtolower(trim((string) $email));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Services\GesconImport;
|
namespace App\Services\GesconImport;
|
||||||
|
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
class StabileEnrichmentService
|
class StabileEnrichmentService
|
||||||
{
|
{
|
||||||
|
|
@ -15,53 +14,69 @@ class StabileEnrichmentService
|
||||||
public function enrich(string $legacyCode, string $basePath = '/mnt/gescon-archives/gescon'): array
|
public function enrich(string $legacyCode, string $basePath = '/mnt/gescon-archives/gescon'): array
|
||||||
{
|
{
|
||||||
$result = ['legacy' => $legacyCode, 'catasto' => false, 'banca_inline' => false, 'mdb_found' => false];
|
$result = ['legacy' => $legacyCode, 'catasto' => false, 'banca_inline' => false, 'mdb_found' => false];
|
||||||
if (!$legacyCode) return $result;
|
if (! $legacyCode) {
|
||||||
if (!Schema::hasTable('stabili')) return $result;
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::hasTable('stabili')) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
// Trova record stabile di dominio
|
// Trova record stabile di dominio
|
||||||
$stabile = DB::table('stabili')->where(function ($q) use ($legacyCode) {
|
$stabile = DB::table('stabili')->where(function ($q) use ($legacyCode) {
|
||||||
if (Schema::hasColumn('stabili', 'codice_stabile')) $q->orWhere('codice_stabile', $legacyCode);
|
if (Schema::hasColumn('stabili', 'codice_stabile')) {
|
||||||
|
$q->orWhere('codice_stabile', $legacyCode);
|
||||||
|
}
|
||||||
|
|
||||||
$q->orWhere('denominazione', $legacyCode); // fallback
|
$q->orWhere('denominazione', $legacyCode); // fallback
|
||||||
})->first();
|
})->first();
|
||||||
if (!$stabile) return $result;
|
if (! $stabile) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mdbPath = $this->resolveMdbPath($basePath);
|
||||||
|
if (! is_file($mdbPath) || ! is_readable($mdbPath)) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
$mdbPath = rtrim($basePath, '/') . '/Stabili.mdb';
|
|
||||||
if (!is_file($mdbPath) || !is_readable($mdbPath)) return $result;
|
|
||||||
$result['mdb_found'] = true;
|
$result['mdb_found'] = true;
|
||||||
|
|
||||||
$row = $this->extractRow($mdbPath, $legacyCode);
|
$row = $this->extractRow($mdbPath, $legacyCode);
|
||||||
if (!$row) return $result;
|
if (! $row) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
$updates = [];
|
$updates = [];
|
||||||
// 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', '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'],
|
||||||
'classe' => ['classe', 'cat_classe'],
|
'classe' => ['classe', 'cat_classe'],
|
||||||
'consistenza' => ['consistenza', 'cat_consistenza'],
|
'consistenza' => ['consistenza', 'cat_consistenza'],
|
||||||
'rendita_catastale' => ['rendita_catastale', 'rendita', 'cat_rendita'],
|
'rendita_catastale' => ['rendita_catastale', 'rendita', 'cat_rendita'],
|
||||||
'superficie_catastale' => ['superficie_catastale', 'superficie'],
|
'superficie_catastale' => ['superficie_catastale', 'superficie'],
|
||||||
];
|
];
|
||||||
$targetColumnAliases = [
|
$targetColumnAliases = [
|
||||||
'foglio' => ['foglio', 'foglio_catasto'],
|
'foglio' => ['foglio', 'foglio_catasto'],
|
||||||
'particella' => ['particella', 'particella_catasto', 'mappale'],
|
'particella' => ['particella', 'particella_catasto', 'mappale'],
|
||||||
'subalterno' => ['subalterno'],
|
'subalterno' => ['subalterno'],
|
||||||
'sezione' => ['sezione'],
|
'sezione' => ['sezione'],
|
||||||
'categoria' => ['categoria_catastale', 'categoria'],
|
'categoria' => ['categoria_catastale', 'categoria'],
|
||||||
'classe' => ['classe', 'classe_catastale'],
|
'classe' => ['classe', 'classe_catastale'],
|
||||||
'consistenza' => ['consistenza', 'consistenza_catastale'],
|
'consistenza' => ['consistenza', 'consistenza_catastale'],
|
||||||
'rendita_catastale' => ['rendita_catastale'],
|
'rendita_catastale' => ['rendita_catastale'],
|
||||||
'superficie_catastale' => ['superficie_catastale'],
|
'superficie_catastale' => ['superficie_catastale'],
|
||||||
];
|
];
|
||||||
foreach ($mapCatasto as $logical => $candidates) {
|
foreach ($mapCatasto as $logical => $candidates) {
|
||||||
$column = $this->resolveStabileColumn($targetColumnAliases[$logical] ?? [$logical]);
|
$column = $this->resolveStabileColumn($targetColumnAliases[$logical] ?? [$logical]);
|
||||||
if (!$column) {
|
if (! $column) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$current = $stabile->$column ?? null;
|
$current = $stabile->$column ?? null;
|
||||||
if (!empty($current) && $current !== 'ND') {
|
if (! empty($current) && $current !== 'ND') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
foreach ($candidates as $cand) {
|
foreach ($candidates as $cand) {
|
||||||
|
|
@ -73,7 +88,7 @@ public function enrich(string $legacyCode, string $basePath = '/mnt/gescon-archi
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!empty(array_intersect(array_keys($updates), array_filter(array_map(function ($aliases) {
|
if (! empty(array_intersect(array_keys($updates), array_filter(array_map(function ($aliases) {
|
||||||
foreach ($aliases as $alias) {
|
foreach ($aliases as $alias) {
|
||||||
if (Schema::hasColumn('stabili', $alias)) {
|
if (Schema::hasColumn('stabili', $alias)) {
|
||||||
return $alias;
|
return $alias;
|
||||||
|
|
@ -90,7 +105,7 @@ public function enrich(string $legacyCode, string $basePath = '/mnt/gescon-archi
|
||||||
$cc = $row['cod_comune'] ?? ($row['codice_catastale'] ?? ($row['cod_catastale'] ?? null));
|
$cc = $row['cod_comune'] ?? ($row['codice_catastale'] ?? ($row['cod_catastale'] ?? null));
|
||||||
if ($cc && is_string($cc) && trim($cc) !== '') {
|
if ($cc && is_string($cc) && trim($cc) !== '') {
|
||||||
$updates[$codiceComuneColumn] = strtoupper(trim($cc));
|
$updates[$codiceComuneColumn] = strtoupper(trim($cc));
|
||||||
$result['catasto'] = true;
|
$result['catasto'] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,14 +115,14 @@ public function enrich(string $legacyCode, string $basePath = '/mnt/gescon-archi
|
||||||
$currentIban = $stabile->$ibanColumn ?? null;
|
$currentIban = $stabile->$ibanColumn ?? null;
|
||||||
if (empty($currentIban)) {
|
if (empty($currentIban)) {
|
||||||
$ibanKey = $this->firstAvailable($row, ['iban_condominio', 'iban', 'iban1', 'iban_1']);
|
$ibanKey = $this->firstAvailable($row, ['iban_condominio', 'iban', 'iban1', 'iban_1']);
|
||||||
if ($ibanKey && !empty($row[$ibanKey])) {
|
if ($ibanKey && ! empty($row[$ibanKey])) {
|
||||||
$updates[$ibanColumn] = trim($row[$ibanKey]);
|
$updates[$ibanColumn] = trim($row[$ibanKey]);
|
||||||
$result['banca_inline'] = true;
|
$result['banca_inline'] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($updates)) {
|
if (! empty($updates)) {
|
||||||
$updates['updated_at'] = now();
|
$updates['updated_at'] = now();
|
||||||
DB::table('stabili')->where('id', $stabile->id)->update($updates);
|
DB::table('stabili')->where('id', $stabile->id)->update($updates);
|
||||||
}
|
}
|
||||||
|
|
@ -117,13 +132,19 @@ public function enrich(string $legacyCode, string $basePath = '/mnt/gescon-archi
|
||||||
private function extractRow(string $mdbPath, string $legacyCode): ?array
|
private function extractRow(string $mdbPath, string $legacyCode): ?array
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$binTables = trim((string)@shell_exec('command -v mdb-tables')) ?: '/usr/bin/mdb-tables';
|
$binTables = trim((string) @shell_exec('command -v mdb-tables')) ?: '/usr/bin/mdb-tables';
|
||||||
$binExport = trim((string)@shell_exec('command -v mdb-export')) ?: '/usr/bin/mdb-export';
|
$binExport = trim((string) @shell_exec('command -v mdb-export')) ?: '/usr/bin/mdb-export';
|
||||||
if (!is_file($mdbPath) || !is_readable($mdbPath)) return null;
|
if (! is_file($mdbPath) || ! is_readable($mdbPath)) {
|
||||||
$tables = @shell_exec(sprintf('%s -1 %s 2>/dev/null', escapeshellarg($binTables), escapeshellarg($mdbPath))) ?: '';
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tables = @shell_exec(sprintf('%s -1 %s 2>/dev/null', escapeshellarg($binTables), escapeshellarg($mdbPath))) ?: '';
|
||||||
$candidateTables = [];
|
$candidateTables = [];
|
||||||
foreach (['Stabili', 'stabili', 'Condomin', 'condomin'] as $t) {
|
foreach (['Stabili', 'stabili', 'Condomin', 'condomin'] as $t) {
|
||||||
if (preg_match('/(^|\n)' . preg_quote($t, '/') . '(\n|$)/', $tables)) $candidateTables[] = $t;
|
if (preg_match('/(^|\n)' . preg_quote($t, '/') . '(\n|$)/', $tables)) {
|
||||||
|
$candidateTables[] = $t;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
$candidateTables = array_unique($candidateTables);
|
$candidateTables = array_unique($candidateTables);
|
||||||
foreach ($candidateTables as $table) {
|
foreach ($candidateTables as $table) {
|
||||||
|
|
@ -139,27 +160,33 @@ private function extractRow(string $mdbPath, string $legacyCode): ?array
|
||||||
escapeshellarg($tmp)
|
escapeshellarg($tmp)
|
||||||
);
|
);
|
||||||
@shell_exec($cmd);
|
@shell_exec($cmd);
|
||||||
if (!is_file($tmp) || filesize($tmp) === 0) {
|
if (! is_file($tmp) || filesize($tmp) === 0) {
|
||||||
@unlink($tmp);
|
@unlink($tmp);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$fh = fopen($tmp, 'r');
|
$fh = fopen($tmp, 'r');
|
||||||
if (!$fh) {
|
if (! $fh) {
|
||||||
@unlink($tmp);
|
@unlink($tmp);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$headers = fgetcsv($fh, 0, '|');
|
$headers = fgetcsv($fh, 0, '|');
|
||||||
if (!$headers) {
|
if (! $headers) {
|
||||||
fclose($fh);
|
fclose($fh);
|
||||||
@unlink($tmp);
|
@unlink($tmp);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$headers = array_map(fn($h) => strtolower(trim((string)$h)), $headers);
|
$headers = array_map(fn($h) => strtolower(trim((string) $h)), $headers);
|
||||||
while (($cols = fgetcsv($fh, 0, '|')) !== false) {
|
while (($cols = fgetcsv($fh, 0, '|')) !== false) {
|
||||||
if (count($cols) !== count($headers)) continue;
|
if (count($cols) !== count($headers)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$row = @array_combine($headers, $cols) ?: [];
|
$row = @array_combine($headers, $cols) ?: [];
|
||||||
if (!$row) continue;
|
if (! $row) {
|
||||||
$codeFields = ['cod_stabile', 'codice', 'codice_stabile', 'stabile', 'cod'];
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$codeFields = ['cod_stabile', 'codice', 'codice_stabile', 'stabile', 'cod', 'nome_directory', 'codice_directory', 'internet_cod_stab'];
|
||||||
foreach ($codeFields as $cf) {
|
foreach ($codeFields as $cf) {
|
||||||
if (isset($row[$cf]) && trim($row[$cf]) === $legacyCode) {
|
if (isset($row[$cf]) && trim($row[$cf]) === $legacyCode) {
|
||||||
fclose($fh);
|
fclose($fh);
|
||||||
|
|
@ -180,7 +207,10 @@ private function extractRow(string $mdbPath, string $legacyCode): ?array
|
||||||
private function firstAvailable(array $row, array $keys): ?string
|
private function firstAvailable(array $row, array $keys): ?string
|
||||||
{
|
{
|
||||||
foreach ($keys as $k) {
|
foreach ($keys as $k) {
|
||||||
if (isset($row[$k]) && trim((string)$row[$k]) !== '') return $k;
|
if (isset($row[$k]) && trim((string) $row[$k]) !== '') {
|
||||||
|
return $k;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -194,4 +224,23 @@ private function resolveStabileColumn(array $candidates): ?string
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function resolveMdbPath(string $basePath): string
|
||||||
|
{
|
||||||
|
$basePath = rtrim($basePath, '/');
|
||||||
|
$candidates = [
|
||||||
|
$basePath . '/dbc/Stabili.mdb',
|
||||||
|
$basePath . '/dbc/stabili.mdb',
|
||||||
|
$basePath . '/Stabili.mdb',
|
||||||
|
$basePath . '/stabili.mdb',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($candidates as $candidate) {
|
||||||
|
if (is_file($candidate) && is_readable($candidate)) {
|
||||||
|
return $candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $candidates[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
61
app/Services/Stabili/StabileTransferService.php
Normal file
61
app/Services/Stabili/StabileTransferService.php
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Stabili;
|
||||||
|
|
||||||
|
use App\Models\Amministratore;
|
||||||
|
use App\Models\Stabile;
|
||||||
|
use App\Models\StabileAmministratoreTransfer;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class StabileTransferService
|
||||||
|
{
|
||||||
|
public function transfer(
|
||||||
|
Stabile $stabile,
|
||||||
|
Amministratore $destination,
|
||||||
|
?User $actor = null,
|
||||||
|
bool $alsoRubrica = true,
|
||||||
|
?string $reason = null,
|
||||||
|
string $source = 'portal-superadmin',
|
||||||
|
?string $ipAddress = null,
|
||||||
|
array $meta = []
|
||||||
|
): StabileAmministratoreTransfer {
|
||||||
|
$fromId = (int) ($stabile->amministratore_id ?? 0);
|
||||||
|
$toId = (int) $destination->id;
|
||||||
|
|
||||||
|
if ($fromId === $toId) {
|
||||||
|
throw new \InvalidArgumentException('Lo stabile e` gia` assegnato a questo amministratore.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($stabile, $destination, $actor, $alsoRubrica, $reason, $source, $ipAddress, $meta, $fromId, $toId): StabileAmministratoreTransfer {
|
||||||
|
$stabile->amministratore_id = $toId;
|
||||||
|
$stabile->save();
|
||||||
|
|
||||||
|
if ($alsoRubrica && (int) ($stabile->rubrica_id ?? 0) > 0) {
|
||||||
|
if (Schema::hasTable('rubrica_universale') && Schema::hasColumn('rubrica_universale', 'amministratore_id')) {
|
||||||
|
DB::table('rubrica_universale')
|
||||||
|
->where('id', (int) $stabile->rubrica_id)
|
||||||
|
->update(['amministratore_id' => $toId, 'updated_at' => now()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method_exists($destination, 'provisionArchiveIfMissing')) {
|
||||||
|
$destination->provisionArchiveIfMissing();
|
||||||
|
}
|
||||||
|
|
||||||
|
return StabileAmministratoreTransfer::query()->create([
|
||||||
|
'stabile_id' => (int) $stabile->id,
|
||||||
|
'from_amministratore_id' => $fromId > 0 ? $fromId : null,
|
||||||
|
'to_amministratore_id' => $toId,
|
||||||
|
'changed_by_user_id' => $actor?->id,
|
||||||
|
'changed_by_name' => $actor?->name ?: 'Sistema',
|
||||||
|
'source' => $source,
|
||||||
|
'reason' => $reason !== null ? trim($reason) : null,
|
||||||
|
'also_rubrica' => $alsoRubrica,
|
||||||
|
'ip_address' => $ipAddress,
|
||||||
|
'meta' => $meta,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,6 +24,12 @@
|
||||||
// Timeout client per check/download.
|
// Timeout client per check/download.
|
||||||
'http_timeout_seconds' => (int) env('NETGESCON_UPDATE_HTTP_TIMEOUT', 60),
|
'http_timeout_seconds' => (int) env('NETGESCON_UPDATE_HTTP_TIMEOUT', 60),
|
||||||
|
|
||||||
|
// Backup pre-update: abilita upload su Google Drive solo se richiesto dal nodo.
|
||||||
|
'preupdate_drive_enabled' => (bool) env('NETGESCON_PREUPDATE_DRIVE_ENABLED', false),
|
||||||
|
|
||||||
|
// Se true, l'update fallisce quando la copia Drive non puo essere completata.
|
||||||
|
'preupdate_require_drive' => (bool) env('NETGESCON_PREUPDATE_REQUIRE_DRIVE', false),
|
||||||
|
|
||||||
// Override opzionale DNS per richieste update (formato CSV host:port:ip).
|
// Override opzionale DNS per richieste update (formato CSV host:port:ip).
|
||||||
// Esempio: updates.netgescon.it:443:192.168.0.53
|
// Esempio: updates.netgescon.it:443:192.168.0.53
|
||||||
'http_resolve' => env('NETGESCON_UPDATE_RESOLVE', ''),
|
'http_resolve' => env('NETGESCON_UPDATE_RESOLVE', ''),
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
return [
|
return [
|
||||||
'version' => env('NETGESCON_VERSION', '0.8.1'),
|
'version' => env('NETGESCON_VERSION', '0.8.1'),
|
||||||
'ui' => [
|
'ui' => [
|
||||||
'force_filament' => env('NETGESCON_FORCE_FILAMENT', false),
|
'force_filament' => env('NETGESCON_FORCE_FILAMENT', true),
|
||||||
],
|
],
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?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
|
||||||
|
{
|
||||||
|
if (Schema::hasTable('unita_recapiti_servizio')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema::create('unita_recapiti_servizio', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('unita_id')->constrained('unita_immobiliari')->cascadeOnDelete();
|
||||||
|
$table->foreignId('persona_id')->nullable()->constrained('persone')->nullOnDelete();
|
||||||
|
$table->string('tipo_servizio', 50);
|
||||||
|
$table->string('email', 255);
|
||||||
|
$table->string('etichetta', 120)->nullable();
|
||||||
|
$table->text('note')->nullable();
|
||||||
|
$table->unsignedInteger('ordine')->default(1);
|
||||||
|
$table->boolean('is_default')->default(true);
|
||||||
|
$table->boolean('attivo')->default(true);
|
||||||
|
$table->string('sorgente', 50)->default('manuale');
|
||||||
|
$table->timestamp('last_confirmed_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index(['unita_id', 'tipo_servizio']);
|
||||||
|
$table->index(['persona_id', 'tipo_servizio']);
|
||||||
|
$table->index(['attivo', 'tipo_servizio']);
|
||||||
|
$table->unique(['unita_id', 'persona_id', 'tipo_servizio', 'email'], 'unita_recapito_servizio_unico');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('unita_recapiti_servizio');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?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
|
||||||
|
{
|
||||||
|
if (Schema::hasTable('adempimenti_fiscali')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema::create('adempimenti_fiscali', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('tenant_id', 50)->nullable()->index();
|
||||||
|
$table->foreignId('stabile_id')->constrained('stabili')->cascadeOnDelete();
|
||||||
|
$table->foreignId('gestione_id')->nullable()->constrained('gestioni_contabili')->nullOnDelete();
|
||||||
|
$table->string('tipo', 50)->index();
|
||||||
|
$table->integer('anno_fiscale')->nullable()->index();
|
||||||
|
$table->string('titolo', 255);
|
||||||
|
$table->text('descrizione')->nullable();
|
||||||
|
$table->string('stato', 30)->default('bozza')->index();
|
||||||
|
$table->date('scadenza')->nullable()->index();
|
||||||
|
$table->date('data_completamento')->nullable();
|
||||||
|
$table->string('periodicita', 30)->nullable();
|
||||||
|
$table->json('metadati')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index(['stabile_id', 'anno_fiscale', 'tipo'], 'adempimenti_fiscali_stabile_anno_tipo_idx');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('adempimenti_fiscali');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?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
|
||||||
|
{
|
||||||
|
if (Schema::hasTable('stabile_amministratore_transfers')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema::create('stabile_amministratore_transfers', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('stabile_id')->constrained('stabili')->cascadeOnDelete();
|
||||||
|
$table->foreignId('from_amministratore_id')->nullable()->constrained('amministratori')->nullOnDelete();
|
||||||
|
$table->foreignId('to_amministratore_id')->constrained('amministratori')->cascadeOnDelete();
|
||||||
|
$table->foreignId('changed_by_user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||||
|
$table->string('changed_by_name', 150)->nullable();
|
||||||
|
$table->string('source', 50)->default('portal-superadmin');
|
||||||
|
$table->text('reason')->nullable();
|
||||||
|
$table->boolean('also_rubrica')->default(true);
|
||||||
|
$table->string('ip_address', 45)->nullable();
|
||||||
|
$table->json('meta')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index(['stabile_id', 'created_at'], 'stabile_transfer_stabile_created_idx');
|
||||||
|
$table->index(['to_amministratore_id', 'created_at'], 'stabile_transfer_dest_created_idx');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('stabile_amministratore_transfers');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
<?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
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('insurance_policies')) {
|
||||||
|
Schema::create('insurance_policies', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('stabile_id')->constrained('stabili')->cascadeOnDelete();
|
||||||
|
$table->foreignId('broker_rubrica_id')->nullable()->constrained('rubrica_universale')->nullOnDelete();
|
||||||
|
$table->foreignId('company_rubrica_id')->nullable()->constrained('rubrica_universale')->nullOnDelete();
|
||||||
|
$table->string('policy_name')->nullable();
|
||||||
|
$table->string('policy_number')->nullable();
|
||||||
|
$table->string('policy_reference')->nullable();
|
||||||
|
$table->string('status', 50)->default('attiva');
|
||||||
|
$table->date('started_at')->nullable();
|
||||||
|
$table->date('expires_at')->nullable();
|
||||||
|
$table->date('renewal_at')->nullable();
|
||||||
|
$table->decimal('annual_amount', 12, 2)->nullable();
|
||||||
|
$table->string('payment_split')->nullable();
|
||||||
|
$table->text('payment_schedule_notes')->nullable();
|
||||||
|
$table->text('coverage_summary')->nullable();
|
||||||
|
$table->longText('special_conditions')->nullable();
|
||||||
|
$table->longText('claim_form_template')->nullable();
|
||||||
|
$table->string('policy_document_path')->nullable();
|
||||||
|
$table->string('policy_document_name')->nullable();
|
||||||
|
$table->string('policy_document_mime')->nullable();
|
||||||
|
$table->string('signature_image_path')->nullable();
|
||||||
|
$table->text('notes')->nullable();
|
||||||
|
$table->json('metadata')->nullable();
|
||||||
|
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index(['stabile_id', 'status']);
|
||||||
|
$table->index(['policy_number']);
|
||||||
|
$table->index(['expires_at']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Schema::hasTable('insurance_claims') && ! Schema::hasColumn('insurance_claims', 'insurance_policy_id')) {
|
||||||
|
Schema::table('insurance_claims', function (Blueprint $table): void {
|
||||||
|
$table->foreignId('insurance_policy_id')
|
||||||
|
->nullable()
|
||||||
|
->after('stabile_id')
|
||||||
|
->constrained('insurance_policies')
|
||||||
|
->nullOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if (Schema::hasTable('insurance_claims') && Schema::hasColumn('insurance_claims', 'insurance_policy_id')) {
|
||||||
|
Schema::table('insurance_claims', function (Blueprint $table): void {
|
||||||
|
$table->dropConstrainedForeignId('insurance_policy_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema::dropIfExists('insurance_policies');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
<?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
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('stabile_servizio_letture')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema::table('stabile_servizio_letture', function (Blueprint $table): void {
|
||||||
|
if (! Schema::hasColumn('stabile_servizio_letture', 'deadline_lettura_at')) {
|
||||||
|
$table->dateTime('deadline_lettura_at')->nullable()->after('prossima_lettura_scadenza_at');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::hasColumn('stabile_servizio_letture', 'sollecito_inviato_at')) {
|
||||||
|
$table->dateTime('sollecito_inviato_at')->nullable()->after('deadline_lettura_at');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::hasColumn('stabile_servizio_letture', 'rilevatore_tipo')) {
|
||||||
|
$table->string('rilevatore_tipo', 40)->nullable()->after('sollecito_inviato_at');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::hasColumn('stabile_servizio_letture', 'rilevatore_nome')) {
|
||||||
|
$table->string('rilevatore_nome')->nullable()->after('rilevatore_tipo');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::hasColumn('stabile_servizio_letture', 'lettura_precedente_valore')) {
|
||||||
|
$table->decimal('lettura_precedente_valore', 12, 3)->nullable()->after('lettura_inizio');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::hasColumn('stabile_servizio_letture', 'lettura_foto_path')) {
|
||||||
|
$table->string('lettura_foto_path')->nullable()->after('lettura_fine');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::hasColumn('stabile_servizio_letture', 'lettura_foto_original_name')) {
|
||||||
|
$table->string('lettura_foto_original_name')->nullable()->after('lettura_foto_path');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::hasColumn('stabile_servizio_letture', 'lettura_foto_metadata')) {
|
||||||
|
$table->json('lettura_foto_metadata')->nullable()->after('lettura_foto_original_name');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::hasColumn('stabile_servizio_letture', 'lettura_precedente_foto_path')) {
|
||||||
|
$table->string('lettura_precedente_foto_path')->nullable()->after('lettura_foto_metadata');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::hasColumn('stabile_servizio_letture', 'lettura_ocr_valore')) {
|
||||||
|
$table->string('lettura_ocr_valore', 64)->nullable()->after('lettura_precedente_foto_path');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::hasColumn('stabile_servizio_letture', 'lettura_ocr_confidenza')) {
|
||||||
|
$table->decimal('lettura_ocr_confidenza', 5, 2)->nullable()->after('lettura_ocr_valore');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('stabile_servizio_letture')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema::table('stabile_servizio_letture', function (Blueprint $table): void {
|
||||||
|
foreach ([
|
||||||
|
'lettura_ocr_confidenza',
|
||||||
|
'lettura_ocr_valore',
|
||||||
|
'lettura_precedente_foto_path',
|
||||||
|
'lettura_foto_metadata',
|
||||||
|
'lettura_foto_original_name',
|
||||||
|
'lettura_foto_path',
|
||||||
|
'lettura_precedente_valore',
|
||||||
|
'rilevatore_nome',
|
||||||
|
'rilevatore_tipo',
|
||||||
|
'sollecito_inviato_at',
|
||||||
|
'deadline_lettura_at',
|
||||||
|
] as $column) {
|
||||||
|
if (Schema::hasColumn('stabile_servizio_letture', $column)) {
|
||||||
|
$table->dropColumn($column);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
# 🤖 NETGESCON - GUIDA PER GITHUB COPILOT/AI
|
# 🤖 NETGESCON - GUIDA PER GITHUB COPILOT/AI
|
||||||
|
|
||||||
## 📋 Istruzioni Complete per Continuare lo Sviluppo
|
## 📋 Istruzioni Complete per Continuare lo Sviluppo
|
||||||
|
|
||||||
> **🎯 DOCUMENTO MASTER** per GitHub Copilot/AI
|
> **🎯 DOCUMENTO MASTER** per GitHub Copilot/AI
|
||||||
|
|
@ -7,19 +8,38 @@ ## 📋 Istruzioni Complete per Continuare lo Sviluppo
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Stato Documento
|
||||||
|
|
||||||
|
Questo documento contiene materiale storico utile come contesto, ma non definisce piu il workspace attivo.
|
||||||
|
|
||||||
|
Regola corrente:
|
||||||
|
|
||||||
|
- workspace attivo: `/home/michele/netgescon/netgescon-day0`
|
||||||
|
- documentazione autorevole: `/home/michele/netgescon/netgescon-day0/docs`
|
||||||
|
- repository Git da usare per commit, push e distribuzione: `netgescon-day0`
|
||||||
|
- `netgescon-laravel` resta archivio tecnico e non va usato come base operativa corrente
|
||||||
|
|
||||||
|
Per le regole aggiornate prevale sempre [docs/DOCS-AND-WORKSPACE-POLICY-DAY0.md](/home/michele/netgescon/netgescon-day0/docs/DOCS-AND-WORKSPACE-POLICY-DAY0.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🚨 **ATTENZIONE: PRIMA DI INIZIARE**
|
## 🚨 **ATTENZIONE: PRIMA DI INIZIARE**
|
||||||
|
|
||||||
### 📍 **Percorso di lavoro attuale (DEV)**
|
### 📍 **Percorso di lavoro attuale (DEV)**
|
||||||
- Usare **/home/michele/netgescon/netgescon-laravel** per tutti i comandi (composer, artisan, import).
|
|
||||||
- Non usare **/var/www/netgescon**: è l'albero produzione ed è stato escluso dal workspace finché non validiamo le importazioni.
|
- Usare **/home/michele/netgescon/netgescon-day0** per tutti i comandi (composer, artisan, import, test, git).
|
||||||
|
- Non usare **netgescon-laravel** come base di sviluppo corrente: resta archivio storico.
|
||||||
|
- Non usare **/var/www/netgescon** come albero di sviluppo.
|
||||||
|
|
||||||
### ⚠️ **REGOLA D'ORO: NON PERDERE I DATI**
|
### ⚠️ **REGOLA D'ORO: NON PERDERE I DATI**
|
||||||
|
|
||||||
1. **MAI** modificare direttamente il database di produzione
|
1. **MAI** modificare direttamente il database di produzione
|
||||||
2. **SEMPRE** fare backup prima di modifiche strutturali
|
2. **SEMPRE** fare backup prima di modifiche strutturali
|
||||||
3. **TESTARE** ogni modifica su ambiente di sviluppo
|
3. **TESTARE** ogni modifica su ambiente di sviluppo
|
||||||
4. **VERIFICARE** autenticazione e permessi dopo ogni modifica
|
4. **VERIFICARE** autenticazione e permessi dopo ogni modifica
|
||||||
|
|
||||||
### 🔑 **CREDENZIALI E ACCESSI ATTUALI**
|
### 🔑 **CREDENZIALI E ACCESSI ATTUALI**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Login applicazione
|
# Login applicazione
|
||||||
URL: http://192.168.0.200:8000
|
URL: http://192.168.0.200:8000
|
||||||
|
|
@ -42,6 +62,7 @@ # Database
|
||||||
## 📂 **STRUTTURA PROGETTO - MAPPA COMPLETA**
|
## 📂 **STRUTTURA PROGETTO - MAPPA COMPLETA**
|
||||||
|
|
||||||
### 🏠 **DIRECTORY PRINCIPALE**
|
### 🏠 **DIRECTORY PRINCIPALE**
|
||||||
|
|
||||||
```
|
```
|
||||||
~/netgescon/
|
~/netgescon/
|
||||||
├── netgescon-laravel/ # 🌐 APPLICAZIONE PRINCIPALE LARAVEL
|
├── netgescon-laravel/ # 🌐 APPLICAZIONE PRINCIPALE LARAVEL
|
||||||
|
|
@ -65,12 +86,15 @@ ### 🏠 **DIRECTORY PRINCIPALE**
|
||||||
### 🎯 **ENTRY POINT - DA DOVE INIZIARE**
|
### 🎯 **ENTRY POINT - DA DOVE INIZIARE**
|
||||||
|
|
||||||
#### 1️⃣ **DOCUMENTO PRINCIPALE**
|
#### 1️⃣ **DOCUMENTO PRINCIPALE**
|
||||||
|
|
||||||
**📍 INIZIO QUI:** [`docs/00-INDICE-DOCS-UNIFICATA.md`](00-INDICE-DOCS-UNIFICATA.md)
|
**📍 INIZIO QUI:** [`docs/00-INDICE-DOCS-UNIFICATA.md`](00-INDICE-DOCS-UNIFICATA.md)
|
||||||
|
|
||||||
- Contiene **navigazione completa** di tutta la documentazione
|
- Contiene **navigazione completa** di tutta la documentazione
|
||||||
- Link diretti a tutti i documenti chiave
|
- Link diretti a tutti i documenti chiave
|
||||||
- Scenari di navigazione per ogni situazione
|
- Scenari di navigazione per ogni situazione
|
||||||
|
|
||||||
#### 2️⃣ **DOCUMENTI CHIAVE FONDAMENTALI**
|
#### 2️⃣ **DOCUMENTI CHIAVE FONDAMENTALI**
|
||||||
|
|
||||||
1. **[`docs/00-transizione-linux/README-TRANSITION-COMPLETE.md`](00-transizione-linux/README-TRANSITION-COMPLETE.md)**
|
1. **[`docs/00-transizione-linux/README-TRANSITION-COMPLETE.md`](00-transizione-linux/README-TRANSITION-COMPLETE.md)**
|
||||||
- 🔑 **GUIDA TRANSIZIONE COMPLETA**
|
- 🔑 **GUIDA TRANSIZIONE COMPLETA**
|
||||||
- Struttura progetto, configurazione, troubleshooting
|
- Struttura progetto, configurazione, troubleshooting
|
||||||
|
|
@ -95,6 +119,7 @@ #### 2️⃣ **DOCUMENTI CHIAVE FONDAMENTALI**
|
||||||
## 🔧 **WORKFLOW SVILUPPO SICURO**
|
## 🔧 **WORKFLOW SVILUPPO SICURO**
|
||||||
|
|
||||||
### ✅ **PROCEDURA STANDARD**
|
### ✅ **PROCEDURA STANDARD**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Backup prima di qualsiasi modifica
|
# 1. Backup prima di qualsiasi modifica
|
||||||
cd ~/netgescon/netgescon-laravel
|
cd ~/netgescon/netgescon-laravel
|
||||||
|
|
@ -116,6 +141,7 @@ # 5. Deploy step-by-step
|
||||||
```
|
```
|
||||||
|
|
||||||
### ⚠️ **COSA NON FARE MAI**
|
### ⚠️ **COSA NON FARE MAI**
|
||||||
|
|
||||||
- ❌ `php artisan migrate:fresh` in produzione
|
- ❌ `php artisan migrate:fresh` in produzione
|
||||||
- ❌ Modificare .env senza backup
|
- ❌ Modificare .env senza backup
|
||||||
- ❌ Cancellare migrations esistenti
|
- ❌ Cancellare migrations esistenti
|
||||||
|
|
@ -127,6 +153,7 @@ ### ⚠️ **COSA NON FARE MAI**
|
||||||
## 🎯 **TASK PRIORITARI - COSA FARE DOPO**
|
## 🎯 **TASK PRIORITARI - COSA FARE DOPO**
|
||||||
|
|
||||||
### 🚀 **IMMEDIATE (Prossimi giorni)**
|
### 🚀 **IMMEDIATE (Prossimi giorni)**
|
||||||
|
|
||||||
1. **Completare gestione economica**
|
1. **Completare gestione economica**
|
||||||
- File: `app/Http/Controllers/EconomicaController.php`
|
- File: `app/Http/Controllers/EconomicaController.php`
|
||||||
- Riferimento: [`docs/00-transizione-linux/FEATURES-INVENTORY-COMPLETE.md`](00-transizione-linux/FEATURES-INVENTORY-COMPLETE.md) sezione "GESTIONE ECONOMICA"
|
- Riferimento: [`docs/00-transizione-linux/FEATURES-INVENTORY-COMPLETE.md`](00-transizione-linux/FEATURES-INVENTORY-COMPLETE.md) sezione "GESTIONE ECONOMICA"
|
||||||
|
|
@ -140,6 +167,7 @@ ### 🚀 **IMMEDIATE (Prossimi giorni)**
|
||||||
- Riferimento: [`docs/07-API-INTEGRAZIONI.md`](07-API-INTEGRAZIONI.md)
|
- Riferimento: [`docs/07-API-INTEGRAZIONI.md`](07-API-INTEGRAZIONI.md)
|
||||||
|
|
||||||
### 📋 **BREVE TERMINE (1-2 settimane)**
|
### 📋 **BREVE TERMINE (1-2 settimane)**
|
||||||
|
|
||||||
1. **API REST complete**
|
1. **API REST complete**
|
||||||
- Directory: `routes/api.php`
|
- Directory: `routes/api.php`
|
||||||
- Riferimento: [`docs/02-architettura-laravel/ARCHITETTURA_MODULARE_COMPLETATA.md`](02-architettura-laravel/ARCHITETTURA_MODULARE_COMPLETATA.md)
|
- Riferimento: [`docs/02-architettura-laravel/ARCHITETTURA_MODULARE_COMPLETATA.md`](02-architettura-laravel/ARCHITETTURA_MODULARE_COMPLETATA.md)
|
||||||
|
|
@ -155,15 +183,18 @@ ## 🛠️ **SCRIPT E AUTOMAZIONE DISPONIBILI**
|
||||||
### 📁 **Directory:** `docs/03-scripts-automazione/`
|
### 📁 **Directory:** `docs/03-scripts-automazione/`
|
||||||
|
|
||||||
#### 🔧 **Setup e Manutenzione**
|
#### 🔧 **Setup e Manutenzione**
|
||||||
|
|
||||||
- `setup-complete-environment.sh` - Setup ambiente completo
|
- `setup-complete-environment.sh` - Setup ambiente completo
|
||||||
- `fix-vscode-install.sh` - Fix installazione VS Code
|
- `fix-vscode-install.sh` - Fix installazione VS Code
|
||||||
- `manage-database.sh` - Gestione database (backup/restore)
|
- `manage-database.sh` - Gestione database (backup/restore)
|
||||||
|
|
||||||
#### 🔄 **Sincronizzazione**
|
#### 🔄 **Sincronizzazione**
|
||||||
|
|
||||||
- `sync-bidirectional.sh` - Sync bidirezionale file
|
- `sync-bidirectional.sh` - Sync bidirezionale file
|
||||||
- `sync-to-remote.sh` - Sync verso server remoto
|
- `sync-to-remote.sh` - Sync verso server remoto
|
||||||
|
|
||||||
#### ⚡ **Testing e Deploy**
|
#### ⚡ **Testing e Deploy**
|
||||||
|
|
||||||
- `test-dashboard.sh` - Test dashboard e funzionalità
|
- `test-dashboard.sh` - Test dashboard e funzionalità
|
||||||
- `quick-deploy.sh` - Deploy rapido
|
- `quick-deploy.sh` - Deploy rapido
|
||||||
- `verify-system.sh` - Verifica sistema
|
- `verify-system.sh` - Verifica sistema
|
||||||
|
|
@ -173,6 +204,7 @@ #### ⚡ **Testing e Deploy**
|
||||||
## 🗄️ **COMPRENSIONE DATABASE**
|
## 🗄️ **COMPRENSIONE DATABASE**
|
||||||
|
|
||||||
### 📊 **Tabelle Principali**
|
### 📊 **Tabelle Principali**
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- Riferimento: docs/04-DATABASE-STRUTTURE.md
|
-- Riferimento: docs/04-DATABASE-STRUTTURE.md
|
||||||
stabili # Edifici/condomini
|
stabili # Edifici/condomini
|
||||||
|
|
@ -183,6 +215,7 @@ ### 📊 **Tabelle Principali**
|
||||||
```
|
```
|
||||||
|
|
||||||
### 🔑 **Migrations Critiche**
|
### 🔑 **Migrations Critiche**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Directory: database/migrations/
|
# Directory: database/migrations/
|
||||||
# ATTENZIONE: Non modificare quelle esistenti
|
# ATTENZIONE: Non modificare quelle esistenti
|
||||||
|
|
@ -190,6 +223,7 @@ # Per nuove modifiche: php artisan make:migration nome_modifica
|
||||||
```
|
```
|
||||||
|
|
||||||
### 👥 **Sistema Autenticazione**
|
### 👥 **Sistema Autenticazione**
|
||||||
|
|
||||||
- **Controller:** `app/Http/Controllers/Auth/SecureDashboardController.php`
|
- **Controller:** `app/Http/Controllers/Auth/SecureDashboardController.php`
|
||||||
- **Middleware:** `app/Http/Middleware/`
|
- **Middleware:** `app/Http/Middleware/`
|
||||||
- **Spatie Roles:** Sistema ruoli e permessi
|
- **Spatie Roles:** Sistema ruoli e permessi
|
||||||
|
|
@ -200,16 +234,19 @@ ### 👥 **Sistema Autenticazione**
|
||||||
## 🎨 **SISTEMA INTERFACCIA**
|
## 🎨 **SISTEMA INTERFACCIA**
|
||||||
|
|
||||||
### 🏗️ **Layout Universale**
|
### 🏗️ **Layout Universale**
|
||||||
|
|
||||||
- **File principale:** `resources/views/components/layout/universal.blade.php`
|
- **File principale:** `resources/views/components/layout/universal.blade.php`
|
||||||
- **Sistema sidebar:** `resources/views/components/sidebar-dynamic.blade.php`
|
- **Sistema sidebar:** `resources/views/components/sidebar-dynamic.blade.php`
|
||||||
- **AJAX navigation:** `public/js/dashboard.js`
|
- **AJAX navigation:** `public/js/dashboard.js`
|
||||||
|
|
||||||
### 📱 **Componenti UI**
|
### 📱 **Componenti UI**
|
||||||
|
|
||||||
- **Cards dashboard:** `resources/views/dashboard/`
|
- **Cards dashboard:** `resources/views/dashboard/`
|
||||||
- **Form modali:** `resources/views/forms/`
|
- **Form modali:** `resources/views/forms/`
|
||||||
- **Tabelle data:** `resources/views/tables/`
|
- **Tabelle data:** `resources/views/tables/`
|
||||||
|
|
||||||
### 🎯 **Riferimenti Interfaccia**
|
### 🎯 **Riferimenti Interfaccia**
|
||||||
|
|
||||||
- [`docs/05-INTERFACCIA-UNIVERSALE.md`](05-INTERFACCIA-UNIVERSALE.md) - **Sistema completo**
|
- [`docs/05-INTERFACCIA-UNIVERSALE.md`](05-INTERFACCIA-UNIVERSALE.md) - **Sistema completo**
|
||||||
- [`docs/02-architettura-laravel/sidebar-modulare.md`](02-architettura-laravel/sidebar-modulare.md) - **Sidebar**
|
- [`docs/02-architettura-laravel/sidebar-modulare.md`](02-architettura-laravel/sidebar-modulare.md) - **Sidebar**
|
||||||
- [`docs/08-FRONTEND-UX.md`](08-FRONTEND-UX.md) - **Frontend guidelines**
|
- [`docs/08-FRONTEND-UX.md`](08-FRONTEND-UX.md) - **Frontend guidelines**
|
||||||
|
|
@ -219,6 +256,7 @@ ### 🎯 **Riferimenti Interfaccia**
|
||||||
## 🔍 **TROUBLESHOOTING RAPIDO**
|
## 🔍 **TROUBLESHOOTING RAPIDO**
|
||||||
|
|
||||||
### ❌ **Problema: Dashboard non carica**
|
### ❌ **Problema: Dashboard non carica**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Soluzione rapida
|
# Soluzione rapida
|
||||||
php artisan optimize:clear
|
php artisan optimize:clear
|
||||||
|
|
@ -227,6 +265,7 @@ # Riferimento: docs/01-manuali-aggiuntivi/QUICK_REFERENCE_LARAVEL.md
|
||||||
```
|
```
|
||||||
|
|
||||||
### ❌ **Problema: Errori permessi**
|
### ❌ **Problema: Errori permessi**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Fix permessi
|
# Fix permessi
|
||||||
sudo chown -R www-data:www-data ~/netgescon/netgescon-laravel
|
sudo chown -R www-data:www-data ~/netgescon/netgescon-laravel
|
||||||
|
|
@ -235,6 +274,7 @@ # Fix permessi
|
||||||
```
|
```
|
||||||
|
|
||||||
### ❌ **Problema: Database connection**
|
### ❌ **Problema: Database connection**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Verifica configurazione
|
# Verifica configurazione
|
||||||
cat .env | grep DB_
|
cat .env | grep DB_
|
||||||
|
|
@ -243,6 +283,7 @@ # Riferimento: docs/00-transizione-linux/README-TRANSITION-COMPLETE.md
|
||||||
```
|
```
|
||||||
|
|
||||||
### ❌ **Problema: Autenticazione rotta**
|
### ❌ **Problema: Autenticazione rotta**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Verifica ruoli
|
# Verifica ruoli
|
||||||
php artisan tinker
|
php artisan tinker
|
||||||
|
|
@ -255,6 +296,7 @@ # Riferimento: docs/06-SISTEMA-MULTI-RUOLO.md
|
||||||
## 🔄 **SINCRONIZZAZIONE SERVER REMOTO**
|
## 🔄 **SINCRONIZZAZIONE SERVER REMOTO**
|
||||||
|
|
||||||
### 📡 **Script Rsync Pronto**
|
### 📡 **Script Rsync Pronto**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Da eseguire per sincronizzare con server remoto
|
# Da eseguire per sincronizzare con server remoto
|
||||||
cd ~/netgescon
|
cd ~/netgescon
|
||||||
|
|
@ -267,6 +309,7 @@ # Da eseguire per sincronizzare con server remoto
|
||||||
```
|
```
|
||||||
|
|
||||||
### 🎯 **Cartelle da Sincronizzare SEMPRE**
|
### 🎯 **Cartelle da Sincronizzare SEMPRE**
|
||||||
|
|
||||||
- `docs/` - Documentazione completa
|
- `docs/` - Documentazione completa
|
||||||
- `netgescon-laravel/app/` - Codice applicazione
|
- `netgescon-laravel/app/` - Codice applicazione
|
||||||
- `netgescon-laravel/resources/` - Template e asset
|
- `netgescon-laravel/resources/` - Template e asset
|
||||||
|
|
@ -274,6 +317,7 @@ ### 🎯 **Cartelle da Sincronizzare SEMPRE**
|
||||||
- `netgescon-laravel/.env.example` - Configurazione template
|
- `netgescon-laravel/.env.example` - Configurazione template
|
||||||
|
|
||||||
### ⚠️ **Cartelle da NON Sincronizzare**
|
### ⚠️ **Cartelle da NON Sincronizzare**
|
||||||
|
|
||||||
- `netgescon-laravel/storage/logs/` - Log locali
|
- `netgescon-laravel/storage/logs/` - Log locali
|
||||||
- `netgescon-laravel/vendor/` - Dipendenze (ricostruire con composer)
|
- `netgescon-laravel/vendor/` - Dipendenze (ricostruire con composer)
|
||||||
- `netgescon-laravel/node_modules/` - Dipendenze npm
|
- `netgescon-laravel/node_modules/` - Dipendenze npm
|
||||||
|
|
@ -284,6 +328,7 @@ ### ⚠️ **Cartelle da NON Sincronizzare**
|
||||||
## 🤖 **MESSAGGI PER IL TUO ALTER EGO**
|
## 🤖 **MESSAGGI PER IL TUO ALTER EGO**
|
||||||
|
|
||||||
### 💬 **Cosa dirgli per iniziare**
|
### 💬 **Cosa dirgli per iniziare**
|
||||||
|
|
||||||
```
|
```
|
||||||
"Ciao! Sono il tuo 'alter ego' Michele. Ho preparato tutto per te:
|
"Ciao! Sono il tuo 'alter ego' Michele. Ho preparato tutto per te:
|
||||||
|
|
||||||
|
|
@ -305,6 +350,7 @@ ### 💬 **Cosa dirgli per iniziare**
|
||||||
```
|
```
|
||||||
|
|
||||||
### 🎯 **Come risolvere primi problemi**
|
### 🎯 **Come risolvere primi problemi**
|
||||||
|
|
||||||
```
|
```
|
||||||
"Per i primi problemi:
|
"Per i primi problemi:
|
||||||
|
|
||||||
|
|
@ -322,6 +368,7 @@ ### 🎯 **Come risolvere primi problemi**
|
||||||
## 📋 **CHECKLIST FINALE HANDOFF**
|
## 📋 **CHECKLIST FINALE HANDOFF**
|
||||||
|
|
||||||
### ✅ **Verifica Completezza**
|
### ✅ **Verifica Completezza**
|
||||||
|
|
||||||
- [ ] Documentazione unificata in docs/
|
- [ ] Documentazione unificata in docs/
|
||||||
- [ ] Script automazione testati
|
- [ ] Script automazione testati
|
||||||
- [ ] Database funzionante
|
- [ ] Database funzionante
|
||||||
|
|
@ -330,6 +377,7 @@ ### ✅ **Verifica Completezza**
|
||||||
- [ ] Rsync script preparato
|
- [ ] Rsync script preparato
|
||||||
|
|
||||||
### ✅ **Test Funzionalità**
|
### ✅ **Test Funzionalità**
|
||||||
|
|
||||||
- [ ] Login admin funziona
|
- [ ] Login admin funziona
|
||||||
- [ ] Dashboard carica
|
- [ ] Dashboard carica
|
||||||
- [ ] CRUD stabili operativo
|
- [ ] CRUD stabili operativo
|
||||||
|
|
@ -337,6 +385,7 @@ ### ✅ **Test Funzionalità**
|
||||||
- [ ] Sidebar AJAX funziona
|
- [ ] Sidebar AJAX funziona
|
||||||
|
|
||||||
### ✅ **Documentazione Accessibile**
|
### ✅ **Documentazione Accessibile**
|
||||||
|
|
||||||
- [ ] Indici navigabili
|
- [ ] Indici navigabili
|
||||||
- [ ] Cross-reference funzionanti
|
- [ ] Cross-reference funzionanti
|
||||||
- [ ] Screenshot disponibili
|
- [ ] Screenshot disponibili
|
||||||
|
|
@ -347,12 +396,12 @@ ### ✅ **Documentazione Accessibile**
|
||||||
## 🚀 **PASSAGGIO DI CONSEGNE COMPLETATO**
|
## 🚀 **PASSAGGIO DI CONSEGNE COMPLETATO**
|
||||||
|
|
||||||
> **🎯 Michele → GitHub Copilot/AI**
|
> **🎯 Michele → GitHub Copilot/AI**
|
||||||
>
|
>
|
||||||
> **Data handoff:** 18/07/2025
|
> **Data handoff:** 18/07/2025
|
||||||
> **Stato sistema:** ✅ Funzionante e documentato
|
> **Stato sistema:** ✅ Funzionante e documentato
|
||||||
> **Copertura documentazione:** 301 file, 11MB
|
> **Copertura documentazione:** 301 file, 11MB
|
||||||
> **Entry point:** `docs/00-INDICE-DOCS-UNIFICATA.md`
|
> **Entry point:** `docs/00-INDICE-DOCS-UNIFICATA.md`
|
||||||
>
|
>
|
||||||
> **🔑 Tutto è pronto per continuare lo sviluppo in autonomia!**
|
> **🔑 Tutto è pronto per continuare lo sviluppo in autonomia!**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,23 @@ # 🚀 NETGESCON - STRATEGIA GIT DISTRIBUITO E SISTEMA DISTRIBUZIONE
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Nota Day0
|
||||||
|
|
||||||
|
Per l'operativita corrente del progetto vale questa regola prioritaria:
|
||||||
|
|
||||||
|
- il solo repository attivo e distribuibile e `netgescon-day0`
|
||||||
|
- il solo workspace di sviluppo da usare e `/home/michele/netgescon/netgescon-day0`
|
||||||
|
- la documentazione attuale da seguire e `/home/michele/netgescon/netgescon-day0/docs`
|
||||||
|
- eventuali riferimenti storici a `~/netgescon/docs` o `netgescon-laravel` sono da considerare archivio o contesto legacy
|
||||||
|
|
||||||
|
In caso di conflitto prevalgono sempre le regole Day0 e la policy [docs/DOCS-AND-WORKSPACE-POLICY-DAY0.md](/home/michele/netgescon/netgescon-day0/docs/DOCS-AND-WORKSPACE-POLICY-DAY0.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🏗️ **ARCHITETTURA GIT DISTRIBUITA**
|
## 🏗️ **ARCHITETTURA GIT DISTRIBUITA**
|
||||||
|
|
||||||
### 🎯 **FILOSOFIA SISTEMA**
|
### 🎯 **FILOSOFIA SISTEMA**
|
||||||
|
|
||||||
- **Autonomia completa** del team di sviluppo
|
- **Autonomia completa** del team di sviluppo
|
||||||
- **Controllo versioni** professionale interno
|
- **Controllo versioni** professionale interno
|
||||||
- **Distribuzione sicura** agli utilizzatori finali
|
- **Distribuzione sicura** agli utilizzatori finali
|
||||||
|
|
@ -41,6 +55,7 @@ ### 🌐 **STRUTTURA GIT MULTI-LIVELLO**
|
||||||
## ✅ **STATUS IMPLEMENTAZIONE**
|
## ✅ **STATUS IMPLEMENTAZIONE**
|
||||||
|
|
||||||
### 🎯 **COMPLETATO**
|
### 🎯 **COMPLETATO**
|
||||||
|
|
||||||
- ✅ Repository Git inizializzato con commit iniziale
|
- ✅ Repository Git inizializzato con commit iniziale
|
||||||
- ✅ Struttura branches creata: master, development, release, hotfix
|
- ✅ Struttura branches creata: master, development, release, hotfix
|
||||||
- ✅ Scripts di automazione Git workflow
|
- ✅ Scripts di automazione Git workflow
|
||||||
|
|
@ -49,6 +64,7 @@ ### 🎯 **COMPLETATO**
|
||||||
- ✅ Plugin system progettato
|
- ✅ Plugin system progettato
|
||||||
|
|
||||||
### 🔄 **IN CORSO**
|
### 🔄 **IN CORSO**
|
||||||
|
|
||||||
- 🚧 Setup Git server (Gitea) su macchina master
|
- 🚧 Setup Git server (Gitea) su macchina master
|
||||||
- 🚧 Configurazione domini: git.netgescon.it
|
- 🚧 Configurazione domini: git.netgescon.it
|
||||||
- 🚧 Sistema distribuzione pacchetti
|
- 🚧 Sistema distribuzione pacchetti
|
||||||
|
|
@ -60,6 +76,7 @@ ## 🔧 **SETUP GIT SERVER INTERNO**
|
||||||
### 📋 **Configurazione Git Server su MASTER**
|
### 📋 **Configurazione Git Server su MASTER**
|
||||||
|
|
||||||
#### 1️⃣ **Setup Gitea/GitLab Self-Hosted**
|
#### 1️⃣ **Setup Gitea/GitLab Self-Hosted**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Opzione A: Gitea (leggero e veloce) - SCRIPT PREPARATO
|
# Opzione A: Gitea (leggero e veloce) - SCRIPT PREPARATO
|
||||||
sudo docs/03-scripts-automazione/setup-git-server-master.sh
|
sudo docs/03-scripts-automazione/setup-git-server-master.sh
|
||||||
|
|
@ -81,6 +98,7 @@ # Opzione B: GitLab CE (più completo)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2️⃣ **Configurazione Accessi**
|
#### 2️⃣ **Configurazione Accessi**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# URL Git server interno
|
# URL Git server interno
|
||||||
https://git.netgescon.local # Web interface
|
https://git.netgescon.local # Web interface
|
||||||
|
|
@ -95,6 +113,7 @@ # Users del team
|
||||||
### 🗂️ **Repository Structure**
|
### 🗂️ **Repository Structure**
|
||||||
|
|
||||||
#### 📦 **netgescon-core.git**
|
#### 📦 **netgescon-core.git**
|
||||||
|
|
||||||
```
|
```
|
||||||
netgescon-core/
|
netgescon-core/
|
||||||
├── app/ # Laravel application
|
├── app/ # Laravel application
|
||||||
|
|
@ -109,6 +128,7 @@ #### 📦 **netgescon-core.git**
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 🔌 **netgescon-plugins.git**
|
#### 🔌 **netgescon-plugins.git**
|
||||||
|
|
||||||
```
|
```
|
||||||
netgescon-plugins/
|
netgescon-plugins/
|
||||||
├── core-plugins/ # Plugin essenziali
|
├── core-plugins/ # Plugin essenziali
|
||||||
|
|
@ -122,6 +142,7 @@ #### 🔌 **netgescon-plugins.git**
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 🎨 **netgescon-themes.git**
|
#### 🎨 **netgescon-themes.git**
|
||||||
|
|
||||||
```
|
```
|
||||||
netgescon-themes/
|
netgescon-themes/
|
||||||
├── default/ # Tema di default
|
├── default/ # Tema di default
|
||||||
|
|
@ -139,6 +160,7 @@ ## 📋 **WORKFLOW DI SVILUPPO**
|
||||||
### 🔄 **Processo di Sviluppo Interno**
|
### 🔄 **Processo di Sviluppo Interno**
|
||||||
|
|
||||||
#### 1️⃣ **Sviluppo Locale (Michele + AI)**
|
#### 1️⃣ **Sviluppo Locale (Michele + AI)**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone del repo principale
|
# Clone del repo principale
|
||||||
git clone git@git.netgescon.local:netgescon/netgescon-core.git
|
git clone git@git.netgescon.local:netgescon/netgescon-core.git
|
||||||
|
|
@ -155,6 +177,7 @@ # Merge request via web interface
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2️⃣ **Review e Testing**
|
#### 2️⃣ **Review e Testing**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# AI Remoto testa la feature
|
# AI Remoto testa la feature
|
||||||
git checkout feature/nuova-funzionalita
|
git checkout feature/nuova-funzionalita
|
||||||
|
|
@ -168,6 +191,7 @@ # Se OK, merge in develop
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 3️⃣ **Release Process**
|
#### 3️⃣ **Release Process**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Quando develop è stabile
|
# Quando develop è stabile
|
||||||
git checkout main
|
git checkout main
|
||||||
|
|
@ -180,6 +204,7 @@ # Trigger deploy automatico
|
||||||
```
|
```
|
||||||
|
|
||||||
### 🚀 **Branching Strategy**
|
### 🚀 **Branching Strategy**
|
||||||
|
|
||||||
```
|
```
|
||||||
main # Produzione stabile
|
main # Produzione stabile
|
||||||
├── develop # Sviluppo attivo
|
├── develop # Sviluppo attivo
|
||||||
|
|
@ -195,6 +220,7 @@ ## 📦 **SISTEMA DISTRIBUZIONE AUTOMATICA**
|
||||||
### 🎯 **Multi-Platform Distribution**
|
### 🎯 **Multi-Platform Distribution**
|
||||||
|
|
||||||
#### 1️⃣ **Distribuzione via APT Repository**
|
#### 1️⃣ **Distribuzione via APT Repository**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Setup repository NetGescon
|
# Setup repository NetGescon
|
||||||
curl -fsSL https://packages.netgescon.it/gpg | sudo apt-key add -
|
curl -fsSL https://packages.netgescon.it/gpg | sudo apt-key add -
|
||||||
|
|
@ -209,6 +235,7 @@ # Auto-update
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2️⃣ **Distribuzione via Docker**
|
#### 2️⃣ **Distribuzione via Docker**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Pull immagine ufficiale
|
# Pull immagine ufficiale
|
||||||
docker pull netgescon/netgescon:latest
|
docker pull netgescon/netgescon:latest
|
||||||
|
|
@ -219,6 +246,7 @@ # Deploy completo con docker-compose
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 3️⃣ **Distribuzione via VM Template**
|
#### 3️⃣ **Distribuzione via VM Template**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Download template VM
|
# Download template VM
|
||||||
curl -fsSL https://get.netgescon.it/netgescon-vm-latest.ova -o netgescon.ova
|
curl -fsSL https://get.netgescon.it/netgescon-vm-latest.ova -o netgescon.ova
|
||||||
|
|
@ -230,6 +258,7 @@ # Auto-configurazione al primo boot
|
||||||
### ⚙️ **Auto-Updater System**
|
### ⚙️ **Auto-Updater System**
|
||||||
|
|
||||||
#### 📋 **Script Auto-Update**
|
#### 📋 **Script Auto-Update**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# /usr/local/bin/netgescon-updater
|
# /usr/local/bin/netgescon-updater
|
||||||
|
|
@ -255,6 +284,7 @@ # Check for updates
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 🔐 **Gestione Permessi Update**
|
#### 🔐 **Gestione Permessi Update**
|
||||||
|
|
||||||
```php
|
```php
|
||||||
// Nel core NetGescon
|
// Nel core NetGescon
|
||||||
class UpdateManager {
|
class UpdateManager {
|
||||||
|
|
@ -279,6 +309,7 @@ ## 🔌 **PLUGIN SYSTEM ARCHITECTURE**
|
||||||
### 🏗️ **Framework Plugin**
|
### 🏗️ **Framework Plugin**
|
||||||
|
|
||||||
#### 📋 **Plugin Structure**
|
#### 📋 **Plugin Structure**
|
||||||
|
|
||||||
```php
|
```php
|
||||||
<?php
|
<?php
|
||||||
// plugins/esempio-plugin/Plugin.php
|
// plugins/esempio-plugin/Plugin.php
|
||||||
|
|
@ -311,6 +342,7 @@ #### 📋 **Plugin Structure**
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 🛒 **Plugin Marketplace**
|
#### 🛒 **Plugin Marketplace**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# CLI per gestione plugin
|
# CLI per gestione plugin
|
||||||
netgescon plugin:search "fatturazione"
|
netgescon plugin:search "fatturazione"
|
||||||
|
|
@ -321,6 +353,7 @@ # CLI per gestione plugin
|
||||||
```
|
```
|
||||||
|
|
||||||
### 📱 **Plugin Development Kit**
|
### 📱 **Plugin Development Kit**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Scaffold nuovo plugin
|
# Scaffold nuovo plugin
|
||||||
netgescon make:plugin MyAwesomePlugin
|
netgescon make:plugin MyAwesomePlugin
|
||||||
|
|
@ -344,6 +377,7 @@ ## ⚖️ **STRATEGIA LICENZE A-GPL**
|
||||||
### 📜 **GNU Affero General Public License v3**
|
### 📜 **GNU Affero General Public License v3**
|
||||||
|
|
||||||
#### 🎯 **Vantaggi A-GPL per NetGescon**
|
#### 🎯 **Vantaggi A-GPL per NetGescon**
|
||||||
|
|
||||||
1. **🔒 Protezione IP:** Impedisce appropriazione indebita
|
1. **🔒 Protezione IP:** Impedisce appropriazione indebita
|
||||||
2. **🔄 Contributi obbligatori:** Modifiche devono tornare al progetto
|
2. **🔄 Contributi obbligatori:** Modifiche devono tornare al progetto
|
||||||
3. **🌐 Network copyleft:** Vale anche per servizi web
|
3. **🌐 Network copyleft:** Vale anche per servizi web
|
||||||
|
|
@ -351,6 +385,7 @@ #### 🎯 **Vantaggi A-GPL per NetGescon**
|
||||||
5. **🆓 Uso gratuito:** Per usi non commerciali conformi
|
5. **🆓 Uso gratuito:** Per usi non commerciali conformi
|
||||||
|
|
||||||
#### 📋 **Implementazione Licenza**
|
#### 📋 **Implementazione Licenza**
|
||||||
|
|
||||||
```php
|
```php
|
||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
|
|
@ -374,6 +409,7 @@ #### 📋 **Implementazione Licenza**
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 💼 **Modello Business A-GPL**
|
#### 💼 **Modello Business A-GPL**
|
||||||
|
|
||||||
```
|
```
|
||||||
🆓 COMMUNITY EDITION (A-GPL)
|
🆓 COMMUNITY EDITION (A-GPL)
|
||||||
├── ✅ Core completo
|
├── ✅ Core completo
|
||||||
|
|
@ -403,6 +439,7 @@ ## 🔐 **SICUREZZA E CONTROLLO VERSIONI**
|
||||||
### 🛡️ **Controllo Accessi Git**
|
### 🛡️ **Controllo Accessi Git**
|
||||||
|
|
||||||
#### 📋 **Permessi Repository**
|
#### 📋 **Permessi Repository**
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# .gitlab-ci.yml / .gitea/workflows/
|
# .gitlab-ci.yml / .gitea/workflows/
|
||||||
permissions:
|
permissions:
|
||||||
|
|
@ -419,6 +456,7 @@ # .gitlab-ci.yml / .gitea/workflows/
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 🔍 **Quality Gates**
|
#### 🔍 **Quality Gates**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Pre-commit hooks
|
# Pre-commit hooks
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
@ -451,6 +489,7 @@ # Security check
|
||||||
### 📊 **Monitoring e Analytics**
|
### 📊 **Monitoring e Analytics**
|
||||||
|
|
||||||
#### 📈 **Usage Analytics**
|
#### 📈 **Usage Analytics**
|
||||||
|
|
||||||
```php
|
```php
|
||||||
// Tracking uso installazioni (anonimo)
|
// Tracking uso installazioni (anonimo)
|
||||||
class UsageAnalytics {
|
class UsageAnalytics {
|
||||||
|
|
@ -474,24 +513,28 @@ #### 📈 **Usage Analytics**
|
||||||
## 🚀 **ROADMAP IMPLEMENTAZIONE**
|
## 🚀 **ROADMAP IMPLEMENTAZIONE**
|
||||||
|
|
||||||
### 📅 **Phase 1: Git Setup (1-2 settimane)**
|
### 📅 **Phase 1: Git Setup (1-2 settimane)**
|
||||||
|
|
||||||
- ✅ Setup Git server interno (Gitea)
|
- ✅ Setup Git server interno (Gitea)
|
||||||
- ✅ Migrazione codice esistente
|
- ✅ Migrazione codice esistente
|
||||||
- ✅ Configurazione workflow
|
- ✅ Configurazione workflow
|
||||||
- ✅ Training team
|
- ✅ Training team
|
||||||
|
|
||||||
### 📅 **Phase 2: Plugin System (2-3 settimane)**
|
### 📅 **Phase 2: Plugin System (2-3 settimane)**
|
||||||
|
|
||||||
- ⏳ Framework plugin
|
- ⏳ Framework plugin
|
||||||
- ⏳ API plugin system
|
- ⏳ API plugin system
|
||||||
- ⏳ Plugin development kit
|
- ⏳ Plugin development kit
|
||||||
- ⏳ Marketplace base
|
- ⏳ Marketplace base
|
||||||
|
|
||||||
### 📅 **Phase 3: Distribution System (3-4 settimane)**
|
### 📅 **Phase 3: Distribution System (3-4 settimane)**
|
||||||
|
|
||||||
- ⏳ APT repository
|
- ⏳ APT repository
|
||||||
- ⏳ Docker images
|
- ⏳ Docker images
|
||||||
- ⏳ Auto-updater
|
- ⏳ Auto-updater
|
||||||
- ⏳ VM templates
|
- ⏳ VM templates
|
||||||
|
|
||||||
### 📅 **Phase 4: Community & Licensing (2-3 settimane)**
|
### 📅 **Phase 4: Community & Licensing (2-3 settimane)**
|
||||||
|
|
||||||
- ⏳ Licenza A-GPL implementation
|
- ⏳ Licenza A-GPL implementation
|
||||||
- ⏳ GitHub public repository
|
- ⏳ GitHub public repository
|
||||||
- ⏳ Documentation community
|
- ⏳ Documentation community
|
||||||
|
|
@ -513,6 +556,7 @@ ### 🏆 **ECCELLENTE PIANO MICHELE!**
|
||||||
6. **✅ Innovation protection** - Contributi obbligatori al core
|
6. **✅ Innovation protection** - Contributi obbligatori al core
|
||||||
|
|
||||||
### 🎯 **BENEFICI STRATEGICI**
|
### 🎯 **BENEFICI STRATEGICI**
|
||||||
|
|
||||||
- **Controllo totale** del codice e sviluppo
|
- **Controllo totale** del codice e sviluppo
|
||||||
- **Protezione legale** da appropriazione indebita
|
- **Protezione legale** da appropriazione indebita
|
||||||
- **Revenue stream** da licenze commerciali
|
- **Revenue stream** da licenze commerciali
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,12 @@ ## Checklist update
|
||||||
- clear cache
|
- clear cache
|
||||||
- health check
|
- health check
|
||||||
|
|
||||||
|
## Backup pre-update applicativo
|
||||||
|
- Il backup locale pre-update resta sempre obbligatorio.
|
||||||
|
- Upload Google Drive opzionale per nodo tramite `NETGESCON_PREUPDATE_DRIVE_ENABLED=true`.
|
||||||
|
- Blocco hard dell update se Drive fallisce tramite `NETGESCON_PREUPDATE_REQUIRE_DRIVE=true`.
|
||||||
|
- Per staging e ambienti di test remoti si puo lasciare entrambi i flag a `false` e usare solo il backup locale.
|
||||||
|
|
||||||
## Note d'aggiornamento
|
## Note d'aggiornamento
|
||||||
- File: `CHANGELOG.md`
|
- File: `CHANGELOG.md`
|
||||||
- Versione: `VERSION`
|
- Versione: `VERSION`
|
||||||
|
|
|
||||||
32
docs/000-FILAMENT/14-SUPPORTO-TICKET-UX.md
Normal file
32
docs/000-FILAMENT/14-SUPPORTO-TICKET-UX.md
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Supporto Ticket UX
|
||||||
|
|
||||||
|
## Obiettivo
|
||||||
|
|
||||||
|
Allineare le pagine Filament del supporto a un uso rapido da mobile, riducendo campi lenti da usare e spostando le istruzioni in pagine help dedicate.
|
||||||
|
|
||||||
|
## Ticket Mobile
|
||||||
|
|
||||||
|
- La pagina `admin-filament/supporto/ticket-mobile` deve restare snella su telefono.
|
||||||
|
- Il box informativo esteso non deve occupare la parte alta della schermata.
|
||||||
|
- Le istruzioni operative stanno nella pagina dedicata `admin-filament/supporto/ticket-mobile-help`.
|
||||||
|
- In fondo alla pagina devono restare due azioni chiare:
|
||||||
|
- apertura `Gestione Ticket`
|
||||||
|
- apertura `Help Ticket Mobile`
|
||||||
|
|
||||||
|
## Gestione Ticket
|
||||||
|
|
||||||
|
- L'assegnazione fornitore non deve usare un select classico come UX primaria su mobile.
|
||||||
|
- La scelta del fornitore deve usare ricerca live a risultati cliccabili, come la ricerca chiamante di Ticket Mobile.
|
||||||
|
- La ricerca deve considerare almeno:
|
||||||
|
- tag fornitore
|
||||||
|
- telefono/cellulare
|
||||||
|
- nome/cognome
|
||||||
|
- ragione sociale
|
||||||
|
- email
|
||||||
|
- I tag sono criterio prioritario per casi operativi come `idraulico`, `spurghi`, `elettricista`, `ascensorista`.
|
||||||
|
- Il risultato selezionato deve restare visibile con possibilità rapida di cambiarlo.
|
||||||
|
|
||||||
|
## Regola Progetto
|
||||||
|
|
||||||
|
- Quando una pagina Filament diventa troppo densa per mobile, spostare il contenuto esplicativo in una pagina help dedicata invece di comprimere ulteriormente la UI operativa.
|
||||||
|
- Prima di introdurre un nuovo selettore complesso, verificare se esiste già nel progetto una UX di ricerca live riutilizzabile.
|
||||||
|
|
@ -37,4 +37,4 @@ ## Note implementative
|
||||||
|
|
||||||
- Le configurazioni dello studio sono salvate in `amministratori.impostazioni`.
|
- Le configurazioni dello studio sono salvate in `amministratori.impostazioni`.
|
||||||
- Le configurazioni dello stabile sono salvate in `stabili.configurazione_avanzata['posta']`.
|
- Le configurazioni dello stabile sono salvate in `stabili.configurazione_avanzata['posta']`.
|
||||||
- La lettura automatica dei messaggi non è ancora attiva in questo step: è pronto il perimetro di configurazione.
|
- La lettura automatica dei messaggi non è ancora attiva in questo step: è pronto il perimetro di configurazione.
|
||||||
|
|
|
||||||
98
docs/ANAGRAFE-CONDOMINIALE-PORTALE-POSTA.md
Normal file
98
docs/ANAGRAFE-CONDOMINIALE-PORTALE-POSTA.md
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
# Anagrafe Condominiale, Portale e Posta
|
||||||
|
|
||||||
|
## Obiettivo
|
||||||
|
|
||||||
|
Questa implementazione introduce la base web per tre esigenze operative:
|
||||||
|
|
||||||
|
- aggiornamento self-service della scheda anagrafica da parte di condomini e inquilini;
|
||||||
|
- gestione di piu recapiti email e recapiti differenziati per servizio e unita immobiliare;
|
||||||
|
- prima integrazione esplicita tra email in ingresso e ticket da seguire.
|
||||||
|
|
||||||
|
## Portale condomino/inquilino
|
||||||
|
|
||||||
|
Nuovi percorsi:
|
||||||
|
|
||||||
|
- `/condomino/anagrafe`
|
||||||
|
- `/condomino/anagrafe/modulo`
|
||||||
|
- `/condomino/unita`
|
||||||
|
- `/condomino/unita/{unitaImmobiliare}`
|
||||||
|
|
||||||
|
Funzioni disponibili:
|
||||||
|
|
||||||
|
- aggiornamento dati anagrafici base del profilo digitale associato;
|
||||||
|
- gestione di email multiple;
|
||||||
|
- definizione recapiti email di default per servizio e unita immobiliare;
|
||||||
|
- download PDF del modulo anagrafe da stampare o firmare manualmente;
|
||||||
|
- richiesta strutturata di aggiornamento catastale come servizio aggiuntivo.
|
||||||
|
|
||||||
|
## Tracciabilita
|
||||||
|
|
||||||
|
Le modifiche dirette della scheda anagrafica vengono registrate in `persone_audit` con:
|
||||||
|
|
||||||
|
- campo modificato;
|
||||||
|
- valore precedente;
|
||||||
|
- valore nuovo;
|
||||||
|
- utente che ha eseguito la modifica;
|
||||||
|
- IP e timestamp.
|
||||||
|
|
||||||
|
I recapiti per servizio vengono tracciati come campi audit del tipo:
|
||||||
|
|
||||||
|
- `unita_recapito:{unita_id}:{servizio}`
|
||||||
|
|
||||||
|
## Modello dati nuovo
|
||||||
|
|
||||||
|
Tabella introdotta:
|
||||||
|
|
||||||
|
- `unita_recapiti_servizio`
|
||||||
|
|
||||||
|
Scopo:
|
||||||
|
|
||||||
|
- salvare, per una specifica unita e una specifica persona, l'indirizzo email da usare come default per:
|
||||||
|
- comunicazioni generali
|
||||||
|
- convocazioni
|
||||||
|
- ticket
|
||||||
|
- solleciti
|
||||||
|
- contabilita
|
||||||
|
- amministrazione
|
||||||
|
- anagrafe
|
||||||
|
|
||||||
|
## Resolver recapiti
|
||||||
|
|
||||||
|
Servizio applicativo:
|
||||||
|
|
||||||
|
- `App\Services\Comunicazioni\RecapitiServizioResolver`
|
||||||
|
|
||||||
|
Ordine logico di risoluzione:
|
||||||
|
|
||||||
|
1. override esplicito su `unita_recapiti_servizio`;
|
||||||
|
2. fallback sui soggetti attivi collegati all'unita tramite `persone_unita_relazioni`;
|
||||||
|
3. email principale e email multiple attive della persona.
|
||||||
|
|
||||||
|
## Ticket e posta
|
||||||
|
|
||||||
|
Durante l'import EML da ticket (`Admin\TicketController::storeEmailMessage`) ora vengono creati entrambi:
|
||||||
|
|
||||||
|
- il documento EML archiviato;
|
||||||
|
- il `TicketMessage` interno;
|
||||||
|
- una `CommunicationMessage` collegata al ticket.
|
||||||
|
|
||||||
|
Questo prepara il terreno per una gestione futura di mailbox multiple e collegamento messaggio -> ticket senza passare solo da allegati manuali.
|
||||||
|
|
||||||
|
## Deploy e staging
|
||||||
|
|
||||||
|
Dopo aver copiato i file in staging eseguire sempre:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan migrate --force
|
||||||
|
php artisan optimize:clear
|
||||||
|
```
|
||||||
|
|
||||||
|
La migration da applicare e:
|
||||||
|
|
||||||
|
- `2026_03_18_090000_create_unita_recapiti_servizio_table.php`
|
||||||
|
|
||||||
|
## Limiti attuali
|
||||||
|
|
||||||
|
- il collegamento portale -> persona avviene solo quando l'utente autenticato e riconoscibile con sicurezza tramite email principale o email multipla della persona e una relazione attiva con le unita accessibili;
|
||||||
|
- la gestione completa di mailbox Google/IMAP non e ancora automatizzata;
|
||||||
|
- il servizio catastale e registrato come richiesta strutturata, non ancora come workflow economico/fatturabile completo.
|
||||||
69
docs/OPERATIVITA-STUDIO-COLLABORATORI-POSTA.md
Normal file
69
docs/OPERATIVITA-STUDIO-COLLABORATORI-POSTA.md
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
# Operativita Studio: Collaboratori, SMDR e Posta
|
||||||
|
|
||||||
|
## Collaboratori studio da Rubrica Universale
|
||||||
|
|
||||||
|
Percorso operativo previsto:
|
||||||
|
|
||||||
|
1. Crea o aggiorna il nominativo in Rubrica Universale con email reale.
|
||||||
|
2. Apri la scheda rubrica del nominativo.
|
||||||
|
3. Usa la tab `Collaboratore studio`.
|
||||||
|
4. Abilita l'accesso collaboratore.
|
||||||
|
5. Assegna l'interno PBX.
|
||||||
|
6. Seleziona gli stabili del tenant da associare al collaboratore.
|
||||||
|
7. Salva interno e stabili.
|
||||||
|
|
||||||
|
Regole applicative:
|
||||||
|
|
||||||
|
- Se esiste gia un utente con la stessa email, il sistema riusa quell'utente.
|
||||||
|
- Viene aggiunto il solo ruolo `collaboratore` senza rimuovere eventuali altri ruoli gia presenti.
|
||||||
|
- La sincronizzazione degli stabili agisce solo sugli stabili dell'amministratore corrente.
|
||||||
|
- Gli stabili assegnati fuori tenant non vengono rimossi.
|
||||||
|
|
||||||
|
## SMDR / Post-It tecnico
|
||||||
|
|
||||||
|
La tab tecnica Post-It ora distingue tre casi:
|
||||||
|
|
||||||
|
- `Interno collaboratore`: numero riconosciuto come `users.pbx_extension`.
|
||||||
|
- `Numero studio gestito`: numero esterno che coincide con uno dei numeri centralino configurati nella scheda amministratore.
|
||||||
|
- `Numero esterno`: tutto cio che non rientra nei due casi precedenti.
|
||||||
|
|
||||||
|
Filtri disponibili:
|
||||||
|
|
||||||
|
- vista chiamate: esterne, tutte, interne
|
||||||
|
- ambito numero: tutti, studio, interno, esterno
|
||||||
|
- direzione: inbound, outbound, internal
|
||||||
|
- ricerca libera su numero, interno o raw line
|
||||||
|
|
||||||
|
Nota tecnica:
|
||||||
|
|
||||||
|
- gli interni PBX non vengono piu risolti tramite Rubrica Universale come se fossero telefoni esterni.
|
||||||
|
|
||||||
|
## Posta: stato reale e percorso consigliato
|
||||||
|
|
||||||
|
Stato attuale:
|
||||||
|
|
||||||
|
- configurazione SMTP presente
|
||||||
|
- configurazione Google OAuth presente
|
||||||
|
- configurazione caselle Gmail/IMAP/PEC presente
|
||||||
|
- archiviazione manuale EML su ticket presente
|
||||||
|
- polling automatico caselle non ancora implementato come flusso applicativo stabile
|
||||||
|
|
||||||
|
Percorso consigliato per rendere la posta pronta via web:
|
||||||
|
|
||||||
|
1. Attiva un dominio posta reale, ad esempio `netgescon.it` su Google Workspace.
|
||||||
|
2. Crea la casella operativa, ad esempio `info@netgescon.it` o `studio@netgescon.it`.
|
||||||
|
3. Mantieni `netgescon@gmail.com` solo come account storico o tecnico, non come casella principale di produzione.
|
||||||
|
4. Configura in Scheda Amministratore:
|
||||||
|
- client id
|
||||||
|
- client secret
|
||||||
|
- redirect uri
|
||||||
|
- workspace email
|
||||||
|
- label/query Gmail
|
||||||
|
5. Usa il pulsante web `Collega Google`.
|
||||||
|
6. Usa il pulsante web `Verifica Google OAuth` per controllare APP_URL, redirect e credenziali.
|
||||||
|
7. Quando il check e pulito, il passo successivo di sviluppo da fare e un importer applicativo per Gmail/IMAP che trasformi i messaggi in `communication_messages`, ticket o documenti.
|
||||||
|
|
||||||
|
Indicazione architetturale:
|
||||||
|
|
||||||
|
- tutte le verifiche e le azioni operative vanno esposte da pagina web amministrativa
|
||||||
|
- la shell deve restare uno strumento di manutenzione, non il percorso operativo principale del prodotto ospitato
|
||||||
|
|
@ -2,6 +2,17 @@ # 🏢 NetGescon - Sistema di Gestione Condominiale
|
||||||
|
|
||||||
> **Sistema Unificato** per la gestione completa di condomini, stabili, unità immobiliari e amministrazione condominiale.
|
> **Sistema Unificato** per la gestione completa di condomini, stabili, unità immobiliari e amministrazione condominiale.
|
||||||
|
|
||||||
|
## Nota Operativa Day0
|
||||||
|
|
||||||
|
Questo repository e questo workspace sono la sola base attiva di sviluppo, test, commit, push e distribuzione.
|
||||||
|
|
||||||
|
- Workspace attivo: `/home/michele/netgescon/netgescon-day0`
|
||||||
|
- Documentazione autorevole: `/home/michele/netgescon/netgescon-day0/docs`
|
||||||
|
- Repository Git da usare: `netgescon-day0`
|
||||||
|
- La cartella storica `netgescon-laravel` va considerata archivio tecnico e non base di lavoro corrente.
|
||||||
|
|
||||||
|
Se un documento riporta path o workflow precedenti, prevalgono sempre le regole Day0.
|
||||||
|
|
||||||
## 📎 Descrizione
|
## 📎 Descrizione
|
||||||
|
|
||||||
NetGescon è una piattaforma web sviluppata in **Laravel** per la gestione completa di condomini e amministrazioni condominiali. Il sistema offre funzionalità avanzate per:
|
NetGescon è una piattaforma web sviluppata in **Laravel** per la gestione completa di condomini e amministrazioni condominiali. Il sistema offre funzionalità avanzate per:
|
||||||
|
|
@ -23,20 +34,23 @@ ## <20> **Tecnologie**
|
||||||
## 🛠️ **Installazione**
|
## 🛠️ **Installazione**
|
||||||
|
|
||||||
### 📋 **Prerequisiti**
|
### 📋 **Prerequisiti**
|
||||||
|
|
||||||
- PHP 8.3+
|
- PHP 8.3+
|
||||||
- Composer
|
- Composer
|
||||||
- MySQL/MariaDB
|
- MySQL/MariaDB
|
||||||
- Node.js + NPM (per asset)
|
- Node.js + NPM (per asset)
|
||||||
|
|
||||||
### ⚡ Setup rapido
|
### ⚡ Setup rapido
|
||||||
|
|
||||||
1) Installa dipendenze PHP: composer install
|
1) Installa dipendenze PHP: composer install
|
||||||
2) Copia env e chiave: cp .env.example .env; php artisan key:generate
|
2) Copia env e chiave: cp .env.example .env; php artisan key:generate
|
||||||
3) Migrazioni/seed: php artisan migrate:fresh --seed
|
3) Migrazioni/seed: php artisan migrate:fresh --seed
|
||||||
4) Avvio server: php artisan serve --host=0.0.0.0 --port=8000
|
4) Avvio server: php artisan serve --host=0.0.0.0 --port=8000
|
||||||
|
|
||||||
### 🔑 **Primo Accesso**
|
### 🔑 **Primo Accesso**
|
||||||
- **URL:** http://localhost:8000
|
|
||||||
- **Email:** admin@example.com
|
- **URL:** <http://localhost:8000>
|
||||||
|
- **Email:** <admin@example.com>
|
||||||
- **Password:** password
|
- **Password:** password
|
||||||
|
|
||||||
## 🧭 Documentazione & Indici
|
## 🧭 Documentazione & Indici
|
||||||
|
|
@ -46,34 +60,41 @@ ## 🧭 Documentazione & Indici
|
||||||
- `docs/automation/` – commit log mensile, summary giornalieri e protocollo handoff automatici.
|
- `docs/automation/` – commit log mensile, summary giornalieri e protocollo handoff automatici.
|
||||||
- `07-IMPORT-UNITA-IMMOBILIARI.md` – blueprint operativo per importare unità e collegare proprietari/inquilini.
|
- `07-IMPORT-UNITA-IMMOBILIARI.md` – blueprint operativo per importare unità e collegare proprietari/inquilini.
|
||||||
- `08-ANAGRAFICA-UNICA-UI.md` – specifica UX della scheda anagrafica unica e relativi widget condominiali.
|
- `08-ANAGRAFICA-UNICA-UI.md` – specifica UX della scheda anagrafica unica e relativi widget condominiali.
|
||||||
|
- `OPERATIVITA-STUDIO-COLLABORATORI-POSTA.md` – flusso operativo per collaboratori studio, filtri SMDR/Post-It e roadmap posta Google/IMAP via web.
|
||||||
|
- `ANAGRAFE-CONDOMINIALE-PORTALE-POSTA.md` – base tecnica e operativa per anagrafe self-service, recapiti email per servizio e primo aggancio posta-ticket.
|
||||||
|
|
||||||
## 📁 Struttura Progetto (workspace)
|
## 📁 Struttura Progetto (workspace)
|
||||||
|
|
||||||
- `/var/www/netgescon` – app Laravel principale (PHP 8.3, Laravel 12)
|
- `/home/michele/netgescon/netgescon-day0` – app Laravel principale e repository Git attivo
|
||||||
- `/home/michele/netgescon/docs` – documentazione completa (vedi indici sopra)
|
- `/home/michele/netgescon/netgescon-day0/docs` – documentazione attuale e autorevole
|
||||||
- `/home/michele/netgescon/scripts` – script import/export e riconciliazioni
|
- `/home/michele/netgescon/netgescon-day0/scripts` – script operativi del workspace attivo
|
||||||
|
- `netgescon-laravel` – archivio storico, non base attiva per sviluppo o distribuzione
|
||||||
|
|
||||||
## ✨ **Funzionalità Principali**
|
## ✨ **Funzionalità Principali**
|
||||||
|
|
||||||
### 🏢 **Gestione Stabili**
|
### 🏢 **Gestione Stabili**
|
||||||
|
|
||||||
- Anagrafica completa con dati catastali
|
- Anagrafica completa con dati catastali
|
||||||
- Multi-palazzine per complessi residenziali
|
- Multi-palazzine per complessi residenziali
|
||||||
- Gestione dati bancari e coordinate IBAN
|
- Gestione dati bancari e coordinate IBAN
|
||||||
- Upload documenti e planimetrie
|
- Upload documenti e planimetrie
|
||||||
|
|
||||||
### 👥 **Anagrafica Condominiale**
|
### 👥 **Anagrafica Condominiale**
|
||||||
|
|
||||||
- Proprietari e inquilini
|
- Proprietari e inquilini
|
||||||
- Cariche e deleghe amministrative
|
- Cariche e deleghe amministrative
|
||||||
- Storico variazioni quote millesimali
|
- Storico variazioni quote millesimali
|
||||||
- Gestione incarichi (portiere, pulizie, etc.)
|
- Gestione incarichi (portiere, pulizie, etc.)
|
||||||
|
|
||||||
### <20> **Area Finanziaria**
|
### <20> **Area Finanziaria**
|
||||||
|
|
||||||
- Conti correnti multipli
|
- Conti correnti multipli
|
||||||
- Budget preventivi e consuntivi
|
- Budget preventivi e consuntivi
|
||||||
- Ripartizione spese per criterio
|
- Ripartizione spese per criterio
|
||||||
- Estratti conto e solleciti
|
- Estratti conto e solleciti
|
||||||
|
|
||||||
### <20> **Reports e Stampe**
|
### <20> **Reports e Stampe**
|
||||||
|
|
||||||
- Bilanci consuntivi
|
- Bilanci consuntivi
|
||||||
- Situazione debitoria
|
- Situazione debitoria
|
||||||
- Comunicazioni personalizzate
|
- Comunicazioni personalizzate
|
||||||
|
|
@ -98,6 +119,7 @@ # Debug in tempo reale
|
||||||
## 🤝 **Contributi**
|
## 🤝 **Contributi**
|
||||||
|
|
||||||
Per contribuire al progetto:
|
Per contribuire al progetto:
|
||||||
|
|
||||||
1. Fork del repository
|
1. Fork del repository
|
||||||
2. Creazione branch feature (`git checkout -b feature/nome-feature`)
|
2. Creazione branch feature (`git checkout -b feature/nome-feature`)
|
||||||
3. Commit modifiche (`git commit -am 'Aggiunta nuova feature'`)
|
3. Commit modifiche (`git commit -am 'Aggiunta nuova feature'`)
|
||||||
|
|
@ -111,13 +133,15 @@ ## 📄 **Licenza**
|
||||||
## 📞 **Supporto**
|
## 📞 **Supporto**
|
||||||
|
|
||||||
Per supporto tecnico o domande:
|
Per supporto tecnico o domande:
|
||||||
|
|
||||||
- **Issues:** Usa il sistema issues di GitHub
|
- **Issues:** Usa il sistema issues di GitHub
|
||||||
- **Email:** info@netgescon.it
|
- **Email:** <info@netgescon.it>
|
||||||
- **Documentazione:** Disponibile nel repository
|
- **Documentazione:** Disponibile nel repository
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Link rapidi
|
Link rapidi
|
||||||
|
|
||||||
- Guida import GESCON (Stabili.mdb): `docs/000-IMPORT/01-GESCON/GUIDA-IMPORT-GESCON-MDB.md`
|
- Guida import GESCON (Stabili.mdb): `docs/000-IMPORT/01-GESCON/GUIDA-IMPORT-GESCON-MDB.md`
|
||||||
- Piano import unità → anagrafica unica: `docs/07-IMPORT-UNITA-IMMOBILIARI.md`
|
- Piano import unità → anagrafica unica: `docs/07-IMPORT-UNITA-IMMOBILIARI.md`
|
||||||
- Specifica UI anagrafica unica: `docs/08-ANAGRAFICA-UNICA-UI.md`
|
- Specifica UI anagrafica unica: `docs/08-ANAGRAFICA-UNICA-UI.md`
|
||||||
|
|
@ -125,4 +149,4 @@ ## 📞 **Supporto**
|
||||||
—
|
—
|
||||||
|
|
||||||
🏢 NetGescon – Sistema di Gestione Condominiale Unificato
|
🏢 NetGescon – Sistema di Gestione Condominiale Unificato
|
||||||
📧 Info: info@netgescon.it | 🌐 Demo: https://demo.netgescon.it
|
📧 Info: <info@netgescon.it> | 🌐 Demo: <https://demo.netgescon.it>
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,61 @@ ## Comando base
|
||||||
--reconnect-delay=5
|
--reconnect-delay=5
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Wrapper operativo consigliato
|
||||||
|
|
||||||
|
Per evitare comandi manuali lunghi e mantenere la stessa configurazione tra sviluppo e staging:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/michele/netgescon/netgescon-day0
|
||||||
|
bash scripts/ops/netgescon-smdr-listener.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Configurazione opzionale via file dedicato `.env.smdr` nella root applicativa:
|
||||||
|
|
||||||
|
```env
|
||||||
|
NETGESCON_SMDR_HOST=192.168.0.101
|
||||||
|
NETGESCON_SMDR_PORT=2300
|
||||||
|
NETGESCON_SMDR_USER=SMDR
|
||||||
|
NETGESCON_SMDR_PASS=PCCSMDR
|
||||||
|
NETGESCON_SMDR_RECONNECT=1
|
||||||
|
NETGESCON_SMDR_RECONNECT_DELAY=5
|
||||||
|
NETGESCON_SMDR_WRITE_POSTIT=1
|
||||||
|
NETGESCON_SMDR_WRITE_COMMUNICATIONS=1
|
||||||
|
NETGESCON_SMDR_TIMEOUT=0
|
||||||
|
NETGESCON_SMDR_MAX_LINES=0
|
||||||
|
NETGESCON_SMDR_NO_AUTH=0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Daemon systemd consigliato
|
||||||
|
|
||||||
|
Il blocco osservato il `13/03/2026` e stato coerente con un listener non piu in esecuzione. La soluzione stabile e tenerlo sotto `systemd` con restart automatico.
|
||||||
|
|
||||||
|
Template repo:
|
||||||
|
|
||||||
|
- `scripts/systemd/netgescon-smdr-listener.service.template`
|
||||||
|
- `scripts/ops/install-smdr-systemd.sh`
|
||||||
|
|
||||||
|
Installazione tipica sviluppo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/michele/netgescon/netgescon-day0
|
||||||
|
RUN_AS=michele RUN_GROUP=michele bash scripts/ops/install-smdr-systemd.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Installazione tipica staging:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/www/netgescon
|
||||||
|
RUN_AS=www-data RUN_GROUP=www-data bash scripts/ops/install-smdr-systemd.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Verifica:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl status netgescon-smdr-listener.service --no-pager
|
||||||
|
journalctl -u netgescon-smdr-listener.service -n 100 --no-pager
|
||||||
|
```
|
||||||
|
|
||||||
## Opzioni utili
|
## Opzioni utili
|
||||||
|
|
||||||
- `--no-auth`: se il centralino non richiede credenziali su socket.
|
- `--no-auth`: se il centralino non richiede credenziali su socket.
|
||||||
|
|
@ -32,6 +87,7 @@ ## Verifica dati acquisiti
|
||||||
```bash
|
```bash
|
||||||
php artisan tinker --execute="echo App\\Models\\CommunicationMessage::where('channel','smdr')->count();"
|
php artisan tinker --execute="echo App\\Models\\CommunicationMessage::where('channel','smdr')->count();"
|
||||||
php artisan tinker --execute="echo App\\Models\\ChiamataPostIt::count();"
|
php artisan tinker --execute="echo App\\Models\\ChiamataPostIt::count();"
|
||||||
|
php artisan tinker --execute="echo optional(App\\Models\\CommunicationMessage::where('channel','smdr')->latest('created_at')->first())->created_at;"
|
||||||
```
|
```
|
||||||
|
|
||||||
## URL operativi correlati
|
## URL operativi correlati
|
||||||
|
|
|
||||||
157
docs/checklists/DAY0-GITEA-UPDATES-CHECKLIST.md
Normal file
157
docs/checklists/DAY0-GITEA-UPDATES-CHECKLIST.md
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
# Day0 Gitea + Updates Checklist
|
||||||
|
|
||||||
|
Data: 17-03-2026
|
||||||
|
|
||||||
|
## Stato chiarito
|
||||||
|
|
||||||
|
- La sola versione realmente prevista oggi e attiva e `free`.
|
||||||
|
- La versione `licensed` e solo prevista sulla carta.
|
||||||
|
- Quindi un canale `licensed` vuoto, non pubblicato o non configurato non e un errore bloccante in questa fase.
|
||||||
|
- Il focus corretto e allineare `netgescon-day0`, Gitea e `updates.netgescon.it` sul solo canale `free`.
|
||||||
|
|
||||||
|
## Cosa risulta gia verificato
|
||||||
|
|
||||||
|
- Repository Git attivo: `ssh://git@192.168.0.53:2222/michele/netgescon-day0.git`
|
||||||
|
- Branch visibile da Day0: solo `main`
|
||||||
|
- Route Day0 locale presenti:
|
||||||
|
- `GET api/v1/distribution/updates/manifest`
|
||||||
|
- `GET api/v1/distribution/updates/package`
|
||||||
|
- Artefatti locali Day0 presenti:
|
||||||
|
- `storage/app/distribution/free/manifest.json`
|
||||||
|
- `storage/app/distribution/free/netgescon-0.8.1.zip`
|
||||||
|
- `updates.netgescon.it` ora risolve verso `192.168.0.53`
|
||||||
|
- `https://updates.netgescon.it/api/v1/distribution/health` risponde correttamente
|
||||||
|
- `https://updates.netgescon.it/api/v1/distribution/updates/manifest?channel=free` attualmente restituisce `404`
|
||||||
|
|
||||||
|
## Cosa chiedere all'altro agent
|
||||||
|
|
||||||
|
Chiedere di verificare sulla macchina `192.168.0.53` solo questi punti.
|
||||||
|
|
||||||
|
### 1. Repository e working copy corretti
|
||||||
|
|
||||||
|
Verificare:
|
||||||
|
|
||||||
|
- qual e la cartella reale dell'app che serve `updates.netgescon.it`
|
||||||
|
- se quella cartella deriva da `netgescon-day0`
|
||||||
|
- se il commit checked out sulla macchina `.53` coincide con `origin/main` di `netgescon-day0`
|
||||||
|
|
||||||
|
Comandi utili:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pwd
|
||||||
|
git remote -v
|
||||||
|
git branch --show-current
|
||||||
|
git rev-parse HEAD
|
||||||
|
git log --oneline -n 5
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Route Laravel sul server update
|
||||||
|
|
||||||
|
Verificare se sull'app che gira sulla `.53` esistono davvero le route distribution update:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan route:list | rg "distribution|updates"
|
||||||
|
```
|
||||||
|
|
||||||
|
Ci aspettiamo almeno:
|
||||||
|
|
||||||
|
- `api/v1/distribution/health`
|
||||||
|
- `api/v1/distribution/updates/manifest`
|
||||||
|
- `api/v1/distribution/updates/package`
|
||||||
|
|
||||||
|
### 3. Cache Laravel e route cache
|
||||||
|
|
||||||
|
Se le route nel codice esistono ma il server risponde `404`, chiedere di verificare e pulire cache:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan optimize:clear
|
||||||
|
php artisan route:clear
|
||||||
|
php artisan config:clear
|
||||||
|
php artisan cache:clear
|
||||||
|
php artisan route:list | rg "distribution|updates"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Document root e virtual host di updates.netgescon.it
|
||||||
|
|
||||||
|
Verificare che nginx/apache per `updates.netgescon.it` punti al `public/` dell'app Day0 corretta, non a un'altra installazione Laravel.
|
||||||
|
|
||||||
|
Chiedere:
|
||||||
|
|
||||||
|
- file di configurazione del virtual host usato da `updates.netgescon.it`
|
||||||
|
- document root impostata
|
||||||
|
- eventuale symlink verso cartella sbagliata
|
||||||
|
|
||||||
|
Controlli utili:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nginx -T | rg -n "updates.netgescon.it|root |server_name"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Artefatti distribution free sul server `.53`
|
||||||
|
|
||||||
|
Verificare che sulla macchina `.53` esistano davvero:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls -lah storage/app/distribution/free/
|
||||||
|
cat storage/app/distribution/free/manifest.json
|
||||||
|
sha256sum storage/app/distribution/free/*.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
Se mancano, il server update non puo servire il canale `free`.
|
||||||
|
|
||||||
|
### 6. Configurazione distribution
|
||||||
|
|
||||||
|
Verificare questi valori reali dentro l'app della `.53`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan tinker --execute='echo "APP_URL=".config("app.url").PHP_EOL; echo "DIST_ROOT=".config("distribution.storage_root").PHP_EOL; echo "LICENSED_PAIRS=".(config("distribution.licensed_pairs") ?: "").PHP_EOL;'
|
||||||
|
```
|
||||||
|
|
||||||
|
Ci serve sapere:
|
||||||
|
|
||||||
|
- `app.url`
|
||||||
|
- `distribution.storage_root`
|
||||||
|
- se `distribution.storage_root` punta davvero alla cartella giusta
|
||||||
|
|
||||||
|
### 7. Test HTTP locale dal server `.53`
|
||||||
|
|
||||||
|
Chiedere di testare dalla macchina stessa:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i http://127.0.0.1/api/v1/distribution/health
|
||||||
|
curl -i "http://127.0.0.1/api/v1/distribution/updates/manifest?channel=free"
|
||||||
|
```
|
||||||
|
|
||||||
|
Se da localhost funziona ma dal dominio no, il problema e nginx/vhost.
|
||||||
|
Se da localhost non funziona, il problema e app/codice/cache/storage.
|
||||||
|
|
||||||
|
### 8. Allineamento staging con sviluppo Day0
|
||||||
|
|
||||||
|
Per capire se staging e allineato alla nostra versione di sviluppo, chiedere:
|
||||||
|
|
||||||
|
- commit SHA della macchina `.53`
|
||||||
|
- commit SHA locale di `netgescon-day0`
|
||||||
|
- se il pacchetto `free` e stato rigenerato dopo gli ultimi cambi Day0
|
||||||
|
|
||||||
|
## Risposta minima che ci serve dall'altro agent
|
||||||
|
|
||||||
|
Chiedere di rispondere in questo formato:
|
||||||
|
|
||||||
|
1. Path reale dell'app che serve `updates.netgescon.it`
|
||||||
|
2. Commit SHA attuale sulla `.53`
|
||||||
|
3. Output di `php artisan route:list | rg "distribution|updates"`
|
||||||
|
4. Presenza o assenza di `storage/app/distribution/free/manifest.json`
|
||||||
|
5. Output di `cat storage/app/distribution/free/manifest.json`
|
||||||
|
6. Conferma del virtual host e document root di `updates.netgescon.it`
|
||||||
|
7. Esito di `curl` locale a `updates/manifest?channel=free`
|
||||||
|
8. Eventuale motivo del `404`
|
||||||
|
|
||||||
|
## Nota importante
|
||||||
|
|
||||||
|
Per ora non serve trattare `licensed` come errore.
|
||||||
|
|
||||||
|
Il requisito corretto di questa fase e:
|
||||||
|
|
||||||
|
- `free` deve funzionare
|
||||||
|
- Gitea Day0 deve essere la sola sorgente Git attiva
|
||||||
|
- `updates.netgescon.it` deve servire il manifest e il package del canale `free`
|
||||||
|
|
@ -216,11 +216,11 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<a href="{{ route('fornitore.tickets.index') }}" class="netgescon-btn netgescon-btn-outline-primary text-center">
|
<a href="{{ \App\Filament\Pages\Fornitore\TicketOperativi::getUrl(panel: 'admin-filament') }}" class="netgescon-btn netgescon-btn-outline-primary text-center">
|
||||||
<i class="fas fa-tools mr-2"></i>
|
<i class="fas fa-tools mr-2"></i>
|
||||||
Ticket Operativi Fornitore
|
Ticket Operativi Fornitore
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ route('fornitore.dipendenti.index') }}" class="netgescon-btn netgescon-btn-outline-secondary text-center">
|
<a href="{{ \App\Filament\Pages\Fornitore\Collaboratori::getUrl(panel: 'admin-filament') }}" class="netgescon-btn netgescon-btn-outline-secondary text-center">
|
||||||
<i class="fas fa-users-cog mr-2"></i>
|
<i class="fas fa-users-cog mr-2"></i>
|
||||||
Dipendenti Fornitore
|
Dipendenti Fornitore
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,33 @@
|
||||||
@endif
|
@endif
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="mt-4 rounded-2xl bg-white p-4 shadow-sm">
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<h2 class="text-sm font-semibold">Stabili essenziali</h2>
|
||||||
|
<span class="text-[11px] text-slate-500">Vista rapida mobile</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 space-y-2">
|
||||||
|
@forelse($stabili as $stabile)
|
||||||
|
<article class="rounded-xl border border-slate-200 p-3">
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-slate-900">{{ $stabile->denominazione ?: 'Stabile #' . $stabile->id }}</h3>
|
||||||
|
<p class="mt-1 text-xs text-slate-500">{{ trim(collect([$stabile->indirizzo, $stabile->citta])->filter()->implode(' · ')) ?: 'Indirizzo non valorizzato' }}</p>
|
||||||
|
</div>
|
||||||
|
<span class="rounded-full bg-slate-100 px-2 py-1 text-[11px] text-slate-600">{{ $stabile->stato ?: 'n.d.' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 grid grid-cols-2 gap-2">
|
||||||
|
<a href="{{ url('/admin-filament/condomini/stabile?stabile_id=' . $stabile->id) }}" class="rounded-lg bg-slate-900 px-3 py-3 text-center text-xs font-medium text-white">Apri stabile</a>
|
||||||
|
<a href="{{ url('/admin-filament/condomini/stabile?stabile_id=' . $stabile->id . '&tab=gestioni') }}" class="rounded-lg border border-slate-300 px-3 py-3 text-center text-xs font-medium text-slate-700">Gestioni</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
@empty
|
||||||
|
<p class="text-xs text-slate-500">Nessuno stabile disponibile per l'utente corrente.</p>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="mt-4 rounded-2xl bg-white p-4 shadow-sm">
|
<section class="mt-4 rounded-2xl bg-white p-4 shadow-sm">
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<h2 class="text-sm font-semibold">Ticket rapidi</h2>
|
<h2 class="text-sm font-semibold">Ticket rapidi</h2>
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,7 @@ class="bg-light0 hover:bg-gray-700 text-white fw-bold py-2 px-3 rounded">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary">Crea Intervento Operativo</button>
|
<button type="submit" class="btn btn-primary">Crea Intervento Operativo</button>
|
||||||
<a href="{{ route('fornitore.tickets.index') }}" class="btn btn-outline-secondary">Apri area manutentore</a>
|
<a href="{{ \App\Filament\Pages\Fornitore\TicketOperativi::getUrl(panel: 'admin-filament') }}" class="btn btn-outline-secondary">Apri area manutentore</a>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@if($ticket->interventi->count() > 0)
|
@if($ticket->interventi->count() > 0)
|
||||||
|
|
|
||||||
90
resources/views/condomino/anagrafe/modulo-pdf.blade.php
Normal file
90
resources/views/condomino/anagrafe/modulo-pdf.blade.php
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="it">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
body { font-family: DejaVu Sans, sans-serif; font-size: 12px; color: #1f2937; }
|
||||||
|
h1, h2, h3 { margin: 0 0 8px; }
|
||||||
|
.section { margin-bottom: 18px; }
|
||||||
|
.box { border: 1px solid #cbd5e1; padding: 10px; border-radius: 6px; }
|
||||||
|
.grid { width: 100%; border-collapse: collapse; }
|
||||||
|
.grid td, .grid th { border: 1px solid #cbd5e1; padding: 6px 8px; vertical-align: top; }
|
||||||
|
.muted { color: #64748b; }
|
||||||
|
.small { font-size: 10px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="section">
|
||||||
|
<h1>Modulo anagrafe condominiale</h1>
|
||||||
|
<p class="muted">Generato il {{ $generatedAt->format('d/m/Y H:i') }}. Il dichiarante si impegna a comunicare entro 30 giorni le variazioni rilevanti per l'anagrafe condominiale.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section box">
|
||||||
|
<h2>Dati del dichiarante</h2>
|
||||||
|
<table class="grid">
|
||||||
|
<tr><th width="30%">Cognome e nome</th><td>{{ trim((string) ($persona?->cognome ?? '') . ' ' . (string) ($persona?->nome ?? '')) ?: ($utente?->name ?? 'Da compilare') }}</td></tr>
|
||||||
|
<tr><th>Codice fiscale</th><td>{{ $persona?->codice_fiscale ?? '____________________________' }}</td></tr>
|
||||||
|
<tr><th>Partita IVA</th><td>{{ $persona?->partita_iva ?? '____________________________' }}</td></tr>
|
||||||
|
<tr><th>Data nascita</th><td>{{ optional($persona?->data_nascita)->format('d/m/Y') ?? '____________________________' }}</td></tr>
|
||||||
|
<tr><th>Residenza</th><td>{{ $persona?->residenza_via ?? '______________________________________________________________' }}</td></tr>
|
||||||
|
<tr><th>Telefono</th><td>{{ $persona?->telefono_principale ?? '____________________________' }}</td></tr>
|
||||||
|
<tr><th>Email principale</th><td>{{ $persona?->email_principale ?? ($utente?->email ?? '____________________________') }}</td></tr>
|
||||||
|
<tr><th>PEC</th><td>{{ $persona?->email_pec ?? '____________________________' }}</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section box">
|
||||||
|
<h2>Email aggiuntive</h2>
|
||||||
|
<table class="grid">
|
||||||
|
<tr>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Tipo</th>
|
||||||
|
<th>Attiva</th>
|
||||||
|
</tr>
|
||||||
|
@forelse ($emailRows as $row)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $row['email'] ?: '____________________________' }}</td>
|
||||||
|
<td>{{ $row['tipo_email'] ?: 'secondaria' }}</td>
|
||||||
|
<td>{{ ! empty($row['email']) ? (! empty($row['attiva']) ? 'Si' : 'No') : '___' }}</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr><td colspan="3">Nessun recapito aggiuntivo indicato.</td></tr>
|
||||||
|
@endforelse
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section box">
|
||||||
|
<h2>Unità immobiliari e recapiti per servizio</h2>
|
||||||
|
@forelse ($unitaImmobiliari as $unita)
|
||||||
|
@php $matrix = $serviceContactMatrix[$unita->id] ?? []; @endphp
|
||||||
|
<h3>{{ $unita->stabile->denominazione ?? 'Stabile' }} · {{ $unita->denominazione ?? ('Unità #' . $unita->id) }}</h3>
|
||||||
|
<table class="grid" style="margin-bottom: 12px;">
|
||||||
|
<tr>
|
||||||
|
<th width="35%">Servizio</th>
|
||||||
|
<th>Recapito</th>
|
||||||
|
</tr>
|
||||||
|
@foreach ($matrix as $row)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $row['label'] }}</td>
|
||||||
|
<td>{{ $row['value'] ?: collect($row['resolved'] ?? [])->pluck('email')->implode(', ') ?: '____________________________' }}</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</table>
|
||||||
|
@empty
|
||||||
|
<p>Nessuna unità collegata al profilo.</p>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section box">
|
||||||
|
<h2>Dichiarazione e firma</h2>
|
||||||
|
<p>Il sottoscritto dichiara che i dati riportati sono aggiornati e si impegna a comunicare entro 30 giorni ogni variazione utile alla gestione condominiale e all'anagrafe.</p>
|
||||||
|
<table style="width: 100%; margin-top: 22px;">
|
||||||
|
<tr>
|
||||||
|
<td width="50%">Data ____________________________</td>
|
||||||
|
<td width="50%">Firma ____________________________</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p class="small muted" style="margin-top: 16px;">Per chi non dispone di strumenti elettronici il modulo può essere stampato, firmato e consegnato allo studio.</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
189
resources/views/condomino/anagrafe/show.blade.php
Normal file
189
resources/views/condomino/anagrafe/show.blade.php
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
<title>Scheda anagrafica - NetGescon</title>
|
||||||
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
|
</head>
|
||||||
|
<body class="min-h-screen bg-slate-100 text-slate-900">
|
||||||
|
<div class="max-w-6xl mx-auto p-4 md:p-8 space-y-4">
|
||||||
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-xl md:text-2xl font-semibold">Scheda anagrafica condominiale</h1>
|
||||||
|
<p class="text-sm text-slate-600">Puoi aggiornare i tuoi recapiti, le email aggiuntive e i recapiti per servizio. Ogni modifica viene tracciata.</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<a href="{{ route('condomino.dashboard') }}" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm">Dashboard</a>
|
||||||
|
<a href="{{ route('condomino.unita.index') }}" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm">Le mie unità</a>
|
||||||
|
<a href="{{ route('condomino.anagrafe.modulo') }}" class="inline-flex items-center rounded-lg bg-slate-800 px-4 py-2 text-sm text-white">Scarica modulo PDF</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (session('success'))
|
||||||
|
<div class="rounded-lg border border-emerald-300 bg-emerald-50 p-3 text-sm text-emerald-700">{{ session('success') }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if ($errors->any())
|
||||||
|
<div class="rounded-lg border border-red-300 bg-red-50 p-3 text-sm text-red-700">
|
||||||
|
<ul class="list-disc pl-5">
|
||||||
|
@foreach ($errors->all() as $error)
|
||||||
|
<li>{{ $error }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if (! $persona)
|
||||||
|
<div class="rounded-lg border border-amber-300 bg-amber-50 p-4 text-sm text-amber-800">
|
||||||
|
Non è stato possibile associare con certezza il tuo account a una scheda anagrafica digitale. Puoi comunque scaricare il modulo e usare la scheda unità per inviare richieste strutturate.
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('condomino.anagrafe.update') }}" class="space-y-4">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||||
|
<div class="rounded-lg bg-white p-4 shadow space-y-4">
|
||||||
|
<h2 class="text-lg font-semibold">Dati anagrafici</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<label class="text-sm">
|
||||||
|
<span class="block text-slate-600 mb-1">Cognome</span>
|
||||||
|
<input name="cognome" type="text" value="{{ old('cognome', $persona?->cognome) }}" class="w-full rounded-md border-slate-300">
|
||||||
|
</label>
|
||||||
|
<label class="text-sm">
|
||||||
|
<span class="block text-slate-600 mb-1">Nome</span>
|
||||||
|
<input name="nome" type="text" value="{{ old('nome', $persona?->nome) }}" class="w-full rounded-md border-slate-300">
|
||||||
|
</label>
|
||||||
|
<label class="text-sm">
|
||||||
|
<span class="block text-slate-600 mb-1">Codice fiscale</span>
|
||||||
|
<input name="codice_fiscale" type="text" value="{{ old('codice_fiscale', $persona?->codice_fiscale) }}" class="w-full rounded-md border-slate-300">
|
||||||
|
</label>
|
||||||
|
<label class="text-sm">
|
||||||
|
<span class="block text-slate-600 mb-1">Partita IVA</span>
|
||||||
|
<input name="partita_iva" type="text" value="{{ old('partita_iva', $persona?->partita_iva) }}" class="w-full rounded-md border-slate-300">
|
||||||
|
</label>
|
||||||
|
<label class="text-sm">
|
||||||
|
<span class="block text-slate-600 mb-1">Data nascita</span>
|
||||||
|
<input name="data_nascita" type="date" value="{{ old('data_nascita', optional($persona?->data_nascita)->format('Y-m-d')) }}" class="w-full rounded-md border-slate-300">
|
||||||
|
</label>
|
||||||
|
<label class="text-sm md:col-span-2">
|
||||||
|
<span class="block text-slate-600 mb-1">Residenza</span>
|
||||||
|
<input name="residenza_via" type="text" value="{{ old('residenza_via', $persona?->residenza_via) }}" class="w-full rounded-md border-slate-300">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg bg-white p-4 shadow space-y-4">
|
||||||
|
<h2 class="text-lg font-semibold">Recapiti principali</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<label class="text-sm">
|
||||||
|
<span class="block text-slate-600 mb-1">Telefono principale</span>
|
||||||
|
<input name="telefono_principale" type="text" value="{{ old('telefono_principale', $persona?->telefono_principale) }}" class="w-full rounded-md border-slate-300">
|
||||||
|
</label>
|
||||||
|
<label class="text-sm">
|
||||||
|
<span class="block text-slate-600 mb-1">WhatsApp</span>
|
||||||
|
<input name="whatsapp" type="text" value="{{ old('whatsapp', $persona?->whatsapp) }}" class="w-full rounded-md border-slate-300">
|
||||||
|
</label>
|
||||||
|
<label class="text-sm">
|
||||||
|
<span class="block text-slate-600 mb-1">Email principale</span>
|
||||||
|
<input name="email_principale" type="email" value="{{ old('email_principale', $persona?->email_principale) }}" class="w-full rounded-md border-slate-300">
|
||||||
|
</label>
|
||||||
|
<label class="text-sm">
|
||||||
|
<span class="block text-slate-600 mb-1">PEC</span>
|
||||||
|
<input name="email_pec" type="email" value="{{ old('email_pec', $persona?->email_pec) }}" class="w-full rounded-md border-slate-300">
|
||||||
|
</label>
|
||||||
|
<label class="text-sm">
|
||||||
|
<span class="block text-slate-600 mb-1">Canale preferito</span>
|
||||||
|
<select name="modalita_comunicazione_preferita" class="w-full rounded-md border-slate-300">
|
||||||
|
@foreach (['email', 'sms', 'whatsapp', 'telefono', 'posta'] as $channel)
|
||||||
|
<option value="{{ $channel }}" @selected(old('modalita_comunicazione_preferita', $persona?->modalita_comunicazione_preferita) === $channel)>{{ ucfirst($channel) }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="text-sm">
|
||||||
|
<span class="block text-slate-600 mb-1">Lingua preferita</span>
|
||||||
|
<input name="lingua_preferita" type="text" value="{{ old('lingua_preferita', $persona?->lingua_preferita ?? 'it') }}" class="w-full rounded-md border-slate-300">
|
||||||
|
</label>
|
||||||
|
<label class="text-sm md:col-span-2 flex items-center gap-2">
|
||||||
|
<input name="consenso_privacy" type="hidden" value="0">
|
||||||
|
<input name="consenso_privacy" type="checkbox" value="1" class="rounded border-slate-300" @checked(old('consenso_privacy', $persona?->consenso_privacy))>
|
||||||
|
<span>Confermo il trattamento dei dati per comunicazioni e gestione condominiale.</span>
|
||||||
|
</label>
|
||||||
|
<label class="text-sm md:col-span-2">
|
||||||
|
<span class="block text-slate-600 mb-1">Note</span>
|
||||||
|
<textarea name="note" rows="3" class="w-full rounded-md border-slate-300">{{ old('note', $persona?->note) }}</textarea>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg bg-white p-4 shadow space-y-4">
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<h2 class="text-lg font-semibold">Email aggiuntive</h2>
|
||||||
|
<p class="text-xs text-slate-500">Puoi indicare più caselle per lavoro, personale, studio o altri recapiti.</p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3">
|
||||||
|
@foreach ($emailRows as $index => $row)
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-[minmax(0,1fr)_180px_120px] gap-3 rounded-lg border border-slate-200 p-3">
|
||||||
|
<input type="hidden" name="email_multiple[{{ $index }}][id]" value="{{ old('email_multiple.' . $index . '.id', $row['id']) }}">
|
||||||
|
<label class="text-sm">
|
||||||
|
<span class="block text-slate-600 mb-1">Email</span>
|
||||||
|
<input name="email_multiple[{{ $index }}][email]" type="email" value="{{ old('email_multiple.' . $index . '.email', $row['email']) }}" class="w-full rounded-md border-slate-300">
|
||||||
|
</label>
|
||||||
|
<label class="text-sm">
|
||||||
|
<span class="block text-slate-600 mb-1">Tipo</span>
|
||||||
|
<input name="email_multiple[{{ $index }}][tipo_email]" type="text" value="{{ old('email_multiple.' . $index . '.tipo_email', $row['tipo_email']) }}" class="w-full rounded-md border-slate-300">
|
||||||
|
</label>
|
||||||
|
<label class="text-sm flex items-end gap-2">
|
||||||
|
<input type="hidden" name="email_multiple[{{ $index }}][attiva]" value="0">
|
||||||
|
<input name="email_multiple[{{ $index }}][attiva]" type="checkbox" value="1" class="rounded border-slate-300" @checked(old('email_multiple.' . $index . '.attiva', $row['attiva']))>
|
||||||
|
<span class="pb-2">Attiva</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg bg-white p-4 shadow space-y-4">
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<h2 class="text-lg font-semibold">Recapiti per servizio e unità</h2>
|
||||||
|
<p class="text-xs text-slate-500">Se compili questi campi, NetGescon userà questi indirizzi come default per la specifica unità e il servizio indicato.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@forelse ($unitaImmobiliari as $unita)
|
||||||
|
@php $matrix = $serviceContactMatrix[$unita->id] ?? []; @endphp
|
||||||
|
<div class="rounded-xl border border-slate-200 p-4 space-y-3">
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold">{{ $unita->stabile->denominazione ?? 'Stabile' }} · {{ $unita->denominazione ?? ('Unità #' . $unita->id) }}</h3>
|
||||||
|
<p class="text-xs text-slate-500">{{ trim(($unita->scala ? 'Scala ' . $unita->scala : '') . ' ' . ($unita->interno ? 'Interno ' . $unita->interno : '')) ?: 'Unità senza dettaglio interno' }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
@foreach ($matrix as $serviceType => $row)
|
||||||
|
<label class="text-sm">
|
||||||
|
<span class="block text-slate-600 mb-1">{{ $row['label'] }}</span>
|
||||||
|
<input name="service_contacts[{{ $unita->id }}][{{ $serviceType }}]" type="email" value="{{ old('service_contacts.' . $unita->id . '.' . $serviceType, $row['value']) }}" class="w-full rounded-md border-slate-300" placeholder="email@dominio.it">
|
||||||
|
@if (! empty($row['resolved']))
|
||||||
|
<span class="mt-1 block text-xs text-slate-500">
|
||||||
|
Attualmente risolto:
|
||||||
|
{{ collect($row['resolved'])->pluck('email')->implode(', ') }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</label>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<p class="text-sm text-slate-500">Nessuna unità collegata al profilo corrente.</p>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-3">
|
||||||
|
<button type="submit" class="inline-flex items-center rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white {{ $persona ? '' : 'opacity-50 cursor-not-allowed' }}" @disabled(! $persona)>Salva aggiornamenti</button>
|
||||||
|
<a href="{{ route('condomino.anagrafe.modulo') }}" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm">Scarica il modulo da firmare o stampare</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -26,6 +26,33 @@
|
||||||
<p class="text-lg font-semibold">Letture acqua</p>
|
<p class="text-lg font-semibold">Letture acqua</p>
|
||||||
<p class="text-sm text-slate-600 mt-1">Invia autoletture con foto e controlla lo storico consumo.</p>
|
<p class="text-sm text-slate-600 mt-1">Invia autoletture con foto e controlla lo storico consumo.</p>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<a href="{{ route('condomino.anagrafe.show') }}" class="rounded-lg bg-white p-5 shadow hover:shadow-md transition block">
|
||||||
|
<p class="text-sm text-slate-500">Anagrafe</p>
|
||||||
|
<p class="text-lg font-semibold">Scheda anagrafica</p>
|
||||||
|
<p class="text-sm text-slate-600 mt-1">Aggiorna recapiti, email multiple e indirizzi di recapito per servizio.</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{{ route('condomino.unita.index') }}" class="rounded-lg bg-white p-5 shadow hover:shadow-md transition block">
|
||||||
|
<p class="text-sm text-slate-500">Unità immobiliari</p>
|
||||||
|
<p class="text-lg font-semibold">Le mie unità</p>
|
||||||
|
<p class="text-sm text-slate-600 mt-1">Consulta dati unità, recapiti attivi e richiedi servizi catastali.</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div class="rounded-lg bg-white p-4 shadow">
|
||||||
|
<p class="text-sm text-slate-500">Unità accessibili</p>
|
||||||
|
<p class="text-2xl font-semibold">{{ $stats['unita_possedute'] ?? 0 }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg bg-white p-4 shadow">
|
||||||
|
<p class="text-sm text-slate-500">Ticket aperti</p>
|
||||||
|
<p class="text-2xl font-semibold">{{ $stats['ticket_aperti'] ?? 0 }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg bg-white p-4 shadow">
|
||||||
|
<p class="text-sm text-slate-500">Recapiti di servizio attivi</p>
|
||||||
|
<p class="text-2xl font-semibold">{{ $serviceContactsCount ?? 0 }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
41
resources/views/condomino/unita/index.blade.php
Normal file
41
resources/views/condomino/unita/index.blade.php
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
<title>Le mie unità - NetGescon</title>
|
||||||
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
|
</head>
|
||||||
|
<body class="min-h-screen bg-slate-100 text-slate-900">
|
||||||
|
<div class="max-w-5xl mx-auto p-4 md:p-8 space-y-4">
|
||||||
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-xl md:text-2xl font-semibold">Le mie unità immobiliari</h1>
|
||||||
|
<p class="text-sm text-slate-600">Consulta le unità collegate al tuo profilo e i relativi recapiti operativi.</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<a href="{{ route('condomino.dashboard') }}" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm">Dashboard</a>
|
||||||
|
<a href="{{ route('condomino.anagrafe.show') }}" class="inline-flex items-center rounded-lg bg-slate-800 px-4 py-2 text-sm text-white">Scheda anagrafica</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@forelse ($unitaImmobiliari as $unita)
|
||||||
|
<div class="rounded-lg bg-white p-4 shadow">
|
||||||
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-semibold">{{ $unita->stabile->denominazione ?? 'Stabile' }} · {{ $unita->denominazione ?? ('Unità #' . $unita->id) }}</h2>
|
||||||
|
<p class="text-sm text-slate-600">
|
||||||
|
{{ trim(($unita->scala ? 'Scala ' . $unita->scala : '') . ' ' . ($unita->piano !== null ? 'Piano ' . $unita->piano : '') . ' ' . ($unita->interno ? 'Interno ' . $unita->interno : '')) ?: 'Posizione non dettagliata' }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-slate-500 mt-1">Foglio {{ $unita->foglio ?: '—' }} · Particella {{ $unita->particella ?: '—' }} · Subalterno {{ $unita->subalterno ?: '—' }}</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('condomino.unita.show', $unita) }}" class="inline-flex items-center rounded-lg border border-slate-300 px-4 py-2 text-sm">Apri scheda</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<div class="rounded-lg bg-white p-4 shadow text-sm text-slate-600">Nessuna unità collegata al tuo account.</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
132
resources/views/condomino/unita/show.blade.php
Normal file
132
resources/views/condomino/unita/show.blade.php
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
<title>Scheda unità - NetGescon</title>
|
||||||
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
|
</head>
|
||||||
|
<body class="min-h-screen bg-slate-100 text-slate-900">
|
||||||
|
<div class="max-w-6xl mx-auto p-4 md:p-8 space-y-4">
|
||||||
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-xl md:text-2xl font-semibold">{{ $unitaImmobiliare->stabile->denominazione ?? 'Stabile' }} · {{ $unitaImmobiliare->denominazione ?? ('Unità #' . $unitaImmobiliare->id) }}</h1>
|
||||||
|
<p class="text-sm text-slate-600">Scheda unità, recapiti effettivi per servizio e richieste di aggiornamento.</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<a href="{{ route('condomino.unita.index') }}" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm">Torna alle unità</a>
|
||||||
|
<a href="{{ route('condomino.anagrafe.show') }}" class="inline-flex items-center rounded-lg bg-slate-800 px-4 py-2 text-sm text-white">Scheda anagrafica</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (session('success'))
|
||||||
|
<div class="rounded-lg border border-emerald-300 bg-emerald-50 p-3 text-sm text-emerald-700">{{ session('success') }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if ($errors->any())
|
||||||
|
<div class="rounded-lg border border-red-300 bg-red-50 p-3 text-sm text-red-700">
|
||||||
|
<ul class="list-disc pl-5">
|
||||||
|
@foreach ($errors->all() as $error)
|
||||||
|
<li>{{ $error }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||||
|
<div class="rounded-lg bg-white p-4 shadow space-y-3">
|
||||||
|
<h2 class="text-lg font-semibold">Dati unità</h2>
|
||||||
|
<dl class="space-y-2 text-sm">
|
||||||
|
<div class="flex items-center justify-between gap-3"><dt class="text-slate-500">Scala / piano / interno</dt><dd>{{ trim(($unitaImmobiliare->scala ? 'Scala ' . $unitaImmobiliare->scala : '') . ' ' . ($unitaImmobiliare->piano !== null ? 'Piano ' . $unitaImmobiliare->piano : '') . ' ' . ($unitaImmobiliare->interno ? 'Interno ' . $unitaImmobiliare->interno : '')) ?: '—' }}</dd></div>
|
||||||
|
<div class="flex items-center justify-between gap-3"><dt class="text-slate-500">Foglio</dt><dd>{{ $unitaImmobiliare->foglio ?: '—' }}</dd></div>
|
||||||
|
<div class="flex items-center justify-between gap-3"><dt class="text-slate-500">Particella</dt><dd>{{ $unitaImmobiliare->particella ?: '—' }}</dd></div>
|
||||||
|
<div class="flex items-center justify-between gap-3"><dt class="text-slate-500">Subalterno</dt><dd>{{ $unitaImmobiliare->subalterno ?: '—' }}</dd></div>
|
||||||
|
<div class="flex items-center justify-between gap-3"><dt class="text-slate-500">Categoria catastale</dt><dd>{{ $unitaImmobiliare->categoria_catastale ?: '—' }}</dd></div>
|
||||||
|
<div class="flex items-center justify-between gap-3"><dt class="text-slate-500">Rendita catastale</dt><dd>{{ $unitaImmobiliare->rendita_catastale ?: '—' }}</dd></div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg bg-white p-4 shadow space-y-3">
|
||||||
|
<h2 class="text-lg font-semibold">Soggetti collegati</h2>
|
||||||
|
@forelse ($unitaImmobiliare->soggetti as $soggetto)
|
||||||
|
<div class="rounded-lg border border-slate-200 p-3 text-sm">
|
||||||
|
<div class="font-medium">{{ $soggetto->nominativo ?? $soggetto->nome ?? ('Soggetto #' . $soggetto->id) }}</div>
|
||||||
|
<div class="text-slate-500">{{ $soggetto->email ?? 'Email non disponibile' }}</div>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<p class="text-sm text-slate-500">Nessun soggetto legacy collegato in modo esplicito.</p>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg bg-white p-4 shadow space-y-4">
|
||||||
|
<h2 class="text-lg font-semibold">Recapiti effettivi per servizio</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
@foreach ($serviceLabels as $serviceType => $label)
|
||||||
|
<div class="rounded-lg border border-slate-200 p-3">
|
||||||
|
<p class="text-sm font-medium">{{ $label }}</p>
|
||||||
|
@php $rows = $serviceRecipients[$serviceType] ?? []; @endphp
|
||||||
|
@if (empty($rows))
|
||||||
|
<p class="mt-1 text-sm text-slate-500">Nessun recapito definito.</p>
|
||||||
|
@else
|
||||||
|
<ul class="mt-2 space-y-1 text-sm text-slate-700">
|
||||||
|
@foreach ($rows as $row)
|
||||||
|
<li>{{ $row['email'] }} @if(! empty($row['persona']))<span class="text-slate-500">· {{ $row['persona'] }}</span>@endif</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||||
|
<div class="rounded-lg bg-white p-4 shadow space-y-3">
|
||||||
|
<h2 class="text-lg font-semibold">Richiesta di aggiornamento</h2>
|
||||||
|
<form method="POST" action="{{ route('condomino.unita.richiesta-modifica', $unitaImmobiliare) }}" class="space-y-3">
|
||||||
|
@csrf
|
||||||
|
<label class="text-sm block">
|
||||||
|
<span class="block text-slate-600 mb-1">Tipo richiesta</span>
|
||||||
|
<select name="tipo_modifica" class="w-full rounded-md border-slate-300">
|
||||||
|
<option value="anagrafica">Aggiornamento anagrafico</option>
|
||||||
|
<option value="catastale">Aggiornamento dati catastali</option>
|
||||||
|
<option value="proprieta">Aggiornamento proprietà / diritti</option>
|
||||||
|
<option value="catastale_servizio">Servizio catastale a pagamento</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="text-sm block">
|
||||||
|
<span class="block text-slate-600 mb-1">Descrizione</span>
|
||||||
|
<textarea name="descrizione" rows="4" class="w-full rounded-md border-slate-300" required>{{ old('descrizione') }}</textarea>
|
||||||
|
</label>
|
||||||
|
<label class="text-sm block">
|
||||||
|
<span class="block text-slate-600 mb-1">Dati proposti / note operative</span>
|
||||||
|
<textarea name="dati_proposti[note]" rows="3" class="w-full rounded-md border-slate-300">{{ old('dati_proposti.note') }}</textarea>
|
||||||
|
</label>
|
||||||
|
<button type="submit" class="inline-flex items-center rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white">Invia richiesta</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg border border-amber-300 bg-amber-50 p-4 shadow-sm space-y-3">
|
||||||
|
<h2 class="text-lg font-semibold text-amber-900">Servizio aggiuntivo: aggiornamento catastale</h2>
|
||||||
|
<p class="text-sm text-amber-800">Puoi richiedere l'avvio del servizio a pagamento per aggiornare la posizione catastale dell'unità. La richiesta viene registrata e presa in carico dallo studio.</p>
|
||||||
|
<form method="POST" action="{{ route('condomino.unita.richiesta-modifica', $unitaImmobiliare) }}" class="space-y-3">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="tipo_modifica" value="catastale_servizio">
|
||||||
|
<input type="hidden" name="dati_proposti[servizio]" value="aggiornamento_posizione_catastale">
|
||||||
|
<input type="hidden" name="dati_proposti[servizio_a_pagamento]" value="1">
|
||||||
|
<label class="text-sm block">
|
||||||
|
<span class="block text-amber-900 mb-1">Descrivi cosa va verificato</span>
|
||||||
|
<textarea name="descrizione" rows="4" class="w-full rounded-md border-amber-300" required>{{ old('descrizione') }}</textarea>
|
||||||
|
</label>
|
||||||
|
<label class="text-sm block">
|
||||||
|
<span class="block text-amber-900 mb-1">Email per contatto sul servizio</span>
|
||||||
|
<input name="dati_proposti[email_recapito]" type="email" value="{{ old('dati_proposti.email_recapito', auth()->user()->email) }}" class="w-full rounded-md border-amber-300">
|
||||||
|
</label>
|
||||||
|
<button type="submit" class="inline-flex items-center rounded-lg bg-amber-600 px-4 py-2 text-sm font-medium text-white">Richiedi servizio catastale</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -40,10 +40,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
@if($user instanceof \App\Models\User && $user->hasAnyRole(['super-admin','admin','amministratore','collaboratore']))
|
|
||||||
<a href="{{ route('admin.dashboard') }}" class="text-primary-600 hover:underline">Apri UI classica</a>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
<a href="{{ \App\Filament\Pages\Supporto\RichiestaHelp::getUrl(panel: 'admin-filament') }}" class="text-primary-600 hover:underline">
|
<a href="{{ \App\Filament\Pages\Supporto\RichiestaHelp::getUrl(panel: 'admin-filament') }}" class="text-primary-600 hover:underline">
|
||||||
Richiedi help / segnala bug
|
Richiedi help / segnala bug
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,201 @@
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="rounded-xl border bg-white p-4">
|
||||||
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-semibold">Modulo assicurazione stabile</div>
|
||||||
|
<div class="mt-1 text-xs text-gray-500">Storico polizze, clausole, PDF, firma immagine e collegamento dei sinistri ai ticket.</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button type="button" wire:click="resetInsurancePolicyForm" class="inline-flex items-center rounded-md bg-slate-100 px-3 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-200">Nuova polizza</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-4 xl:grid-cols-5">
|
||||||
|
<div class="rounded-xl border bg-white p-4 xl:col-span-2">
|
||||||
|
<div class="text-sm font-semibold">Elenco polizze</div>
|
||||||
|
<div class="mt-3 space-y-3">
|
||||||
|
@forelse($this->insurancePolicies as $policy)
|
||||||
|
<div class="rounded-lg border p-3 text-sm {{ (int) ($this->selectedInsurancePolicyId ?? 0) === (int) $policy->id ? 'border-primary-500 bg-primary-50' : 'border-slate-200 bg-white' }}">
|
||||||
|
<div class="flex flex-wrap items-start justify-between gap-2">
|
||||||
|
<div>
|
||||||
|
<div class="font-medium">{{ $policy->display_name }}</div>
|
||||||
|
<div class="mt-1 text-xs text-gray-500">{{ $policy->policy_reference ?: 'Riferimento non indicato' }}</div>
|
||||||
|
<div class="mt-1 text-xs text-gray-500">Scadenza {{ optional($policy->expires_at)->format('d/m/Y') ?: '-' }} · Rinnovo {{ optional($policy->renewal_at)->format('d/m/Y') ?: '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button type="button" wire:click="selectInsurancePolicy({{ (int) $policy->id }})" class="inline-flex items-center rounded-md bg-slate-800 px-2 py-1 text-[11px] font-medium text-white hover:bg-slate-700">Apri</button>
|
||||||
|
<button type="button" wire:click="deleteInsurancePolicy({{ (int) $policy->id }})" class="inline-flex items-center rounded-md bg-rose-600 px-2 py-1 text-[11px] font-medium text-white hover:bg-rose-500">Elimina</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 flex flex-wrap gap-2 text-[11px] text-gray-600">
|
||||||
|
<span>Stato: {{ $policy->status ?: '-' }}</span>
|
||||||
|
<span>Premio annuo: {{ $policy->annual_amount !== null ? number_format((float) $policy->annual_amount, 2, ',', '.') . ' EUR' : '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<div class="rounded-lg border border-dashed p-4 text-sm text-gray-500">Nessuna polizza registrata per questo stabile.</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border bg-white p-4 xl:col-span-3">
|
||||||
|
<div class="text-sm font-semibold">Scheda polizza</div>
|
||||||
|
<div class="mt-4 grid gap-3 md:grid-cols-2">
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Broker / referente polizza</span>
|
||||||
|
<select wire:model.defer="insurancePolicyForm.broker_rubrica_id" class="w-full rounded-lg border-gray-300">
|
||||||
|
<option value="">Seleziona contatto</option>
|
||||||
|
@foreach($this->insuranceRubricaOptions as $id => $label)
|
||||||
|
<option value="{{ $id }}">{{ $label }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Compagnia assicurativa</span>
|
||||||
|
<select wire:model.defer="insurancePolicyForm.company_rubrica_id" class="w-full rounded-lg border-gray-300">
|
||||||
|
<option value="">Seleziona compagnia</option>
|
||||||
|
@foreach($this->insuranceRubricaOptions as $id => $label)
|
||||||
|
<option value="{{ $id }}">{{ $label }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Nome polizza</span>
|
||||||
|
<input type="text" wire:model.defer="insurancePolicyForm.policy_name" class="w-full rounded-lg border-gray-300" placeholder="Fabbricato globale" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Numero polizza</span>
|
||||||
|
<input type="text" wire:model.defer="insurancePolicyForm.policy_number" class="w-full rounded-lg border-gray-300" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm md:col-span-2">
|
||||||
|
<span class="mb-1 block font-medium">Riferimento / riferimento di pagamento</span>
|
||||||
|
<input type="text" wire:model.defer="insurancePolicyForm.policy_reference" class="w-full rounded-lg border-gray-300" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Stato</span>
|
||||||
|
<select wire:model.defer="insurancePolicyForm.status" class="w-full rounded-lg border-gray-300">
|
||||||
|
<option value="attiva">Attiva</option>
|
||||||
|
<option value="in_scadenza">In scadenza</option>
|
||||||
|
<option value="scaduta">Scaduta</option>
|
||||||
|
<option value="sostituita">Sostituita</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Premio annuo</span>
|
||||||
|
<input type="number" step="0.01" wire:model.defer="insurancePolicyForm.annual_amount" class="w-full rounded-lg border-gray-300" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Decorrenza</span>
|
||||||
|
<input type="date" wire:model.defer="insurancePolicyForm.started_at" class="w-full rounded-lg border-gray-300" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Scadenza</span>
|
||||||
|
<input type="date" wire:model.defer="insurancePolicyForm.expires_at" class="w-full rounded-lg border-gray-300" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Rinnovo</span>
|
||||||
|
<input type="date" wire:model.defer="insurancePolicyForm.renewal_at" class="w-full rounded-lg border-gray-300" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Come viene diviso il pagamento</span>
|
||||||
|
<input type="text" wire:model.defer="insurancePolicyForm.payment_split" class="w-full rounded-lg border-gray-300" placeholder="Annuale, semestrale, rateizzato" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm md:col-span-2">
|
||||||
|
<span class="mb-1 block font-medium">Note pagamenti / quietanze</span>
|
||||||
|
<textarea wire:model.defer="insurancePolicyForm.payment_schedule_notes" rows="3" class="w-full rounded-lg border-gray-300" placeholder="Quietanze pagate, riferimenti contabili, scadenze rate"></textarea>
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm md:col-span-2">
|
||||||
|
<span class="mb-1 block font-medium">Garanzie principali</span>
|
||||||
|
<textarea wire:model.defer="insurancePolicyForm.coverage_summary" rows="4" class="w-full rounded-lg border-gray-300" placeholder="Riassunto garanzie, massimali, franchigie"></textarea>
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm md:col-span-2">
|
||||||
|
<span class="mb-1 block font-medium">Clausole e condizioni particolari</span>
|
||||||
|
<textarea wire:model.defer="insurancePolicyForm.special_conditions" rows="6" class="w-full rounded-lg border-gray-300" placeholder="Puoi incollare qui solo le clausole essenziali"></textarea>
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm md:col-span-2">
|
||||||
|
<span class="mb-1 block font-medium">Template richiesta apertura sinistro</span>
|
||||||
|
<textarea wire:model.defer="insurancePolicyForm.claim_form_template" rows="5" class="w-full rounded-lg border-gray-300" placeholder="Base per il modulo di apertura sinistro con intestazione amministratore e riferimenti polizza"></textarea>
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm md:col-span-2">
|
||||||
|
<span class="mb-1 block font-medium">Note operative</span>
|
||||||
|
<textarea wire:model.defer="insurancePolicyForm.notes" rows="4" class="w-full rounded-lg border-gray-300" placeholder="Perito, procedura, raccolta documenti, eventuale link protetto"></textarea>
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">PDF polizza</span>
|
||||||
|
<input type="file" wire:model="insurancePolicyDocumentUpload" accept="application/pdf,image/*" class="w-full rounded-lg border-gray-300 text-sm" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Firma immagine</span>
|
||||||
|
<input type="file" wire:model="insuranceSignatureUpload" accept="image/*" class="w-full rounded-lg border-gray-300 text-sm" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(($selectedPolicy = $this->insurancePolicies->firstWhere('id', (int) ($this->selectedInsurancePolicyId ?? 0))) instanceof \App\Models\InsurancePolicy)
|
||||||
|
<div class="mt-4 grid gap-3 md:grid-cols-2">
|
||||||
|
<div class="rounded-lg border bg-slate-50 p-3 text-xs text-slate-700">
|
||||||
|
<div class="font-medium">Documento polizza</div>
|
||||||
|
@if($this->getInsuranceDocumentUrl($selectedPolicy))
|
||||||
|
<a href="{{ $this->getInsuranceDocumentUrl($selectedPolicy) }}" target="_blank" class="mt-2 inline-flex text-primary-600 hover:underline">Apri PDF / allegato polizza</a>
|
||||||
|
@else
|
||||||
|
<div class="mt-2">Nessun documento caricato.</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg border bg-slate-50 p-3 text-xs text-slate-700">
|
||||||
|
<div class="font-medium">Firma immagine</div>
|
||||||
|
@if($this->getInsuranceSignatureUrl($selectedPolicy))
|
||||||
|
<img src="{{ $this->getInsuranceSignatureUrl($selectedPolicy) }}" alt="Firma" class="mt-2 max-h-24 rounded border bg-white p-1" />
|
||||||
|
@else
|
||||||
|
<div class="mt-2">Nessuna firma immagine caricata.</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="mt-4 flex flex-wrap gap-2">
|
||||||
|
<button type="button" wire:click="saveInsurancePolicy" class="inline-flex items-center rounded-md bg-primary-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-primary-500">Salva polizza</button>
|
||||||
|
<button type="button" wire:click="resetInsurancePolicyForm" class="inline-flex items-center rounded-md bg-slate-100 px-3 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-200">Pulisci scheda</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border bg-white p-4">
|
||||||
|
<div class="text-sm font-semibold">Sinistri collegati ai ticket</div>
|
||||||
|
<div class="mt-1 text-xs text-gray-500">Separati dalla sola scheda ticket: qui rimangono tracciati per polizza e per stabile.</div>
|
||||||
|
<div class="mt-3 overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200 text-sm">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-3 py-2 text-left font-medium text-gray-600">Apertura</th>
|
||||||
|
<th class="px-3 py-2 text-left font-medium text-gray-600">Polizza</th>
|
||||||
|
<th class="px-3 py-2 text-left font-medium text-gray-600">Sinistro</th>
|
||||||
|
<th class="px-3 py-2 text-left font-medium text-gray-600">Ticket</th>
|
||||||
|
<th class="px-3 py-2 text-left font-medium text-gray-600">Note</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-100 bg-white">
|
||||||
|
@forelse($this->insuranceClaimsRows as $claim)
|
||||||
|
<tr>
|
||||||
|
<td class="px-3 py-2 align-top">{{ optional($claim->opened_at)->format('d/m/Y H:i') ?: '-' }}</td>
|
||||||
|
<td class="px-3 py-2 align-top">{{ $claim->insurancePolicy?->display_name ?: ($claim->policy_reference ?: '-') }}</td>
|
||||||
|
<td class="px-3 py-2 align-top">{{ $claim->claim_number ?: '-' }}<div class="text-xs text-gray-500">{{ $claim->status ?: 'aperta' }}</div></td>
|
||||||
|
<td class="px-3 py-2 align-top">
|
||||||
|
@if($claim->ticket)
|
||||||
|
<a href="{{ $this->getInsuranceTicketUrl($claim->ticket) }}" class="text-primary-600 hover:underline">Ticket #{{ (int) $claim->ticket->id }}</a>
|
||||||
|
<div class="text-xs text-gray-500">{{ $claim->ticket->titolo ?: '-' }}</div>
|
||||||
|
@else
|
||||||
|
-
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-2 align-top whitespace-pre-wrap text-xs text-gray-700">{{ $claim->notes ?: '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="px-3 py-6 text-center text-sm text-gray-500">Nessun sinistro registrato per questo stabile.</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
86
resources/views/filament/pages/fiscale/cruscotto.blade.php
Normal file
86
resources/views/filament/pages/fiscale/cruscotto.blade.php
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
<x-filament-panels::page>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<section class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||||
|
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-semibold text-slate-900">Archivio fiscale condominiale</h2>
|
||||||
|
<p class="mt-1 text-sm text-slate-600">
|
||||||
|
@if($this->stabileAttivo)
|
||||||
|
Stabile attivo: <span class="font-medium">{{ $this->stabileAttivo->denominazione }}</span> · anno fiscale {{ $this->annoFiscale }}
|
||||||
|
@else
|
||||||
|
Seleziona uno stabile attivo per vedere ritenute, certificazioni e scadenze fiscali.
|
||||||
|
@endif
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<a href="{{ \App\Filament\Pages\Fiscale\FiscaleDashboard::getRegistroRaUrl() }}" class="inline-flex items-center rounded-lg bg-slate-900 px-4 py-2 text-sm font-medium text-white">Registro RA</a>
|
||||||
|
<a href="{{ \App\Filament\Pages\Contabilita\GestioneStraordinaria::getUrl(panel: 'admin-filament') }}" class="inline-flex items-center rounded-lg border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700">Straordinarie</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||||
|
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||||
|
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Ritenute</div>
|
||||||
|
<div class="mt-2 text-3xl font-semibold text-slate-900">{{ $summary['ritenute_count'] ?? 0 }}</div>
|
||||||
|
<p class="mt-2 text-sm text-slate-600">Registrazioni incluse nell'anno fiscale selezionato.</p>
|
||||||
|
<div class="mt-3 text-xs text-amber-700">Da versare: {{ $summary['ritenute_da_versare'] ?? 0 }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||||
|
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Detrazioni</div>
|
||||||
|
<div class="mt-2 text-3xl font-semibold text-slate-900">{{ $summary['detrazioni_count'] ?? 0 }}</div>
|
||||||
|
<p class="mt-2 text-sm text-slate-600">Certificazioni e posizioni fiscali da lavori agevolati.</p>
|
||||||
|
<div class="mt-3 text-xs text-slate-500">Dichiarazioni fiscali da straordinarie: {{ $summary['straordinarie_fiscali'] ?? 0 }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||||
|
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Adempimenti</div>
|
||||||
|
<div class="mt-2 text-3xl font-semibold text-slate-900">{{ $summary['adempimenti_count'] ?? 0 }}</div>
|
||||||
|
<p class="mt-2 text-sm text-slate-600">Archivio operativo fiscale del condominio.</p>
|
||||||
|
<div class="mt-3 text-xs text-rose-700">Aperti o da completare: {{ $summary['adempimenti_aperti'] ?? 0 }}</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="grid gap-4 xl:grid-cols-[1.1fr_0.9fr]">
|
||||||
|
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||||
|
<h3 class="text-base font-semibold text-slate-900">Scadenze e archivio</h3>
|
||||||
|
<div class="mt-4 space-y-3">
|
||||||
|
@forelse($adempimentiInScadenza as $adempimento)
|
||||||
|
<article class="rounded-xl border border-slate-200 p-4">
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-semibold text-slate-900">{{ $adempimento->titolo }}</div>
|
||||||
|
<div class="mt-1 text-xs uppercase tracking-wide text-slate-500">{{ str_replace('_', ' ', $adempimento->tipo) }}</div>
|
||||||
|
</div>
|
||||||
|
<span class="rounded-full bg-slate-100 px-2 py-1 text-[11px] text-slate-600">{{ $adempimento->stato }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-sm text-slate-600">{{ $adempimento->descrizione ?: 'Nessuna descrizione operativa.' }}</div>
|
||||||
|
<div class="mt-3 text-xs text-slate-500">
|
||||||
|
Anno fiscale: {{ $adempimento->anno_fiscale ?: 'n.d.' }}
|
||||||
|
@if($adempimento->scadenza)
|
||||||
|
· Scadenza {{ $adempimento->scadenza->format('d/m/Y') }}
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
@empty
|
||||||
|
<div class="rounded-xl border border-dashed border-slate-300 p-5 text-sm text-slate-500">
|
||||||
|
Nessun adempimento archiviato. Il menu Fiscale e` pronto per raccogliere ritenute, certificazioni, Quadro AC, 770 e compensi amministratore.
|
||||||
|
</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||||
|
<h3 class="text-base font-semibold text-slate-900">Struttura iniziale</h3>
|
||||||
|
<div class="mt-4 space-y-3">
|
||||||
|
@foreach($templates as $template)
|
||||||
|
<article class="rounded-xl bg-slate-50 p-4">
|
||||||
|
<div class="text-sm font-semibold text-slate-900">{{ $template['titolo'] }}</div>
|
||||||
|
<div class="mt-1 text-xs uppercase tracking-wide text-slate-500">{{ str_replace('_', ' ', $template['tipo']) }}</div>
|
||||||
|
<p class="mt-2 text-sm text-slate-600">{{ $template['descrizione'] }}</p>
|
||||||
|
</article>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</x-filament-panels::page>
|
||||||
|
|
@ -24,75 +24,162 @@
|
||||||
Questa vista per amministratori richiede un fornitore selezionato.
|
Questa vista per amministratori richiede un fornitore selezionato.
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<div class="grid gap-4 xl:grid-cols-5">
|
<div class="rounded-xl border bg-white p-1">
|
||||||
<div class="rounded-xl border bg-white p-4 xl:col-span-2">
|
<div class="flex flex-wrap gap-2 border-b px-3 py-3">
|
||||||
<div class="text-sm font-semibold">Nuovo collaboratore</div>
|
<button type="button" wire:click="$set('activeTab', 'elenco')" class="inline-flex items-center rounded-lg px-3 py-1.5 text-sm font-medium {{ $activeTab === 'elenco' ? 'bg-gray-900 text-white' : 'bg-white text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50' }}">Elenco collaboratori</button>
|
||||||
<div class="mt-1 text-xs text-gray-500">Puoi registrare sia un collaboratore interno sia un altro fornitore da usare come subfornitore operativo.</div>
|
<button type="button" wire:click="$set('activeTab', 'nuovo')" class="inline-flex items-center rounded-lg px-3 py-1.5 text-sm font-medium {{ $activeTab === 'nuovo' ? 'bg-primary-600 text-white' : 'bg-white text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50' }}">Aggiungi collaboratore</button>
|
||||||
<div class="mt-4 grid gap-3">
|
|
||||||
<label class="block text-sm">
|
|
||||||
<span class="mb-1 block font-medium">Tipo collaboratore</span>
|
|
||||||
<select wire:model.live="collaboratoreTipo" class="w-full rounded-lg border-gray-300">
|
|
||||||
<option value="interno">Collaboratore interno</option>
|
|
||||||
<option value="fornitore_esterno">Altro fornitore / subfornitore</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
@if($collaboratoreTipo === 'fornitore_esterno')
|
|
||||||
<label class="block text-sm">
|
|
||||||
<span class="mb-1 block font-medium">Cerca fornitore</span>
|
|
||||||
<input type="text" wire:model.live.debounce.300ms="fornitoreSearch" class="w-full rounded-lg border-gray-300" placeholder="Ragione sociale, telefono, email, tag" />
|
|
||||||
</label>
|
|
||||||
<label class="block text-sm">
|
|
||||||
<span class="mb-1 block font-medium">Fornitore da collegare</span>
|
|
||||||
<select wire:model="fornitoreEsternoId" class="w-full rounded-lg border-gray-300">
|
|
||||||
<option value="">Seleziona un fornitore</option>
|
|
||||||
@foreach($fornitoreOptions as $option)
|
|
||||||
<option value="{{ $option['id'] }}">{{ $option['label'] }}{{ $option['meta'] !== '' ? ' · ' . $option['meta'] : '' }}</option>
|
|
||||||
@endforeach
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
@else
|
|
||||||
<label class="block text-sm">
|
|
||||||
<span class="mb-1 block font-medium">Nome</span>
|
|
||||||
<input type="text" wire:model.defer="nome" class="w-full rounded-lg border-gray-300" />
|
|
||||||
</label>
|
|
||||||
<label class="block text-sm">
|
|
||||||
<span class="mb-1 block font-medium">Cognome</span>
|
|
||||||
<input type="text" wire:model.defer="cognome" class="w-full rounded-lg border-gray-300" />
|
|
||||||
</label>
|
|
||||||
<label class="block text-sm">
|
|
||||||
<span class="mb-1 block font-medium">Email</span>
|
|
||||||
<input type="email" wire:model.defer="email" class="w-full rounded-lg border-gray-300" />
|
|
||||||
</label>
|
|
||||||
<label class="block text-sm">
|
|
||||||
<span class="mb-1 block font-medium">Telefono</span>
|
|
||||||
<input type="text" wire:model.defer="telefono" class="w-full rounded-lg border-gray-300" />
|
|
||||||
</label>
|
|
||||||
@endif
|
|
||||||
<label class="block text-sm">
|
|
||||||
<span class="mb-1 block font-medium">Note</span>
|
|
||||||
<textarea wire:model.defer="note" rows="3" class="w-full rounded-lg border-gray-300"></textarea>
|
|
||||||
</label>
|
|
||||||
@if($collaboratoreTipo === 'interno')
|
|
||||||
<label class="inline-flex items-center gap-2 text-sm">
|
|
||||||
<input type="checkbox" wire:model="createUserAccess" class="rounded border-gray-300" />
|
|
||||||
<span>Crea o collega account utente</span>
|
|
||||||
</label>
|
|
||||||
<label class="block text-sm">
|
|
||||||
<span class="mb-1 block font-medium">Password accesso</span>
|
|
||||||
<input type="text" wire:model.defer="accessPassword" class="w-full rounded-lg border-gray-300" placeholder="Minimo 8 caratteri" />
|
|
||||||
</label>
|
|
||||||
@else
|
|
||||||
<div class="rounded-lg border border-sky-200 bg-sky-50 px-3 py-2 text-xs text-sky-800">
|
|
||||||
Il subfornitore usa il suo account esistente. Qui salvi solo il collegamento operativo.
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
<x-filament::button wire:click="createCollaboratore">Salva collaboratore</x-filament::button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-xl border bg-white p-0 xl:col-span-3">
|
@if($activeTab === 'nuovo')
|
||||||
<div class="border-b px-4 py-3 text-sm font-semibold">Elenco collaboratori</div>
|
<div class="p-4">
|
||||||
|
<div class="text-sm font-semibold">Nuovo collaboratore</div>
|
||||||
|
<div class="mt-1 text-xs text-gray-500">Scheda semplificata per il fornitore, con salvataggio anagrafico su rubrica universale per i collaboratori interni.</div>
|
||||||
|
<div class="mt-4 grid gap-4 lg:grid-cols-2">
|
||||||
|
<div class="grid gap-3">
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Tipo collaboratore</span>
|
||||||
|
<select wire:model.live="collaboratoreTipo" class="w-full rounded-lg border-gray-300">
|
||||||
|
<option value="interno">Collaboratore interno</option>
|
||||||
|
<option value="fornitore_esterno">Altro fornitore / subfornitore</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
@if($collaboratoreTipo === 'fornitore_esterno')
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Cerca fornitore</span>
|
||||||
|
<input type="text" wire:model.live.debounce.300ms="fornitoreSearch" class="w-full rounded-lg border-gray-300" placeholder="Ragione sociale, telefono, email, tag" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Fornitore da collegare</span>
|
||||||
|
<select wire:model="fornitoreEsternoId" class="w-full rounded-lg border-gray-300">
|
||||||
|
<option value="">Seleziona un fornitore</option>
|
||||||
|
@foreach($fornitoreOptions as $option)
|
||||||
|
<option value="{{ $option['id'] }}">{{ $option['label'] }}{{ $option['meta'] !== '' ? ' · ' . $option['meta'] : '' }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<div class="rounded-lg border border-sky-200 bg-sky-50 px-3 py-2 text-xs text-sky-800">
|
||||||
|
Il subfornitore usa il suo account esistente. Qui salvi solo il collegamento operativo.
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="grid gap-3 md:grid-cols-2">
|
||||||
|
<label class="block text-sm md:col-span-2">
|
||||||
|
<span class="mb-1 block font-medium">Tipo contatto</span>
|
||||||
|
<select wire:model.live="tipoContatto" class="w-full rounded-lg border-gray-300">
|
||||||
|
<option value="persona_fisica">Persona fisica</option>
|
||||||
|
<option value="persona_giuridica">Persona giuridica</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
@if($tipoContatto === 'persona_giuridica')
|
||||||
|
<label class="block text-sm md:col-span-2">
|
||||||
|
<span class="mb-1 block font-medium">Ragione sociale</span>
|
||||||
|
<input type="text" wire:model.defer="ragioneSociale" class="w-full rounded-lg border-gray-300" />
|
||||||
|
</label>
|
||||||
|
@else
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Nome</span>
|
||||||
|
<input type="text" wire:model.defer="nome" class="w-full rounded-lg border-gray-300" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Cognome</span>
|
||||||
|
<input type="text" wire:model.defer="cognome" class="w-full rounded-lg border-gray-300" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Sesso</span>
|
||||||
|
<select wire:model.defer="sesso" class="w-full rounded-lg border-gray-300">
|
||||||
|
<option value="">Non specificato</option>
|
||||||
|
<option value="M">Maschile</option>
|
||||||
|
<option value="F">Femminile</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Data nascita</span>
|
||||||
|
<input type="date" wire:model.defer="dataNascita" class="w-full rounded-lg border-gray-300" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Luogo nascita</span>
|
||||||
|
<input type="text" wire:model.defer="luogoNascita" class="w-full rounded-lg border-gray-300" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Provincia nascita</span>
|
||||||
|
<input type="text" wire:model.defer="provinciaNascita" class="w-full rounded-lg border-gray-300" maxlength="10" />
|
||||||
|
</label>
|
||||||
|
@endif
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Codice fiscale</span>
|
||||||
|
<input type="text" wire:model.defer="codiceFiscale" class="w-full rounded-lg border-gray-300" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Partita IVA</span>
|
||||||
|
<input type="text" wire:model.defer="partitaIva" class="w-full rounded-lg border-gray-300" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Email</span>
|
||||||
|
<input type="email" wire:model.defer="email" class="w-full rounded-lg border-gray-300" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">PEC</span>
|
||||||
|
<input type="text" wire:model.defer="pec" class="w-full rounded-lg border-gray-300" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Telefono principale</span>
|
||||||
|
<input type="text" wire:model.defer="telefono" class="w-full rounded-lg border-gray-300" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm md:col-span-2">
|
||||||
|
<span class="mb-1 block font-medium">Indirizzo</span>
|
||||||
|
<input type="text" wire:model.defer="indirizzo" class="w-full rounded-lg border-gray-300" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Civico</span>
|
||||||
|
<input type="text" wire:model.defer="civico" class="w-full rounded-lg border-gray-300" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">CAP</span>
|
||||||
|
<input type="text" wire:model.defer="cap" class="w-full rounded-lg border-gray-300" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Citta</span>
|
||||||
|
<input type="text" wire:model.defer="citta" class="w-full rounded-lg border-gray-300" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Provincia</span>
|
||||||
|
<input type="text" wire:model.defer="provincia" class="w-full rounded-lg border-gray-300" maxlength="10" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm md:col-span-2">
|
||||||
|
<span class="mb-1 block font-medium">Nazione</span>
|
||||||
|
<input type="text" wire:model.defer="nazione" class="w-full rounded-lg border-gray-300" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-3">
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Note</span>
|
||||||
|
<textarea wire:model.defer="note" rows="5" class="w-full rounded-lg border-gray-300"></textarea>
|
||||||
|
</label>
|
||||||
|
@if($collaboratoreTipo === 'interno')
|
||||||
|
<label class="inline-flex items-center gap-2 text-sm">
|
||||||
|
<input type="checkbox" wire:model="createUserAccess" class="rounded border-gray-300" />
|
||||||
|
<span>Crea o collega account utente</span>
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Password accesso</span>
|
||||||
|
<input type="text" wire:model.defer="accessPassword" class="w-full rounded-lg border-gray-300" placeholder="Minimo 8 caratteri" />
|
||||||
|
</label>
|
||||||
|
<div class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-xs text-gray-600">
|
||||||
|
Per i collaboratori interni viene creata o aggiornata anche la relativa scheda in rubrica, senza sezione conti bancari.
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
<div class="pt-2">
|
||||||
|
<x-filament::button wire:click="createCollaboratore">Salva collaboratore</x-filament::button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="p-0">
|
||||||
|
<div class="border-b px-4 py-3 text-sm font-semibold">Elenco collaboratori</div>
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="min-w-full divide-y divide-gray-200 text-sm">
|
<table class="min-w-full divide-y divide-gray-200 text-sm">
|
||||||
<thead class="bg-gray-50">
|
<thead class="bg-gray-50">
|
||||||
|
|
@ -130,6 +217,9 @@
|
||||||
@else
|
@else
|
||||||
<span class="inline-flex rounded-full bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700">Nessun account</span>
|
<span class="inline-flex rounded-full bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700">Nessun account</span>
|
||||||
@endif
|
@endif
|
||||||
|
@if($row['rubrica_id'])
|
||||||
|
<div class="mt-1 text-xs text-gray-500">Rubrica #{{ $row['rubrica_id'] }}</div>
|
||||||
|
@endif
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 align-top">
|
<td class="px-4 py-3 align-top">
|
||||||
<span class="inline-flex rounded-full px-2 py-1 text-xs font-medium {{ $row['attivo'] ? 'bg-primary-100 text-primary-700' : 'bg-gray-100 text-gray-700' }}">{{ $row['attivo'] ? 'Attivo' : 'Disattivo' }}</span>
|
<span class="inline-flex rounded-full px-2 py-1 text-xs font-medium {{ $row['attivo'] ? 'bg-primary-100 text-primary-700' : 'bg-gray-100 text-gray-700' }}">{{ $row['attivo'] ? 'Attivo' : 'Disattivo' }}</span>
|
||||||
|
|
@ -155,7 +245,8 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,20 @@
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-xs uppercase tracking-wide text-gray-500">Email</div>
|
||||||
|
<div class="mt-1 text-sm">
|
||||||
|
@if($caller['email'] !== '')
|
||||||
|
<a href="mailto:{{ $caller['email'] }}" class="text-primary-600 hover:underline">{{ $caller['email'] }}</a>
|
||||||
|
@else
|
||||||
|
-
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-xs uppercase tracking-wide text-gray-500">Origine dato</div>
|
||||||
|
<div class="mt-1 text-sm">{{ $caller['sorgente'] !== '' ? $caller['sorgente'] : '-' }}</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs uppercase tracking-wide text-gray-500">Stato intervento</div>
|
<div class="text-xs uppercase tracking-wide text-gray-500">Stato intervento</div>
|
||||||
<div class="mt-1 text-sm">{{ $this->intervento->stato }}</div>
|
<div class="mt-1 text-sm">{{ $this->intervento->stato }}</div>
|
||||||
|
|
@ -43,6 +57,10 @@
|
||||||
<div class="text-xs uppercase tracking-wide text-gray-500">Operatore</div>
|
<div class="text-xs uppercase tracking-wide text-gray-500">Operatore</div>
|
||||||
<div class="mt-1 text-sm">{{ $this->intervento->operatore_assegnato_label }}</div>
|
<div class="mt-1 text-sm">{{ $this->intervento->operatore_assegnato_label }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<div class="text-xs uppercase tracking-wide text-gray-500">Riferimento stabile</div>
|
||||||
|
<div class="mt-1 text-sm">{{ $caller['riferimento'] !== '' ? $caller['riferimento'] : '-' }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 whitespace-pre-wrap rounded-lg bg-gray-50 p-3 text-sm text-gray-700">{{ $ticket->descrizione }}</div>
|
<div class="mt-4 whitespace-pre-wrap rounded-lg bg-gray-50 p-3 text-sm text-gray-700">{{ $ticket->descrizione }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -202,10 +220,11 @@
|
||||||
<div class="rounded-lg border p-3 text-sm">
|
<div class="rounded-lg border p-3 text-sm">
|
||||||
<div class="font-medium">{{ $attachment->original_file_name }}</div>
|
<div class="font-medium">{{ $attachment->original_file_name }}</div>
|
||||||
<div class="text-xs text-gray-500">{{ $attachment->description ?: 'Nessuna descrizione' }}</div>
|
<div class="text-xs text-gray-500">{{ $attachment->description ?: 'Nessuna descrizione' }}</div>
|
||||||
|
<div class="mt-1 text-xs text-gray-500">Caricato da {{ $attachment->user->name ?? 'Sistema' }} il {{ optional($attachment->created_at)->format('d/m/Y H:i') ?: '-' }}</div>
|
||||||
<a href="{{ route('filament.tickets.attachments.view', ['attachment' => (int) $attachment->id]) }}" target="_blank" class="mt-2 inline-flex text-xs font-medium text-primary-600 hover:underline">Apri allegato</a>
|
<a href="{{ route('filament.tickets.attachments.view', ['attachment' => (int) $attachment->id]) }}" target="_blank" class="mt-2 inline-flex text-xs font-medium text-primary-600 hover:underline">Apri allegato</a>
|
||||||
</div>
|
</div>
|
||||||
@empty
|
@empty
|
||||||
<div class="text-sm text-gray-500">Nessun allegato disponibile.</div>
|
<div class="text-sm text-gray-500">Nessun allegato disponibile. Se il ticket e stato creato senza file persistiti, qui non verra mostrato nulla finche non vengono caricati allegati reali.</div>
|
||||||
@endforelse
|
@endforelse
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-semibold text-gray-900">{{ $stabile->codice_stabile }} · {{ $stabile->denominazione }}</div>
|
||||||
|
<div class="text-xs text-gray-500">Cronologia dei passaggi amministratore registrati dal portale.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($transfers->isEmpty())
|
||||||
|
<div class="rounded-lg border border-dashed border-gray-300 px-4 py-6 text-sm text-gray-500">
|
||||||
|
Nessun passaggio registrato per questo stabile.
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="space-y-3">
|
||||||
|
@foreach($transfers as $transfer)
|
||||||
|
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm">
|
||||||
|
<div class="flex items-start justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-medium text-gray-900">
|
||||||
|
{{ trim((string) ($transfer->fromAmministratore?->denominazione_studio ?: $transfer->fromAmministratore?->nome_completo ?: 'N/D')) }}
|
||||||
|
<span class="text-gray-400">-></span>
|
||||||
|
{{ trim((string) ($transfer->toAmministratore?->denominazione_studio ?: $transfer->toAmministratore?->nome_completo ?: 'N/D')) }}
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 text-xs text-gray-500">
|
||||||
|
Storico #{{ $transfer->id }} · {{ optional($transfer->created_at)->format('d/m/Y H:i') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-right text-xs text-gray-500">
|
||||||
|
<div>{{ $transfer->changed_by_name ?: ($transfer->changedByUser?->name ?? 'Sistema') }}</div>
|
||||||
|
<div>{{ $transfer->source ?: 'portal-superadmin' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(!empty($transfer->reason))
|
||||||
|
<div class="mt-3 rounded-md bg-gray-50 px-3 py-2 text-sm text-gray-700">
|
||||||
|
{{ $transfer->reason }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="mt-2 text-xs text-gray-500">
|
||||||
|
Rubrica collegata aggiornata: {{ $transfer->also_rubrica ? 'si' : 'no' }}
|
||||||
|
@if(!empty($transfer->ip_address))
|
||||||
|
· IP {{ $transfer->ip_address }}
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
@ -385,11 +385,70 @@
|
||||||
<div class="rounded-lg border border-gray-200 bg-white p-2">
|
<div class="rounded-lg border border-gray-200 bg-white p-2">
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<button type="button" wire:click="$set('sideTab', 'collegamenti')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $sideTab === 'collegamenti' ? 'bg-indigo-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">Collegamenti</button>
|
<button type="button" wire:click="$set('sideTab', 'collegamenti')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $sideTab === 'collegamenti' ? 'bg-indigo-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">Collegamenti</button>
|
||||||
|
<button type="button" wire:click="$set('sideTab', 'studio')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $sideTab === 'studio' ? 'bg-indigo-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">Collaboratore studio</button>
|
||||||
<button type="button" wire:click="$set('sideTab', 'dipendenti')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $sideTab === 'dipendenti' ? 'bg-indigo-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">Dipendenti</button>
|
<button type="button" wire:click="$set('sideTab', 'dipendenti')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $sideTab === 'dipendenti' ? 'bg-indigo-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">Dipendenti</button>
|
||||||
<button type="button" wire:click="$set('sideTab', 'autorizzazioni')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $sideTab === 'autorizzazioni' ? 'bg-indigo-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">Ruoli e autorizzazioni</button>
|
<button type="button" wire:click="$set('sideTab', 'autorizzazioni')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $sideTab === 'autorizzazioni' ? 'bg-indigo-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">Ruoli e autorizzazioni</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if($sideTab === 'studio')
|
||||||
|
<x-filament::section>
|
||||||
|
<x-slot name="heading">Collegamento collaboratore studio</x-slot>
|
||||||
|
<x-slot name="description">Trasforma questo nominativo rubrica in collaboratore di studio senza perdere eventuali altri profili già esistenti sullo stesso utente.</x-slot>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm text-gray-500">Utente collegato</div>
|
||||||
|
<div class="font-medium">{{ $studioCollaboratoreUserName ?: ($rubrica->nome_completo ?? '—') }}</div>
|
||||||
|
<div class="text-xs text-gray-500">{{ $studioCollaboratoreUserEmail ?: 'Email non presente in rubrica' }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-sm text-gray-500">Ruoli attuali utente</div>
|
||||||
|
<div class="text-sm text-gray-800">{{ count($studioCollaboratoreRoles) > 0 ? implode(', ', $studioCollaboratoreRoles) : 'Nessun ruolo applicativo rilevato' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(filled($studioCollaboratoreGeneratedPassword))
|
||||||
|
<div class="rounded-md border border-amber-300 bg-amber-50 px-3 py-2 text-xs text-amber-900">
|
||||||
|
Ultima password temporanea generata: <span class="font-mono font-semibold">{{ $studioCollaboratoreGeneratedPassword }}</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs font-semibold text-gray-600">Interno PBX collaboratore</label>
|
||||||
|
<input type="text" wire:model.defer="studioCollaboratorePbxExtension" class="w-full rounded-md border-gray-300 text-sm" placeholder="Es. 101" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="mb-1 block text-xs font-semibold text-gray-600">Stabili assegnati al collaboratore</div>
|
||||||
|
<div class="max-h-40 space-y-1 overflow-auto rounded-md border border-gray-200 bg-gray-50 p-2">
|
||||||
|
@forelse($studioCollaboratoreStabileOptions as $stabileId => $stabileLabel)
|
||||||
|
<label class="flex items-center gap-2 text-xs text-gray-700">
|
||||||
|
<input type="checkbox" value="{{ (int) $stabileId }}" wire:model.defer="studioCollaboratoreStabileIds" class="rounded border-gray-300" />
|
||||||
|
<span>{{ $stabileLabel }}</span>
|
||||||
|
</label>
|
||||||
|
@empty
|
||||||
|
<div class="text-xs text-gray-500">Nessuno stabile disponibile per l'amministratore attivo.</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<x-filament::button type="button" color="primary" wire:click="abilitaAccessoCollaboratoreStudio">
|
||||||
|
{{ $studioCollaboratoreUserId ? 'Riabilita / conferma collaboratore' : 'Abilita collaboratore studio' }}
|
||||||
|
</x-filament::button>
|
||||||
|
<x-filament::button type="button" color="gray" wire:click="salvaCollaboratoreStudio">Salva interno e stabili</x-filament::button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-md border border-slate-200 bg-slate-50 p-3 text-xs text-slate-700">
|
||||||
|
Questo collegamento usa l'email della rubrica come chiave utente. Se l'utente esiste già, aggiunge solo il ruolo <strong>collaboratore</strong> e sincronizza gli stabili dell'amministratore corrente senza rimuovere eventuali altri ruoli o profili già presenti.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-filament::section>
|
||||||
|
@endif
|
||||||
|
|
||||||
@if($sideTab === 'collegamenti')
|
@if($sideTab === 'collegamenti')
|
||||||
<x-filament::section>
|
<x-filament::section>
|
||||||
<x-slot name="heading">Collegamenti</x-slot>
|
<x-slot name="heading">Collegamenti</x-slot>
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,13 @@ class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium {{ $t
|
||||||
<div class="mt-1 text-xs text-gray-500">Vista operativa in stile ticket-gestione. Cerca rapidamente e apri HUB/scheda inline.</div>
|
<div class="mt-1 text-xs text-gray-500">Vista operativa in stile ticket-gestione. Cerca rapidamente e apri HUB/scheda inline.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-lg border bg-gray-50 px-3 py-2 text-xs md:col-span-2">
|
<div class="rounded-lg border bg-gray-50 px-3 py-2 text-xs md:col-span-2">
|
||||||
Totale fornitori importati: <span class="font-semibold">{{ (int) ($fornitoriCount ?? 0) }}</span>
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div>Totale fornitori importati: <span class="font-semibold">{{ (int) ($fornitoriCount ?? 0) }}</span></div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button type="button" wire:click="importaTagLegacy(false, true)" class="inline-flex items-center rounded-md bg-slate-100 px-3 py-1.5 text-[11px] font-medium text-slate-700 hover:bg-slate-200">Anteprima tag legacy</button>
|
||||||
|
<button type="button" wire:click="importaTagLegacy(false, false)" class="inline-flex items-center rounded-md bg-emerald-700 px-3 py-1.5 text-[11px] font-medium text-white hover:bg-emerald-600">Riallinea archivio legacy</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -170,6 +176,7 @@ class="inline-flex h-4 w-4 items-center justify-center rounded-full text-slate-5
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<button type="button" wire:click="apriElenco" class="inline-flex items-center rounded-md bg-slate-100 px-3 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-200">Torna a elenco</button>
|
<button type="button" wire:click="apriElenco" class="inline-flex items-center rounded-md bg-slate-100 px-3 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-200">Torna a elenco</button>
|
||||||
<button type="button" wire:click="apriTabDipendenti" class="inline-flex items-center rounded-md bg-slate-100 px-3 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-200">Tab 3: Dipendenti fornitore</button>
|
<button type="button" wire:click="apriTabDipendenti" class="inline-flex items-center rounded-md bg-slate-100 px-3 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-200">Tab 3: Dipendenti fornitore</button>
|
||||||
|
<button type="button" wire:click="importaTagLegacy(true, false)" class="inline-flex items-center rounded-md bg-emerald-700 px-3 py-1.5 text-xs font-medium text-white hover:bg-emerald-600">Riallinea tag legacy</button>
|
||||||
<button type="button" wire:click="apriNuovoFornitore" class="inline-flex items-center rounded-md bg-emerald-700 px-3 py-1.5 text-xs font-medium text-white hover:bg-emerald-600">Nuovo fornitore</button>
|
<button type="button" wire:click="apriNuovoFornitore" class="inline-flex items-center rounded-md bg-emerald-700 px-3 py-1.5 text-xs font-medium text-white hover:bg-emerald-600">Nuovo fornitore</button>
|
||||||
<a href="{{ $this->getFornitoreAnagraficaUrl((int) $fornitore->id) }}" class="inline-flex items-center rounded-md bg-slate-100 px-3 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-200">Apri vecchia scheda (immutata)</a>
|
<a href="{{ $this->getFornitoreAnagraficaUrl((int) $fornitore->id) }}" class="inline-flex items-center rounded-md bg-slate-100 px-3 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-200">Apri vecchia scheda (immutata)</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="rounded-lg border border-gray-200 bg-white p-3">
|
||||||
|
<div class="mb-2 text-sm font-semibold text-gray-700">Operazioni distribuzione, backup e sincronizzazione</div>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<x-filament::button type="button" color="warning" wire:click="runProductionUpgrade">Aggiorna produzione</x-filament::button>
|
||||||
|
<x-filament::button type="button" color="primary" wire:click="runBuildProdUpdatePackage">Crea pacchetto aggiornamento</x-filament::button>
|
||||||
|
<x-filament::button type="button" color="gray" wire:click="downloadLastUpdatePackage">Scarica ultimo pacchetto</x-filament::button>
|
||||||
|
<x-filament::button type="button" color="gray" wire:click="reviewUsersToEnable">Controlla nuovi utenti</x-filament::button>
|
||||||
|
<x-filament::button type="button" color="success" wire:click="runBackupProductionData">Backup produzione</x-filament::button>
|
||||||
|
<x-filament::button type="button" color="success" wire:click="runBackupModifiedDataToGdrive">Backup incrementale dati su Drive</x-filament::button>
|
||||||
|
<x-filament::button type="button" color="info" wire:click="runGoogleDriveCheck">Verifica Google Drive</x-filament::button>
|
||||||
|
<x-filament::button type="button" color="info" wire:click="runRestoreDataNonDistruttivo">Restore dati non distruttivo</x-filament::button>
|
||||||
|
<x-filament::button type="button" color="primary" wire:click="runSyncProdUpdate">Sync aggiornamenti + archivi</x-filament::button>
|
||||||
|
<x-filament::button type="button" color="success" wire:click="connectGoogle">Collega Google</x-filament::button>
|
||||||
|
<x-filament::button type="button" color="danger" wire:click="disconnectGoogle">Scollega Google</x-filament::button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(filled($this->lastUpdatePackageName))
|
||||||
|
<div class="mt-3 rounded-lg border border-primary-200 bg-primary-50 p-3 text-sm text-primary-700">
|
||||||
|
Ultimo pacchetto aggiornamento: <strong>{{ $this->lastUpdatePackageName }}</strong>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if(filled($this->opsLastOutput))
|
||||||
|
<div class="mt-3 rounded-lg border border-gray-200 bg-gray-50 p-3">
|
||||||
|
<div class="mb-2 text-sm font-semibold text-gray-700">Output operazioni</div>
|
||||||
|
<pre class="max-h-72 overflow-auto whitespace-pre-wrap text-xs text-gray-800">{{ $this->opsLastOutput }}</pre>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -48,36 +48,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-lg border border-gray-200 bg-white p-3">
|
|
||||||
<div class="mb-2 text-sm font-semibold text-gray-700">Operazioni distribuzione, backup e sincronizzazione</div>
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
|
||||||
<x-filament::button type="button" color="warning" wire:click="runProductionUpgrade">Aggiorna produzione</x-filament::button>
|
|
||||||
<x-filament::button type="button" color="primary" wire:click="runBuildProdUpdatePackage">Crea pacchetto aggiornamento</x-filament::button>
|
|
||||||
<x-filament::button type="button" color="gray" wire:click="downloadLastUpdatePackage">Scarica ultimo pacchetto</x-filament::button>
|
|
||||||
<x-filament::button type="button" color="gray" wire:click="reviewUsersToEnable">Controlla nuovi utenti</x-filament::button>
|
|
||||||
<x-filament::button type="button" color="success" wire:click="runBackupProductionData">Backup produzione</x-filament::button>
|
|
||||||
<x-filament::button type="button" color="success" wire:click="runBackupModifiedDataToGdrive">Backup incrementale dati su Drive</x-filament::button>
|
|
||||||
<x-filament::button type="button" color="info" wire:click="runGoogleDriveCheck">Verifica Google Drive</x-filament::button>
|
|
||||||
<x-filament::button type="button" color="info" wire:click="runRestoreDataNonDistruttivo">Restore dati non distruttivo</x-filament::button>
|
|
||||||
<x-filament::button type="button" color="primary" wire:click="runSyncProdUpdate">Sync aggiornamenti + archivi</x-filament::button>
|
|
||||||
<x-filament::button type="button" color="success" wire:click="connectGoogle">Collega Google</x-filament::button>
|
|
||||||
<x-filament::button type="button" color="danger" wire:click="disconnectGoogle">Scollega Google</x-filament::button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if(filled($this->lastUpdatePackageName))
|
|
||||||
<div class="mt-3 rounded-lg border border-primary-200 bg-primary-50 p-3 text-sm text-primary-700">
|
|
||||||
Ultimo pacchetto aggiornamento: <strong>{{ $this->lastUpdatePackageName }}</strong>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
@if(filled($this->opsLastOutput))
|
|
||||||
<div class="mt-3 rounded-lg border border-gray-200 bg-gray-50 p-3">
|
|
||||||
<div class="mb-2 text-sm font-semibold text-gray-700">Output operazioni</div>
|
|
||||||
<pre class="max-h-72 overflow-auto whitespace-pre-wrap text-xs text-gray-800">{{ $this->opsLastOutput }}</pre>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-lg border border-gray-200 bg-white p-3">
|
<div class="rounded-lg border border-gray-200 bg-white p-3">
|
||||||
<div class="mb-2 text-sm font-semibold text-gray-700">Nuovi utenti da abilitare (risultato controllo)</div>
|
<div class="mb-2 text-sm font-semibold text-gray-700">Nuovi utenti da abilitare (risultato controllo)</div>
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="rounded-lg border border-gray-200 bg-white p-3 text-xs text-gray-700">
|
||||||
|
Questa area serve a verificare via web la prontezza dell'integrazione Google e della posta studio. La lettura automatica caselle non e ancora attiva come daemon applicativo, ma puoi validare configurazione OAuth e collegamento account senza entrare in shell.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<x-filament::button type="button" color="success" wire:click="connectGoogle">Collega Google</x-filament::button>
|
||||||
|
<x-filament::button type="button" color="info" wire:click="runGoogleOAuthReadinessCheck">Verifica Google OAuth</x-filament::button>
|
||||||
|
<x-filament::button type="button" color="danger" wire:click="disconnectGoogle">Scollega Google</x-filament::button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(filled($this->opsLastOutput))
|
||||||
|
<div class="rounded-lg border border-gray-200 bg-gray-50 p-3">
|
||||||
|
<div class="mb-2 text-sm font-semibold text-gray-700">Ultimo output verifica posta / Google</div>
|
||||||
|
<pre class="max-h-64 overflow-auto whitespace-pre-wrap text-xs text-gray-800">{{ $this->opsLastOutput }}</pre>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
@ -98,6 +98,39 @@
|
||||||
<h2 class="mb-1 text-base font-semibold">Elenco tecnico chiamate (1 riga = 1 evento)</h2>
|
<h2 class="mb-1 text-base font-semibold">Elenco tecnico chiamate (1 riga = 1 evento)</h2>
|
||||||
<p class="mb-3 text-xs text-gray-500">Qui vedi in colonne i dati disponibili dal centralino: interno, linea/trunk, numero, durata, costo, utente assegnato, stabile, raw line, ecc.</p>
|
<p class="mb-3 text-xs text-gray-500">Qui vedi in colonne i dati disponibili dal centralino: interno, linea/trunk, numero, durata, costo, utente assegnato, stabile, raw line, ecc.</p>
|
||||||
|
|
||||||
|
<div class="mb-4 grid grid-cols-1 gap-3 rounded-lg border border-slate-200 bg-slate-50 p-3 md:grid-cols-4">
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-[11px] font-semibold uppercase tracking-wide text-slate-600">Vista chiamate</label>
|
||||||
|
<select wire:model.live="tecnicoCallView" class="w-full rounded-md border-gray-300 text-xs">
|
||||||
|
<option value="esterne">Solo esterne</option>
|
||||||
|
<option value="tutte">Tutte</option>
|
||||||
|
<option value="interne">Solo interne</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-[11px] font-semibold uppercase tracking-wide text-slate-600">Ambito numero</label>
|
||||||
|
<select wire:model.live="tecnicoScopeFilter" class="w-full rounded-md border-gray-300 text-xs">
|
||||||
|
<option value="tutti">Tutti</option>
|
||||||
|
<option value="studio">Solo numeri studio gestiti</option>
|
||||||
|
<option value="interno">Solo interni collaboratori</option>
|
||||||
|
<option value="esterno">Solo numeri esterni</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-[11px] font-semibold uppercase tracking-wide text-slate-600">Direzione</label>
|
||||||
|
<select wire:model.live="tecnicoDirectionFilter" class="w-full rounded-md border-gray-300 text-xs">
|
||||||
|
<option value="tutte">Tutte</option>
|
||||||
|
<option value="inbound">Inbound</option>
|
||||||
|
<option value="outbound">Outbound</option>
|
||||||
|
<option value="internal">Internal</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-1">
|
||||||
|
<label class="mb-1 block text-[11px] font-semibold uppercase tracking-wide text-slate-600">Ricerca</label>
|
||||||
|
<input type="text" wire:model.live.debounce.300ms="tecnicoSearch" class="w-full rounded-md border-gray-300 text-xs" placeholder="Numero, interno, testo riga, nominativo tecnico" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="min-w-full border text-xs">
|
<table class="min-w-full border text-xs">
|
||||||
<thead class="bg-gray-50">
|
<thead class="bg-gray-50">
|
||||||
|
|
@ -105,6 +138,7 @@
|
||||||
<th class="border px-2 py-1 text-left">ID</th>
|
<th class="border px-2 py-1 text-left">ID</th>
|
||||||
<th class="border px-2 py-1 text-left">Data/Ora</th>
|
<th class="border px-2 py-1 text-left">Data/Ora</th>
|
||||||
<th class="border px-2 py-1 text-left">Direzione</th>
|
<th class="border px-2 py-1 text-left">Direzione</th>
|
||||||
|
<th class="border px-2 py-1 text-left">Ambito</th>
|
||||||
<th class="border px-2 py-1 text-left">Interno</th>
|
<th class="border px-2 py-1 text-left">Interno</th>
|
||||||
<th class="border px-2 py-1 text-left">Linea/CO</th>
|
<th class="border px-2 py-1 text-left">Linea/CO</th>
|
||||||
<th class="border px-2 py-1 text-left">Numero</th>
|
<th class="border px-2 py-1 text-left">Numero</th>
|
||||||
|
|
@ -123,15 +157,16 @@
|
||||||
@php($smdr = (array) data_get($m->metadata, 'smdr', []))
|
@php($smdr = (array) data_get($m->metadata, 'smdr', []))
|
||||||
@php($cost = trim((string) (($smdr['cost_currency'] ?? '') . ' ' . ($smdr['cost_amount'] ?? ''))))
|
@php($cost = trim((string) (($smdr['cost_currency'] ?? '') . ' ' . ($smdr['cost_amount'] ?? ''))))
|
||||||
@php($duration = (string) (($smdr['duration_raw'] ?? '') ?: ($smdr['ring_duration_raw'] ?? '')))
|
@php($duration = (string) (($smdr['duration_raw'] ?? '') ?: ($smdr['ring_duration_raw'] ?? '')))
|
||||||
|
@php($phoneValue = (string) ($m->phone_number ?: ($smdr['dial_number'] ?? '')))
|
||||||
|
@php($rubricaNomeTech = $this->getTecnicoNominativo($m))
|
||||||
<tr>
|
<tr>
|
||||||
<td class="border px-2 py-1">{{ $m->id }}</td>
|
<td class="border px-2 py-1">{{ $m->id }}</td>
|
||||||
<td class="border px-2 py-1">{{ optional($m->received_at)->format('d/m/Y H:i:s') }}</td>
|
<td class="border px-2 py-1">{{ optional($m->received_at)->format('d/m/Y H:i:s') }}</td>
|
||||||
<td class="border px-2 py-1">{{ $m->direction }}</td>
|
<td class="border px-2 py-1">{{ $m->direction }}</td>
|
||||||
<td class="border px-2 py-1">{{ (string) ($m->target_extension ?: ($smdr['extension'] ?? '-')) }}</td>
|
<td class="border px-2 py-1">{{ $this->getTecnicoScopeLabel($m) }}</td>
|
||||||
|
<td class="border px-2 py-1">{{ $this->getTecnicoInternoLabel($m) }}</td>
|
||||||
<td class="border px-2 py-1">{{ (string) ($smdr['co'] ?? '-') }}</td>
|
<td class="border px-2 py-1">{{ (string) ($smdr['co'] ?? '-') }}</td>
|
||||||
<td class="border px-2 py-1">{{ (string) ($m->phone_number ?: ($smdr['dial_number'] ?? '-')) }}</td>
|
<td class="border px-2 py-1">{{ (string) ($m->phone_number ?: ($smdr['dial_number'] ?? '-')) }}</td>
|
||||||
@php($phoneValue = (string) ($m->phone_number ?: ($smdr['dial_number'] ?? '')))
|
|
||||||
@php($rubricaNomeTech = $this->getRubricaNomeByPhone($phoneValue))
|
|
||||||
<td class="border px-2 py-1">{{ $rubricaNomeTech ?: '-' }}</td>
|
<td class="border px-2 py-1">{{ $rubricaNomeTech ?: '-' }}</td>
|
||||||
<td class="border px-2 py-1">{{ $duration !== '' ? $duration : '-' }}</td>
|
<td class="border px-2 py-1">{{ $duration !== '' ? $duration : '-' }}</td>
|
||||||
<td class="border px-2 py-1">{{ $cost !== '' ? $cost : '-' }}</td>
|
<td class="border px-2 py-1">{{ $cost !== '' ? $cost : '-' }}</td>
|
||||||
|
|
@ -141,7 +176,7 @@
|
||||||
<td class="border px-2 py-1 max-w-[340px] truncate" title="{{ $m->message_text }}">{{ $m->message_text }}</td>
|
<td class="border px-2 py-1 max-w-[340px] truncate" title="{{ $m->message_text }}">{{ $m->message_text }}</td>
|
||||||
<td class="border px-2 py-1">
|
<td class="border px-2 py-1">
|
||||||
@php($rubricaUrlTech = $this->getRubricaUrlByPhone($phoneValue))
|
@php($rubricaUrlTech = $this->getRubricaUrlByPhone($phoneValue))
|
||||||
@if($rubricaUrlTech)
|
@if($rubricaUrlTech && ! $this->isInternalSmdrMessage($m))
|
||||||
<a href="{{ $rubricaUrlTech }}" class="inline-flex items-center rounded-md border px-2 py-1 text-[11px] text-indigo-700 hover:bg-indigo-50">Apri Rubrica</a>
|
<a href="{{ $rubricaUrlTech }}" class="inline-flex items-center rounded-md border px-2 py-1 text-[11px] text-indigo-700 hover:bg-indigo-50">Apri Rubrica</a>
|
||||||
@endif
|
@endif
|
||||||
@if(!$m->post_it_id)
|
@if(!$m->post_it_id)
|
||||||
|
|
@ -153,7 +188,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
@empty
|
@empty
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="14" class="border px-2 py-3 text-center text-gray-500">Nessun evento SMDR disponibile.</td>
|
<td colspan="15" class="border px-2 py-3 text-center text-gray-500">Nessun evento SMDR disponibile.</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endforelse
|
@endforelse
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
||||||
|
|
@ -220,14 +220,46 @@
|
||||||
<div class="rounded-xl border bg-white p-4">
|
<div class="rounded-xl border bg-white p-4">
|
||||||
<div class="text-sm font-semibold">Assegnazione fornitore</div>
|
<div class="text-sm font-semibold">Assegnazione fornitore</div>
|
||||||
<label class="mt-3 block text-sm">
|
<label class="mt-3 block text-sm">
|
||||||
<span class="mb-1 block font-medium">Fornitore</span>
|
<span class="mb-1 block font-medium">Ricerca fornitore</span>
|
||||||
<select wire:model.defer="fornitoreId" class="w-full rounded-lg border-gray-300">
|
<input type="text" wire:model.live.debounce.250ms="fornitoreSearch" class="w-full rounded-lg border-gray-300" placeholder="Es. idraulico, spurghi, 333..., Skynet, email" />
|
||||||
<option value="">Seleziona fornitore</option>
|
|
||||||
@foreach($fornitoriOptions as $f)
|
|
||||||
<option value="{{ (int) $f['id'] }}">{{ $f['nome'] }}</option>
|
|
||||||
@endforeach
|
|
||||||
</select>
|
|
||||||
</label>
|
</label>
|
||||||
|
<div class="mt-2 text-xs text-gray-500">Ricerca per tag, telefono, nome, ragione sociale o email. Tocca il risultato per selezionarlo.</div>
|
||||||
|
|
||||||
|
@if($fornitoreId)
|
||||||
|
<div class="mt-3 rounded-lg border border-emerald-200 bg-emerald-50 p-3 text-xs text-emerald-800">
|
||||||
|
Fornitore selezionato: <span class="font-semibold">{{ $fornitoreSearch }}</span>
|
||||||
|
<button type="button" wire:click="resetFornitoreSelection" class="ml-2 inline-flex items-center rounded-md bg-white px-2 py-1 text-[11px] font-medium text-emerald-700 ring-1 ring-inset ring-emerald-300 hover:bg-emerald-100">Cambia</button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if(count($fornitoreMatches) > 0)
|
||||||
|
<div class="mt-3 space-y-2">
|
||||||
|
@foreach($fornitoreMatches as $match)
|
||||||
|
<button type="button" wire:click="selezionaFornitore({{ (int) $match['id'] }})" class="w-full rounded-lg border p-3 text-left {{ (int) $fornitoreId === (int) $match['id'] ? 'border-emerald-400 bg-emerald-50 ring-1 ring-emerald-200' : 'bg-white hover:bg-gray-50' }}">
|
||||||
|
<div class="text-sm font-medium">{{ $match['nome'] }}</div>
|
||||||
|
<div class="mt-1 text-xs text-gray-600">
|
||||||
|
@if($match['telefono'])
|
||||||
|
{{ $match['telefono'] }}
|
||||||
|
@else
|
||||||
|
Telefono non valorizzato
|
||||||
|
@endif
|
||||||
|
@if($match['email'])
|
||||||
|
· {{ $match['email'] }}
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@if($match['tags_label'])
|
||||||
|
<div class="mt-1 text-xs text-indigo-700">Tag: {{ $match['tags_label'] }}</div>
|
||||||
|
@endif
|
||||||
|
@if($match['note'])
|
||||||
|
<div class="mt-1 text-xs text-gray-500">{{ $match['note'] }}</div>
|
||||||
|
@endif
|
||||||
|
</button>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@elseif(filled($fornitoreSearch) && ! $fornitoreId)
|
||||||
|
<div class="mt-3 text-xs text-gray-500">Nessun fornitore trovato con questa ricerca.</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<label class="mt-3 block text-sm">
|
<label class="mt-3 block text-sm">
|
||||||
<span class="mb-1 block font-medium">Note assegnazione/intervento</span>
|
<span class="mb-1 block font-medium">Note assegnazione/intervento</span>
|
||||||
<textarea rows="3" wire:model.defer="noteAssegnazione" class="w-full rounded-lg border-gray-300" placeholder="Istruzioni operative"></textarea>
|
<textarea rows="3" wire:model.defer="noteAssegnazione" class="w-full rounded-lg border-gray-300" placeholder="Istruzioni operative"></textarea>
|
||||||
|
|
@ -321,6 +353,64 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 rounded-xl border bg-white p-4">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-semibold">Assicurazione e sinistro</div>
|
||||||
|
<div class="mt-1 text-xs text-gray-500">Gestione apertura o aggiornamento del sinistro senza uscire da Filament.</div>
|
||||||
|
</div>
|
||||||
|
@if($ticket->insuranceClaim)
|
||||||
|
<span class="inline-flex rounded-full bg-emerald-100 px-2 py-1 text-xs font-medium text-emerald-700">
|
||||||
|
Sinistro {{ $ticket->insuranceClaim->status ?: 'aperto' }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 grid gap-3 md:grid-cols-2">
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Polizza stabile</span>
|
||||||
|
<select wire:model.defer="insurancePolicyId" class="w-full rounded-lg border-gray-300">
|
||||||
|
<option value="">Seleziona una polizza</option>
|
||||||
|
@foreach($this->insurancePolicyOptions as $policyId => $policyLabel)
|
||||||
|
<option value="{{ $policyId }}">{{ $policyLabel }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Riferimento polizza</span>
|
||||||
|
<input type="text" wire:model.defer="insurancePolicyReference" class="w-full rounded-lg border-gray-300" placeholder="Polizza o riferimento assicurativo" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Numero sinistro</span>
|
||||||
|
<input type="text" wire:model.defer="insuranceClaimNumber" class="w-full rounded-lg border-gray-300" placeholder="Numero pratica/sinistro" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="mt-3 block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Note sinistro</span>
|
||||||
|
<textarea rows="3" wire:model.defer="insuranceNotes" class="w-full rounded-lg border-gray-300" placeholder="Dettagli apertura o aggiornamento pratica"></textarea>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
@if($ticket->insuranceClaim)
|
||||||
|
<div class="mt-3 rounded-lg border border-slate-200 bg-slate-50 p-3 text-xs text-slate-700">
|
||||||
|
Aperto il {{ optional($ticket->insuranceClaim->opened_at)->format('d/m/Y H:i') ?: '-' }}
|
||||||
|
@if($ticket->insuranceClaim->insurancePolicy)
|
||||||
|
· {{ $ticket->insuranceClaim->insurancePolicy->display_name }}
|
||||||
|
@endif
|
||||||
|
@if($ticket->insuranceClaim->claim_number)
|
||||||
|
· Sinistro {{ $ticket->insuranceClaim->claim_number }}
|
||||||
|
@endif
|
||||||
|
@if($ticket->insuranceClaim->policy_reference)
|
||||||
|
· Polizza {{ $ticket->insuranceClaim->policy_reference }}
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<x-filament::button color="warning" wire:click="salvaSinistroAssicurativo">Salva sinistro</x-filament::button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 rounded-xl border bg-white p-4">
|
<div class="mt-4 rounded-xl border bg-white p-4">
|
||||||
<div class="text-sm font-semibold">Storico note/messaggi</div>
|
<div class="text-sm font-semibold">Storico note/messaggi</div>
|
||||||
<div class="mt-3 space-y-2">
|
<div class="mt-3 space-y-2">
|
||||||
|
|
@ -341,7 +431,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if($attachmentPreview)
|
@if($attachmentPreview)
|
||||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4">
|
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4" x-data="{ zoom: 1, pdfPage: 1, pdfZoom: 'page-fit', iframeKey: 0, refreshPdf() { this.iframeKey++; }, zoomIn() { this.zoom = Math.min(this.zoom + 0.15, 3); }, zoomOut() { this.zoom = Math.max(this.zoom - 0.15, 0.4); }, resetZoom() { this.zoom = 1; }, nextPage() { this.pdfPage = this.pdfPage + 1; this.refreshPdf(); }, prevPage() { this.pdfPage = Math.max(this.pdfPage - 1, 1); this.refreshPdf(); }, printPdf() { const frame = this.$refs.pdfFrame; if (frame?.contentWindow) { frame.contentWindow.focus(); frame.contentWindow.print(); } else { window.open(@js($attachmentPreview['url']), '_blank'); } } }">
|
||||||
<div class="h-[92vh] w-full max-w-[96vw] rounded-xl bg-white shadow-2xl">
|
<div class="h-[92vh] w-full max-w-[96vw] rounded-xl bg-white shadow-2xl">
|
||||||
<div class="flex items-center justify-between border-b px-4 py-3">
|
<div class="flex items-center justify-between border-b px-4 py-3">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -351,6 +441,16 @@
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
@if($attachmentPreview['is_image'] ?? false)
|
||||||
|
<button type="button" x-on:click="zoomOut()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">-</button>
|
||||||
|
<button type="button" x-on:click="resetZoom()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">100%</button>
|
||||||
|
<button type="button" x-on:click="zoomIn()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">+</button>
|
||||||
|
@elseif($attachmentPreview['is_pdf'] ?? false)
|
||||||
|
<button type="button" x-on:click="prevPage()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">Pagina precedente</button>
|
||||||
|
<span class="text-xs text-slate-500">Pagina <span x-text="pdfPage"></span></span>
|
||||||
|
<button type="button" x-on:click="nextPage()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">Pagina successiva</button>
|
||||||
|
<button type="button" x-on:click="printPdf()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">Stampa PDF</button>
|
||||||
|
@endif
|
||||||
<a href="{{ $attachmentPreview['url'] }}" target="_blank" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">Apri in nuova scheda</a>
|
<a href="{{ $attachmentPreview['url'] }}" target="_blank" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">Apri in nuova scheda</a>
|
||||||
<button type="button" wire:click="closeAttachmentPreview" class="rounded-md bg-gray-100 px-2 py-1 text-xs hover:bg-gray-200">Chiudi</button>
|
<button type="button" wire:click="closeAttachmentPreview" class="rounded-md bg-gray-100 px-2 py-1 text-xs hover:bg-gray-200">Chiudi</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -358,9 +458,11 @@
|
||||||
|
|
||||||
<div class="h-[calc(92vh-64px)] overflow-auto p-4">
|
<div class="h-[calc(92vh-64px)] overflow-auto p-4">
|
||||||
@if($attachmentPreview['is_image'] ?? false)
|
@if($attachmentPreview['is_image'] ?? false)
|
||||||
<img src="{{ $attachmentPreview['url'] }}" alt="Anteprima allegato" class="mx-auto max-h-[84vh] w-auto rounded border" />
|
<div class="flex min-h-full items-center justify-center overflow-auto rounded-lg bg-slate-50 p-4">
|
||||||
|
<img src="{{ $attachmentPreview['url'] }}" alt="Anteprima allegato" class="max-h-[84vh] max-w-full rounded border object-contain shadow" x-bind:style="`transform: scale(${zoom}); transform-origin: center center;`" />
|
||||||
|
</div>
|
||||||
@elseif($attachmentPreview['is_pdf'] ?? false)
|
@elseif($attachmentPreview['is_pdf'] ?? false)
|
||||||
<iframe src="{{ $attachmentPreview['url'] }}" class="h-[84vh] w-full rounded border"></iframe>
|
<iframe x-ref="pdfFrame" x-bind:key="iframeKey" x-bind:src="@js($attachmentPreview['url']) + '#page=' + pdfPage + '&zoom=' + pdfZoom + '&toolbar=1&navpanes=0'" class="h-[84vh] w-full rounded border"></iframe>
|
||||||
@else
|
@else
|
||||||
<div class="rounded-md border bg-gray-50 p-4 text-sm text-gray-700">
|
<div class="rounded-md border bg-gray-50 p-4 text-sm text-gray-700">
|
||||||
Anteprima non disponibile per questo formato. Scarica o apri il file:
|
Anteprima non disponibile per questo formato. Scarica o apri il file:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
<x-filament-panels::page>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="rounded-xl border bg-white p-4">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<div class="text-lg font-semibold">Help Ticket Mobile</div>
|
||||||
|
<div class="mt-1 text-sm text-gray-600">Guida rapida per usare la pagina in modo lineare dal telefono senza ingombro visivo inutile.</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<a href="{{ $this->getBackUrl() }}" class="inline-flex items-center rounded-md bg-gray-800 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-700">Torna a Ticket Mobile</a>
|
||||||
|
<a href="{{ $this->getGestioneUrl() }}" class="inline-flex items-center rounded-md bg-indigo-700 px-3 py-1.5 text-xs font-medium text-white hover:bg-indigo-600">Apri Gestione Ticket</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-4 md:grid-cols-3">
|
||||||
|
<div class="rounded-xl border border-emerald-200 bg-emerald-50 p-4">
|
||||||
|
<div class="text-sm font-semibold text-emerald-900">1. Apertura rapida</div>
|
||||||
|
<div class="mt-2 text-sm text-emerald-800">Seleziona prima il chiamante dalla ricerca, poi compila titolo, descrizione e priorità del ticket.</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl border border-amber-200 bg-amber-50 p-4">
|
||||||
|
<div class="text-sm font-semibold text-amber-900">2. Assegnazione e intervento</div>
|
||||||
|
<div class="mt-2 text-sm text-amber-800">Dopo la creazione vai in Gestione Ticket per assegnare il fornitore corretto e aprire gli interventi operativi.</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl border border-slate-200 bg-slate-50 p-4">
|
||||||
|
<div class="text-sm font-semibold text-slate-900">3. Verifica e chiusura</div>
|
||||||
|
<div class="mt-2 text-sm text-slate-700">Chi usa la parte amministrativa verifica esito, proforma o fattura e chiude il ticket solo a lavoro coerente.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border bg-white p-4">
|
||||||
|
<div class="text-sm font-semibold">Buone pratiche</div>
|
||||||
|
<div class="mt-3 space-y-2 text-sm text-gray-700">
|
||||||
|
<div>Usa la ricerca chiamante per telefono, nome, cognome, ragione sociale o email e seleziona il contatto con un solo tocco.</div>
|
||||||
|
<div>Compila il campo descrizione con il problema reale, non solo con il titolo, così il fornitore trova subito il contesto.</div>
|
||||||
|
<div>Se hai foto o file, allegali subito dal telefono e usa una descrizione breve per ogni allegato quando serve.</div>
|
||||||
|
<div>Per la gestione operativa successiva usa la pagina Gestione Ticket, che è la vista corretta per assegnazione, sinistro, note e controllo avanzamento.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-filament-panels::page>
|
||||||
|
|
@ -68,28 +68,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-xl border bg-white p-4">
|
|
||||||
<div class="text-sm font-semibold">Workflow Ticket Chiaro</div>
|
|
||||||
<div class="mt-1 text-xs text-gray-500">Percorso operativo consigliato per evitare passaggi persi.</div>
|
|
||||||
<div class="mt-3 grid gap-3 md:grid-cols-3">
|
|
||||||
<div class="rounded-lg border border-emerald-200 bg-emerald-50 p-3">
|
|
||||||
<div class="text-xs font-semibold text-emerald-900">1) Apertura rapida</div>
|
|
||||||
<div class="mt-1 text-xs text-emerald-800">Seleziona chiamante, compila titolo e descrizione, poi crea il ticket.</div>
|
|
||||||
</div>
|
|
||||||
<div class="rounded-lg border border-amber-200 bg-amber-50 p-3">
|
|
||||||
<div class="text-xs font-semibold text-amber-900">2) Assegnazione e intervento</div>
|
|
||||||
<div class="mt-1 text-xs text-amber-800">Apri l'archivio ticket, assegna fornitore e registra gli interventi.</div>
|
|
||||||
</div>
|
|
||||||
<div class="rounded-lg border border-slate-200 bg-slate-50 p-3">
|
|
||||||
<div class="text-xs font-semibold text-slate-900">3) Verifica e chiusura</div>
|
|
||||||
<div class="mt-1 text-xs text-slate-700">Verifica esito/fattura, segna risolto e chiudi definitivamente.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-3">
|
|
||||||
<a href="{{ \App\Filament\Pages\Supporto\TicketGestione::getUrl(panel: 'admin-filament') }}" class="inline-flex items-center rounded-md bg-indigo-700 px-3 py-1.5 text-xs font-medium text-white hover:bg-indigo-600">Apri blade dedicata Gestione Ticket</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-xl border bg-white p-4">
|
<div class="rounded-xl border bg-white p-4">
|
||||||
<div class="text-sm font-semibold">Ricerca chiamante</div>
|
<div class="text-sm font-semibold">Ricerca chiamante</div>
|
||||||
<div class="mt-2 text-xs text-gray-500">Ricerca per telefono, nome, cognome, ragione sociale o email e seleziona il contatto con un click.</div>
|
<div class="mt-2 text-xs text-gray-500">Ricerca per telefono, nome, cognome, ragione sociale o email e seleziona il contatto con un click.</div>
|
||||||
|
|
@ -242,8 +220,9 @@
|
||||||
<div class="rounded-xl border bg-white p-4">
|
<div class="rounded-xl border bg-white p-4">
|
||||||
<div class="text-sm font-semibold">Gestione operativa separata</div>
|
<div class="text-sm font-semibold">Gestione operativa separata</div>
|
||||||
<div class="mt-1 text-xs text-gray-500">Per lavorazione, presa in carico, risoluzione e chiusura usa la pagina dedicata.</div>
|
<div class="mt-1 text-xs text-gray-500">Per lavorazione, presa in carico, risoluzione e chiusura usa la pagina dedicata.</div>
|
||||||
<div class="mt-3">
|
<div class="mt-3 flex flex-wrap gap-2">
|
||||||
<a href="{{ \App\Filament\Pages\Supporto\TicketGestione::getUrl(panel: 'admin-filament') }}" class="inline-flex items-center rounded-md bg-indigo-700 px-3 py-1.5 text-xs font-medium text-white hover:bg-indigo-600">Vai a Gestione Ticket</a>
|
<a href="{{ \App\Filament\Pages\Supporto\TicketGestione::getUrl(panel: 'admin-filament') }}" class="inline-flex items-center rounded-md bg-indigo-700 px-3 py-1.5 text-xs font-medium text-white hover:bg-indigo-600">Vai a Gestione Ticket</a>
|
||||||
|
<a href="{{ $this->getHelpUrl() }}" class="inline-flex items-center rounded-md bg-gray-800 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-700">Apri Help Ticket Mobile</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
@extends('admin.layouts.netgescon')
|
|
||||||
|
|
||||||
@section('title', 'Dipendenti fornitore')
|
|
||||||
|
|
||||||
@section('content')
|
|
||||||
<div class="container-fluid py-4">
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
||||||
<div>
|
|
||||||
<h1 class="h4 mb-0">Dipendenti - {{ $fornitore->ragione_sociale ?? $fornitore->nome ?? 'Fornitore' }}</h1>
|
|
||||||
<small class="text-muted">Gestione operatori che possono lavorare i ticket da cellulare e web</small>
|
|
||||||
</div>
|
|
||||||
<a href="{{ route('fornitore.tickets.index') }}" class="btn btn-outline-secondary btn-sm">Torna ai ticket</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if(session('success'))
|
|
||||||
<div class="alert alert-success">{{ session('success') }}</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
@if($errors->any())
|
|
||||||
<div class="alert alert-danger">
|
|
||||||
<ul class="mb-0">
|
|
||||||
@foreach($errors->all() as $error)
|
|
||||||
<li>{{ $error }}</li>
|
|
||||||
@endforeach
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-header">Nuovo dipendente</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form method="POST" action="{{ route('fornitore.dipendenti.store') }}" class="row g-3">
|
|
||||||
@csrf
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Nome</label>
|
|
||||||
<input type="text" class="form-control" name="nome" value="{{ old('nome') }}" required>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Cognome</label>
|
|
||||||
<input type="text" class="form-control" name="cognome" value="{{ old('cognome') }}">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Email</label>
|
|
||||||
<input type="email" class="form-control" name="email" value="{{ old('email') }}">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Telefono</label>
|
|
||||||
<input type="text" class="form-control" name="telefono" value="{{ old('telefono') }}">
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<label class="form-label">Note</label>
|
|
||||||
<textarea class="form-control" name="note" rows="2">{{ old('note') }}</textarea>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="checkbox" value="1" id="create_user_access" name="create_user_access" {{ old('create_user_access') ? 'checked' : '' }}>
|
|
||||||
<label class="form-check-label" for="create_user_access">
|
|
||||||
Crea/aggancia account utente con accesso al portale fornitore
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<button class="btn btn-primary" type="submit">Salva dipendente</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">Elenco dipendenti</div>
|
|
||||||
<div class="card-body p-0">
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-hover mb-0">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Nome</th>
|
|
||||||
<th>Contatti</th>
|
|
||||||
<th>Accesso</th>
|
|
||||||
<th>Stato</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@forelse($dipendenti as $dipendente)
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<strong>{{ $dipendente->nome_completo }}</strong>
|
|
||||||
@if($dipendente->note)
|
|
||||||
<div class="small text-muted">{{ \Illuminate\Support\Str::limit($dipendente->note, 80) }}</div>
|
|
||||||
@endif
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div>{{ $dipendente->email ?: '-' }}</div>
|
|
||||||
<div class="small text-muted">{{ $dipendente->telefono ?: '-' }}</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
@if($dipendente->user_id)
|
|
||||||
<span class="badge bg-success">Account collegato</span>
|
|
||||||
@else
|
|
||||||
<span class="badge bg-secondary">Nessun account</span>
|
|
||||||
@endif
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="badge {{ $dipendente->attivo ? 'bg-primary' : 'bg-secondary' }}">{{ $dipendente->attivo ? 'Attivo' : 'Disattivo' }}</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-end">
|
|
||||||
<form method="POST" action="{{ route('fornitore.dipendenti.toggle', $dipendente) }}" class="d-inline">
|
|
||||||
@csrf
|
|
||||||
<button class="btn btn-sm btn-outline-warning" type="submit">{{ $dipendente->attivo ? 'Disattiva' : 'Attiva' }}</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
@empty
|
|
||||||
<tr>
|
|
||||||
<td colspan="5" class="text-center text-muted py-4">Nessun dipendente registrato.</td>
|
|
||||||
</tr>
|
|
||||||
@endforelse
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer">{{ $dipendenti->links() }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@endsection
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
@extends('admin.layouts.netgescon')
|
|
||||||
|
|
||||||
@section('title', 'I miei ticket operativi')
|
|
||||||
|
|
||||||
@section('content')
|
|
||||||
<div class="container-fluid py-4">
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
||||||
<div>
|
|
||||||
<h1 class="h4 mb-0">Ticket operativi - {{ $fornitore->ragione_sociale ?? $fornitore->nome ?? 'Fornitore' }}</h1>
|
|
||||||
<small class="text-muted">Gestione interventi, chiusure operative e raggruppamento fattura</small>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a href="{{ route('fornitore.dipendenti.index') }}" class="btn btn-sm btn-outline-primary">Gestisci dipendenti</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3 d-flex gap-2">
|
|
||||||
<a class="btn btn-sm {{ $stato === 'aperti' ? 'btn-primary' : 'btn-outline-primary' }}" href="{{ route('fornitore.tickets.index', ['stato' => 'aperti']) }}">Aperti</a>
|
|
||||||
<a class="btn btn-sm {{ $stato === 'fatturabili' ? 'btn-primary' : 'btn-outline-primary' }}" href="{{ route('fornitore.tickets.index', ['stato' => 'fatturabili']) }}">Fatturabili</a>
|
|
||||||
<a class="btn btn-sm {{ $stato === 'chiusi' ? 'btn-primary' : 'btn-outline-primary' }}" href="{{ route('fornitore.tickets.index', ['stato' => 'chiusi']) }}">Chiusi</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-header">Interventi da fatturare</div>
|
|
||||||
<div class="card-body">
|
|
||||||
@if($fatturabili->isEmpty())
|
|
||||||
<p class="text-muted mb-0">Nessun intervento ancora pronto per fattura.</p>
|
|
||||||
@else
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-sm align-middle mb-0">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Ticket</th>
|
|
||||||
<th>Stabile</th>
|
|
||||||
<th>Stato</th>
|
|
||||||
<th>Rif. chiusura</th>
|
|
||||||
<th>Fattura</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@foreach($fatturabili as $item)
|
|
||||||
<tr>
|
|
||||||
<td><a href="{{ route('fornitore.tickets.show', $item) }}">#{{ $item->ticket_id }} - {{ $item->ticket->titolo ?? '-' }}</a></td>
|
|
||||||
<td>{{ $item->ticket->stabile->denominazione ?? '-' }}</td>
|
|
||||||
<td><span class="badge bg-secondary">{{ $item->stato }}</span></td>
|
|
||||||
<td>{{ $item->riferimento_chiusura ?: '-' }}</td>
|
|
||||||
<td>{{ $item->fattura_numero ?: 'Da emettere' }}</td>
|
|
||||||
</tr>
|
|
||||||
@endforeach
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">Pratiche / Interventi / Manutenzioni</div>
|
|
||||||
<div class="card-body p-0">
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-hover mb-0">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Ingresso</th>
|
|
||||||
<th>Contatto</th>
|
|
||||||
<th>Telefono</th>
|
|
||||||
<th>Problema</th>
|
|
||||||
<th>Apparato</th>
|
|
||||||
<th>Stato</th>
|
|
||||||
<th>Aggiornato</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@forelse($interventi as $idx => $intervento)
|
|
||||||
@php($row = $rows[$idx] ?? ['ingresso' => '-', 'contatto' => '-', 'telefono' => '', 'problema' => '-', 'apparato' => '-'])
|
|
||||||
<tr>
|
|
||||||
<td>{{ $row['ingresso'] }}</td>
|
|
||||||
<td>
|
|
||||||
<div class="fw-semibold">{{ $row['contatto'] }}</div>
|
|
||||||
<small class="text-muted">Ticket #{{ $intervento->ticket_id }} - {{ $intervento->ticket->stabile->denominazione ?? '-' }}</small>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
@if(!empty($row['telefono']))
|
|
||||||
<a href="tel:{{ preg_replace('/\s+/', '', (string) $row['telefono']) }}" class="btn btn-sm btn-outline-success">
|
|
||||||
{{ $row['telefono'] }}
|
|
||||||
</a>
|
|
||||||
@else
|
|
||||||
<span class="text-muted">-</span>
|
|
||||||
@endif
|
|
||||||
</td>
|
|
||||||
<td>{{ \Illuminate\Support\Str::limit((string) $row['problema'], 90) }}</td>
|
|
||||||
<td>{{ $row['apparato'] }}</td>
|
|
||||||
<td><span class="badge bg-info text-dark">{{ $intervento->stato }}</span></td>
|
|
||||||
<td>{{ optional($intervento->updated_at)->format('d/m/Y H:i') }}</td>
|
|
||||||
<td>
|
|
||||||
<a href="{{ route('fornitore.tickets.show', $intervento) }}" class="btn btn-sm btn-outline-primary">Scheda</a>
|
|
||||||
<form method="POST" action="{{ route('fornitore.tickets.sollecito', $intervento) }}" class="d-inline">
|
|
||||||
@csrf
|
|
||||||
<button type="submit" class="btn btn-sm btn-outline-warning">Sollecito</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
@empty
|
|
||||||
<tr>
|
|
||||||
<td colspan="9" class="text-center text-muted py-4">Nessun intervento disponibile.</td>
|
|
||||||
</tr>
|
|
||||||
@endforelse
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer">{{ $interventi->links() }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@endsection
|
|
||||||
|
|
@ -1,220 +0,0 @@
|
||||||
@extends('admin.layouts.netgescon')
|
|
||||||
|
|
||||||
@section('title', 'Dettaglio intervento fornitore')
|
|
||||||
|
|
||||||
@section('content')
|
|
||||||
<div class="container-fluid py-4">
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
||||||
<div>
|
|
||||||
<h1 class="h4 mb-0">Intervento Ticket #{{ $intervento->ticket_id }}</h1>
|
|
||||||
<small class="text-muted">{{ $intervento->ticket->titolo ?? '-' }} - {{ $intervento->ticket->stabile->denominazione ?? '-' }}</small>
|
|
||||||
</div>
|
|
||||||
<a href="{{ route('fornitore.tickets.index') }}" class="btn btn-outline-secondary btn-sm">Torna ai ticket</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-body py-2">
|
|
||||||
<ul class="nav nav-tabs card-header-tabs" id="fornitoreTicketTabs" role="tablist">
|
|
||||||
<li class="nav-item" role="presentation"><button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tab-scheda" type="button">1. Scheda Segnalazione</button></li>
|
|
||||||
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-operativo" type="button">2. Operativo e Ricambi</button></li>
|
|
||||||
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-comunicazioni" type="button">3. Comunicazioni</button></li>
|
|
||||||
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-chiusura" type="button">4. Chiusura e Proforma</button></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-content">
|
|
||||||
<div class="tab-pane fade show active" id="tab-scheda">
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-lg-8">
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-header">Dati segnalazione</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="mb-2"><strong>Richiedente:</strong> {{ $caller['contatto'] ?: '-' }}</div>
|
|
||||||
<div class="mb-2"><strong>Telefono:</strong>
|
|
||||||
@if(!empty($caller['telefono']))
|
|
||||||
<a href="tel:{{ preg_replace('/\s+/', '', (string) $caller['telefono']) }}">{{ $caller['telefono'] }}</a>
|
|
||||||
@else
|
|
||||||
-
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
<div class="mb-2"><strong>Aperto da:</strong> {{ $intervento->ticket->apertoDaUser->name ?? '-' }}</div>
|
|
||||||
<div class="mb-2"><strong>Assegnato a fornitore da:</strong> {{ $intervento->ticket->assegnatoAUser->name ?? '-' }}</div>
|
|
||||||
<div class="mb-0"><strong>Descrizione:</strong>
|
|
||||||
<div class="mt-1 text-muted" style="white-space: pre-wrap;">{{ $intervento->ticket->descrizione }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-header">Foto e documenti segnalati</div>
|
|
||||||
<div class="card-body">
|
|
||||||
@if($intervento->ticket->attachments->isEmpty())
|
|
||||||
<p class="text-muted mb-0">Nessun allegato disponibile.</p>
|
|
||||||
@else
|
|
||||||
<div class="row g-3">
|
|
||||||
@foreach($intervento->ticket->attachments as $a)
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="border rounded p-2 h-100">
|
|
||||||
<div class="small fw-semibold">{{ $a->original_file_name }}</div>
|
|
||||||
<div class="small text-muted mb-2">{{ $a->description ?: 'Nessuna descrizione' }}</div>
|
|
||||||
<a class="btn btn-sm btn-outline-primary" href="{{ route('admin.tickets.attachments.view', ['attachment' => (int) $a->id]) }}" target="_blank">Apri</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-lg-4">
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-header">Stato operativo</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="mb-2"><strong>Stato:</strong> <span class="badge bg-info text-dark">{{ $intervento->stato }}</span></div>
|
|
||||||
<div class="mb-2"><strong>Operatore:</strong> {{ $intervento->eseguitoDaDipendente?->nome_completo ?: 'Responsabile fornitore' }}</div>
|
|
||||||
<div class="mb-2"><strong>Iniziato:</strong> {{ optional($intervento->iniziato_at)->format('d/m/Y H:i') ?: '-' }}</div>
|
|
||||||
<div class="mb-2"><strong>Terminato:</strong> {{ optional($intervento->terminato_at)->format('d/m/Y H:i') ?: '-' }}</div>
|
|
||||||
<div class="mb-2"><strong>Rif. chiusura:</strong> {{ $intervento->riferimento_chiusura ?: '-' }}</div>
|
|
||||||
<div class="mb-0"><strong>QR token:</strong> {{ $intervento->qr_token ?: '-' }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">Storico stesso richiedente/stabile</div>
|
|
||||||
<div class="card-body">
|
|
||||||
@forelse($storico as $old)
|
|
||||||
<div class="border-bottom pb-2 mb-2">
|
|
||||||
<div class="small fw-semibold">#{{ $old->ticket_id }} - {{ $old->ticket->titolo ?? '-' }}</div>
|
|
||||||
<div class="small text-muted">{{ $old->stato }} · {{ optional($old->updated_at)->format('d/m/Y H:i') }}</div>
|
|
||||||
@if($old->note_amministratore)
|
|
||||||
<div class="small text-muted">Nota admin: {{ \Illuminate\Support\Str::limit($old->note_amministratore, 90) }}</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
@empty
|
|
||||||
<p class="text-muted mb-0">Nessuno storico correlato.</p>
|
|
||||||
@endforelse
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-pane fade" id="tab-operativo">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">Rapporto operativo, ricambi e apparato</div>
|
|
||||||
<div class="card-body">
|
|
||||||
@if($intervento->stato === 'assegnato')
|
|
||||||
<form method="POST" action="{{ route('fornitore.tickets.start', $intervento) }}" class="mb-3">
|
|
||||||
@csrf
|
|
||||||
<button type="submit" class="btn btn-primary">Prendi in carico</button>
|
|
||||||
</form>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
<form method="POST" action="{{ route('fornitore.tickets.complete', $intervento) }}" enctype="multipart/form-data">
|
|
||||||
@csrf
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label" for="rapporto_fornitore">Rapporto intervento</label>
|
|
||||||
<textarea class="form-control" name="rapporto_fornitore" id="rapporto_fornitore" rows="5" required>{{ old('rapporto_fornitore', $intervento->rapporto_fornitore) }}</textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row g-2 mb-3">
|
|
||||||
<div class="col-md-4"><input type="text" class="form-control" name="apparato_marca" value="{{ old('apparato_marca') }}" placeholder="Marca apparato"></div>
|
|
||||||
<div class="col-md-4"><input type="text" class="form-control" name="apparato_modello" value="{{ old('apparato_modello') }}" placeholder="Modello"></div>
|
|
||||||
<div class="col-md-4"><input type="text" class="form-control" name="apparato_seriale" value="{{ old('apparato_seriale') }}" placeholder="Seriale"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label" for="tempo_minuti">Tempo impiegato (minuti)</label>
|
|
||||||
<input type="number" min="1" max="1440" class="form-control" name="tempo_minuti" id="tempo_minuti" value="{{ old('tempo_minuti', $intervento->tempo_minuti) }}">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Ricambi/attivita utilizzati</label>
|
|
||||||
@php($items = old('articoli_utilizzati', $intervento->articoli_utilizzati ?? []))
|
|
||||||
@for($i = 0; $i < 10; $i++)
|
|
||||||
<input type="text" class="form-control mb-2" name="articoli_utilizzati[]" value="{{ $items[$i] ?? '' }}" placeholder="Ricambio o attivita {{ $i + 1 }}">
|
|
||||||
@endfor
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row g-2 mb-3">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">Foto lavoro (max 10)</label>
|
|
||||||
<input type="file" class="form-control" name="foto_lavoro[]" accept="image/*" multiple>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">Documenti lavoro (max 10)</label>
|
|
||||||
<input type="file" class="form-control" name="documenti_lavoro[]" accept=".pdf,.doc,.docx,.xls,.xlsx,.txt,image/*" multiple>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@for($i = 0; $i < 10; $i++)
|
|
||||||
<input type="text" class="form-control mb-2" name="descrizione_file[]" value="{{ old('descrizione_file.' . $i) }}" placeholder="Descrizione file #{{ $i + 1 }} (opzionale)">
|
|
||||||
@endfor
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label" for="messaggio_veloce">Messaggio veloce allo studio (nelle note)</label>
|
|
||||||
<textarea class="form-control" name="messaggio_veloce" id="messaggio_veloce" rows="2" placeholder="Messaggio rapido">{{ old('messaggio_veloce') }}</textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-success">Salva avanzamento</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-pane fade" id="tab-comunicazioni">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">Note e comunicazioni ticket</div>
|
|
||||||
<div class="card-body">
|
|
||||||
@forelse($intervento->ticket->messages as $msg)
|
|
||||||
<div class="border-bottom pb-2 mb-2">
|
|
||||||
<div class="small fw-semibold">{{ $msg->user->name ?? 'Sistema' }} · {{ optional($msg->created_at)->format('d/m/Y H:i') }}</div>
|
|
||||||
<div class="small" style="white-space: pre-wrap;">{{ $msg->messaggio }}</div>
|
|
||||||
</div>
|
|
||||||
@empty
|
|
||||||
<p class="text-muted mb-0">Nessuna comunicazione disponibile.</p>
|
|
||||||
@endforelse
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-pane fade" id="tab-chiusura">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">Chiusura rapportino e proforma</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form method="POST" action="{{ route('fornitore.tickets.complete', $intervento) }}" enctype="multipart/form-data">
|
|
||||||
@csrf
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Rapporto finale</label>
|
|
||||||
<textarea class="form-control" name="rapporto_fornitore" rows="4" required>{{ old('rapporto_fornitore', $intervento->rapporto_fornitore) }}</textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row g-2 mb-3">
|
|
||||||
<div class="col-md-4"><input type="text" class="form-control" name="proforma_codice" value="{{ old('proforma_codice') }}" placeholder="Codice proforma/FE"></div>
|
|
||||||
<div class="col-md-8"><input type="text" class="form-control" name="proforma_note" value="{{ old('proforma_note') }}" placeholder="Note proforma/autorizzazione"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-check mb-2">
|
|
||||||
<input class="form-check-input" type="checkbox" name="richiesta_proforma" value="1" id="richiesta_proforma" {{ old('richiesta_proforma') ? 'checked' : '' }}>
|
|
||||||
<label class="form-check-label" for="richiesta_proforma">Richiedi autorizzazione proforma</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check mb-3">
|
|
||||||
<input class="form-check-input" type="checkbox" name="lavoro_standard" value="1" id="lavoro_standard" {{ old('lavoro_standard') ? 'checked' : '' }}>
|
|
||||||
<label class="form-check-label" for="lavoro_standard">Lavoro standard gia autorizzato (passa a fatturabile)</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label" for="qr_token">QR token (se scansionato)</label>
|
|
||||||
<input type="text" class="form-control" name="qr_token" id="qr_token" placeholder="Inserisci il codice QR" value="{{ old('qr_token') }}">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-success">Chiudi rapportino e invia</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@endsection
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user