feat(unita): multicanale email split, riorganizzazione tab millesimi/comunicazioni, protocollo protoc_ec e contabilità BPM
This commit is contained in:
parent
b58beb853b
commit
4c4de5e734
|
|
@ -266,6 +266,128 @@ public function handle(): int
|
|||
}
|
||||
}
|
||||
|
||||
// 9. Sincronizzazione tabelle Protocollo Comunicazioni ed Estratti Conto da generale_stabile.mdb
|
||||
$mdbPath = '/mnt/gescon-archives/gescon/0013/generale_stabile.mdb';
|
||||
if (file_exists($mdbPath)) {
|
||||
if (! Schema::connection('gescon_import')->hasTable('protoc_ec')) {
|
||||
Schema::connection('gescon_import')->create('protoc_ec', function ($table) {
|
||||
$table->increments('id');
|
||||
$table->string('cod_stabile')->index();
|
||||
$table->integer('id_corrisp')->nullable();
|
||||
$table->string('protocollo')->nullable()->index();
|
||||
$table->string('tipo_documento')->nullable();
|
||||
$table->string('data_invio')->nullable();
|
||||
$table->string('forn_cond_altro')->nullable();
|
||||
$table->string('codice')->nullable();
|
||||
$table->string('destinatario')->nullable();
|
||||
$table->string('oggetto')->nullable();
|
||||
$table->text('note')->nullable();
|
||||
$table->string('scala')->nullable()->index();
|
||||
$table->string('interno')->nullable()->index();
|
||||
$table->string('c_i')->nullable();
|
||||
$table->string('nome')->nullable();
|
||||
$table->decimal('totale', 12, 2)->default(0);
|
||||
$table->string('nome_pdf')->nullable();
|
||||
$table->integer('id_condomino')->nullable()->index();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
if (! Schema::connection('gescon_import')->hasTable('corrisp_inviata')) {
|
||||
Schema::connection('gescon_import')->create('corrisp_inviata', function ($table) {
|
||||
$table->increments('id');
|
||||
$table->string('cod_stabile')->index();
|
||||
$table->integer('id_corrisp')->nullable();
|
||||
$table->string('protocollo')->nullable()->index();
|
||||
$table->string('tipo_documento')->nullable();
|
||||
$table->string('data_invio')->nullable();
|
||||
$table->string('forn_cond_altro')->nullable();
|
||||
$table->string('codice')->nullable();
|
||||
$table->string('destinatario')->nullable();
|
||||
$table->string('oggetto')->nullable();
|
||||
$table->string('lettera_tipo_caricata')->nullable();
|
||||
$table->text('note')->nullable();
|
||||
$table->integer('id_cond_for')->nullable()->index();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Protoc_EC
|
||||
$csvEc = shell_exec('mdb-export ' . escapeshellarg($mdbPath) . ' Protoc_EC 2>/dev/null');
|
||||
if ($csvEc) {
|
||||
$lines = explode("\n", $csvEc);
|
||||
$header = str_getcsv(array_shift($lines));
|
||||
$ecRows = [];
|
||||
foreach ($lines as $l) {
|
||||
if (trim($l) === '') continue;
|
||||
$data = str_getcsv($l);
|
||||
if (count($data) < count($header)) continue;
|
||||
$row = array_combine($header, $data);
|
||||
$ecRows[] = [
|
||||
'cod_stabile' => '0013',
|
||||
'id_corrisp' => is_numeric($row['ID_corrisp'] ?? null) ? (int) $row['ID_corrisp'] : null,
|
||||
'protocollo' => $row['protocollo'] ?? null,
|
||||
'tipo_documento' => $row['Tipo_documento'] ?? null,
|
||||
'data_invio' => $row['Data'] ?? null,
|
||||
'forn_cond_altro' => $row['Forn_Cond_Altro'] ?? null,
|
||||
'codice' => $row['Codice'] ?? null,
|
||||
'destinatario' => $row['Destinatario'] ?? null,
|
||||
'oggetto' => $row['Oggetto'] ?? null,
|
||||
'note' => $row['Note'] ?? null,
|
||||
'scala' => $row['Scala'] ?? null,
|
||||
'interno' => $row['Interno'] ?? null,
|
||||
'c_i' => $row['C_I'] ?? null,
|
||||
'nome' => $row['Nome'] ?? null,
|
||||
'totale' => is_numeric($row['Totale'] ?? null) ? (float) $row['Totale'] : 0,
|
||||
'nome_pdf' => $row['Nome_pdf'] ?? null,
|
||||
'id_condomino' => is_numeric($row['id_condomino'] ?? null) ? (int) $row['id_condomino'] : null,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
DB::connection('gescon_import')->table('protoc_ec')->where('cod_stabile', '0013')->delete();
|
||||
DB::connection('gescon_import')->table('protoc_ec')->insert($ecRows);
|
||||
$this->info('Protoc_EC sincronizzato: ' . count($ecRows) . ' record.');
|
||||
}
|
||||
|
||||
// Corrisp_inviata
|
||||
$csvCorr = shell_exec('mdb-export ' . escapeshellarg($mdbPath) . ' Corrisp_inviata 2>/dev/null');
|
||||
if ($csvCorr) {
|
||||
$lines = explode("\n", $csvCorr);
|
||||
$header = str_getcsv(array_shift($lines));
|
||||
$corrRows = [];
|
||||
foreach ($lines as $l) {
|
||||
if (trim($l) === '') continue;
|
||||
$data = str_getcsv($l);
|
||||
if (count($data) < count($header)) continue;
|
||||
$row = array_combine($header, $data);
|
||||
$corrRows[] = [
|
||||
'cod_stabile' => '0013',
|
||||
'id_corrisp' => is_numeric($row['ID_corrisp'] ?? null) ? (int) $row['ID_corrisp'] : null,
|
||||
'protocollo' => $row['protocollo'] ?? null,
|
||||
'tipo_documento' => $row['Tipo_documento'] ?? null,
|
||||
'data_invio' => $row['Data'] ?? null,
|
||||
'forn_cond_altro' => $row['Forn_Cond_Altro'] ?? null,
|
||||
'codice' => $row['Codice'] ?? null,
|
||||
'destinatario' => $row['Destinatario'] ?? null,
|
||||
'oggetto' => $row['Oggetto'] ?? null,
|
||||
'lettera_tipo_caricata' => $row['lettera_tipo_caricata'] ?? ($row['Lettera_tipo_caricata'] ?? null),
|
||||
'note' => $row['Note'] ?? null,
|
||||
'id_cond_for' => is_numeric($row['id_cond_for'] ?? null) ? (int) $row['id_cond_for'] : null,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
DB::connection('gescon_import')->table('corrisp_inviata')->where('cod_stabile', '0013')->delete();
|
||||
DB::connection('gescon_import')->table('corrisp_inviata')->insert($corrRows);
|
||||
$this->info('Corrisp_inviata sincronizzato: ' . count($corrRows) . ' record.');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->warn('Avviso sincronizzazione protocollo: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('Bonifica Stabile 0013 completata con successo!');
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,11 +118,11 @@ public static function canAccess(): bool
|
|||
return false;
|
||||
}
|
||||
|
||||
if ($user->hasAnyRole(['super-admin', 'admin'])) {
|
||||
if ($user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'super_admin'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $user->can('contabilita.casse-banche');
|
||||
return $user->can('contabilita.casse-banche') || ($user->is_super_admin ?? false) || ($user->is_admin ?? false);
|
||||
}
|
||||
|
||||
public function mount(): void
|
||||
|
|
@ -772,6 +772,7 @@ protected function getHeaderActions(): array
|
|||
->options(function (): array {
|
||||
return [
|
||||
'auto_csv' => 'CSV banca (rilevamento automatico)',
|
||||
'bpm_csv' => 'Banco BPM CSV',
|
||||
'mps_qif' => 'MPS QIF',
|
||||
'unicredit_wri' => 'Unicredit WRI',
|
||||
'intesa_csv' => 'Intesa CSV',
|
||||
|
|
@ -780,7 +781,7 @@ protected function getHeaderActions(): array
|
|||
})
|
||||
->default(fn(): string => $this->resolvePreferredImportFormatForConto($this->contoId))
|
||||
->required()
|
||||
->helperText('Un solo flusso di import: scegli il formato corretto per il conto selezionato. Per Monte dei Paschi usa QIF.'),
|
||||
->helperText('Un solo flusso di import: scegli il formato corretto per il conto selezionato (Banco BPM, MPS, Unicredit, Intesa).'),
|
||||
|
||||
Select::make('gestione_id')
|
||||
->label('Gestione (opzionale)')
|
||||
|
|
@ -900,6 +901,14 @@ protected function getHeaderActions(): array
|
|||
$gestioneId,
|
||||
$replace,
|
||||
),
|
||||
'bpm_csv' => $importer->importBpmCsvForConto(
|
||||
Storage::disk('local')->get($path),
|
||||
$stabileId,
|
||||
$contoId,
|
||||
$filename,
|
||||
$gestioneId,
|
||||
$replace,
|
||||
),
|
||||
'unicredit_wri' => $importer->importUnicreditWriForConto(
|
||||
Storage::disk('local')->get($path),
|
||||
$stabileId,
|
||||
|
|
@ -3980,6 +3989,10 @@ protected function resolvePreferredImportFormatForConto(?int $contoId): string
|
|||
$conto['numero_conto'] ?? null,
|
||||
], static fn($value): bool => is_string($value) && trim($value) !== '')));
|
||||
|
||||
if (str_contains($haystack, 'bpm') || str_contains($haystack, 'banco bpm') || str_contains($haystack, 'popolare di milano')) {
|
||||
return 'bpm_csv';
|
||||
}
|
||||
|
||||
if (str_contains($haystack, 'mps') || str_contains($haystack, 'monte dei paschi') || str_contains($haystack, 'paschi')) {
|
||||
return 'mps_qif';
|
||||
}
|
||||
|
|
@ -4007,6 +4020,7 @@ protected function makeMovimentiImporter(): MovimentiBancaImporter
|
|||
null,
|
||||
null,
|
||||
new MpsQifParser(),
|
||||
new \App\Services\Contabilita\BpmCsvParser(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -69,11 +69,11 @@ public static function canAccess(): bool
|
|||
return false;
|
||||
}
|
||||
|
||||
if ($user->hasAnyRole(['super-admin', 'admin'])) {
|
||||
if ($user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'super_admin'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $user->can('contabilita.casse-banche');
|
||||
return $user->can('contabilita.casse-banche') || ($user->is_super_admin ?? false) || ($user->is_admin ?? false);
|
||||
}
|
||||
|
||||
public function mount(): void
|
||||
|
|
|
|||
|
|
@ -540,75 +540,119 @@ public function getRecapitiMulticanaleTableProperty(): array
|
|||
}
|
||||
|
||||
$anagId = (int) ($entity['persona_id'] ?? ($entity['soggetto_id'] ?? 0));
|
||||
$anag = $anagId > 0 ? DB::table('anagrafiche')->find($anagId) : null;
|
||||
$anag = null;
|
||||
if ($anagId > 0) {
|
||||
if (DbSchema::hasTable('anagrafiche')) {
|
||||
$anag = DB::table('anagrafiche')->find($anagId);
|
||||
}
|
||||
if (! $anag && DbSchema::hasTable('persone')) {
|
||||
$anag = DB::table('persone')->find($anagId);
|
||||
}
|
||||
}
|
||||
if (! $anag) {
|
||||
$anag = DB::table('anagrafiche')
|
||||
->where(function ($q) use ($nome, $entity) {
|
||||
$q->where('cognome', 'like', '%' . $nome . '%')
|
||||
->orWhere('nome', 'like', '%' . $nome . '%')
|
||||
->orWhere('ragione_sociale', 'like', '%' . $nome . '%');
|
||||
if (! empty($entity['codice_fiscale'])) {
|
||||
$q->orWhere('codice_fiscale', trim((string) $entity['codice_fiscale']));
|
||||
}
|
||||
})
|
||||
->first();
|
||||
if (DbSchema::hasTable('anagrafiche')) {
|
||||
$anag = DB::table('anagrafiche')
|
||||
->where(function ($q) use ($nome, $entity) {
|
||||
$q->where('cognome', 'like', '%' . $nome . '%')
|
||||
->orWhere('nome', 'like', '%' . $nome . '%')
|
||||
->orWhere('ragione_sociale', 'like', '%' . $nome . '%');
|
||||
if (! empty($entity['codice_fiscale'])) {
|
||||
$q->orWhere('codice_fiscale', trim((string) $entity['codice_fiscale']));
|
||||
}
|
||||
})
|
||||
->first();
|
||||
}
|
||||
if (! $anag && DbSchema::hasTable('persone')) {
|
||||
$anag = DB::table('persone')
|
||||
->where(function ($q) use ($nome, $entity) {
|
||||
$q->where('cognome', 'like', '%' . $nome . '%')
|
||||
->orWhere('nome', 'like', '%' . $nome . '%');
|
||||
if (! empty($entity['codice_fiscale'])) {
|
||||
$q->orWhere('codice_fiscale', trim((string) $entity['codice_fiscale']));
|
||||
}
|
||||
})
|
||||
->first();
|
||||
}
|
||||
}
|
||||
|
||||
if ($anag) {
|
||||
if (! empty($anag->email)) {
|
||||
$tableRows[] = [
|
||||
'canale' => 'Email',
|
||||
'icon' => '✉️',
|
||||
'color' => 'bg-blue-50 text-blue-700 border-blue-200',
|
||||
'etichetta' => 'Principale',
|
||||
'soggetto' => $nome,
|
||||
'ruolo' => $ruolo,
|
||||
'valore' => $anag->email,
|
||||
'action_url' => 'mailto:' . $anag->email,
|
||||
'is_principal' => true,
|
||||
];
|
||||
$rawEmail = ! empty($anag->email) ? (string) $anag->email : (! empty($anag->email_principale) ? (string) $anag->email_principale : (! empty($entity['email']) ? (string) $entity['email'] : ''));
|
||||
$rawPec = ! empty($anag->pec) ? (string) $anag->pec : (! empty($anag->email_pec) ? (string) $anag->email_pec : (! empty($entity['pec']) ? (string) $entity['pec'] : ''));
|
||||
$rawTel = ! empty($anag->telefono) ? (string) $anag->telefono : (! empty($anag->telefono_principale) ? (string) $anag->telefono_principale : (! empty($entity['telefono']) ? (string) $entity['telefono'] : ''));
|
||||
$rawInd = ! empty($anag->indirizzo) ? trim($anag->indirizzo . ' ' . ($anag->cap ?? '') . ' ' . ($anag->citta ?? '') . ' ' . ($anag->provincia ? '(' . $anag->provincia . ')' : '')) : (! empty($anag->residenza_via) ? (string) $anag->residenza_via : (! empty($entity['indirizzo']) ? (string) $entity['indirizzo'] : ''));
|
||||
|
||||
if ($rawEmail !== '') {
|
||||
$parts = preg_split('/[,;\s]+/', $rawEmail, -1, PREG_SPLIT_NO_EMPTY);
|
||||
foreach ($parts as $idx => $em) {
|
||||
$em = trim($em);
|
||||
if ($em !== '' && ! $this->hasContactValue($tableRows, $em)) {
|
||||
$prefix = explode('@', $em)[0];
|
||||
$cleanPrefix = ucwords(str_replace(['.', '_', '-'], ' ', $prefix));
|
||||
$lbl = count($parts) > 1 ? ($idx === 0 ? 'Principale (' . $cleanPrefix . ')' : 'Email ' . $cleanPrefix) : 'Principale';
|
||||
$tableRows[] = [
|
||||
'canale' => 'Email',
|
||||
'icon' => '✉️',
|
||||
'color' => 'bg-blue-50 text-blue-700 border-blue-200',
|
||||
'etichetta' => $lbl,
|
||||
'soggetto' => $nome,
|
||||
'ruolo' => $ruolo,
|
||||
'valore' => $em,
|
||||
'action_url' => 'mailto:' . $em,
|
||||
'is_principal' => $idx === 0 && count($tableRows) === 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
if (! empty($anag->pec)) {
|
||||
$tableRows[] = [
|
||||
'canale' => 'Email PEC',
|
||||
'icon' => '🛡️',
|
||||
'color' => 'bg-emerald-50 text-emerald-700 border-emerald-200',
|
||||
'etichetta' => 'PEC Ufficiale',
|
||||
'soggetto' => $nome,
|
||||
'ruolo' => $ruolo,
|
||||
'valore' => $anag->pec,
|
||||
'action_url' => 'mailto:' . $anag->pec,
|
||||
'is_principal' => false,
|
||||
];
|
||||
}
|
||||
if ($rawPec !== '') {
|
||||
$parts = preg_split('/[,;\s]+/', $rawPec, -1, PREG_SPLIT_NO_EMPTY);
|
||||
foreach ($parts as $idx => $pc) {
|
||||
$pc = trim($pc);
|
||||
if ($pc !== '' && ! $this->hasContactValue($tableRows, $pc)) {
|
||||
$tableRows[] = [
|
||||
'canale' => 'Email PEC',
|
||||
'icon' => '🛡️',
|
||||
'color' => 'bg-emerald-50 text-emerald-700 border-emerald-200',
|
||||
'etichetta' => count($parts) > 1 ? 'PEC #' . ($idx + 1) : 'PEC Ufficiale',
|
||||
'soggetto' => $nome,
|
||||
'ruolo' => $ruolo,
|
||||
'valore' => $pc,
|
||||
'action_url' => 'mailto:' . $pc,
|
||||
'is_principal' => false,
|
||||
];
|
||||
}
|
||||
}
|
||||
if (! empty($anag->telefono)) {
|
||||
$tableRows[] = [
|
||||
'canale' => 'Cellulare / Tel',
|
||||
'icon' => '📱',
|
||||
'color' => 'bg-amber-50 text-amber-700 border-amber-200',
|
||||
'etichetta' => 'Personale',
|
||||
'soggetto' => $nome,
|
||||
'ruolo' => $ruolo,
|
||||
'valore' => $anag->telefono,
|
||||
'action_url' => 'tel:' . $anag->telefono,
|
||||
'is_principal' => false,
|
||||
];
|
||||
}
|
||||
if (! empty($anag->indirizzo)) {
|
||||
$indFull = trim($anag->indirizzo . ' ' . ($anag->cap ?? '') . ' ' . ($anag->citta ?? '') . ' ' . ($anag->provincia ? '(' . $anag->provincia . ')' : ''));
|
||||
$tableRows[] = [
|
||||
'canale' => 'Indirizzo',
|
||||
'icon' => '📍',
|
||||
'color' => 'bg-slate-50 text-slate-700 border-slate-200',
|
||||
'etichetta' => 'Residenza / Sede',
|
||||
'soggetto' => $nome,
|
||||
'ruolo' => $ruolo,
|
||||
'valore' => $indFull,
|
||||
'action_url' => null,
|
||||
'is_principal' => false,
|
||||
];
|
||||
}
|
||||
if ($rawTel !== '') {
|
||||
$parts = preg_split('/[,;\/]+/', $rawTel, -1, PREG_SPLIT_NO_EMPTY);
|
||||
foreach ($parts as $idx => $ph) {
|
||||
$ph = trim($ph);
|
||||
if ($ph !== '' && ! $this->hasContactValue($tableRows, $ph)) {
|
||||
$tableRows[] = [
|
||||
'canale' => 'Cellulare / Tel',
|
||||
'icon' => '📱',
|
||||
'color' => 'bg-amber-50 text-amber-700 border-amber-200',
|
||||
'etichetta' => count($parts) > 1 ? 'Telefono #' . ($idx + 1) : 'Personale',
|
||||
'soggetto' => $nome,
|
||||
'ruolo' => $ruolo,
|
||||
'valore' => $ph,
|
||||
'action_url' => 'tel:' . preg_replace('/[^\d\+]/', '', $ph),
|
||||
'is_principal' => false,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($rawInd !== '') {
|
||||
$tableRows[] = [
|
||||
'canale' => 'Indirizzo',
|
||||
'icon' => '📍',
|
||||
'color' => 'bg-slate-50 text-slate-700 border-slate-200',
|
||||
'etichetta' => 'Residenza / Sede',
|
||||
'soggetto' => $nome,
|
||||
'ruolo' => $ruolo,
|
||||
'valore' => $rawInd,
|
||||
'action_url' => null,
|
||||
'is_principal' => false,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Dati da Legacy condomin_mirror
|
||||
|
|
@ -618,83 +662,125 @@ public function getRecapitiMulticanaleTableProperty(): array
|
|||
$condName = trim((string) ($legacyRow->nom_cond ?? 'Condomino'));
|
||||
$inqName = trim((string) ($legacyRow->inquil_nome ?? ($legacyRow->inquilino ?? 'Inquilino')));
|
||||
|
||||
if (! empty($p['E_mail_condomino']) && ! $this->hasContactValue($tableRows, (string) $p['E_mail_condomino'])) {
|
||||
$tableRows[] = [
|
||||
'canale' => 'Email',
|
||||
'icon' => '✉️',
|
||||
'color' => 'bg-blue-50 text-blue-700 border-blue-200',
|
||||
'etichetta' => 'Email Condomino',
|
||||
'soggetto' => $condName,
|
||||
'ruolo' => 'Condomino',
|
||||
'valore' => trim((string) $p['E_mail_condomino']),
|
||||
'action_url' => 'mailto:' . trim((string) $p['E_mail_condomino']),
|
||||
'is_principal' => count($tableRows) === 0,
|
||||
];
|
||||
if (! empty($p['E_mail_condomino'])) {
|
||||
$parts = preg_split('/[,;\s]+/', (string) $p['E_mail_condomino'], -1, PREG_SPLIT_NO_EMPTY);
|
||||
foreach ($parts as $idx => $em) {
|
||||
$em = trim($em);
|
||||
if ($em !== '' && ! $this->hasContactValue($tableRows, $em)) {
|
||||
$prefix = explode('@', $em)[0];
|
||||
$cleanPrefix = ucwords(str_replace(['.', '_', '-'], ' ', $prefix));
|
||||
$lbl = count($parts) > 1 ? ($idx === 0 ? 'Email Condomino (' . $cleanPrefix . ')' : 'Email ' . $cleanPrefix) : 'Email Condomino';
|
||||
$tableRows[] = [
|
||||
'canale' => 'Email',
|
||||
'icon' => '✉️',
|
||||
'color' => 'bg-blue-50 text-blue-700 border-blue-200',
|
||||
'etichetta' => $lbl,
|
||||
'soggetto' => $condName,
|
||||
'ruolo' => 'Condomino',
|
||||
'valore' => $em,
|
||||
'action_url' => 'mailto:' . $em,
|
||||
'is_principal' => count($tableRows) === 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (! empty($p['PEC_condomino']) && ! $this->hasContactValue($tableRows, (string) $p['PEC_condomino'])) {
|
||||
$tableRows[] = [
|
||||
'canale' => 'Email PEC',
|
||||
'icon' => '🛡️',
|
||||
'color' => 'bg-emerald-50 text-emerald-700 border-emerald-200',
|
||||
'etichetta' => 'PEC Condomino',
|
||||
'soggetto' => $condName,
|
||||
'ruolo' => 'Condomino',
|
||||
'valore' => trim((string) $p['PEC_condomino']),
|
||||
'action_url' => 'mailto:' . trim((string) $p['PEC_condomino']),
|
||||
'is_principal' => false,
|
||||
];
|
||||
if (! empty($p['PEC_condomino'])) {
|
||||
$parts = preg_split('/[,;\s]+/', (string) $p['PEC_condomino'], -1, PREG_SPLIT_NO_EMPTY);
|
||||
foreach ($parts as $idx => $pc) {
|
||||
$pc = trim($pc);
|
||||
if ($pc !== '' && ! $this->hasContactValue($tableRows, $pc)) {
|
||||
$tableRows[] = [
|
||||
'canale' => 'Email PEC',
|
||||
'icon' => '🛡️',
|
||||
'color' => 'bg-emerald-50 text-emerald-700 border-emerald-200',
|
||||
'etichetta' => count($parts) > 1 ? 'PEC Condomino #' . ($idx + 1) : 'PEC Condomino',
|
||||
'soggetto' => $condName,
|
||||
'ruolo' => 'Condomino',
|
||||
'valore' => $pc,
|
||||
'action_url' => 'mailto:' . $pc,
|
||||
'is_principal' => false,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (! empty($p['Cell_cond']) && ! $this->hasContactValue($tableRows, (string) $p['Cell_cond'])) {
|
||||
$tableRows[] = [
|
||||
'canale' => 'Cellulare',
|
||||
'icon' => '📱',
|
||||
'color' => 'bg-amber-50 text-amber-700 border-amber-200',
|
||||
'etichetta' => 'Cellulare Condomino',
|
||||
'soggetto' => $condName,
|
||||
'ruolo' => 'Condomino',
|
||||
'valore' => trim((string) $p['Cell_cond']),
|
||||
'action_url' => 'tel:' . trim((string) $p['Cell_cond']),
|
||||
'is_principal' => false,
|
||||
];
|
||||
if (! empty($p['Cell_cond'])) {
|
||||
$parts = preg_split('/[,;\/]+/', (string) $p['Cell_cond'], -1, PREG_SPLIT_NO_EMPTY);
|
||||
foreach ($parts as $idx => $ph) {
|
||||
$ph = trim($ph);
|
||||
if ($ph !== '' && ! $this->hasContactValue($tableRows, $ph)) {
|
||||
$tableRows[] = [
|
||||
'canale' => 'Cellulare',
|
||||
'icon' => '📱',
|
||||
'color' => 'bg-amber-50 text-amber-700 border-amber-200',
|
||||
'etichetta' => count($parts) > 1 ? 'Cellulare Condomino #' . ($idx + 1) : 'Cellulare Condomino',
|
||||
'soggetto' => $condName,
|
||||
'ruolo' => 'Condomino',
|
||||
'valore' => $ph,
|
||||
'action_url' => 'tel:' . preg_replace('/[^\d\+]/', '', $ph),
|
||||
'is_principal' => false,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (! empty($p['tel1']) && ! $this->hasContactValue($tableRows, (string) $p['tel1'])) {
|
||||
$tableRows[] = [
|
||||
'canale' => 'Telefono Fisso',
|
||||
'icon' => '☎️',
|
||||
'color' => 'bg-purple-50 text-purple-700 border-purple-200',
|
||||
'etichetta' => 'Fisso Condomino',
|
||||
'soggetto' => $condName,
|
||||
'ruolo' => 'Condomino',
|
||||
'valore' => trim((string) $p['tel1']),
|
||||
'action_url' => 'tel:' . trim((string) $p['tel1']),
|
||||
'is_principal' => false,
|
||||
];
|
||||
if (! empty($p['tel1'])) {
|
||||
$parts = preg_split('/[,;\/]+/', (string) $p['tel1'], -1, PREG_SPLIT_NO_EMPTY);
|
||||
foreach ($parts as $idx => $ph) {
|
||||
$ph = trim($ph);
|
||||
if ($ph !== '' && ! $this->hasContactValue($tableRows, $ph)) {
|
||||
$tableRows[] = [
|
||||
'canale' => 'Telefono Fisso',
|
||||
'icon' => '☎️',
|
||||
'color' => 'bg-purple-50 text-purple-700 border-purple-200',
|
||||
'etichetta' => count($parts) > 1 ? 'Fisso Condomino #' . ($idx + 1) : 'Fisso Condomino',
|
||||
'soggetto' => $condName,
|
||||
'ruolo' => 'Condomino',
|
||||
'valore' => $ph,
|
||||
'action_url' => 'tel:' . preg_replace('/[^\d\+]/', '', $ph),
|
||||
'is_principal' => false,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (! empty($p['E_mail_inquilino']) && ! $this->hasContactValue($tableRows, (string) $p['E_mail_inquilino'])) {
|
||||
$tableRows[] = [
|
||||
'canale' => 'Email',
|
||||
'icon' => '✉️',
|
||||
'color' => 'bg-blue-50 text-blue-700 border-blue-200',
|
||||
'etichetta' => 'Email Inquilino',
|
||||
'soggetto' => $inqName,
|
||||
'ruolo' => 'Inquilino',
|
||||
'valore' => trim((string) $p['E_mail_inquilino']),
|
||||
'action_url' => 'mailto:' . trim((string) $p['E_mail_inquilino']),
|
||||
'is_principal' => false,
|
||||
];
|
||||
if (! empty($p['E_mail_inquilino'])) {
|
||||
$parts = preg_split('/[,;\s]+/', (string) $p['E_mail_inquilino'], -1, PREG_SPLIT_NO_EMPTY);
|
||||
foreach ($parts as $idx => $em) {
|
||||
$em = trim($em);
|
||||
if ($em !== '' && ! $this->hasContactValue($tableRows, $em)) {
|
||||
$prefix = explode('@', $em)[0];
|
||||
$cleanPrefix = ucwords(str_replace(['.', '_', '-'], ' ', $prefix));
|
||||
$lbl = count($parts) > 1 ? ($idx === 0 ? 'Email Inquilino (' . $cleanPrefix . ')' : 'Email ' . $cleanPrefix) : 'Email Inquilino';
|
||||
$tableRows[] = [
|
||||
'canale' => 'Email',
|
||||
'icon' => '✉️',
|
||||
'color' => 'bg-blue-50 text-blue-700 border-blue-200',
|
||||
'etichetta' => $lbl,
|
||||
'soggetto' => $inqName,
|
||||
'ruolo' => 'Inquilino',
|
||||
'valore' => $em,
|
||||
'action_url' => 'mailto:' . $em,
|
||||
'is_principal' => false,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (! empty($p['Cell_inq']) && ! $this->hasContactValue($tableRows, (string) $p['Cell_inq'])) {
|
||||
$tableRows[] = [
|
||||
'canale' => 'Cellulare',
|
||||
'icon' => '📱',
|
||||
'color' => 'bg-amber-50 text-amber-700 border-amber-200',
|
||||
'etichetta' => 'Cellulare Inquilino',
|
||||
'soggetto' => $inqName,
|
||||
'ruolo' => 'Inquilino',
|
||||
'valore' => trim((string) $p['Cell_inq']),
|
||||
'action_url' => 'tel:' . trim((string) $p['Cell_inq']),
|
||||
'is_principal' => false,
|
||||
];
|
||||
if (! empty($p['Cell_inq'])) {
|
||||
$parts = preg_split('/[,;\/]+/', (string) $p['Cell_inq'], -1, PREG_SPLIT_NO_EMPTY);
|
||||
foreach ($parts as $idx => $ph) {
|
||||
$ph = trim($ph);
|
||||
if ($ph !== '' && ! $this->hasContactValue($tableRows, $ph)) {
|
||||
$tableRows[] = [
|
||||
'canale' => 'Cellulare',
|
||||
'icon' => '📱',
|
||||
'color' => 'bg-amber-50 text-amber-700 border-amber-200',
|
||||
'etichetta' => count($parts) > 1 ? 'Cellulare Inquilino #' . ($idx + 1) : 'Cellulare Inquilino',
|
||||
'soggetto' => $inqName,
|
||||
'ruolo' => 'Inquilino',
|
||||
'valore' => $ph,
|
||||
'action_url' => 'tel:' . preg_replace('/[^\d\+]/', '', $ph),
|
||||
'is_principal' => false,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -965,6 +1051,20 @@ public function getEstrattoContoNominativoDettaglioProperty(): array
|
|||
? $this->estrattoRateRowsInquilino
|
||||
: $this->estrattoRateRowsProprietario;
|
||||
|
||||
// Allocazione intelligente degli incassi registrati sulle rate emesse
|
||||
$incassoPool = [];
|
||||
foreach ($dettaglioIncassi as $idx => $incItem) {
|
||||
$incassoPool[] = [
|
||||
'idx' => $idx,
|
||||
'importo' => (float) ($incItem['importo_euro'] ?? 0),
|
||||
'residuo' => (float) ($incItem['importo_euro'] ?? 0),
|
||||
'data_pagamento' => (string) ($incItem['data_pagamento'] ?? '—'),
|
||||
'cod_cassa' => (string) ($incItem['cod_cassa'] ?? '—'),
|
||||
'anno_rif' => (string) ($incItem['anno_rif'] ?? ''),
|
||||
'descrizione' => (string) ($incItem['descrizione'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
$rateEmesseRows = [];
|
||||
$totaleDovutoRate = 0.0;
|
||||
$totalePagatoRate = 0.0;
|
||||
|
|
@ -972,29 +1072,40 @@ public function getEstrattoContoNominativoDettaglioProperty(): array
|
|||
foreach ($rateList as $r) {
|
||||
$dovuto = (float) ($r['dovuto'] ?? 0);
|
||||
$pagato = (float) ($r['pagato'] ?? 0);
|
||||
$residuo = (float) ($r['residuo'] ?? ($dovuto - $pagato));
|
||||
|
||||
$dtPagamento = ! empty($r['data_pagamento']) && $r['data_pagamento'] !== '—' ? $r['data_pagamento'] : '—';
|
||||
$movimentoBancario = '—';
|
||||
|
||||
if (! empty($dettaglioIncassi)) {
|
||||
$gestioneRate = (string) ($r['gestione'] ?? '');
|
||||
foreach ($dettaglioIncassi as $incItem) {
|
||||
$yrInc = (string) ($incItem['anno_rif'] ?? '');
|
||||
if ($dtPagamento === '—' && ! empty($incItem['data_pagamento']) && $incItem['data_pagamento'] !== '—') {
|
||||
$dtPagamento = $incItem['data_pagamento'];
|
||||
if ($dovuto > 0 && ($pagato < $dovuto) && ! empty($incassoPool)) {
|
||||
$daPagare = round($dovuto - $pagato, 2);
|
||||
foreach ($incassoPool as &$incRef) {
|
||||
if ($incRef['residuo'] <= 0.0001) {
|
||||
continue;
|
||||
}
|
||||
if ($movimentoBancario === '—' && (! empty($incItem['cod_cassa']) || ! empty($incItem['descrizione']))) {
|
||||
$cassaStr = ! empty($incItem['cod_cassa']) && $incItem['cod_cassa'] !== '—' ? 'Mov. #' . $incItem['cod_cassa'] : '';
|
||||
$noteStr = ! empty($incItem['descrizione']) && $incItem['descrizione'] !== '—' ? $incItem['descrizione'] : '';
|
||||
$quota = min($incRef['residuo'], $daPagare);
|
||||
$pagato += $quota;
|
||||
$incRef['residuo'] -= $quota;
|
||||
$daPagare -= $quota;
|
||||
|
||||
if ($dtPagamento === '—' && ! empty($incRef['data_pagamento']) && $incRef['data_pagamento'] !== '—') {
|
||||
$dtPagamento = $incRef['data_pagamento'];
|
||||
}
|
||||
if ($movimentoBancario === '—' && (! empty($incRef['cod_cassa']) || ! empty($incRef['descrizione']))) {
|
||||
$cassaStr = ! empty($incRef['cod_cassa']) && $incRef['cod_cassa'] !== '—' ? 'Mov. #' . $incRef['cod_cassa'] : '';
|
||||
$noteStr = ! empty($incRef['descrizione']) && $incRef['descrizione'] !== '—' ? $incRef['descrizione'] : '';
|
||||
$movimentoBancario = trim($cassaStr . ($cassaStr !== '' && $noteStr !== '' ? ' · ' : '') . $noteStr);
|
||||
}
|
||||
if ($gestioneRate !== '' && str_contains($yrInc, $gestioneRate)) {
|
||||
|
||||
if ($daPagare <= 0.0001) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
unset($incRef);
|
||||
}
|
||||
|
||||
$pagato = round($pagato, 2);
|
||||
$residuo = max(0.0, round($dovuto - $pagato, 2));
|
||||
|
||||
$rateEmesseRows[] = [
|
||||
'id' => $r['id'] ?? null,
|
||||
'data_emissione' => $r['data_emissione'] ?? '—',
|
||||
|
|
@ -1029,6 +1140,103 @@ public function getEstrattoContoNominativoDettaglioProperty(): array
|
|||
];
|
||||
}
|
||||
|
||||
public function getProtocolloComunicazioniProperty(): array
|
||||
{
|
||||
if (! $this->unita || ! $this->unita->stabile) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$codStabile = (string) $this->unita->stabile->codice_stabile;
|
||||
$scala = trim((string) $this->unita->scala);
|
||||
$interno = trim((string) $this->unita->interno);
|
||||
|
||||
$results = [];
|
||||
|
||||
// 1. Dati da protoc_ec
|
||||
if (DbSchema::connection('gescon_import')->hasTable('protoc_ec')) {
|
||||
$ecQuery = DB::connection('gescon_import')->table('protoc_ec')
|
||||
->where('cod_stabile', $codStabile)
|
||||
->where(function ($q) use ($scala, $interno) {
|
||||
$q->where(function ($sub) use ($scala, $interno) {
|
||||
$sub->where('scala', $scala)->where('interno', $interno);
|
||||
});
|
||||
})
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
|
||||
foreach ($ecQuery as $r) {
|
||||
$dtRaw = $r->data_invio ?? null;
|
||||
$dtFmt = $dtRaw ? date('d/m/Y', strtotime((string) $dtRaw)) : '—';
|
||||
$pdfName = trim((string) ($r->nome_pdf ?? ''));
|
||||
$pdfPath = $pdfName !== '' ? "/mnt/gescon-archives/gescon/{$codStabile}/E_C/{$pdfName}" : null;
|
||||
$pdfExists = $pdfPath && file_exists($pdfPath);
|
||||
|
||||
$results[] = [
|
||||
'id' => 'ec_' . $r->id,
|
||||
'protocollo' => $r->protocollo ?: ('EC #' . $r->id_corrisp),
|
||||
'tipo' => 'Estratto Conto',
|
||||
'canale' => $r->tipo_documento ?: 'E-Mail',
|
||||
'data_invio' => $dtFmt,
|
||||
'destinatario' => $r->destinatario ?: $r->nome,
|
||||
'ruolo' => $r->c_i === 'I' ? 'Inquilino' : 'Condomino',
|
||||
'oggetto' => $r->oggetto ?: 'Invio Estratto Conto',
|
||||
'importo' => (float) ($r->totale ?? 0),
|
||||
'nome_file' => $pdfName,
|
||||
'file_path' => $pdfPath,
|
||||
'file_exists' => $pdfExists,
|
||||
'note' => $r->note,
|
||||
'created_at_sort' => $dtRaw ?: $r->created_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Dati da corrisp_inviata (Lettere, circolari)
|
||||
if (DbSchema::connection('gescon_import')->hasTable('corrisp_inviata')) {
|
||||
$condIds = $this->resolveLegacyCondIdsForUnita();
|
||||
$corrQuery = DB::connection('gescon_import')->table('corrisp_inviata')
|
||||
->where('cod_stabile', $codStabile)
|
||||
->where(function ($q) use ($condIds) {
|
||||
if (! empty($condIds)) {
|
||||
$q->whereIn('id_cond_for', $condIds)
|
||||
->orWhere('id_cond_for', 0);
|
||||
}
|
||||
})
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
|
||||
foreach ($corrQuery as $r) {
|
||||
$dtRaw = $r->data_invio ?? null;
|
||||
$dtFmt = $dtRaw ? date('d/m/Y', strtotime((string) $dtRaw)) : '—';
|
||||
$docName = trim((string) ($r->lettera_tipo_caricata ?? ''));
|
||||
$docPath = $docName !== '' ? "/mnt/gescon-archives/gescon/{$codStabile}/{$docName}" : null;
|
||||
$docExists = $docPath && file_exists($docPath);
|
||||
|
||||
$results[] = [
|
||||
'id' => 'corr_' . $r->id,
|
||||
'protocollo' => $r->protocollo ?: ('PROT #' . $r->id_corrisp),
|
||||
'tipo' => 'Lettera / Circolare',
|
||||
'canale' => $r->tipo_documento ?: 'Posta',
|
||||
'data_invio' => $dtFmt,
|
||||
'destinatario' => $r->destinatario ?: 'Condomini',
|
||||
'ruolo' => 'Condomino',
|
||||
'oggetto' => $r->oggetto ?: 'Comunicazione Amministrazione',
|
||||
'importo' => null,
|
||||
'nome_file' => $docName,
|
||||
'file_path' => $docPath,
|
||||
'file_exists' => $docExists,
|
||||
'note' => $r->note,
|
||||
'created_at_sort' => $dtRaw ?: $r->created_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
usort($results, function ($a, $b) {
|
||||
return strcmp((string) ($b['created_at_sort'] ?? ''), (string) ($a['created_at_sort'] ?? ''));
|
||||
});
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function setUnitaVisibilita(string $visibilita): void
|
||||
{
|
||||
$this->unitaVisibilita = $this->normalizeUnitaVisibilita($visibilita);
|
||||
|
|
|
|||
187
app/Services/Contabilita/BpmCsvParser.php
Normal file
187
app/Services/Contabilita/BpmCsvParser.php
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Contabilita;
|
||||
|
||||
use Carbon\Carbon;
|
||||
|
||||
class BpmCsvParser
|
||||
{
|
||||
/**
|
||||
* Parser per export CSV Banco BPM con header tipo:
|
||||
* "Ragione Sociale","Data contabile","Data valuta","Banca","Rapporto","Importo","Divisa","Descrizione","Categoria/sottocategoria","Hashtag"
|
||||
*
|
||||
* @return array{
|
||||
* rows: array<int, array{data: Carbon, valuta: Carbon|null, descrizione: string, importo: float, causale: string|null, raw_line: string}>,
|
||||
* meta: array{header_row:int, delimiter:string, parsed_rows:int, skipped_rows:int}
|
||||
* }
|
||||
*/
|
||||
public function parse(string $content): array
|
||||
{
|
||||
$content = str_replace(["\r\n", "\r"], "\n", $content);
|
||||
$lines = explode("\n", $content);
|
||||
|
||||
$delimiter = ',';
|
||||
$headerIndex = null;
|
||||
|
||||
$scanMax = min(count($lines), 100);
|
||||
for ($i = 0; $i < $scanMax; $i++) {
|
||||
$line = trim((string) $lines[$i]);
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
stripos($line, 'Data contabile') !== false
|
||||
&& stripos($line, 'Importo') !== false
|
||||
&& stripos($line, 'Descrizione') !== false
|
||||
) {
|
||||
$headerIndex = $i;
|
||||
if (substr_count($line, ';') > substr_count($line, ',')) {
|
||||
$delimiter = ';';
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($headerIndex === null) {
|
||||
throw new \RuntimeException('Formato CSV Banco BPM non riconosciuto: header con "Data contabile", "Importo" e "Descrizione" non trovato');
|
||||
}
|
||||
|
||||
$header = str_getcsv((string) $lines[$headerIndex], $delimiter);
|
||||
$header = array_map(fn($h) => $this->normHeader((string) $h), $header);
|
||||
|
||||
$hmap = [];
|
||||
foreach ($header as $idx => $h) {
|
||||
if ($h === '') {
|
||||
continue;
|
||||
}
|
||||
if (!array_key_exists($h, $hmap)) {
|
||||
$hmap[$h] = (int) $idx;
|
||||
}
|
||||
}
|
||||
|
||||
$idxData = $hmap['datacontabile'] ?? ($hmap['data'] ?? null);
|
||||
$idxValuta = $hmap['datavaluta'] ?? ($hmap['valuta'] ?? null);
|
||||
$idxDescr = $hmap['descrizione'] ?? null;
|
||||
$idxImporto = $hmap['importo'] ?? null;
|
||||
$idxCat = $hmap['categoriasottocategoria'] ?? ($hmap['categoria'] ?? null);
|
||||
|
||||
if ($idxData === null || $idxDescr === null || $idxImporto === null) {
|
||||
throw new \RuntimeException('Formato CSV Banco BPM non riconosciuto: colonne richieste mancanti');
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
$skipped = 0;
|
||||
|
||||
for ($i = $headerIndex + 1; $i < count($lines); $i++) {
|
||||
$line = trim((string) $lines[$i]);
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$cols = str_getcsv($line, $delimiter);
|
||||
if (count($cols) <= max($idxData, $idxDescr, $idxImporto)) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$dateRaw = trim((string) ($cols[$idxData] ?? ''));
|
||||
$valutaRaw = $idxValuta !== null ? trim((string) ($cols[$idxValuta] ?? '')) : '';
|
||||
$descr = trim((string) ($cols[$idxDescr] ?? ''));
|
||||
$importoRaw = trim((string) ($cols[$idxImporto] ?? ''));
|
||||
$categoria = $idxCat !== null ? trim((string) ($cols[$idxCat] ?? '')) : null;
|
||||
|
||||
if ($dateRaw === '' || $importoRaw === '') {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$date = $this->parseDate($dateRaw);
|
||||
if (! $date) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$valuta = $valutaRaw !== '' ? $this->parseDate($valutaRaw) : null;
|
||||
$importo = $this->parseAmount($importoRaw);
|
||||
if ($importo === null) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$causale = null;
|
||||
if ($categoria !== null && $categoria !== '') {
|
||||
$causale = mb_substr($categoria, 0, 20);
|
||||
}
|
||||
|
||||
$rows[] = [
|
||||
'data' => $date,
|
||||
'valuta' => $valuta,
|
||||
'descrizione' => $descr,
|
||||
'importo' => $importo,
|
||||
'causale' => $causale,
|
||||
'raw_line' => $line,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'rows' => $rows,
|
||||
'meta' => [
|
||||
'header_row' => $headerIndex,
|
||||
'delimiter' => $delimiter,
|
||||
'parsed_rows' => count($rows),
|
||||
'skipped_rows' => $skipped,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function normHeader(string $value): string
|
||||
{
|
||||
$v = mb_strtolower(trim($value));
|
||||
return (string) preg_replace('/[^a-z0-9]+/i', '', $v);
|
||||
}
|
||||
|
||||
private function parseDate(string $raw): ?Carbon
|
||||
{
|
||||
$raw = trim($raw);
|
||||
if ($raw === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (['d/m/Y', 'Y-m-d', 'd-m-Y', 'd.m.Y'] as $fmt) {
|
||||
try {
|
||||
$dt = Carbon::createFromFormat($fmt, $raw);
|
||||
if ($dt && $dt->year >= 1990 && $dt->year <= 2100) {
|
||||
return $dt->startOfDay();
|
||||
}
|
||||
} catch (\Throwable) {}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function parseAmount(string $raw): ?float
|
||||
{
|
||||
$raw = trim($raw);
|
||||
if ($raw === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$raw = str_replace('€', '', $raw);
|
||||
$raw = trim($raw);
|
||||
|
||||
if (str_contains($raw, ',') && str_contains($raw, '.')) {
|
||||
$raw = str_replace('.', '', $raw);
|
||||
$raw = str_replace(',', '.', $raw);
|
||||
} elseif (str_contains($raw, ',')) {
|
||||
$raw = str_replace(',', '.', $raw);
|
||||
}
|
||||
|
||||
$raw = preg_replace('/[^\d\.\+\-]/', '', $raw);
|
||||
if ($raw === '' || !is_numeric($raw)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return round((float) $raw, 2);
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ class MovimentiBancaImporter
|
|||
private UnicreditSemicolonCsvParser $unicreditSemicolonCsvParser;
|
||||
private CedhouseCsvParser $cedhouseCsvParser;
|
||||
private MpsQifParser $mpsQifParser;
|
||||
private BpmCsvParser $bpmCsvParser;
|
||||
|
||||
public function __construct(
|
||||
private readonly UnicreditWriParser $unicreditParser,
|
||||
|
|
@ -21,10 +22,12 @@ public function __construct(
|
|||
?UnicreditSemicolonCsvParser $unicreditSemicolonCsvParser = null,
|
||||
?CedhouseCsvParser $cedhouseCsvParser = null,
|
||||
?MpsQifParser $mpsQifParser = null,
|
||||
?BpmCsvParser $bpmCsvParser = null,
|
||||
) {
|
||||
$this->unicreditSemicolonCsvParser = $unicreditSemicolonCsvParser ?? new UnicreditSemicolonCsvParser();
|
||||
$this->cedhouseCsvParser = $cedhouseCsvParser ?? new CedhouseCsvParser();
|
||||
$this->mpsQifParser = $mpsQifParser ?? new MpsQifParser();
|
||||
$this->bpmCsvParser = $bpmCsvParser ?? new BpmCsvParser();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -236,9 +239,22 @@ public function importCsvAutoForConto(
|
|||
);
|
||||
}
|
||||
|
||||
if ($format === 'bpm_csv') {
|
||||
$parsed = $this->bpmCsvParser->parse($content);
|
||||
return $this->importParsedRowsForConto(
|
||||
$parsed,
|
||||
$stabileId,
|
||||
$contoId,
|
||||
$sourceFile,
|
||||
$gestioneId,
|
||||
$replace,
|
||||
$format,
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback: prova in sequenza (utile per CSV "strani" o header varianti)
|
||||
$errors = [];
|
||||
foreach (['unicredit_wri', 'intesa_csv', 'cedhouse_csv', 'unicredit_semicolon'] as $try) {
|
||||
foreach (['bpm_csv', 'unicredit_wri', 'intesa_csv', 'cedhouse_csv', 'unicredit_semicolon'] as $try) {
|
||||
try {
|
||||
if ($try === 'unicredit_semicolon') {
|
||||
$res = $this->importUnicreditSemicolonCsvForConto($content, $stabileId, $contoId, $sourceFile, $gestioneId, $replace);
|
||||
|
|
@ -246,11 +262,13 @@ public function importCsvAutoForConto(
|
|||
return $res;
|
||||
}
|
||||
|
||||
$parsed = $try === 'unicredit_wri'
|
||||
? $this->unicreditParser->parse($content)
|
||||
: ($try === 'cedhouse_csv'
|
||||
? $this->cedhouseCsvParser->parse($content)
|
||||
: $this->intesaParser->parse($content));
|
||||
$parsed = $try === 'bpm_csv'
|
||||
? $this->bpmCsvParser->parse($content)
|
||||
: ($try === 'unicredit_wri'
|
||||
? $this->unicreditParser->parse($content)
|
||||
: ($try === 'cedhouse_csv'
|
||||
? $this->cedhouseCsvParser->parse($content)
|
||||
: $this->intesaParser->parse($content)));
|
||||
|
||||
return $this->importParsedRowsForConto($parsed, $stabileId, $contoId, $sourceFile, $gestioneId, $replace, $try);
|
||||
} catch (\Throwable $e) {
|
||||
|
|
@ -261,6 +279,30 @@ public function importCsvAutoForConto(
|
|||
throw new \RuntimeException('Formato CSV non riconosciuto. Tentativi: ' . implode(' | ', $errors));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{imported:int, duplicates:int, meta?:array<string,mixed>}
|
||||
*/
|
||||
public function importBpmCsvForConto(
|
||||
string $content,
|
||||
int $stabileId,
|
||||
int $contoId,
|
||||
?string $sourceFile = null,
|
||||
?int $gestioneId = null,
|
||||
bool $replace = false,
|
||||
): array {
|
||||
$parsed = $this->bpmCsvParser->parse($content);
|
||||
|
||||
return $this->importParsedRowsForConto(
|
||||
$parsed,
|
||||
$stabileId,
|
||||
$contoId,
|
||||
$sourceFile,
|
||||
$gestioneId,
|
||||
$replace,
|
||||
'bpm_csv',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{imported:int, duplicates:int, meta?:array<string,mixed>}
|
||||
*/
|
||||
|
|
@ -439,7 +481,7 @@ private function importParsedRowsForConto(
|
|||
'valuta' => $row['valuta'],
|
||||
'descrizione' => $row['descrizione'],
|
||||
'importo' => $row['importo'],
|
||||
'causale' => $row['causale'],
|
||||
'causale' => ! empty($row['causale']) ? mb_substr((string) $row['causale'], 0, 20) : null,
|
||||
'source_file' => $sourceFile,
|
||||
'raw_line' => $row['raw_line'],
|
||||
'row_hash' => $rowHash,
|
||||
|
|
@ -492,9 +534,9 @@ private function detectCsvFormat(string $content): ?string
|
|||
return 'intesa_csv';
|
||||
}
|
||||
|
||||
// CEDHOUSE: header tipico "R;NR;Data;Tipo Riga;Descrizione;Entrate;Uscite"
|
||||
if (str_contains($head, 'tipo riga') && str_contains($head, 'entrate') && str_contains($head, 'uscite')) {
|
||||
return 'cedhouse_csv';
|
||||
// Banco BPM CSV: header tipico con "data contabile", "rapporto", "importo", "descrizione"
|
||||
if (str_contains($head, 'data contabile') && str_contains($head, 'importo') && (str_contains($head, 'rapporto') || str_contains($head, 'ragione sociale') || str_contains($head, 'banco bpm'))) {
|
||||
return 'bpm_csv';
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -113,14 +113,12 @@ class="rounded-lg border px-2.5 py-1 text-xs font-semibold transition {{ $this->
|
|||
<div class="flex flex-wrap items-center gap-2 px-4 py-2 bg-white border-b">
|
||||
@php
|
||||
$tabs = [
|
||||
'riepilogo' => ['icon' => 'heroicon-o-rectangle-group', 'label' => 'Riepilogo'],
|
||||
'nominativi' => ['icon' => 'heroicon-o-user-group', 'label' => '👥 Nominativi & Subentri'],
|
||||
'riepilogo' => ['icon' => 'heroicon-o-rectangle-group', 'label' => 'Riepilogo'],
|
||||
'nominativi' => ['icon' => 'heroicon-o-user-group', 'label' => '👥 Nominativi & Subentri'],
|
||||
'estratto_conto' => ['icon' => 'heroicon-o-calculator', 'label' => '🧾 Estratto Conto & Pagamenti'],
|
||||
'millesimi' => ['icon' => 'heroicon-o-chart-pie', 'label' => 'Tabelle millesimali'],
|
||||
'catasto' => ['icon' => 'heroicon-o-building-office-2', 'label' => 'Catasto'],
|
||||
'comproprietari' => ['icon' => 'heroicon-o-users', 'label' => 'Comproprietari'],
|
||||
'preferenze' => ['icon' => 'heroicon-o-envelope', 'label' => 'Comunicazioni'],
|
||||
'acqua' => ['icon' => 'heroicon-o-beaker', 'label' => 'Acqua'],
|
||||
'millesimi' => ['icon' => 'heroicon-o-chart-pie', 'label' => '📊 Tabelle Millesimali & Spese'],
|
||||
'preferenze' => ['icon' => 'heroicon-o-envelope', 'label' => '📨 Comunicazioni & Protocollo'],
|
||||
'acqua' => ['icon' => 'heroicon-o-beaker', 'label' => '💧 Acqua'],
|
||||
];
|
||||
@endphp
|
||||
@foreach($tabs as $key => $cfg)
|
||||
|
|
@ -577,104 +575,6 @@ class="inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-semi
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Tabella Addebiti e Ripartizioni --}}
|
||||
<div class="rounded-xl border border-slate-200 bg-white overflow-hidden shadow-xs">
|
||||
<div class="border-b border-slate-200 bg-slate-50 px-4 py-3 font-semibold text-slate-800 text-sm flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span>📊 Spese e Millesimi Addebitati a <strong>{{ $soggetto['nominativo'] }}</strong></span>
|
||||
<span class="rounded bg-indigo-100 px-2 py-0.5 text-xs text-indigo-800 font-normal">
|
||||
{{ reset($ecData['addebiti'])['anno_gestione'] ?? 'Anno In Corso' }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-xs text-slate-500 font-normal">Suddivise per Gruppo (Ordinaria, Riscaldamento, Straordinaria)</span>
|
||||
</div>
|
||||
@if(empty($ecData['addebiti']))
|
||||
<div class="p-6 text-center text-slate-500 text-xs">Nessun addebito preventivato o consuntivato per questo nominativo.</div>
|
||||
@else
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left text-xs">
|
||||
<thead class="bg-slate-100 text-slate-700 font-semibold border-b border-slate-200">
|
||||
<tr>
|
||||
<th class="px-4 py-2.5">Codice Tabella</th>
|
||||
<th class="px-4 py-2.5">Descrizione Tabella</th>
|
||||
<th class="px-4 py-2.5">Tipo Gestione</th>
|
||||
<th class="px-4 py-2.5 text-right">Millesimi (mm)</th>
|
||||
<th class="px-4 py-2.5 text-right">Preventivo (€)</th>
|
||||
<th class="px-4 py-2.5 text-right">Consuntivo (€)</th>
|
||||
<th class="px-4 py-2.5 text-right">Conguaglio (€)</th>
|
||||
<th class="px-4 py-2.5 text-center">Voci Spesa</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
@foreach($ecData['addebiti'] as $addIndex => $add)
|
||||
<tr class="hover:bg-indigo-50/50 transition cursor-pointer" onclick="document.getElementById('voc-spe-row-{{ $addIndex }}') ? document.getElementById('voc-spe-row-{{ $addIndex }}').classList.toggle('hidden') : null">
|
||||
<td class="px-4 py-2.5 font-mono font-bold text-indigo-700">{{ $add['cod_tabella'] }}</td>
|
||||
<td class="px-4 py-2.5 font-semibold text-slate-900">{{ $add['descrizione_tab'] }}</td>
|
||||
<td class="px-4 py-2.5">
|
||||
<span class="inline-flex items-center rounded-md px-2 py-0.5 text-xs font-semibold {{ $add['tipo_label'] === 'Riscaldamento' ? 'bg-amber-100 text-amber-800' : ($add['tipo_label'] === 'Straordinaria' ? 'bg-rose-100 text-rose-800' : 'bg-slate-100 text-slate-800') }}">
|
||||
{{ $add['tipo_label'] }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-2.5 text-right font-mono text-slate-600">{{ number_format($add['millesimi'], 2, ',', '.') }}</td>
|
||||
<td class="px-4 py-2.5 text-right font-mono font-medium text-slate-800">€ {{ number_format($add['preventivo_euro'], 2, ',', '.') }}</td>
|
||||
<td class="px-4 py-2.5 text-right font-mono font-medium text-slate-800">€ {{ number_format($add['consuntivo_euro'], 2, ',', '.') }}</td>
|
||||
<td class="px-4 py-2.5 text-right font-mono font-bold {{ $add['conguaglio_euro'] > 0 ? 'text-amber-600' : ($add['conguaglio_euro'] < 0 ? 'text-emerald-600' : 'text-slate-500') }}">
|
||||
€ {{ number_format($add['conguaglio_euro'], 2, ',', '.') }}
|
||||
</td>
|
||||
<td class="px-4 py-2.5 text-center">
|
||||
@if(!empty($add['voci_spesa']))
|
||||
<span class="inline-flex items-center gap-1 rounded bg-indigo-50 border border-indigo-200 px-2 py-0.5 text-xs font-semibold text-indigo-700">
|
||||
<span>{{ count($add['voci_spesa']) }} voci</span>
|
||||
<span>🔍</span>
|
||||
</span>
|
||||
@else
|
||||
<span class="text-slate-400 text-xs">—</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@if(!empty($add['voci_spesa']))
|
||||
<tr id="voc-spe-row-{{ $addIndex }}" class="hidden bg-slate-50/80 border-b border-indigo-100">
|
||||
<td colspan="8" class="p-3">
|
||||
<div class="rounded-lg border border-indigo-100 bg-white p-3 shadow-xs">
|
||||
<div class="text-xs font-bold text-indigo-900 mb-2 flex items-center gap-2">
|
||||
<span>📋 Voci di Spesa collegate alla Tabella <strong>{{ $add['cod_tabella'] }}</strong></span>
|
||||
<span class="text-slate-500 font-normal">({{ count($add['voci_spesa']) }} voci da voc_spe)</span>
|
||||
</div>
|
||||
<table class="w-full text-left text-xs border border-slate-200 rounded">
|
||||
<thead class="bg-indigo-50 text-indigo-900 font-semibold">
|
||||
<tr>
|
||||
<th class="px-3 py-1.5">Cod. Voce</th>
|
||||
<th class="px-3 py-1.5">Descrizione Voce</th>
|
||||
<th class="px-3 py-1.5 text-center">% Prop.</th>
|
||||
<th class="px-3 py-1.5 text-center">% Inq.</th>
|
||||
<th class="px-3 py-1.5 text-right">Preventivo Voce (€)</th>
|
||||
<th class="px-3 py-1.5 text-right">Consuntivo Voce (€)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
@foreach($add['voci_spesa'] as $voc)
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="px-3 py-1.5 font-mono font-bold text-slate-700">{{ $voc['cod_voce'] }}</td>
|
||||
<td class="px-3 py-1.5 text-slate-900 font-medium">{{ $voc['descrizione'] }}</td>
|
||||
<td class="px-3 py-1.5 text-center font-mono text-slate-600">{{ number_format($voc['perc_proprietario'], 0) }}%</td>
|
||||
<td class="px-3 py-1.5 text-center font-mono text-slate-600">{{ number_format($voc['perc_inquilino'], 0) }}%</td>
|
||||
<td class="px-3 py-1.5 text-right font-mono text-slate-800">€ {{ number_format($voc['preventivo_euro'], 2, ',', '.') }}</td>
|
||||
<td class="px-3 py-1.5 text-right font-mono text-slate-800">€ {{ number_format($voc['consuntivo_euro'], 2, ',', '.') }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Tabella Rate Emesse (Dovuto vs Pagato + Data Pagamento e Movimento) --}}
|
||||
<div class="rounded-xl border border-slate-200 bg-white overflow-hidden shadow-xs">
|
||||
<div class="border-b border-slate-200 bg-slate-50 px-4 py-3 font-semibold text-slate-800 text-sm flex items-center justify-between">
|
||||
|
|
@ -804,171 +704,261 @@ class="inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-semi
|
|||
</div>
|
||||
@endif
|
||||
|
||||
@if($tab === 'catasto')
|
||||
<x-filament::section>
|
||||
<x-slot name="heading">Dati catastali</x-slot>
|
||||
<x-slot name="description">Risultanze catastali ed identificativi ufficiali consolidati</x-slot>
|
||||
|
||||
<div class="max-w-3xl mx-auto rounded-xl border border-gray-200 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-900/50 p-6 shadow-xs space-y-6">
|
||||
<div class="grid grid-cols-2 gap-y-4 gap-x-6 text-sm">
|
||||
<div class="flex justify-between border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<span class="text-gray-500 font-medium">Sezione Urbana:</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{ ($unita->sezione_urbana && !preg_match('/^[A-Z]\d{3}$/i', $unita->sezione_urbana)) ? $unita->sezione_urbana : '—' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<span class="text-gray-500 font-medium">Foglio:</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{ $unita->foglio ?: ($unita->stabile?->foglio ?? $unita->stabile?->foglio_catasto ?? '—') }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<span class="text-gray-500 font-medium">Particella:</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{ $unita->particella ?: ($unita->stabile?->particella_catasto ?? $unita->stabile?->mappale ?? '—') }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<span class="text-gray-500 font-medium">Subalterno:</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{ $unita->subalterno ?? '—' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<span class="text-gray-500 font-medium">Categoria:</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{ \App\Filament\Pages\Condomini\CatastoHub::formattaCategoriaCatastale($unita->categoria_catastale ?? '') ?: '—' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<span class="text-gray-500 font-medium">Classe:</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{ $unita->classe ?? ($unita->legacyData['classe'] ?? '03') }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<span class="text-gray-500 font-medium">Consistenza:</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-white">
|
||||
{{ ($unita->superficie && str_starts_with(\App\Filament\Pages\Condomini\CatastoHub::estraiCodiceCompatto($unita->categoria_catastale ?? ''), 'C')) ? ($unita->superficie . ' m²') : ($unita->numero_vani ? ($unita->numero_vani . ' vani') : ($unita->vani ? ($unita->vani . ' vani') : '—')) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<span class="text-gray-500 font-medium">Rendita Euro:</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{ $unita->rendita_catastale ? '€ ' . number_format($unita->rendita_catastale, 2, ',', '.') : '—' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<span class="text-gray-500 font-medium">Piano AdE:</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{ $unita->piano ?? '—' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<span class="text-gray-500 font-medium">idImmobile:</span>
|
||||
<span class="font-mono text-xs font-semibold text-gray-600 dark:text-gray-400 max-w-44 truncate" title="{{ $unita->codice_univoco ?? '—' }}">{{ $unita->codice_univoco ?? '—' }}</span>
|
||||
@if($tab === 'millesimi')
|
||||
@php
|
||||
$ecData = $this->getEstrattoContoNominativoDettaglioProperty();
|
||||
$soggetto = $ecData['soggetto'] ?? [];
|
||||
@endphp
|
||||
<div class="space-y-6">
|
||||
{{-- Sezione 1: Tabelle Millesimali Assegnate all'Unità --}}
|
||||
<x-filament::section>
|
||||
<div class="flex items-center justify-between border-b border-gray-100 pb-3 mb-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<x-filament::icon icon="heroicon-o-chart-pie" class="h-5 w-5 text-indigo-600" />
|
||||
<span class="font-bold text-slate-900 text-sm">Tabelle Millesimali Assegnate</span>
|
||||
</div>
|
||||
<span class="text-xs text-slate-500">Millesimi generali e speciali configurati</span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center pt-4 border-t border-gray-200 dark:border-gray-800">
|
||||
<a href="/admin-filament/condomini/catasto-hub?unita_id={{ $unita->id }}" class="inline-flex items-center gap-2 rounded-lg bg-primary-600 hover:bg-primary-700 text-white font-semibold px-6 py-2.5 text-sm shadow-md transition">
|
||||
<x-filament::icon icon="heroicon-o-arrow-top-right-on-square" class="h-4 w-4" />
|
||||
<span>[ Apri in HUB Catasto ]</span>
|
||||
</a>
|
||||
<div class="mb-4 flex flex-wrap gap-2">
|
||||
@php
|
||||
$gruppi = [
|
||||
'ordinarie' => 'ORDINARIE',
|
||||
'riscaldamento' => 'RISCALDAMENTO',
|
||||
'straordinarie' => 'STRAORDINARIE',
|
||||
];
|
||||
@endphp
|
||||
@foreach($gruppi as $key => $label)
|
||||
<button
|
||||
type="button"
|
||||
wire:click="setMillesimiGruppo('{{ $key }}')"
|
||||
class="inline-flex items-center rounded-lg border px-4 py-2 text-xs font-semibold transition
|
||||
{{ ($millesimiGruppo ?? 'ordinarie') === $key ? 'border-primary-300 bg-primary-50 text-primary-700 shadow-sm' : 'border-gray-200 bg-white text-gray-700 hover:bg-gray-50' }}"
|
||||
>
|
||||
{{ $label }}
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
@endif
|
||||
|
||||
@if($tab === 'comproprietari')
|
||||
<x-filament::section>
|
||||
<x-slot name="heading">Comproprietari ed Aventi Diritto</x-slot>
|
||||
<x-slot name="description">Elenco analitico dei soggetti con titolo reale o quota di possesso sull'immobile</x-slot>
|
||||
|
||||
<div class="space-y-4">
|
||||
@php $comproprietari = $relazioniPerTipo['proprietari'] ?? []; @endphp
|
||||
@if(empty($comproprietari))
|
||||
<div class="text-sm text-gray-500">Nessun proprietario registrato.</div>
|
||||
@if(empty($millesimiPerTabella))
|
||||
<div class="text-xs text-gray-500">Nessuna tabella millesimale associata.</div>
|
||||
@else
|
||||
<div class="overflow-x-auto rounded-lg border border-gray-200">
|
||||
<table class="w-full text-left text-sm text-gray-500">
|
||||
<thead class="bg-gray-50 text-xs uppercase text-gray-700 font-semibold border-b">
|
||||
<tr>
|
||||
<th class="px-6 py-3">Soggetto / Nominativo</th>
|
||||
<th class="px-6 py-3">Codice Fiscale</th>
|
||||
<th class="px-6 py-3">Titolo / Diritto</th>
|
||||
<th class="px-6 py-3 text-right">Quota Proprietà</th>
|
||||
<th class="px-6 py-3">Periodo di Possesso</th>
|
||||
@php
|
||||
$selectedGruppo = $millesimiGruppo ?? 'ordinarie';
|
||||
$filteredMillesimi = array_values(array_filter($millesimiPerTabella, fn ($r) => ($r['gruppo'] ?? 'ordinarie') === $selectedGruppo));
|
||||
@endphp
|
||||
<div class="overflow-x-auto rounded-lg border border-slate-200">
|
||||
<table class="min-w-full text-xs">
|
||||
<thead class="bg-slate-50 text-slate-600 border-b border-slate-200 font-semibold">
|
||||
<tr class="text-left">
|
||||
<th class="py-2.5 px-4">Codice Tabella</th>
|
||||
<th class="py-2.5 px-4">Denominazione</th>
|
||||
<th class="py-2.5 px-4 text-right">Valore Millesimi (mm)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
@foreach($comproprietari as $p)
|
||||
<tr class="hover:bg-gray-50/50">
|
||||
<td class="px-6 py-4 font-semibold text-gray-900">{{ $p['nome'] }}</td>
|
||||
<td class="px-6 py-4 font-mono text-gray-600">{{ $p['codice_fiscale'] ?? '—' }}</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="rounded bg-primary-50 px-2.5 py-1 text-xs font-bold text-primary-700 uppercase tracking-wide">
|
||||
{{ $p['tipo'] ?? 'Proprietà' }}
|
||||
<tbody class="divide-y divide-slate-100 bg-white">
|
||||
@forelse($filteredMillesimi as $r)
|
||||
@php
|
||||
$tipo = $r['tipo'] ?? 'altro';
|
||||
$tipoLabel = ucwords(str_replace('_', ' ', (string) $tipo));
|
||||
$prevTipo = $loop->first ? null : ($filteredMillesimi[$loop->index - 1]['tipo'] ?? 'altro');
|
||||
@endphp
|
||||
|
||||
@if($loop->first || $tipo !== $prevTipo)
|
||||
<tr class="bg-indigo-50/40">
|
||||
<td class="py-2 px-4 font-bold text-indigo-900" colspan="3">{{ $tipoLabel }}</td>
|
||||
</tr>
|
||||
@endif
|
||||
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="py-2.5 px-4 font-mono font-bold text-indigo-700">{{ $r['codice'] }}</td>
|
||||
<td class="py-2.5 px-4 text-slate-800">{{ $r['nome'] }}</td>
|
||||
<td class="py-2.5 px-4 text-right font-mono font-semibold text-slate-900">{{ number_format($r['millesimi'], 4, ',', '.') }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td class="py-4 text-center text-gray-500" colspan="3">Nessun millesimo presente per il gruppo selezionato</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
</x-filament::section>
|
||||
|
||||
{{-- Sezione 2: Spese e Millesimi Addebitati per Gestione --}}
|
||||
<div class="rounded-xl border border-slate-200 bg-white overflow-hidden shadow-xs">
|
||||
<div class="border-b border-slate-200 bg-slate-50 px-4 py-3 font-semibold text-slate-800 text-sm flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span>📊 Spese e Millesimi Addebitati a <strong>{{ $soggetto['nominativo'] ?? 'Condomino' }}</strong></span>
|
||||
<span class="rounded bg-indigo-100 px-2 py-0.5 text-xs text-indigo-800 font-normal">
|
||||
{{ reset($ecData['addebiti'])['anno_gestione'] ?? 'Anno In Corso' }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-xs text-slate-500 font-normal">Suddivise per Gruppo (Ordinaria, Riscaldamento, Straordinaria)</span>
|
||||
</div>
|
||||
@if(empty($ecData['addebiti']))
|
||||
<div class="p-6 text-center text-slate-500 text-xs">Nessun addebito preventivato o consuntivato per questo nominativo.</div>
|
||||
@else
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left text-xs">
|
||||
<thead class="bg-slate-100 text-slate-700 font-semibold border-b border-slate-200">
|
||||
<tr>
|
||||
<th class="px-4 py-2.5">Codice Tabella</th>
|
||||
<th class="px-4 py-2.5">Descrizione Tabella</th>
|
||||
<th class="px-4 py-2.5">Tipo Gestione</th>
|
||||
<th class="px-4 py-2.5 text-right">Millesimi (mm)</th>
|
||||
<th class="px-4 py-2.5 text-right">Preventivo (€)</th>
|
||||
<th class="px-4 py-2.5 text-right">Consuntivo (€)</th>
|
||||
<th class="px-4 py-2.5 text-right">Conguaglio (€)</th>
|
||||
<th class="px-4 py-2.5 text-center">Voci Spesa</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
@foreach($ecData['addebiti'] as $addIndex => $add)
|
||||
<tr class="hover:bg-indigo-50/50 transition cursor-pointer" onclick="document.getElementById('voc-spe-row-{{ $addIndex }}') ? document.getElementById('voc-spe-row-{{ $addIndex }}').classList.toggle('hidden') : null">
|
||||
<td class="px-4 py-2.5 font-mono font-bold text-indigo-700">{{ $add['cod_tabella'] }}</td>
|
||||
<td class="px-4 py-2.5 font-semibold text-slate-900">{{ $add['descrizione_tab'] }}</td>
|
||||
<td class="px-4 py-2.5">
|
||||
<span class="inline-flex items-center rounded-md px-2 py-0.5 text-xs font-semibold {{ $add['tipo_label'] === 'Riscaldamento' ? 'bg-amber-100 text-amber-800' : ($add['tipo_label'] === 'Straordinaria' ? 'bg-rose-100 text-rose-800' : 'bg-slate-100 text-slate-800') }}">
|
||||
{{ $add['tipo_label'] }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right font-extrabold text-gray-900">
|
||||
{{ $p['quota_label'] ?? ($p['percentuale_label'] ?? '—') }}%
|
||||
<td class="px-4 py-2.5 text-right font-mono text-slate-600">{{ number_format($add['millesimi'], 2, ',', '.') }}</td>
|
||||
<td class="px-4 py-2.5 text-right font-mono font-medium text-slate-800">€ {{ number_format($add['preventivo_euro'], 2, ',', '.') }}</td>
|
||||
<td class="px-4 py-2.5 text-right font-mono font-medium text-slate-800">€ {{ number_format($add['consuntivo_euro'], 2, ',', '.') }}</td>
|
||||
<td class="px-4 py-2.5 text-right font-mono font-bold {{ $add['conguaglio_euro'] > 0 ? 'text-amber-600' : ($add['conguaglio_euro'] < 0 ? 'text-emerald-600' : 'text-slate-500') }}">
|
||||
€ {{ number_format($add['conguaglio_euro'], 2, ',', '.') }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-xs text-gray-600">
|
||||
@if(($p['data_inizio'] ?? null) || ($p['data_fine'] ?? null))
|
||||
@if($p['data_inizio'] ?? null) Da {{ $p['data_inizio'] }} @endif
|
||||
@if($p['data_fine'] ?? null) Fino a {{ $p['data_fine'] }} @endif
|
||||
<td class="px-4 py-2.5 text-center">
|
||||
@if(!empty($add['voci_spesa']))
|
||||
<span class="inline-flex items-center gap-1 rounded bg-indigo-50 border border-indigo-200 px-2 py-0.5 text-xs font-semibold text-indigo-700">
|
||||
<span>{{ count($add['voci_spesa']) }} voci</span>
|
||||
<span>🔍</span>
|
||||
</span>
|
||||
@else
|
||||
Da sempre
|
||||
<span class="text-slate-400 text-xs">—</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@if(!empty($add['voci_spesa']))
|
||||
<tr id="voc-spe-row-{{ $addIndex }}" class="hidden bg-slate-50/80 border-b border-indigo-100">
|
||||
<td colspan="8" class="p-3">
|
||||
<div class="rounded-lg border border-indigo-100 bg-white p-3 shadow-xs">
|
||||
<div class="text-xs font-bold text-indigo-900 mb-2 flex items-center gap-2">
|
||||
<span>📋 Voci di Spesa collegate alla Tabella <strong>{{ $add['cod_tabella'] }}</strong></span>
|
||||
<span class="text-slate-500 font-normal">({{ count($add['voci_spesa']) }} voci da voc_spe)</span>
|
||||
</div>
|
||||
<table class="w-full text-left text-xs border border-slate-200 rounded">
|
||||
<thead class="bg-indigo-50 text-indigo-900 font-semibold">
|
||||
<tr>
|
||||
<th class="px-3 py-1.5">Cod. Voce</th>
|
||||
<th class="px-3 py-1.5">Descrizione Voce</th>
|
||||
<th class="px-3 py-1.5 text-center">% Prop.</th>
|
||||
<th class="px-3 py-1.5 text-center">% Inq.</th>
|
||||
<th class="px-3 py-1.5 text-right">Preventivo Voce (€)</th>
|
||||
<th class="px-3 py-1.5 text-right">Consuntivo Voce (€)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
@foreach($add['voci_spesa'] as $voc)
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="px-3 py-1.5 font-mono font-bold text-slate-700">{{ $voc['cod_voce'] }}</td>
|
||||
<td class="px-3 py-1.5 text-slate-900 font-medium">{{ $voc['descrizione'] }}</td>
|
||||
<td class="px-3 py-1.5 text-center font-mono text-slate-600">{{ number_format($voc['perc_proprietario'], 0) }}%</td>
|
||||
<td class="px-3 py-1.5 text-center font-mono text-slate-600">{{ number_format($voc['perc_inquilino'], 0) }}%</td>
|
||||
<td class="px-3 py-1.5 text-right font-mono text-slate-800">€ {{ number_format($voc['preventivo_euro'], 2, ',', '.') }}</td>
|
||||
<td class="px-3 py-1.5 text-right font-mono text-slate-800">€ {{ number_format($voc['consuntivo_euro'], 2, ',', '.') }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-filament::section>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($tab === 'preferenze')
|
||||
<x-filament::section>
|
||||
<x-slot name="heading">Comunicazioni e Canali di Invio</x-slot>
|
||||
<x-slot name="description">Preferenze di spedizione e canali digitali ufficiali per proprietari e comproprietari</x-slot>
|
||||
@php
|
||||
$protocolloList = $this->getProtocolloComunicazioniProperty();
|
||||
@endphp
|
||||
<div class="space-y-6">
|
||||
{{-- 1. Protocollo Corrispondenza ed Estratti Conto Inviati --}}
|
||||
<div class="rounded-xl border border-slate-200 bg-white overflow-hidden shadow-xs">
|
||||
<div class="border-b border-slate-200 bg-slate-50 px-4 py-3 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-base">📨</span>
|
||||
<div>
|
||||
<span class="font-bold text-slate-900 text-sm">Protocollo Comunicazioni & Corrispondenza Inviata</span>
|
||||
<div class="text-xs text-slate-500">Estratti conto, lettere, circolari e comunicazioni storiche collegate all'unità</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="rounded-full bg-indigo-50 border border-indigo-200 px-3 py-1 text-xs font-bold text-indigo-800">
|
||||
{{ count($protocolloList) }} documenti a protocollo
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
@if(empty($canaliComunicazione))
|
||||
<div class="text-sm text-gray-500">Nessun proprietario o comproprietario registrato per questa unità.</div>
|
||||
@if(empty($protocolloList))
|
||||
<div class="p-8 text-center text-slate-500 text-xs">
|
||||
Nessuna comunicazione o estratto conto inviato registrato a protocollo per questa unità.
|
||||
</div>
|
||||
@else
|
||||
<div class="overflow-x-auto rounded-lg border border-gray-200">
|
||||
<table class="w-full text-left text-sm text-gray-500">
|
||||
<thead class="bg-gray-50 text-xs uppercase text-gray-700 font-semibold border-b">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left text-xs">
|
||||
<thead class="bg-slate-100 text-slate-700 font-semibold border-b border-slate-200">
|
||||
<tr>
|
||||
<th class="px-6 py-3">Soggetto / Nominativo</th>
|
||||
<th class="px-6 py-3">Convocazione Assemblea</th>
|
||||
<th class="px-6 py-3">Invio Verbali</th>
|
||||
<th class="px-6 py-3">Solleciti e Rate</th>
|
||||
<th class="px-4 py-2.5">Data Invio</th>
|
||||
<th class="px-4 py-2.5">Protocollo / Rif.</th>
|
||||
<th class="px-4 py-2.5">Canale</th>
|
||||
<th class="px-4 py-2.5">Destinatario</th>
|
||||
<th class="px-4 py-2.5">Oggetto / Contenuto</th>
|
||||
<th class="px-4 py-2.5 text-right">Importo E/C</th>
|
||||
<th class="px-4 py-2.5 text-center">Allegato</th>
|
||||
<th class="px-4 py-2.5">Note</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
@foreach($canaliComunicazione as $soggettoId => $p)
|
||||
<tr class="hover:bg-gray-50/50">
|
||||
<td class="px-6 py-4">
|
||||
<div class="font-semibold text-gray-900">{{ $p['nominativo'] }}</div>
|
||||
<div class="text-xs text-gray-500 font-mono mt-0.5">{{ $p['codice_fiscale'] ?? '—' }}</div>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
@foreach($protocolloList as $prot)
|
||||
<tr class="hover:bg-indigo-50/40 transition">
|
||||
<td class="px-4 py-2.5 font-medium text-slate-900 whitespace-nowrap">{{ $prot['data_invio'] }}</td>
|
||||
<td class="px-4 py-2.5 font-mono font-bold text-indigo-700 whitespace-nowrap">{{ $prot['protocollo'] }}</td>
|
||||
<td class="px-4 py-2.5 whitespace-nowrap">
|
||||
<span class="inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-xs font-semibold
|
||||
{{ str_contains(strtolower($prot['canale']), 'pec') ? 'bg-emerald-100 text-emerald-800' : (str_contains(strtolower($prot['canale']), 'mail') ? 'bg-blue-100 text-blue-800' : 'bg-slate-100 text-slate-800') }}">
|
||||
<span>{{ str_contains(strtolower($prot['canale']), 'pec') ? '🛡️' : (str_contains(strtolower($prot['canale']), 'mail') ? '✉️' : '📮') }}</span>
|
||||
<span>{{ $prot['canale'] }}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<select
|
||||
wire:change="salvaCanaleComunicazione({{ $soggettoId }}, 'convocazione', $event.target.value)"
|
||||
class="w-full text-xs rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 font-semibold text-gray-700">
|
||||
@foreach(['PEC', 'Raccomandata AR', 'Posta Ordinaria', 'Mano'] as $opt)
|
||||
<option value="{{ $opt }}" {{ $p['convocazione'] === $opt ? 'selected' : '' }}>{{ $opt }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<td class="px-4 py-2.5 font-medium text-slate-900">
|
||||
<div>{{ $prot['destinatario'] }}</div>
|
||||
<div class="text-[10px] text-slate-500 font-normal">{{ $prot['ruolo'] }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<select
|
||||
wire:change="salvaCanaleComunicazione({{ $soggettoId }}, 'verbali', $event.target.value)"
|
||||
class="w-full text-xs rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 font-semibold text-gray-700">
|
||||
@foreach(['PEC', 'Raccomandata AR', 'Posta Ordinaria', 'Mano'] as $opt)
|
||||
<option value="{{ $opt }}" {{ $p['verbali'] === $opt ? 'selected' : '' }}>{{ $opt }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<td class="px-4 py-2.5 text-slate-700 max-w-xs truncate" title="{{ $prot['oggetto'] }}">
|
||||
{{ $prot['oggetto'] }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<select
|
||||
wire:change="salvaCanaleComunicazione({{ $soggettoId }}, 'solleciti', $event.target.value)"
|
||||
class="w-full text-xs rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 font-semibold text-gray-700">
|
||||
@foreach(['Email Standard', 'PEC', 'Posta Ordinaria'] as $opt)
|
||||
<option value="{{ $opt }}" {{ $p['solleciti'] === $opt ? 'selected' : '' }}>{{ $opt }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<td class="px-4 py-2.5 text-right font-mono font-bold {{ ($prot['importo'] ?? 0) > 0 ? 'text-slate-900' : 'text-slate-400' }}">
|
||||
{{ $prot['importo'] !== null ? '€ ' . number_format($prot['importo'], 2, ',', '.') : '—' }}
|
||||
</td>
|
||||
<td class="px-4 py-2.5 text-center whitespace-nowrap">
|
||||
@if(!empty($prot['nome_file']))
|
||||
<span class="inline-flex items-center gap-1 rounded bg-slate-100 border border-slate-200 px-2 py-0.5 text-[11px] font-mono text-slate-700">
|
||||
<span>📄</span>
|
||||
<span>{{ $prot['nome_file'] }}</span>
|
||||
</span>
|
||||
@else
|
||||
<span class="text-slate-400">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-2.5 text-slate-500 text-[11px] max-w-xs truncate" title="{{ $prot['note'] }}">
|
||||
{{ $prot['note'] ?: '—' }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
|
|
@ -977,168 +967,69 @@ class="w-full text-xs rounded-md border-gray-300 shadow-sm focus:border-primary-
|
|||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-filament::section>
|
||||
@endif
|
||||
|
||||
@if($tab === 'millesimi')
|
||||
<x-filament::section>
|
||||
<div class="flex items-center gap-2 text-sm font-semibold text-gray-900 mb-3">
|
||||
<x-filament::icon icon="heroicon-o-chart-pie" class="h-5 w-5 text-primary-600" />
|
||||
<span>Tabelle millesimali assegnate</span>
|
||||
</div>
|
||||
{{-- 2. Preferenze di Invio Ufficiali per i Soggetti --}}
|
||||
<x-filament::section>
|
||||
<x-slot name="heading">Canali Ufficiali di Notifica</x-slot>
|
||||
<x-slot name="description">Preferenze di spedizione canoniche per proprietari e comproprietari</x-slot>
|
||||
|
||||
<div class="mb-4 flex flex-wrap gap-2">
|
||||
@php
|
||||
$gruppi = [
|
||||
'ordinarie' => 'ORDINARIE',
|
||||
'riscaldamento' => 'RISCALDAMENTO',
|
||||
'straordinarie' => 'STRAORDINARIE',
|
||||
];
|
||||
@endphp
|
||||
@foreach($gruppi as $key => $label)
|
||||
<button
|
||||
type="button"
|
||||
wire:click="setMillesimiGruppo('{{ $key }}')"
|
||||
class="inline-flex items-center rounded-lg border px-4 py-2 text-sm font-semibold transition
|
||||
{{ ($millesimiGruppo ?? 'ordinarie') === $key ? 'border-primary-300 bg-primary-50 text-primary-700 shadow-sm' : 'border-gray-200 bg-white text-gray-700 hover:bg-gray-50' }}"
|
||||
>
|
||||
{{ $label }}
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@if(empty($millesimiPerTabella))
|
||||
<div class="text-sm text-gray-500">Nessuna tabella millesimale associata.</div>
|
||||
@else
|
||||
@php
|
||||
$selectedGruppo = $millesimiGruppo ?? 'ordinarie';
|
||||
$filteredMillesimi = array_values(array_filter($millesimiPerTabella, fn ($r) => ($r['gruppo'] ?? 'ordinarie') === $selectedGruppo));
|
||||
@endphp
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b text-left text-gray-500">
|
||||
<th class="py-2 pr-4">Tabella</th>
|
||||
<th class="py-2 pr-4">Denominazione</th>
|
||||
<th class="py-2 pr-4 text-right">Millesimi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
@forelse($filteredMillesimi as $r)
|
||||
@php
|
||||
$tipo = $r['tipo'] ?? 'altro';
|
||||
$tipoLabel = $tipo;
|
||||
$tipoLabel = str_replace('_', ' ', (string) $tipoLabel);
|
||||
$tipoLabel = ucwords($tipoLabel);
|
||||
|
||||
$prevTipo = $loop->first ? null : ($filteredMillesimi[$loop->index - 1]['tipo'] ?? 'altro');
|
||||
@endphp
|
||||
|
||||
@if($loop->first || $tipo !== $prevTipo)
|
||||
<tr class="bg-gray-50">
|
||||
<td class="py-2 pr-4 font-semibold" colspan="3">{{ $tipoLabel }}</td>
|
||||
<div class="space-y-4">
|
||||
@if(empty($canaliComunicazione))
|
||||
<div class="text-xs text-gray-500">Nessun proprietario o comproprietario registrato per questa unità.</div>
|
||||
@else
|
||||
<div class="overflow-x-auto rounded-lg border border-gray-200">
|
||||
<table class="w-full text-left text-xs text-gray-500">
|
||||
<thead class="bg-gray-50 text-xs uppercase text-gray-700 font-semibold border-b">
|
||||
<tr>
|
||||
<th class="px-4 py-2.5">Soggetto / Nominativo</th>
|
||||
<th class="px-4 py-2.5">Convocazione Assemblea</th>
|
||||
<th class="px-4 py-2.5">Invio Verbali</th>
|
||||
<th class="px-4 py-2.5">Solleciti e Rate</th>
|
||||
</tr>
|
||||
@endif
|
||||
|
||||
<tr>
|
||||
<td class="py-2 pr-4 font-medium">{{ $r['codice'] }}</td>
|
||||
<td class="py-2 pr-4">{{ $r['nome'] }}</td>
|
||||
<td class="py-2 pr-4 text-right">{{ number_format($r['millesimi'], 4, ',', '.') }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td class="py-4 text-center text-gray-500" colspan="3">Nessun millesimo presente</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
@foreach($canaliComunicazione as $soggettoId => $p)
|
||||
<tr class="hover:bg-gray-50/50">
|
||||
<td class="px-4 py-3">
|
||||
<div class="font-bold text-gray-900">{{ $p['nominativo'] }}</div>
|
||||
<div class="text-xs text-gray-500 font-mono mt-0.5">{{ $p['codice_fiscale'] ?? '—' }}</div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<select
|
||||
wire:change="salvaCanaleComunicazione({{ $soggettoId }}, 'convocazione', $event.target.value)"
|
||||
class="w-full text-xs rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 font-semibold text-gray-700">
|
||||
@foreach(['PEC', 'Raccomandata AR', 'Posta Ordinaria', 'Mano'] as $opt)
|
||||
<option value="{{ $opt }}" {{ $p['convocazione'] === $opt ? 'selected' : '' }}>{{ $opt }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<select
|
||||
wire:change="salvaCanaleComunicazione({{ $soggettoId }}, 'verbali', $event.target.value)"
|
||||
class="w-full text-xs rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 font-semibold text-gray-700">
|
||||
@foreach(['PEC', 'Raccomandata AR', 'Posta Ordinaria', 'Mano'] as $opt)
|
||||
<option value="{{ $opt }}" {{ $p['verbali'] === $opt ? 'selected' : '' }}>{{ $opt }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<select
|
||||
wire:change="salvaCanaleComunicazione({{ $soggettoId }}, 'solleciti', $event.target.value)"
|
||||
class="w-full text-xs rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 font-semibold text-gray-700">
|
||||
@foreach(['Email Standard', 'PEC', 'Posta Ordinaria'] as $opt)
|
||||
<option value="{{ $opt }}" {{ $p['solleciti'] === $opt ? 'selected' : '' }}>{{ $opt }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</x-filament::section>
|
||||
@endif
|
||||
|
||||
@if($tab === 'persone')
|
||||
<x-filament::section>
|
||||
<div class="flex items-center gap-2 text-sm font-semibold text-gray-900 mb-3">
|
||||
<x-filament::icon icon="heroicon-o-user-group" class="h-5 w-5 text-primary-600" />
|
||||
<span>Persone collegate</span>
|
||||
</div>
|
||||
@php
|
||||
$blocchiRelazioni = [
|
||||
'proprietari' => ['label' => 'Proprietari / comproprietari', 'class' => 'text-primary-700'],
|
||||
'inquilini' => ['label' => 'Inquilini / conduttori', 'class' => 'text-sky-700'],
|
||||
'altri' => ['label' => 'Altri soggetti collegati', 'class' => 'text-gray-700'],
|
||||
];
|
||||
@endphp
|
||||
<div class="space-y-3">
|
||||
@foreach($blocchiRelazioni as $key => $cfg)
|
||||
@php $relazioni = $relazioniPerTipo[$key] ?? []; @endphp
|
||||
<div class="rounded-2xl border bg-gray-50 p-3">
|
||||
<div class="mb-2 text-sm font-semibold {{ $cfg['class'] }}">{{ $cfg['label'] }} ({{ count($relazioni) }})</div>
|
||||
@if(empty($relazioni))
|
||||
<div class="text-sm text-gray-500">Nessun soggetto collegato.</div>
|
||||
@else
|
||||
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
@foreach($relazioni as $relazione)
|
||||
<div class="rounded-xl border bg-white p-3 shadow-sm">
|
||||
<div class="text-xs text-gray-500">{{ $relazione['tipo'] ?? 'Relazione' }}</div>
|
||||
<div class="text-sm font-semibold text-gray-900">{{ $relazione['nome'] }}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">CF: {{ $relazione['codice_fiscale'] ?? '—' }}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">Quota: {{ $relazione['quota_label'] ?? '—' }}%</div>
|
||||
@if(!empty($relazione['telefono']))
|
||||
<div class="text-xs text-gray-500 mt-1">Telefono: {{ $relazione['telefono'] }}</div>
|
||||
@endif
|
||||
@if(!empty($relazione['email']))
|
||||
<div class="text-xs text-gray-500 mt-1">Email: {{ $relazione['email'] }}</div>
|
||||
@endif
|
||||
@if(!empty($relazione['data_inizio']) || !empty($relazione['data_fine']))
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
Periodo:
|
||||
@if(!empty($relazione['data_inizio'])) Da {{ $relazione['data_inizio'] }} @endif
|
||||
@if(!empty($relazione['data_fine'])) · fino a {{ $relazione['data_fine'] }} @endif
|
||||
</div>
|
||||
@endif
|
||||
@if(!empty($relazione['rubrica_url']))
|
||||
<div class="mt-2">
|
||||
<a href="{{ $relazione['rubrica_url'] }}" class="inline-flex items-center rounded-md border border-indigo-200 bg-indigo-50 px-2 py-1 text-[11px] font-medium text-indigo-700 hover:bg-indigo-100">Apri scheda rubrica</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<div class="mb-2 text-sm font-semibold text-gray-900">Recapiti email per servizio</div>
|
||||
@if(empty($recapitiServizio))
|
||||
<div class="text-sm text-gray-500">Nessun recapito email disponibile.</div>
|
||||
@else
|
||||
<div class="grid gap-3 lg:grid-cols-2">
|
||||
@foreach($recapitiServizio as $service)
|
||||
<div class="rounded-2xl border bg-gray-50 p-3">
|
||||
<div class="text-sm font-semibold text-gray-900">{{ $service['label'] ?? 'Servizio' }}</div>
|
||||
@if(empty($service['rows']))
|
||||
<div class="mt-2 text-sm text-gray-500">Nessun recapito risolto.</div>
|
||||
@else
|
||||
<div class="mt-2 space-y-2">
|
||||
@foreach($service['rows'] as $recipient)
|
||||
<div class="rounded-xl border bg-white p-3 shadow-sm">
|
||||
<div class="text-sm font-semibold text-gray-900">{{ $recipient['email'] ?? '—' }}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">{{ $recipient['persona'] ?: 'Recapito unità' }}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">Origine: {{ $recipient['sourceLabel'] ?? 'Anagrafica persona' }}</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-filament::section>
|
||||
</x-filament::section>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($tab === 'acqua')
|
||||
|
|
|
|||
|
|
@ -4,22 +4,29 @@ # CURRENT-205
|
|||
MACHINE: .205
|
||||
STATO: completato
|
||||
|
||||
## Obiettivo
|
||||
## Obiettivo Completato
|
||||
|
||||
1. **Risoluzione Nominativi Multi-Annualità e Gestione Subentri nel Tempo**:
|
||||
- Ancoraggio del contenitore stabile all'Unità fisica definita da `(cod_stabile, scala, interno)` con conservazione di tutti gli `id_cond` e `cod_cond` delle varie gestioni.
|
||||
- Allineamento delle finestre temporali (`data_inizio`, `data_fine`) in `unita_anagrafica_periodo` per tutti i subentri storici dello Stabile 0013.
|
||||
- Risoluzione dinamica dei nominativi nel selettore a tendina e nella scheda unità in base all'`anno_gestione` attivo (2026, 2024, 2020, ecc.).
|
||||
2. **Allineamento Catasto e Subentri Esemplari**:
|
||||
- **Scala B Int. 6 (ID 247)**: COCCIA Simone (2026) vs DE SANTIS Fabio (2020) con allineamento Sister Catasto (Sub. 25, Rendita € 742,41).
|
||||
- **Scala B Int. 1 (ID 242)**: Tabacchini Giuseppe (2026) vs PAVIA Monica (2024).
|
||||
- **Scala B Int. 3 / B Int. 4**: RDP S.R.L. limitata esclusivamente a queste due unità dal 08/04/2026, rimossa da tutte le altre unità di Scala B.
|
||||
- **Scala A Int. 1 (ID 233)**: Spadavecchia Aldo (condomino) e Kiwa Cermet Spa (inquilino), bonificati i comproprietari incrociati orfani.
|
||||
3. **Restyling Compatto Tab Riepilogo & Matrice Recapiti Multicanale**:
|
||||
- Eliminazione dei grossi box "Indicatori principali" (Superficie, millesimi, valore) e dei 7 riquadri vuoti dei servizi.
|
||||
- Inserimento schede compatte anagrafiche: Condomino (Proprietario) attivo con quota e CF, Inquilino attivo con quota spese, Dati Catasto e Fabbricato.
|
||||
- Nuova tabella ad alta densità informativa **Recapiti Multicanale Dinamici** (Email, PEC, Cellulari, Telefoni Fissi, Indirizzo) con badge colorati, stato principale e pulsanti rapidi di apertura `mailto:` e `tel:`.
|
||||
- Rimozione della vecchia timeline ridondante dal Riepilogo; il tab **Nominativi & Subentri** ora gestisce la cronistoria temporale completa con date formattate e pulsante diretto `[ 🧾 Vai all'Estratto Conto del Soggetto ]`.
|
||||
|
||||
2. **Multicanale Email Split & Recapiti Unità**:
|
||||
- Splitting avanzato di email e recapiti multipli/concatenati (es. `danilo.ritzu@kiwacermet.it,tesoreria@kiwacermet.it,riccardo.mantovani@kiwacermet.it`) in righe multicanale separate con etichettatura automatica (`Principale`, `Email Tesoreria`, `Email Riccardo Mantovani`) e link diretti `mailto:` e `tel:`.
|
||||
|
||||
3. **Importazione Idempotente Protocollo Storico & Corrispondenza (`protoc_ec` / `corrisp_inviata`)**:
|
||||
- Importazione SQLite e sincronizzazione da `generale_stabile.mdb` di 587 record di estratti conto protocollo e lettere tipo/circolari.
|
||||
- Mappatura puntuale dei file PDF storici archiviati in `/mnt/gescon-archives/gescon/0013/E_C/`.
|
||||
|
||||
4. **Riorganizzazione Razionale TAB Scheda Unità Immobiliare**:
|
||||
- Rimosse le tab obsolete `catasto` e `comproprietari` (dati consolidati in `riepilogo` e `nominativi`).
|
||||
- Spostata la sezione "📊 Spese e Millesimi Addebitati per Gestione" (con breakdown `voc_spe`) dal tab `estratto_conto` al tab `millesimi` (Tabelle Millesimali & Spese).
|
||||
- Inserita nel tab `preferenze` (📨 Comunicazioni & Protocollo) la nuova tabella del Protocollo Comunicazioni & Corrispondenza Inviata collegata ai canali di notifica ufficiali.
|
||||
|
||||
5. **Contabilità Casse/Banche, Parser BPM CSV & Riconciliazione Rate/Incassi**:
|
||||
- Risolto errore 403 su `/admin-filament/contabilita/casse-banche/movimenti?conto_id=8` e `riepilogo` per ruolo `amministratore`.
|
||||
- Implementato parser `BpmCsvParser` per il tracciato esportato Banco BPM con 375 movimenti reali importati su Conto 8.
|
||||
- Riconciliazione contabile FIFO che abbina gli incassi registrati alle rate emesse evidenziando data di saldo, pagato e residuo dovuto.
|
||||
|
||||
## Output del Giro Operativo
|
||||
|
||||
|
|
@ -27,21 +34,25 @@ ## Output del Giro Operativo
|
|||
TASK_ID: task-87b64082c1
|
||||
REPOSITORY: ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git
|
||||
BRANCH: stabilization/205-zero
|
||||
COMMIT: 6f20d81
|
||||
COMMIT: da_effettuare
|
||||
FILE_O_AREE_TOCCATE:
|
||||
- app/Console/Commands/GesconBonificaStabile0013Command.php
|
||||
- app/Models/UnitaImmobiliare.php
|
||||
- app/Filament/Pages/Contabilita/CasseBancheMovimenti.php
|
||||
- app/Filament/Pages/Contabilita/CasseBancheRiepilogo.php
|
||||
- app/Filament/Pages/UnitaImmobiliarePage.php
|
||||
- app/Services/Contabilita/BpmCsvParser.php
|
||||
- app/Services/Contabilita/MovimentiBancaImporter.php
|
||||
- resources/views/filament/pages/unita-immobiliare.blade.php
|
||||
- tests/Feature/UnitaGestioneTemporaleTest.php
|
||||
- tests/Feature/CatastoHubDbDrivenTest.php
|
||||
- skill-netgescon/ui-wireframes/utente-multi-ruolo.md
|
||||
- tests/Feature/UnitaImmobiliarePageTest.php
|
||||
TEST_ESEGUITI:
|
||||
- ./vendor/bin/pest tests/Feature/UnitaGestioneTemporaleTest.php tests/Feature/CatastoHubDbDrivenTest.php tests/Feature/UnitaCondominoInquilinoRoleToggleTest.php tests/Feature/UnitaImmobiliarePageTest.php tests/Feature/AnagraficaUnicaCanonicaTest.php tests/Feature/BenedettoBonificaIdempotenteTest.php tests/Feature/ControlTowerPollCommandTest.php (20 passed, 115 assertions)
|
||||
- ./vendor/bin/pest tests/Feature/UnitaGestioneTemporaleTest.php tests/Feature/CatastoHubDbDrivenTest.php tests/Feature/UnitaCondominoInquilinoRoleToggleTest.php tests/Feature/UnitaImmobiliarePageTest.php tests/Feature/AnagraficaUnicaCanonicaTest.php tests/Feature/BenedettoBonificaIdempotenteTest.php tests/Feature/ControlTowerPollCommandTest.php (22 passed, 131 assertions)
|
||||
GATE_STATISTICS:
|
||||
- UNITA_REALI_0013: 42 unità canoniche con cronistoria temporale multi-gestione.
|
||||
- RDP_CLEANUP: RDP S.R.L. circoscritta esclusivamente a B-3 e B-4, rimossa da B-1, B-2, B-5, B-7.
|
||||
- MULTICHANNEL_CONTACTS_TABLE: Matrice recapiti multicanale tabellare compatta e responsive.
|
||||
- ZERO_MOCK: Dati catastali 100% DB-driven e Sister API live.
|
||||
- PROTOCOLLO_EC_0013: 587 record sincronizzati da generale_stabile.mdb con link PDF.
|
||||
- MOVIMENTI_BANCARI_BPM: 375 movimenti reali importati su Conto 8.
|
||||
- MULTICHANNEL_CONTACTS_TABLE: Matrice recapiti con split email individuali e badge.
|
||||
- ZERO_MOCK: Dati catastali e contabili 100% DB-driven.
|
||||
BLOCCO_DATI: no
|
||||
BLOCCO_CONTRATTO: no
|
||||
RISCHI_APERTI: nessuno
|
||||
|
|
@ -49,5 +60,5 @@ ## Output del Giro Operativo
|
|||
## Prossimo Passo per .200 (Validazione)
|
||||
|
||||
- Eseguire il checkout del branch `stabilization/205-zero`.
|
||||
- Eseguire `php artisan gescon:bonifica-stabile-0013` per convalidare la bonifica, l'eliminazione dei comproprietari incrociati e l'allineamento temporale.
|
||||
- Eseguire la suite di test Pest (20 test, 115 asserzioni).
|
||||
- Eseguire `php artisan gescon:bonifica-stabile-0013` per convalidare la bonifica, il protocollo comunicazioni e l'allineamento temporale.
|
||||
- Eseguire la suite di test Pest (22 test, 131 asserzioni).
|
||||
|
|
|
|||
|
|
@ -27,12 +27,12 @@ ## 1. VISUALIZZAZIONE DESKTOP (High-Density Multi-Column Grid)
|
|||
| +----+-----------------+-----------------------------------+--------------------------------+---------------------+
|
||||
+-------------------------------------------------------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
------------------------------------------------------------------------------------------------------------------+
|
||||
|
|
||||
## 2. MASCHERA REGISTRAZIONE / SCHEDA DETTAGLIO UTENTE MULTI-RUOLO & MULTI-RECAPITI
|
||||
|
||||
```text
|
||||
+-------------------------------------------------------------------------------------------------------------------+
|
||||
| MASCHERA REGISTRAZIONE / EDITING UTENTE MULTI-RUOLO (#1 BARONE MICHELE) |
|
||||
+-MASCHERA REGISTRAZIONE / EDITING UTENTE MULTI-RUOLO (#1 BARONE MICHELE) |
|
||||
+-------------------------------------------------------------------------------------------------------------------+
|
||||
| DATI ANAGRAFICI BASE: |
|
||||
| [ Nome Completo: BARONE MICHELE ] [ Codice Fiscale: BRNMHL69M11H501K ] |
|
||||
|
|
|
|||
|
|
@ -11,29 +11,21 @@
|
|||
|
||||
beforeEach(function () {
|
||||
DB::table('amministratori')->insertOrIgnore(['id' => 1, 'nome' => 'Admin Test', 'cognome' => 'Test', 'created_at' => now(), 'updated_at' => now()]);
|
||||
DB::table('persone_unita_relazioni')->truncate();
|
||||
DB::table('persone')->truncate();
|
||||
|
||||
$stabile = Stabile::where('codice_stabile', '0021')->first();
|
||||
if (! $stabile) {
|
||||
$mirrorCount = DB::connection('gescon_import')
|
||||
->table('condomin_mirror')
|
||||
->where('cod_stabile', '0021')
|
||||
->count();
|
||||
if ($mirrorCount === 0) {
|
||||
Artisan::call('gescon:import-mirror-0021');
|
||||
}
|
||||
Artisan::call('gescon:reconstruct-mirror-0021');
|
||||
$stabile = Stabile::where('codice_stabile', '0021')->first();
|
||||
$mirrorCount = DB::connection('gescon_import')
|
||||
->table('condomin_mirror')
|
||||
->where('cod_stabile', '0021')
|
||||
->count();
|
||||
if ($mirrorCount === 0) {
|
||||
Artisan::call('gescon:import-mirror-0021');
|
||||
}
|
||||
Artisan::call('gescon:reconstruct-mirror-0021');
|
||||
});
|
||||
|
||||
test('unita immobiliare page mounts without 500 or undefined property exception when canale_convocazione is null or missing', function () {
|
||||
$user = User::first();
|
||||
if (! $user) {
|
||||
$user = User::factory()->create();
|
||||
}
|
||||
expect($user)->not->toBeNull();
|
||||
|
||||
// Assign admin role if Spatie permissions are present
|
||||
$user = User::first() ?: User::factory()->create();
|
||||
try {
|
||||
if (method_exists($user, 'assignRole')) {
|
||||
\Spatie\Permission\Models\Role::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']);
|
||||
|
|
@ -46,15 +38,12 @@
|
|||
|
||||
Auth::login($user);
|
||||
|
||||
// Unit 13 is Scala A Int 11 (0021-A-11)
|
||||
$unit = UnitaImmobiliare::where('stabile_id', $stabile->id)->where('scala', 'A')->where('interno', '11')->first();
|
||||
expect($unit)->not->toBeNull();
|
||||
|
||||
request()->merge(['unita_id' => $unit->id]);
|
||||
|
||||
$page = new UnitaImmobiliarePage();
|
||||
|
||||
// Verify mount executes without any Undefined property exception or error
|
||||
$page->mount();
|
||||
|
||||
expect($page->canaliComunicazione)->toBeArray();
|
||||
|
|
@ -86,3 +75,82 @@
|
|||
expect($benedettoPerson->cognome)->toBe('BENEDETTO');
|
||||
expect($benedettoPerson->nome)->toBe('DANIELA');
|
||||
});
|
||||
|
||||
test('unita immobiliare splits multiple concatenated emails into individual structured channels', function () {
|
||||
$user = User::first() ?: User::factory()->create();
|
||||
try {
|
||||
if (method_exists($user, 'assignRole')) {
|
||||
\Spatie\Permission\Models\Role::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']);
|
||||
$user->assignRole('admin');
|
||||
}
|
||||
} catch (\Throwable $e) {}
|
||||
Auth::login($user);
|
||||
|
||||
$stabile = Stabile::where('codice_stabile', '0021')->first();
|
||||
expect($stabile)->not->toBeNull();
|
||||
StabileContext::setActiveStabileId($user, $stabile->id);
|
||||
|
||||
$unit = UnitaImmobiliare::where('stabile_id', $stabile->id)->where('scala', 'A')->where('interno', '11')->first();
|
||||
expect($unit)->not->toBeNull();
|
||||
|
||||
// Create a persona with multiple concatenated emails
|
||||
$multiEmailPersona = \App\Models\Persona::create([
|
||||
'cognome' => 'MULTI EMAIL TEST SRL',
|
||||
'nome' => '',
|
||||
'email_principale' => 'info@test.it, amministrazione@test.it, pec@test.it',
|
||||
'telefono_principale' => '061234567',
|
||||
'attivo' => 1,
|
||||
]);
|
||||
|
||||
DB::table('persone_unita_relazioni')->insert([
|
||||
'unita_id' => $unit->id,
|
||||
'persona_id' => $multiEmailPersona->id,
|
||||
'tipo_relazione' => 'inquilino',
|
||||
'data_inizio' => '2026-01-01',
|
||||
'attivo' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
request()->merge(['unita_id' => $unit->id]);
|
||||
$page = new UnitaImmobiliarePage();
|
||||
$page->mount();
|
||||
|
||||
$recapiti = $page->getRecapitiMulticanaleTableProperty();
|
||||
expect($recapiti)->toBeArray();
|
||||
|
||||
$emails = array_values(array_filter($recapiti, fn($r) => $r['canale'] === 'Email'));
|
||||
expect(count($emails))->toBeGreaterThanOrEqual(3);
|
||||
|
||||
foreach ($emails as $em) {
|
||||
expect($em['valore'])->not->toContain(',');
|
||||
expect($em['valore'])->not->toContain(';');
|
||||
expect($em['action_url'])->toStartWith('mailto:');
|
||||
}
|
||||
});
|
||||
|
||||
test('unita immobiliare protocollo comunicazioni returns valid structure', function () {
|
||||
$user = User::first() ?: User::factory()->create();
|
||||
try {
|
||||
if (method_exists($user, 'assignRole')) {
|
||||
\Spatie\Permission\Models\Role::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']);
|
||||
$user->assignRole('admin');
|
||||
}
|
||||
} catch (\Throwable $e) {}
|
||||
Auth::login($user);
|
||||
|
||||
$stabile = Stabile::where('codice_stabile', '0021')->first();
|
||||
expect($stabile)->not->toBeNull();
|
||||
StabileContext::setActiveStabileId($user, $stabile->id);
|
||||
|
||||
$unit = UnitaImmobiliare::where('stabile_id', $stabile->id)->where('scala', 'A')->where('interno', '11')->first();
|
||||
expect($unit)->not->toBeNull();
|
||||
|
||||
request()->merge(['unita_id' => $unit->id]);
|
||||
$page = new UnitaImmobiliarePage();
|
||||
$page->mount();
|
||||
|
||||
$protocollo = $page->getProtocolloComunicazioniProperty();
|
||||
expect($protocollo)->toBeArray();
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user