1423 lines
58 KiB
PHP
Executable File
1423 lines
58 KiB
PHP
Executable File
<?php
|
|
namespace App\Filament\Pages\Contabilita;
|
|
|
|
use App\Filament\Pages\Contabilita\CasseBancheMovimenti;
|
|
use App\Models\Incasso;
|
|
use App\Models\PersonaUnitaRelazione;
|
|
use App\Models\RataEmessaNg;
|
|
use App\Models\Soggetto;
|
|
use App\Models\UnitaImmobiliare;
|
|
use App\Models\User;
|
|
use App\Support\AnnoGestioneContext;
|
|
use App\Support\GestioneVisibility;
|
|
use App\Support\StabileContext;
|
|
use BackedEnum;
|
|
use Carbon\Carbon;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Pages\Page;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema as DbSchema;
|
|
use UnitEnum;
|
|
|
|
class EstrattoContoSoggetto extends Page
|
|
{
|
|
protected static ?string $title = 'Estratto conto soggetto';
|
|
|
|
protected static ?string $slug = 'contabilita/estratto-conto/{record}';
|
|
|
|
protected static bool $shouldRegisterNavigation = false;
|
|
|
|
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-document-text';
|
|
|
|
protected static UnitEnum|string|null $navigationGroup = 'Stabile';
|
|
|
|
protected string $view = 'filament.pages.contabilita.estratto-conto-soggetto';
|
|
|
|
public ?Soggetto $soggetto = null;
|
|
|
|
public ?string $headerNome = null;
|
|
|
|
public ?string $headerCodiceFiscale = null;
|
|
|
|
public ?string $headerLegacyNome = null;
|
|
|
|
public ?string $headerLegacyCodiceFiscale = null;
|
|
|
|
/** Vista: 'unita' (singola unità) oppure 'soggetto' (tutte le unità del soggetto) */
|
|
public string $vista = 'unita';
|
|
|
|
/** Ricerca unità (codice/int.) */
|
|
public string $qUnita = '';
|
|
|
|
/** Unità selezionata (per vista=unita) */
|
|
public ?int $unitaId = null;
|
|
|
|
public ?int $prevUnitaId = null;
|
|
|
|
public ?int $nextUnitaId = null;
|
|
|
|
/** Link rapidi: estratto conto condomino/inquilino per la stessa unità (se presente) */
|
|
public ?int $recordCondominoId = null;
|
|
|
|
public ?int $recordInquilinoId = null;
|
|
|
|
/** Mostra colonna scadenza */
|
|
public bool $showScadenza = false;
|
|
|
|
/** @var array<int, array{id:int,label:string,short:string,totale:float,residuo:float}> */
|
|
public array $unitaList = [];
|
|
|
|
/** @var array<int, array<string, mixed>> */
|
|
public array $rows = [];
|
|
|
|
/** @var array<int, array{key:string,label:string,rows:array<int, array<string,mixed>>,totali:array{dovuto:float,pagato:float,residuo:float}}> */
|
|
public array $groups = [];
|
|
|
|
/** @var array<int, array<string, mixed>> */
|
|
public array $incassi = [];
|
|
|
|
/** @var array{addebitato:float,pagato:float,residuo:float,rate:int} */
|
|
public array $totali = [
|
|
'addebitato' => 0.0,
|
|
'pagato' => 0.0,
|
|
'residuo' => 0.0,
|
|
'rate' => 0,
|
|
];
|
|
|
|
/** @var array{addebitato:float,pagato:float,residuo:float,rate:int} */
|
|
public array $totaliUnita = [
|
|
'addebitato' => 0.0,
|
|
'pagato' => 0.0,
|
|
'residuo' => 0.0,
|
|
'rate' => 0,
|
|
];
|
|
|
|
public array $gestioneYearPolicy = [];
|
|
|
|
public static function canAccess(): bool
|
|
{
|
|
$user = Auth::user();
|
|
|
|
return $user instanceof User
|
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
|
}
|
|
|
|
public function mount(int | string $record): void
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
abort(403);
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
Notification::make()
|
|
->title('Stabile non selezionato')
|
|
->body('Seleziona uno stabile per consultare l\'estratto conto.')
|
|
->warning()
|
|
->send();
|
|
return;
|
|
}
|
|
|
|
$req = request();
|
|
$vista = trim((string) $req->query('vista', 'unita'));
|
|
$this->vista = in_array($vista, ['unita', 'soggetto'], true) ? $vista : 'unita';
|
|
$this->qUnita = trim((string) $req->query('q', ''));
|
|
$this->showScadenza = $req->boolean('scadenza');
|
|
$unitaIdRaw = $req->query('unita_id');
|
|
$this->unitaId = is_numeric($unitaIdRaw) ? (int) $unitaIdRaw : null;
|
|
if ($this->unitaId === null) {
|
|
try {
|
|
$ref = (string) ($req->headers->get('referer') ?? '');
|
|
if ($ref !== '') {
|
|
$q = parse_url($ref, PHP_URL_QUERY);
|
|
if (is_string($q) && $q !== '') {
|
|
parse_str($q, $params);
|
|
$refUnita = $params['unita_id'] ?? null;
|
|
if (is_numeric($refUnita)) {
|
|
$this->unitaId = (int) $refUnita;
|
|
}
|
|
}
|
|
}
|
|
} catch (\Throwable $e) {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
$this->soggetto = Soggetto::query()->findOrFail((int) $record);
|
|
$this->headerNome = trim((string) ($this->soggetto->ragione_sociale ?: trim((string) $this->soggetto->nome . ' ' . (string) $this->soggetto->cognome))) ?: ('Soggetto #' . (int) $this->soggetto->id);
|
|
$this->headerCodiceFiscale = $this->soggetto->codice_fiscale ?: null;
|
|
$this->headerLegacyNome = null;
|
|
$this->headerLegacyCodiceFiscale = null;
|
|
|
|
if (! DbSchema::hasTable('rate_emesse')) {
|
|
return;
|
|
}
|
|
|
|
$annoGestione = AnnoGestioneContext::resolveActiveAnno($user);
|
|
$this->gestioneYearPolicy = GestioneVisibility::policyForStabile((int) $stabileId, $annoGestione ?: null);
|
|
$visibleYears = (array) ($this->gestioneYearPolicy['visible_years'] ?? []);
|
|
$countableYears = (array) ($this->gestioneYearPolicy['countable_years'] ?? []);
|
|
|
|
if ($this->vista === 'unita' && $this->unitaId !== null && $this->hydrateLegacyUnitView((int) $stabileId, $visibleYears, $countableYears)) {
|
|
return;
|
|
}
|
|
|
|
$query = RataEmessaNg::query()
|
|
->where('soggetto_responsabile_id', (int) $this->soggetto->id)
|
|
->whereHas('unitaImmobiliare', function ($q) use ($stabileId): void {
|
|
$q->where('stabile_id', (int) $stabileId);
|
|
})
|
|
->with([
|
|
'unitaImmobiliare:id,stabile_id,codice_unita,denominazione,scala,piano,interno',
|
|
'pianoRateizzazione:id,stabile_id,descrizione',
|
|
])
|
|
->orderBy('data_emissione')
|
|
->orderBy('id')
|
|
->limit(5000);
|
|
|
|
$items = $query->get()
|
|
->filter(fn(RataEmessaNg $r): bool => GestioneVisibility::matchesYear($this->resolveRateCalendarYear($r), $visibleYears))
|
|
->values();
|
|
|
|
$itemsCountable = $items
|
|
->filter(fn(RataEmessaNg $r): bool => GestioneVisibility::matchesYear($this->resolveRateCalendarYear($r), $countableYears))
|
|
->values();
|
|
|
|
// Totali soggetto (tutte le unità)
|
|
$this->totali['rate'] = (int) $itemsCountable->count();
|
|
$this->totali['addebitato'] = (float) $itemsCountable->sum(fn(RataEmessaNg $r) => (float) $r->importo_addebitato_soggetto);
|
|
$this->totali['pagato'] = (float) $itemsCountable->sum(fn(RataEmessaNg $r) => (float) $r->importo_pagato);
|
|
$this->totali['residuo'] = (float) ($this->totali['addebitato'] - $this->totali['pagato']);
|
|
|
|
// Unità disponibili per questo soggetto
|
|
$unitaMap = [];
|
|
foreach ($items as $r) {
|
|
$u = $r->unitaImmobiliare;
|
|
if (! ($u instanceof UnitaImmobiliare)) {
|
|
continue;
|
|
}
|
|
$unitaMap[$u->id] = $u;
|
|
}
|
|
$unitaAll = array_values($unitaMap);
|
|
usort($unitaAll, function (UnitaImmobiliare $a, UnitaImmobiliare $b): int {
|
|
$ak = (string) ($a->codice_unita ?? '');
|
|
$bk = (string) ($b->codice_unita ?? '');
|
|
if ($ak !== '' && $bk !== '') {
|
|
return strcmp($ak, $bk);
|
|
}
|
|
return $a->id <=> $b->id;
|
|
});
|
|
|
|
$unitaFiltered = $unitaAll;
|
|
if ($this->qUnita !== '') {
|
|
$needle = mb_strtolower($this->qUnita);
|
|
$unitaFiltered = array_values(array_filter($unitaFiltered, function (UnitaImmobiliare $u) use ($needle): bool {
|
|
$hay = mb_strtolower(
|
|
trim((string) ($u->codice_unita ?? '') . ' ' . (string) ($u->interno ?? '') . ' ' . (string) ($u->denominazione ?? ''))
|
|
);
|
|
return $hay !== '' && str_contains($hay, $needle);
|
|
}));
|
|
}
|
|
|
|
if ($this->unitaId === null || ! array_key_exists($this->unitaId, $unitaMap)) {
|
|
$this->unitaId = ! empty($unitaFiltered) ? (int) $unitaFiltered[0]->id : (! empty($unitaAll) ? (int) $unitaAll[0]->id : null);
|
|
}
|
|
|
|
if ($this->vista === 'unita' && $this->unitaId !== null) {
|
|
$reference = $this->resolveCurrentUnitReference($this->unitaId);
|
|
if ($reference !== null) {
|
|
$currentName = trim((string) ($reference['nome'] ?? ''));
|
|
$currentCf = trim((string) ($reference['codice_fiscale'] ?? ''));
|
|
|
|
if ($currentName !== '' && $currentName !== $this->headerNome) {
|
|
$this->headerLegacyNome = $this->headerNome;
|
|
$this->headerLegacyCodiceFiscale = $this->headerCodiceFiscale;
|
|
$this->headerNome = $currentName;
|
|
$this->headerCodiceFiscale = $currentCf !== '' ? $currentCf : null;
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->unitaList = [];
|
|
foreach ($unitaFiltered as $u) {
|
|
$short = $this->formatUnitaShort($u);
|
|
$tot = (float) $items->where('unita_immobiliare_id', $u->id)->sum(fn(RataEmessaNg $r) => (float) $r->importo_addebitato_soggetto);
|
|
$pag = (float) $items->where('unita_immobiliare_id', $u->id)->sum(fn(RataEmessaNg $r) => (float) $r->importo_pagato);
|
|
$this->unitaList[] = [
|
|
'id' => (int) $u->id,
|
|
'label' => $this->formatUnitaLabel($u),
|
|
'short' => $short,
|
|
'totale' => $tot,
|
|
'residuo' => $tot - $pag,
|
|
];
|
|
}
|
|
|
|
$this->prevUnitaId = null;
|
|
$this->nextUnitaId = null;
|
|
if ($this->unitaId !== null && ! empty($this->unitaList)) {
|
|
$idx = null;
|
|
foreach ($this->unitaList as $i => $u) {
|
|
if ((int) $u['id'] === (int) $this->unitaId) {
|
|
$idx = $i;
|
|
break;
|
|
}
|
|
}
|
|
if ($idx !== null) {
|
|
$this->prevUnitaId = $idx > 0 ? (int) $this->unitaList[$idx - 1]['id'] : null;
|
|
$this->nextUnitaId = $idx < (count($this->unitaList) - 1) ? (int) $this->unitaList[$idx + 1]['id'] : null;
|
|
}
|
|
}
|
|
|
|
// Determina se esistono quote inquilino/condomino per la stessa unità (per link rapido)
|
|
if ($this->unitaId !== null) {
|
|
$baseOther = RataEmessaNg::query()
|
|
->where('unita_immobiliare_id', (int) $this->unitaId)
|
|
->whereHas('unitaImmobiliare', function ($q) use ($stabileId): void {
|
|
$q->where('stabile_id', (int) $stabileId);
|
|
})
|
|
->whereNotNull('note');
|
|
|
|
$condIds = (clone $baseOther)
|
|
->where(function ($q): void {
|
|
$q->where('note', 'like', '%tipo_soggetto=C%')
|
|
->orWhere('note', 'like', '%cond_inq=C%')
|
|
->orWhere('note', 'like', '%o_r_s=C%');
|
|
})
|
|
->distinct()
|
|
->pluck('soggetto_responsabile_id')
|
|
->map(fn($v) => (int) $v)
|
|
->values();
|
|
|
|
$inqIds = (clone $baseOther)
|
|
->where(function ($q): void {
|
|
$q->where('note', 'like', '%tipo_soggetto=I%')
|
|
->orWhere('note', 'like', '%cond_inq=I%')
|
|
->orWhere('note', 'like', '%o_r_s=I%');
|
|
})
|
|
->distinct()
|
|
->pluck('soggetto_responsabile_id')
|
|
->map(fn($v) => (int) $v)
|
|
->values();
|
|
|
|
$this->recordCondominoId = $condIds->count() === 1 ? (int) $condIds[0] : null;
|
|
$this->recordInquilinoId = $inqIds->count() === 1 ? (int) $inqIds[0] : null;
|
|
}
|
|
|
|
$aggregateItems = function ($collection) {
|
|
return $collection
|
|
->groupBy(function (RataEmessaNg $r): string {
|
|
return (int) ($r->unita_immobiliare_id ?? 0)
|
|
. '|' . (int) ($r->piano_rateizzazione_id ?? 0)
|
|
. '|' . (int) ($r->numero_rata_progressivo ?? 0);
|
|
})
|
|
->map(function ($group) {
|
|
/** @var RataEmessaNg $first */
|
|
$first = $group->first();
|
|
if ($group->count() > 1) {
|
|
$first->importo_addebitato_soggetto = $group->sum('importo_addebitato_soggetto');
|
|
$first->importo_pagato = $group->sum('importo_pagato');
|
|
}
|
|
return $first;
|
|
})
|
|
->values();
|
|
};
|
|
|
|
$itemsAggregati = $aggregateItems($items);
|
|
$itemsCountableAggregati = $aggregateItems($itemsCountable);
|
|
|
|
// Filtra righe in base alla vista scelta
|
|
$displayItems = $itemsAggregati;
|
|
if ($this->vista === 'unita' && $this->unitaId !== null) {
|
|
$displayItems = $itemsAggregati->where('unita_immobiliare_id', (int) $this->unitaId)->values();
|
|
}
|
|
|
|
// Pre-carica saldi iniziali (esercizi precedenti) da dett_tab importato (CONG.*)
|
|
$saldoRowsAll = $this->loadSaldiInizialiRows((int) $stabileId, $items, $unitaAll);
|
|
$saldoRowsDisplay = $saldoRowsAll;
|
|
if ($this->vista === 'unita' && $this->unitaId !== null) {
|
|
$saldoRowsDisplay = array_values(array_filter($saldoRowsAll, fn($r) => (int) ($r['unita_id'] ?? 0) === (int) $this->unitaId));
|
|
}
|
|
|
|
$displayItemsCountable = $itemsCountableAggregati;
|
|
if ($this->vista === 'unita' && $this->unitaId !== null) {
|
|
$displayItemsCountable = $itemsCountableAggregati->where('unita_immobiliare_id', (int) $this->unitaId)->values();
|
|
}
|
|
|
|
$this->totaliUnita['rate'] = $this->unitaId !== null ? (int) $displayItemsCountable->count() : 0;
|
|
$this->totaliUnita['addebitato'] = $this->unitaId !== null ? (float) $displayItemsCountable->sum(fn(RataEmessaNg $r) => (float) $r->importo_addebitato_soggetto) : 0.0;
|
|
$this->totaliUnita['pagato'] = $this->unitaId !== null ? (float) $displayItemsCountable->sum(fn(RataEmessaNg $r) => (float) $r->importo_pagato) : 0.0;
|
|
$this->totaliUnita['residuo'] = (float) ($this->totaliUnita['addebitato'] - $this->totaliUnita['pagato']);
|
|
|
|
$rateRows = $displayItems
|
|
->map(function (RataEmessaNg $r): array {
|
|
$u = $r->unitaImmobiliare;
|
|
$pianoId = is_numeric($r->piano_rateizzazione_id ?? null) ? (int) $r->piano_rateizzazione_id : 0;
|
|
$pianoLabel = (string) ($r->pianoRateizzazione?->descrizione ?? '');
|
|
|
|
$unitaShort = '';
|
|
if ($u instanceof UnitaImmobiliare) {
|
|
$unitaShort = $this->formatUnitaShort($u);
|
|
}
|
|
|
|
$note = (string) ($r->note ?? '');
|
|
$nEmissione = null;
|
|
if ($note !== '' && preg_match('/\bn_emissione=(\d+)\b/', $note, $m)) {
|
|
$nEmissione = (int) ($m[1] ?? 0);
|
|
}
|
|
$gestioneLegacy = $this->extractGestioneLabelFromRateNote($note);
|
|
$ricevutaLegacy = $this->extractNumeroRicevutaFromRateNote($note);
|
|
$avvisoLegacy = $this->extractAvvisoCodeFromRateNote($note);
|
|
|
|
$dovuto = (float) ($r->importo_addebitato_soggetto ?? 0);
|
|
$pagato = (float) ($r->importo_pagato ?? 0);
|
|
$residuo = $dovuto - $pagato;
|
|
|
|
return [
|
|
'id' => (int) $r->id,
|
|
'piano_id' => $pianoId,
|
|
'piano' => $pianoLabel,
|
|
'data_emissione' => $r->data_emissione?->format('d/m/Y'),
|
|
'data_emissione_sort' => $r->data_emissione?->format('Y-m-d') ?? '9999-12-31',
|
|
'data_scadenza' => $r->data_scadenza?->format('d/m/Y'),
|
|
'avviso' => (int) ($r->numero_rata_progressivo ?? 0),
|
|
'avviso_legacy' => $avvisoLegacy,
|
|
'n_emissione' => $nEmissione,
|
|
'descrizione' => (string) ($r->descrizione ?? ''),
|
|
'gestione_legacy' => $gestioneLegacy,
|
|
'numero_ricevuta' => $ricevutaLegacy,
|
|
'riferimento_legacy' => $this->buildLegacyReferenceLabel($gestioneLegacy, $ricevutaLegacy, $avvisoLegacy),
|
|
'unita_short' => $unitaShort,
|
|
'dovuto' => $dovuto,
|
|
'data_pagamento' => $r->data_ultimo_pagamento?->format('d/m/Y'),
|
|
'pagato' => $pagato,
|
|
'residuo' => $residuo,
|
|
];
|
|
})
|
|
->all();
|
|
|
|
// Ordina: saldi iniziali prima delle rate
|
|
$this->rows = array_merge($saldoRowsDisplay, $rateRows);
|
|
|
|
// Aggiorna totali includendo i saldi iniziali (solo dovuto/residuo)
|
|
$this->groups = $this->buildGroups($this->rows);
|
|
|
|
$this->hydrateIncassiForSoggetto((int) $stabileId, $items, $visibleYears);
|
|
}
|
|
|
|
/**
|
|
* @param array<int, int|string> $visibleYears
|
|
* @param array<int, int|string> $countableYears
|
|
*/
|
|
private function hydrateLegacyUnitView(int $stabileId, array $visibleYears = [], array $countableYears = []): bool
|
|
{
|
|
if ($this->unitaId === null) {
|
|
return false;
|
|
}
|
|
|
|
$unita = UnitaImmobiliare::query()
|
|
->with('stabile:id,codice_stabile')
|
|
->where('stabile_id', $stabileId)
|
|
->find($this->unitaId);
|
|
|
|
if (! $unita instanceof UnitaImmobiliare) {
|
|
return false;
|
|
}
|
|
|
|
$legacyRateRows = $this->loadLegacyRateRowsForUnita($unita, $visibleYears);
|
|
if ($legacyRateRows === []) {
|
|
return false;
|
|
}
|
|
|
|
$legacyRow = $this->getLegacyCondominRowForUnita($unita);
|
|
$ownerName = trim((string) ($legacyRow?->nom_cond ?? ''));
|
|
$ownerCf = trim((string) ($legacyRow?->cond_cod_fisc ?? $legacyRow?->codice_fiscale ?? ''));
|
|
$tenantName = trim((string) ($legacyRow?->inquil_nome ?? ''));
|
|
$tenantCf = trim((string) ($legacyRow?->inquil_cod_fisc ?? ''));
|
|
|
|
if ($ownerName !== '') {
|
|
$this->headerLegacyNome = $this->headerNome;
|
|
$this->headerLegacyCodiceFiscale = $this->headerCodiceFiscale;
|
|
$this->headerNome = $ownerName;
|
|
$this->headerCodiceFiscale = $ownerCf !== '' ? $ownerCf : null;
|
|
}
|
|
|
|
$this->unitaList = [[
|
|
'id' => (int) $unita->id,
|
|
'label' => $this->formatUnitaLabel($unita),
|
|
'short' => $this->formatUnitaShort($unita),
|
|
'totale' => (float) collect($legacyRateRows)->sum(fn(array $row): float => (float) ($row['dovuto'] ?? 0)),
|
|
'residuo' => (float) collect($legacyRateRows)->sum(fn(array $row): float => (float) ($row['residuo'] ?? 0)),
|
|
]];
|
|
$this->prevUnitaId = null;
|
|
$this->nextUnitaId = null;
|
|
$this->recordCondominoId = $this->resolveLegacySoggettoId($ownerCf, $legacyRow?->id_cond ?? $legacyRow?->legacy_id_cond ?? null);
|
|
$this->recordInquilinoId = $this->resolveLegacySoggettoId($tenantCf, null);
|
|
|
|
$countableRows = collect($legacyRateRows)
|
|
->filter(fn(array $row): bool => GestioneVisibility::matchesYear(isset($row['year']) && is_numeric($row['year']) ? (int) $row['year'] : null, $countableYears))
|
|
->values();
|
|
|
|
$this->totali = [
|
|
'rate' => (int) $countableRows->count(),
|
|
'addebitato' => (float) $countableRows->sum(fn(array $row): float => (float) ($row['dovuto'] ?? 0)),
|
|
'pagato' => (float) $countableRows->sum(fn(array $row): float => (float) ($row['pagato'] ?? 0)),
|
|
'residuo' => (float) $countableRows->sum(fn(array $row): float => (float) ($row['residuo'] ?? 0)),
|
|
];
|
|
$this->totaliUnita = $this->totali;
|
|
|
|
$this->rows = collect($legacyRateRows)
|
|
->map(function (array $row) use ($unita): array {
|
|
$gestioneLegacy = trim((string) ($row['gestione'] ?? '')) ?: null;
|
|
$ricevutaLegacy = is_numeric($row['numero_ricevuta'] ?? null) ? (int) $row['numero_ricevuta'] : null;
|
|
$avvisoLegacy = trim((string) ($row['avviso_code'] ?? $row['avviso'] ?? '')) ?: null;
|
|
$dovuto = (float) ($row['dovuto'] ?? 0);
|
|
$pagato = (float) ($row['pagato'] ?? 0);
|
|
|
|
return [
|
|
'id' => (int) ($row['id'] ?? 0),
|
|
'piano_id' => 0,
|
|
'piano' => $gestioneLegacy !== null ? ('Gestione ' . $gestioneLegacy) : 'Gestione',
|
|
'data_emissione' => $row['data_emissione'] ?? null,
|
|
'data_emissione_sort' => $this->normalizeLegacyDateSort($row['data_emissione'] ?? null),
|
|
'data_scadenza' => $row['data_scadenza'] ?? null,
|
|
'avviso' => $ricevutaLegacy ?? $avvisoLegacy,
|
|
'avviso_legacy' => $avvisoLegacy,
|
|
'n_emissione' => $row['numero_emissione'] ?? null,
|
|
'descrizione' => (string) ($row['descrizione'] ?? ''),
|
|
'gestione_legacy' => $gestioneLegacy,
|
|
'numero_ricevuta' => $ricevutaLegacy,
|
|
'riferimento_legacy' => $this->buildLegacyReferenceLabel($gestioneLegacy, $ricevutaLegacy, $avvisoLegacy),
|
|
'unita_short' => $this->formatUnitaShort($unita),
|
|
'dovuto' => $dovuto,
|
|
'data_pagamento' => $row['data_pagamento'] ?? null,
|
|
'pagato' => $pagato,
|
|
'residuo' => (float) ($row['residuo'] ?? ($dovuto - $pagato)),
|
|
];
|
|
})
|
|
->all();
|
|
|
|
$this->groups = $this->buildGroups($this->rows);
|
|
$this->incassi = $this->loadLegacyIncassiForUnita($unita, $visibleYears);
|
|
|
|
return true;
|
|
}
|
|
|
|
private function resolveRateCalendarYear(RataEmessaNg $rate): ?int
|
|
{
|
|
$year = GestioneVisibility::normalizeCalendarYear($rate->data_emissione?->format('Y'));
|
|
if ($year !== null) {
|
|
return $year;
|
|
}
|
|
|
|
$year = GestioneVisibility::normalizeCalendarYear($rate->data_scadenza?->format('Y'));
|
|
if ($year !== null) {
|
|
return $year;
|
|
}
|
|
|
|
$legacyYear = $this->extractLegacyYearFromRateNote((string) ($rate->note ?? ''));
|
|
|
|
return GestioneVisibility::normalizeCalendarYear($legacyYear);
|
|
}
|
|
|
|
private function formatUnitaLabel(UnitaImmobiliare $u): string
|
|
{
|
|
$cod = (string) ($u->codice_unita ?? ('ID ' . $u->id));
|
|
$nome = trim((string) ($u->denominazione ?? ''));
|
|
if ($nome === '' || preg_match('/^\s*area\s+condominiale\s*$/i', $nome)) {
|
|
$nome = 'Unità';
|
|
}
|
|
$pos = trim('Scala ' . ($u->scala ?? '-') . ' · Piano ' . ($u->piano ?? '-') . ' · Int. ' . ($u->interno ?? '-'));
|
|
return trim($cod . ' — ' . $nome . ' (' . $pos . ')');
|
|
}
|
|
|
|
private function formatUnitaShort(UnitaImmobiliare $u): string
|
|
{
|
|
$scala = trim((string) ($u->scala ?? ''));
|
|
$piano = trim((string) ($u->piano ?? ''));
|
|
$interno = trim((string) ($u->interno ?? ''));
|
|
|
|
$parts = [];
|
|
if ($scala !== '') {
|
|
$parts[] = $scala;
|
|
}
|
|
if ($piano !== '') {
|
|
$parts[] = 'P' . $piano;
|
|
}
|
|
if ($interno !== '') {
|
|
$parts[] = 'Int.' . $interno;
|
|
}
|
|
return implode('-', $parts);
|
|
}
|
|
|
|
private function normalizeLegacyDateSort(?string $value): string
|
|
{
|
|
$raw = trim((string) ($value ?? ''));
|
|
if ($raw === '') {
|
|
return '9999-12-31';
|
|
}
|
|
|
|
try {
|
|
return Carbon::createFromFormat('d/m/Y', $raw)->format('Y-m-d');
|
|
} catch (\Throwable) {
|
|
return '9999-12-31';
|
|
}
|
|
}
|
|
|
|
private function getLegacyCondominRowForUnita(UnitaImmobiliare $unita): ?object
|
|
{
|
|
if (! DbSchema::connection('gescon_import')->hasTable('condomin')) {
|
|
return null;
|
|
}
|
|
|
|
$codStabile = trim((string) ($unita->stabile?->codice_stabile ?? ''));
|
|
$interno = trim((string) ($unita->interno ?? ''));
|
|
if ($codStabile === '' || $interno === '') {
|
|
return null;
|
|
}
|
|
|
|
$query = DB::connection('gescon_import')
|
|
->table('condomin')
|
|
->where('cod_stabile', $codStabile)
|
|
->where('interno', $interno);
|
|
|
|
$scala = trim((string) ($unita->scala ?? ''));
|
|
if ($scala !== '') {
|
|
$query->where('scala', $scala);
|
|
}
|
|
|
|
$desiredCols = [
|
|
'legacy_id_cond',
|
|
'id_cond',
|
|
'cod_cond',
|
|
'nom_cond',
|
|
'codice_fiscale',
|
|
'cond_cod_fisc',
|
|
'inquil_nome',
|
|
'inquil_cod_fisc',
|
|
];
|
|
$actualCols = \Illuminate\Support\Facades\Schema::connection('gescon_import')->getColumnListing('condomin');
|
|
$selectCols = array_intersect($desiredCols, $actualCols);
|
|
|
|
return $query
|
|
->orderByDesc('legacy_year')
|
|
->orderByDesc('id')
|
|
->first($selectCols);
|
|
}
|
|
|
|
/** @return array<int, string> */
|
|
private function resolveLegacyCondIdsForUnita(UnitaImmobiliare $unita): array
|
|
{
|
|
$fallbackIds = [];
|
|
$storedId = trim((string) ($unita->legacy_cond_id ?? ''));
|
|
if ($storedId !== '') {
|
|
$fallbackIds[] = $storedId;
|
|
}
|
|
|
|
if (! DbSchema::connection('gescon_import')->hasTable('condomin')) {
|
|
return array_values(array_unique($fallbackIds));
|
|
}
|
|
|
|
$codStabile = trim((string) ($unita->stabile?->codice_stabile ?? ''));
|
|
$interno = trim((string) ($unita->interno ?? ''));
|
|
if ($codStabile === '' || $interno === '') {
|
|
return array_values(array_unique($fallbackIds));
|
|
}
|
|
|
|
$query = DB::connection('gescon_import')
|
|
->table('condomin')
|
|
->where('cod_stabile', $codStabile)
|
|
->where('interno', $interno);
|
|
|
|
$scala = trim((string) ($unita->scala ?? ''));
|
|
if ($scala !== '') {
|
|
$query->where('scala', $scala);
|
|
}
|
|
|
|
$resolved = $query
|
|
->orderByDesc('legacy_year')
|
|
->pluck('cod_cond')
|
|
->map(fn($value) => trim((string) $value))
|
|
->filter(fn(string $value): bool => $value !== '')
|
|
->unique()
|
|
->values()
|
|
->all();
|
|
|
|
return collect($fallbackIds)
|
|
->concat($resolved)
|
|
->map(fn($value) => trim((string) $value))
|
|
->filter(fn(string $value): bool => $value !== '')
|
|
->unique()
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
/** @param array<int, string> $candidateIds
|
|
* @return array<int, string>
|
|
*/
|
|
private function preferStoredLegacyCondIdForImportTable(UnitaImmobiliare $unita, string $table, string $column, array $candidateIds, string $codStabile): array
|
|
{
|
|
$storedId = trim((string) ($unita->legacy_cond_id ?? ''));
|
|
if ($storedId === '' || $candidateIds === [] || ! DbSchema::connection('gescon_import')->hasTable($table)) {
|
|
return $candidateIds;
|
|
}
|
|
|
|
$hasStoredRows = DB::connection('gescon_import')
|
|
->table($table)
|
|
->where('cod_stabile', $codStabile)
|
|
->where($column, $storedId)
|
|
->exists();
|
|
|
|
return $hasStoredRows ? [$storedId] : $candidateIds;
|
|
}
|
|
|
|
/** @param array<int, int|string> $visibleYears
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function loadLegacyRateRowsForUnita(UnitaImmobiliare $unita, array $visibleYears = []): array
|
|
{
|
|
if (! DbSchema::connection('gescon_import')->hasTable('rate_emissioni_dettaglio')) {
|
|
return [];
|
|
}
|
|
|
|
$legacyCondIds = $this->resolveLegacyCondIdsForUnita($unita);
|
|
$codStabile = trim((string) ($unita->stabile?->codice_stabile ?? ''));
|
|
if ($legacyCondIds === [] || $codStabile === '') {
|
|
return [];
|
|
}
|
|
|
|
$legacyCondIds = $this->preferStoredLegacyCondIdForImportTable($unita, 'rate_emissioni_dettaglio', 'cod_cond', $legacyCondIds, $codStabile);
|
|
|
|
$rows = DB::connection('gescon_import')
|
|
->table('rate_emissioni_dettaglio')
|
|
->where('cod_stabile', $codStabile)
|
|
->whereIn('cod_cond', $legacyCondIds)
|
|
->orderBy('data_emissione')
|
|
->orderBy('numero_emissione')
|
|
->orderBy('numero_ricevuta')
|
|
->orderBy('id')
|
|
->limit(5000)
|
|
->get();
|
|
|
|
$mapped = $rows->map(function ($row): array {
|
|
$gestioneLabel = trim((string) ($row->anno_gestione ?? ''));
|
|
$year = $this->extractEffectiveGestioneEndYearFromLabel($gestioneLabel);
|
|
|
|
if ($year === null) {
|
|
try {
|
|
if (! empty($row->data_emissione)) {
|
|
$year = GestioneVisibility::normalizeCalendarYear(Carbon::parse($row->data_emissione)->format('Y'));
|
|
}
|
|
} catch (\Throwable) {
|
|
$year = null;
|
|
}
|
|
}
|
|
|
|
$dovuto = is_numeric($row->importo_dovuto_euro ?? null)
|
|
? (float) $row->importo_dovuto_euro
|
|
: (is_numeric($row->importo_dovuto ?? null) ? (float) $row->importo_dovuto : 0.0);
|
|
|
|
$pagato = is_numeric($row->gia_pagato ?? null)
|
|
? (float) $row->gia_pagato
|
|
: 0.0;
|
|
|
|
$mapped = [
|
|
'id' => (int) ($row->id ?? 0),
|
|
'numero_emissione' => is_numeric($row->numero_emissione ?? null) ? (int) $row->numero_emissione : null,
|
|
'data_emissione' => $this->formatLegacyDateValue($row->data_emissione ?? null),
|
|
'data_scadenza' => null,
|
|
'data_pagamento' => null,
|
|
'descrizione' => trim((string) ($row->descrizione ?? '')),
|
|
'avviso' => $this->buildLegacyAvvisoCodeFromRow($gestioneLabel !== '' ? $gestioneLabel : (string) ($row->anno_emissione ?? ''), is_numeric($row->numero_mese ?? null) ? (int) $row->numero_mese : null, $row->data_emissione ?? null),
|
|
'avviso_code' => $this->buildLegacyAvvisoCodeFromRow($gestioneLabel !== '' ? $gestioneLabel : (string) ($row->anno_emissione ?? ''), is_numeric($row->numero_mese ?? null) ? (int) $row->numero_mese : null, $row->data_emissione ?? null),
|
|
'numero_ricevuta' => is_numeric($row->numero_ricevuta ?? null) ? (int) $row->numero_ricevuta : null,
|
|
'gestione' => $gestioneLabel !== '' ? $gestioneLabel : null,
|
|
'tipo' => trim((string) ($row->cond_inq ?? $row->tipo_soggetto ?? '')) ?: null,
|
|
'dovuto' => $dovuto,
|
|
'pagato' => $pagato,
|
|
'residuo' => is_numeric($row->residuo_emesso ?? null) ? (float) $row->residuo_emesso : ($dovuto - $pagato),
|
|
'year' => $year,
|
|
];
|
|
|
|
return $mapped;
|
|
});
|
|
|
|
if ($visibleYears !== []) {
|
|
$mapped = $mapped->filter(fn(array $row): bool => GestioneVisibility::matchesYear(isset($row['year']) && is_numeric($row['year']) ? (int) $row['year'] : null, $visibleYears));
|
|
}
|
|
|
|
return $mapped->values()->all();
|
|
}
|
|
|
|
/** @param array<int, int|string> $visibleYears
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function loadLegacyIncassiForUnita(UnitaImmobiliare $unita, array $visibleYears = []): array
|
|
{
|
|
if (! DbSchema::hasTable('incassi')) {
|
|
return [];
|
|
}
|
|
|
|
$legacyCondIds = $this->resolveLegacyCondIdsForUnita($unita);
|
|
if ($legacyCondIds === []) {
|
|
return [];
|
|
}
|
|
|
|
$codCol = DbSchema::hasColumn('incassi', 'cod_cond_gescon')
|
|
? 'cod_cond_gescon'
|
|
: (DbSchema::hasColumn('incassi', 'cod_cond') ? 'cod_cond' : null);
|
|
$tipoCol = DbSchema::hasColumn('incassi', 'cond_inquil')
|
|
? 'cond_inquil'
|
|
: (DbSchema::hasColumn('incassi', 'cond_inq') ? 'cond_inq' : null);
|
|
|
|
if (! $codCol) {
|
|
return [];
|
|
}
|
|
|
|
$incassi = Incasso::query()
|
|
->when(DbSchema::hasColumn('incassi', 'condominio_id'), fn($query) => $query->where('condominio_id', $stabileId = (int) $unita->stabile_id))
|
|
->whereIn($codCol, $legacyCondIds)
|
|
->orderByDesc(DbSchema::hasColumn('incassi', 'dt_empag') ? 'dt_empag' : (DbSchema::hasColumn('incassi', 'data_pagamento') ? 'data_pagamento' : 'id'))
|
|
->orderByDesc('id')
|
|
->limit(2000)
|
|
->get()
|
|
->filter(fn(Incasso $i): bool => GestioneVisibility::matchesYear($this->resolveIncassoCalendarYear($i), $visibleYears))
|
|
->values();
|
|
|
|
$contoMovimentiBase = CasseBancheMovimenti::getUrl(panel: 'admin-filament');
|
|
|
|
return $incassi->map(function (Incasso $i) use ($codCol, $tipoCol, $contoMovimentiBase): array {
|
|
$importo = (float) ($i->importo_pagato_euro ?? $i->importo_pagato ?? $i->importo_euro ?? $i->importo ?? 0);
|
|
$contoId = is_numeric($i->conto_bancario_id ?? null) ? (int) $i->conto_bancario_id : null;
|
|
|
|
return [
|
|
'id' => (int) $i->id,
|
|
'tipo' => $tipoCol ? (string) (data_get($i, $tipoCol) ?? '') : '',
|
|
'cod_cond' => (string) (data_get($i, $codCol) ?? ''),
|
|
'anno' => (string) ($i->anno_rif ?? $i->anno ?? ''),
|
|
'n_ricevuta' => $i->n_ricevuta ?? $i->n_riferimento ?? null,
|
|
'dt_empag' => $this->formatLegacyDateValue($i->dt_empag ?? null),
|
|
'data_pagamento' => $this->formatLegacyDateValue($i->data_pagamento ?? null),
|
|
'importo' => $importo,
|
|
'descrizione' => (string) ($i->descrizione ?? ''),
|
|
'cod_cassa' => (string) ($i->cod_cassa_gescon ?? $i->cod_cassa ?? $i->cod_cassa_legacy ?? ''),
|
|
'conto_id' => $contoId,
|
|
'conto_url' => $contoId ? ($contoMovimentiBase . '?conto_id=' . $contoId) : null,
|
|
'stato_riconciliazione' => (string) ($i->stato_riconciliazione ?? ''),
|
|
'confidenza_match' => is_numeric($i->confidenza_match ?? null) ? (float) $i->confidenza_match : null,
|
|
'semaforo' => ! empty($i->movimento_bancario_id) ? 'OK' : 'DA RICONCILIARE',
|
|
];
|
|
})->all();
|
|
}
|
|
|
|
private function resolveLegacySoggettoId(?string $codiceFiscale, mixed $legacyId = null): ?int
|
|
{
|
|
$cf = strtoupper(trim((string) ($codiceFiscale ?? '')));
|
|
if ($cf !== '') {
|
|
$id = Soggetto::query()->whereRaw('UPPER(codice_fiscale) = ?', [$cf])->value('id');
|
|
if (is_numeric($id) && (int) $id > 0) {
|
|
return (int) $id;
|
|
}
|
|
}
|
|
|
|
if (is_numeric($legacyId) && (int) $legacyId > 0) {
|
|
$id = Soggetto::query()->where('old_id', (int) $legacyId)->value('id');
|
|
if (is_numeric($id) && (int) $id > 0) {
|
|
return (int) $id;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function extractEffectiveGestioneEndYearFromLabel(?string $gestioneLabel): ?int
|
|
{
|
|
$label = trim((string) ($gestioneLabel ?? ''));
|
|
if ($label === '') {
|
|
return null;
|
|
}
|
|
|
|
if (preg_match('/\b(19|20)\d{2}\/(\d{2})\b/', $label, $m)) {
|
|
$startYear = (int) $m[1] . substr((string) $m[0], 2, 2);
|
|
$prefix = (int) substr((string) $startYear, 0, 2);
|
|
return ((int) ($prefix * 100)) + (int) ($m[2] ?? 0);
|
|
}
|
|
if (preg_match('/\b(19|20)\d{2}\b/', $label, $m)) {
|
|
return (int) ($m[0] ?? 0);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function buildLegacyAvvisoCodeFromRow(?string $gestioneLabel, ?int $numeroMese, mixed $dataEmissione): ?string
|
|
{
|
|
$month = $numeroMese;
|
|
if ($month === null || $month <= 0) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
if (! empty($dataEmissione)) {
|
|
$year = Carbon::parse($dataEmissione)->format('Y');
|
|
return $year . str_pad((string) $month, 2, '0', STR_PAD_LEFT);
|
|
}
|
|
} catch (\Throwable) {
|
|
}
|
|
|
|
$year = $this->extractEffectiveGestioneEndYearFromLabel($gestioneLabel);
|
|
return $year !== null ? ((string) $year . str_pad((string) $month, 2, '0', STR_PAD_LEFT)) : null;
|
|
}
|
|
|
|
private function formatLegacyDateValue(mixed $value): ?string
|
|
{
|
|
if (empty($value)) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return Carbon::parse($value)->format('d/m/Y');
|
|
} catch (\Throwable) {
|
|
$raw = trim((string) $value);
|
|
if ($raw === '') {
|
|
return null;
|
|
}
|
|
if (preg_match('/^\d{2}\/\d{2}\/\d{4}$/', $raw)) {
|
|
return $raw;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function resolveCurrentUnitReference(int $unitaId): ?array
|
|
{
|
|
$relation = PersonaUnitaRelazione::query()
|
|
->with('persona')
|
|
->where('unita_id', $unitaId)
|
|
->where('attivo', true)
|
|
->orderByRaw("case when lower(tipo_relazione) = 'comproprietario' then 1 else 0 end")
|
|
->orderByRaw("case when riceve_comunicazioni = 1 then 0 else 1 end")
|
|
->orderByDesc('id')
|
|
->first();
|
|
|
|
$persona = $relation?->persona;
|
|
if (! $persona) {
|
|
return null;
|
|
}
|
|
|
|
$nome = trim((string) ($persona->ragione_sociale ?? ''));
|
|
if ($nome === '') {
|
|
$nome = trim((string) (($persona->cognome ?? '') . ' ' . ($persona->nome ?? '')));
|
|
}
|
|
if ($nome === '') {
|
|
$nome = trim((string) ($persona->nome ?? ''));
|
|
}
|
|
|
|
if ($nome === '') {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'nome' => $nome,
|
|
'codice_fiscale' => trim((string) ($persona->codice_fiscale ?? '')) ?: null,
|
|
'tipo_relazione' => strtolower(trim((string) ($relation->tipo_relazione ?? ''))),
|
|
'ruolo_rate' => trim((string) ($relation->ruolo_rate ?? '')) ?: null,
|
|
];
|
|
}
|
|
|
|
/** @param array<int, array<string,mixed>> $rows */
|
|
private function buildGroups(array $rows): array
|
|
{
|
|
$rank = function (string $label): int {
|
|
$l = mb_strtolower($label);
|
|
if (str_contains($l, 'bilancio iniziale') || str_contains($l, 'situazione iniziale') || str_contains($l, 'esercizi precedenti')) {
|
|
return -1;
|
|
}
|
|
if (str_contains($l, 'ordinaria')) {
|
|
return 0;
|
|
}
|
|
if (str_contains($l, 'riscaldamento')) {
|
|
return 1;
|
|
}
|
|
if (str_contains($l, 'straordinaria')) {
|
|
return 2;
|
|
}
|
|
return 9;
|
|
};
|
|
|
|
$by = [];
|
|
foreach ($rows as $r) {
|
|
$key = (string) (($r['piano_id'] ?? 0) ?: ($r['piano'] ?? ''));
|
|
if (! array_key_exists($key, $by)) {
|
|
$by[$key] = [
|
|
'key' => $key,
|
|
'label' => (string) ($r['piano'] ?? 'Gestione'),
|
|
'rows' => [],
|
|
'min_emissione' => (string) ($r['data_emissione_sort'] ?? '9999-12-31'),
|
|
'rank' => $rank((string) ($r['piano'] ?? '')),
|
|
'totali' => ['dovuto' => 0.0, 'pagato' => 0.0, 'residuo' => 0.0],
|
|
];
|
|
}
|
|
$by[$key]['rows'][] = $r;
|
|
$by[$key]['totali']['dovuto'] += (float) ($r['dovuto'] ?? 0);
|
|
$by[$key]['totali']['pagato'] += (float) ($r['pagato'] ?? 0);
|
|
$by[$key]['totali']['residuo'] += (float) ($r['residuo'] ?? 0);
|
|
$d = (string) ($r['data_emissione_sort'] ?? '9999-12-31');
|
|
if ($d < $by[$key]['min_emissione']) {
|
|
$by[$key]['min_emissione'] = $d;
|
|
}
|
|
}
|
|
|
|
$groups = array_values($by);
|
|
usort($groups, function ($a, $b): int {
|
|
$ra = (int) ($a['rank'] ?? 9);
|
|
$rb = (int) ($b['rank'] ?? 9);
|
|
if ($ra !== $rb) {
|
|
return $ra <=> $rb;
|
|
}
|
|
return strcmp((string) $a['min_emissione'], (string) $b['min_emissione']);
|
|
});
|
|
|
|
foreach ($groups as &$g) {
|
|
usort($g['rows'], function ($a, $b): int {
|
|
$da = (string) ($a['data_emissione_sort'] ?? '9999-12-31');
|
|
$db = (string) ($b['data_emissione_sort'] ?? '9999-12-31');
|
|
if ($da === $db) {
|
|
return ((int) ($a['id'] ?? 0)) <=> ((int) ($b['id'] ?? 0));
|
|
}
|
|
return strcmp($da, $db);
|
|
});
|
|
unset($g['min_emissione']);
|
|
unset($g['rank']);
|
|
}
|
|
|
|
return $groups;
|
|
}
|
|
|
|
/**
|
|
* Costruisce righe "Bilancio iniziale / esercizi precedenti" per unità e ruolo (C/I) dal dominio.
|
|
* Per evitare collisioni tra import multipli, usa la prima (min) legacy_year disponibile per ogni unità/ruolo.
|
|
*
|
|
* @param \Illuminate\Support\Collection<int, RataEmessaNg> $rateItems
|
|
* @param array<int, UnitaImmobiliare> $unitaAll
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function loadSaldiInizialiRows(int $stabileId, $rateItems, array $unitaAll): array
|
|
{
|
|
if (! DbSchema::hasTable('dettaglio_importi_tabella') || ! DbSchema::hasTable('tabelle_millesimali')) {
|
|
return [];
|
|
}
|
|
if (! DbSchema::hasColumn('tabelle_millesimali', 'stabile_id') || ! DbSchema::hasColumn('tabelle_millesimali', 'codice_tabella')) {
|
|
return [];
|
|
}
|
|
|
|
$unitIds = array_values(array_unique(array_map(fn(UnitaImmobiliare $u) => (int) $u->id, $unitaAll)));
|
|
if (empty($unitIds)) {
|
|
return [];
|
|
}
|
|
|
|
// Inferisci tipo soggetto (C/I) per unità dalle note delle rate
|
|
$tipoPerUnita = [];
|
|
$emittedConguaglioKeys = [];
|
|
foreach ($rateItems as $r) {
|
|
try {
|
|
$uid = is_numeric($r->unita_immobiliare_id ?? null) ? (int) $r->unita_immobiliare_id : null;
|
|
if (! $uid) {
|
|
continue;
|
|
}
|
|
$note = (string) ($r->note ?? '');
|
|
$tipo = $this->extractLegacyRoleFromRateNote($note);
|
|
if (! array_key_exists($uid, $tipoPerUnita) && $tipo !== null) {
|
|
$tipoPerUnita[$uid] = $tipo;
|
|
}
|
|
|
|
if (
|
|
$tipo !== null
|
|
&& str_starts_with(strtolower(trim((string) ($r->descrizione ?? ''))), 'conguaglio')
|
|
) {
|
|
$legacyYear = $this->extractLegacyYearFromRateNote($note);
|
|
$amount = round((float) ($r->importo_addebitato_soggetto ?? 0), 2);
|
|
if ($legacyYear !== null) {
|
|
$emittedConguaglioKeys[$uid . '|' . $this->makeConguaglioDedupKey($tipo, $legacyYear, $amount)] = true;
|
|
}
|
|
}
|
|
} catch (\Throwable $e) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
$domainHasLegacyYear = DbSchema::hasColumn('dettaglio_importi_tabella', 'legacy_year');
|
|
|
|
$base = DB::table('dettaglio_importi_tabella as dit')
|
|
->join('tabelle_millesimali as tm', 'tm.id', '=', 'dit.tabella_millesimale_id')
|
|
->where('tm.stabile_id', $stabileId)
|
|
->whereIn('dit.unita_immobiliare_id', $unitIds)
|
|
->where('tm.codice_tabella', 'like', 'CONG.%')
|
|
->whereNotNull('dit.cons_euro');
|
|
|
|
// Se c'è legacy_year, seleziona solo la prima (min) per unità+ruolo per ottenere la "situazione iniziale"
|
|
if ($domainHasLegacyYear) {
|
|
$minYears = (clone $base)
|
|
->selectRaw('dit.unita_immobiliare_id, dit.ruolo_legacy, MIN(dit.legacy_year) as legacy_year')
|
|
->groupBy('dit.unita_immobiliare_id', 'dit.ruolo_legacy');
|
|
|
|
$base->joinSub($minYears, 'mn', function ($join) {
|
|
$join->on('mn.unita_immobiliare_id', '=', 'dit.unita_immobiliare_id')
|
|
->on('mn.ruolo_legacy', '=', 'dit.ruolo_legacy')
|
|
->on('mn.legacy_year', '=', 'dit.legacy_year');
|
|
});
|
|
}
|
|
|
|
$select = 'dit.unita_immobiliare_id as unita_id, dit.ruolo_legacy as ruolo, tm.codice_tabella as codice_tabella, SUM(dit.cons_euro) as importo';
|
|
if ($domainHasLegacyYear) {
|
|
$select = 'dit.unita_immobiliare_id as unita_id, dit.ruolo_legacy as ruolo, dit.legacy_year as legacy_year, tm.codice_tabella as codice_tabella, SUM(dit.cons_euro) as importo';
|
|
}
|
|
|
|
$rows = $base
|
|
->selectRaw($select)
|
|
->groupBy('dit.unita_immobiliare_id', 'dit.ruolo_legacy', 'tm.codice_tabella')
|
|
->when($domainHasLegacyYear, fn($query) => $query->groupBy('dit.legacy_year'))
|
|
->get();
|
|
|
|
if ($rows->isEmpty()) {
|
|
return [];
|
|
}
|
|
|
|
$unitaById = [];
|
|
foreach ($unitaAll as $u) {
|
|
$unitaById[(int) $u->id] = $u;
|
|
}
|
|
|
|
$out = [];
|
|
$seq = 0;
|
|
foreach ($rows as $r) {
|
|
$unitaId = is_numeric($r->unita_id ?? null) ? (int) $r->unita_id : 0;
|
|
if (! $unitaId || ! array_key_exists($unitaId, $unitaById)) {
|
|
continue;
|
|
}
|
|
|
|
$ruolo = strtoupper(trim((string) ($r->ruolo ?? '')));
|
|
$tipoAtteso = $tipoPerUnita[$unitaId] ?? null;
|
|
if ($tipoAtteso && $ruolo !== $tipoAtteso) {
|
|
continue;
|
|
}
|
|
|
|
$importo = is_numeric($r->importo ?? null) ? (float) $r->importo : 0.0;
|
|
if (abs($importo) < 0.00001) {
|
|
continue;
|
|
}
|
|
|
|
if (isset($emittedConguaglioKeys[$unitaId . '|' . $this->makeConguaglioDedupKey($ruolo, (string) ($domainHasLegacyYear ? ($r->legacy_year ?? '') : ''), $importo)])) {
|
|
continue;
|
|
}
|
|
|
|
$u = $unitaById[$unitaId];
|
|
$unitaShort = $this->formatUnitaShort($u);
|
|
$codTab = trim((string) ($r->codice_tabella ?? ''));
|
|
|
|
$seq++;
|
|
$out[] = [
|
|
'id' => -1000000 - $seq,
|
|
'piano_id' => 0,
|
|
'piano' => 'Bilancio iniziale (esercizi precedenti)',
|
|
'data_emissione' => '',
|
|
'data_emissione_sort' => '1900-01-01',
|
|
'data_scadenza' => null,
|
|
'avviso' => 0,
|
|
'n_emissione' => null,
|
|
'descrizione' => 'Conguagli es. precedenti (' . $codTab . ')',
|
|
'unita_short' => $unitaShort,
|
|
'dovuto' => $importo,
|
|
'data_pagamento' => null,
|
|
'pagato' => 0.0,
|
|
'residuo' => $importo,
|
|
'unita_id' => $unitaId,
|
|
'ruolo' => $ruolo,
|
|
];
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
private function extractLegacyRoleFromRateNote(string $note): ?string
|
|
{
|
|
$note = trim($note);
|
|
if ($note === '') {
|
|
return null;
|
|
}
|
|
|
|
foreach (['tipo_soggetto', 'cond_inq', 'o_r_s'] as $field) {
|
|
if (preg_match('/\b' . preg_quote($field, '/') . '=([CI])\b/', $note, $m)) {
|
|
return (string) ($m[1] ?? null);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function extractLegacyYearFromRateNote(string $note): ?string
|
|
{
|
|
if (preg_match('/\banno_emissione=([^|\s]+)/', $note, $m)) {
|
|
$value = trim((string) ($m[1] ?? ''));
|
|
|
|
return $value !== '' ? $value : null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function extractGestioneLabelFromRateNote(string $note): ?string
|
|
{
|
|
if (preg_match('/\banno_gestione=([^|\s]+)/', $note, $m)) {
|
|
$value = trim((string) ($m[1] ?? ''));
|
|
|
|
return $value !== '' ? $value : null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function extractNumeroRicevutaFromRateNote(string $note): ?int
|
|
{
|
|
if (preg_match('/\bnumero_ricevuta=(\d+)\b/', $note, $m)) {
|
|
$value = (int) ($m[1] ?? 0);
|
|
|
|
return $value > 0 ? $value : null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function extractAvvisoCodeFromRateNote(string $note): ?string
|
|
{
|
|
if (preg_match('/\bavviso_code=([^|\s]+)/', $note, $m)) {
|
|
$value = trim((string) ($m[1] ?? ''));
|
|
|
|
return $value !== '' ? $value : null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function buildLegacyReferenceLabel(?string $gestioneLabel, ?int $numeroRicevuta, ?string $avvisoCode): ?string
|
|
{
|
|
$parts = [];
|
|
|
|
$gestioneLabel = trim((string) ($gestioneLabel ?? ''));
|
|
if ($gestioneLabel !== '') {
|
|
$parts[] = 'Gestione ' . $gestioneLabel;
|
|
}
|
|
|
|
if ($numeroRicevuta !== null && $numeroRicevuta > 0) {
|
|
$parts[] = 'ricevuta n.' . $numeroRicevuta;
|
|
} elseif ($avvisoCode !== null && trim($avvisoCode) !== '') {
|
|
$parts[] = 'avviso ' . trim($avvisoCode);
|
|
}
|
|
|
|
return $parts !== [] ? implode(' · ', $parts) : null;
|
|
}
|
|
|
|
private function makeConguaglioDedupKey(string $role, string $legacyYear, float $amount): string
|
|
{
|
|
return strtoupper(trim($role)) . '|' . trim($legacyYear) . '|' . number_format(round($amount, 2), 2, '.', '');
|
|
}
|
|
|
|
/**
|
|
* Collega incassi al soggetto usando la stessa codifica d'unione delle rate:
|
|
* - tipo_soggetto=([CI])
|
|
* - cod_cond_dest=...
|
|
*
|
|
* In incassi: cond_inquil + cod_cond_gescon (o cod_cond)
|
|
*/
|
|
private function hydrateIncassiForSoggetto(int $stabileId, $rateItems, array $visibleYears = []): void
|
|
{
|
|
$this->incassi = [];
|
|
|
|
if (! DbSchema::hasTable('incassi')) {
|
|
return;
|
|
}
|
|
|
|
$codCol = DbSchema::hasColumn('incassi', 'cod_cond_gescon')
|
|
? 'cod_cond_gescon'
|
|
: (DbSchema::hasColumn('incassi', 'cod_cond') ? 'cod_cond' : null);
|
|
|
|
$tipoCol = DbSchema::hasColumn('incassi', 'cond_inquil')
|
|
? 'cond_inquil'
|
|
: (DbSchema::hasColumn('incassi', 'cond_inq') ? 'cond_inq' : null);
|
|
|
|
if (! $codCol || ! $tipoCol) {
|
|
return;
|
|
}
|
|
|
|
$pairs = [];
|
|
foreach ($rateItems as $r) {
|
|
try {
|
|
$note = (string) ($r->note ?? '');
|
|
if ($note === '') {
|
|
continue;
|
|
}
|
|
$tipo = '';
|
|
$cod = '';
|
|
|
|
if (preg_match('/\btipo_soggetto=([CI])\b/', $note, $mTipo)) {
|
|
$tipo = (string) ($mTipo[1] ?? '');
|
|
} elseif (preg_match('/\bcond_inq=([CI])\b/', $note, $mTipoAlt)) {
|
|
$tipo = (string) ($mTipoAlt[1] ?? '');
|
|
}
|
|
|
|
if (preg_match('/\bcod_cond_dest=([^\s|]+)/', $note, $mCod)) {
|
|
$cod = trim((string) ($mCod[1] ?? ''));
|
|
} elseif (preg_match('/\bcod_cond_src=([^\s|]+)/', $note, $mCodAlt)) {
|
|
$cod = trim((string) ($mCodAlt[1] ?? ''));
|
|
}
|
|
|
|
if ($tipo === '' || $cod === '') {
|
|
continue;
|
|
}
|
|
|
|
$pairs[$tipo . '|' . $cod] = ['tipo' => $tipo, 'cod' => $cod];
|
|
} catch (\Throwable $e) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (empty($pairs)) {
|
|
return;
|
|
}
|
|
|
|
$incassiQuery = Incasso::query();
|
|
|
|
// Per NetGescon NG, l'import incassi salva condominio_id = stabileId.
|
|
if (DbSchema::hasColumn('incassi', 'condominio_id')) {
|
|
$incassiQuery->where('condominio_id', $stabileId);
|
|
}
|
|
|
|
$incassiQuery->where(function ($q) use ($pairs, $codCol, $tipoCol): void {
|
|
foreach ($pairs as $p) {
|
|
$q->orWhere(function ($qq) use ($p, $codCol, $tipoCol): void {
|
|
$qq->where($codCol, (string) $p['cod'])
|
|
->where($tipoCol, (string) $p['tipo']);
|
|
});
|
|
}
|
|
});
|
|
|
|
$orderCol = DbSchema::hasColumn('incassi', 'dt_empag')
|
|
? 'dt_empag'
|
|
: (DbSchema::hasColumn('incassi', 'data_pagamento') ? 'data_pagamento' : 'id');
|
|
|
|
$incassi = $incassiQuery
|
|
->orderByDesc($orderCol)
|
|
->orderByDesc('id')
|
|
->limit(2000)
|
|
->get()
|
|
->filter(fn(Incasso $i): bool => GestioneVisibility::matchesYear($this->resolveIncassoCalendarYear($i), $visibleYears))
|
|
->values();
|
|
|
|
$contoMovimentiBase = CasseBancheMovimenti::getUrl(panel: 'admin-filament');
|
|
|
|
$this->incassi = $incassi
|
|
->map(function (Incasso $i) use ($codCol, $tipoCol, $contoMovimentiBase): array {
|
|
$codCond = (string) (data_get($i, $codCol) ?? '');
|
|
$tipo = (string) (data_get($i, $tipoCol) ?? '');
|
|
|
|
$importo = (float) ($i->importo_pagato_euro ?? $i->importo_pagato ?? 0);
|
|
|
|
$dt = null;
|
|
if (! empty($i->dt_empag)) {
|
|
try {
|
|
$dt = Carbon::parse($i->dt_empag);
|
|
} catch (\Throwable $e) {
|
|
$dt = null;
|
|
}
|
|
}
|
|
$dp = null;
|
|
if (! empty($i->data_pagamento)) {
|
|
try {
|
|
$dp = Carbon::parse($i->data_pagamento);
|
|
} catch (\Throwable $e) {
|
|
$dp = null;
|
|
}
|
|
}
|
|
|
|
$annoRaw = $i->anno_rif;
|
|
$annoRawStr = is_null($annoRaw) ? '' : (string) $annoRaw;
|
|
$annoNum = is_numeric($annoRawStr) ? (int) $annoRawStr : null;
|
|
|
|
$annoUmano = null;
|
|
if ($annoNum !== null && $annoNum >= 1900) {
|
|
$annoUmano = $annoNum;
|
|
} elseif ($dt) {
|
|
$annoUmano = (int) $dt->format('Y');
|
|
} elseif ($dp) {
|
|
$annoUmano = (int) $dp->format('Y');
|
|
}
|
|
|
|
$annoLabel = $annoUmano ? (string) $annoUmano : ($annoRawStr !== '' ? ltrim($annoRawStr, '0') : '—');
|
|
if ($annoUmano && $annoNum !== null && $annoNum > 0 && $annoNum < 1900) {
|
|
$annoLabel .= ' (' . str_pad((string) $annoNum, 4, '0', STR_PAD_LEFT) . ')';
|
|
}
|
|
|
|
$contoId = is_numeric($i->conto_bancario_id ?? null) ? (int) $i->conto_bancario_id : null;
|
|
$contoUrl = $contoId ? ($contoMovimentiBase . '?conto_id=' . $contoId) : null;
|
|
|
|
$stato = trim((string) ($i->stato_riconciliazione ?? ''));
|
|
$conf = is_numeric($i->confidenza_match ?? null) ? (float) $i->confidenza_match : null;
|
|
$hasMov = ! empty($i->movimento_bancario_id);
|
|
|
|
$semaforo = 'DA RICONCILIARE';
|
|
if ($hasMov || strtolower($stato) === 'riconciliato') {
|
|
$semaforo = 'OK';
|
|
} elseif ($conf !== null && $conf >= 0.7) {
|
|
$semaforo = 'MATCH';
|
|
}
|
|
|
|
$codCassa = (string) ($i->cod_cassa_gescon ?? $i->cod_cassa ?? $i->cod_cassa_legacy ?? $i->cod_cassa_gescon ?? '');
|
|
|
|
return [
|
|
'id' => (int) $i->id,
|
|
'tipo' => $tipo,
|
|
'cod_cond' => $codCond,
|
|
'anno' => $annoLabel,
|
|
'n_ricevuta' => $i->n_ricevuta ?? null,
|
|
'dt_empag' => $dt ? $dt->format('d/m/Y') : null,
|
|
'data_pagamento' => $dp ? $dp->format('d/m/Y') : null,
|
|
'importo' => $importo,
|
|
'descrizione' => (string) ($i->descrizione ?? ''),
|
|
'cod_cassa' => $codCassa,
|
|
'conto_id' => $contoId,
|
|
'conto_url' => $contoUrl,
|
|
'stato_riconciliazione' => $stato,
|
|
'confidenza_match' => $conf,
|
|
'semaforo' => $semaforo,
|
|
];
|
|
})
|
|
->all();
|
|
}
|
|
|
|
private function resolveIncassoCalendarYear(Incasso $incasso): ?int
|
|
{
|
|
$year = GestioneVisibility::normalizeCalendarYear($incasso->anno_rif ?? null);
|
|
if ($year !== null) {
|
|
return $year;
|
|
}
|
|
|
|
$year = GestioneVisibility::normalizeCalendarYear($incasso->anno ?? null);
|
|
if ($year !== null) {
|
|
return $year;
|
|
}
|
|
|
|
try {
|
|
if (! empty($incasso->dt_empag)) {
|
|
return GestioneVisibility::normalizeCalendarYear(Carbon::parse($incasso->dt_empag)->format('Y'));
|
|
}
|
|
if (! empty($incasso->data_pagamento)) {
|
|
return GestioneVisibility::normalizeCalendarYear(Carbon::parse($incasso->data_pagamento)->format('Y'));
|
|
}
|
|
} catch (\Throwable $e) {
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|