chore: checkpoint current workspace state
This commit is contained in:
parent
09022d5753
commit
b3e626b613
|
|
@ -4,6 +4,8 @@ # Changelog
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
- Consolidated the banking workflow around Contabilita > Casse e banche > Movimenti, now used as a 4-tab hub for account overview, official balances and statement PDFs, imported bank ledger movements, and first-pass reconciliation candidates.
|
||||
- Moved official bank balance and statement management out of Situazione iniziale, fixed bank document URLs to use storage-backed paths, and anchored balance reconstruction on `conto_id` so legacy accounts without IBAN still reconcile correctly.
|
||||
- Added stable-level Documentazione, Posta ufficiale and Protocollo comunicazioni tabs, plus a real admin-wide Comunicazioni regia page reusing Google accounts, Gmail import, Drive templates and protocol records.
|
||||
- Seeded the official Gmail path for stabile `0023` `GERMANICO 79` (`viagermanico79@gmail.com`), hardened PEC studio persistence against the missing `pec_amministratore` DB column, and linked the new stable documentation hub to the existing Google settings workflow.
|
||||
- Applied `gescon:auto-sync-anagrafiche` for `GERMANICO 79` with stable/unit role linking and imported the 2024 legacy water dataset from `generale_stabile.mdb` into servizi/letture without closing historic years.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
|
@ -20,7 +19,7 @@ class GesconSyncOrdinariaPreventivoCommand extends Command
|
|||
public function handle(): int
|
||||
{
|
||||
$stableCode = trim((string) $this->argument('stabile_code'));
|
||||
$year = (int) $this->argument('year');
|
||||
$year = (int) $this->argument('year');
|
||||
|
||||
if ($stableCode === '' || $year < 1900) {
|
||||
$this->error('Parametri non validi.');
|
||||
|
|
@ -28,7 +27,7 @@ public function handle(): int
|
|||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$stableId = $this->resolveStableId($stableCode);
|
||||
$stableId = $this->resolveStableId($stableCode);
|
||||
$gestioneId = $this->resolveGestioneId($stableId, $year);
|
||||
$legacyVoci = $this->loadLegacyVoci($stableCode, $year);
|
||||
|
||||
|
|
@ -38,15 +37,16 @@ public function handle(): int
|
|||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$tableCodes = $legacyVoci
|
||||
->pluck('tabella')
|
||||
->filter(fn ($value): bool => is_string($value) && trim($value) !== '')
|
||||
->map(fn (string $value): string => trim($value))
|
||||
->unique()
|
||||
->values();
|
||||
$legacyHeaders = $this->loadLegacyTableHeaders($stableCode);
|
||||
if ($legacyHeaders === []) {
|
||||
$this->error('Nessuna intestazione tabella legacy trovata per stabile ' . $stableCode . '.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$tableCodes = collect(array_keys($legacyHeaders))->values();
|
||||
$legacyDetails = $this->loadLegacyDettagli($stableCode, $year, $tableCodes);
|
||||
$legacyHeaders = $this->loadLegacyTableHeaders($stableCode, $tableCodes);
|
||||
$this->assertLegacyVociHaveKnownTables($legacyVoci, $tableCodes);
|
||||
|
||||
DB::transaction(function () use ($stableCode, $stableId, $year, $gestioneId, $legacyVoci, $legacyDetails, $tableCodes, $legacyHeaders): void {
|
||||
$this->ensureLegacyUnitCoverage($stableCode, $stableId, $legacyDetails);
|
||||
|
|
@ -76,7 +76,7 @@ private function resolveStableId(string $stableCode): int
|
|||
return (int) $optionStableId;
|
||||
}
|
||||
|
||||
$query = DB::table('stabili');
|
||||
$query = DB::table('stabili');
|
||||
$stable = $query
|
||||
->where('codice_stabile', $stableCode)
|
||||
->orWhere('codice_stabile', ltrim($stableCode, '0'))
|
||||
|
|
@ -159,13 +159,13 @@ private function loadLegacyDettagli(string $stableCode, int $year, Collection $t
|
|||
/**
|
||||
* @return array<string, object>
|
||||
*/
|
||||
private function loadLegacyTableHeaders(string $stableCode, Collection $tableCodes): array
|
||||
private function loadLegacyTableHeaders(string $stableCode): array
|
||||
{
|
||||
if ($tableCodes->isEmpty() || ! Schema::connection('gescon_import')->hasTable('tabelle_millesimali')) {
|
||||
if (! Schema::connection('gescon_import')->hasTable('tabelle_millesimali')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$columns = Schema::connection('gescon_import')->getColumnListing('tabelle_millesimali');
|
||||
$columns = Schema::connection('gescon_import')->getColumnListing('tabelle_millesimali');
|
||||
$codeColumn = in_array('cod_tabella', $columns, true)
|
||||
? 'cod_tabella'
|
||||
: (in_array('cod_tab', $columns, true) ? 'cod_tab' : null);
|
||||
|
|
@ -179,8 +179,13 @@ private function loadLegacyTableHeaders(string $stableCode, Collection $tableCod
|
|||
$query->where('cod_stabile', $stableCode);
|
||||
}
|
||||
|
||||
if (in_array('nord', $columns, true)) {
|
||||
$query
|
||||
->orderByRaw('CASE WHEN nord IS NULL THEN 1 ELSE 0 END')
|
||||
->orderBy('nord');
|
||||
}
|
||||
|
||||
$rows = $query
|
||||
->whereIn($codeColumn, $tableCodes->all())
|
||||
->orderBy($codeColumn)
|
||||
->get();
|
||||
|
||||
|
|
@ -198,12 +203,29 @@ private function loadLegacyTableHeaders(string $stableCode, Collection $tableCod
|
|||
return $headers;
|
||||
}
|
||||
|
||||
private function assertLegacyVociHaveKnownTables(Collection $legacyVoci, Collection $tableCodes): void
|
||||
{
|
||||
$knownTables = array_fill_keys($tableCodes->all(), true);
|
||||
$unknown = $legacyVoci
|
||||
->pluck('tabella')
|
||||
->filter(fn($value): bool => is_string($value) && trim($value) !== '')
|
||||
->map(fn(string $value): string => trim($value))
|
||||
->reject(fn(string $value): bool => isset($knownTables[$value]))
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
|
||||
if ($unknown !== []) {
|
||||
throw new \RuntimeException('Tabelle legacy referenziate dalle voci ma assenti nelle intestazioni archive: ' . implode(', ', $unknown) . '.');
|
||||
}
|
||||
}
|
||||
|
||||
private function ensureLegacyUnitCoverage(string $stableCode, int $stableId, Collection $legacyDetails): void
|
||||
{
|
||||
$legacyIds = $legacyDetails
|
||||
->pluck('id_cond')
|
||||
->filter(fn ($value): bool => is_string($value) && trim($value) !== '')
|
||||
->map(fn (string $value): string => trim($value))
|
||||
->filter(fn($value): bool => is_string($value) && trim($value) !== '')
|
||||
->map(fn(string $value): string => trim($value))
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
|
|
@ -212,7 +234,7 @@ private function ensureLegacyUnitCoverage(string $stableCode, int $stableId, Col
|
|||
->whereNull('deleted_at')
|
||||
->whereIn('legacy_cond_id', $legacyIds->all())
|
||||
->pluck('legacy_cond_id')
|
||||
->map(fn ($value): string => trim((string) $value))
|
||||
->map(fn($value): string => trim((string) $value))
|
||||
->all();
|
||||
|
||||
$existingSet = array_fill_keys($existingIds, true);
|
||||
|
|
@ -263,9 +285,9 @@ private function createMissingLegacyUnit(string $stableCode, int $stableId, stri
|
|||
throw new \RuntimeException('Interno mancante per il legacy id ' . $legacyId . '.');
|
||||
}
|
||||
|
||||
$scale = trim((string) ($sourceRow->scala ?? ''));
|
||||
$scale = $scale !== '' ? strtoupper($scale) : 'A';
|
||||
$floor = $this->normalizeFloor($sourceRow->piano ?? null);
|
||||
$scale = trim((string) ($sourceRow->scala ?? ''));
|
||||
$scale = $scale !== '' ? strtoupper($scale) : 'A';
|
||||
$floor = $this->normalizeFloor($sourceRow->piano ?? null);
|
||||
$displayName = trim((string) (($sourceRow->cognome ?? '') . ' ' . ($sourceRow->nome ?? '')));
|
||||
if ($displayName === '') {
|
||||
$displayName = trim((string) ($sourceRow->nom_cond ?? ''));
|
||||
|
|
@ -278,21 +300,21 @@ private function createMissingLegacyUnit(string $stableCode, int $stableId, stri
|
|||
}
|
||||
|
||||
DB::table('unita_immobiliari')->insert([
|
||||
'stabile_id' => $stableId,
|
||||
'codice_unita' => $code,
|
||||
'denominazione' => $displayName !== '' ? $displayName : ('Unita legacy ' . $legacyId),
|
||||
'scala' => $scale,
|
||||
'piano' => $floor,
|
||||
'interno' => $internal,
|
||||
'legacy_cond_id' => $legacyId,
|
||||
'tipo_unita' => 'abitazione',
|
||||
'stato_occupazione' => 'occupata_proprietario',
|
||||
'stabile_id' => $stableId,
|
||||
'codice_unita' => $code,
|
||||
'denominazione' => $displayName !== '' ? $displayName : ('Unita legacy ' . $legacyId),
|
||||
'scala' => $scale,
|
||||
'piano' => $floor,
|
||||
'interno' => $internal,
|
||||
'legacy_cond_id' => $legacyId,
|
||||
'tipo_unita' => 'abitazione',
|
||||
'stato_occupazione' => 'occupata_proprietario',
|
||||
'stato_conservazione' => 'buono',
|
||||
'millesimi_generali' => 0,
|
||||
'attiva' => true,
|
||||
'unita_demo' => false,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
'millesimi_generali' => 0,
|
||||
'attiva' => true,
|
||||
'unita_demo' => false,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -301,31 +323,64 @@ private function createMissingLegacyUnit(string $stableCode, int $stableId, stri
|
|||
*/
|
||||
private function syncTabelleMillesimali(int $stableId, int $year, Collection $tableCodes, Collection $legacyDetails, array $legacyHeaders): array
|
||||
{
|
||||
$existingRows = DB::table('tabelle_millesimali')
|
||||
->where('stabile_id', $stableId)
|
||||
->get(['id', 'codice_tabella', 'nome_tabella']);
|
||||
|
||||
$existingByCode = [];
|
||||
foreach ($existingRows as $existingRow) {
|
||||
foreach ([
|
||||
strtoupper(trim((string) ($existingRow->codice_tabella ?? ''))),
|
||||
strtoupper(trim((string) ($existingRow->nome_tabella ?? ''))),
|
||||
] as $existingCode) {
|
||||
if ($existingCode === '' || isset($existingByCode[$existingCode])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$existingByCode[$existingCode] = (int) $existingRow->id;
|
||||
}
|
||||
}
|
||||
|
||||
$tableIdsToRefresh = [];
|
||||
foreach ($tableCodes as $tableCode) {
|
||||
$normalizedCode = strtoupper(trim((string) $tableCode));
|
||||
$existingId = $existingByCode[$normalizedCode] ?? null;
|
||||
if ($existingId) {
|
||||
$tableIdsToRefresh[] = $existingId;
|
||||
}
|
||||
}
|
||||
|
||||
if ($tableIdsToRefresh !== []) {
|
||||
DB::table('dettaglio_millesimi')
|
||||
->whereIn('tabella_millesimale_id', $tableIdsToRefresh)
|
||||
->delete();
|
||||
}
|
||||
|
||||
$detailSummary = $legacyDetails
|
||||
->groupBy('cod_tab')
|
||||
->map(function (Collection $rows): array {
|
||||
return [
|
||||
'total_mm' => (float) $rows->sum(fn ($row): float => is_numeric($row->mm ?? null) ? (float) $row->mm : 0.0),
|
||||
'rows' => $rows->count(),
|
||||
'total_mm' => (float) $rows->sum(fn($row): float => is_numeric($row->mm ?? null) ? (float) $row->mm : 0.0),
|
||||
'rows' => $rows->count(),
|
||||
];
|
||||
});
|
||||
|
||||
$tableMap = [];
|
||||
$tableMap = [];
|
||||
$sortOrder = 1;
|
||||
|
||||
foreach ($tableCodes as $tableCode) {
|
||||
$normalizedCode = strtoupper(trim((string) $tableCode));
|
||||
$summary = $detailSummary->get($tableCode, ['total_mm' => 0.0, 'rows' => 0]);
|
||||
$header = $legacyHeaders[$normalizedCode] ?? null;
|
||||
$rawCalcolo = $header->tipo_calcolo ?? $header->calcolo ?? null;
|
||||
$calcolo = $this->normalizeLegacyCalcolo($rawCalcolo);
|
||||
$tipoLegacy = $this->normalizeLegacyTipo($header->tipologia ?? $header->tipo ?? null) ?? 'O';
|
||||
$tipoTabella = $this->resolveTipoTabellaFromLegacy($tipoLegacy, $calcolo, $normalizedCode);
|
||||
$tipoCalcoloDb = in_array($calcolo, ['millesimi', 'parti', 'fisso'], true)
|
||||
$summary = $detailSummary->get($tableCode, ['total_mm' => 0.0, 'rows' => 0]);
|
||||
$header = $legacyHeaders[$normalizedCode] ?? null;
|
||||
$rawCalcolo = $header->tipo_calcolo ?? $header->calcolo ?? null;
|
||||
$calcolo = $this->normalizeLegacyCalcolo($rawCalcolo);
|
||||
$tipoLegacy = $this->normalizeLegacyTipo($header->tipologia ?? $header->tipo ?? null) ?? 'O';
|
||||
$tipoTabella = $this->resolveTipoTabellaFromLegacy($tipoLegacy, $calcolo, $normalizedCode);
|
||||
$tipoCalcoloDb = in_array($calcolo, ['millesimi', 'parti', 'fisso'], true)
|
||||
? $calcolo
|
||||
: ($normalizedCode === 'ACQUA' ? 'parti' : 'millesimi');
|
||||
$denominazione = trim((string) ($header->denominazione ?? $header->descrizione ?? $tableCode));
|
||||
$ordine = null;
|
||||
$ordine = null;
|
||||
|
||||
foreach (['nord', 'ordinamento'] as $orderKey) {
|
||||
if (isset($header->{$orderKey}) && is_numeric($header->{$orderKey})) {
|
||||
|
|
@ -337,38 +392,38 @@ private function syncTabelleMillesimali(int $stableId, int $year, Collection $ta
|
|||
$sortValue = $ordine ?? $sortOrder;
|
||||
|
||||
$payload = [
|
||||
'stabile_id' => $stableId,
|
||||
'anno_gestione' => $year,
|
||||
'nome_tabella' => $tableCode,
|
||||
'denominazione' => $denominazione !== '' ? $denominazione : $tableCode,
|
||||
'codice_tabella' => $tableCode,
|
||||
'legacy_codice' => $tableCode,
|
||||
'stabile_id' => $stableId,
|
||||
'anno_gestione' => $year,
|
||||
'nome_tabella' => $tableCode,
|
||||
'denominazione' => $denominazione !== '' ? $denominazione : $tableCode,
|
||||
'codice_tabella' => $tableCode,
|
||||
'legacy_codice' => $tableCode,
|
||||
'ordine_visualizzazione' => $sortValue,
|
||||
'tipo_tabella' => $tipoTabella,
|
||||
'tipo_calcolo' => $tipoCalcoloDb,
|
||||
'attiva' => true,
|
||||
'ordinamento' => $sortValue,
|
||||
'nord' => $sortValue,
|
||||
'totale_millesimi' => $summary['total_mm'],
|
||||
'meta_legacy' => json_encode([
|
||||
'tipo_tabella' => $tipoTabella,
|
||||
'tipo_calcolo' => $tipoCalcoloDb,
|
||||
'attiva' => true,
|
||||
'ordinamento' => $sortValue,
|
||||
'nord' => $sortValue,
|
||||
'totale_millesimi' => $summary['total_mm'],
|
||||
'meta_legacy' => json_encode([
|
||||
'strict_source' => 'gescon_import',
|
||||
'legacy_year' => $year,
|
||||
'cod_tab' => $normalizedCode,
|
||||
'tipo' => $tipoLegacy,
|
||||
'calcolo' => is_string($rawCalcolo) && trim($rawCalcolo) !== '' ? strtoupper(trim($rawCalcolo)) : null,
|
||||
'detail_rows' => $summary['rows'],
|
||||
'legacy_year' => $year,
|
||||
'cod_tab' => $normalizedCode,
|
||||
'tipo' => $tipoLegacy,
|
||||
'calcolo' => is_string($rawCalcolo) && trim($rawCalcolo) !== '' ? strtoupper(trim($rawCalcolo)) : null,
|
||||
'detail_rows' => $summary['rows'],
|
||||
]),
|
||||
'updated_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
|
||||
$existing = DB::table('tabelle_millesimali')
|
||||
->where('stabile_id', $stableId)
|
||||
->where('codice_tabella', $tableCode)
|
||||
->first(['id']);
|
||||
$existingId = $existingByCode[$normalizedCode] ?? null;
|
||||
|
||||
if ($existing) {
|
||||
DB::table('tabelle_millesimali')->where('id', (int) $existing->id)->update($payload);
|
||||
$tableMap[$tableCode] = (int) $existing->id;
|
||||
if ($existingId) {
|
||||
DB::table('tabelle_millesimali')
|
||||
->where('id', $existingId)
|
||||
->update($payload);
|
||||
|
||||
$tableMap[$tableCode] = (int) $existingId;
|
||||
} else {
|
||||
$payload['created_at'] = now();
|
||||
DB::table('tabelle_millesimali')->insert($payload);
|
||||
|
|
@ -381,9 +436,49 @@ private function syncTabelleMillesimali(int $stableId, int $year, Collection $ta
|
|||
$sortOrder++;
|
||||
}
|
||||
|
||||
$this->pruneUnusedTablesOutsideLegacySet($stableId, $tableCodes);
|
||||
|
||||
return $tableMap;
|
||||
}
|
||||
|
||||
private function pruneUnusedTablesOutsideLegacySet(int $stableId, Collection $tableCodes): void
|
||||
{
|
||||
$allowedCodes = $tableCodes
|
||||
->map(fn($value): string => strtoupper(trim((string) $value)))
|
||||
->filter(fn(string $value): bool => $value !== '')
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$rows = DB::table('tabelle_millesimali')
|
||||
->where('stabile_id', $stableId)
|
||||
->get(['id', 'codice_tabella', 'nome_tabella']);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$code = strtoupper(trim((string) ($row->codice_tabella ?: $row->nome_tabella ?: '')));
|
||||
if ($code === '' || in_array($code, $allowedCodes, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tableId = (int) $row->id;
|
||||
$hasVoci = DB::table('voci_spesa')
|
||||
->where('stabile_id', $stableId)
|
||||
->where('tabella_millesimale_default_id', $tableId)
|
||||
->exists();
|
||||
|
||||
$hasDettagli = DB::table('dettaglio_millesimi')
|
||||
->where('tabella_millesimale_id', $tableId)
|
||||
->exists();
|
||||
|
||||
if ($hasVoci || $hasDettagli) {
|
||||
continue;
|
||||
}
|
||||
|
||||
DB::table('tabelle_millesimali')
|
||||
->where('id', $tableId)
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, int> $tableMap
|
||||
*/
|
||||
|
|
@ -409,7 +504,7 @@ private function syncDettagliMillesimali(int $stableId, int $year, Collection $l
|
|||
|
||||
foreach ($groupedRows as $groupKey => $rows) {
|
||||
[$tableCode, $legacyId, $role] = explode('|', $groupKey);
|
||||
$tableId = $tableMap[$tableCode] ?? null;
|
||||
$tableId = $tableMap[$tableCode] ?? null;
|
||||
if (! $tableId) {
|
||||
throw new \RuntimeException('Tabella millesimale non risolta per codice ' . $tableCode . '.');
|
||||
}
|
||||
|
|
@ -424,29 +519,29 @@ private function syncDettagliMillesimali(int $stableId, int $year, Collection $l
|
|||
throw new \RuntimeException('Unita NetGescon non risolta per legacy_cond_id ' . $legacyId . '.');
|
||||
}
|
||||
|
||||
$millesimi = (float) $rows->sum(fn ($row): float => is_numeric($row->mm ?? null) ? (float) $row->mm : 0.0);
|
||||
$valorePrev = (float) $rows->sum(fn ($row): float => is_numeric($row->prev_euro ?? null) ? (float) $row->prev_euro : 0.0);
|
||||
$valoreCons = (float) $rows->sum(fn ($row): float => is_numeric($row->cons_euro ?? null) ? (float) $row->cons_euro : 0.0);
|
||||
$millesimi = (float) $rows->sum(fn($row): float => is_numeric($row->mm ?? null) ? (float) $row->mm : 0.0);
|
||||
$valorePrev = (float) $rows->sum(fn($row): float => is_numeric($row->prev_euro ?? null) ? (float) $row->prev_euro : 0.0);
|
||||
$valoreCons = (float) $rows->sum(fn($row): float => is_numeric($row->cons_euro ?? null) ? (float) $row->cons_euro : 0.0);
|
||||
|
||||
DB::table('dettaglio_millesimi')->insert([
|
||||
'tabella_millesimale_id' => $tableId,
|
||||
'unita_immobiliare_id' => (int) $unitId,
|
||||
'millesimi' => $millesimi,
|
||||
'valore_prev' => $valorePrev,
|
||||
'valore_cons' => $valoreCons,
|
||||
'ruolo_legacy' => $role !== '' ? $role : null,
|
||||
'legacy_interno' => $legacyId,
|
||||
'partecipa' => true,
|
||||
'note' => null,
|
||||
'legacy_payload' => json_encode([
|
||||
'unita_immobiliare_id' => (int) $unitId,
|
||||
'millesimi' => $millesimi,
|
||||
'valore_prev' => $valorePrev,
|
||||
'valore_cons' => $valoreCons,
|
||||
'ruolo_legacy' => $role !== '' ? $role : null,
|
||||
'legacy_interno' => $legacyId,
|
||||
'partecipa' => true,
|
||||
'note' => null,
|
||||
'legacy_payload' => json_encode([
|
||||
'strict_source' => 'gescon_import',
|
||||
'legacy_year' => $year,
|
||||
'cod_tab' => $tableCode,
|
||||
'id_cond' => $legacyId,
|
||||
'cond_inquil' => $role !== '' ? $role : null,
|
||||
'legacy_year' => $year,
|
||||
'cod_tab' => $tableCode,
|
||||
'id_cond' => $legacyId,
|
||||
'cond_inquil' => $role !== '' ? $role : null,
|
||||
]),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -456,6 +551,11 @@ private function syncDettagliMillesimali(int $stableId, int $year, Collection $l
|
|||
*/
|
||||
private function syncVociSpesa(int $stableId, int $gestioneId, int $year, Collection $legacyVoci, array $tableMap): void
|
||||
{
|
||||
DB::table('voci_spesa')
|
||||
->where('stabile_id', $stableId)
|
||||
->where('gestione_contabile_id', $gestioneId)
|
||||
->delete();
|
||||
|
||||
$sortOrder = 1;
|
||||
|
||||
foreach ($legacyVoci as $voce) {
|
||||
|
|
@ -465,51 +565,41 @@ private function syncVociSpesa(int $stableId, int $gestioneId, int $year, Collec
|
|||
}
|
||||
|
||||
$tableCode = trim((string) ($voce->tabella ?? ''));
|
||||
$tableId = $tableCode !== '' ? ($tableMap[$tableCode] ?? null) : null;
|
||||
$tableId = $tableCode !== '' ? ($tableMap[$tableCode] ?? null) : null;
|
||||
|
||||
if ($tableCode !== '' && ! $tableId) {
|
||||
throw new \RuntimeException('Tabella legacy non risolta per la voce ' . $code . ': ' . $tableCode . '.');
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'stabile_id' => $stableId,
|
||||
'gestione_contabile_id' => $gestioneId,
|
||||
'codice' => $code,
|
||||
'legacy_codice' => $code,
|
||||
'descrizione' => trim((string) ($voce->descriz ?? '')),
|
||||
'categoria' => 'ordinaria',
|
||||
'tipo_gestione' => 'ordinaria',
|
||||
'stabile_id' => $stableId,
|
||||
'gestione_contabile_id' => $gestioneId,
|
||||
'codice' => $code,
|
||||
'legacy_codice' => $code,
|
||||
'descrizione' => trim((string) ($voce->descriz ?? '')),
|
||||
'categoria' => 'ordinaria',
|
||||
'tipo_gestione' => 'ordinaria',
|
||||
'tabella_millesimale_default_id' => $tableId,
|
||||
'importo_default' => is_numeric($voce->preventivo_euro ?? null) ? (float) $voce->preventivo_euro : 0.0,
|
||||
'importo_consuntivo' => is_numeric($voce->consuntivo_euro ?? null) ? (float) $voce->consuntivo_euro : 0.0,
|
||||
'percentuale_condomino' => is_numeric($voce->perc_proprietario ?? null) ? (float) $voce->perc_proprietario : 0.0,
|
||||
'percentuale_inquilino' => is_numeric($voce->perc_inquilino ?? null) ? (float) $voce->perc_inquilino : 0.0,
|
||||
'attiva' => true,
|
||||
'ordinamento' => $sortOrder,
|
||||
'nord' => $sortOrder,
|
||||
'tipo' => 'ordinaria',
|
||||
'legacy_tipo' => trim((string) ($voce->v_ors ?? 'O')) ?: 'O',
|
||||
'note' => trim((string) ($voce->note ?? '')) !== '' ? trim((string) ($voce->note ?? '')) : null,
|
||||
'meta_legacy' => json_encode([
|
||||
'importo_default' => is_numeric($voce->preventivo_euro ?? null) ? (float) $voce->preventivo_euro : 0.0,
|
||||
'importo_consuntivo' => is_numeric($voce->consuntivo_euro ?? null) ? (float) $voce->consuntivo_euro : 0.0,
|
||||
'percentuale_condomino' => is_numeric($voce->perc_proprietario ?? null) ? (float) $voce->perc_proprietario : 0.0,
|
||||
'percentuale_inquilino' => is_numeric($voce->perc_inquilino ?? null) ? (float) $voce->perc_inquilino : 0.0,
|
||||
'attiva' => true,
|
||||
'ordinamento' => $sortOrder,
|
||||
'nord' => $sortOrder,
|
||||
'tipo' => 'ordinaria',
|
||||
'legacy_tipo' => trim((string) ($voce->v_ors ?? 'O')) ?: 'O',
|
||||
'note' => trim((string) ($voce->note ?? '')) !== '' ? trim((string) ($voce->note ?? '')) : null,
|
||||
'meta_legacy' => json_encode([
|
||||
'strict_source' => 'gescon_import',
|
||||
'legacy_year' => $year,
|
||||
'cod_tab' => $tableCode !== '' ? $tableCode : null,
|
||||
'legacy_year' => $year,
|
||||
'cod_tab' => $tableCode !== '' ? $tableCode : null,
|
||||
]),
|
||||
'updated_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
|
||||
$existing = DB::table('voci_spesa')
|
||||
->where('stabile_id', $stableId)
|
||||
->where('gestione_contabile_id', $gestioneId)
|
||||
->where('codice', $code)
|
||||
->first(['id']);
|
||||
|
||||
if ($existing) {
|
||||
DB::table('voci_spesa')->where('id', (int) $existing->id)->update($payload);
|
||||
} else {
|
||||
$payload['created_at'] = now();
|
||||
DB::table('voci_spesa')->insert($payload);
|
||||
}
|
||||
$payload['created_at'] = now();
|
||||
DB::table('voci_spesa')->insert($payload);
|
||||
|
||||
$sortOrder++;
|
||||
}
|
||||
|
|
@ -528,8 +618,8 @@ private function normalizeFloor(mixed $rawFloor): int
|
|||
|
||||
private function buildUnitCode(string $stableCode, string $scale, string $internal, string $legacyId): string
|
||||
{
|
||||
$normalizedStable = trim($stableCode);
|
||||
$normalizedScale = trim($scale) !== '' ? trim($scale) : 'A';
|
||||
$normalizedStable = trim($stableCode);
|
||||
$normalizedScale = trim($scale) !== '' ? trim($scale) : 'A';
|
||||
$normalizedInternal = strtoupper(trim(preg_replace('/\s+/', '', $internal) ?? $internal));
|
||||
|
||||
return $normalizedStable . '-' . $normalizedScale . '-' . $normalizedInternal . '-C' . $legacyId;
|
||||
|
|
@ -543,10 +633,10 @@ private function normalizeLegacyCalcolo(?string $raw): ?string
|
|||
}
|
||||
|
||||
return match ($value) {
|
||||
'm' => 'millesimi',
|
||||
'm' => 'millesimi',
|
||||
'a', 'c' => 'a_consumo',
|
||||
'x' => 'conguagli',
|
||||
'p' => 'personali',
|
||||
'x' => 'conguagli',
|
||||
'p' => 'personali',
|
||||
default => $value,
|
||||
};
|
||||
}
|
||||
|
|
@ -559,7 +649,7 @@ private function normalizeLegacyTipo(?string $raw): ?string
|
|||
}
|
||||
|
||||
return match ($value) {
|
||||
'O', 'ORD', 'ORDINARIA' => 'O',
|
||||
'O', 'ORD', 'ORDINARIA' => 'O',
|
||||
'R', 'RISC', 'RISCALDAMENTO' => 'R',
|
||||
'S', 'STRA', 'STRAORDINARIA' => 'S',
|
||||
default => $value,
|
||||
|
|
@ -569,16 +659,15 @@ private function normalizeLegacyTipo(?string $raw): ?string
|
|||
private function resolveTipoTabellaFromLegacy(?string $tipoLegacy, ?string $calcolo, ?string $codice): string
|
||||
{
|
||||
$normalizedCalcolo = strtolower(trim((string) ($calcolo ?? '')));
|
||||
$normalizedCode = strtoupper(trim((string) ($codice ?? '')));
|
||||
$normalizedCode = strtoupper(trim((string) ($codice ?? '')));
|
||||
|
||||
if ($normalizedCode === 'ACQUA' || in_array($normalizedCalcolo, ['a_consumo', 'acqua', 'consumo', 'c', 'a'], true)) {
|
||||
return 'custom';
|
||||
}
|
||||
|
||||
return match ($tipoLegacy) {
|
||||
'R' => 'riscaldamento',
|
||||
'S' => 'straordinaria',
|
||||
'R' => 'riscaldamento',
|
||||
default => 'custom',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -215,8 +215,8 @@ private function collectUniqueCustomers(Collection $rows): Collection
|
|||
}
|
||||
|
||||
foreach (['display_name', 'phone', 'phone_alt', 'email', 'indirizzo', 'cap', 'citta', 'provincia', 'partita_iva', 'codice_fiscale', 'note', 'imported_from_path'] as $field) {
|
||||
if (($customers[$identity][$field] ?? null) === null && ${Str::camel($field)} !== null) {
|
||||
$customers[$identity][$field] = ${Str::camel($field)};
|
||||
if (($customers[$identity][$field] ?? null) === null && ${Str::camel($field);} !== null) {
|
||||
$customers[$identity][$field] = ${Str::camel($field);}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Filament\Pages\Contabilita;
|
||||
|
||||
use App\Models\DatiBancari;
|
||||
use App\Models\DocumentoStabile;
|
||||
use App\Models\GestioneContabile;
|
||||
use App\Models\Stabile;
|
||||
use App\Models\User;
|
||||
|
|
@ -343,6 +344,19 @@ public function table(Table $table): Table
|
|||
->getStateUsing(fn(DatiBancari $record): ?float => $this->resolveSaldoConto($record))
|
||||
->formatStateUsing(fn($state) => $state !== null ? ('€ ' . number_format((float) $state, 2, ',', '.')) : '—'),
|
||||
|
||||
TextColumn::make('saldo_ufficiale')
|
||||
->label('Saldo ufficiale')
|
||||
->alignEnd()
|
||||
->getStateUsing(fn(DatiBancari $record): ?float => $this->resolveLatestSaldoSnapshotValue($record))
|
||||
->formatStateUsing(fn($state) => $state !== null ? ('€ ' . number_format((float) $state, 2, ',', '.')) : '—')
|
||||
->toggleable(),
|
||||
|
||||
TextColumn::make('ultimo_estratto')
|
||||
->label('Ultimo estratto')
|
||||
->getStateUsing(fn(DatiBancari $record): string => $this->resolveLatestSaldoSnapshotLabel($record))
|
||||
->wrap()
|
||||
->toggleable(),
|
||||
|
||||
TextColumn::make('stato_conto')
|
||||
->label('Stato')
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
|
@ -379,56 +393,18 @@ public function getTotaleDisponibilita(): ?float
|
|||
|
||||
$conti = DatiBancari::query()
|
||||
->where('stabile_id', $stabileId)
|
||||
->whereNotNull('iban')
|
||||
->where('iban', '!=', '')
|
||||
->get(['iban', 'saldo_iniziale']);
|
||||
->get();
|
||||
|
||||
if ($conti->isEmpty()) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$ibans = $conti->pluck('iban')->map(fn($v) => trim((string) $v))->filter()->values()->all();
|
||||
if ($ibans === []) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$movQ = MovimentoBanca::query()
|
||||
->where('stabile_id', $stabileId)
|
||||
->whereIn('iban', $ibans);
|
||||
|
||||
if ($from) {
|
||||
$movQ->whereDate('data', '>=', $from);
|
||||
}
|
||||
if ($to) {
|
||||
$movQ->whereDate('data', '<=', $to);
|
||||
}
|
||||
if ($gestioneIds !== []) {
|
||||
$movQ->whereIn('gestione_id', $gestioneIds);
|
||||
}
|
||||
|
||||
$sums = $movQ
|
||||
->groupBy('iban')
|
||||
->selectRaw('iban, COALESCE(SUM(importo), 0) AS saldo_movimenti')
|
||||
->pluck('saldo_movimenti', 'iban')
|
||||
->map(fn($v) => (float) $v)
|
||||
->all();
|
||||
|
||||
$total = 0.0;
|
||||
foreach ($conti as $c) {
|
||||
$iban = trim((string) ($c->iban ?? ''));
|
||||
if ($iban === '') {
|
||||
continue;
|
||||
$saldo = $this->resolveSaldoConto($c);
|
||||
if ($saldo !== null) {
|
||||
$total += $saldo;
|
||||
}
|
||||
|
||||
$saldoIniziale = is_numeric($c->saldo_iniziale) ? (float) $c->saldo_iniziale : 0.0;
|
||||
if ($from) {
|
||||
$alt = $this->resolveSaldoIniziale($stabileId, $iban, (string) $from);
|
||||
if ($alt !== null) {
|
||||
$saldoIniziale = $alt;
|
||||
}
|
||||
}
|
||||
|
||||
$total += $saldoIniziale + ($sums[$iban] ?? 0.0);
|
||||
}
|
||||
|
||||
return $total;
|
||||
|
|
@ -527,6 +503,109 @@ protected function resolveSaldoIniziale(int $stabileId, string $iban, string $fr
|
|||
return $row ? (float) $row->saldo : null;
|
||||
}
|
||||
|
||||
protected function resolveLatestSaldoSnapshot(DatiBancari $record): ?SaldoConto
|
||||
{
|
||||
static $cache = [];
|
||||
|
||||
$cacheKey = (int) $record->id;
|
||||
if (array_key_exists($cacheKey, $cache)) {
|
||||
return $cache[$cacheKey];
|
||||
}
|
||||
|
||||
if (! DB::getSchemaBuilder()->hasTable('contabilita_saldi_conti')) {
|
||||
return $cache[$cacheKey] = null;
|
||||
}
|
||||
|
||||
return $cache[$cacheKey] = SaldoConto::query()
|
||||
->with('documento')
|
||||
->where('stabile_id', (int) $record->stabile_id)
|
||||
->where('conto_id', (int) $record->id)
|
||||
->orderByDesc('data_saldo')
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
}
|
||||
|
||||
protected function resolveLatestSaldoSnapshotValue(DatiBancari $record): ?float
|
||||
{
|
||||
$snapshot = $this->resolveLatestSaldoSnapshot($record);
|
||||
|
||||
return $snapshot ? (float) $snapshot->saldo : null;
|
||||
}
|
||||
|
||||
protected function resolveLatestSaldoSnapshotLabel(DatiBancari $record): string
|
||||
{
|
||||
$snapshot = $this->resolveLatestSaldoSnapshot($record);
|
||||
if (! $snapshot) {
|
||||
return 'Nessuno estratto registrato';
|
||||
}
|
||||
|
||||
$parts = [];
|
||||
$parts[] = $snapshot->tipo_estratto
|
||||
? ($this->getTipoEstrattoOptions()[$snapshot->tipo_estratto] ?? 'Saldo registrato')
|
||||
: 'Saldo registrato';
|
||||
if ($snapshot->periodo_da || $snapshot->periodo_a) {
|
||||
$from = $snapshot->periodo_da?->format('d/m/Y');
|
||||
$to = $snapshot->periodo_a?->format('d/m/Y');
|
||||
$parts[] = trim(($from ?: '...') . ' - ' . ($to ?: '...'));
|
||||
}
|
||||
if ($snapshot->documento instanceof DocumentoStabile) {
|
||||
$parts[] = $snapshot->documento->nome_originale ?: 'Documento allegato';
|
||||
}
|
||||
|
||||
return implode(' · ', $parts);
|
||||
}
|
||||
|
||||
protected function getTipoEstrattoOptions(): array
|
||||
{
|
||||
return [
|
||||
'estratto_conto' => 'Estratto conto',
|
||||
'saldo_banca' => 'Saldo banca',
|
||||
'riconciliazione' => 'Riconciliazione',
|
||||
'altro' => 'Altro',
|
||||
];
|
||||
}
|
||||
|
||||
protected function getContiOptions(): array
|
||||
{
|
||||
$user = Auth::user();
|
||||
if (! $user instanceof User) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$stabileId = StabileContext::resolveActiveStabileId($user);
|
||||
if (! $stabileId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return DatiBancari::query()
|
||||
->where('stabile_id', $stabileId)
|
||||
->orderBy('denominazione_banca')
|
||||
->orderBy('legacy_cod_cassa')
|
||||
->orderBy('numero_conto')
|
||||
->orderBy('id')
|
||||
->get(['id', 'denominazione_banca', 'iban', 'legacy_cod_cassa', 'numero_conto'])
|
||||
->mapWithKeys(function (DatiBancari $conto): array {
|
||||
$parts = [];
|
||||
$parts[] = trim((string) ($conto->denominazione_banca ?: 'Conto'));
|
||||
|
||||
$legacyCodCassa = is_string($conto->legacy_cod_cassa) ? trim((string) $conto->legacy_cod_cassa) : '';
|
||||
$numeroConto = is_string($conto->numero_conto) ? trim((string) $conto->numero_conto) : '';
|
||||
$iban = is_string($conto->iban) ? trim((string) $conto->iban) : '';
|
||||
|
||||
if ($legacyCodCassa !== '') {
|
||||
$parts[] = 'cassa ' . strtoupper($legacyCodCassa);
|
||||
}
|
||||
if ($iban !== '') {
|
||||
$parts[] = $iban;
|
||||
} elseif ($numeroConto !== '') {
|
||||
$parts[] = 'n. ' . $numeroConto;
|
||||
}
|
||||
|
||||
return [(int) $conto->id => implode(' · ', $parts)];
|
||||
})
|
||||
->all();
|
||||
}
|
||||
|
||||
/** @return array<string,string> */
|
||||
protected function getIbanOptions(): array
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@
|
|||
namespace App\Filament\Pages\Contabilita;
|
||||
|
||||
use App\Models\DatiBancari;
|
||||
use App\Models\DocumentoStabile;
|
||||
use App\Models\User;
|
||||
use App\Modules\Contabilita\Models\SaldoConto;
|
||||
use App\Support\StabileContext;
|
||||
use BackedEnum;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
|
|
@ -117,6 +119,24 @@ public function table(Table $table): Table
|
|||
->alignEnd()
|
||||
->formatStateUsing(fn($state) => '€ ' . number_format((float) $state, 2, ',', '.')),
|
||||
|
||||
TextColumn::make('tipo_estratto')
|
||||
->label('Tipo')
|
||||
->getStateUsing(fn(SaldoConto $record): string => $this->formatTipoEstrattoLabel($record->tipo_estratto))
|
||||
->toggleable(),
|
||||
|
||||
TextColumn::make('periodo_estratto')
|
||||
->label('Periodo estratto')
|
||||
->getStateUsing(fn(SaldoConto $record): string => $this->formatPeriodoEstrattoLabel($record->periodo_da, $record->periodo_a))
|
||||
->wrap()
|
||||
->toggleable(),
|
||||
|
||||
TextColumn::make('documento')
|
||||
->label('Documento')
|
||||
->getStateUsing(fn(SaldoConto $record): string => $record->documento?->nome_originale ?: '—')
|
||||
->url(fn(SaldoConto $record): ?string => $record->documento?->url_view)
|
||||
->openUrlInNewTab()
|
||||
->toggleable(),
|
||||
|
||||
TextColumn::make('note')
|
||||
->label('Note')
|
||||
->wrap()
|
||||
|
|
@ -140,7 +160,30 @@ public function table(Table $table): Table
|
|||
->required(),
|
||||
|
||||
DatePicker::make('data_saldo')->label('Data')->required(),
|
||||
DatePicker::make('periodo_da')->label('Periodo dal'),
|
||||
DatePicker::make('periodo_a')->label('Periodo al'),
|
||||
TextInput::make('saldo')->label('Saldo')->numeric()->required(),
|
||||
Select::make('tipo_estratto')
|
||||
->label('Tipo riferimento')
|
||||
->native(false)
|
||||
->options($this->getTipoEstrattoOptions())
|
||||
->default('estratto_conto'),
|
||||
FileUpload::make('documento')
|
||||
->label('Allega estratto')
|
||||
->directory('tmp/banca')
|
||||
->disk('local')
|
||||
->acceptedFileTypes([
|
||||
'application/pdf',
|
||||
'text/plain',
|
||||
'text/csv',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'.pdf',
|
||||
'.csv',
|
||||
'.txt',
|
||||
'.qif',
|
||||
'.xlsx',
|
||||
]),
|
||||
Textarea::make('note')->label('Note (opzionale)')->rows(2)->maxLength(255),
|
||||
])
|
||||
->action(function (array $data): void {
|
||||
|
|
@ -161,18 +204,26 @@ public function table(Table $table): Table
|
|||
$conto = DatiBancari::query()
|
||||
->where('stabile_id', $stabileId)
|
||||
->where('id', $contoId)
|
||||
->first(['id', 'iban']);
|
||||
->first(['id', 'denominazione_banca', 'iban', 'numero_conto', 'legacy_cod_cassa']);
|
||||
|
||||
$iban = $conto && is_string($conto->iban) ? trim((string) $conto->iban) : null;
|
||||
$iban = is_string($iban) && $iban !== '' ? $iban : null;
|
||||
|
||||
$documento = $conto
|
||||
? $this->storeSaldoDocumento($stabileId, $conto, $data['documento'] ?? null, $user, $data)
|
||||
: null;
|
||||
|
||||
SaldoConto::query()->create([
|
||||
'stabile_id' => (int) $stabileId,
|
||||
'conto_id' => $contoId,
|
||||
'iban' => $iban,
|
||||
'data_saldo' => $data['data_saldo'] ?? null,
|
||||
'periodo_da' => $data['periodo_da'] ?? null,
|
||||
'periodo_a' => $data['periodo_a'] ?? null,
|
||||
'saldo' => isset($data['saldo']) ? (float) $data['saldo'] : 0.0,
|
||||
'tipo_estratto' => $this->normalizeTipoEstrattoValue($data['tipo_estratto'] ?? null),
|
||||
'note' => isset($data['note']) && is_string($data['note']) ? trim((string) $data['note']) : null,
|
||||
'documento_stabile_id' => $documento?->id,
|
||||
'created_by' => (int) ($user->id ?? 0) ?: null,
|
||||
'updated_by' => (int) ($user->id ?? 0) ?: null,
|
||||
]);
|
||||
|
|
@ -184,20 +235,53 @@ public function table(Table $table): Table
|
|||
->icon('heroicon-o-pencil-square')
|
||||
->form([
|
||||
DatePicker::make('data_saldo')->label('Data')->required(),
|
||||
DatePicker::make('periodo_da')->label('Periodo dal'),
|
||||
DatePicker::make('periodo_a')->label('Periodo al'),
|
||||
TextInput::make('saldo')->label('Saldo')->numeric()->required(),
|
||||
Select::make('tipo_estratto')
|
||||
->label('Tipo riferimento')
|
||||
->native(false)
|
||||
->options($this->getTipoEstrattoOptions()),
|
||||
FileUpload::make('documento')
|
||||
->label('Sostituisci / aggiungi allegato')
|
||||
->directory('tmp/banca')
|
||||
->disk('local')
|
||||
->acceptedFileTypes([
|
||||
'application/pdf',
|
||||
'text/plain',
|
||||
'text/csv',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'.pdf',
|
||||
'.csv',
|
||||
'.txt',
|
||||
'.qif',
|
||||
'.xlsx',
|
||||
]),
|
||||
Textarea::make('note')->label('Note (opzionale)')->rows(2)->maxLength(255),
|
||||
])
|
||||
->fillForm(fn(SaldoConto $record): array => [
|
||||
'data_saldo' => $record->data_saldo,
|
||||
'periodo_da' => $record->periodo_da,
|
||||
'periodo_a' => $record->periodo_a,
|
||||
'saldo' => (float) $record->saldo,
|
||||
'tipo_estratto' => $record->tipo_estratto,
|
||||
'note' => $record->note,
|
||||
])
|
||||
->action(function (SaldoConto $record, array $data): void {
|
||||
$user = Auth::user();
|
||||
$documento = $record->conto
|
||||
? $this->storeSaldoDocumento((int) $record->stabile_id, $record->conto, $data['documento'] ?? null, $user instanceof User ? $user : null, $data)
|
||||
: null;
|
||||
|
||||
$record->fill([
|
||||
'data_saldo' => $data['data_saldo'] ?? $record->data_saldo,
|
||||
'periodo_da' => $data['periodo_da'] ?? null,
|
||||
'periodo_a' => $data['periodo_a'] ?? null,
|
||||
'saldo' => isset($data['saldo']) ? (float) $data['saldo'] : (float) $record->saldo,
|
||||
'tipo_estratto' => $this->normalizeTipoEstrattoValue($data['tipo_estratto'] ?? null),
|
||||
'note' => isset($data['note']) && is_string($data['note']) ? trim((string) $data['note']) : null,
|
||||
'documento_stabile_id' => $documento?->id ?: $record->documento_stabile_id,
|
||||
'updated_by' => $user instanceof User ? ((int) $user->id ?: null) : null,
|
||||
]);
|
||||
$record->save();
|
||||
|
|
@ -243,4 +327,84 @@ protected function getContiOptions(): array
|
|||
})
|
||||
->all();
|
||||
}
|
||||
|
||||
protected function getTipoEstrattoOptions(): array
|
||||
{
|
||||
return [
|
||||
'estratto_conto' => 'Estratto conto',
|
||||
'saldo_banca' => 'Saldo banca',
|
||||
'riconciliazione' => 'Riconciliazione',
|
||||
'altro' => 'Altro',
|
||||
];
|
||||
}
|
||||
|
||||
protected function normalizeTipoEstrattoValue(mixed $value): ?string
|
||||
{
|
||||
$value = is_string($value) ? trim((string) $value) : '';
|
||||
if ($value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array_key_exists($value, $this->getTipoEstrattoOptions()) ? $value : 'altro';
|
||||
}
|
||||
|
||||
protected function formatTipoEstrattoLabel(?string $value): string
|
||||
{
|
||||
$normalized = $this->normalizeTipoEstrattoValue($value);
|
||||
if (! $normalized) {
|
||||
return 'Saldo registrato';
|
||||
}
|
||||
|
||||
return $this->getTipoEstrattoOptions()[$normalized] ?? 'Saldo registrato';
|
||||
}
|
||||
|
||||
protected function formatPeriodoEstrattoLabel(mixed $from, mixed $to): string
|
||||
{
|
||||
$fromDate = $from ? \Illuminate\Support\Carbon::parse((string) $from) : null;
|
||||
$toDate = $to ? \Illuminate\Support\Carbon::parse((string) $to) : null;
|
||||
|
||||
if ($fromDate && $toDate) {
|
||||
return $fromDate->format('d/m/Y') . ' - ' . $toDate->format('d/m/Y');
|
||||
}
|
||||
if ($toDate) {
|
||||
return 'Fino al ' . $toDate->format('d/m/Y');
|
||||
}
|
||||
if ($fromDate) {
|
||||
return 'Dal ' . $fromDate->format('d/m/Y');
|
||||
}
|
||||
|
||||
return 'Periodo non indicato';
|
||||
}
|
||||
|
||||
protected function storeSaldoDocumento(int $stabileId, DatiBancari $conto, mixed $tempPath, ?User $user, array $data): ?DocumentoStabile
|
||||
{
|
||||
$path = is_string($tempPath) ? trim((string) $tempPath) : '';
|
||||
if ($path === '' || ! $user instanceof User) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
||||
$extension = $extension !== '' ? $extension : 'bin';
|
||||
$filename = 'estratto-conto-' . $stabileId . '-' . (int) $conto->id . '-' . now()->format('YmdHis') . '.' . $extension;
|
||||
$publicPath = 'documenti/stabili/' . $stabileId . '/bancari/' . $filename;
|
||||
|
||||
\Illuminate\Support\Facades\Storage::disk('public')->put($publicPath, \Illuminate\Support\Facades\Storage::disk('local')->get($path));
|
||||
try {
|
||||
\Illuminate\Support\Facades\Storage::disk('local')->delete($path);
|
||||
} catch (\Throwable) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return DocumentoStabile::query()->create([
|
||||
'stabile_id' => $stabileId,
|
||||
'nome_file' => $filename,
|
||||
'nome_originale' => $filename,
|
||||
'percorso_file' => $publicPath,
|
||||
'categoria' => 'bancari',
|
||||
'tipo_mime' => \Illuminate\Support\Facades\Storage::disk('public')->mimeType($publicPath),
|
||||
'dimensione' => \Illuminate\Support\Facades\Storage::disk('public')->size($publicPath),
|
||||
'descrizione' => 'Estratto conto ' . trim((string) ($conto->denominazione_banca ?: 'Conto')) . ' · ' . $this->formatPeriodoEstrattoLabel($data['periodo_da'] ?? null, $data['periodo_a'] ?? null),
|
||||
'caricato_da' => (int) $user->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,14 @@
|
|||
namespace App\Filament\Pages\Contabilita;
|
||||
|
||||
use App\Models\DatiBancari;
|
||||
use App\Models\DocumentoStabile;
|
||||
use App\Models\FatturaElettronica;
|
||||
use App\Models\GestioneContabile;
|
||||
use App\Models\PersonaUnitaRelazione;
|
||||
use App\Models\RegistroRitenuteAcconto;
|
||||
use App\Models\Stabile;
|
||||
use App\Models\UnitaImmobiliare;
|
||||
use App\Models\UnitaImmobiliareNominativo;
|
||||
use App\Models\User;
|
||||
use App\Modules\Contabilita\Models\MovimentoBanca;
|
||||
use App\Modules\Contabilita\Models\SaldoConto;
|
||||
|
|
@ -16,12 +20,17 @@
|
|||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Attributes\Url;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
use Livewire\WithFileUploads;
|
||||
use UnitEnum;
|
||||
use BackedEnum;
|
||||
|
||||
class SituazioneIniziale extends Page
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
protected static ?string $navigationLabel = 'Situazione iniziale';
|
||||
|
||||
protected static ?string $title = 'Situazione iniziale';
|
||||
|
|
@ -116,6 +125,25 @@ class SituazioneIniziale extends Page
|
|||
|
||||
public array $detailRows = [];
|
||||
|
||||
public ?string $saldoContoId = null;
|
||||
|
||||
public ?string $saldoSnapshotData = null;
|
||||
|
||||
public ?string $saldoSnapshotPeriodoDa = null;
|
||||
|
||||
public ?string $saldoSnapshotPeriodoA = null;
|
||||
|
||||
public ?string $saldoSnapshotSaldo = null;
|
||||
|
||||
public string $saldoSnapshotTipo = 'estratto_conto';
|
||||
|
||||
public ?string $saldoSnapshotNote = null;
|
||||
|
||||
public TemporaryUploadedFile|string|null $saldoSnapshotPdf = null;
|
||||
|
||||
/** @var array<int, array<string, mixed>> */
|
||||
public array $saldoSnapshots = [];
|
||||
|
||||
/** @var array<string, string> */
|
||||
public array $vocSpeOptions = [];
|
||||
// Editing "a riga" (riduce la pagina: si modifica una riga alla volta)
|
||||
|
|
@ -239,7 +267,7 @@ public function cancelEditDettTabRow(): void
|
|||
|
||||
private function loadCondominiLabels(string $codStabileLegacy, ?string $legacyYear): array
|
||||
{
|
||||
$out = [];
|
||||
$out = $this->loadLocalCondominiLabels();
|
||||
|
||||
// Tabella "condomini" (più snella) se presente
|
||||
if (Schema::connection('gescon_import')->hasTable('condomini')) {
|
||||
|
|
@ -260,12 +288,14 @@ private function loadCondominiLabels(string $codStabileLegacy, ?string $legacyYe
|
|||
if ($label === '') {
|
||||
$label = trim((string) ($r->cod_cond ?? ''));
|
||||
}
|
||||
$out[$id] = $label !== '' ? $label : $id;
|
||||
if (! isset($out[$id]) || trim((string) $out[$id]) === '') {
|
||||
$out[$id] = $label !== '' ? $label : $id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback su "condomin" (più completa)
|
||||
if (empty($out) && Schema::connection('gescon_import')->hasTable('condomin')) {
|
||||
if (Schema::connection('gescon_import')->hasTable('condomin')) {
|
||||
$q = DB::connection('gescon_import')->table('condomin')
|
||||
->where('cod_stabile', $codStabileLegacy);
|
||||
|
||||
|
|
@ -289,13 +319,144 @@ private function loadCondominiLabels(string $codStabileLegacy, ?string $legacyYe
|
|||
if ($label === '') {
|
||||
$label = trim((string) ($r->cod_cond ?? ''));
|
||||
}
|
||||
$out[$id] = $label !== '' ? $label : $id;
|
||||
if (! isset($out[$id]) || trim((string) $out[$id]) === '') {
|
||||
$out[$id] = $label !== '' ? $label : $id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function loadLocalCondominiLabels(): array
|
||||
{
|
||||
$stabileId = $this->getActiveStabile()?->id;
|
||||
if (! $stabileId || ! Schema::hasTable('unita_immobiliari') || ! Schema::hasColumn('unita_immobiliari', 'legacy_cond_id')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$referenceDate = $this->resolveReferenceDate();
|
||||
$units = UnitaImmobiliare::query()
|
||||
->where('stabile_id', (int) $stabileId)
|
||||
->whereNotNull('legacy_cond_id')
|
||||
->get(['id', 'legacy_cond_id', 'scala', 'interno', 'denominazione']);
|
||||
|
||||
if ($units->isEmpty()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$unitIds = $units->pluck('id')->map(fn ($id) => (int) $id)->all();
|
||||
$namesByUnit = [];
|
||||
|
||||
if (Schema::hasTable('persone_unita_relazioni')) {
|
||||
$relations = PersonaUnitaRelazione::query()
|
||||
->with('persona')
|
||||
->whereIn('unita_id', $unitIds)
|
||||
->where('attivo', true)
|
||||
->where(function ($query) use ($referenceDate): void {
|
||||
$query->whereNull('data_inizio')->orWhere('data_inizio', '<=', $referenceDate->toDateString());
|
||||
})
|
||||
->where(function ($query) use ($referenceDate): void {
|
||||
$query->whereNull('data_fine')->orWhere('data_fine', '>=', $referenceDate->toDateString());
|
||||
})
|
||||
->get();
|
||||
|
||||
foreach ($relations as $relation) {
|
||||
$unitId = (int) $relation->unita_id;
|
||||
$role = strtoupper((string) ($relation->ruolo_rate ?: PersonaUnitaRelazione::deriveRuoloRate($relation->tipo_relazione)));
|
||||
if (! in_array($role, ['C', 'I'], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = trim((string) ($relation->persona?->nome_display ?? $relation->persona?->nome_completo ?? ''));
|
||||
if ($name === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$namesByUnit[$unitId][$role][$name] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
if (Schema::hasTable('unita_immobiliare_nominativi')) {
|
||||
$legacyNames = UnitaImmobiliareNominativo::query()
|
||||
->where('stabile_id', (int) $stabileId)
|
||||
->whereIn('unita_immobiliare_id', $unitIds)
|
||||
->where(function ($query) use ($referenceDate): void {
|
||||
$query->whereNull('data_inizio')->orWhere('data_inizio', '<=', $referenceDate->toDateString());
|
||||
})
|
||||
->where(function ($query) use ($referenceDate): void {
|
||||
$query->whereNull('data_fine')->orWhere('data_fine', '>=', $referenceDate->toDateString());
|
||||
})
|
||||
->get(['unita_immobiliare_id', 'ruolo', 'nominativo']);
|
||||
|
||||
foreach ($legacyNames as $row) {
|
||||
$unitId = (int) ($row->unita_immobiliare_id ?? 0);
|
||||
$role = strtoupper((string) ($row->ruolo ?? ''));
|
||||
if (! in_array($role, ['C', 'I'], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = trim((string) ($row->nominativo ?? ''));
|
||||
if ($name === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$namesByUnit[$unitId][$role][$name] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
$labels = [];
|
||||
foreach ($units as $unit) {
|
||||
$legacyCondId = trim((string) ($unit->legacy_cond_id ?? ''));
|
||||
if ($legacyCondId === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pieces = [];
|
||||
$scala = trim((string) ($unit->scala ?? ''));
|
||||
$interno = trim((string) ($unit->interno ?? ''));
|
||||
$denominazione = trim((string) ($unit->denominazione ?? ''));
|
||||
|
||||
if ($scala !== '' || $interno !== '') {
|
||||
$unitLabel = trim(($scala !== '' ? ('Scala ' . $scala) : '') . ($interno !== '' ? (' Int. ' . $interno) : ''));
|
||||
if ($unitLabel !== '') {
|
||||
$pieces[] = $unitLabel;
|
||||
}
|
||||
} elseif ($denominazione !== '') {
|
||||
$pieces[] = $denominazione;
|
||||
}
|
||||
|
||||
$owners = array_values($namesByUnit[(int) $unit->id]['C'] ?? []);
|
||||
$tenants = array_values($namesByUnit[(int) $unit->id]['I'] ?? []);
|
||||
|
||||
if ($owners !== []) {
|
||||
$pieces[] = 'Prop: ' . implode(', ', $owners);
|
||||
}
|
||||
if ($tenants !== []) {
|
||||
$pieces[] = 'Inq: ' . implode(', ', $tenants);
|
||||
}
|
||||
if ($pieces === [] && $denominazione !== '') {
|
||||
$pieces[] = $denominazione;
|
||||
}
|
||||
|
||||
$labels[$legacyCondId] = $pieces !== [] ? implode(' · ', $pieces) : $legacyCondId;
|
||||
}
|
||||
|
||||
return $labels;
|
||||
}
|
||||
|
||||
private function resolveReferenceDate(): Carbon
|
||||
{
|
||||
try {
|
||||
return $this->dataBilancio ? Carbon::parse($this->dataBilancio)->startOfDay() : now()->startOfDay();
|
||||
} catch (\Throwable) {
|
||||
return now()->startOfDay();
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedLegacyYear(): void
|
||||
{
|
||||
$this->legacyYear = is_string($this->legacyYear) ? trim((string) $this->legacyYear) : null;
|
||||
|
|
@ -317,6 +478,13 @@ public function updatedSaldoBancaManuale(): void
|
|||
$this->reloadData();
|
||||
}
|
||||
|
||||
public function updatedSaldoContoId(): void
|
||||
{
|
||||
$this->saldoContoId = is_string($this->saldoContoId) ? trim((string) $this->saldoContoId) : null;
|
||||
$this->saldoContoId = $this->saldoContoId !== '' ? $this->saldoContoId : null;
|
||||
$this->loadSaldoSnapshots();
|
||||
}
|
||||
|
||||
public function updatedDett(): void
|
||||
{
|
||||
$this->dett = is_string($this->dett) ? trim((string) $this->dett) : null;
|
||||
|
|
@ -359,6 +527,13 @@ private function reloadData(): void
|
|||
$stabileId = $stabile?->id ? (int) $stabile->id : null;
|
||||
$this->conti = $stabileId ? $this->loadContiOptions($stabileId) : [];
|
||||
|
||||
if (($this->saldoContoId === null || $this->saldoContoId === '') && $this->conti !== []) {
|
||||
$this->saldoContoId = (string) array_key_first($this->conti);
|
||||
}
|
||||
if ($this->saldoContoId !== null && $this->saldoContoId !== '' && ! array_key_exists($this->saldoContoId, $this->conti)) {
|
||||
$this->saldoContoId = $this->conti !== [] ? (string) array_key_first($this->conti) : null;
|
||||
}
|
||||
|
||||
if (! $this->dataBilancio) {
|
||||
$this->dataBilancio = $this->defaultDataBilancio();
|
||||
}
|
||||
|
|
@ -370,6 +545,7 @@ private function reloadData(): void
|
|||
$this->conguagliExcelCols = [];
|
||||
$this->detailMeta = [];
|
||||
$this->detailRows = [];
|
||||
$this->saldoSnapshots = [];
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -379,7 +555,8 @@ private function reloadData(): void
|
|||
$this->vocSpeOptions = $this->loadVocSpeOptions($codStabileLegacy, $this->legacyYear);
|
||||
|
||||
if ($this->tab === 'conguagli') {
|
||||
[$this->conguagliExcelCols, $this->conguagliExcel] = $this->computeConguagliExcel($codStabileLegacy, $this->legacyYear);
|
||||
$this->conguagliExcel = [];
|
||||
$this->conguagliExcelCols = [];
|
||||
[$this->dettTabRows, $this->dettTabEdit] = $this->loadDettTabCrudRows($codStabileLegacy, $this->legacyYear);
|
||||
} else {
|
||||
$this->conguagliExcel = [];
|
||||
|
|
@ -405,6 +582,219 @@ private function reloadData(): void
|
|||
}
|
||||
|
||||
$this->reloadDettaglio($codStabileLegacy, $this->legacyYear);
|
||||
$this->loadSaldoSnapshots();
|
||||
}
|
||||
|
||||
public function saveSaldoSnapshot(): void
|
||||
{
|
||||
$stabile = $this->getActiveStabile();
|
||||
$stabileId = $stabile?->id ? (int) $stabile->id : null;
|
||||
if (! $stabileId) {
|
||||
$this->addError('saldoContoId', 'Seleziona prima uno stabile attivo.');
|
||||
return;
|
||||
}
|
||||
|
||||
$validated = $this->validate([
|
||||
'saldoContoId' => ['required', 'string'],
|
||||
'saldoSnapshotData' => ['required', 'date'],
|
||||
'saldoSnapshotPeriodoDa' => ['nullable', 'date'],
|
||||
'saldoSnapshotPeriodoA' => ['nullable', 'date'],
|
||||
'saldoSnapshotSaldo' => ['required', 'string'],
|
||||
'saldoSnapshotTipo' => ['required', 'string', 'in:estratto_conto,saldo_banca,riconciliazione,altro'],
|
||||
'saldoSnapshotNote' => ['nullable', 'string', 'max:255'],
|
||||
'saldoSnapshotPdf' => ['nullable', 'file', 'mimes:pdf', 'max:20480'],
|
||||
]);
|
||||
|
||||
$contoId = (int) ($validated['saldoContoId'] ?? 0);
|
||||
$conto = DatiBancari::query()
|
||||
->where('stabile_id', $stabileId)
|
||||
->where('id', $contoId)
|
||||
->first(['id', 'denominazione_banca', 'iban', 'numero_conto', 'legacy_cod_cassa']);
|
||||
|
||||
if (! $conto) {
|
||||
$this->addError('saldoContoId', 'Conto non valido per lo stabile attivo.');
|
||||
return;
|
||||
}
|
||||
|
||||
$saldo = $this->parseMoney((string) ($validated['saldoSnapshotSaldo'] ?? ''));
|
||||
if ($saldo === null) {
|
||||
$this->addError('saldoSnapshotSaldo', 'Saldo non valido.');
|
||||
return;
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
$documento = $this->storeSaldoSnapshotDocumento($stabileId, $conto, $this->saldoSnapshotPdf, $validated);
|
||||
|
||||
SaldoConto::query()->updateOrCreate(
|
||||
[
|
||||
'stabile_id' => $stabileId,
|
||||
'conto_id' => $contoId,
|
||||
'data_saldo' => $validated['saldoSnapshotData'],
|
||||
],
|
||||
[
|
||||
'iban' => $this->normalizeIbanValue($conto->iban),
|
||||
'periodo_da' => $validated['saldoSnapshotPeriodoDa'] ?? null,
|
||||
'periodo_a' => $validated['saldoSnapshotPeriodoA'] ?? null,
|
||||
'saldo' => $saldo,
|
||||
'tipo_estratto' => $validated['saldoSnapshotTipo'],
|
||||
'note' => isset($validated['saldoSnapshotNote']) ? trim((string) $validated['saldoSnapshotNote']) : null,
|
||||
'documento_stabile_id' => $documento?->id,
|
||||
'created_by' => (int) ($user?->id ?? 0) ?: null,
|
||||
'updated_by' => (int) ($user?->id ?? 0) ?: null,
|
||||
]
|
||||
);
|
||||
|
||||
$this->resetSaldoSnapshotForm(false);
|
||||
$this->reloadData();
|
||||
}
|
||||
|
||||
public function deleteSaldoSnapshot(int $snapshotId): void
|
||||
{
|
||||
if ($snapshotId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$stabile = $this->getActiveStabile();
|
||||
$stabileId = $stabile?->id ? (int) $stabile->id : null;
|
||||
if (! $stabileId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$snapshot = SaldoConto::query()
|
||||
->where('stabile_id', $stabileId)
|
||||
->where('id', $snapshotId)
|
||||
->first();
|
||||
|
||||
if (! $snapshot) {
|
||||
return;
|
||||
}
|
||||
|
||||
$documentoId = (int) ($snapshot->documento_stabile_id ?? 0);
|
||||
$snapshot->delete();
|
||||
|
||||
if ($documentoId > 0) {
|
||||
DocumentoStabile::query()->where('id', $documentoId)->delete();
|
||||
}
|
||||
|
||||
$this->reloadData();
|
||||
}
|
||||
|
||||
private function loadSaldoSnapshots(): void
|
||||
{
|
||||
$this->saldoSnapshots = [];
|
||||
|
||||
$stabile = $this->getActiveStabile();
|
||||
$stabileId = $stabile?->id ? (int) $stabile->id : null;
|
||||
$contoId = is_numeric($this->saldoContoId) ? (int) $this->saldoContoId : 0;
|
||||
if (! $stabileId || $contoId <= 0 || ! DB::getSchemaBuilder()->hasTable('contabilita_saldi_conti')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->saldoSnapshots = SaldoConto::query()
|
||||
->with('documento')
|
||||
->where('stabile_id', $stabileId)
|
||||
->where('conto_id', $contoId)
|
||||
->orderByDesc('data_saldo')
|
||||
->orderByDesc('id')
|
||||
->get()
|
||||
->map(function (SaldoConto $row): array {
|
||||
return [
|
||||
'id' => (int) $row->id,
|
||||
'data_saldo' => $row->data_saldo?->format('d/m/Y'),
|
||||
'periodo' => $this->formatPeriodoEstrattoLabel($row->periodo_da, $row->periodo_a),
|
||||
'tipo' => $this->formatTipoEstrattoLabel($row->tipo_estratto),
|
||||
'saldo' => (float) $row->saldo,
|
||||
'note' => (string) ($row->note ?? ''),
|
||||
'documento_nome' => $row->documento?->nome_originale,
|
||||
'documento_url' => $row->documento?->url_view,
|
||||
];
|
||||
})
|
||||
->all();
|
||||
}
|
||||
|
||||
private function resetSaldoSnapshotForm(bool $resetConto = true): void
|
||||
{
|
||||
if ($resetConto) {
|
||||
$this->saldoContoId = $this->conti !== [] ? (string) array_key_first($this->conti) : null;
|
||||
}
|
||||
|
||||
$this->saldoSnapshotData = $this->dataBilancio;
|
||||
$this->saldoSnapshotPeriodoDa = null;
|
||||
$this->saldoSnapshotPeriodoA = null;
|
||||
$this->saldoSnapshotSaldo = null;
|
||||
$this->saldoSnapshotTipo = 'estratto_conto';
|
||||
$this->saldoSnapshotNote = null;
|
||||
$this->saldoSnapshotPdf = null;
|
||||
$this->resetValidation([
|
||||
'saldoContoId',
|
||||
'saldoSnapshotData',
|
||||
'saldoSnapshotPeriodoDa',
|
||||
'saldoSnapshotPeriodoA',
|
||||
'saldoSnapshotSaldo',
|
||||
'saldoSnapshotTipo',
|
||||
'saldoSnapshotNote',
|
||||
'saldoSnapshotPdf',
|
||||
]);
|
||||
}
|
||||
|
||||
private function formatTipoEstrattoLabel(?string $value): string
|
||||
{
|
||||
return match (trim((string) $value)) {
|
||||
'estratto_conto' => 'Estratto conto',
|
||||
'saldo_banca' => 'Saldo banca',
|
||||
'riconciliazione' => 'Riconciliazione',
|
||||
'altro' => 'Altro',
|
||||
default => 'Saldo registrato',
|
||||
};
|
||||
}
|
||||
|
||||
private function formatPeriodoEstrattoLabel(mixed $from, mixed $to): string
|
||||
{
|
||||
$fromDate = $from instanceof Carbon ? $from : ($from ? Carbon::parse((string) $from) : null);
|
||||
$toDate = $to instanceof Carbon ? $to : ($to ? Carbon::parse((string) $to) : null);
|
||||
|
||||
if ($fromDate && $toDate) {
|
||||
return $fromDate->format('d/m/Y') . ' - ' . $toDate->format('d/m/Y');
|
||||
}
|
||||
if ($toDate) {
|
||||
return 'Fino al ' . $toDate->format('d/m/Y');
|
||||
}
|
||||
if ($fromDate) {
|
||||
return 'Dal ' . $fromDate->format('d/m/Y');
|
||||
}
|
||||
|
||||
return 'Periodo non indicato';
|
||||
}
|
||||
|
||||
private function normalizeIbanValue(mixed $value): ?string
|
||||
{
|
||||
$iban = is_string($value) ? trim((string) $value) : '';
|
||||
return $iban !== '' ? $iban : null;
|
||||
}
|
||||
|
||||
private function storeSaldoSnapshotDocumento(int $stabileId, DatiBancari $conto, TemporaryUploadedFile|string|null $file, array $validated): ?DocumentoStabile
|
||||
{
|
||||
if (! $file instanceof TemporaryUploadedFile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$originalName = trim((string) $file->getClientOriginalName());
|
||||
$extension = strtolower((string) $file->getClientOriginalExtension());
|
||||
$extension = $extension !== '' ? $extension : 'pdf';
|
||||
$storedName = 'estratto-conto-' . $stabileId . '-' . (int) $conto->id . '-' . now()->format('YmdHis') . '.' . $extension;
|
||||
$storedPath = $file->storeAs('documenti/stabili/' . $stabileId . '/bancari', $storedName, 'public');
|
||||
|
||||
return DocumentoStabile::query()->create([
|
||||
'stabile_id' => $stabileId,
|
||||
'nome_file' => $storedName,
|
||||
'nome_originale' => $originalName !== '' ? $originalName : $storedName,
|
||||
'percorso_file' => $storedPath,
|
||||
'categoria' => 'bancari',
|
||||
'tipo_mime' => Storage::disk('public')->mimeType($storedPath),
|
||||
'dimensione' => Storage::disk('public')->size($storedPath),
|
||||
'descrizione' => 'Saldo/estratto ' . ($this->conti[(string) $conto->id] ?? ('Conto #' . $conto->id)) . ' · ' . $this->formatPeriodoEstrattoLabel($validated['saldoSnapshotPeriodoDa'] ?? null, $validated['saldoSnapshotPeriodoA'] ?? null),
|
||||
'caricato_da' => Auth::id(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -427,14 +817,18 @@ private function loadLegacyYearLabels(string $codStabileLegacy): array
|
|||
if ($cartella === '') {
|
||||
continue;
|
||||
}
|
||||
$label = trim((string) ($r->anno_riscaldamento ?? ''));
|
||||
if ($label === '') {
|
||||
$label = trim((string) ($r->anno_ordinario ?? ''));
|
||||
$ord = trim((string) ($r->anno_ordinario ?? ''));
|
||||
$risc = trim((string) ($r->anno_riscaldamento ?? ''));
|
||||
|
||||
$parts = [];
|
||||
if ($ord !== '') {
|
||||
$parts[] = 'Ord. ' . $ord;
|
||||
}
|
||||
if ($label === '') {
|
||||
$label = $cartella;
|
||||
if ($risc !== '' && $risc !== $ord) {
|
||||
$parts[] = 'Risc. ' . $risc;
|
||||
}
|
||||
$out[$cartella] = $label;
|
||||
$parts[] = 'Dir. ' . $cartella;
|
||||
$out[$cartella] = implode(' · ', $parts);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
|
@ -884,7 +1278,10 @@ private function loadDettTabCrudRows(string $codStabileLegacy, ?string $legacyYe
|
|||
}
|
||||
|
||||
$q = DB::connection('gescon_import')->table('dett_tab')
|
||||
->where('cod_tab', 'like', 'CONG.%');
|
||||
->where('cod_tab', 'CONG.O')
|
||||
->where(function ($query) {
|
||||
$query->whereNull('n_stra')->orWhere('n_stra', '<=', 0);
|
||||
});
|
||||
|
||||
if (Schema::connection('gescon_import')->hasColumn('dett_tab', 'cod_stabile')) {
|
||||
$hasForStabile = DB::connection('gescon_import')->table('dett_tab')
|
||||
|
|
@ -1061,12 +1458,12 @@ public function saveDettTabRow(int $id): void
|
|||
DB::connection('gescon_import')->table('dett_tab')
|
||||
->where('id', $id)
|
||||
->update([
|
||||
'cod_tab' => is_string($edit['cod_tab'] ?? null) ? trim((string) $edit['cod_tab']) : null,
|
||||
'cod_tab' => 'CONG.O',
|
||||
'id_cond' => is_string($edit['id_cond'] ?? null) ? trim((string) $edit['id_cond']) : null,
|
||||
'cond_inquil' => is_string($edit['cond_inquil'] ?? null) ? trim((string) $edit['cond_inquil']) : null,
|
||||
'mm' => is_numeric($edit['mm'] ?? null) ? (float) $edit['mm'] : null,
|
||||
'cons_euro' => round((float) $cons, 2),
|
||||
'n_stra' => is_numeric($edit['n_stra'] ?? null) ? (int) $edit['n_stra'] : null,
|
||||
'n_stra' => null,
|
||||
'unico' => ! empty($edit['unico']) ? 1 : 0,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
|
@ -1097,7 +1494,7 @@ public function addDettTabRow(): void
|
|||
return;
|
||||
}
|
||||
|
||||
$codTab = is_string($this->formDettTabAdd['cod_tab'] ?? null) ? trim((string) $this->formDettTabAdd['cod_tab']) : '';
|
||||
$codTab = 'CONG.O';
|
||||
$idCond = is_string($this->formDettTabAdd['id_cond'] ?? null) ? trim((string) $this->formDettTabAdd['id_cond']) : '';
|
||||
if ($codTab === '' || $idCond === '') {
|
||||
return;
|
||||
|
|
@ -1114,7 +1511,7 @@ public function addDettTabRow(): void
|
|||
'cond_inquil' => is_string($this->formDettTabAdd['cond_inquil'] ?? null) ? trim((string) $this->formDettTabAdd['cond_inquil']) : null,
|
||||
'mm' => is_numeric($this->formDettTabAdd['mm'] ?? null) ? (float) $this->formDettTabAdd['mm'] : null,
|
||||
'cons_euro' => round((float) $cons, 2),
|
||||
'n_stra' => is_numeric($this->formDettTabAdd['n_stra'] ?? null) ? (int) $this->formDettTabAdd['n_stra'] : null,
|
||||
'n_stra' => null,
|
||||
'unico' => ! empty($this->formDettTabAdd['unico']) ? 1 : 0,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
|
|
@ -1283,11 +1680,14 @@ private function loadContiOptions(int $stabileId): array
|
|||
return DatiBancari::query()
|
||||
->where('stabile_id', $stabileId)
|
||||
->orderBy('id')
|
||||
->get(['id', 'denominazione_banca', 'iban'])
|
||||
->get(['id', 'denominazione_banca', 'iban', 'numero_conto', 'legacy_cod_cassa'])
|
||||
->mapWithKeys(function (DatiBancari $c): array {
|
||||
$iban = is_string($c->iban) ? trim((string) $c->iban) : '';
|
||||
$numeroConto = is_string($c->numero_conto) ? trim((string) ($c->numero_conto ?? '')) : '';
|
||||
$legacyCodCassa = is_string($c->legacy_cod_cassa) ? strtoupper(trim((string) ($c->legacy_cod_cassa ?? ''))) : '';
|
||||
$label = trim((string) ($c->denominazione_banca ?? ''));
|
||||
$label = $label !== '' ? ($label . ($iban !== '' ? (' · ' . $iban) : '')) : ($iban !== '' ? $iban : ('Conto #' . $c->id));
|
||||
$identifier = $iban !== '' ? $iban : ($numeroConto !== '' ? $numeroConto : ($legacyCodCassa !== '' ? ('Cassa ' . $legacyCodCassa) : ('Conto #' . $c->id)));
|
||||
$label = $label !== '' ? ($label . ' · ' . $identifier) : $identifier;
|
||||
return [(string) $c->id => $label];
|
||||
})
|
||||
->all();
|
||||
|
|
@ -1367,9 +1767,6 @@ private function computeBucketOrdinaria(string $codStabileLegacy, ?string $legac
|
|||
$debiti = $this->sumCreDebPreced($codStabileLegacy, $legacyYear, 'D', 'non_stra')
|
||||
+ $this->sumManualVoci('ordinaria', 'debiti', null, $legacyYear);
|
||||
[$congPos, $congNeg] = $this->sumConguagli($codStabileLegacy, $legacyYear, 'CONG.O', 'non_stra');
|
||||
[$mPos, $mNeg] = $this->sumManualConguagli($legacyYear, 'CONG.O', 'non_stra');
|
||||
$congPos += $mPos;
|
||||
$congNeg += $mNeg;
|
||||
|
||||
$att = round($crediti + $congPos, 2);
|
||||
$pas = round($debiti + $congNeg, 2);
|
||||
|
|
@ -1393,9 +1790,6 @@ private function computeBucketRiscaldamento(string $codStabileLegacy, ?string $l
|
|||
$crediti = 0.0;
|
||||
$debiti = 0.0;
|
||||
[$congPos, $congNeg] = $this->sumConguagli($codStabileLegacy, $legacyYear, 'CONG.R', 'non_stra');
|
||||
[$mPos, $mNeg] = $this->sumManualConguagli($legacyYear, 'CONG.R', 'non_stra');
|
||||
$congPos += $mPos;
|
||||
$congNeg += $mNeg;
|
||||
|
||||
$att = round($crediti + $congPos, 2);
|
||||
$pas = round($debiti + $congNeg, 2);
|
||||
|
|
@ -1417,9 +1811,6 @@ private function computeBucketStraordinaria(string $codStabileLegacy, ?string $l
|
|||
$debiti = $this->sumCreDebPreced($codStabileLegacy, $legacyYear, 'D', $nStra)
|
||||
+ $this->sumManualVoci('straordinaria', 'debiti', $nStra, $legacyYear);
|
||||
[$congPos, $congNeg] = $this->sumConguagli($codStabileLegacy, $legacyYear, null, $nStra);
|
||||
[$mPos, $mNeg] = $this->sumManualConguagli($legacyYear, null, $nStra);
|
||||
$congPos += $mPos;
|
||||
$congNeg += $mNeg;
|
||||
|
||||
$titolo = $this->resolveStraordinariaTitolo($codStabileLegacy, $legacyYear, $nStra);
|
||||
|
||||
|
|
@ -1830,10 +2221,13 @@ private function computeRisorseFinanziarie(int $stabileId, string $date): array
|
|||
|
||||
$conti = DatiBancari::query()
|
||||
->where('stabile_id', $stabileId)
|
||||
->whereNotNull('iban')
|
||||
->where('iban', '!=', '')
|
||||
->when(Schema::hasColumn('dati_bancari', 'stato_conto'), function ($query) {
|
||||
$query->where(function ($query) {
|
||||
$query->whereNull('stato_conto')->orWhere('stato_conto', '!=', 'chiuso');
|
||||
});
|
||||
})
|
||||
->orderBy('id')
|
||||
->get(['id', 'denominazione_banca', 'iban', 'saldo_iniziale']);
|
||||
->get(['id', 'denominazione_banca', 'iban', 'numero_conto', 'legacy_cod_cassa', 'saldo_iniziale', 'data_saldo_iniziale']);
|
||||
|
||||
if ($conti->isEmpty()) {
|
||||
return [];
|
||||
|
|
@ -1843,24 +2237,13 @@ private function computeRisorseFinanziarie(int $stabileId, string $date): array
|
|||
$out = [];
|
||||
foreach ($conti as $conto) {
|
||||
$iban = is_string($conto->iban) ? trim((string) $conto->iban) : '';
|
||||
if ($iban === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$saldoIniziale = is_numeric($conto->saldo_iniziale) ? (float) $conto->saldo_iniziale : 0.0;
|
||||
$sumImporti = (float) (MovimentoBanca::query()
|
||||
->where('stabile_id', $stabileId)
|
||||
->where('conto_id', (int) $conto->id)
|
||||
->where('iban', $iban)
|
||||
->whereDate('data', '<=', $dateC->toDateString())
|
||||
->sum('importo') ?? 0.0);
|
||||
|
||||
$offset = $this->resolveSaldoOffsetDaSaldi($stabileId, (int) $conto->id, $iban, $dateC);
|
||||
|
||||
$saldo = round($saldoIniziale + $sumImporti + $offset, 2);
|
||||
$numeroConto = is_string($conto->numero_conto) ? trim((string) ($conto->numero_conto ?? '')) : '';
|
||||
$legacyCodCassa = is_string($conto->legacy_cod_cassa) ? strtoupper(trim((string) ($conto->legacy_cod_cassa ?? ''))) : '';
|
||||
$saldo = $this->resolveContoSaldoAllaData($stabileId, $conto, $dateC);
|
||||
|
||||
$label = trim((string) ($conto->denominazione_banca ?? ''));
|
||||
$label = $label !== '' ? ($label . ' · ' . $iban) : $iban;
|
||||
$identifier = $iban !== '' ? $iban : ($numeroConto !== '' ? $numeroConto : ($legacyCodCassa !== '' ? ('Cassa ' . $legacyCodCassa) : ('Conto #' . $conto->id)));
|
||||
$label = $label !== '' ? ($label . ' · ' . $identifier) : $identifier;
|
||||
|
||||
$out[] = [
|
||||
'conto_id' => (int) $conto->id,
|
||||
|
|
@ -1873,6 +2256,66 @@ private function computeRisorseFinanziarie(int $stabileId, string $date): array
|
|||
return $out;
|
||||
}
|
||||
|
||||
private function resolveContoSaldoAllaData(int $stabileId, DatiBancari $conto, Carbon $date): float
|
||||
{
|
||||
$contoId = (int) $conto->id;
|
||||
|
||||
$snapshot = SaldoConto::query()
|
||||
->where('stabile_id', $stabileId)
|
||||
->where('conto_id', $contoId)
|
||||
->whereDate('data_saldo', '<=', $date->toDateString())
|
||||
->orderByDesc('data_saldo')
|
||||
->orderByDesc('id')
|
||||
->first(['data_saldo', 'saldo']);
|
||||
|
||||
if ($snapshot) {
|
||||
$baseDate = $snapshot->data_saldo instanceof Carbon
|
||||
? $snapshot->data_saldo->copy()->startOfDay()
|
||||
: Carbon::parse((string) $snapshot->data_saldo)->startOfDay();
|
||||
$delta = (float) (MovimentoBanca::query()
|
||||
->where('stabile_id', $stabileId)
|
||||
->where('conto_id', $contoId)
|
||||
->whereDate('data', '>', $baseDate->toDateString())
|
||||
->whereDate('data', '<=', $date->toDateString())
|
||||
->sum('importo') ?? 0.0);
|
||||
|
||||
return round((float) ($snapshot->saldo ?? 0.0) + $delta, 2);
|
||||
}
|
||||
|
||||
$saldoBase = is_numeric($conto->saldo_iniziale) ? (float) $conto->saldo_iniziale : 0.0;
|
||||
$dataSaldoIniziale = $conto->data_saldo_iniziale instanceof Carbon
|
||||
? $conto->data_saldo_iniziale->copy()->startOfDay()
|
||||
: ($conto->data_saldo_iniziale ? Carbon::parse((string) $conto->data_saldo_iniziale)->startOfDay() : null);
|
||||
|
||||
$movimenti = MovimentoBanca::query()
|
||||
->where('stabile_id', $stabileId)
|
||||
->where('conto_id', $contoId);
|
||||
|
||||
if ($dataSaldoIniziale instanceof Carbon) {
|
||||
if ($dataSaldoIniziale->lte($date)) {
|
||||
$delta = (float) (clone $movimenti)
|
||||
->whereDate('data', '>', $dataSaldoIniziale->toDateString())
|
||||
->whereDate('data', '<=', $date->toDateString())
|
||||
->sum('importo');
|
||||
|
||||
return round($saldoBase + $delta, 2);
|
||||
}
|
||||
|
||||
$delta = (float) (clone $movimenti)
|
||||
->whereDate('data', '>', $date->toDateString())
|
||||
->whereDate('data', '<=', $dataSaldoIniziale->toDateString())
|
||||
->sum('importo');
|
||||
|
||||
return round($saldoBase - $delta, 2);
|
||||
}
|
||||
|
||||
$delta = (float) (clone $movimenti)
|
||||
->whereDate('data', '<=', $date->toDateString())
|
||||
->sum('importo');
|
||||
|
||||
return round($saldoBase + $delta, 2);
|
||||
}
|
||||
|
||||
private function computeSaldoBancaTotaleLetto(array $risorseFinanziarie): ?float
|
||||
{
|
||||
if ($risorseFinanziarie === []) {
|
||||
|
|
@ -2082,6 +2525,8 @@ private function loadDettTabRows(string $codStabileLegacy, ?string $legacyYear,
|
|||
return [];
|
||||
}
|
||||
|
||||
$labels = $this->loadCondominiLabels($codStabileLegacy, $legacyYear);
|
||||
|
||||
$q = DB::connection('gescon_import')->table('dett_tab')
|
||||
->where('cod_tab', 'like', 'CONG.%');
|
||||
|
||||
|
|
@ -2127,6 +2572,7 @@ private function loadDettTabRows(string $codStabileLegacy, ?string $legacyYear,
|
|||
->map(fn ($r) => [
|
||||
'cod_tab' => (string) ($r->cod_tab ?? ''),
|
||||
'id_cond' => (string) ($r->id_cond ?? ''),
|
||||
'cond_label' => (string) ($labels[trim((string) ($r->id_cond ?? ''))] ?? ''),
|
||||
'cond_inquil' => (string) ($r->cond_inquil ?? ''),
|
||||
'mm' => $r->mm !== null ? (float) $r->mm : null,
|
||||
'cons_euro' => (float) ($r->cons_euro ?? 0),
|
||||
|
|
|
|||
|
|
@ -85,13 +85,6 @@ public function mount(): void
|
|||
$this->tipoGestioneTab = in_array($tab, ['ordinaria', 'acqua', 'riscaldamento', 'straordinaria'], true) ? $tab : 'ordinaria';
|
||||
|
||||
$this->gestioneContabileId = $this->resolveGestioneContabileId();
|
||||
if (! $this->gestioneContabileId) {
|
||||
$opts = $this->getGestioniOptions();
|
||||
$firstKey = array_key_first($opts);
|
||||
if (is_numeric($firstKey)) {
|
||||
$this->gestioneContabileId = (int) $firstKey;
|
||||
}
|
||||
}
|
||||
|
||||
$preset = (string) request()->query('preset', 'manuale');
|
||||
$this->presetRipartizione = in_array($preset, ['manuale', 'confedilizia'], true) ? $preset : 'manuale';
|
||||
|
|
@ -202,9 +195,7 @@ private function mapTabellaToAnno(?int $sourceTabellaId, int $stabileId, int $ta
|
|||
}
|
||||
|
||||
if ($hasAnno) {
|
||||
$q->where(function (Builder $qq) use ($targetAnno) {
|
||||
$qq->where('anno_gestione', $targetAnno)->orWhereNull('anno_gestione');
|
||||
})->orderByRaw('CASE WHEN anno_gestione IS NULL THEN 1 ELSE 0 END');
|
||||
$q->where('anno_gestione', $targetAnno);
|
||||
}
|
||||
|
||||
$id = $q->orderBy('ordine_visualizzazione')
|
||||
|
|
@ -464,9 +455,7 @@ public function getTabelleMillesimaliOptions(): array
|
|||
->orderBy('nome_tabella');
|
||||
|
||||
if ($hasAnno) {
|
||||
$q->where(function (Builder $qq) use ($anno) {
|
||||
$qq->where('anno_gestione', $anno)->orWhereNull('anno_gestione');
|
||||
})->orderByRaw('CASE WHEN anno_gestione IS NULL THEN 1 ELSE 0 END');
|
||||
$q->where('anno_gestione', $anno);
|
||||
}
|
||||
|
||||
if ($hasLegacyTipo) {
|
||||
|
|
@ -625,23 +614,8 @@ private function applyTipoGestioneFilter(Builder $q, string $tipoTab, bool $hasL
|
|||
{
|
||||
$tipo = $this->normalizeTipoGestione($tipoTab);
|
||||
|
||||
if ($hasLegacyTipo) {
|
||||
$legacy = $this->getLegacyTipoValues($tipo);
|
||||
$q->where(function (Builder $qq) use ($legacy, $tipo) {
|
||||
$qq->whereIn('tm.tipo', $legacy)
|
||||
->orWhereIn('voci_spesa.tipo_gestione', $legacy)
|
||||
->orWhere('voci_spesa.tipo_gestione', $tipo)
|
||||
->orWhereNull('tm.tipo');
|
||||
});
|
||||
} else {
|
||||
if ($tipo === 'ordinaria') {
|
||||
$q->where(function (Builder $qq) {
|
||||
$qq->where('voci_spesa.tipo_gestione', 'ordinaria')
|
||||
->orWhereNull('voci_spesa.tipo_gestione');
|
||||
});
|
||||
} else {
|
||||
$q->where('voci_spesa.tipo_gestione', $tipo);
|
||||
}
|
||||
if (Schema::hasColumn('voci_spesa', 'tipo_gestione')) {
|
||||
$q->where('voci_spesa.tipo_gestione', $tipo);
|
||||
}
|
||||
|
||||
if ($tipoTab === 'acqua') {
|
||||
|
|
@ -650,9 +624,7 @@ private function applyTipoGestioneFilter(Builder $q, string $tipoTab, bool $hasL
|
|||
->orWhere('voci_spesa.categoria', 'acqua');
|
||||
});
|
||||
} elseif ($tipoTab === 'ordinaria') {
|
||||
$q->where(function (Builder $qq) {
|
||||
$qq->whereNull('tm.codice_tabella')->orWhereRaw('UPPER(TRIM(tm.codice_tabella)) != ?', ['ACQUA']);
|
||||
});
|
||||
$q->whereRaw('(tm.codice_tabella IS NULL OR UPPER(TRIM(tm.codice_tabella)) != ?)', ['ACQUA']);
|
||||
$q->where(function (Builder $qq) {
|
||||
$qq->whereNull('voci_spesa.categoria')->orWhere('voci_spesa.categoria', '!=', 'acqua');
|
||||
});
|
||||
|
|
@ -747,9 +719,7 @@ private function getNordOptions(): array
|
|||
$q = TabellaMillesimale::query()->where('stabile_id', $stabileId);
|
||||
if (Schema::hasColumn('tabelle_millesimali', 'anno_gestione')) {
|
||||
$anno = AnnoGestioneContext::resolveActiveAnno($user);
|
||||
$q->where(function (Builder $qq) use ($anno) {
|
||||
$qq->where('anno_gestione', $anno)->orWhereNull('anno_gestione');
|
||||
});
|
||||
$q->where('anno_gestione', $anno);
|
||||
}
|
||||
|
||||
$values = $q->selectRaw('DISTINCT ' . $expr . ' as nord_val')
|
||||
|
|
@ -779,9 +749,7 @@ private function getFirstTabellaCodesToHide(int $stabileId): array
|
|||
$q = TabellaMillesimale::query()->where('stabile_id', $stabileId);
|
||||
|
||||
if ($hasAnno) {
|
||||
$q->where(function (Builder $qq) use ($anno) {
|
||||
$qq->where('anno_gestione', $anno)->orWhereNull('anno_gestione');
|
||||
})->orderByRaw('CASE WHEN anno_gestione IS NULL THEN 1 ELSE 0 END');
|
||||
$q->where('anno_gestione', $anno);
|
||||
}
|
||||
|
||||
if ($hasLegacyTipo) {
|
||||
|
|
@ -950,16 +918,6 @@ protected function resolveGestioneContabileId(): ?int
|
|||
->orderByDesc('id')
|
||||
->first();
|
||||
|
||||
if (! $match && $tipo === 'ordinaria') {
|
||||
$match = GestioneContabile::query()
|
||||
->where('stabile_id', $stabileId)
|
||||
->where('stato', 'aperta')
|
||||
->orderByDesc('gestione_attiva')
|
||||
->orderByDesc('anno_gestione')
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
}
|
||||
|
||||
return $match?->id ? (int) $match->id : null;
|
||||
}
|
||||
|
||||
|
|
@ -975,6 +933,10 @@ protected function getTableQuery(): Builder
|
|||
return VoceSpesa::query()->whereRaw('1 = 0');
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('voci_spesa', 'gestione_contabile_id') && ! $this->gestioneContabileId) {
|
||||
return VoceSpesa::query()->whereRaw('1 = 0');
|
||||
}
|
||||
|
||||
$tipoTab = $this->tipoGestioneTab;
|
||||
|
||||
// Requisito (legacy): tabs filtrano per `tabelle.tipo` e ordinano per `tabelle.nord`.
|
||||
|
|
@ -1028,6 +990,10 @@ public function getTotalePreventivo(): float
|
|||
return 0.0;
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('voci_spesa', 'gestione_contabile_id') && ! $this->gestioneContabileId) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$tipoTab = $this->tipoGestioneTab;
|
||||
|
||||
$hasLegacyTipo = Schema::hasColumn('tabelle_millesimali', 'tipo');
|
||||
|
|
@ -1044,6 +1010,20 @@ public function getTotalePreventivo(): float
|
|||
return (float) $q->sum('importo_default');
|
||||
}
|
||||
|
||||
public function getTotalePreventivoAcqua(): float
|
||||
{
|
||||
return $this->getTotaleByTipoTab('acqua', 'importo_default');
|
||||
}
|
||||
|
||||
public function getTotalePreventivoGenerale(): float
|
||||
{
|
||||
if (($this->tipoGestioneTab ?? 'ordinaria') === 'acqua') {
|
||||
return $this->getTotalePreventivo();
|
||||
}
|
||||
|
||||
return $this->getTotalePreventivo() + $this->getTotalePreventivoAcqua();
|
||||
}
|
||||
|
||||
public function getTotaleConsuntivo(): float
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
|
@ -1056,6 +1036,10 @@ public function getTotaleConsuntivo(): float
|
|||
return 0.0;
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('voci_spesa', 'gestione_contabile_id') && ! $this->gestioneContabileId) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('voci_spesa', 'importo_consuntivo')) {
|
||||
return 0.0;
|
||||
}
|
||||
|
|
@ -1074,6 +1058,53 @@ public function getTotaleConsuntivo(): float
|
|||
return (float) $q->sum('importo_consuntivo');
|
||||
}
|
||||
|
||||
public function getTotaleConsuntivoAcqua(): float
|
||||
{
|
||||
return $this->getTotaleByTipoTab('acqua', 'importo_consuntivo');
|
||||
}
|
||||
|
||||
public function getTotaleConsuntivoGenerale(): float
|
||||
{
|
||||
if (($this->tipoGestioneTab ?? 'ordinaria') === 'acqua') {
|
||||
return $this->getTotaleConsuntivo();
|
||||
}
|
||||
|
||||
return $this->getTotaleConsuntivo() + $this->getTotaleConsuntivoAcqua();
|
||||
}
|
||||
|
||||
private function getTotaleByTipoTab(string $tipoTab, string $column): float
|
||||
{
|
||||
$user = Auth::user();
|
||||
if (! $user instanceof User) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$activeStabileId = StabileContext::resolveActiveStabileId($user);
|
||||
if (! $activeStabileId) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('voci_spesa', 'gestione_contabile_id') && ! $this->gestioneContabileId) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('voci_spesa', $column)) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$hasLegacyTipo = Schema::hasColumn('tabelle_millesimali', 'tipo');
|
||||
|
||||
$q = VoceSpesa::query()->where('voci_spesa.stabile_id', $activeStabileId);
|
||||
if (Schema::hasColumn('voci_spesa', 'gestione_contabile_id') && $this->gestioneContabileId) {
|
||||
$q->where('gestione_contabile_id', $this->gestioneContabileId);
|
||||
}
|
||||
|
||||
$q->leftJoin('tabelle_millesimali as tm', 'tm.id', '=', 'voci_spesa.tabella_millesimale_default_id');
|
||||
$this->applyTipoGestioneFilter($q, $tipoTab, $hasLegacyTipo);
|
||||
|
||||
return (float) $q->sum($column);
|
||||
}
|
||||
|
||||
/**
|
||||
* Totali per tabella (codice_tabella) e per Prev/Cons.
|
||||
* @return array<int, array{tabella: string, totale_prev: float, totale_cons: float}>
|
||||
|
|
@ -1090,11 +1121,12 @@ public function getTotaliPerTabella(): array
|
|||
return [];
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('voci_spesa', 'gestione_contabile_id') && ! $this->gestioneContabileId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$tipoTab = $this->tipoGestioneTab;
|
||||
$hasLegacyTipo = Schema::hasColumn('tabelle_millesimali', 'tipo');
|
||||
$hasAnno = Schema::hasColumn('tabelle_millesimali', 'anno_gestione');
|
||||
$anno = AnnoGestioneContext::resolveActiveAnno($user);
|
||||
|
||||
$q = VoceSpesa::query()
|
||||
->leftJoin('tabelle_millesimali as tm', 'tm.id', '=', 'voci_spesa.tabella_millesimale_default_id')
|
||||
->where('voci_spesa.stabile_id', $activeStabileId)
|
||||
|
|
@ -1102,12 +1134,13 @@ public function getTotaliPerTabella(): array
|
|||
$qq->where('voci_spesa.gestione_contabile_id', $this->gestioneContabileId);
|
||||
});
|
||||
|
||||
$q->selectRaw('COALESCE(tm.codice_tabella, "—") as tabella, SUM(voci_spesa.importo_default) as totale_prev, SUM(COALESCE(voci_spesa.importo_consuntivo, 0)) as totale_cons');
|
||||
$q->selectRaw('COALESCE(tm.codice_tabella, "—") as tabella, COALESCE(tm.nord, tm.ordine_visualizzazione, tm.ordinamento, 9999) as tabella_nord, SUM(voci_spesa.importo_default) as totale_prev, SUM(COALESCE(voci_spesa.importo_consuntivo, 0)) as totale_cons');
|
||||
|
||||
$this->applyTipoGestioneFilter($q, $tipoTab, $hasLegacyTipo);
|
||||
|
||||
return $q
|
||||
->groupBy('tabella')
|
||||
->groupBy('tabella', 'tabella_nord')
|
||||
->orderBy('tabella_nord')
|
||||
->orderByRaw('tabella = "—"')
|
||||
->orderBy('tabella')
|
||||
->get()
|
||||
|
|
@ -1159,50 +1192,52 @@ public function table(Table $table): Table
|
|||
->label('Descrizione')
|
||||
->searchable()
|
||||
->wrap()
|
||||
->extraAttributes(['class' => 'text-[11px]']),
|
||||
->extraAttributes(['class' => 'text-[13px] leading-tight']),
|
||||
|
||||
TextColumn::make('importo_default')
|
||||
->label('Prev.')
|
||||
->formatStateUsing(fn($state) => number_format((float) ($state ?? 0), 2, ',', '.'))
|
||||
->alignRight()
|
||||
->summarize(Sum::make()->label('Subtotale')->formatStateUsing(fn($state) => number_format((float) ($state ?? 0), 2, ',', '.')))
|
||||
->extraAttributes(['class' => 'text-[11px]']),
|
||||
->extraAttributes(['class' => 'text-[13px] leading-tight']),
|
||||
|
||||
TextColumn::make('importo_consuntivo')
|
||||
->label('Cons.')
|
||||
->formatStateUsing(fn($state) => number_format((float) ($state ?? 0), 2, ',', '.'))
|
||||
->alignRight()
|
||||
->summarize(Sum::make()->label('Subtotale')->formatStateUsing(fn($state) => number_format((float) ($state ?? 0), 2, ',', '.')))
|
||||
->extraAttributes(['class' => 'text-[11px]']),
|
||||
->extraAttributes(['class' => 'text-[13px] leading-tight']),
|
||||
|
||||
TextColumn::make('percentuale_condomino')
|
||||
->label('% Prop.')
|
||||
->formatStateUsing(fn($state) => number_format((float) ($state ?? 0), 2, ',', '.'))
|
||||
->alignRight()
|
||||
->extraAttributes(['class' => 'text-[11px]']),
|
||||
->extraAttributes(['class' => 'text-[13px] leading-tight']),
|
||||
|
||||
TextColumn::make('percentuale_inquilino')
|
||||
->label('% Inq.')
|
||||
->formatStateUsing(fn($state) => number_format((float) ($state ?? 0), 2, ',', '.'))
|
||||
->alignRight()
|
||||
->extraAttributes(['class' => 'text-[11px]']),
|
||||
->extraAttributes(['class' => 'text-[13px] leading-tight']),
|
||||
|
||||
IconColumn::make('attiva')
|
||||
->label('Attiva')
|
||||
->boolean()
|
||||
->extraAttributes(['class' => 'text-[11px]']),
|
||||
->extraAttributes(['class' => 'text-[13px] leading-tight']),
|
||||
])
|
||||
->actions([
|
||||
Action::make('mastrino')
|
||||
->label('Mastrino')
|
||||
->hiddenLabel()
|
||||
->icon('heroicon-o-document-chart-bar')
|
||||
->tooltip('Mastrino')
|
||||
->url(fn (VoceSpesa $record) => VoceSpesaMastrino::getUrl([
|
||||
'record' => (int) $record->id,
|
||||
'gestione' => $this->gestioneContabileId,
|
||||
], panel: 'admin-filament')),
|
||||
Action::make('modifica')
|
||||
->label('Modifica')
|
||||
->hiddenLabel()
|
||||
->icon('heroicon-o-pencil-square')
|
||||
->tooltip('Modifica')
|
||||
->modalHeading('Modifica voce di spesa')
|
||||
->form([
|
||||
Select::make('tabella_millesimale_default_id')
|
||||
|
|
@ -1279,8 +1314,9 @@ public function table(Table $table): Table
|
|||
$this->dispatch('$refresh');
|
||||
}),
|
||||
Action::make('assegnaTabella')
|
||||
->label('Tabella')
|
||||
->hiddenLabel()
|
||||
->icon('heroicon-o-rectangle-stack')
|
||||
->tooltip('Tabella')
|
||||
->modalHeading('Imposta tabella di riferimento')
|
||||
->form([
|
||||
Select::make('tabella_millesimale_default_id')
|
||||
|
|
@ -1303,8 +1339,8 @@ public function table(Table $table): Table
|
|||
$this->dispatch('$refresh');
|
||||
}),
|
||||
])
|
||||
->paginationPageOptions([16, 32, 64, 100])
|
||||
->defaultPaginationPageOption(32)
|
||||
->paginationPageOptions([24, 48, 96, 150])
|
||||
->defaultPaginationPageOption(48)
|
||||
->bulkActions([
|
||||
BulkAction::make('bulkAssegnaTabella')
|
||||
->label('Assegna tabella')
|
||||
|
|
|
|||
|
|
@ -288,7 +288,7 @@ protected function loadData(): void
|
|||
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
* @return array{imported:int,duplicates:int,skipped:int,errors:int,messages:array<int,string>}
|
||||
* @return array{imported:int,duplicates:int,skipped:int,errors:int,messages:array<int,string>}
|
||||
*/
|
||||
private function importSupplierUploadedFeFile(
|
||||
string $path,
|
||||
|
|
@ -410,7 +410,7 @@ private function importSupplierUploadedFeFile(
|
|||
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
* @return array{status:string,message:string}
|
||||
* @return array{status:string,message:string}
|
||||
*/
|
||||
private function importSupplierParsedXml(
|
||||
string $xml,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
use App\Models\User;
|
||||
use App\Models\UserSetting;
|
||||
use App\Services\Contabilita\PreventivoOrdinarioPreviewService;
|
||||
use App\Support\AnnoGestioneContext;
|
||||
use App\Support\StabileContext;
|
||||
use BackedEnum;
|
||||
|
|
@ -64,7 +65,17 @@ public static function canAccess(): bool
|
|||
public ?string $mastrinoCodSpe = null;
|
||||
public array $mastrinoRows = [];
|
||||
|
||||
public array $acquaRipartoRows = [];
|
||||
public array $acquaRipartoRows = [];
|
||||
public array $preventivoEditorRows = [];
|
||||
public array $preventivoSummaryRows = [];
|
||||
public array $preventivoTabellaOptions = [];
|
||||
public array $ripartoPreviewColumns = [];
|
||||
public array $ripartoPreviewRows = [];
|
||||
public array $ripartoRateMonths = [];
|
||||
public array $ripartoFocusBreakdown = [];
|
||||
public array $personeCollegateAudit = [];
|
||||
public array $preventivoGestioneMeta = [];
|
||||
public ?int $ripartoFocusUnitId = null;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
|
|
@ -74,7 +85,7 @@ public function mount(): void
|
|||
}
|
||||
|
||||
$tab = request()->query('tab');
|
||||
if (in_array($tab, ['operazioni', 'consuntivo', 'straordinarie', 'acqua', 'incassi', 'fatture'], true)) {
|
||||
if (in_array($tab, ['operazioni', 'preventivo', 'riparto', 'persone', 'consuntivo', 'straordinarie', 'acqua', 'incassi', 'fatture'], true)) {
|
||||
$this->viewTab = $tab;
|
||||
}
|
||||
|
||||
|
|
@ -93,6 +104,183 @@ public function mount(): void
|
|||
$this->sortField = 'dt_spe';
|
||||
$this->sortDirection = 'asc';
|
||||
}
|
||||
|
||||
$this->hydratePreventivoWorkspace();
|
||||
}
|
||||
|
||||
public function updatedFilterAnno(): void
|
||||
{
|
||||
$this->hydratePreventivoWorkspace();
|
||||
}
|
||||
|
||||
public function updatedRipartoFocusUnitId(): void
|
||||
{
|
||||
$this->applyFocusRipartoBreakdown();
|
||||
}
|
||||
|
||||
public function savePreventivoOrdinario(): void
|
||||
{
|
||||
$activeStabileId = StabileContext::resolveActiveStabileId(Auth::user());
|
||||
$gestioneId = (int) ($this->preventivoGestioneMeta['id'] ?? 0);
|
||||
|
||||
if (! $activeStabileId || ! $gestioneId || ! Schema::hasTable('voci_spesa')) {
|
||||
Notification::make()
|
||||
->title('Preventivo non disponibile')
|
||||
->body('Manca una gestione ordinaria attiva o la struttura dati delle voci spesa.')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
DB::transaction(function (): void {
|
||||
foreach ($this->preventivoEditorRows as $row) {
|
||||
$payload = [
|
||||
'importo_default' => is_numeric($row['importo_default'] ?? null) ? round((float) $row['importo_default'], 2) : 0.0,
|
||||
'importo_consuntivo' => is_numeric($row['importo_consuntivo'] ?? null) ? round((float) $row['importo_consuntivo'], 2) : 0.0,
|
||||
'percentuale_inquilino' => is_numeric($row['percentuale_inquilino'] ?? null) ? round((float) $row['percentuale_inquilino'], 2) : 0.0,
|
||||
'percentuale_condomino' => max(0, 100 - (is_numeric($row['percentuale_inquilino'] ?? null) ? round((float) $row['percentuale_inquilino'], 2) : 0.0)),
|
||||
'tabella_millesimale_default_id' => ! empty($row['tabella_millesimale_default_id']) ? (int) $row['tabella_millesimale_default_id'] : null,
|
||||
];
|
||||
|
||||
DB::table('voci_spesa')
|
||||
->where('id', (int) ($row['id'] ?? 0))
|
||||
->where('stabile_id', (int) $activeStabileId)
|
||||
->where('gestione_contabile_id', $gestioneId)
|
||||
->update($payload);
|
||||
}
|
||||
});
|
||||
|
||||
$this->hydratePreventivoWorkspace();
|
||||
|
||||
Notification::make()
|
||||
->title('Preventivo aggiornato')
|
||||
->body('Le voci della gestione ordinaria sono state salvate e il riparto è stato ricalcolato.')
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
|
||||
protected function hydratePreventivoWorkspace(): void
|
||||
{
|
||||
$this->preventivoEditorRows = [];
|
||||
$this->preventivoSummaryRows = [];
|
||||
$this->preventivoTabellaOptions = [];
|
||||
$this->ripartoPreviewColumns = [];
|
||||
$this->ripartoPreviewRows = [];
|
||||
$this->ripartoRateMonths = [];
|
||||
$this->ripartoFocusBreakdown = [];
|
||||
$this->personeCollegateAudit = [];
|
||||
$this->preventivoGestioneMeta = [];
|
||||
|
||||
$activeStabileId = StabileContext::resolveActiveStabileId(Auth::user());
|
||||
if (! $activeStabileId || ! $this->filterAnno) {
|
||||
return;
|
||||
}
|
||||
|
||||
$service = app(PreventivoOrdinarioPreviewService::class);
|
||||
$payload = $service->build((int) $activeStabileId, (int) $this->filterAnno);
|
||||
|
||||
$this->preventivoGestioneMeta = $payload['gestione'] ?? [];
|
||||
$this->preventivoEditorRows = $payload['preventivo_rows'] ?? [];
|
||||
$this->preventivoSummaryRows = $payload['preventivo_summary'] ?? [];
|
||||
$this->preventivoTabellaOptions = $payload['tabella_options'] ?? [];
|
||||
$this->ripartoPreviewColumns = $payload['riparto_columns'] ?? [];
|
||||
$this->ripartoPreviewRows = $payload['riparto_rows'] ?? [];
|
||||
$this->ripartoRateMonths = $payload['mesi_rate'] ?? [];
|
||||
$this->personeCollegateAudit = $payload['people_audit'] ?? [];
|
||||
|
||||
$focusId = $payload['focus_unit_id'] ?? null;
|
||||
if ($this->ripartoFocusUnitId === null || ! collect($this->ripartoPreviewRows)->contains(fn(array $row): bool => (int) $row['unit_id'] === (int) $this->ripartoFocusUnitId)) {
|
||||
$this->ripartoFocusUnitId = $focusId ? (int) $focusId : null;
|
||||
}
|
||||
|
||||
$this->applyFocusRipartoBreakdown($payload['focus_breakdown'] ?? []);
|
||||
}
|
||||
|
||||
protected function applyFocusRipartoBreakdown(array $fallback = []): void
|
||||
{
|
||||
if ($this->ripartoFocusUnitId === null) {
|
||||
$this->ripartoFocusBreakdown = [];
|
||||
return;
|
||||
}
|
||||
|
||||
if ($fallback !== []) {
|
||||
$this->ripartoFocusBreakdown = $fallback;
|
||||
return;
|
||||
}
|
||||
|
||||
$row = collect($this->ripartoPreviewRows)->firstWhere('unit_id', (int) $this->ripartoFocusUnitId);
|
||||
if (! is_array($row)) {
|
||||
$this->ripartoFocusBreakdown = [];
|
||||
return;
|
||||
}
|
||||
|
||||
$subjects = [];
|
||||
$owners = $this->expandRipartoSubjects($row, 'owners');
|
||||
$tenants = $this->expandRipartoSubjects($row, 'tenants');
|
||||
foreach ($owners as $owner) {
|
||||
$subjects[] = $owner;
|
||||
}
|
||||
foreach ($tenants as $tenant) {
|
||||
$subjects[] = $tenant;
|
||||
}
|
||||
|
||||
$this->ripartoFocusBreakdown = $subjects;
|
||||
}
|
||||
|
||||
protected function expandRipartoSubjects(array $row, string $bucket): array
|
||||
{
|
||||
$source = $bucket === 'owners' ? ($row['owners'] ?? []) : ($row['tenants'] ?? []);
|
||||
$pieces = [];
|
||||
|
||||
if (is_array($source) && $source !== []) {
|
||||
foreach ($source as $item) {
|
||||
$pieces[] = [
|
||||
'name' => (string) ($item['name'] ?? 'Soggetto'),
|
||||
'quota' => is_numeric($item['quota'] ?? null) ? (float) $item['quota'] : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($pieces === []) {
|
||||
$summary = (string) ($row['people_summary'] ?? '');
|
||||
$names = $bucket === 'owners'
|
||||
? preg_split('/\s*,\s*/', preg_replace('/^Prop:\s*/', '', preg_match('/Prop:\s*([^·]+)/', $summary, $m) ? $m[1] : ''), -1, PREG_SPLIT_NO_EMPTY)
|
||||
: preg_split('/\s*,\s*/', preg_replace('/^Inq:\s*/', '', preg_match('/Inq:\s*(.+)$/', $summary, $m) ? $m[1] : ''), -1, PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
$pieces = array_map(fn(string $name): array=> ['name' => $name, 'quota' => null], $names ?: []);
|
||||
}
|
||||
|
||||
if ($pieces === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$count = count($pieces);
|
||||
$declared = collect($pieces)->sum(fn(array $item): float => is_numeric($item['quota'] ?? null) ? (float) $item['quota'] : 0.0);
|
||||
$baseKey = $bucket === 'owners' ? 'totale_condomini' : 'totale_inquilini';
|
||||
$rows = [];
|
||||
|
||||
foreach ($pieces as $item) {
|
||||
$share = $declared > 0 ? (((float) ($item['quota'] ?? 0.0)) / $declared) : (1 / max($count, 1));
|
||||
$columns = [];
|
||||
$totale = 0.0;
|
||||
foreach (($row['table_breakdown'] ?? []) as $code => $values) {
|
||||
$base = (float) ($values[$bucket === 'owners' ? 'condomini' : 'inquilini'] ?? 0.0);
|
||||
$quota = $base * $share;
|
||||
$columns[$code] = $quota;
|
||||
$totale += $quota;
|
||||
}
|
||||
|
||||
$rows[] = [
|
||||
'name' => $item['name'],
|
||||
'role' => $bucket === 'owners' ? 'Condomino' : 'Inquilino',
|
||||
'quota_percent' => $share * 100,
|
||||
'columns' => $columns,
|
||||
'totale' => $totale > 0 ? $totale : ((float) ($row[$baseKey] ?? 0.0) * $share),
|
||||
];
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public function getStraordinarieGestioniProperty(): array
|
||||
|
|
@ -267,7 +455,7 @@ public function getAcquaSummaryProperty(): array
|
|||
->where('cod_tab', 'ACQUA')
|
||||
->when($legacyCode, fn($q) => $q->where('cod_stabile', $legacyCode))
|
||||
->when($legacyYear, fn($q) => $q->where('legacy_year', $legacyYear))
|
||||
->sum(DB::raw('COALESCE(cons_euro, 0)'));
|
||||
->sum(DB::raw('COALESCE(prev_euro, 0)'));
|
||||
|
||||
$totaleFatture = array_sum($byCode);
|
||||
|
||||
|
|
@ -319,7 +507,7 @@ public function getAcquaRipartoProperty(): array
|
|||
$ripartoRows = $ripartoQuery
|
||||
->orderBy('id_cond')
|
||||
->orderBy('cond_inquil')
|
||||
->get(['id_cond', 'cond_inquil', 'cons_euro']);
|
||||
->get(['id_cond', 'cond_inquil', 'prev_euro']);
|
||||
|
||||
$out = [];
|
||||
$totale = 0.0;
|
||||
|
|
@ -349,7 +537,7 @@ public function getAcquaRipartoProperty(): array
|
|||
$nominativo = '—';
|
||||
}
|
||||
|
||||
$val = (float) ($r->cons_euro ?? 0);
|
||||
$val = (float) ($r->prev_euro ?? 0);
|
||||
$totale += $val;
|
||||
$out[] = [
|
||||
'id_cond' => $r->id_cond,
|
||||
|
|
@ -358,13 +546,35 @@ public function getAcquaRipartoProperty(): array
|
|||
'scala' => $cond->scala ?? null,
|
||||
'interno' => $cond->interno ?? null,
|
||||
'piano' => $cond->piano ?? null,
|
||||
'cons_euro' => $val,
|
||||
'prev_euro' => $val,
|
||||
];
|
||||
}
|
||||
|
||||
usort($out, function (array $left, array $right): int {
|
||||
return strnatcasecmp($this->buildAcquaRipartoSortKey($left), $this->buildAcquaRipartoSortKey($right));
|
||||
});
|
||||
|
||||
return ['rows' => $out, 'totale' => $totale];
|
||||
}
|
||||
|
||||
private function buildAcquaRipartoSortKey(array $row): string
|
||||
{
|
||||
return implode('|', [
|
||||
$this->normalizeAcquaNaturalToken((string) ($row['scala'] ?? '')),
|
||||
$this->normalizeAcquaNaturalToken((string) ($row['interno'] ?? '')),
|
||||
$this->normalizeAcquaNaturalToken((string) ($row['piano'] ?? '')),
|
||||
$this->normalizeAcquaNaturalToken((string) ($row['nominativo'] ?? '')),
|
||||
str_pad((string) ((int) ($row['id_cond'] ?? 0)), 10, '0', STR_PAD_LEFT),
|
||||
]);
|
||||
}
|
||||
|
||||
private function normalizeAcquaNaturalToken(string $value): string
|
||||
{
|
||||
$normalized = strtoupper(trim($value));
|
||||
|
||||
return preg_replace('/[\s\.\-\/]+/', '', $normalized) ?? '';
|
||||
}
|
||||
|
||||
public ?int $dettPersNSpe = null;
|
||||
public array $dettPersRows = [];
|
||||
public float $dettPersTotale = 0.0;
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ class PostIt extends Page
|
|||
public ?int $assegnazioneFornitoreId = null;
|
||||
|
||||
public ?int $selectedSuggerimentoId = null;
|
||||
public bool $disableAutoStabileAssociation = false;
|
||||
|
||||
/** @var \Illuminate\Support\Collection<int, RubricaUniversale> */
|
||||
public $suggerimentiRubrica;
|
||||
|
|
@ -65,6 +66,8 @@ public function mount(): void
|
|||
$focus = (int) request()->query('focus_post_it', 0);
|
||||
$this->focusPostItId = $focus > 0 ? $focus : null;
|
||||
$this->selectedPostItId = $this->focusPostItId;
|
||||
|
||||
$this->applyQueryPrefill();
|
||||
}
|
||||
|
||||
public function getGestionePostItUrl(): string
|
||||
|
|
@ -201,9 +204,11 @@ public function updatedTelefono(?string $value): void
|
|||
$this->nomeChiamante = $match->nome_completo ?: $match->ragione_sociale;
|
||||
}
|
||||
|
||||
$stabile = Stabile::query()->where('rubrica_id', $match->id)->first();
|
||||
if ($stabile) {
|
||||
$this->stabileId = $stabile->id;
|
||||
if (! $this->disableAutoStabileAssociation) {
|
||||
$stabile = Stabile::query()->where('rubrica_id', $match->id)->first();
|
||||
if ($stabile) {
|
||||
$this->stabileId = $stabile->id;
|
||||
}
|
||||
}
|
||||
|
||||
if (blank($this->ricercaChiamante)) {
|
||||
|
|
@ -228,8 +233,10 @@ public function selezionaSuggerimentoRubrica(int $rubricaId): void
|
|||
$this->telefono = $match->telefono_cellulare ?: ($match->telefono_ufficio ?: $match->telefono_casa);
|
||||
$this->ricercaChiamante = $match->nome_completo ?: ($match->ragione_sociale ?: '');
|
||||
|
||||
$stabile = Stabile::query()->where('rubrica_id', $match->id)->first();
|
||||
$this->stabileId = $stabile?->id;
|
||||
if (! $this->disableAutoStabileAssociation) {
|
||||
$stabile = Stabile::query()->where('rubrica_id', $match->id)->first();
|
||||
$this->stabileId = $stabile?->id;
|
||||
}
|
||||
|
||||
$this->suggerimentiRubrica = $this->searchRubrica((string) $this->ricercaChiamante);
|
||||
}
|
||||
|
|
@ -393,15 +400,38 @@ public function getRecentiProperty()
|
|||
$items = ChiamataPostIt::query()
|
||||
->with(['rubrica', 'stabile', 'ticket', 'creatoDa', 'assegnatoAUser:id,name', 'assegnatoAFornitore:id,ragione_sociale,nome,cognome'])
|
||||
->orderByDesc('chiamata_il')
|
||||
->limit(25)
|
||||
->limit(60)
|
||||
->get();
|
||||
|
||||
if (! $this->selectedPostItId && $items->isNotEmpty()) {
|
||||
$this->selectedPostItId = (int) ($this->focusPostItId ?: $items->first()->id);
|
||||
$grouped = $items
|
||||
->groupBy(fn(ChiamataPostIt $item): string => $this->buildRecenteGroupKey($item))
|
||||
->map(function (Collection $group): array {
|
||||
/** @var ChiamataPostIt $primary */
|
||||
$primary = $group->first();
|
||||
$callerLabel = $this->resolveRecenteCallerLabel($group);
|
||||
$phoneLabel = $this->resolveRecentePhoneLabel($group);
|
||||
|
||||
return [
|
||||
'primary' => $primary,
|
||||
'items' => $group->values(),
|
||||
'count' => $group->count(),
|
||||
'caller_label' => $callerLabel,
|
||||
'phone' => $phoneLabel,
|
||||
'latest_at' => optional($primary->chiamata_il)->format('d/m/Y H:i'),
|
||||
'contains_selected' => $this->selectedPostItId
|
||||
? $group->contains(fn(ChiamataPostIt $item): bool => (int) $item->id === (int) $this->selectedPostItId)
|
||||
: false,
|
||||
];
|
||||
})
|
||||
->values();
|
||||
|
||||
if (! $this->selectedPostItId && $grouped->isNotEmpty()) {
|
||||
$first = $grouped->first();
|
||||
$this->selectedPostItId = (int) ($this->focusPostItId ?: ($first['primary']->id ?? 0));
|
||||
$this->loadSelectedPostItAssignment();
|
||||
}
|
||||
|
||||
return $items;
|
||||
return $grouped;
|
||||
} catch (QueryException) {
|
||||
return collect();
|
||||
}
|
||||
|
|
@ -518,6 +548,181 @@ private function createPostItRecord(): ChiamataPostIt
|
|||
]);
|
||||
}
|
||||
|
||||
private function applyQueryPrefill(): void
|
||||
{
|
||||
$source = trim((string) request()->query('source', ''));
|
||||
$this->disableAutoStabileAssociation = request()->boolean('lock_stabile') || $source === 'topbar_live_call';
|
||||
|
||||
$telefono = trim((string) request()->query('telefono', ''));
|
||||
$nome = trim((string) request()->query('nome', ''));
|
||||
$nota = trim((string) request()->query('nota', ''));
|
||||
$oggetto = trim((string) request()->query('oggetto', ''));
|
||||
$direzione = trim((string) request()->query('direzione', ''));
|
||||
$rubricaId = (int) request()->query('rubrica_id', 0);
|
||||
|
||||
if ($telefono !== '') {
|
||||
$this->telefono = $telefono;
|
||||
}
|
||||
if ($nome !== '') {
|
||||
$this->nomeChiamante = $nome;
|
||||
$this->ricercaChiamante = $nome;
|
||||
}
|
||||
if ($nota !== '') {
|
||||
$this->nota = $nota;
|
||||
}
|
||||
if ($oggetto !== '') {
|
||||
$this->oggetto = $oggetto;
|
||||
}
|
||||
if (in_array($direzione, ['in_arrivo', 'in_uscita', 'persa'], true)) {
|
||||
$this->direzione = $direzione;
|
||||
}
|
||||
|
||||
if ($rubricaId > 0) {
|
||||
$rubrica = RubricaUniversale::query()->find($rubricaId);
|
||||
if ($rubrica) {
|
||||
$this->rubricaId = (int) $rubrica->id;
|
||||
if ($this->nomeChiamante === null || trim((string) $this->nomeChiamante) === '') {
|
||||
$this->nomeChiamante = $rubrica->nome_completo ?: $rubrica->ragione_sociale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->disableAutoStabileAssociation) {
|
||||
$this->stabileId = null;
|
||||
}
|
||||
|
||||
if ($source === 'topbar_live_call' && $telefono !== '') {
|
||||
$this->hydrateRubricaForPhonePrefill($telefono, $nome);
|
||||
if ($this->nota === null || trim((string) $this->nota) === '') {
|
||||
$this->nota = 'Chiamata in ingresso da ' . $telefono;
|
||||
}
|
||||
if ($this->oggetto === null || trim((string) $this->oggetto) === '') {
|
||||
$this->oggetto = 'Chiamata in arrivo da valutare';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function hydrateRubricaForPhonePrefill(string $phone, string $name = ''): void
|
||||
{
|
||||
$match = $this->findRubricaByPhone($phone);
|
||||
if (! $match && $this->canCreateRubricaDraftFromPhone($phone)) {
|
||||
$match = $this->createRubricaDraftFromPhone($phone, $name);
|
||||
}
|
||||
|
||||
if (! $match) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->rubricaId = (int) $match->id;
|
||||
if ($this->nomeChiamante === null || trim((string) $this->nomeChiamante) === '') {
|
||||
$this->nomeChiamante = $match->nome_completo ?: $match->ragione_sociale;
|
||||
}
|
||||
if ($this->ricercaChiamante === null || trim((string) $this->ricercaChiamante) === '') {
|
||||
$this->ricercaChiamante = $match->nome_completo ?: ($match->ragione_sociale ?: '');
|
||||
}
|
||||
}
|
||||
|
||||
private function canCreateRubricaDraftFromPhone(string $phone): bool
|
||||
{
|
||||
$digits = preg_replace('/\D+/', '', $phone) ?: '';
|
||||
|
||||
return strlen($digits) >= 6;
|
||||
}
|
||||
|
||||
private function createRubricaDraftFromPhone(string $phone, string $name = ''): ?RubricaUniversale
|
||||
{
|
||||
$digits = preg_replace('/\D+/', '', $phone) ?: '';
|
||||
if ($digits === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$normalizedPhone = PhoneNumber::toE164Italy($phone) ?? $digits;
|
||||
$adminId = $this->resolveCurrentAmministratoreId();
|
||||
$cleanName = trim($name);
|
||||
|
||||
$payload = [
|
||||
'amministratore_id' => $adminId > 0 ? $adminId : null,
|
||||
'tipo_contatto' => str_contains($cleanName, ' ') ? 'persona_fisica' : 'persona_giuridica',
|
||||
'telefono_cellulare' => $normalizedPhone,
|
||||
'categoria' => 'altro',
|
||||
'stato' => 'attivo',
|
||||
'data_inserimento' => now()->toDateString(),
|
||||
'data_ultima_modifica' => now()->toDateString(),
|
||||
'creato_da' => Auth::id(),
|
||||
'modificato_da' => Auth::id(),
|
||||
'note_segreteria' => 'Bozza creata dal box Post-it in topbar per una chiamata in ingresso.',
|
||||
];
|
||||
|
||||
if ($cleanName !== '' && str_contains($cleanName, ' ')) {
|
||||
$parts = preg_split('/\s+/', $cleanName) ?: [];
|
||||
$payload['nome'] = array_shift($parts) ?: null;
|
||||
$payload['cognome'] = $parts !== [] ? implode(' ', $parts) : null;
|
||||
} elseif ($cleanName !== '') {
|
||||
$payload['ragione_sociale'] = $cleanName;
|
||||
} else {
|
||||
$payload['ragione_sociale'] = 'Contatto ' . $digits;
|
||||
}
|
||||
|
||||
return RubricaUniversale::query()->create($payload);
|
||||
}
|
||||
|
||||
private function buildRecenteGroupKey(ChiamataPostIt $item): string
|
||||
{
|
||||
$phone = preg_replace('/\D+/', '', (string) ($item->telefono ?? '')) ?: '';
|
||||
if ($phone !== '') {
|
||||
return 'phone:' . $phone;
|
||||
}
|
||||
|
||||
if ((int) ($item->rubrica_id ?? 0) > 0) {
|
||||
return 'rubrica:' . (int) $item->rubrica_id;
|
||||
}
|
||||
|
||||
return 'caller:' . mb_strtolower(trim((string) ($item->nome_chiamante ?? 'sconosciuto')));
|
||||
}
|
||||
|
||||
private function resolveRecenteCallerLabel(Collection $group): string
|
||||
{
|
||||
foreach ($group as $item) {
|
||||
if ($item instanceof ChiamataPostIt && $item->rubrica) {
|
||||
return $item->rubrica->nome_completo ?: ($item->rubrica->ragione_sociale ?: 'Contatto');
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($group as $item) {
|
||||
$label = trim((string) ($item->nome_chiamante ?? ''));
|
||||
if ($label !== '' && ! $this->isGenericRecenteCallerLabel($label, (string) ($item->telefono ?? ''))) {
|
||||
return $label;
|
||||
}
|
||||
}
|
||||
|
||||
return 'Contatto non in Rubrica';
|
||||
}
|
||||
|
||||
private function resolveRecentePhoneLabel(Collection $group): string
|
||||
{
|
||||
foreach ($group as $item) {
|
||||
$phone = trim((string) ($item->telefono ?? ''));
|
||||
if ($phone !== '') {
|
||||
return $phone;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private function isGenericRecenteCallerLabel(string $label, string $phone = ''): bool
|
||||
{
|
||||
$normalizedLabel = mb_strtolower(trim($label));
|
||||
if ($normalizedLabel === '' || in_array($normalizedLabel, ['chiamante non identificato', 'contatto non in rubrica'], true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$labelDigits = preg_replace('/\D+/', '', $label) ?: '';
|
||||
$phoneDigits = preg_replace('/\D+/', '', $phone) ?: '';
|
||||
|
||||
return $labelDigits !== '' && $phoneDigits !== '' && $labelDigits === $phoneDigits;
|
||||
}
|
||||
|
||||
private function loadSelectedPostItAssignment(): void
|
||||
{
|
||||
$postIt = $this->selectedPostIt;
|
||||
|
|
|
|||
|
|
@ -57,6 +57,9 @@ class TicketAcqua extends Page
|
|||
/** @var array<int,mixed> */
|
||||
public array $pendingWaterPhotos = [];
|
||||
|
||||
/** @var array<int,mixed> */
|
||||
public array $pendingWaterLibraryPhotos = [];
|
||||
|
||||
/** @var array<int,string> */
|
||||
public array $waterPhotoDescriptions = [];
|
||||
|
||||
|
|
@ -151,6 +154,11 @@ public function updatedPendingWaterPhotos(): void
|
|||
$this->appendPendingWaterPhotos();
|
||||
}
|
||||
|
||||
public function updatedPendingWaterLibraryPhotos(): void
|
||||
{
|
||||
$this->appendPendingWaterLibraryPhotos();
|
||||
}
|
||||
|
||||
public function removeWaterPhoto(int $index): void
|
||||
{
|
||||
$file = $this->newWaterPhotos[$index] ?? null;
|
||||
|
|
@ -648,10 +656,39 @@ private function appendPendingWaterPhotos(): void
|
|||
$this->syncWaterDescriptionSlots();
|
||||
}
|
||||
|
||||
private function appendPendingWaterLibraryPhotos(): void
|
||||
{
|
||||
$pending = array_values(array_filter($this->pendingWaterLibraryPhotos, fn($file) => is_object($file)));
|
||||
if ($pending === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$available = max(0, 4 - count($this->newWaterPhotos));
|
||||
if ($available <= 0) {
|
||||
$this->pendingWaterLibraryPhotos = [];
|
||||
|
||||
Notification::make()
|
||||
->title('Limite foto raggiunto')
|
||||
->body('Puoi tenere in coda al massimo 4 foto per una lettura acqua.')
|
||||
->warning()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$accepted = array_slice($pending, 0, $available);
|
||||
$currentTotal = count($this->newWaterPhotos);
|
||||
$this->newWaterPhotos = array_values(array_merge($this->newWaterPhotos, $accepted));
|
||||
$this->pendingWaterLibraryPhotos = [];
|
||||
$this->assignDraftUploadCodes($accepted, $currentTotal);
|
||||
$this->syncWaterDescriptionSlots();
|
||||
}
|
||||
|
||||
private function resetWaterDraftUploadState(): void
|
||||
{
|
||||
$this->newWaterPhotos = [];
|
||||
$this->pendingWaterPhotos = [];
|
||||
$this->pendingWaterLibraryPhotos = [];
|
||||
$this->waterPhotoDescriptions = [];
|
||||
$this->waterUploadCodes = [];
|
||||
$this->waterClientMetadata = [];
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@
|
|||
namespace App\Livewire\Condomini;
|
||||
|
||||
use App\Filament\Pages\Contabilita\CasseBancheMovimenti;
|
||||
use App\Filament\Pages\Contabilita\SaldiContiArchivio;
|
||||
use App\Filament\Pages\Gescon\RubricaUniversaleScheda;
|
||||
use App\Models\DatiBancari;
|
||||
use App\Models\RubricaUniversale;
|
||||
use App\Models\Stabile;
|
||||
use App\Models\User;
|
||||
use App\Modules\Contabilita\Models\SaldoConto;
|
||||
use App\Services\GesconImport\StabileEnrichmentService;
|
||||
use App\Support\StabileContext;
|
||||
use Filament\Actions\Action;
|
||||
|
|
@ -93,6 +95,18 @@ public function table(Table $table): Table
|
|||
->date('d/m/Y')
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
||||
TextColumn::make('saldo_ufficiale')
|
||||
->label('Saldo ufficiale')
|
||||
->stateUsing(fn(DatiBancari $record): ?float => $this->resolveLatestSaldoSnapshotValue($record))
|
||||
->money('EUR')
|
||||
->toggleable(),
|
||||
|
||||
TextColumn::make('ultimo_estratto')
|
||||
->label('Ultimo estratto')
|
||||
->stateUsing(fn(DatiBancari $record): string => $this->resolveLatestSaldoSnapshotLabel($record))
|
||||
->wrap()
|
||||
->toggleable(),
|
||||
|
||||
TextColumn::make('contatto.nome_completo')
|
||||
->label('Contatto')
|
||||
->wrap()
|
||||
|
|
@ -198,6 +212,12 @@ public function table(Table $table): Table
|
|||
})
|
||||
->openUrlInNewTab(),
|
||||
|
||||
Action::make('saldi')
|
||||
->label('Saldi')
|
||||
->icon('heroicon-o-scale')
|
||||
->url(fn(): string => SaldiContiArchivio::getUrl(panel: 'admin-filament'))
|
||||
->openUrlInNewTab(),
|
||||
|
||||
Action::make('contatto')
|
||||
->label('Contatto')
|
||||
->icon('heroicon-o-identification')
|
||||
|
|
@ -419,4 +439,59 @@ private function normalizePayload(array $data): array
|
|||
'note' => isset($data['note']) ? trim((string) $data['note']) : null,
|
||||
];
|
||||
}
|
||||
|
||||
private function resolveLatestSaldoSnapshot(DatiBancari $record): ?SaldoConto
|
||||
{
|
||||
static $cache = [];
|
||||
|
||||
$cacheKey = (int) $record->id;
|
||||
if (array_key_exists($cacheKey, $cache)) {
|
||||
return $cache[$cacheKey];
|
||||
}
|
||||
|
||||
return $cache[$cacheKey] = SaldoConto::query()
|
||||
->with('documento')
|
||||
->where('stabile_id', (int) $record->stabile_id)
|
||||
->where('conto_id', (int) $record->id)
|
||||
->orderByDesc('data_saldo')
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
}
|
||||
|
||||
private function resolveLatestSaldoSnapshotValue(DatiBancari $record): ?float
|
||||
{
|
||||
$snapshot = $this->resolveLatestSaldoSnapshot($record);
|
||||
|
||||
return $snapshot ? (float) $snapshot->saldo : null;
|
||||
}
|
||||
|
||||
private function resolveLatestSaldoSnapshotLabel(DatiBancari $record): string
|
||||
{
|
||||
$snapshot = $this->resolveLatestSaldoSnapshot($record);
|
||||
if (! $snapshot) {
|
||||
return 'Nessun estratto registrato';
|
||||
}
|
||||
|
||||
$parts = [];
|
||||
if ($snapshot->tipo_estratto) {
|
||||
$parts[] = match ($snapshot->tipo_estratto) {
|
||||
'estratto_conto' => 'Estratto conto',
|
||||
'saldo_banca' => 'Saldo banca',
|
||||
'riconciliazione' => 'Riconciliazione',
|
||||
default => 'Altro',
|
||||
};
|
||||
}
|
||||
if ($snapshot->periodo_da || $snapshot->periodo_a) {
|
||||
$from = $snapshot->periodo_da?->format('d/m/Y') ?: '...';
|
||||
$to = $snapshot->periodo_a?->format('d/m/Y') ?: '...';
|
||||
$parts[] = $from . ' - ' . $to;
|
||||
} else {
|
||||
$parts[] = $snapshot->data_saldo?->format('d/m/Y') ?: 'Data non indicata';
|
||||
}
|
||||
if ($snapshot->documento?->nome_originale) {
|
||||
$parts[] = $snapshot->documento->nome_originale;
|
||||
}
|
||||
|
||||
return implode(' · ', $parts);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
<?php
|
||||
namespace App\Livewire\Filament;
|
||||
|
||||
use App\Filament\Pages\Strumenti\PostItGestione;
|
||||
use App\Models\ChiamataPostIt;
|
||||
use App\Filament\Pages\Strumenti\PostIt;
|
||||
use App\Models\User;
|
||||
use App\Services\Cti\LiveIncomingCallService;
|
||||
use App\Support\StabileContext;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Component;
|
||||
|
||||
|
|
@ -38,34 +35,16 @@ public function createPostIt(): void
|
|||
return;
|
||||
}
|
||||
|
||||
$stabileId = StabileContext::resolveActiveStabileId($user);
|
||||
if (! $stabileId) {
|
||||
Notification::make()->title('Seleziona prima uno stabile attivo')->warning()->send();
|
||||
return;
|
||||
}
|
||||
|
||||
$postIt = ChiamataPostIt::query()->create([
|
||||
'stabile_id' => (int) $stabileId,
|
||||
'creato_da_user_id' => (int) $user->id,
|
||||
'rubrica_id' => (int) ($this->liveIncomingCall['rubrica_id'] ?? 0) ?: null,
|
||||
'telefono' => (string) ($this->liveIncomingCall['phone'] ?? ''),
|
||||
'nome_chiamante' => (string) (($this->liveIncomingCall['rubrica_nome'] ?? '') ?: ('Chiamata ' . ($this->liveIncomingCall['phone'] ?? ''))),
|
||||
'direzione' => 'in_arrivo',
|
||||
'origine' => 'topbar_live_call',
|
||||
'origine_id' => (string) ($this->liveIncomingCall['message_id'] ?? ''),
|
||||
'oggetto' => 'Chiamata in arrivo da valutare',
|
||||
'nota' => (string) ($this->liveIncomingCall['line'] ?? ('Chiamata in ingresso da ' . ($this->liveIncomingCall['phone'] ?? ''))),
|
||||
'priorita' => 'Media',
|
||||
'stato' => 'post_it',
|
||||
'chiamata_il' => now(),
|
||||
]);
|
||||
|
||||
Notification::make()->title('Post-it creato dalla testata')->success()->send();
|
||||
|
||||
$this->redirect(
|
||||
PostItGestione::getUrl([
|
||||
'tab' => 'storico',
|
||||
'focus_post_it' => (int) $postIt->id,
|
||||
PostIt::getUrl([
|
||||
'source' => 'topbar_live_call',
|
||||
'telefono' => (string) ($this->liveIncomingCall['phone'] ?? ''),
|
||||
'nome' => (string) ($this->liveIncomingCall['rubrica_nome'] ?? ''),
|
||||
'rubrica_id' => (int) ($this->liveIncomingCall['rubrica_id'] ?? 0) ?: null,
|
||||
'direzione' => 'in_arrivo',
|
||||
'oggetto' => 'Chiamata in arrivo da valutare',
|
||||
'nota' => (string) ($this->liveIncomingCall['line'] ?? ('Chiamata in ingresso da ' . ($this->liveIncomingCall['phone'] ?? ''))),
|
||||
'lock_stabile' => 1,
|
||||
], panel: 'admin-filament'),
|
||||
navigate: true,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -170,7 +170,11 @@ public function getStatoScadenzaAttribute()
|
|||
*/
|
||||
public function getUrlDownloadAttribute()
|
||||
{
|
||||
return route('admin.documenti.download', $this->id);
|
||||
if (is_string($this->percorso_file) && trim($this->percorso_file) !== '' && Storage::disk('public')->exists($this->percorso_file)) {
|
||||
return Storage::disk('public')->url($this->percorso_file);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -178,7 +182,7 @@ public function getUrlDownloadAttribute()
|
|||
*/
|
||||
public function getUrlViewAttribute()
|
||||
{
|
||||
return route('admin.documenti.view', $this->id);
|
||||
return $this->url_download;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -195,7 +199,9 @@ public function incrementaDownload()
|
|||
*/
|
||||
public function fileEsiste()
|
||||
{
|
||||
return Storage::exists($this->percorso_file);
|
||||
return is_string($this->percorso_file)
|
||||
&& trim($this->percorso_file) !== ''
|
||||
&& (Storage::disk('public')->exists($this->percorso_file) || Storage::disk('local')->exists($this->percorso_file));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -203,9 +209,18 @@ public function fileEsiste()
|
|||
*/
|
||||
public function eliminaFile()
|
||||
{
|
||||
if ($this->fileEsiste()) {
|
||||
return Storage::delete($this->percorso_file);
|
||||
if (! is_string($this->percorso_file) || trim($this->percorso_file) === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Storage::disk('public')->exists($this->percorso_file)) {
|
||||
return Storage::disk('public')->delete($this->percorso_file);
|
||||
}
|
||||
|
||||
if (Storage::disk('local')->exists($this->percorso_file)) {
|
||||
return Storage::disk('local')->delete($this->percorso_file);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Modules\Contabilita\Models;
|
||||
|
||||
use App\Models\DatiBancari;
|
||||
use App\Models\DocumentoStabile;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class SaldoConto extends Model
|
||||
|
|
@ -14,14 +15,20 @@ class SaldoConto extends Model
|
|||
'conto_id',
|
||||
'iban',
|
||||
'data_saldo',
|
||||
'periodo_da',
|
||||
'periodo_a',
|
||||
'saldo',
|
||||
'tipo_estratto',
|
||||
'note',
|
||||
'documento_stabile_id',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'data_saldo' => 'date',
|
||||
'periodo_da' => 'date',
|
||||
'periodo_a' => 'date',
|
||||
'saldo' => 'decimal:2',
|
||||
];
|
||||
|
||||
|
|
@ -29,4 +36,9 @@ public function conto()
|
|||
{
|
||||
return $this->belongsTo(DatiBancari::class, 'conto_id');
|
||||
}
|
||||
|
||||
public function documento()
|
||||
{
|
||||
return $this->belongsTo(DocumentoStabile::class, 'documento_stabile_id');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -171,4 +171,4 @@ protected function resolveSupplierCatalogUrl(): string
|
|||
|
||||
return TicketOperativi::getUrl(panel: 'admin-filament');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Contabilita;
|
||||
|
||||
use App\Models\DatiBancari;
|
||||
use App\Models\RubricaUniversale;
|
||||
use App\Models\Fornitore;
|
||||
use App\Models\RubricaUniversale;
|
||||
use App\Modules\Contabilita\Models\MovimentoBanca;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
|
@ -24,8 +23,8 @@ public function __construct(
|
|||
?MpsQifParser $mpsQifParser = null,
|
||||
) {
|
||||
$this->unicreditSemicolonCsvParser = $unicreditSemicolonCsvParser ?? new UnicreditSemicolonCsvParser();
|
||||
$this->cedhouseCsvParser = $cedhouseCsvParser ?? new CedhouseCsvParser();
|
||||
$this->mpsQifParser = $mpsQifParser ?? new MpsQifParser();
|
||||
$this->cedhouseCsvParser = $cedhouseCsvParser ?? new CedhouseCsvParser();
|
||||
$this->mpsQifParser = $mpsQifParser ?? new MpsQifParser();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -51,7 +50,7 @@ public function importUnicreditSemicolonCsvForConto(
|
|||
$iban = null;
|
||||
}
|
||||
|
||||
$imported = 0;
|
||||
$imported = 0;
|
||||
$duplicates = 0;
|
||||
|
||||
$minDateStr = null;
|
||||
|
|
@ -61,7 +60,10 @@ public function importUnicreditSemicolonCsvForConto(
|
|||
$max = null;
|
||||
foreach ($parsed['rows'] as $r) {
|
||||
$d = $r['data'] ?? null;
|
||||
if (! $d) continue;
|
||||
if (! $d) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$min = $min ? ($d->lt($min) ? $d : $min) : $d;
|
||||
$max = $max ? ($d->gt($max) ? $d : $max) : $d;
|
||||
}
|
||||
|
|
@ -117,18 +119,18 @@ public function importUnicreditSemicolonCsvForConto(
|
|||
}
|
||||
|
||||
$payload = [
|
||||
'stabile_id' => $stabileId,
|
||||
'conto_id' => $contoId,
|
||||
'stabile_id' => $stabileId,
|
||||
'conto_id' => $contoId,
|
||||
'gestione_id' => $gestioneId,
|
||||
'iban' => $iban,
|
||||
'data' => $row['data'],
|
||||
'valuta' => $row['valuta'],
|
||||
'iban' => $iban,
|
||||
'data' => $row['data'],
|
||||
'valuta' => $row['valuta'],
|
||||
'descrizione' => $row['descrizione'],
|
||||
'importo' => $row['importo'],
|
||||
'causale' => $row['causale'],
|
||||
'importo' => $row['importo'],
|
||||
'causale' => $row['causale'],
|
||||
'source_file' => $sourceFile,
|
||||
'raw_line' => $row['raw_line'],
|
||||
'row_hash' => $rowHash,
|
||||
'raw_line' => $row['raw_line'],
|
||||
'row_hash' => $rowHash,
|
||||
];
|
||||
|
||||
if (Schema::hasColumn('contabilita_movimenti_banca', 'descrizione_estesa')) {
|
||||
|
|
@ -136,8 +138,8 @@ public function importUnicreditSemicolonCsvForConto(
|
|||
? trim($row['descrizione_estesa'])
|
||||
: null;
|
||||
if (($payload['descrizione_estesa'] ?? null) === null && is_string($row['raw_line'] ?? null) && str_contains($row['raw_line'], '|')) {
|
||||
$parts = explode('|', (string) $row['raw_line'], 2);
|
||||
$fallback = trim((string) ($parts[1] ?? ''));
|
||||
$parts = explode('|', (string) $row['raw_line'], 2);
|
||||
$fallback = trim((string) ($parts[1] ?? ''));
|
||||
$payload['descrizione_estesa'] = $fallback !== '' ? $fallback : null;
|
||||
}
|
||||
}
|
||||
|
|
@ -190,7 +192,7 @@ public function importCsvAutoForConto(
|
|||
$format = $this->detectCsvFormat($content);
|
||||
|
||||
if ($format === 'unicredit_semicolon') {
|
||||
$res = $this->importUnicreditSemicolonCsvForConto($content, $stabileId, $contoId, $sourceFile, $gestioneId, $replace);
|
||||
$res = $this->importUnicreditSemicolonCsvForConto($content, $stabileId, $contoId, $sourceFile, $gestioneId, $replace);
|
||||
$res['meta'] = array_merge(['format' => $format], is_array($res['meta'] ?? null) ? $res['meta'] : []);
|
||||
return $res;
|
||||
}
|
||||
|
|
@ -239,7 +241,7 @@ public function importCsvAutoForConto(
|
|||
foreach (['unicredit_wri', 'intesa_csv', 'cedhouse_csv', 'unicredit_semicolon'] as $try) {
|
||||
try {
|
||||
if ($try === 'unicredit_semicolon') {
|
||||
$res = $this->importUnicreditSemicolonCsvForConto($content, $stabileId, $contoId, $sourceFile, $gestioneId, $replace);
|
||||
$res = $this->importUnicreditSemicolonCsvForConto($content, $stabileId, $contoId, $sourceFile, $gestioneId, $replace);
|
||||
$res['meta'] = array_merge(['format' => $try], is_array($res['meta'] ?? null) ? $res['meta'] : []);
|
||||
return $res;
|
||||
}
|
||||
|
|
@ -357,7 +359,7 @@ private function importParsedRowsForConto(
|
|||
$iban = null;
|
||||
}
|
||||
|
||||
$imported = 0;
|
||||
$imported = 0;
|
||||
$duplicates = 0;
|
||||
|
||||
$hasDescrizioneEstesa = false;
|
||||
|
|
@ -369,11 +371,14 @@ private function importParsedRowsForConto(
|
|||
|
||||
$minDateStr = null;
|
||||
$maxDateStr = null;
|
||||
$min = null;
|
||||
$max = null;
|
||||
$min = null;
|
||||
$max = null;
|
||||
foreach ($rows as $r) {
|
||||
$d = $r['data'] ?? null;
|
||||
if (! $d) continue;
|
||||
if (! $d) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$min = $min ? ($d->lt($min) ? $d : $min) : $d;
|
||||
$max = $max ? ($d->gt($max) ? $d : $max) : $d;
|
||||
}
|
||||
|
|
@ -386,7 +391,10 @@ private function importParsedRowsForConto(
|
|||
$maxDate = null;
|
||||
foreach ($rows as $r) {
|
||||
$d = $r['data'] ?? null;
|
||||
if (! $d) continue;
|
||||
if (! $d) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$minDate = $minDate ? ($d->lt($minDate) ? $d : $minDate) : $d;
|
||||
$maxDate = $maxDate ? ($d->gt($maxDate) ? $d : $maxDate) : $d;
|
||||
}
|
||||
|
|
@ -423,18 +431,18 @@ private function importParsedRowsForConto(
|
|||
}
|
||||
|
||||
$payload = [
|
||||
'stabile_id' => $stabileId,
|
||||
'conto_id' => $contoId,
|
||||
'stabile_id' => $stabileId,
|
||||
'conto_id' => $contoId,
|
||||
'gestione_id' => $gestioneId,
|
||||
'iban' => $iban,
|
||||
'data' => $row['data'],
|
||||
'valuta' => $row['valuta'],
|
||||
'iban' => $iban,
|
||||
'data' => $row['data'],
|
||||
'valuta' => $row['valuta'],
|
||||
'descrizione' => $row['descrizione'],
|
||||
'importo' => $row['importo'],
|
||||
'causale' => $row['causale'],
|
||||
'importo' => $row['importo'],
|
||||
'causale' => $row['causale'],
|
||||
'source_file' => $sourceFile,
|
||||
'raw_line' => $row['raw_line'],
|
||||
'row_hash' => $rowHash,
|
||||
'raw_line' => $row['raw_line'],
|
||||
'row_hash' => $rowHash,
|
||||
];
|
||||
|
||||
if ($hasDescrizioneEstesa) {
|
||||
|
|
@ -504,7 +512,7 @@ public function importUnicreditWri(
|
|||
bool $replace = false,
|
||||
): array {
|
||||
$parsed = $this->unicreditParser->parse($content);
|
||||
$iban = $forcedIban ?: ($parsed['iban'] ?? null);
|
||||
$iban = $forcedIban ?: ($parsed['iban'] ?? null);
|
||||
|
||||
$contoId = null;
|
||||
try {
|
||||
|
|
@ -524,11 +532,11 @@ public function importUnicreditWri(
|
|||
$contoId = null;
|
||||
}
|
||||
|
||||
$imported = 0;
|
||||
$imported = 0;
|
||||
$duplicates = 0;
|
||||
|
||||
DB::transaction(function () use ($parsed, $stabileId, $contoId, $iban, $sourceFile, $gestioneId, $replace, &$imported, &$duplicates) {
|
||||
if ($replace && !empty($parsed['rows'])) {
|
||||
if ($replace && ! empty($parsed['rows'])) {
|
||||
$minDate = null;
|
||||
$maxDate = null;
|
||||
foreach ($parsed['rows'] as $r) {
|
||||
|
|
@ -582,18 +590,18 @@ public function importUnicreditWri(
|
|||
}
|
||||
|
||||
MovimentoBanca::query()->create([
|
||||
'stabile_id' => $stabileId,
|
||||
'conto_id' => $contoId,
|
||||
'stabile_id' => $stabileId,
|
||||
'conto_id' => $contoId,
|
||||
'gestione_id' => $gestioneId,
|
||||
'iban' => $iban,
|
||||
'data' => $row['data'],
|
||||
'valuta' => $row['valuta'],
|
||||
'iban' => $iban,
|
||||
'data' => $row['data'],
|
||||
'valuta' => $row['valuta'],
|
||||
'descrizione' => $row['descrizione'],
|
||||
'importo' => $row['importo'],
|
||||
'causale' => $row['causale'],
|
||||
'importo' => $row['importo'],
|
||||
'causale' => $row['causale'],
|
||||
'source_file' => $sourceFile,
|
||||
'raw_line' => $row['raw_line'],
|
||||
'row_hash' => $rowHash,
|
||||
'raw_line' => $row['raw_line'],
|
||||
'row_hash' => $rowHash,
|
||||
]);
|
||||
|
||||
$imported++;
|
||||
|
|
@ -615,7 +623,7 @@ public function importIntesaCsv(
|
|||
bool $replace = false,
|
||||
): array {
|
||||
$parsed = $this->intesaParser->parse($content);
|
||||
$iban = $forcedIban ?: ($parsed['iban'] ?? null);
|
||||
$iban = $forcedIban ?: ($parsed['iban'] ?? null);
|
||||
|
||||
$hasDescrizioneEstesa = false;
|
||||
try {
|
||||
|
|
@ -642,7 +650,7 @@ public function importIntesaCsv(
|
|||
$contoId = null;
|
||||
}
|
||||
|
||||
$imported = 0;
|
||||
$imported = 0;
|
||||
$duplicates = 0;
|
||||
|
||||
DB::transaction(function () use ($parsed, $stabileId, $contoId, $iban, $sourceFile, $gestioneId, $replace, $hasDescrizioneEstesa, &$imported, &$duplicates) {
|
||||
|
|
@ -699,18 +707,18 @@ public function importIntesaCsv(
|
|||
}
|
||||
|
||||
$payload = [
|
||||
'stabile_id' => $stabileId,
|
||||
'conto_id' => $contoId,
|
||||
'stabile_id' => $stabileId,
|
||||
'conto_id' => $contoId,
|
||||
'gestione_id' => $gestioneId,
|
||||
'iban' => $iban,
|
||||
'data' => $row['data'],
|
||||
'valuta' => $row['valuta'],
|
||||
'iban' => $iban,
|
||||
'data' => $row['data'],
|
||||
'valuta' => $row['valuta'],
|
||||
'descrizione' => $row['descrizione'],
|
||||
'importo' => $row['importo'],
|
||||
'causale' => $row['causale'],
|
||||
'importo' => $row['importo'],
|
||||
'causale' => $row['causale'],
|
||||
'source_file' => $sourceFile,
|
||||
'raw_line' => $row['raw_line'],
|
||||
'row_hash' => $rowHash,
|
||||
'raw_line' => $row['raw_line'],
|
||||
'row_hash' => $rowHash,
|
||||
];
|
||||
|
||||
if ($hasDescrizioneEstesa) {
|
||||
|
|
@ -753,7 +761,7 @@ public function importExcelXlsxForConto(
|
|||
$iban = null;
|
||||
}
|
||||
|
||||
$imported = 0;
|
||||
$imported = 0;
|
||||
$duplicates = 0;
|
||||
|
||||
$minDateStr = null;
|
||||
|
|
@ -763,7 +771,10 @@ public function importExcelXlsxForConto(
|
|||
$max = null;
|
||||
foreach ($parsed['rows'] as $r) {
|
||||
$d = $r['data'] ?? null;
|
||||
if (! $d) continue;
|
||||
if (! $d) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$min = $min ? ($d->lt($min) ? $d : $min) : $d;
|
||||
$max = $max ? ($d->gt($max) ? $d : $max) : $d;
|
||||
}
|
||||
|
|
@ -819,18 +830,18 @@ public function importExcelXlsxForConto(
|
|||
}
|
||||
|
||||
$payload = [
|
||||
'stabile_id' => $stabileId,
|
||||
'conto_id' => $contoId,
|
||||
'stabile_id' => $stabileId,
|
||||
'conto_id' => $contoId,
|
||||
'gestione_id' => $gestioneId,
|
||||
'iban' => $iban,
|
||||
'data' => $row['data'],
|
||||
'valuta' => $row['valuta'],
|
||||
'iban' => $iban,
|
||||
'data' => $row['data'],
|
||||
'valuta' => $row['valuta'],
|
||||
'descrizione' => $row['descrizione'],
|
||||
'importo' => $row['importo'],
|
||||
'causale' => $row['causale'],
|
||||
'importo' => $row['importo'],
|
||||
'causale' => $row['causale'],
|
||||
'source_file' => $sourceFile,
|
||||
'raw_line' => $row['raw_line'],
|
||||
'row_hash' => $rowHash,
|
||||
'raw_line' => $row['raw_line'],
|
||||
'row_hash' => $rowHash,
|
||||
];
|
||||
|
||||
if (Schema::hasColumn('contabilita_movimenti_banca', 'descrizione_estesa')) {
|
||||
|
|
@ -838,9 +849,9 @@ public function importExcelXlsxForConto(
|
|||
? trim($row['descrizione_estesa'])
|
||||
: null;
|
||||
if (($descEst ?? '') === '' && is_string($row['raw_line'] ?? null) && str_contains((string) $row['raw_line'], '|')) {
|
||||
$parts = explode('|', (string) $row['raw_line'], 2);
|
||||
$parts = explode('|', (string) $row['raw_line'], 2);
|
||||
$fallback = trim((string) ($parts[1] ?? ''));
|
||||
$descEst = $fallback !== '' ? $fallback : null;
|
||||
$descEst = $fallback !== '' ? $fallback : null;
|
||||
}
|
||||
$payload['descrizione_estesa'] = $descEst !== '' ? $descEst : null;
|
||||
}
|
||||
|
|
@ -867,11 +878,11 @@ public function importExcelXlsxForConto(
|
|||
|
||||
private function appendMatchData(array $payload, ?string $descrizioneEstesa, ?string $fallbackRaw, ?string $fallbackDescrizione): array
|
||||
{
|
||||
$hasMatch = Schema::hasColumn('contabilita_movimenti_banca', 'match_data');
|
||||
$hasConfirm = Schema::hasColumn('contabilita_movimenti_banca', 'da_confermare');
|
||||
$hasMatch = Schema::hasColumn('contabilita_movimenti_banca', 'match_data');
|
||||
$hasConfirm = Schema::hasColumn('contabilita_movimenti_banca', 'da_confermare');
|
||||
$hasRubricaMitt = Schema::hasColumn('contabilita_movimenti_banca', 'rubrica_mittente_id');
|
||||
$hasRubricaBen = Schema::hasColumn('contabilita_movimenti_banca', 'rubrica_beneficiario_id');
|
||||
$hasFornitore = Schema::hasColumn('contabilita_movimenti_banca', 'fornitore_id');
|
||||
$hasRubricaBen = Schema::hasColumn('contabilita_movimenti_banca', 'rubrica_beneficiario_id');
|
||||
$hasFornitore = Schema::hasColumn('contabilita_movimenti_banca', 'fornitore_id');
|
||||
|
||||
if (! $hasMatch && ! $hasConfirm && ! $hasRubricaMitt && ! $hasRubricaBen && ! $hasFornitore) {
|
||||
return $payload;
|
||||
|
|
@ -892,16 +903,16 @@ private function appendMatchData(array $payload, ?string $descrizioneEstesa, ?st
|
|||
$payload['match_data'] = $match;
|
||||
}
|
||||
|
||||
$mittente = $match['mittente'] ?? null;
|
||||
$mittente = $match['mittente'] ?? null;
|
||||
$beneficiario = $match['beneficiario'] ?? null;
|
||||
|
||||
$rubricaMittenteId = null;
|
||||
$rubricaMittenteId = null;
|
||||
$rubricaBeneficiarioId = null;
|
||||
$fornitoreId = null;
|
||||
$fornitoreId = null;
|
||||
|
||||
if ($mittente) {
|
||||
$rubricaMittenteId = $this->matchRubricaId($mittente);
|
||||
$fornitoreId = $this->matchFornitoreId($mittente);
|
||||
$fornitoreId = $this->matchFornitoreId($mittente);
|
||||
}
|
||||
|
||||
if ($beneficiario) {
|
||||
|
|
@ -919,7 +930,7 @@ private function appendMatchData(array $payload, ?string $descrizioneEstesa, ?st
|
|||
}
|
||||
|
||||
if ($hasConfirm) {
|
||||
$needs = ! empty($match) && ! $rubricaMittenteId && ! $fornitoreId;
|
||||
$needs = ! empty($match) && ! $rubricaMittenteId && ! $fornitoreId;
|
||||
$payload['da_confermare'] = $needs;
|
||||
}
|
||||
|
||||
|
|
@ -928,9 +939,11 @@ private function appendMatchData(array $payload, ?string $descrizioneEstesa, ?st
|
|||
|
||||
private function parseDescrizioneEstesa(string $text): array
|
||||
{
|
||||
$res = [];
|
||||
$res = [];
|
||||
$text = trim(preg_replace('/\s+/', ' ', $text));
|
||||
if ($text === '') return $res;
|
||||
if ($text === '') {
|
||||
return $res;
|
||||
}
|
||||
|
||||
if (preg_match('/COD\.\s*DISP\.:\s*([A-Z0-9]+)/i', $text, $m)) {
|
||||
$res['cod_disp'] = trim($m[1]);
|
||||
|
|
@ -991,7 +1004,10 @@ private function parseDescrizioneEstesa(string $text): array
|
|||
private function matchRubricaId(string $name): ?int
|
||||
{
|
||||
$term = trim($name);
|
||||
if ($term === '') return null;
|
||||
if ($term === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$like = '%' . str_replace(['%', '_'], ['\%', '\_'], $term) . '%';
|
||||
|
||||
$rows = RubricaUniversale::query()
|
||||
|
|
@ -1008,7 +1024,10 @@ private function matchRubricaId(string $name): ?int
|
|||
private function matchFornitoreId(string $name): ?int
|
||||
{
|
||||
$term = trim($name);
|
||||
if ($term === '') return null;
|
||||
if ($term === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$like = '%' . str_replace(['%', '_'], ['\%', '\_'], $term) . '%';
|
||||
|
||||
$rows = Fornitore::query()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Contabilita;
|
||||
|
||||
use Carbon\Carbon;
|
||||
|
|
@ -12,18 +11,18 @@ class MpsQifParser
|
|||
public function parse(string $content): array
|
||||
{
|
||||
$content = str_replace(["\r\n", "\r"], "\n", $content);
|
||||
$lines = array_values(array_filter(explode("\n", $content), static fn(string $line): bool => trim($line) !== ''));
|
||||
$lines = array_values(array_filter(explode("\n", $content), static fn(string $line): bool => trim($line) !== ''));
|
||||
|
||||
if ($lines === []) {
|
||||
throw new \RuntimeException('File QIF vuoto.');
|
||||
}
|
||||
|
||||
$type = null;
|
||||
$rows = [];
|
||||
$type = null;
|
||||
$rows = [];
|
||||
$transaction = [];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = preg_replace('/^\xEF\xBB\xBF/', '', $line) ?? $line;
|
||||
$line = preg_replace('/^\xEF\xBB\xBF/', '', $line) ?? $line;
|
||||
$trimmed = trim($line);
|
||||
|
||||
if ($trimmed === '') {
|
||||
|
|
@ -44,7 +43,7 @@ public function parse(string $content): array
|
|||
continue;
|
||||
}
|
||||
|
||||
$code = substr($trimmed, 0, 1);
|
||||
$code = substr($trimmed, 0, 1);
|
||||
$value = trim(substr($trimmed, 1));
|
||||
|
||||
if (! isset($transaction[$code])) {
|
||||
|
|
@ -68,7 +67,7 @@ public function parse(string $content): array
|
|||
return [
|
||||
'rows' => $rows,
|
||||
'meta' => [
|
||||
'format' => 'mps_qif',
|
||||
'format' => 'mps_qif',
|
||||
'qif_type' => $type,
|
||||
],
|
||||
];
|
||||
|
|
@ -80,17 +79,17 @@ public function parse(string $content): array
|
|||
*/
|
||||
private function buildRow(array $transaction, ?string $type): ?array
|
||||
{
|
||||
$date = $this->parseDate($transaction['D'][0] ?? null);
|
||||
$date = $this->parseDate($transaction['D'][0] ?? null);
|
||||
$amount = $this->parseMoney($transaction['T'][0] ?? null);
|
||||
|
||||
if (! $date || $amount === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$payee = $this->joinParts($transaction['P'] ?? []);
|
||||
$memo = $this->joinParts($transaction['M'] ?? []);
|
||||
$payee = $this->joinParts($transaction['P'] ?? []);
|
||||
$memo = $this->joinParts($transaction['M'] ?? []);
|
||||
$description = $payee !== '' ? $payee : ($memo !== '' ? mb_substr($memo, 0, 255) : 'Movimento QIF');
|
||||
$extended = trim($payee . ($payee !== '' && $memo !== '' ? ' · ' : '') . $memo);
|
||||
$extended = trim($payee . ($payee !== '' && $memo !== '' ? ' · ' : '') . $memo);
|
||||
|
||||
$rawLines = [];
|
||||
foreach ($transaction as $code => $values) {
|
||||
|
|
@ -100,13 +99,13 @@ private function buildRow(array $transaction, ?string $type): ?array
|
|||
}
|
||||
|
||||
return [
|
||||
'data' => $date,
|
||||
'valuta' => $date->copy(),
|
||||
'descrizione' => $description,
|
||||
'data' => $date,
|
||||
'valuta' => $date->copy(),
|
||||
'descrizione' => $description,
|
||||
'descrizione_estesa' => $extended !== '' ? $extended : null,
|
||||
'importo' => $amount,
|
||||
'causale' => $type ? ('QIF:' . strtoupper($type)) : 'QIF',
|
||||
'raw_line' => implode("\n", $rawLines),
|
||||
'importo' => $amount,
|
||||
'causale' => $type ? ('QIF:' . strtoupper($type)) : 'QIF',
|
||||
'raw_line' => implode("\n", $rawLines),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -159,4 +158,4 @@ private function parseMoney(?string $value): ?float
|
|||
|
||||
return is_numeric($value) ? (float) $value : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
761
app/Services/Contabilita/PreventivoOrdinarioPreviewService.php
Normal file
761
app/Services/Contabilita/PreventivoOrdinarioPreviewService.php
Normal file
|
|
@ -0,0 +1,761 @@
|
|||
<?php
|
||||
namespace App\Services\Contabilita;
|
||||
|
||||
use App\Models\PersonaUnitaRelazione;
|
||||
use App\Models\UnitaImmobiliareNominativo;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class PreventivoOrdinarioPreviewService
|
||||
{
|
||||
public function build(int $stabileId, int $annoGestione, ?int $gestioneId = null): array
|
||||
{
|
||||
$gestione = $this->resolveGestione($stabileId, $annoGestione, $gestioneId);
|
||||
$gestioneId = $gestione?->id ? (int) $gestione->id : $gestioneId;
|
||||
|
||||
if (! $gestioneId) {
|
||||
return [
|
||||
'gestione' => [
|
||||
'id' => null,
|
||||
'anno' => $annoGestione,
|
||||
'denominazione' => (string) ($gestione?->denominazione ?? ''),
|
||||
],
|
||||
'mesi_rate' => [],
|
||||
'preventivo_rows' => [],
|
||||
'preventivo_summary' => [],
|
||||
'tabella_options' => [],
|
||||
'riparto_columns' => [],
|
||||
'riparto_rows' => [],
|
||||
'focus_unit_id' => null,
|
||||
'focus_breakdown' => [],
|
||||
'people_audit' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$mesiRate = $this->normalizeMonths($gestione?->mesi_rate_ordinaria ?? null);
|
||||
$voci = $this->loadVoci($stabileId, $gestioneId);
|
||||
$tabellaOptions = $this->buildTabellaOptions($stabileId, $annoGestione);
|
||||
$tabellaMap = $tabellaOptions['map'];
|
||||
|
||||
$summaryByTable = $this->buildTableSummary($voci, $tabellaMap);
|
||||
$unitaRows = $this->loadUnitaRows($stabileId);
|
||||
$detailRows = $this->loadDetailRows($stabileId, $annoGestione);
|
||||
$peopleMap = $this->loadPeopleMap($stabileId, $unitaRows->pluck('id')->all());
|
||||
|
||||
$matrix = $this->buildMatrixRows($unitaRows, $summaryByTable, $detailRows, $peopleMap, $mesiRate);
|
||||
$focusUnitId = $this->resolveFocusUnitId($matrix);
|
||||
$focusBreakdown = $this->buildFocusBreakdown($matrix, $focusUnitId);
|
||||
|
||||
return [
|
||||
'gestione' => [
|
||||
'id' => $gestioneId,
|
||||
'anno' => $annoGestione,
|
||||
'denominazione' => (string) ($gestione?->denominazione ?? ''),
|
||||
],
|
||||
'mesi_rate' => $mesiRate,
|
||||
'preventivo_rows' => $voci->map(function (array $row) use ($tabellaMap): array {
|
||||
$tabella = $tabellaMap[$row['tabella_millesimale_default_id'] ?? 0] ?? null;
|
||||
|
||||
return [
|
||||
'id' => $row['id'],
|
||||
'codice' => $row['codice'],
|
||||
'descrizione' => $row['descrizione'],
|
||||
'tabella_millesimale_default_id' => $row['tabella_millesimale_default_id'],
|
||||
'tabella_codice' => $tabella['codice'] ?? '',
|
||||
'tabella_nome' => $tabella['label'] ?? '',
|
||||
'importo_default' => $row['importo_default'],
|
||||
'importo_consuntivo' => $row['importo_consuntivo'],
|
||||
'percentuale_inquilino' => $row['percentuale_inquilino'],
|
||||
'percentuale_condomino' => $row['percentuale_condomino'],
|
||||
'conto_pd' => $row['conto_pd'],
|
||||
'sottoconto_pd' => $row['sottoconto_pd'],
|
||||
];
|
||||
})->all(),
|
||||
'preventivo_summary' => array_values($summaryByTable),
|
||||
'tabella_options' => $tabellaOptions['options'],
|
||||
'riparto_columns' => array_values(array_map(fn(array $table): array=> [
|
||||
'codice' => $table['codice'],
|
||||
'label' => $table['codice'],
|
||||
'descrizione' => $table['label'],
|
||||
], $summaryByTable)),
|
||||
'riparto_rows' => $matrix->values()->all(),
|
||||
'focus_unit_id' => $focusUnitId,
|
||||
'focus_breakdown' => $focusBreakdown,
|
||||
'people_audit' => $this->buildPeopleAudit($matrix, $peopleMap),
|
||||
];
|
||||
}
|
||||
|
||||
private function resolveGestione(int $stabileId, int $annoGestione, ?int $gestioneId): ?object
|
||||
{
|
||||
if (! Schema::hasTable('gestioni_contabili')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$query = DB::table('gestioni_contabili')
|
||||
->where('stabile_id', $stabileId)
|
||||
->where('tipo_gestione', 'ordinaria');
|
||||
|
||||
if ($gestioneId) {
|
||||
$query->where('id', $gestioneId);
|
||||
} else {
|
||||
$query->where('anno_gestione', $annoGestione)
|
||||
->orderByDesc('gestione_attiva')
|
||||
->orderByDesc('id');
|
||||
}
|
||||
|
||||
return $query->first();
|
||||
}
|
||||
|
||||
private function normalizeMonths(mixed $raw): array
|
||||
{
|
||||
$months = [];
|
||||
|
||||
if (is_array($raw)) {
|
||||
$months = $raw;
|
||||
} elseif (is_string($raw) && $raw !== '') {
|
||||
$decoded = json_decode($raw, true);
|
||||
if (is_array($decoded)) {
|
||||
$months = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
$months = collect($months)
|
||||
->map(fn($month) => (int) $month)
|
||||
->filter(fn(int $month) => $month >= 1 && $month <= 12)
|
||||
->unique()
|
||||
->sort()
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return array_map(fn(int $month): array=> [
|
||||
'month' => $month,
|
||||
'label' => $this->monthLabel($month),
|
||||
], $months);
|
||||
}
|
||||
|
||||
private function buildTabellaOptions(int $stabileId, int $annoGestione): array
|
||||
{
|
||||
$query = DB::table('tabelle_millesimali')
|
||||
->where('stabile_id', $stabileId);
|
||||
|
||||
if (Schema::hasColumn('tabelle_millesimali', 'anno_gestione')) {
|
||||
$query->where('anno_gestione', $annoGestione);
|
||||
}
|
||||
|
||||
$rows = $query
|
||||
->orderByRaw('COALESCE(nord, ordine_visualizzazione, ordinamento, 999999)')
|
||||
->orderBy('codice_tabella')
|
||||
->get([
|
||||
'id',
|
||||
'codice_tabella',
|
||||
'denominazione',
|
||||
'totale_millesimi',
|
||||
'tipo_calcolo',
|
||||
'meta_legacy',
|
||||
'nord',
|
||||
'ordine_visualizzazione',
|
||||
'ordinamento',
|
||||
]);
|
||||
|
||||
$options = [];
|
||||
$map = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$metaLegacy = $this->decodeJson($row->meta_legacy ?? null);
|
||||
$code = trim((string) ($row->codice_tabella ?? ''));
|
||||
$label = trim((string) ($row->denominazione ?? ''));
|
||||
$display = $code !== '' ? $code . ' · ' . ($label !== '' ? $label : $code) : ($label !== '' ? $label : 'Tabella');
|
||||
$nord = null;
|
||||
|
||||
foreach ([$row->nord ?? null, $row->ordine_visualizzazione ?? null, $row->ordinamento ?? null] as $orderValue) {
|
||||
if (is_numeric($orderValue)) {
|
||||
$nord = (int) $orderValue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$options[(int) $row->id] = $display;
|
||||
$map[(int) $row->id] = [
|
||||
'id' => (int) $row->id,
|
||||
'codice' => $code,
|
||||
'label' => $label !== '' ? $label : $code,
|
||||
'totale_millesimi' => is_numeric($row->totale_millesimi ?? null) ? (float) $row->totale_millesimi : 0.0,
|
||||
'tipo_calcolo' => $row->tipo_calcolo,
|
||||
'legacy_tipo' => $metaLegacy['tipo'] ?? null,
|
||||
'nord' => $nord,
|
||||
];
|
||||
}
|
||||
|
||||
return ['options' => $options, 'map' => $map];
|
||||
}
|
||||
|
||||
private function loadVoci(int $stabileId, ?int $gestioneId): Collection
|
||||
{
|
||||
if (! Schema::hasTable('voci_spesa')) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('voci_spesa', 'gestione_contabile_id') && ! $gestioneId) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$query = DB::table('voci_spesa')->where('stabile_id', $stabileId);
|
||||
|
||||
if (Schema::hasColumn('voci_spesa', 'gestione_contabile_id') && $gestioneId) {
|
||||
$query->where('gestione_contabile_id', $gestioneId);
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('voci_spesa', 'tipo_gestione')) {
|
||||
$query->where('tipo_gestione', 'ordinaria');
|
||||
}
|
||||
|
||||
$rows = $query
|
||||
->orderByRaw('COALESCE(nord, ordinamento, 999999)')
|
||||
->orderBy('codice')
|
||||
->get([
|
||||
'id',
|
||||
'codice',
|
||||
'descrizione',
|
||||
'tabella_millesimale_default_id',
|
||||
'importo_default',
|
||||
'importo_consuntivo',
|
||||
'percentuale_condomino',
|
||||
'percentuale_inquilino',
|
||||
'conto_pd',
|
||||
'sottoconto_pd',
|
||||
]);
|
||||
|
||||
return $rows->map(fn(object $row): array=> [
|
||||
'id' => (int) $row->id,
|
||||
'codice' => (string) ($row->codice ?? ''),
|
||||
'descrizione' => (string) ($row->descrizione ?? ''),
|
||||
'tabella_millesimale_default_id' => $row->tabella_millesimale_default_id ? (int) $row->tabella_millesimale_default_id : 0,
|
||||
'importo_default' => is_numeric($row->importo_default ?? null) ? (float) $row->importo_default : 0.0,
|
||||
'importo_consuntivo' => is_numeric($row->importo_consuntivo ?? null) ? (float) $row->importo_consuntivo : 0.0,
|
||||
'percentuale_condomino' => is_numeric($row->percentuale_condomino ?? null) ? (float) $row->percentuale_condomino : 100.0,
|
||||
'percentuale_inquilino' => is_numeric($row->percentuale_inquilino ?? null) ? (float) $row->percentuale_inquilino : 0.0,
|
||||
'conto_pd' => (string) ($row->conto_pd ?? ''),
|
||||
'sottoconto_pd' => (string) ($row->sottoconto_pd ?? ''),
|
||||
]);
|
||||
}
|
||||
|
||||
private function buildTableSummary(Collection $voci, array $tabellaMap): array
|
||||
{
|
||||
return $voci
|
||||
->groupBy(fn(array $row): int => (int) ($row['tabella_millesimale_default_id'] ?? 0))
|
||||
->map(function (Collection $rows, int $tabellaId) use ($tabellaMap): array {
|
||||
$tabella = $tabellaMap[$tabellaId] ?? [
|
||||
'codice' => '',
|
||||
'label' => '',
|
||||
'tipo_calcolo' => null,
|
||||
'legacy_tipo' => null,
|
||||
'totale_millesimi' => 0.0,
|
||||
'nord' => 999999,
|
||||
];
|
||||
$totalePreventivo = (float) $rows->sum('importo_default');
|
||||
$totaleConsuntivo = (float) $rows->sum('importo_consuntivo');
|
||||
$quotaInquilini = (float) $rows->sum(function (array $row): float {
|
||||
return $row['importo_default'] * (($row['percentuale_inquilino'] ?? 0.0) / 100);
|
||||
});
|
||||
|
||||
return [
|
||||
'tabella_id' => $tabellaId,
|
||||
'codice' => $tabella['codice'],
|
||||
'label' => $tabella['label'],
|
||||
'tipo_calcolo' => $tabella['tipo_calcolo'],
|
||||
'legacy_tipo' => $tabella['legacy_tipo'],
|
||||
'nord' => $tabella['nord'] ?? 999999,
|
||||
'totale_millesimi' => $tabella['totale_millesimi'],
|
||||
'totale_preventivo' => $totalePreventivo,
|
||||
'totale_consuntivo' => $totaleConsuntivo,
|
||||
'quota_inquilini' => $quotaInquilini,
|
||||
'quota_condomini' => $totalePreventivo - $quotaInquilini,
|
||||
'voci' => $rows->values()->all(),
|
||||
];
|
||||
})
|
||||
->sortBy(fn(array $row): string => sprintf('%06d|%s', (int) ($row['nord'] ?? 999999), (string) ($row['codice'] ?? '')))
|
||||
->mapWithKeys(fn(array $row): array=> [$row['codice'] => $row])
|
||||
->all();
|
||||
}
|
||||
|
||||
private function loadUnitaRows(int $stabileId): Collection
|
||||
{
|
||||
$query = DB::table('unita_immobiliari')->where('stabile_id', $stabileId);
|
||||
|
||||
if (Schema::hasColumn('unita_immobiliari', 'deleted_at')) {
|
||||
$query->whereNull('deleted_at');
|
||||
}
|
||||
|
||||
return $query
|
||||
->orderBy('scala')
|
||||
->orderBy('piano')
|
||||
->orderBy('interno')
|
||||
->orderBy('id')
|
||||
->get([
|
||||
'id',
|
||||
'codice_unita',
|
||||
'denominazione',
|
||||
'scala',
|
||||
'piano',
|
||||
'interno',
|
||||
'stato_occupazione',
|
||||
'legacy_cond_id',
|
||||
])
|
||||
->map(fn(object $row): array=> [
|
||||
'id' => (int) $row->id,
|
||||
'label' => $this->buildUnitLabel($row),
|
||||
'sort_key' => $this->buildUnitSortKey($row),
|
||||
'stato_occupazione' => (string) ($row->stato_occupazione ?? ''),
|
||||
'legacy_cond_id' => $row->legacy_cond_id !== null ? (string) $row->legacy_cond_id : null,
|
||||
]);
|
||||
}
|
||||
|
||||
private function loadDetailRows(int $stabileId, int $annoGestione): Collection
|
||||
{
|
||||
if (! Schema::hasTable('dettaglio_millesimi') || ! Schema::hasTable('tabelle_millesimali')) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$query = DB::table('dettaglio_millesimi as dm')
|
||||
->join('tabelle_millesimali as tm', 'tm.id', '=', 'dm.tabella_millesimale_id')
|
||||
->where('tm.stabile_id', $stabileId);
|
||||
|
||||
if (Schema::hasColumn('tabelle_millesimali', 'anno_gestione')) {
|
||||
$query->where('tm.anno_gestione', $annoGestione);
|
||||
}
|
||||
|
||||
return $query
|
||||
->get([
|
||||
'dm.unita_immobiliare_id',
|
||||
'tm.codice_tabella',
|
||||
'tm.totale_millesimi',
|
||||
'dm.millesimi',
|
||||
'dm.valore_prev',
|
||||
'dm.ruolo_legacy',
|
||||
])
|
||||
->map(fn(object $row): array=> [
|
||||
'unit_id' => (int) $row->unita_immobiliare_id,
|
||||
'tabella_codice' => (string) ($row->codice_tabella ?? ''),
|
||||
'totale_millesimi' => is_numeric($row->totale_millesimi ?? null) ? (float) $row->totale_millesimi : 0.0,
|
||||
'millesimi' => is_numeric($row->millesimi ?? null) ? (float) $row->millesimi : 0.0,
|
||||
'valore_prev' => is_numeric($row->valore_prev ?? null) ? (float) $row->valore_prev : 0.0,
|
||||
'ruolo_legacy' => strtoupper(trim((string) ($row->ruolo_legacy ?? ''))),
|
||||
]);
|
||||
}
|
||||
|
||||
private function buildMatrixRows(Collection $unitaRows, array $summaryByTable, Collection $detailRows, array $peopleMap, array $mesiRate): Collection
|
||||
{
|
||||
$detailsByUnitTable = $detailRows->groupBy(fn(array $row): string => $row['unit_id'] . '|' . $row['tabella_codice']);
|
||||
|
||||
return $unitaRows->map(function (array $unit) use ($summaryByTable, $detailsByUnitTable, $peopleMap, $mesiRate): array {
|
||||
$columns = [];
|
||||
$breakdown = [];
|
||||
$totaleUnit = 0.0;
|
||||
$totaleCondomini = 0.0;
|
||||
$totaleInquilini = 0.0;
|
||||
$owners = $peopleMap[$unit['id']]['owners'] ?? [];
|
||||
$tenants = $peopleMap[$unit['id']]['tenants'] ?? [];
|
||||
|
||||
foreach ($summaryByTable as $tabellaCode => $table) {
|
||||
$detail = collect($detailsByUnitTable->get($unit['id'] . '|' . $tabellaCode, []));
|
||||
$computed = $this->computeUnitTableAllocation($table, $detail);
|
||||
|
||||
$columns[$tabellaCode] = $computed['totale'];
|
||||
$breakdown[$tabellaCode] = $computed;
|
||||
$totaleUnit += $computed['totale'];
|
||||
$totaleCondomini += $computed['condomini'];
|
||||
$totaleInquilini += $computed['inquilini'];
|
||||
}
|
||||
|
||||
return [
|
||||
'unit_id' => $unit['id'],
|
||||
'unit_label' => $unit['label'],
|
||||
'stato_occupazione' => $unit['stato_occupazione'],
|
||||
'people_summary' => $peopleMap[$unit['id']]['summary'] ?? 'Nessun soggetto collegato',
|
||||
'owners' => $owners,
|
||||
'tenants' => $tenants,
|
||||
'legacy_cond_id' => $unit['legacy_cond_id'],
|
||||
'columns' => $columns,
|
||||
'table_breakdown' => $breakdown,
|
||||
'totale_unita' => $totaleUnit,
|
||||
'totale_condomini' => $totaleCondomini,
|
||||
'totale_inquilini' => $totaleInquilini,
|
||||
'rate' => $this->splitAcrossMonths($totaleUnit, $mesiRate),
|
||||
];
|
||||
})->sort(function (array $left, array $right): int {
|
||||
return strnatcasecmp((string) ($left['sort_key'] ?? $left['unit_label'] ?? ''), (string) ($right['sort_key'] ?? $right['unit_label'] ?? ''));
|
||||
})->values();
|
||||
}
|
||||
|
||||
private function computeUnitTableAllocation(array $table, Collection $detailRows): array
|
||||
{
|
||||
$totaleTabella = (float) ($table['totale_preventivo'] ?? 0.0);
|
||||
$tabellaMillesimi = (float) ($table['totale_millesimi'] ?? 0.0);
|
||||
|
||||
$millesimiUnita = (float) $detailRows
|
||||
->filter(fn(array $row): bool => $row['ruolo_legacy'] !== 'I')
|
||||
->sum('millesimi');
|
||||
|
||||
$ratio = $tabellaMillesimi > 0 ? ($millesimiUnita / $tabellaMillesimi) : 0.0;
|
||||
$impProp = (float) $detailRows
|
||||
->filter(fn(array $row): bool => $row['ruolo_legacy'] !== 'I')
|
||||
->sum('valore_prev');
|
||||
$impInq = (float) $detailRows
|
||||
->filter(fn(array $row): bool => $row['ruolo_legacy'] === 'I')
|
||||
->sum('valore_prev');
|
||||
$impTot = $impProp + $impInq;
|
||||
$hasImporti = abs($impTot) > 0.00001;
|
||||
$useImportiSplit = $hasImporti && abs($impInq) > 0.00001;
|
||||
$quotaUnit = $hasImporti ? $impTot : ($totaleTabella * $ratio);
|
||||
|
||||
$quotaCondomini = 0.0;
|
||||
$quotaInquilini = 0.0;
|
||||
|
||||
if ($useImportiSplit) {
|
||||
$quotaCondomini = $impProp;
|
||||
$quotaInquilini = $impInq;
|
||||
} else {
|
||||
foreach ($table['voci'] as $voce) {
|
||||
$share = $totaleTabella != 0.0 ? ((float) $voce['importo_default'] / $totaleTabella) : 0.0;
|
||||
$quotaVoce = $hasImporti ? ($quotaUnit * $share) : ((float) $voce['importo_default'] * $ratio);
|
||||
$quotaInqVoce = $quotaVoce * (((float) ($voce['percentuale_inquilino'] ?? 0.0)) / 100);
|
||||
$quotaCondomini += $quotaVoce - $quotaInqVoce;
|
||||
$quotaInquilini += $quotaInqVoce;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'totale' => $quotaUnit,
|
||||
'condomini' => $quotaCondomini,
|
||||
'inquilini' => $quotaInquilini,
|
||||
'millesimi_unita' => $millesimiUnita,
|
||||
'millesimi_tabella' => $tabellaMillesimi,
|
||||
];
|
||||
}
|
||||
|
||||
private function loadPeopleMap(int $stabileId, array $unitIds): array
|
||||
{
|
||||
$map = [];
|
||||
|
||||
if ($unitIds === []) {
|
||||
return $map;
|
||||
}
|
||||
|
||||
if (Schema::hasTable('persone_unita_relazioni')) {
|
||||
$relations = PersonaUnitaRelazione::query()
|
||||
->with('persona')
|
||||
->whereIn('unita_id', $unitIds)
|
||||
->attive()
|
||||
->get();
|
||||
|
||||
foreach ($relations as $relation) {
|
||||
$unitId = (int) $relation->unita_id;
|
||||
$role = strtoupper((string) ($relation->ruolo_rate ?: PersonaUnitaRelazione::deriveRuoloRate($relation->tipo_relazione)));
|
||||
if (! in_array($role, ['C', 'I'], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$map[$unitId]['relations'][$role][] = [
|
||||
'name' => (string) ($relation->persona?->nome_display ?? $relation->persona?->nome_completo ?? 'Persona'),
|
||||
'quota' => is_numeric($relation->quota_relazione ?? null) ? (float) $relation->quota_relazione : null,
|
||||
'source' => 'relazione_attiva',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (Schema::hasTable('unita_immobiliare_nominativi')) {
|
||||
$today = now()->toDateString();
|
||||
$legacyRows = UnitaImmobiliareNominativo::query()
|
||||
->where('stabile_id', $stabileId)
|
||||
->whereIn('unita_immobiliare_id', $unitIds)
|
||||
->where(function ($query) use ($today): void {
|
||||
$query->whereNull('data_inizio')->orWhere('data_inizio', '<=', $today);
|
||||
})
|
||||
->where(function ($query) use ($today): void {
|
||||
$query->whereNull('data_fine')->orWhere('data_fine', '>=', $today);
|
||||
})
|
||||
->get();
|
||||
|
||||
foreach ($legacyRows as $row) {
|
||||
$unitId = (int) $row->unita_immobiliare_id;
|
||||
$role = strtoupper((string) ($row->ruolo ?? ''));
|
||||
if (! in_array($role, ['C', 'I'], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$map[$unitId]['legacy'][$role][] = [
|
||||
'name' => (string) ($row->nominativo ?? 'Nominativo legacy'),
|
||||
'quota' => is_numeric($row->percentuale ?? null) ? (float) $row->percentuale : null,
|
||||
'source' => (string) ($row->fonte ?? 'legacy'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($unitIds as $unitId) {
|
||||
$owners = $map[$unitId]['relations']['C'] ?? $map[$unitId]['legacy']['C'] ?? [];
|
||||
$tenants = $map[$unitId]['relations']['I'] ?? $map[$unitId]['legacy']['I'] ?? [];
|
||||
|
||||
$ownerNames = collect($owners)->pluck('name')->filter()->values()->all();
|
||||
$tenantNames = collect($tenants)->pluck('name')->filter()->values()->all();
|
||||
|
||||
$summary = [];
|
||||
if ($ownerNames !== []) {
|
||||
$summary[] = 'Prop: ' . implode(', ', $ownerNames);
|
||||
}
|
||||
if ($tenantNames !== []) {
|
||||
$summary[] = 'Inq: ' . implode(', ', $tenantNames);
|
||||
}
|
||||
|
||||
$map[$unitId]['owners'] = $owners;
|
||||
$map[$unitId]['tenants'] = $tenants;
|
||||
$map[$unitId]['summary'] = $summary === [] ? 'Nessun soggetto collegato' : implode(' · ', $summary);
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
private function buildPeopleAudit(Collection $matrix, array $peopleMap): array
|
||||
{
|
||||
return $matrix->map(function (array $row) use ($peopleMap): array {
|
||||
$people = $peopleMap[$row['unit_id']] ?? ['owners' => [], 'tenants' => [], 'relations' => [], 'legacy' => []];
|
||||
|
||||
$owners = $people['owners'] ?? [];
|
||||
$tenants = $people['tenants'] ?? [];
|
||||
$ownerQuota = $this->sumDeclaredQuota($owners);
|
||||
$tenantQuota = $this->sumDeclaredQuota($tenants);
|
||||
$flags = [];
|
||||
|
||||
if ($owners === []) {
|
||||
$flags[] = 'Manca proprietario attivo';
|
||||
}
|
||||
if ($row['stato_occupazione'] === 'occupata_inquilino' && $tenants === []) {
|
||||
$flags[] = 'Unità occupata da inquilino senza inquilino attivo';
|
||||
}
|
||||
if ($owners !== [] && $ownerQuota !== null && abs($ownerQuota - 100.0) > 0.01) {
|
||||
$flags[] = 'Quote proprietari ' . number_format($ownerQuota, 2, ',', '.') . '%';
|
||||
}
|
||||
if (($people['relations']['C'] ?? []) === [] && ($people['legacy']['C'] ?? []) !== []) {
|
||||
$flags[] = 'Solo nominativi legacy per proprietari';
|
||||
}
|
||||
if (($people['relations']['I'] ?? []) === [] && ($people['legacy']['I'] ?? []) !== []) {
|
||||
$flags[] = 'Solo nominativi legacy per inquilini';
|
||||
}
|
||||
|
||||
return [
|
||||
'unit_id' => $row['unit_id'],
|
||||
'unit_label' => $row['unit_label'],
|
||||
'owners' => implode(', ', array_map(fn(array $item): string => $item['name'], $owners)),
|
||||
'tenants' => implode(', ', array_map(fn(array $item): string => $item['name'], $tenants)),
|
||||
'owner_quota' => $ownerQuota,
|
||||
'tenant_quota' => $tenantQuota,
|
||||
'flags' => $flags,
|
||||
];
|
||||
})->all();
|
||||
}
|
||||
|
||||
private function buildFocusBreakdown(Collection $matrix, ?int $focusUnitId): array
|
||||
{
|
||||
if (! $focusUnitId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$row = $matrix->firstWhere('unit_id', $focusUnitId);
|
||||
if (! is_array($row)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$subjects = [];
|
||||
$ownerShares = $this->normalizePeopleShares($row['table_breakdown'], 'owners');
|
||||
$tenantShares = $this->normalizePeopleShares($row['table_breakdown'], 'tenants');
|
||||
|
||||
foreach ($ownerShares as $share) {
|
||||
$subjects[] = $share;
|
||||
}
|
||||
foreach ($tenantShares as $share) {
|
||||
$subjects[] = $share;
|
||||
}
|
||||
|
||||
return $subjects;
|
||||
}
|
||||
|
||||
private function normalizePeopleShares(array $tableBreakdown, string $bucket): array
|
||||
{
|
||||
$first = reset($tableBreakdown);
|
||||
if (! is_array($first) || ! isset($first[$bucket]) || ! is_array($first[$bucket])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$people = $first[$bucket];
|
||||
$totalBase = $bucket === 'owners' ? 'condomini' : 'inquilini';
|
||||
$shares = $this->normalizeShares($people);
|
||||
$rows = [];
|
||||
|
||||
foreach ($shares as $person) {
|
||||
$totale = 0.0;
|
||||
$columns = [];
|
||||
|
||||
foreach ($tableBreakdown as $code => $values) {
|
||||
$base = (float) ($values[$totalBase] ?? 0.0);
|
||||
$quota = $base * $person['share'];
|
||||
$columns[$code] = $quota;
|
||||
$totale += $quota;
|
||||
}
|
||||
|
||||
$rows[] = [
|
||||
'name' => $person['name'],
|
||||
'role' => $bucket === 'owners' ? 'Condomino' : 'Inquilino',
|
||||
'quota_percent' => $person['share'] * 100,
|
||||
'columns' => $columns,
|
||||
'totale' => $totale,
|
||||
];
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
private function normalizeShares(array $people): array
|
||||
{
|
||||
if ($people === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$declared = collect($people)->sum(fn(array $row): float => is_numeric($row['quota'] ?? null) ? (float) $row['quota'] : 0.0);
|
||||
$count = count($people);
|
||||
|
||||
return array_map(function (array $row) use ($declared, $count): array {
|
||||
$share = $declared > 0
|
||||
? (((float) ($row['quota'] ?? 0.0)) / $declared)
|
||||
: (1 / max($count, 1));
|
||||
|
||||
return [
|
||||
'name' => $row['name'],
|
||||
'share' => $share,
|
||||
];
|
||||
}, $people);
|
||||
}
|
||||
|
||||
private function sumDeclaredQuota(array $people): ?float
|
||||
{
|
||||
$declared = collect($people)
|
||||
->filter(fn(array $row): bool => is_numeric($row['quota'] ?? null))
|
||||
->sum(fn(array $row): float => (float) $row['quota']);
|
||||
|
||||
return $declared > 0 ? $declared : null;
|
||||
}
|
||||
|
||||
private function splitAcrossMonths(float $total, array $mesiRate): array
|
||||
{
|
||||
$count = count($mesiRate);
|
||||
if ($count === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$base = round($total / $count, 2);
|
||||
$out = [];
|
||||
$allocated = 0.0;
|
||||
|
||||
foreach ($mesiRate as $index => $month) {
|
||||
$amount = $index === ($count - 1) ? round($total - $allocated, 2) : $base;
|
||||
$allocated += $amount;
|
||||
$out[] = [
|
||||
'month' => $month['month'],
|
||||
'label' => $month['label'],
|
||||
'amount' => $amount,
|
||||
];
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
private function resolveFocusUnitId(Collection $matrix): ?int
|
||||
{
|
||||
$first = $matrix->first();
|
||||
|
||||
return is_array($first) ? (int) ($first['unit_id'] ?? 0) : null;
|
||||
}
|
||||
|
||||
private function buildUnitLabel(object $row): string
|
||||
{
|
||||
$parts = [];
|
||||
|
||||
$scala = trim((string) ($row->scala ?? ''));
|
||||
$piano = trim((string) ($row->piano ?? ''));
|
||||
$interno = trim((string) ($row->interno ?? ''));
|
||||
$denominazione = trim((string) ($row->denominazione ?? ''));
|
||||
$codice = trim((string) ($row->codice_unita ?? ''));
|
||||
|
||||
if ($scala !== '') {
|
||||
$parts[] = 'Scala ' . $scala;
|
||||
}
|
||||
if ($piano !== '') {
|
||||
$parts[] = 'Piano ' . $piano;
|
||||
}
|
||||
if ($interno !== '') {
|
||||
$parts[] = 'Int. ' . $interno;
|
||||
}
|
||||
|
||||
$label = implode(' · ', $parts);
|
||||
if ($label === '') {
|
||||
$label = $denominazione !== '' ? $denominazione : ($codice !== '' ? $codice : 'Unità #' . $row->id);
|
||||
}
|
||||
|
||||
if ($codice !== '') {
|
||||
$label .= ' [' . $codice . ']';
|
||||
}
|
||||
|
||||
return $label;
|
||||
}
|
||||
|
||||
private function buildUnitSortKey(object $row): string
|
||||
{
|
||||
return implode('|', [
|
||||
$this->normalizeNaturalToken((string) ($row->scala ?? '')),
|
||||
$this->normalizeNaturalToken((string) ($row->piano ?? '')),
|
||||
$this->normalizeNaturalToken((string) ($row->interno ?? '')),
|
||||
$this->normalizeNaturalToken((string) ($row->denominazione ?? '')),
|
||||
str_pad((string) ((int) ($row->id ?? 0)), 10, '0', STR_PAD_LEFT),
|
||||
]);
|
||||
}
|
||||
|
||||
private function normalizeNaturalToken(string $value): string
|
||||
{
|
||||
$normalized = strtoupper(trim($value));
|
||||
|
||||
return preg_replace('/[\s\.\-\/]+/', '', $normalized) ?? '';
|
||||
}
|
||||
|
||||
private function monthLabel(int $month): string
|
||||
{
|
||||
return [
|
||||
1 => 'Gen',
|
||||
2 => 'Feb',
|
||||
3 => 'Mar',
|
||||
4 => 'Apr',
|
||||
5 => 'Mag',
|
||||
6 => 'Giu',
|
||||
7 => 'Lug',
|
||||
8 => 'Ago',
|
||||
9 => 'Set',
|
||||
10 => 'Ott',
|
||||
11 => 'Nov',
|
||||
12 => 'Dic',
|
||||
][$month] ?? (string) $month;
|
||||
}
|
||||
|
||||
private function decodeJson(mixed $value): array
|
||||
{
|
||||
if (is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (is_string($value) && $value !== '') {
|
||||
$decoded = json_decode($value, true);
|
||||
if (is_array($decoded)) {
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,8 @@
|
|||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('dettaglio_millesimi') || ! Schema::hasColumn('dettaglio_millesimi', 'ruolo_legacy')) {
|
||||
|
|
@ -46,4 +47,4 @@ public function down(): void
|
|||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('contabilita_saldi_conti')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('contabilita_saldi_conti', function (Blueprint $table) {
|
||||
if (! Schema::hasColumn('contabilita_saldi_conti', 'periodo_da')) {
|
||||
$table->date('periodo_da')->nullable()->after('data_saldo');
|
||||
}
|
||||
if (! Schema::hasColumn('contabilita_saldi_conti', 'periodo_a')) {
|
||||
$table->date('periodo_a')->nullable()->after('periodo_da');
|
||||
}
|
||||
if (! Schema::hasColumn('contabilita_saldi_conti', 'tipo_estratto')) {
|
||||
$table->string('tipo_estratto', 50)->nullable()->after('saldo');
|
||||
}
|
||||
if (! Schema::hasColumn('contabilita_saldi_conti', 'documento_stabile_id')) {
|
||||
$table->foreignId('documento_stabile_id')
|
||||
->nullable()
|
||||
->after('note')
|
||||
->constrained('documenti_stabili')
|
||||
->nullOnDelete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasTable('contabilita_saldi_conti')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('contabilita_saldi_conti', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('contabilita_saldi_conti', 'documento_stabile_id')) {
|
||||
$table->dropConstrainedForeignId('documento_stabile_id');
|
||||
}
|
||||
if (Schema::hasColumn('contabilita_saldi_conti', 'tipo_estratto')) {
|
||||
$table->dropColumn('tipo_estratto');
|
||||
}
|
||||
if (Schema::hasColumn('contabilita_saldi_conti', 'periodo_a')) {
|
||||
$table->dropColumn('periodo_a');
|
||||
}
|
||||
if (Schema::hasColumn('contabilita_saldi_conti', 'periodo_da')) {
|
||||
$table->dropColumn('periodo_da');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -42454,3 +42454,80 @@ ### Prossimo step operativo
|
|||
### Prompt di ripartenza
|
||||
|
||||
Riparti da docs/ai/SESSION_HANDOFF.md, usa l'ultimo CHECKPOINT, continua dal "Prossimo step operativo" senza rifare analisi generale.
|
||||
|
||||
---
|
||||
|
||||
## CHECKPOINT 2026-04-18 08:46:35
|
||||
|
||||
### Stato rapido
|
||||
- Branch: main
|
||||
- Ultimo commit: 09022d5 2026-04-16 Stabilize supplier ops and strict legacy sync
|
||||
|
||||
### File modificati (git status --short)
|
||||
```
|
||||
M CHANGELOG.md
|
||||
M app/Console/Commands/GesconSyncOrdinariaPreventivoCommand.php
|
||||
M app/Console/Commands/TecnoRepairImportRubricaClientiCommand.php
|
||||
M app/Filament/Pages/Contabilita/CasseBancheMovimenti.php
|
||||
M app/Filament/Pages/Contabilita/CasseBancheRiepilogo.php
|
||||
M app/Filament/Pages/Contabilita/SaldiContiArchivio.php
|
||||
MM app/Filament/Pages/Contabilita/SituazioneIniziale.php
|
||||
M app/Filament/Pages/Contabilita/VociSpesaArchivio.php
|
||||
M app/Filament/Pages/Fornitore/Contabilita.php
|
||||
M app/Filament/Pages/Gescon/Ordinarie.php
|
||||
M app/Filament/Pages/Strumenti/PostIt.php
|
||||
M app/Filament/Pages/Supporto/TicketAcqua.php
|
||||
M app/Livewire/Condomini/StabileDatiBancariTable.php
|
||||
M app/Livewire/Filament/TopbarLiveCall.php
|
||||
M app/Models/DocumentoStabile.php
|
||||
M app/Modules/Contabilita/Models/SaldoConto.php
|
||||
M app/Providers/Filament/AdminFilamentPanelProvider.php
|
||||
M app/Services/Contabilita/MovimentiBancaImporter.php
|
||||
M app/Services/Contabilita/MpsQifParser.php
|
||||
M database/migrations/2026_04_15_220000_update_dettaglio_millesimi_unique_for_ruolo_legacy.php
|
||||
M resources/views/filament/pages/contabilita/casse-banche-movimenti.blade.php
|
||||
MM resources/views/filament/pages/contabilita/situazione-iniziale.blade.php
|
||||
M resources/views/filament/pages/contabilita/voci-spesa-prospetto.blade.php
|
||||
M resources/views/filament/pages/gescon/section.blade.php
|
||||
M resources/views/filament/pages/strumenti/post-it.blade.php
|
||||
M resources/views/filament/pages/supporto/ticket-acqua-light.blade.php
|
||||
M resources/views/livewire/filament/topbar-live-call.blade.php
|
||||
?? app/Services/Contabilita/PreventivoOrdinarioPreviewService.php
|
||||
?? database/migrations/2026_04_17_120000_add_statement_metadata_to_contabilita_saldi_conti.php
|
||||
```
|
||||
|
||||
### Ultimi file toccati (max 20)
|
||||
```
|
||||
CHANGELOG.md
|
||||
app/Console/Commands/GesconSyncOrdinariaPreventivoCommand.php
|
||||
app/Console/Commands/TecnoRepairImportRubricaClientiCommand.php
|
||||
app/Filament/Pages/Contabilita/CasseBancheMovimenti.php
|
||||
app/Filament/Pages/Contabilita/CasseBancheRiepilogo.php
|
||||
app/Filament/Pages/Contabilita/SaldiContiArchivio.php
|
||||
app/Filament/Pages/Contabilita/SituazioneIniziale.php
|
||||
app/Filament/Pages/Contabilita/VociSpesaArchivio.php
|
||||
app/Filament/Pages/Fornitore/Contabilita.php
|
||||
app/Filament/Pages/Gescon/Ordinarie.php
|
||||
app/Filament/Pages/Strumenti/PostIt.php
|
||||
app/Filament/Pages/Supporto/TicketAcqua.php
|
||||
app/Livewire/Condomini/StabileDatiBancariTable.php
|
||||
app/Livewire/Filament/TopbarLiveCall.php
|
||||
app/Models/DocumentoStabile.php
|
||||
app/Modules/Contabilita/Models/SaldoConto.php
|
||||
app/Providers/Filament/AdminFilamentPanelProvider.php
|
||||
app/Services/Contabilita/MovimentiBancaImporter.php
|
||||
app/Services/Contabilita/MpsQifParser.php
|
||||
database/migrations/2026_04_15_220000_update_dettaglio_millesimi_unique_for_ruolo_legacy.php
|
||||
```
|
||||
|
||||
### Bug trovati / verificati in questa sessione
|
||||
-
|
||||
|
||||
### Decisioni prese
|
||||
-
|
||||
|
||||
### Prossimo step operativo
|
||||
-
|
||||
|
||||
### Prompt di ripartenza
|
||||
Riparti da docs/ai/SESSION_HANDOFF.md, usa l'ultimo CHECKPOINT, continua dal "Prossimo step operativo" senza rifare analisi generale.
|
||||
|
|
|
|||
|
|
@ -7,98 +7,420 @@
|
|||
]"
|
||||
/>
|
||||
|
||||
@if(! $this->getActiveStabile())
|
||||
<x-filament::section>
|
||||
<div class="text-sm text-gray-600">Seleziona uno stabile per importare e verificare i movimenti banca.</div>
|
||||
</x-filament::section>
|
||||
@endif
|
||||
|
||||
@php
|
||||
$stabile = $this->getActiveStabile();
|
||||
$contiRows = $this->getHubContiRows();
|
||||
$contiOptions = $this->getContiImportabili();
|
||||
$hdr = $this->getHeaderSaldoInfo();
|
||||
$periodo = $this->periodoTotali;
|
||||
$periodoOptions = $this->periodoOptions;
|
||||
$periodoRateOptions = $this->periodoRateOptions;
|
||||
$periodoRate = $this->periodoRateTotali;
|
||||
$contoImport = $this->getSelectedContoImportInfo();
|
||||
$snapshotCards = $this->getSaldoSnapshotCards();
|
||||
$saldoArchivio = $this->getSaldoArchivioByYear();
|
||||
$ricStats = $this->getRiconciliazioneStats();
|
||||
$ricQueue = $this->getRiconciliazioneQueue();
|
||||
$ricCurrent = $this->getRiconciliazioneCurrent();
|
||||
$ricCandidates = $this->getRiconciliazioneCandidates();
|
||||
@endphp
|
||||
|
||||
<x-filament::section>
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-sm font-semibold">Import unico estratti</div>
|
||||
<div class="mt-1 text-xs text-gray-600">
|
||||
Seleziona il conto o la cassa corretta e importa il file nel formato relativo. Il collegamento al conto resta esplicito, senza inferenze dal file.
|
||||
@if(! $stabile)
|
||||
<x-filament::section>
|
||||
<div class="text-sm text-gray-600">Seleziona uno stabile per importare e verificare i movimenti banca.</div>
|
||||
</x-filament::section>
|
||||
@else
|
||||
<x-filament::section>
|
||||
<div class="grid grid-cols-1 gap-4 xl:grid-cols-[minmax(0,1.4fr)_minmax(320px,0.8fr)]">
|
||||
<div>
|
||||
<div class="text-sm text-gray-500">Stabile attivo</div>
|
||||
<div class="text-lg font-semibold text-gray-900">
|
||||
{{ $stabile->codice_operatore ?? $stabile->codice_stabile ?? '—' }} — {{ $stabile->denominazione ?? 'Stabile' }}
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-gray-600">Hub operativo del conto corrente: conti, saldi ufficiali, movimenti importati e riconciliazione.</div>
|
||||
</div>
|
||||
@if($contoImport)
|
||||
<div class="mt-2 text-xs text-gray-700">
|
||||
Conto selezionato: <span class="font-medium">{{ $contoImport['label'] }}</span>
|
||||
|
||||
<div class="rounded-xl border bg-slate-50 p-4">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-slate-500">Conto attivo</div>
|
||||
<div class="mt-2">
|
||||
<select wire:model.live="contoId" class="w-full rounded-lg border-gray-300 text-sm">
|
||||
<option value="">Seleziona conto</option>
|
||||
@foreach($contiOptions as $conto)
|
||||
<option value="{{ $conto['id'] }}">{{ $conto['label'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="mt-3 text-sm text-slate-700">
|
||||
@if($contoImport)
|
||||
<div class="font-medium">{{ $contoImport['label'] }}</div>
|
||||
@else
|
||||
<div>Nessun conto selezionato.</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<x-filament::button size="sm" color="primary" type="button" wire:click="mountAction('importa_estratto_unificato')">
|
||||
Importa estratto
|
||||
</x-filament::button>
|
||||
<x-filament::button size="sm" color="gray" type="button" wire:click="mountAction('registra_saldo_estratto')">
|
||||
Registra saldo
|
||||
</x-filament::button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
||||
<x-filament::tabs>
|
||||
<x-filament::tabs.item :active="$this->hubTab === 'conti'" wire:click="goToHubTab('conti')">Conti</x-filament::tabs.item>
|
||||
<x-filament::tabs.item :active="$this->hubTab === 'saldi'" wire:click="goToHubTab('saldi')">Saldi e estratti</x-filament::tabs.item>
|
||||
<x-filament::tabs.item :active="$this->hubTab === 'movimenti'" wire:click="goToHubTab('movimenti')">Movimenti</x-filament::tabs.item>
|
||||
<x-filament::tabs.item :active="$this->hubTab === 'riconciliazione'" wire:click="goToHubTab('riconciliazione')">Riconciliazione</x-filament::tabs.item>
|
||||
</x-filament::tabs>
|
||||
|
||||
@if($this->hubTab === 'conti')
|
||||
<x-filament::section>
|
||||
<x-slot name="heading">Conti dello stabile</x-slot>
|
||||
<x-slot name="description">Vista generale equivalente a “Casse e banche”, ma dentro l'hub operativo del conto corrente.</x-slot>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
@forelse($contiRows as $row)
|
||||
<div @class([
|
||||
'rounded-xl border p-4',
|
||||
'border-primary-300 bg-primary-50/40' => $row['selected'],
|
||||
'bg-white' => ! $row['selected'],
|
||||
])>
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-slate-900">{{ $row['label'] }}</div>
|
||||
<div class="mt-1 text-xs text-slate-600">
|
||||
@if($row['snapshot_data'])
|
||||
Ultimo saldo ufficiale: {{ $row['snapshot_data'] }}
|
||||
@if($row['snapshot_tipo'])
|
||||
· {{ $row['snapshot_tipo'] }}
|
||||
@endif
|
||||
@else
|
||||
Nessun saldo ufficiale registrato.
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@if($row['selected'])
|
||||
<span class="rounded-full bg-primary-600 px-2.5 py-1 text-[11px] font-semibold text-white">Attivo</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid grid-cols-2 gap-3 text-sm">
|
||||
<div class="rounded-lg border bg-white p-3">
|
||||
<div class="text-xs text-slate-500">Saldo attuale</div>
|
||||
<div class="mt-1 text-lg font-semibold text-slate-900">€ {{ number_format((float) ($row['saldo_attuale'] ?? 0), 2, ',', '.') }}</div>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-white p-3">
|
||||
<div class="text-xs text-slate-500">Movimenti importati</div>
|
||||
<div class="mt-1 text-lg font-semibold text-slate-900">{{ $row['movimenti_count'] ?? 0 }}</div>
|
||||
<div class="text-xs text-slate-500">Ultima data: {{ $row['ultima_movimentazione'] ?? '—' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
<x-filament::button size="sm" color="gray" wire:click="selectContoAndTab({{ $row['id'] }}, 'saldi')">Saldi / estratti</x-filament::button>
|
||||
<x-filament::button size="sm" color="primary" wire:click="selectContoAndTab({{ $row['id'] }}, 'movimenti')">Movimenti</x-filament::button>
|
||||
<x-filament::button size="sm" color="success" wire:click="selectContoAndTab({{ $row['id'] }}, 'riconciliazione')">Riconciliazione</x-filament::button>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="rounded-lg border border-dashed bg-white p-4 text-sm text-slate-500 lg:col-span-2">Nessun conto configurato per lo stabile attivo.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</x-filament::section>
|
||||
@elseif($this->hubTab === 'saldi')
|
||||
<x-filament::section>
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-lg font-semibold text-slate-900">Archivio saldi ufficiali ed estratti conto</div>
|
||||
<div class="mt-1 text-sm text-slate-600">Vista annuale del conto selezionato, separata dal mastrino dei movimenti.</div>
|
||||
@if($contoImport)
|
||||
<div class="mt-2 text-xs font-medium text-slate-700">Conto: {{ $contoImport['label'] }}</div>
|
||||
@endif
|
||||
</div>
|
||||
<x-filament::button type="button" color="primary" wire:click="mountAction('registra_saldo_estratto')">
|
||||
Nuovo saldo / estratto
|
||||
</x-filament::button>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 space-y-6">
|
||||
@forelse($saldoArchivio as $yearBlock)
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="text-base font-semibold text-slate-900">Anno {{ $yearBlock['year'] }}</div>
|
||||
<div class="text-xs text-slate-500">{{ count($yearBlock['rows']) }} estratti / saldi</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="text-left text-slate-500">
|
||||
<tr>
|
||||
<th class="py-2 pr-4">Data saldo</th>
|
||||
<th class="py-2 pr-4">Periodo</th>
|
||||
<th class="py-2 pr-4">Tipo</th>
|
||||
<th class="py-2 pr-4 text-right">Saldo</th>
|
||||
<th class="py-2 pr-4">Documento</th>
|
||||
<th class="py-2 pr-4">Note</th>
|
||||
<th class="py-2 pr-0 text-right">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
@foreach($yearBlock['rows'] as $row)
|
||||
<tr>
|
||||
<td class="py-3 pr-4 font-medium text-slate-900">{{ $row['data'] }}</td>
|
||||
<td class="py-3 pr-4">
|
||||
<div>{{ $row['periodo_breve'] }}</div>
|
||||
<div class="text-xs text-slate-500">{{ $row['periodo'] }}</div>
|
||||
</td>
|
||||
<td class="py-3 pr-4">{{ $row['tipo'] }}</td>
|
||||
<td class="py-3 pr-4 text-right font-semibold">€ {{ number_format((float) $row['saldo'], 2, ',', '.') }}</td>
|
||||
<td class="py-3 pr-4">
|
||||
@if($row['documento_url'] && $row['documento_label'])
|
||||
<a href="{{ $row['documento_url'] }}" target="_blank" class="text-primary-600 hover:underline">{{ $row['documento_label'] }}</a>
|
||||
@else
|
||||
<span class="text-slate-400">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="py-3 pr-4 text-slate-600">{{ $row['note'] !== '' ? $row['note'] : '—' }}</td>
|
||||
<td class="py-3 pr-0 text-right">
|
||||
<x-filament::button size="sm" color="gray" wire:click="openSaldoPeriodo({{ $row['id'] }})">Apri movimenti</x-filament::button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="rounded-lg border border-dashed bg-white p-4 text-sm text-slate-500">Nessun saldo ufficiale registrato per il conto selezionato.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</x-filament::section>
|
||||
@elseif($this->hubTab === 'riconciliazione')
|
||||
<x-filament::section>
|
||||
<x-slot name="heading">Riconciliazione guidata</x-slot>
|
||||
<x-slot name="description">Pre-riconciliazione automatica con candidati. L’operatore verifica un movimento alla volta e usa il mastrino dove il match non è certo.</x-slot>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3 xl:grid-cols-4">
|
||||
<div class="rounded-lg border bg-white p-3">
|
||||
<div class="text-xs text-slate-500">Totale movimenti</div>
|
||||
<div class="mt-1 text-lg font-semibold text-slate-900">{{ $ricStats['totale'] ?? 0 }}</div>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-white p-3">
|
||||
<div class="text-xs text-slate-500">Senza prima nota</div>
|
||||
<div class="mt-1 text-lg font-semibold text-slate-900">{{ $ricStats['senza_prima_nota'] ?? 0 }}</div>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-white p-3">
|
||||
<div class="text-xs text-slate-500">Già collegati</div>
|
||||
<div class="mt-1 text-lg font-semibold text-slate-900">{{ $ricStats['collegati'] ?? 0 }}</div>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-white p-3">
|
||||
<div class="text-xs text-slate-500">Da confermare</div>
|
||||
<div class="mt-1 text-lg font-semibold text-slate-900">{{ $ricStats['da_confermare'] ?? 0 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(! $contoImport)
|
||||
<div class="mt-4 rounded-lg border border-dashed bg-white p-4 text-sm text-slate-500">Seleziona un conto per avviare la riconciliazione.</div>
|
||||
@elseif(! $ricCurrent)
|
||||
<div class="mt-4 rounded-lg border border-dashed bg-white p-4 text-sm text-slate-500">Nessun movimento in coda. Il conto sembra già lavorato oppure i movimenti sono tutti collegati.</div>
|
||||
@else
|
||||
<div class="mt-4 grid grid-cols-1 gap-4 xl:grid-cols-[minmax(0,1fr)_minmax(340px,0.9fr)]">
|
||||
<div class="rounded-xl border bg-slate-50 p-4">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">Movimento in lavorazione</div>
|
||||
<div class="mt-1 text-sm text-slate-600">{{ ($this->riconciliazioneOffset ?? 0) + 1 }} di {{ count($ricQueue) }}</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<x-filament::button size="sm" color="gray" wire:click="previousRiconciliazione">Precedente</x-filament::button>
|
||||
<x-filament::button size="sm" color="gray" wire:click="nextRiconciliazione">Successivo</x-filament::button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 rounded-lg border bg-white p-4">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-slate-900">{{ $ricCurrent['data'] }}</div>
|
||||
<div class="mt-1 text-xs text-slate-500">Stato: {{ $ricCurrent['stato'] }}</div>
|
||||
</div>
|
||||
<div class="text-right text-lg font-semibold {{ $ricCurrent['importo'] >= 0 ? 'text-emerald-700' : 'text-rose-700' }}">
|
||||
€ {{ number_format((float) $ricCurrent['importo'], 2, ',', '.') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 text-sm text-slate-700">{{ $ricCurrent['descrizione'] }}</div>
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
<x-filament::button size="sm" color="primary" wire:click="goToHubTab('movimenti')">Apri nel mastrino</x-filament::button>
|
||||
@if($ricCurrent['has_registrazione'])
|
||||
<span class="rounded-full bg-emerald-100 px-3 py-1 text-xs font-medium text-emerald-700">Ha già una prima nota</span>
|
||||
@else
|
||||
<span class="rounded-full bg-amber-100 px-3 py-1 text-xs font-medium text-amber-700">Da collegare</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="text-sm font-semibold text-slate-900">Candidati automatici</div>
|
||||
<div class="mt-1 text-xs text-slate-600">
|
||||
@if($ricCurrent['importo'] >= 0)
|
||||
Match su incassi registrati per importo, data e testo della causale.
|
||||
@else
|
||||
Match su fatture fornitore per importo, data e denominazione del fornitore.
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mt-4 space-y-3">
|
||||
@forelse($ricCandidates as $candidate)
|
||||
<div class="rounded-lg border bg-slate-50 p-3">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">{{ $candidate['tipo'] }}</div>
|
||||
<div class="mt-1 text-sm font-semibold text-slate-900">{{ $candidate['titolo'] }}</div>
|
||||
</div>
|
||||
<div class="rounded-full bg-primary-100 px-2.5 py-1 text-xs font-semibold text-primary-700">{{ $candidate['score'] }}%</div>
|
||||
</div>
|
||||
<div class="mt-2 text-sm text-slate-700">{{ $candidate['dettaglio'] }}</div>
|
||||
<div class="mt-2 flex flex-wrap gap-4 text-xs text-slate-500">
|
||||
<span>Data: {{ $candidate['data'] }}</span>
|
||||
<span>Importo: € {{ number_format((float) $candidate['importo'], 2, ',', '.') }}</span>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-slate-600">{{ $candidate['motivo'] }}</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="rounded-lg border border-dashed bg-slate-50 p-4 text-sm text-slate-500">Nessun candidato convincente. In questo caso l’operatore lavora dal mastrino e conferma manualmente il collegamento.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<x-filament::button type="button" color="primary" wire:click="mountAction('importa_estratto_unificato')">
|
||||
Importa estratto
|
||||
</x-filament::button>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
</x-filament::section>
|
||||
@else
|
||||
<x-filament::section>
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-sm font-semibold">Import unico estratti</div>
|
||||
<div class="mt-1 text-xs text-gray-600">
|
||||
Seleziona il conto o la cassa corretta e importa il file nel formato relativo. Il collegamento al conto resta esplicito, senza inferenze dal file.
|
||||
</div>
|
||||
@if($contoImport)
|
||||
<div class="mt-2 text-xs text-gray-700">
|
||||
Conto selezionato: <span class="font-medium">{{ $contoImport['label'] }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<x-filament::button type="button" color="primary" wire:click="mountAction('importa_estratto_unificato')">
|
||||
Importa estratto
|
||||
</x-filament::button>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
||||
<x-filament::section>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="text-sm font-medium">
|
||||
{{ $hdr['label'] }}
|
||||
@if($hdr['saldo'] !== null && $hdr['data'])
|
||||
· saldo al: {{ $hdr['data'] }} € {{ number_format((float) $hdr['saldo'], 2, ',', '.') }}
|
||||
<x-filament::section>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="text-sm font-medium">
|
||||
{{ $hdr['label'] }}
|
||||
@if($hdr['saldo'] !== null && $hdr['data'])
|
||||
· saldo al: {{ $hdr['data'] }} € {{ number_format((float) $hdr['saldo'], 2, ',', '.') }}
|
||||
@endif
|
||||
</div>
|
||||
<div class="text-xs text-gray-600">Filtra per periodo/gestione dai filtri tabella.</div>
|
||||
</div>
|
||||
|
||||
@if($this->movimentiFocusFrom || $this->movimentiFocusTo)
|
||||
<div class="mt-4 flex flex-wrap items-center justify-between gap-3 rounded-lg border bg-amber-50 p-3 text-sm text-amber-900">
|
||||
<div>
|
||||
Filtro rapido attivo sul periodo
|
||||
<span class="font-semibold">{{ $this->movimentiFocusFrom ?: '—' }}</span>
|
||||
→
|
||||
<span class="font-semibold">{{ $this->movimentiFocusTo ?: '—' }}</span>
|
||||
</div>
|
||||
<x-filament::button size="sm" color="gray" wire:click="clearMovimentiFocus">Rimuovi focus</x-filament::button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="text-xs text-gray-600">Filtra per periodo/gestione dai filtri tabella.</div>
|
||||
</div>
|
||||
<div class="mt-4 grid grid-cols-1 gap-3 lg:grid-cols-4">
|
||||
<div class="rounded-lg border bg-white p-3">
|
||||
<div class="text-xs text-gray-500">Periodo</div>
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
<select wire:model="periodoTipo" class="rounded border-gray-300 text-xs">
|
||||
<option value="mese">Mese</option>
|
||||
<option value="trimestre">Trimestre</option>
|
||||
</select>
|
||||
<select wire:model="periodoValore" class="rounded border-gray-300 text-xs">
|
||||
@foreach(($periodoOptions[$periodoTipo] ?? []) as $val => $label)
|
||||
<option value="{{ $val }}">{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<input type="number" wire:model="periodoAnno" class="w-24 rounded border-gray-300 text-xs" min="2000" max="2100" />
|
||||
|
||||
<div class="mt-4 grid grid-cols-1 gap-3 lg:grid-cols-4">
|
||||
<div class="rounded-lg border bg-white p-3">
|
||||
<div class="text-xs text-gray-500">Periodo</div>
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
<select wire:model="periodoTipo" class="rounded border-gray-300 text-xs">
|
||||
<option value="mese">Mese</option>
|
||||
<option value="trimestre">Trimestre</option>
|
||||
</select>
|
||||
<select wire:model="periodoValore" class="rounded border-gray-300 text-xs">
|
||||
@foreach(($periodoOptions[$periodoTipo] ?? []) as $val => $label)
|
||||
<option value="{{ $val }}">{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<input type="number" wire:model="periodoAnno" class="w-24 rounded border-gray-300 text-xs" min="2000" max="2100" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-white p-3">
|
||||
<div class="text-xs text-gray-500">Totali periodo ({{ $periodo['label'] ?? '' }})</div>
|
||||
<div class="mt-2 text-xs">
|
||||
<div>Movimenti: <span class="font-semibold">{{ $periodo['count'] ?? 0 }}</span></div>
|
||||
<div>Entrate: <span class="font-semibold">€ {{ number_format((float)($periodo['entrate'] ?? 0), 2, ',', '.') }}</span></div>
|
||||
<div>Uscite: <span class="font-semibold">€ {{ number_format((float)($periodo['uscite'] ?? 0), 2, ',', '.') }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-white p-3">
|
||||
<div class="text-xs text-gray-500">Periodo rate (senza anno)</div>
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
<select wire:model="periodoRate" class="rounded border-gray-300 text-xs">
|
||||
<option value="">—</option>
|
||||
@foreach($periodoRateOptions as $val => $label)
|
||||
<option value="{{ $val }}">{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="mt-2 text-xs">
|
||||
<div>Movimenti: <span class="font-semibold">{{ $periodoRate['count'] ?? 0 }}</span></div>
|
||||
<div>Entrate: <span class="font-semibold">€ {{ number_format((float)($periodoRate['entrate'] ?? 0), 2, ',', '.') }}</span></div>
|
||||
<div>Uscite: <span class="font-semibold">€ {{ number_format((float)($periodoRate['uscite'] ?? 0), 2, ',', '.') }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-white p-3">
|
||||
<div class="text-xs text-gray-500">Saldo periodo</div>
|
||||
<div class="mt-3 text-lg font-semibold">€ {{ number_format((float)($periodo['saldo'] ?? 0), 2, ',', '.') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-white p-3">
|
||||
<div class="text-xs text-gray-500">Totali periodo ({{ $periodo['label'] ?? '' }})</div>
|
||||
<div class="mt-2 text-xs">
|
||||
<div>Movimenti: <span class="font-semibold">{{ $periodo['count'] ?? 0 }}</span></div>
|
||||
<div>Entrate: <span class="font-semibold">€ {{ number_format((float)($periodo['entrate'] ?? 0), 2, ',', '.') }}</span></div>
|
||||
<div>Uscite: <span class="font-semibold">€ {{ number_format((float)($periodo['uscite'] ?? 0), 2, ',', '.') }}</span></div>
|
||||
|
||||
<div class="mt-4 rounded-lg border bg-slate-50 p-3">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-slate-900">Snapshot ufficiali / estratti conto</div>
|
||||
<div class="text-xs text-slate-600">I saldi ufficiali ancorano il ricalcolo del conto anche quando il conto non ha IBAN.</div>
|
||||
</div>
|
||||
<x-filament::button type="button" size="sm" color="gray" wire:click="mountAction('registra_saldo_estratto')">
|
||||
Registra saldo / estratto
|
||||
</x-filament::button>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
@forelse($snapshotCards as $card)
|
||||
<div class="rounded-lg border bg-white p-3 text-sm">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="font-medium text-slate-900">{{ $card['tipo'] }}</div>
|
||||
<div class="text-xs text-slate-500">{{ $card['data'] ?? '—' }}</div>
|
||||
</div>
|
||||
<div class="mt-2 text-lg font-semibold text-slate-900">€ {{ number_format((float) ($card['saldo'] ?? 0), 2, ',', '.') }}</div>
|
||||
<div class="mt-1 text-xs text-slate-600">{{ $card['periodo'] }}</div>
|
||||
@if(!empty($card['documento_url']) && !empty($card['documento_label']))
|
||||
<a href="{{ $card['documento_url'] }}" target="_blank" class="mt-2 inline-flex text-xs font-medium text-indigo-700 hover:underline">{{ $card['documento_label'] }}</a>
|
||||
@endif
|
||||
</div>
|
||||
@empty
|
||||
<div class="rounded-lg border border-dashed bg-white p-3 text-sm text-slate-500 md:col-span-2 xl:col-span-3">Nessuno snapshot ufficiale registrato per il conto selezionato.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-white p-3">
|
||||
<div class="text-xs text-gray-500">Periodo rate (senza anno)</div>
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
<select wire:model="periodoRate" class="rounded border-gray-300 text-xs">
|
||||
<option value="">—</option>
|
||||
@foreach($periodoRateOptions as $val => $label)
|
||||
<option value="{{ $val }}">{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
<div class="mt-4">
|
||||
{{ $this->table }}
|
||||
</div>
|
||||
<div class="mt-2 text-xs">
|
||||
<div>Movimenti: <span class="font-semibold">{{ $periodoRate['count'] ?? 0 }}</span></div>
|
||||
<div>Entrate: <span class="font-semibold">€ {{ number_format((float)($periodoRate['entrate'] ?? 0), 2, ',', '.') }}</span></div>
|
||||
<div>Uscite: <span class="font-semibold">€ {{ number_format((float)($periodoRate['uscite'] ?? 0), 2, ',', '.') }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-white p-3">
|
||||
<div class="text-xs text-gray-500">Saldo periodo</div>
|
||||
<div class="mt-3 text-lg font-semibold">€ {{ number_format((float)($periodo['saldo'] ?? 0), 2, ',', '.') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
{{ $this->table }}
|
||||
</div>
|
||||
</x-filament::section>
|
||||
</x-filament::section>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</x-filament-panels::page>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
@endif
|
||||
|
||||
<div class="w-64">
|
||||
<div class="text-xs text-gray-500 mb-1">Periodo legacy</div>
|
||||
<div class="text-xs text-gray-500 mb-1">Esercizio archivio</div>
|
||||
<x-filament::input.wrapper>
|
||||
<select name="legacy_year" class="w-full bg-transparent">
|
||||
<option value="">Tutti</option>
|
||||
|
|
@ -57,40 +57,35 @@
|
|||
<x-filament::tabs>
|
||||
<x-filament::tabs.item
|
||||
:active="$tab === 'bilancio'"
|
||||
:href="\App\Filament\Pages\Contabilita\SituazioneIniziale::getUrl(panel: 'admin-filament', parameters: array_merge($query, ['tab' => 'bilancio', 'dett' => null]))"
|
||||
wire:click.prevent="$set('tab','bilancio'); $set('dett', null)"
|
||||
wire:click="$set('tab', 'bilancio'); $set('dett', null)"
|
||||
>
|
||||
Bilancio
|
||||
</x-filament::tabs.item>
|
||||
|
||||
<x-filament::tabs.item
|
||||
:active="$tab === 'crediti'"
|
||||
:href="\App\Filament\Pages\Contabilita\SituazioneIniziale::getUrl(panel: 'admin-filament', parameters: array_merge($query, ['tab' => 'crediti', 'dett' => null]))"
|
||||
wire:click.prevent="$set('tab','crediti'); $set('dett', null)"
|
||||
wire:click="$set('tab', 'crediti'); $set('dett', null)"
|
||||
>
|
||||
Crediti
|
||||
</x-filament::tabs.item>
|
||||
|
||||
<x-filament::tabs.item
|
||||
:active="$tab === 'debiti'"
|
||||
:href="\App\Filament\Pages\Contabilita\SituazioneIniziale::getUrl(panel: 'admin-filament', parameters: array_merge($query, ['tab' => 'debiti', 'dett' => null]))"
|
||||
wire:click.prevent="$set('tab','debiti'); $set('dett', null)"
|
||||
wire:click="$set('tab', 'debiti'); $set('dett', null)"
|
||||
>
|
||||
Debiti
|
||||
</x-filament::tabs.item>
|
||||
|
||||
<x-filament::tabs.item
|
||||
:active="$tab === 'conguagli'"
|
||||
:href="\App\Filament\Pages\Contabilita\SituazioneIniziale::getUrl(panel: 'admin-filament', parameters: array_merge($query, ['tab' => 'conguagli', 'dett' => null]))"
|
||||
wire:click.prevent="$set('tab','conguagli'); $set('dett', null)"
|
||||
wire:click="$set('tab', 'conguagli'); $set('dett', null)"
|
||||
>
|
||||
Conguagli (Excel)
|
||||
Conguagli apertura
|
||||
</x-filament::tabs.item>
|
||||
|
||||
<x-filament::tabs.item
|
||||
:active="$tab === 'dettaglio'"
|
||||
:href="\App\Filament\Pages\Contabilita\SituazioneIniziale::getUrl(panel: 'admin-filament', parameters: array_merge($query, ['tab' => 'dettaglio']))"
|
||||
wire:click.prevent="$set('tab','dettaglio')"
|
||||
wire:click="$set('tab', 'dettaglio')"
|
||||
>
|
||||
Dettaglio
|
||||
</x-filament::tabs.item>
|
||||
|
|
@ -148,7 +143,10 @@
|
|||
@forelse(($detailRows ?? []) as $r)
|
||||
<tr>
|
||||
<td class="py-2 pr-4">{{ $r['cod_tab'] }}</td>
|
||||
<td class="py-2 pr-4">{{ $r['id_cond'] }}</td>
|
||||
<td class="py-2 pr-4">
|
||||
<div class="font-medium">{{ $r['id_cond'] }}</div>
|
||||
<div class="text-xs text-gray-500">{{ $r['cond_label'] ?? '' }}</div>
|
||||
</td>
|
||||
<td class="py-2 pr-4">{{ $r['cond_inquil'] ?: '—' }}</td>
|
||||
<td class="py-2 pr-4">{{ $r['mm'] !== null ? number_format((float) $r['mm'], 3, ',', '.') : '—' }}</td>
|
||||
<td class="py-2 pr-4">{{ $r['n_stra'] ?? '—' }}</td>
|
||||
|
|
@ -168,95 +166,19 @@
|
|||
</x-filament::section>
|
||||
@elseif ($tab === 'conguagli')
|
||||
<x-filament::section>
|
||||
<x-slot name="heading">Conguagli (stile Excel)</x-slot>
|
||||
<x-slot name="description">Raggruppati per condomino e per tabella CONG.* (include straordinarie quando presenti).</x-slot>
|
||||
|
||||
@php
|
||||
$manualConguagliEnabled = \Illuminate\Support\Facades\Schema::hasTable('contabilita_situazione_iniziale_conguagli');
|
||||
@endphp
|
||||
|
||||
@if ($manualConguagliEnabled)
|
||||
<form wire:submit.prevent="addConguaglioManuale" class="mb-4 grid grid-cols-1 md:grid-cols-4 gap-3">
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 mb-1">Tabella</div>
|
||||
<x-filament::input.wrapper>
|
||||
<select wire:model="formConguaglio.cod_tab" class="w-full bg-transparent">
|
||||
<option value="CONG.O">CONG.O (Ordinaria)</option>
|
||||
<option value="CONG.R">CONG.R (Riscaldamento)</option>
|
||||
<option value="CONG.S">CONG.S</option>
|
||||
<option value="CONG">CONG</option>
|
||||
</select>
|
||||
</x-filament::input.wrapper>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 mb-1">n_stra (opzionale)</div>
|
||||
<x-filament::input.wrapper>
|
||||
<input type="number" wire:model="formConguaglio.n_stra" class="w-full bg-transparent" />
|
||||
</x-filament::input.wrapper>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 mb-1">Condomino (id_cond)</div>
|
||||
<x-filament::input.wrapper>
|
||||
<input type="text" wire:model="formConguaglio.id_cond" class="w-full bg-transparent" />
|
||||
</x-filament::input.wrapper>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 mb-1">Consuntivo € (positivo=incasso, negativo=rimborso)</div>
|
||||
<x-filament::input.wrapper>
|
||||
<input type="text" wire:model="formConguaglio.cons_euro" class="w-full bg-transparent" placeholder="es. 123,45 oppure -50,00" />
|
||||
</x-filament::input.wrapper>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-4">
|
||||
<x-filament::button type="submit">Aggiungi conguaglio (manuale)</x-filament::button>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="text-left text-gray-500">
|
||||
<tr>
|
||||
<th class="py-2 pr-4">Condomino</th>
|
||||
@foreach(($conguagliExcelCols ?? []) as $c)
|
||||
<th class="py-2 pr-4 text-right whitespace-nowrap">{{ $c['label'] }}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
@forelse(($conguagliExcel ?? []) as $r)
|
||||
<tr>
|
||||
<td class="py-2 pr-4">
|
||||
<div class="font-medium">{{ $r['id_cond'] }}</div>
|
||||
<div class="text-xs text-gray-500">{{ $r['cond_label'] ?? '' }}</div>
|
||||
</td>
|
||||
@foreach(($conguagliExcelCols ?? []) as $c)
|
||||
@php $v = $r['cells'][$c['key']] ?? 0.0; @endphp
|
||||
<td class="py-2 pr-4 text-right">{{ $fmt($v) }}</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td class="py-3 text-gray-500" colspan="{{ 1 + count($conguagliExcelCols ?? []) }}">Nessun conguaglio disponibile.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<x-slot name="heading">Conguagli di apertura</x-slot>
|
||||
<x-slot name="description">Fonte unica: <span class="font-medium">gescon_import.dett_tab</span> con <span class="font-medium">cod_tab = CONG.O</span>. Qui manteniamo solo i residui iniziali ordinari del passaggio consegne.</x-slot>
|
||||
|
||||
<div class="mt-6">
|
||||
<div class="text-sm font-semibold mb-2">Modifica righe (dett_tab)</div>
|
||||
|
||||
<div class="text-xs text-gray-500 mb-2">Suggerimento: clicca una riga per modificarla (una alla volta).</div>
|
||||
|
||||
<form wire:submit.prevent="addDettTabRow" class="mb-3 grid grid-cols-1 md:grid-cols-6 gap-2">
|
||||
<form wire:submit.prevent="addDettTabRow" class="mb-3 grid grid-cols-1 md:grid-cols-5 gap-2">
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 mb-1">Tabella</div>
|
||||
<div class="text-xs text-gray-500 mb-1">Fonte</div>
|
||||
<x-filament::input.wrapper>
|
||||
<input type="text" wire:model="formDettTabAdd.cod_tab" class="w-full bg-transparent" />
|
||||
<input type="text" value="CONG.O" class="w-full bg-transparent" readonly />
|
||||
</x-filament::input.wrapper>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -265,12 +187,6 @@
|
|||
<input type="text" wire:model="formDettTabAdd.id_cond" class="w-full bg-transparent" />
|
||||
</x-filament::input.wrapper>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 mb-1">n_stra</div>
|
||||
<x-filament::input.wrapper>
|
||||
<input type="number" wire:model="formDettTabAdd.n_stra" class="w-full bg-transparent" />
|
||||
</x-filament::input.wrapper>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 mb-1">cons_euro</div>
|
||||
<x-filament::input.wrapper>
|
||||
|
|
@ -295,7 +211,6 @@
|
|||
<th class="py-2 pr-3">Tabella</th>
|
||||
<th class="py-2 pr-3">id_cond</th>
|
||||
<th class="py-2 pr-3">Condomino</th>
|
||||
<th class="py-2 pr-3">n_stra</th>
|
||||
<th class="py-2 pr-3 text-right">Cons. €</th>
|
||||
<th class="py-2 pr-3 text-right">MM</th>
|
||||
<th class="py-2 pr-3">Unico</th>
|
||||
|
|
@ -328,15 +243,6 @@
|
|||
<td class="py-2 pr-3">
|
||||
<div class="text-xs text-gray-500">{{ $r['cond_label'] ?? '' }}</div>
|
||||
</td>
|
||||
<td class="py-2 pr-3">
|
||||
@if($isEditing)
|
||||
<x-filament::input.wrapper>
|
||||
<input type="number" wire:model.defer="dettTabEdit.{{ $id }}.n_stra" class="w-full bg-transparent" wire:click.stop />
|
||||
</x-filament::input.wrapper>
|
||||
@else
|
||||
{{ $r['n_stra'] ?? '—' }}
|
||||
@endif
|
||||
</td>
|
||||
<td class="py-2 pr-3 text-right">
|
||||
@if($isEditing)
|
||||
<x-filament::input.wrapper>
|
||||
|
|
@ -374,7 +280,7 @@
|
|||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="8" class="py-3 text-gray-500">Nessuna riga dett_tab disponibile.</td>
|
||||
<td colspan="7" class="py-3 text-gray-500">Nessuna riga CONG.O disponibile.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ class="fi-input fi-input-wrp fi-select-input text-xs"
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border bg-white text-[11px] leading-tight">
|
||||
<div class="rounded-lg border bg-white text-[12px] leading-tight [&_th]:py-1.5 [&_td]:py-1.5 [&_.fi-ta-cell]:py-1 [&_.fi-ta-actions]:gap-1 [&_.fi-ta-actions]:whitespace-nowrap">
|
||||
{{ $this->table }}
|
||||
</div>
|
||||
|
||||
|
|
@ -100,8 +100,8 @@ class="fi-input fi-input-wrp fi-select-input text-xs"
|
|||
|
||||
@php($voci = is_array($focus['voci'] ?? null) ? $focus['voci'] : [])
|
||||
<div class="mt-2 overflow-x-auto">
|
||||
<table class="min-w-full text-xs">
|
||||
<thead class="text-xs text-gray-500">
|
||||
<table class="min-w-full text-[12px] leading-tight">
|
||||
<thead class="text-[11px] text-gray-500">
|
||||
<tr class="border-b">
|
||||
<th class="py-2 pr-3 text-left">Cod.</th>
|
||||
<th class="py-2 pr-3 text-left">Descrizione</th>
|
||||
|
|
@ -117,13 +117,13 @@ class="fi-input fi-input-wrp fi-select-input text-xs"
|
|||
@php($code = (string) ($v['codice'] ?? ''))
|
||||
@php($desc = (string) ($v['descrizione'] ?? ''))
|
||||
<tr>
|
||||
<td class="py-2 pr-3 text-gray-700 font-semibold">{{ $code !== '' ? $code : '—' }}</td>
|
||||
<td class="py-2 pr-3 text-gray-700">{{ $desc !== '' ? $desc : '—' }}</td>
|
||||
<td class="py-2 pr-3 text-right">{{ number_format((float) ($v['prev'] ?? 0), 2, ',', '.') }}</td>
|
||||
<td class="py-2 pr-3 text-right">{{ number_format((float) ($v['cons'] ?? 0), 2, ',', '.') }}</td>
|
||||
<td class="py-2 pr-3 text-right">{{ number_format((float) ($v['prop'] ?? 0), 2, ',', '.') }}</td>
|
||||
<td class="py-2 pr-3 text-right">{{ number_format((float) ($v['inq'] ?? 0), 2, ',', '.') }}</td>
|
||||
<td class="py-2 pr-0 text-right">{{ !empty($v['attiva']) ? 'Sì' : 'No' }}</td>
|
||||
<td class="py-1.5 pr-3 text-gray-700 font-semibold">{{ $code !== '' ? $code : '—' }}</td>
|
||||
<td class="py-1.5 pr-3 text-gray-700">{{ $desc !== '' ? $desc : '—' }}</td>
|
||||
<td class="py-1.5 pr-3 text-right">{{ number_format((float) ($v['prev'] ?? 0), 2, ',', '.') }}</td>
|
||||
<td class="py-1.5 pr-3 text-right">{{ number_format((float) ($v['cons'] ?? 0), 2, ',', '.') }}</td>
|
||||
<td class="py-1.5 pr-3 text-right">{{ number_format((float) ($v['prop'] ?? 0), 2, ',', '.') }}</td>
|
||||
<td class="py-1.5 pr-3 text-right">{{ number_format((float) ($v['inq'] ?? 0), 2, ',', '.') }}</td>
|
||||
<td class="py-1.5 pr-0 text-right">{{ !empty($v['attiva']) ? 'Sì' : 'No' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
|
|
@ -146,6 +146,24 @@ class="fi-input fi-input-wrp fi-select-input text-xs"
|
|||
<span class="text-gray-500">Totale Cons:</span>
|
||||
<span class="font-semibold">{{ number_format((float) $this->getTotaleConsuntivo(), 2, ',', '.') }}</span>
|
||||
</div>
|
||||
@if(($this->tipoGestioneTab ?? 'ordinaria') !== 'acqua' && ((float) $this->getTotalePreventivoAcqua() > 0 || (float) $this->getTotaleConsuntivoAcqua() > 0))
|
||||
<div>
|
||||
<span class="text-gray-500">ACQUA Prev:</span>
|
||||
<span class="font-semibold">{{ number_format((float) $this->getTotalePreventivoAcqua(), 2, ',', '.') }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-500">ACQUA Cons:</span>
|
||||
<span class="font-semibold">{{ number_format((float) $this->getTotaleConsuntivoAcqua(), 2, ',', '.') }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-500">Totale Gen. Prev:</span>
|
||||
<span class="font-semibold">{{ number_format((float) $this->getTotalePreventivoGenerale(), 2, ',', '.') }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-500">Totale Gen. Cons:</span>
|
||||
<span class="font-semibold">{{ number_format((float) $this->getTotaleConsuntivoGenerale(), 2, ',', '.') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -114,6 +114,21 @@
|
|||
wire:click="$set('viewTab','consuntivo')"
|
||||
>Consuntivo</x-filament::button>
|
||||
@if(! $isStraordPage)
|
||||
<x-filament::button
|
||||
size="sm"
|
||||
:color="$activeTab === 'preventivo' ? 'primary' : 'gray'"
|
||||
wire:click="$set('viewTab','preventivo')"
|
||||
>Preventivo</x-filament::button>
|
||||
<x-filament::button
|
||||
size="sm"
|
||||
:color="$activeTab === 'riparto' ? 'primary' : 'gray'"
|
||||
wire:click="$set('viewTab','riparto')"
|
||||
>Riparto</x-filament::button>
|
||||
<x-filament::button
|
||||
size="sm"
|
||||
:color="$activeTab === 'persone' ? 'primary' : 'gray'"
|
||||
wire:click="$set('viewTab','persone')"
|
||||
>Persone</x-filament::button>
|
||||
<x-filament::button
|
||||
size="sm"
|
||||
:color="$activeTab === 'straordinarie' ? 'primary' : 'gray'"
|
||||
|
|
@ -380,6 +395,330 @@
|
|||
</div>
|
||||
@endif
|
||||
|
||||
@if($activeTab === 'preventivo')
|
||||
@php
|
||||
$preventivoSummaryCollection = collect($this->preventivoSummaryRows ?? []);
|
||||
$preventivoRowsCollection = collect($this->preventivoEditorRows ?? []);
|
||||
$preventivoRowsGrouped = $preventivoSummaryCollection
|
||||
->map(function ($summary) use ($preventivoRowsCollection) {
|
||||
$code = (string) ($summary['codice'] ?? '');
|
||||
|
||||
return [
|
||||
'summary' => $summary,
|
||||
'rows' => $preventivoRowsCollection
|
||||
->filter(fn ($row) => (string) ($row['tabella_codice'] ?? '') === $code)
|
||||
->values()
|
||||
->all(),
|
||||
];
|
||||
})
|
||||
->filter(fn (array $group) => ! empty($group['rows']) || ! empty($group['summary']))
|
||||
->values();
|
||||
$preventivoOrdTotal = (float) $preventivoSummaryCollection
|
||||
->reject(fn ($summary) => strtoupper((string) ($summary['codice'] ?? '')) === 'ACQUA')
|
||||
->sum(fn ($summary) => (float) ($summary['totale_preventivo'] ?? 0));
|
||||
$preventivoAcquaTotal = (float) $preventivoSummaryCollection
|
||||
->filter(fn ($summary) => strtoupper((string) ($summary['codice'] ?? '')) === 'ACQUA')
|
||||
->sum(fn ($summary) => (float) ($summary['totale_preventivo'] ?? 0));
|
||||
$preventivoGeneralTotal = $preventivoOrdTotal + $preventivoAcquaTotal;
|
||||
@endphp
|
||||
<div class="mt-6 space-y-6">
|
||||
<div class="rounded-lg border border-blue-200 bg-blue-50 px-4 py-3 text-xs text-blue-900">
|
||||
<div class="font-semibold">Preventivo ordinario operativo</div>
|
||||
<div class="mt-1">
|
||||
Gestione {{ $this->preventivoGestioneMeta['denominazione'] ?? 'ordinaria' }}
|
||||
@if(!empty($this->preventivoGestioneMeta['anno']))
|
||||
· anno {{ $this->preventivoGestioneMeta['anno'] }}
|
||||
@endif
|
||||
· rate emesse su
|
||||
@if(!empty($this->ripartoRateMonths))
|
||||
{{ collect($this->ripartoRateMonths)->pluck('label')->implode(', ') }}
|
||||
@else
|
||||
mesi non configurati
|
||||
@endif
|
||||
</div>
|
||||
<div class="mt-1 text-[11px] text-blue-800">Visualizzazione ordinata per tabella, con ACQUA separata ma riportata anche nel totale finale.</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded-lg border">
|
||||
<div class="flex items-center justify-between gap-3 bg-gray-50 px-3 py-2 text-xs font-semibold text-gray-700">
|
||||
<span>Voci di spesa per tabella</span>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-xs">
|
||||
<thead class="bg-white">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left">Codice</th>
|
||||
<th class="px-3 py-2 text-left">Descrizione</th>
|
||||
<th class="px-3 py-2 text-left">Tabella</th>
|
||||
<th class="px-3 py-2 text-right">Preventivo €</th>
|
||||
<th class="px-3 py-2 text-right">Consuntivo €</th>
|
||||
<th class="px-3 py-2 text-right">% Inquilino</th>
|
||||
<th class="px-3 py-2 text-left">Conto PD</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($preventivoRowsGrouped as $group)
|
||||
@php
|
||||
$summary = $group['summary'] ?? [];
|
||||
@endphp
|
||||
<tr class="border-t bg-gray-50">
|
||||
<td class="px-3 py-2 font-semibold text-gray-900" colspan="7">
|
||||
{{ $summary['codice'] ?? '—' }}
|
||||
@if(!empty($summary['label']))
|
||||
<span class="font-normal text-gray-600">· {{ $summary['label'] }}</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@foreach(($group['rows'] ?? []) as $row)
|
||||
<tr class="border-t align-top">
|
||||
<td class="px-3 py-2 font-medium text-gray-900">{{ $row['codice'] }}</td>
|
||||
<td class="px-3 py-2">
|
||||
<div class="font-medium text-gray-900">{{ $row['descrizione'] }}</div>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-gray-600">{{ ($row['tabella_codice'] ?? '—') !== '' ? $row['tabella_codice'] : '—' }}</td>
|
||||
<td class="px-3 py-2 text-right tabular-nums">{{ number_format((float) ($row['importo_default'] ?? 0), 2, ',', '.') }}</td>
|
||||
<td class="px-3 py-2 text-right tabular-nums">{{ number_format((float) ($row['importo_consuntivo'] ?? 0), 2, ',', '.') }}</td>
|
||||
<td class="px-3 py-2 text-right tabular-nums">{{ number_format((float) ($row['percentuale_inquilino'] ?? 0), 2, ',', '.') }}</td>
|
||||
<td class="px-3 py-2 text-[11px] text-gray-600">
|
||||
{{ $row['conto_pd'] ?: '—' }}
|
||||
@if(!empty($row['sottoconto_pd']))
|
||||
<div>{{ $row['sottoconto_pd'] }}</div>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
<tr class="border-t bg-gray-50">
|
||||
<td class="px-3 py-2 font-semibold" colspan="3">Subtotale {{ $summary['codice'] ?? '—' }}</td>
|
||||
<td class="px-3 py-2 text-right font-semibold tabular-nums">{{ number_format((float) ($summary['totale_preventivo'] ?? 0), 2, ',', '.') }}</td>
|
||||
<td class="px-3 py-2 text-right font-semibold tabular-nums">{{ number_format((float) ($summary['totale_consuntivo'] ?? 0), 2, ',', '.') }}</td>
|
||||
<td class="px-3 py-2"></td>
|
||||
<td class="px-3 py-2"></td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="px-3 py-4 text-gray-500">Nessuna voce ordinaria trovata per la gestione attiva.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(!empty($this->preventivoSummaryRows))
|
||||
<div class="overflow-hidden rounded-lg border">
|
||||
<div class="bg-gray-50 px-3 py-2 text-xs font-semibold text-gray-700">Riepilogo per tabella</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-xs">
|
||||
<thead class="bg-white">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left">Tabella</th>
|
||||
<th class="px-3 py-2 text-left">Tipo</th>
|
||||
<th class="px-3 py-2 text-right">Preventivo €</th>
|
||||
<th class="px-3 py-2 text-right">Consuntivo €</th>
|
||||
<th class="px-3 py-2 text-right">Quota condomini €</th>
|
||||
<th class="px-3 py-2 text-right">Quota inquilini €</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($this->preventivoSummaryRows as $summary)
|
||||
<tr class="border-t">
|
||||
<td class="px-3 py-2">
|
||||
<div class="font-medium text-gray-900">{{ $summary['codice'] }}</div>
|
||||
<div class="text-[10px] text-gray-500">{{ $summary['label'] }}</div>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-gray-700">{{ $summary['legacy_tipo'] ?? $summary['tipo_calcolo'] ?? '—' }}</td>
|
||||
<td class="px-3 py-2 text-right tabular-nums">{{ number_format((float) $summary['totale_preventivo'], 2, ',', '.') }}</td>
|
||||
<td class="px-3 py-2 text-right tabular-nums">{{ number_format((float) $summary['totale_consuntivo'], 2, ',', '.') }}</td>
|
||||
<td class="px-3 py-2 text-right tabular-nums">{{ number_format((float) $summary['quota_condomini'], 2, ',', '.') }}</td>
|
||||
<td class="px-3 py-2 text-right tabular-nums">{{ number_format((float) $summary['quota_inquilini'], 2, ',', '.') }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
<tr class="border-t bg-gray-50">
|
||||
<td class="px-3 py-2 font-semibold" colspan="2">Totale ordinarie senza ACQUA</td>
|
||||
<td class="px-3 py-2 text-right font-semibold tabular-nums">{{ number_format((float) $preventivoOrdTotal, 2, ',', '.') }}</td>
|
||||
<td class="px-3 py-2"></td>
|
||||
<td class="px-3 py-2"></td>
|
||||
<td class="px-3 py-2"></td>
|
||||
</tr>
|
||||
@if($preventivoAcquaTotal > 0)
|
||||
<tr class="border-t bg-blue-50">
|
||||
<td class="px-3 py-2 font-semibold" colspan="2">ACQUA</td>
|
||||
<td class="px-3 py-2 text-right font-semibold tabular-nums">{{ number_format((float) $preventivoAcquaTotal, 2, ',', '.') }}</td>
|
||||
<td class="px-3 py-2"></td>
|
||||
<td class="px-3 py-2"></td>
|
||||
<td class="px-3 py-2"></td>
|
||||
</tr>
|
||||
@endif
|
||||
<tr class="border-t bg-gray-100">
|
||||
<td class="px-3 py-2 font-semibold" colspan="2">Totale generale</td>
|
||||
<td class="px-3 py-2 text-right font-semibold tabular-nums">{{ number_format((float) $preventivoGeneralTotal, 2, ',', '.') }}</td>
|
||||
<td class="px-3 py-2"></td>
|
||||
<td class="px-3 py-2"></td>
|
||||
<td class="px-3 py-2"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($activeTab === 'riparto')
|
||||
<div class="mt-6 space-y-6">
|
||||
<div class="rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-3 text-xs text-emerald-900">
|
||||
<div class="font-semibold">Ripartizione simulata per unità</div>
|
||||
<div class="mt-1">Sequenza applicata: totale voce → tabella/unità → quota condomini e quota inquilini → rate sui mesi configurati.</div>
|
||||
<div class="mt-1 text-[11px] text-emerald-800">Le colonne `TAB.A`, `TAB.B`, `ACQUA` ecc. sono il formato base stampabile richiesto per distribuire il riparto per unità immobiliari.</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded-lg border">
|
||||
<div class="bg-gray-50 px-3 py-2 text-xs font-semibold text-gray-700">Matrice ripartizione per unità</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-xs">
|
||||
<thead class="bg-white">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left">Unità</th>
|
||||
<th class="px-3 py-2 text-left">Soggetti</th>
|
||||
@foreach($this->ripartoPreviewColumns as $column)
|
||||
<th class="px-3 py-2 text-right">{{ $column['label'] }}</th>
|
||||
@endforeach
|
||||
<th class="px-3 py-2 text-right">Condomini €</th>
|
||||
<th class="px-3 py-2 text-right">Inquilini €</th>
|
||||
<th class="px-3 py-2 text-right">Totale €</th>
|
||||
@foreach($this->ripartoRateMonths as $month)
|
||||
<th class="px-3 py-2 text-right">{{ $month['label'] }}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($this->ripartoPreviewRows as $row)
|
||||
<tr class="border-t align-top">
|
||||
<td class="px-3 py-2">
|
||||
<div class="font-medium text-gray-900">{{ $row['unit_label'] }}</div>
|
||||
<div class="text-[10px] text-gray-500">{{ $row['stato_occupazione'] ?: 'occupazione non indicata' }}</div>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-[11px] text-gray-700">{{ $row['people_summary'] }}</td>
|
||||
@foreach($this->ripartoPreviewColumns as $column)
|
||||
<td class="px-3 py-2 text-right tabular-nums">{{ number_format((float) ($row['columns'][$column['codice']] ?? 0), 2, ',', '.') }}</td>
|
||||
@endforeach
|
||||
<td class="px-3 py-2 text-right tabular-nums">{{ number_format((float) $row['totale_condomini'], 2, ',', '.') }}</td>
|
||||
<td class="px-3 py-2 text-right tabular-nums">{{ number_format((float) $row['totale_inquilini'], 2, ',', '.') }}</td>
|
||||
<td class="px-3 py-2 text-right font-semibold tabular-nums">{{ number_format((float) $row['totale_unita'], 2, ',', '.') }}</td>
|
||||
@foreach($row['rate'] as $rate)
|
||||
<td class="px-3 py-2 text-right tabular-nums">{{ number_format((float) $rate['amount'], 2, ',', '.') }}</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="99" class="px-3 py-4 text-gray-500">Riparto non disponibile per lo stabile o per l'anno selezionato.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(!empty($this->ripartoPreviewRows))
|
||||
<div class="rounded-lg border">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 bg-gray-50 px-3 py-2 text-xs font-semibold text-gray-700">
|
||||
<span>Dettaglio soggetti per unità</span>
|
||||
<select wire:model.live="ripartoFocusUnitId" class="rounded border-gray-300 text-xs">
|
||||
@foreach($this->ripartoPreviewRows as $row)
|
||||
<option value="{{ $row['unit_id'] }}">{{ $row['unit_label'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-xs">
|
||||
<thead class="bg-white">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left">Soggetto</th>
|
||||
<th class="px-3 py-2 text-left">Ruolo</th>
|
||||
<th class="px-3 py-2 text-right">Quota %</th>
|
||||
@foreach($this->ripartoPreviewColumns as $column)
|
||||
<th class="px-3 py-2 text-right">{{ $column['label'] }}</th>
|
||||
@endforeach
|
||||
<th class="px-3 py-2 text-right">Totale €</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($this->ripartoFocusBreakdown as $subject)
|
||||
<tr class="border-t">
|
||||
<td class="px-3 py-2 font-medium text-gray-900">{{ $subject['name'] }}</td>
|
||||
<td class="px-3 py-2">{{ $subject['role'] }}</td>
|
||||
<td class="px-3 py-2 text-right tabular-nums">{{ number_format((float) $subject['quota_percent'], 2, ',', '.') }}</td>
|
||||
@foreach($this->ripartoPreviewColumns as $column)
|
||||
<td class="px-3 py-2 text-right tabular-nums">{{ number_format((float) ($subject['columns'][$column['codice']] ?? 0), 2, ',', '.') }}</td>
|
||||
@endforeach
|
||||
<td class="px-3 py-2 text-right font-semibold tabular-nums">{{ number_format((float) $subject['totale'], 2, ',', '.') }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="99" class="px-3 py-4 text-gray-500">Nessun soggetto disponibile per il dettaglio dell'unità selezionata.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($activeTab === 'persone')
|
||||
<div class="mt-6 space-y-4">
|
||||
<div class="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-xs text-amber-900">
|
||||
<div class="font-semibold">Controllo persone collegate alle unità</div>
|
||||
<div class="mt-1">Questo controllo evidenzia unità senza proprietario attivo, unità con solo nominativi legacy e unità occupate da inquilino senza un inquilino attivo collegato.</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded-lg border">
|
||||
<div class="bg-gray-50 px-3 py-2 text-xs font-semibold text-gray-700">Audit collegamenti stabile attivo</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-xs">
|
||||
<thead class="bg-white">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left">Unità</th>
|
||||
<th class="px-3 py-2 text-left">Proprietari</th>
|
||||
<th class="px-3 py-2 text-left">Inquilini</th>
|
||||
<th class="px-3 py-2 text-right">Quota prop. %</th>
|
||||
<th class="px-3 py-2 text-left">Segnalazioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($this->personeCollegateAudit as $audit)
|
||||
<tr class="border-t align-top">
|
||||
<td class="px-3 py-2 font-medium text-gray-900">{{ $audit['unit_label'] }}</td>
|
||||
<td class="px-3 py-2">{{ $audit['owners'] ?: '—' }}</td>
|
||||
<td class="px-3 py-2">{{ $audit['tenants'] ?: '—' }}</td>
|
||||
<td class="px-3 py-2 text-right tabular-nums">
|
||||
@if($audit['owner_quota'] !== null)
|
||||
{{ number_format((float) $audit['owner_quota'], 2, ',', '.') }}
|
||||
@else
|
||||
—
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
@if(!empty($audit['flags']))
|
||||
{{ implode(' · ', $audit['flags']) }}
|
||||
@else
|
||||
<span class="text-emerald-700">Nessuna anomalia immediata</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="px-3 py-4 text-gray-500">Nessun dato persone/unità disponibile per lo stabile attivo.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($activeTab === 'incassi')
|
||||
<div class="mt-6 overflow-hidden rounded-lg border">
|
||||
<div class="bg-gray-50 px-3 py-2 text-xs font-semibold text-gray-600">
|
||||
|
|
@ -823,7 +1162,7 @@
|
|||
<div class="font-semibold">€ {{ number_format((float)($acqua['totale'] ?? 0), 2, ',', '.') }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[10px] text-gray-500">Totale riparto (dett_tab.cons_euro)</div>
|
||||
<div class="text-[10px] text-gray-500">Totale riparto (dett_tab.prev_euro)</div>
|
||||
<div class="font-semibold">€ {{ number_format((float)($acqua['riparto_totale'] ?? 0), 2, ',', '.') }}</div>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -844,7 +1183,7 @@
|
|||
<th class="px-3 py-2 text-left">Scala</th>
|
||||
<th class="px-3 py-2 text-left">Interno</th>
|
||||
<th class="px-3 py-2 text-left">Piano</th>
|
||||
<th class="px-3 py-2 text-right">Consuntivo €</th>
|
||||
<th class="px-3 py-2 text-right">Preventivo €</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
|
|
@ -855,7 +1194,7 @@
|
|||
<td class="px-3 py-2">{{ $row['scala'] ?? '—' }}</td>
|
||||
<td class="px-3 py-2">{{ $row['interno'] ?? '—' }}</td>
|
||||
<td class="px-3 py-2">{{ $row['piano'] ?? '—' }}</td>
|
||||
<td class="px-3 py-2 text-right">{{ number_format((float)($row['cons_euro'] ?? 0), 2, ',', '.') }}</td>
|
||||
<td class="px-3 py-2 text-right">{{ number_format((float)($row['prev_euro'] ?? 0), 2, ',', '.') }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
|
|
@ -975,7 +1314,7 @@
|
|||
<th class="px-3 py-2 text-left">Scala</th>
|
||||
<th class="px-3 py-2 text-left">Interno</th>
|
||||
<th class="px-3 py-2 text-left">Piano</th>
|
||||
<th class="px-3 py-2 text-right">Consuntivo €</th>
|
||||
<th class="px-3 py-2 text-right">Preventivo €</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
|
|
@ -986,7 +1325,7 @@
|
|||
<td class="px-3 py-2">{{ $row['scala'] ?? '—' }}</td>
|
||||
<td class="px-3 py-2">{{ $row['interno'] ?? '—' }}</td>
|
||||
<td class="px-3 py-2">{{ $row['piano'] ?? '—' }}</td>
|
||||
<td class="px-3 py-2 text-right">{{ number_format((float)($row['cons_euro'] ?? 0), 2, ',', '.') }}</td>
|
||||
<td class="px-3 py-2 text-right">{{ number_format((float)($row['prev_euro'] ?? 0), 2, ',', '.') }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
|
|
|
|||
|
|
@ -120,25 +120,28 @@
|
|||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
@forelse($this->recenti as $riga)
|
||||
<button type="button" wire:click="selectPostIt({{ (int) $riga->id }})" class="block w-full rounded-lg border p-3 text-left {{ (int) $selectedPostItId === (int) $riga->id ? 'border-emerald-400 bg-emerald-50 ring-1 ring-emerald-200' : 'hover:bg-gray-50' }}">
|
||||
@forelse($this->recenti as $gruppo)
|
||||
@php($riga = $gruppo['primary'])
|
||||
<button type="button" wire:click="selectPostIt({{ (int) $riga->id }})" class="block w-full rounded-lg border p-3 text-left {{ ($gruppo['contains_selected'] ?? false) ? 'border-emerald-400 bg-emerald-50 ring-1 ring-emerald-200' : 'hover:bg-gray-50' }}">
|
||||
<div class="flex flex-wrap items-start justify-between gap-2">
|
||||
<div>
|
||||
<div class="font-medium">{{ $riga->oggetto ?: 'Senza oggetto' }}</div>
|
||||
<div class="font-medium">{{ $gruppo['caller_label'] ?: ($riga->oggetto ?: 'Senza oggetto') }}</div>
|
||||
<div class="text-sm text-gray-600">
|
||||
{{ $riga->nome_chiamante ?: 'Chiamante non identificato' }}
|
||||
@if($riga->telefono)
|
||||
· {{ $riga->telefono }}
|
||||
@if(!empty($gruppo['phone']))
|
||||
{{ $gruppo['phone'] }}
|
||||
@elseif($riga->nome_chiamante)
|
||||
{{ $riga->nome_chiamante }}
|
||||
@endif
|
||||
@if($riga->stabile)
|
||||
· {{ $riga->stabile->denominazione ?: ('Stabile #' . $riga->stabile->id) }}
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">{{ optional($riga->chiamata_il)->format('d/m/Y H:i') }}</div>
|
||||
<div class="text-xs text-gray-500">{{ $gruppo['latest_at'] ?? optional($riga->chiamata_il)->format('d/m/Y H:i') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex flex-wrap gap-2 text-[11px]">
|
||||
<span class="rounded-md bg-slate-100 px-2 py-1 text-slate-700">{{ (int) ($gruppo['count'] ?? 1) }} chiamate</span>
|
||||
<span class="rounded-md bg-slate-100 px-2 py-1 text-slate-700">{{ $riga->stato }}</span>
|
||||
<span class="rounded-md bg-blue-100 px-2 py-1 text-blue-800">{{ $riga->priorita }}</span>
|
||||
@if($riga->assegnazione_tipo)
|
||||
|
|
|
|||
|
|
@ -197,9 +197,9 @@
|
|||
);
|
||||
});
|
||||
},
|
||||
async buildClientMetadata(file) {
|
||||
async buildClientMetadata(file, source = 'camera') {
|
||||
const base = {
|
||||
source: 'camera',
|
||||
source,
|
||||
name: file?.name || '',
|
||||
size: file?.size || 0,
|
||||
captured_at: new Date(file?.lastModified || Date.now()).toISOString(),
|
||||
|
|
@ -222,7 +222,7 @@
|
|||
this.localWaterClientMetadata = Array.from(existing.values());
|
||||
this.persistClientMetadata();
|
||||
},
|
||||
async addFiles(event) {
|
||||
async addFiles(event, source = 'camera') {
|
||||
const batchId = Date.now();
|
||||
const files = Array.from(event?.target?.files || []);
|
||||
const mapped = files.map((file, index) => ({
|
||||
|
|
@ -235,7 +235,7 @@
|
|||
const existing = new Map(this.localWaterPreviews.map((item) => [item.key, item]));
|
||||
mapped.forEach((item) => existing.set(item.key, item));
|
||||
this.localWaterPreviews = Array.from(existing.values());
|
||||
this.mergeClientMetadata(await Promise.all(files.map((file) => this.buildClientMetadata(file))));
|
||||
this.mergeClientMetadata(await Promise.all(files.map((file) => this.buildClientMetadata(file, source))));
|
||||
this.persist();
|
||||
|
||||
if (event?.target) {
|
||||
|
|
@ -258,11 +258,17 @@
|
|||
x-on:ticket-acqua-draft-reset.window="clearAll()"
|
||||
>
|
||||
<span class="mb-2 block font-medium">Foto contatore</span>
|
||||
<label class="inline-flex cursor-pointer items-center rounded-md bg-emerald-600 px-3 py-2 text-xs font-medium text-white hover:bg-emerald-500">
|
||||
Scatta o aggiungi foto
|
||||
<input type="file" wire:model="pendingWaterPhotos" multiple accept="image/*" capture="environment" class="hidden" x-on:change="addFiles($event)" />
|
||||
</label>
|
||||
<div class="mt-1 text-xs text-gray-500">Massimo 4 foto. Se la camera taglia EXIF, salvo comunque GPS/data dal browser quando disponibili.</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<label class="inline-flex cursor-pointer items-center rounded-md bg-emerald-600 px-3 py-2 text-xs font-medium text-white hover:bg-emerald-500">
|
||||
Scatta foto
|
||||
<input type="file" wire:model="pendingWaterPhotos" multiple accept="image/*" capture="environment" class="hidden" x-on:change="addFiles($event, 'camera')" />
|
||||
</label>
|
||||
<label class="inline-flex cursor-pointer items-center rounded-md bg-sky-600 px-3 py-2 text-xs font-medium text-white hover:bg-sky-500">
|
||||
Aggiungi da libreria
|
||||
<input type="file" wire:model="pendingWaterLibraryPhotos" multiple accept="image/*" class="hidden" x-on:change="addFiles($event, 'attachment')" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-gray-500">Massimo 4 foto. Camera e libreria confluiscono nella stessa coda; quando disponibili salvo EXIF e coordinate GPS senza inventare metadati.</div>
|
||||
|
||||
@if(count($this->selectedWaterUploads) > 0)
|
||||
<div class="mt-4 grid gap-3 sm:grid-cols-2">
|
||||
|
|
@ -295,10 +301,28 @@
|
|||
<span class="inline-flex items-center rounded-full bg-sky-100 px-2 py-0.5 text-[10px] font-semibold text-sky-800">EXIF</span>
|
||||
@endif
|
||||
</div>
|
||||
@php
|
||||
$detailRows = [];
|
||||
if ($hasGps) {
|
||||
$detailRows[] = [
|
||||
'label' => 'Coordinate',
|
||||
'value' => number_format((float) $gpsLat, 5, '.', '') . ', ' . number_format((float) $gpsLng, 5, '.', ''),
|
||||
];
|
||||
}
|
||||
if ($exifDate !== '') {
|
||||
$detailRows[] = ['label' => 'Data EXIF', 'value' => $exifDate];
|
||||
}
|
||||
$device = trim((string) data_get($metadata, 'device', ''));
|
||||
if ($device !== '') {
|
||||
$detailRows[] = ['label' => 'Dispositivo', 'value' => $device];
|
||||
}
|
||||
@endphp
|
||||
@include('filament.pages.supporto.partials.photo-metadata', ['badgeRows' => [], 'detailRows' => $detailRows])
|
||||
<label class="mt-2 block">
|
||||
<span class="mb-1 block text-[11px] font-medium">Nota sotto la foto</span>
|
||||
<input type="text" wire:model.defer="waterPhotoDescriptions.{{ $upload['index'] }}" class="w-full rounded-lg border-gray-300" placeholder="Es. lettura chiara" />
|
||||
</label>
|
||||
<button type="button" wire:click="removeWaterPhoto({{ (int) $upload['index'] }})" class="mt-2 inline-flex items-center rounded-md bg-rose-600 px-2 py-1 text-[11px] font-medium text-white hover:bg-rose-500">Rimuovi</button>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
|
@ -312,7 +336,7 @@
|
|||
<textarea rows="3" wire:model.defer="noteGenerali" class="w-full rounded-lg border-gray-300" placeholder="Annotazioni rapide per lo studio..."></textarea>
|
||||
</label>
|
||||
<div class="mt-3">
|
||||
<x-filament::button wire:click="registraLetturaAcqua" wire:loading.attr="disabled" wire:target="registraLetturaAcqua,pendingWaterPhotos">
|
||||
<x-filament::button wire:click="registraLetturaAcqua" wire:loading.attr="disabled" wire:target="registraLetturaAcqua,pendingWaterPhotos,pendingWaterLibraryPhotos">
|
||||
<span wire:loading.remove wire:target="registraLetturaAcqua">Registra lettura light</span>
|
||||
<span wire:loading wire:target="registraLetturaAcqua">Salvataggio...</span>
|
||||
</x-filament::button>
|
||||
|
|
|
|||
|
|
@ -19,16 +19,8 @@
|
|||
|
||||
<div class="flex shrink-0 flex-wrap items-center gap-2">
|
||||
<button type="button" wire:click="createPostIt" class="inline-flex items-center rounded-md bg-emerald-700 px-3 py-1.5 text-xs font-medium text-white hover:bg-emerald-600">
|
||||
Post-it
|
||||
Apri Post-it
|
||||
</button>
|
||||
<button type="button" wire:click="openTicketMobile" class="inline-flex items-center rounded-md bg-slate-900 px-3 py-1.5 text-xs font-medium text-white hover:bg-slate-800">
|
||||
Prendi nota
|
||||
</button>
|
||||
@if(!empty($liveIncomingCall['rubrica_url']))
|
||||
<a href="{{ $liveIncomingCall['rubrica_url'] }}" class="inline-flex items-center rounded-md border border-emerald-300 bg-white px-3 py-1.5 text-xs font-medium text-emerald-800 hover:bg-emerald-100">
|
||||
Rubrica
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user