1535 lines
60 KiB
PHP
1535 lines
60 KiB
PHP
<?php
|
|
namespace App\Filament\Pages\Affitti;
|
|
|
|
use App\Models\AffittoCanoneDovuto;
|
|
use App\Models\AffittoCanonePagato;
|
|
use App\Models\AffittoClausola;
|
|
use App\Models\AffittoContrattoClausola;
|
|
use App\Models\AffittoImmobile;
|
|
use App\Models\AffittoInquilino;
|
|
use App\Models\DatiBancari;
|
|
use App\Models\DocumentoStabile;
|
|
use App\Models\RubricaUniversale;
|
|
use App\Models\Stabile;
|
|
use App\Models\StabileServizio;
|
|
use App\Models\UnitaImmobiliare;
|
|
use App\Services\Affitti\IstatRivalutazioneService;
|
|
use App\Support\StabileContext;
|
|
use BackedEnum;
|
|
use Filament\Pages\Page;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Str;
|
|
use Livewire\Attributes\Url;
|
|
use Livewire\WithFileUploads;
|
|
use Symfony\Component\Process\Process;
|
|
use UnitEnum;
|
|
|
|
class GestioneAffitti extends Page
|
|
{
|
|
use WithFileUploads;
|
|
protected static ?string $navigationLabel = 'Gestione Affitti';
|
|
protected static ?string $title = 'Gestione Affitti';
|
|
protected static UnitEnum|string|null $navigationGroup = 'Stabile';
|
|
protected static ?string $slug = 'affitti/gestione';
|
|
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-home-modern';
|
|
|
|
protected string $view = 'filament.pages.affitti.gestione-affitti';
|
|
|
|
#[Url( as : 'tab')]
|
|
public string $tab = 'elenco';
|
|
public string $anagraficaSection = 'anagrafica';
|
|
public string $elencoTipoFiltro = 'tutti';
|
|
public string $elencoStatoContrattoFiltro = 'attivi';
|
|
|
|
public array $affittiOptions = [];
|
|
public array $affittiList = [];
|
|
public ?int $selectedAffittoId = null;
|
|
public array $form = [];
|
|
|
|
public bool $forceAll = false;
|
|
public ?int $stabileFilterId = null;
|
|
public array $stabileOptions = [];
|
|
|
|
public array $unitaOptions = [];
|
|
public array $localiTecnici = [];
|
|
|
|
public array $rubricaOptions = [];
|
|
public array $contoOptions = [];
|
|
|
|
public ?int $selectedStabileRubricaId = null;
|
|
public ?string $selectedStabileCode = null;
|
|
public ?string $selectedStabileName = null;
|
|
public bool $selectedAffittoObsoleto = false;
|
|
|
|
public array $servizi = [];
|
|
public array $serviziCatalogo = [
|
|
'assicurazione' => 'Assicurazione',
|
|
'ascensore' => 'Ascensore',
|
|
'riscaldamento' => 'Riscaldamento',
|
|
'antenna' => 'Impianto antenna',
|
|
'acqua' => 'Ripartizione acqua',
|
|
'locale_biciclette' => 'Locale biciclette',
|
|
'altro' => 'Altro servizio',
|
|
];
|
|
|
|
public array $canoniDovuti = [];
|
|
public array $canoniPagati = [];
|
|
public array $canoniRighe = [];
|
|
|
|
public array $inquilini = [];
|
|
public array $clausoleContratto = [];
|
|
public array $clausoleCatalogo = [];
|
|
public array $contractTemplates = [];
|
|
public ?string $selectedContractTemplate = null;
|
|
public ?string $contractDraftPreview = null;
|
|
public array $totali = [
|
|
'dovuti' => 0,
|
|
'pagati' => 0,
|
|
'saldo' => 0,
|
|
];
|
|
|
|
public $documentFile;
|
|
public ?string $documentDescrizione = null;
|
|
public ?string $documentCategoria = 'contratti';
|
|
public ?string $documentDataScadenza = null;
|
|
public ?int $documentInquilinoRow = null;
|
|
public string $legacyMdbPath = '/mnt/gescon-archives/gescon/parti_comuni.mdb';
|
|
public ?string $legacyLastSyncMessage = null;
|
|
|
|
public static function canAccess(): bool
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof \App\Models\User) {
|
|
return false;
|
|
}
|
|
return $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
|
}
|
|
|
|
public function mount(): void
|
|
{
|
|
$this->initContractTemplates();
|
|
$this->tab = in_array($this->tab, ['elenco', 'anagrafica', 'banca', 'scadenze', 'canoni', 'documenti'], true)
|
|
? $this->tab
|
|
: 'elenco';
|
|
$this->loadStabiliOptions();
|
|
if (! $this->forceAll) {
|
|
$this->stabileFilterId = $this->resolveStabileIdFromContext();
|
|
}
|
|
$this->loadAffitti();
|
|
if (! $this->selectedAffittoId && ! empty($this->affittiOptions)) {
|
|
$this->selectedAffittoId = (int) array_key_first($this->affittiOptions);
|
|
}
|
|
$this->loadUnita();
|
|
$this->loadServizi();
|
|
$this->loadLocaliTecnici();
|
|
$this->loadRubricaOptions();
|
|
$this->loadContoOptions();
|
|
$this->loadClausoleCatalogo();
|
|
$this->loadSelected();
|
|
}
|
|
|
|
public function updatedStabileFilterId(): void
|
|
{
|
|
$this->loadAffitti();
|
|
if (! $this->selectedAffittoId && ! empty($this->affittiOptions)) {
|
|
$this->selectedAffittoId = (int) array_key_first($this->affittiOptions);
|
|
}
|
|
$this->loadUnita();
|
|
$this->loadServizi();
|
|
$this->loadLocaliTecnici();
|
|
$this->loadSelected();
|
|
}
|
|
|
|
public function updatedSelectedAffittoId(): void
|
|
{
|
|
$this->loadSelected();
|
|
}
|
|
|
|
public function selectAffitto(int $id): void
|
|
{
|
|
$this->selectedAffittoId = $id;
|
|
$this->loadSelected();
|
|
$this->setTab('scadenze');
|
|
}
|
|
|
|
public function setTab(string $tab): void
|
|
{
|
|
$this->tab = in_array($tab, ['elenco', 'anagrafica', 'banca', 'scadenze', 'canoni', 'documenti'], true) ? $tab : 'elenco';
|
|
}
|
|
|
|
public function setAnagraficaSection(string $section): void
|
|
{
|
|
$this->anagraficaSection = in_array($section, ['anagrafica', 'catasto', 'inquilini'], true)
|
|
? $section
|
|
: 'anagrafica';
|
|
}
|
|
|
|
public function mergeCatastoFromUnita(bool $force = false): void
|
|
{
|
|
if (! $this->selectedAffittoId) {
|
|
return;
|
|
}
|
|
|
|
$record = AffittoImmobile::find($this->selectedAffittoId);
|
|
if (! $record) {
|
|
return;
|
|
}
|
|
|
|
$unit = $this->resolveCandidateUnita($record);
|
|
if (! $unit) {
|
|
$this->dispatch('notify', ['type' => 'warning', 'message' => 'Nessuna unità immobiliare trovata da agganciare al contratto.']);
|
|
return;
|
|
}
|
|
|
|
if (! $record->unita_immobiliare_id) {
|
|
$record->unita_immobiliare_id = $unit->id;
|
|
}
|
|
|
|
$map = [
|
|
'sezione_urbana' => $unit->sezione_urbana,
|
|
'foglio' => $unit->foglio,
|
|
'particella' => $unit->particella,
|
|
'subalterno' => $unit->subalterno,
|
|
'categoria_catastale' => $unit->categoria_catastale,
|
|
'classe_catastale' => $unit->classe,
|
|
'consistenza_catastale' => $unit->consistenza,
|
|
'rendita_catastale' => $unit->rendita_catastale,
|
|
'interno_catastale' => $unit->interno,
|
|
'piano_catastale' => $unit->piano,
|
|
];
|
|
|
|
$surface = $unit->superficie_commerciale ?? $unit->superficie_calpestabile;
|
|
if ($surface !== null) {
|
|
$map['superficie_catastale_mq'] = $surface;
|
|
}
|
|
if (! empty($unit->tipo_unita)) {
|
|
$map['destinazione_uso'] = $unit->tipo_unita;
|
|
}
|
|
|
|
foreach ($map as $field => $value) {
|
|
$current = $record->{$field};
|
|
if ($force || $this->isEmptyValue($current)) {
|
|
$record->{$field} = $value;
|
|
}
|
|
}
|
|
|
|
$record->save();
|
|
$this->loadSelected();
|
|
$this->dispatch('notify', ['type' => 'success', 'message' => 'Dati catastali uniti ai valori dell\'unità immobiliare.']);
|
|
}
|
|
|
|
public function generateContractDraft(): void
|
|
{
|
|
if (! $this->selectedAffittoId) {
|
|
return;
|
|
}
|
|
|
|
$record = AffittoImmobile::with(['stabile.amministratore', 'rubricaInquilino', 'inquilini.rubrica', 'unitaImmobiliare'])->find($this->selectedAffittoId);
|
|
if (! $record) {
|
|
return;
|
|
}
|
|
|
|
$templateKey = $this->selectedContractTemplate ?: 'abitativo_431_2';
|
|
if (! isset($this->contractTemplates[$templateKey])) {
|
|
$this->dispatch('notify', ['type' => 'warning', 'message' => 'Template contratto non disponibile.']);
|
|
return;
|
|
}
|
|
|
|
$stabile = $record->stabile;
|
|
$amministratore = $stabile?->amministratore;
|
|
$inquilino = $record->rubricaInquilino;
|
|
if (! $inquilino) {
|
|
$inquilino = $record->inquilini()->where('attivo', true)->with('rubrica')->first()?->rubrica;
|
|
}
|
|
|
|
$locatoreDenominazione = trim((string) ($stabile?->denominazione ?? 'Condominio'));
|
|
$locatoreCf = trim((string) ($stabile?->codice_fiscale ?? ''));
|
|
$locatoreIndirizzo = trim((string) ($stabile?->indirizzo_completo ?? ''));
|
|
|
|
$amministratoreNome = trim((string) (($stabile?->amministratore_nome ?: null) ?? (($amministratore?->nome ?? '') . ' ' . ($amministratore?->cognome ?? ''))));
|
|
$amministratoreCf = trim((string) (($stabile?->cod_fisc_amministratore ?: null) ?? ($amministratore?->codice_fiscale_studio ?? '')));
|
|
|
|
$conduttriceNome = trim((string) ($inquilino?->nome_completo ?? $record->nome_inquilino ?? ''));
|
|
$conduttriceCf = trim((string) ($inquilino?->codice_fiscale ?? ''));
|
|
|
|
$descUnita = trim((string) ($record->descrizione_immobile ?? 'unità immobiliare'));
|
|
$indirizzoUnita = trim((string) ($record->indirizzo_immobile ?: $locatoreIndirizzo));
|
|
$catasto = 'Foglio ' . ($record->foglio ?? '---')
|
|
. ' - Particella ' . ($record->particella ?? '---')
|
|
. ' - Sub ' . ($record->subalterno ?? '---')
|
|
. ' - Cat. ' . ($record->categoria_catastale ?? '---')
|
|
. ' con rendita catastale di EUR ' . number_format((float) ($record->rendita_catastale ?? 0), 2, ',', '.');
|
|
|
|
$dataInizio = $record->inizio_contratto ? Carbon::parse($record->inizio_contratto)->format('d/m/Y') : '---';
|
|
$dataFine = $record->data_fine_contratto ? Carbon::parse($record->data_fine_contratto)->format('d/m/Y') : '---';
|
|
$canoneMensile = number_format((float) ($record->importo_fitto ?? 0), 2, ',', '.');
|
|
|
|
$this->contractDraftPreview = "CONTRATTO DI LOCAZIONE AD USO ABITATIVO\n"
|
|
. "(Ai sensi dell'art. 2 della legge 9 dicembre 1998, n. 431)\n\n"
|
|
. "Tra\n"
|
|
. "{$locatoreDenominazione}" . ($locatoreIndirizzo !== '' ? ", {$locatoreIndirizzo}" : '')
|
|
. ($locatoreCf !== '' ? ", Cod. Fisc. {$locatoreCf}" : '')
|
|
. ", di seguito denominato locatore;\n"
|
|
. "nella persona dell'amministratore pro-tempore {$amministratoreNome}"
|
|
. ($amministratoreCf !== '' ? ", Cod. Fisc. {$amministratoreCf}" : '') . ".\n\n"
|
|
. "E\n"
|
|
. "{$conduttriceNome}" . ($conduttriceCf !== '' ? ", Cod. Fisc. {$conduttriceCf}" : '')
|
|
. ", di seguito denominata conduttrice.\n\n"
|
|
. "CONCEDE IN LOCAZIONE\n"
|
|
. "{$descUnita} posta in {$indirizzoUnita}.\n"
|
|
. "Dati catastali: {$catasto}.\n\n"
|
|
. "SI CONVIENE E STIPULA QUANTO SEGUE\n"
|
|
. "1) Durata: dal {$dataInizio} al {$dataFine}.\n"
|
|
. "2) Canone mensile: EUR {$canoneMensile}.\n"
|
|
. "3) Clausole aggiuntive: come da sezione Clausole e postille del gestionale.\n\n"
|
|
. "Luogo e data: __________________________\n"
|
|
. "Firma locatore: _________________________\n"
|
|
. "Firma conduttrice: ______________________\n";
|
|
|
|
$this->dispatch('notify', ['type' => 'success', 'message' => 'Bozza contratto generata dal template selezionato.']);
|
|
}
|
|
|
|
public function createAffitto(): void
|
|
{
|
|
$stabileId = $this->resolveStabileId();
|
|
if (! $stabileId) {
|
|
$this->dispatch('notify', ['type' => 'warning', 'message' => 'Seleziona prima uno stabile.']);
|
|
return;
|
|
}
|
|
$record = AffittoImmobile::create([
|
|
'stabile_id' => $stabileId,
|
|
'codice_stabile_legacy' => $this->resolveStabileLegacyCode($stabileId),
|
|
'codice_appartamento' => 'NEW-' . now()->format('YmdHis'),
|
|
'descrizione_immobile' => 'Nuovo immobile',
|
|
]);
|
|
$this->loadAffitti();
|
|
$this->selectedAffittoId = $record->id;
|
|
$this->loadSelected();
|
|
}
|
|
|
|
public function saveAffitto(): void
|
|
{
|
|
if (! $this->selectedAffittoId) {
|
|
return;
|
|
}
|
|
|
|
$record = AffittoImmobile::find($this->selectedAffittoId);
|
|
if (! $record) {
|
|
return;
|
|
}
|
|
|
|
$record->fill($this->form);
|
|
if (! $record->stabile_id) {
|
|
$record->stabile_id = $this->resolveStabileId();
|
|
}
|
|
if (! $record->codice_stabile_legacy) {
|
|
$record->codice_stabile_legacy = $this->resolveStabileLegacyCode($record->stabile_id);
|
|
}
|
|
$record->save();
|
|
|
|
$this->saveInquilini($record);
|
|
$this->saveClausoleContratto($record);
|
|
$this->emitCanoniAutomaticiForRecord($record);
|
|
|
|
$this->loadAffitti();
|
|
$this->loadSelected();
|
|
$this->dispatch('notify', ['type' => 'success', 'message' => 'Affitto aggiornato.']);
|
|
}
|
|
|
|
public function markContrattoObsoleto(): void
|
|
{
|
|
if (! $this->selectedAffittoId) {
|
|
return;
|
|
}
|
|
|
|
$record = AffittoImmobile::find($this->selectedAffittoId);
|
|
if (! $record) {
|
|
return;
|
|
}
|
|
|
|
$record->obsoleto = true;
|
|
$record->obsoleto_dal = now()->toDateString();
|
|
if (empty($record->motivo_obsolescenza)) {
|
|
$record->motivo_obsolescenza = 'Contratto non piu in essere';
|
|
}
|
|
$record->save();
|
|
|
|
$this->loadAffitti();
|
|
$this->loadSelected();
|
|
$this->dispatch('notify', ['type' => 'success', 'message' => 'Contratto marcato come obsoleto e nascosto dagli attivi.']);
|
|
}
|
|
|
|
public function riattivaContratto(): void
|
|
{
|
|
if (! $this->selectedAffittoId) {
|
|
return;
|
|
}
|
|
|
|
$record = AffittoImmobile::find($this->selectedAffittoId);
|
|
if (! $record) {
|
|
return;
|
|
}
|
|
|
|
$record->obsoleto = false;
|
|
$record->obsoleto_dal = null;
|
|
$record->save();
|
|
|
|
$this->loadAffitti();
|
|
$this->loadSelected();
|
|
$this->dispatch('notify', ['type' => 'success', 'message' => 'Contratto riattivato.']);
|
|
}
|
|
|
|
public function emitCanoniAutomatici(): void
|
|
{
|
|
if (! $this->selectedAffittoId) {
|
|
return;
|
|
}
|
|
|
|
$record = AffittoImmobile::find($this->selectedAffittoId);
|
|
if (! $record) {
|
|
return;
|
|
}
|
|
|
|
$created = $this->emitCanoniAutomaticiForRecord($record);
|
|
$this->loadSelected();
|
|
|
|
$this->dispatch('notify', [
|
|
'type' => 'success',
|
|
'message' => 'Emissione automatica completata: ' . $created . ' rate create/aggiornate.',
|
|
]);
|
|
}
|
|
|
|
public function addInquilino(): void
|
|
{
|
|
$this->inquilini[] = [
|
|
'id' => null,
|
|
'rubrica_inquilino_id' => null,
|
|
'nome_visualizzato' => null,
|
|
'email' => null,
|
|
'telefono' => null,
|
|
'documenti_note' => null,
|
|
'attivo' => true,
|
|
];
|
|
}
|
|
|
|
public function removeInquilino(int $index): void
|
|
{
|
|
if (! isset($this->inquilini[$index])) {
|
|
return;
|
|
}
|
|
unset($this->inquilini[$index]);
|
|
$this->inquilini = array_values($this->inquilini);
|
|
}
|
|
|
|
public function addClausolaContratto(): void
|
|
{
|
|
$this->clausoleContratto[] = [
|
|
'id' => null,
|
|
'clausola_id' => null,
|
|
'titolo' => null,
|
|
'testo' => null,
|
|
'periodicita' => null,
|
|
'importo' => null,
|
|
'ordine' => count($this->clausoleContratto) + 1,
|
|
'attivo' => true,
|
|
];
|
|
}
|
|
|
|
public function removeClausolaContratto(int $index): void
|
|
{
|
|
if (! isset($this->clausoleContratto[$index])) {
|
|
return;
|
|
}
|
|
unset($this->clausoleContratto[$index]);
|
|
$this->clausoleContratto = array_values($this->clausoleContratto);
|
|
}
|
|
|
|
public function useClausolaTemplate(int $index): void
|
|
{
|
|
if (! isset($this->clausoleContratto[$index])) {
|
|
return;
|
|
}
|
|
|
|
$clausolaId = (int) ($this->clausoleContratto[$index]['clausola_id'] ?? 0);
|
|
if ($clausolaId <= 0) {
|
|
return;
|
|
}
|
|
|
|
$template = AffittoClausola::find($clausolaId);
|
|
if (! $template) {
|
|
return;
|
|
}
|
|
|
|
$this->clausoleContratto[$index]['titolo'] = $template->titolo;
|
|
$this->clausoleContratto[$index]['testo'] = $template->testo;
|
|
$this->clausoleContratto[$index]['attivo'] = (bool) $template->attivo;
|
|
}
|
|
|
|
public function cleanCanoniDuplicates(): void
|
|
{
|
|
if (! $this->selectedAffittoId) {
|
|
return;
|
|
}
|
|
|
|
$removed = 0;
|
|
$removed += $this->deleteCanoniDuplicates(AffittoCanoneDovuto::query()->where('affitto_id', $this->selectedAffittoId)->get()->all());
|
|
$removed += $this->deleteCanoniDuplicates(AffittoCanonePagato::query()->where('affitto_id', $this->selectedAffittoId)->get()->all());
|
|
|
|
$this->loadSelected();
|
|
$this->dispatch('notify', [
|
|
'type' => 'success',
|
|
'message' => 'Pulizia completata: rimossi ' . $removed . ' duplicati canoni.',
|
|
]);
|
|
}
|
|
|
|
public function syncAffittiFromLegacy(): void
|
|
{
|
|
$mdb = trim((string) $this->legacyMdbPath);
|
|
if ($mdb === '' || ! is_file($mdb)) {
|
|
$this->dispatch('notify', ['type' => 'warning', 'message' => 'Percorso MDB non valido.']);
|
|
return;
|
|
}
|
|
|
|
$cmd = ['php', 'artisan', 'affitti:import-mdb', '--mdb=' . $mdb, '--refresh-canoni'];
|
|
$stabileLegacy = $this->resolveStabileLegacyCode($this->resolveStabileId());
|
|
if (! empty($stabileLegacy)) {
|
|
$cmd[] = '--stabile=' . $stabileLegacy;
|
|
}
|
|
|
|
$process = new Process($cmd, base_path());
|
|
$process->setTimeout(900);
|
|
$process->run();
|
|
|
|
if (! $process->isSuccessful()) {
|
|
$this->legacyLastSyncMessage = trim($process->getErrorOutput()) ?: 'Errore durante il sync affitti da legacy.';
|
|
$this->dispatch('notify', ['type' => 'danger', 'message' => 'Sync legacy fallito. Controlla output.']);
|
|
return;
|
|
}
|
|
|
|
$this->legacyLastSyncMessage = trim($process->getOutput()) ?: 'Sincronizzazione completata.';
|
|
$this->loadAffitti();
|
|
$this->loadSelected();
|
|
$this->dispatch('notify', ['type' => 'success', 'message' => 'Dati affitti aggiornati dal legacy MDB.']);
|
|
}
|
|
|
|
public function uploadDocumento(): void
|
|
{
|
|
if (! $this->selectedAffittoId || ! $this->documentFile) {
|
|
return;
|
|
}
|
|
|
|
$affitto = AffittoImmobile::find($this->selectedAffittoId);
|
|
if (! $affitto || ! $affitto->stabile_id) {
|
|
return;
|
|
}
|
|
|
|
$descrizione = $this->documentDescrizione;
|
|
if ($this->documentInquilinoRow !== null && isset($this->inquilini[$this->documentInquilinoRow])) {
|
|
$inq = $this->inquilini[$this->documentInquilinoRow];
|
|
$inqName = trim((string) ($inq['nome_visualizzato'] ?? ''));
|
|
if ($inqName === '' && ! empty($inq['rubrica_inquilino_id'])) {
|
|
$inqName = (string) (RubricaUniversale::find((int) $inq['rubrica_inquilino_id'])?->nome_completo ?? '');
|
|
}
|
|
if ($inqName !== '') {
|
|
$prefix = 'Inquilino: ' . $inqName;
|
|
$descrizione = trim($prefix . ($descrizione ? ' · ' . $descrizione : ''));
|
|
}
|
|
}
|
|
|
|
$file = $this->documentFile;
|
|
$path = $file->store('documenti_stabili/' . $affitto->stabile_id . '/affitti');
|
|
$size = $file->getSize();
|
|
$mime = $file->getMimeType();
|
|
$name = basename($path);
|
|
|
|
DocumentoStabile::create([
|
|
'stabile_id' => $affitto->stabile_id,
|
|
'nome_file' => $name,
|
|
'nome_originale' => $file->getClientOriginalName(),
|
|
'percorso_file' => $path,
|
|
'categoria' => $this->documentCategoria ?: 'contratti',
|
|
'tipo_mime' => $mime ?: 'application/octet-stream',
|
|
'dimensione' => $size ?: 0,
|
|
'descrizione' => $descrizione,
|
|
'data_scadenza' => $this->documentDataScadenza ?: null,
|
|
'tags' => 'affitti,contratto',
|
|
'pubblico' => false,
|
|
'protetto' => false,
|
|
'versione' => 1,
|
|
'downloads' => 0,
|
|
'caricato_da' => Auth::id(),
|
|
]);
|
|
|
|
$this->documentFile = null;
|
|
$this->documentDescrizione = null;
|
|
$this->documentDataScadenza = null;
|
|
$this->documentInquilinoRow = null;
|
|
$this->dispatch('notify', ['type' => 'success', 'message' => 'Documento caricato.']);
|
|
}
|
|
|
|
public function saveServizi(): void
|
|
{
|
|
$stabileId = $this->resolveStabileId();
|
|
if (! $stabileId) {
|
|
return;
|
|
}
|
|
|
|
foreach ($this->serviziCatalogo as $key => $label) {
|
|
$row = $this->servizi[$key] ?? [];
|
|
$payload = [
|
|
'stabile_id' => $stabileId,
|
|
'tipo' => $key,
|
|
'nome' => $label,
|
|
'attivo' => (bool) ($row['attivo'] ?? false),
|
|
'meta' => [
|
|
'data_scadenza' => $row['data_scadenza'] ?? null,
|
|
'note' => $row['note'] ?? null,
|
|
],
|
|
];
|
|
StabileServizio::updateOrCreate(
|
|
['stabile_id' => $stabileId, 'tipo' => $key],
|
|
$payload
|
|
);
|
|
}
|
|
$this->loadServizi();
|
|
$this->dispatch('notify', ['type' => 'success', 'message' => 'Servizi aggiornati.']);
|
|
}
|
|
|
|
private function loadAffitti(): void
|
|
{
|
|
$stabileId = $this->resolveStabileId();
|
|
$query = AffittoImmobile::query();
|
|
if ($stabileId) {
|
|
$query->where('stabile_id', $stabileId);
|
|
}
|
|
$rows = $query
|
|
->withSum('canoniDovuti as dovuti_totale', 'totale')
|
|
->withSum('canoniPagati as pagati_totale', 'totale')
|
|
->orderBy('proprietario_nome')
|
|
->orderBy('codice_appartamento')
|
|
->get();
|
|
$this->affittiOptions = $rows->mapWithKeys(function (AffittoImmobile $a) {
|
|
$label = trim(($a->codice_appartamento ?? '—') . ' · ' . ($a->descrizione_immobile ?? 'Immobiliare'));
|
|
return [$a->id => $label];
|
|
})->all();
|
|
$this->affittiList = $rows->map(fn(AffittoImmobile $a) => [
|
|
'warnings' => $this->buildAffittoWarnings($a),
|
|
'id' => $a->id,
|
|
'codice_appartamento' => $a->codice_appartamento,
|
|
'descrizione_immobile' => $a->descrizione_immobile,
|
|
'nome_inquilino' => $a->nome_inquilino,
|
|
'proprietario_nome' => $a->proprietario_nome,
|
|
'inizio_contratto' => $a->inizio_contratto,
|
|
'presa_in_carico_operativa_dal' => $a->presa_in_carico_operativa_dal,
|
|
'prossima_registrazione' => $a->prossima_registrazione,
|
|
'tipo_riga' => $a->tipo_riga,
|
|
'stabile_id' => $a->stabile_id,
|
|
'dovuti_totale' => (float) ($a->dovuti_totale ?? 0),
|
|
'pagati_totale' => (float) ($a->pagati_totale ?? 0),
|
|
'saldo_totale' => (float) ($a->dovuti_totale ?? 0) - (float) ($a->pagati_totale ?? 0),
|
|
'obsoleto' => (bool) ($a->obsoleto ?? false),
|
|
'obsoleto_dal' => $a->obsoleto_dal,
|
|
'stabile' => $a->stabile?->codice_univoco ?: ($a->stabile?->codice_stabile ?: $a->codice_stabile_legacy),
|
|
'stabile_nome' => $a->stabile?->denominazione,
|
|
])->all();
|
|
}
|
|
|
|
private function buildAffittoWarnings(AffittoImmobile $affitto): array
|
|
{
|
|
$warnings = [];
|
|
$today = Carbon::today();
|
|
|
|
if ($affitto->prossima_scadenza) {
|
|
$days = $today->diffInDays(Carbon::parse($affitto->prossima_scadenza), false);
|
|
if ($days <= 60) {
|
|
$warnings[] = $days < 0
|
|
? 'Rinnovo contratto scaduto da ' . abs($days) . ' giorni'
|
|
: 'Rinnovo contratto entro ' . $days . ' giorni';
|
|
}
|
|
}
|
|
|
|
if ($affitto->prossima_registrazione) {
|
|
$days = $today->diffInDays(Carbon::parse($affitto->prossima_registrazione), false);
|
|
if ($days <= 45) {
|
|
$warnings[] = $days < 0
|
|
? 'Registrazione AdE scaduta da ' . abs($days) . ' giorni'
|
|
: 'Registrazione AdE entro ' . $days . ' giorni';
|
|
}
|
|
}
|
|
|
|
$managementStart = $this->resolveCanoniManagementStart($affitto);
|
|
|
|
$scadutiNonPagati = AffittoCanoneDovuto::query()
|
|
->where('affitto_id', $affitto->id)
|
|
->when($managementStart, function ($query) use ($managementStart) {
|
|
$query->where(function ($inner) use ($managementStart) {
|
|
$inner->whereDate('data_scadenza', '>=', $managementStart->toDateString())
|
|
->orWhere(function ($fallback) use ($managementStart) {
|
|
$fallback->whereNull('data_scadenza')
|
|
->where(function ($monthQuery) use ($managementStart) {
|
|
$monthQuery->where('anno', '>', (int) $managementStart->year)
|
|
->orWhere(function ($sameYear) use ($managementStart) {
|
|
$sameYear->where('anno', (int) $managementStart->year)
|
|
->where('mese', '>=', (int) $managementStart->month);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
})
|
|
->whereDate('data_scadenza', '<', $today->toDateString())
|
|
->sum('totale')
|
|
- AffittoCanonePagato::query()
|
|
->where('affitto_id', $affitto->id)
|
|
->when($managementStart, function ($query) use ($managementStart) {
|
|
$query->where(function ($inner) use ($managementStart) {
|
|
$inner->whereDate('data_pagamento', '>=', $managementStart->toDateString())
|
|
->orWhere(function ($fallback) use ($managementStart) {
|
|
$fallback->whereNull('data_pagamento')
|
|
->where(function ($monthQuery) use ($managementStart) {
|
|
$monthQuery->where('anno', '>', (int) $managementStart->year)
|
|
->orWhere(function ($sameYear) use ($managementStart) {
|
|
$sameYear->where('anno', (int) $managementStart->year)
|
|
->where('mese', '>=', (int) $managementStart->month);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
})
|
|
->whereDate('data_pagamento', '<=', $today->toDateString())
|
|
->sum('totale');
|
|
|
|
if ($scadutiNonPagati > 0.01) {
|
|
$warnings[] = 'Canoni scaduti non pagati: EUR ' . number_format($scadutiNonPagati, 2, ',', '.');
|
|
}
|
|
|
|
return $warnings;
|
|
}
|
|
|
|
private function loadStabiliOptions(): void
|
|
{
|
|
$this->stabileOptions = Stabile::query()
|
|
->orderBy('codice_stabile')
|
|
->get()
|
|
->mapWithKeys(function (Stabile $s) {
|
|
$label = trim(($s->codice_univoco ?: $s->codice_stabile ?: ('Stabile ' . $s->id)) . ' · ' . ($s->denominazione ?? ''));
|
|
return [$s->id => $label];
|
|
})
|
|
->all();
|
|
}
|
|
|
|
private function loadUnita(): void
|
|
{
|
|
$stabileId = $this->resolveStabileId();
|
|
if (! $stabileId) {
|
|
$this->unitaOptions = [];
|
|
return;
|
|
}
|
|
$this->unitaOptions = UnitaImmobiliare::query()
|
|
->withTrashed()
|
|
->where('stabile_id', $stabileId)
|
|
->orderBy('scala')
|
|
->orderBy('piano')
|
|
->orderBy('interno')
|
|
->get()
|
|
->mapWithKeys(function (UnitaImmobiliare $u) {
|
|
$status = $u->trashed() ? ' [archiviata]' : '';
|
|
$label = trim(($u->codice_unita ?? $u->denominazione ?? ('Unità ' . $u->id)) . ' · ' . ($u->denominazione ?? 'unità') . ' · Scala ' . ($u->scala ?? '-') . ' · Int ' . ($u->interno ?? '-') . $status);
|
|
return [$u->id => $label];
|
|
})
|
|
->all();
|
|
}
|
|
|
|
private function loadLocaliTecnici(): void
|
|
{
|
|
$stabileId = $this->resolveStabileId();
|
|
if (! $stabileId) {
|
|
$this->localiTecnici = [];
|
|
return;
|
|
}
|
|
$tipi = ['locale_tecnico', 'centrale_termica', 'autorimessa', 'cantina', 'box', 'solaio', 'posto_auto', 'locale_bici'];
|
|
$this->localiTecnici = UnitaImmobiliare::query()
|
|
->where('stabile_id', $stabileId)
|
|
->whereIn('tipo_unita', $tipi)
|
|
->orderBy('scala')
|
|
->orderBy('piano')
|
|
->orderBy('interno')
|
|
->get()
|
|
->map(fn(UnitaImmobiliare $u) => [
|
|
'id' => $u->id,
|
|
'codice' => $u->codice_unita ?? $u->denominazione ?? ('Unità ' . $u->id),
|
|
'tipo' => $u->tipo_unita,
|
|
'scala' => $u->scala,
|
|
'interno' => $u->interno,
|
|
'descrizione' => $u->descrizione,
|
|
])
|
|
->all();
|
|
}
|
|
|
|
private function loadServizi(): void
|
|
{
|
|
$stabileId = $this->resolveStabileId();
|
|
$this->servizi = [];
|
|
if (! $stabileId) {
|
|
return;
|
|
}
|
|
|
|
$rows = StabileServizio::where('stabile_id', $stabileId)->get()->keyBy('tipo');
|
|
foreach ($this->serviziCatalogo as $key => $label) {
|
|
$row = $rows->get($key);
|
|
$meta = is_array($row?->meta) ? $row->meta : [];
|
|
$this->servizi[$key] = [
|
|
'attivo' => (bool) ($row?->attivo ?? false),
|
|
'data_scadenza' => $meta['data_scadenza'] ?? null,
|
|
'note' => $meta['note'] ?? null,
|
|
];
|
|
}
|
|
}
|
|
|
|
private function loadSelected(): void
|
|
{
|
|
$this->form = [
|
|
'codice_appartamento' => null,
|
|
'unita_immobiliare_id' => null,
|
|
'proprietario_nome' => null,
|
|
'proprietario_intesta' => null,
|
|
'descrizione_immobile' => null,
|
|
'indirizzo_immobile' => null,
|
|
'cap' => null,
|
|
'citta' => null,
|
|
'provincia' => null,
|
|
'nome_inquilino' => null,
|
|
'rubrica_inquilino_id' => null,
|
|
'conto_bancario_id' => null,
|
|
'rendita_catastale' => null,
|
|
'importo_fitto' => null,
|
|
'particella' => null,
|
|
'foglio' => null,
|
|
'subalterno' => null,
|
|
'destinazione_uso' => null,
|
|
'comune_catastale_codice' => null,
|
|
'comune_catastale_nome' => null,
|
|
'sezione_urbana' => null,
|
|
'categoria_catastale' => null,
|
|
'classe_catastale' => null,
|
|
'consistenza_catastale' => null,
|
|
'superficie_catastale_mq' => null,
|
|
'interno_catastale' => null,
|
|
'piano_catastale' => null,
|
|
'numero_registrazione_ade' => null,
|
|
'data_registrazione_ade' => null,
|
|
'inizio_contratto' => null,
|
|
'presa_in_carico_operativa_dal' => null,
|
|
'ultimo_rinnovo' => null,
|
|
'prossima_scadenza' => null,
|
|
'prossima_registrazione' => null,
|
|
'data_fine_contratto' => null,
|
|
'giorno_emissione_canone' => 5,
|
|
'mese_adempimenti_annuali' => 1,
|
|
'giorno_adempimenti_annuali' => 30,
|
|
'imposta_registro_annuale' => null,
|
|
'quota_imposta_inquilino_percent' => 50,
|
|
'imposta_bollo_annuale' => null,
|
|
'obsoleto' => false,
|
|
'obsoleto_dal' => null,
|
|
'motivo_obsolescenza' => null,
|
|
'note' => null,
|
|
'def_ammin' => null,
|
|
'descr_1_voce' => null,
|
|
'tipo_riga' => null,
|
|
'inte_cc' => null,
|
|
'iban' => null,
|
|
'selez' => null,
|
|
'num_ccp' => null,
|
|
'autorizz_674' => null,
|
|
];
|
|
$this->canoniDovuti = [];
|
|
$this->canoniPagati = [];
|
|
$this->canoniRighe = [];
|
|
$this->totali = ['dovuti' => 0, 'pagati' => 0, 'saldo' => 0];
|
|
$this->selectedStabileRubricaId = null;
|
|
$this->selectedStabileCode = null;
|
|
$this->selectedStabileName = null;
|
|
$this->inquilini = [];
|
|
$this->clausoleContratto = [];
|
|
|
|
if (! $this->selectedAffittoId) {
|
|
return;
|
|
}
|
|
|
|
$record = AffittoImmobile::find($this->selectedAffittoId);
|
|
if (! $record) {
|
|
return;
|
|
}
|
|
|
|
$this->ensureUnitLink($record);
|
|
$record = AffittoImmobile::find($this->selectedAffittoId);
|
|
if (! $record) {
|
|
return;
|
|
}
|
|
|
|
foreach ($this->form as $k => $v) {
|
|
$this->form[$k] = $record->{$k};
|
|
}
|
|
|
|
if ($record->stabile) {
|
|
$this->selectedStabileRubricaId = $record->stabile->rubrica_id ?? null;
|
|
$this->selectedStabileCode = $record->stabile->codice_univoco ?: ($record->stabile->codice_stabile ?? null);
|
|
$this->selectedStabileName = $record->stabile->denominazione ?? null;
|
|
}
|
|
|
|
$this->selectedAffittoObsoleto = (bool) ($record->obsoleto ?? false);
|
|
|
|
$this->emitCanoniAutomaticiForRecord($record);
|
|
|
|
$dovuti = AffittoCanoneDovuto::where('affitto_id', $record->id)->orderByDesc('anno')->orderByDesc('mese')->get();
|
|
$this->ensureRicevute($record, $dovuti);
|
|
$pagati = AffittoCanonePagato::where('affitto_id', $record->id)->orderByDesc('anno')->orderByDesc('mese')->get();
|
|
|
|
$managementStart = $this->resolveCanoniManagementStart($record);
|
|
if ($managementStart) {
|
|
$start = $managementStart->copy()->startOfMonth();
|
|
$dovuti = $dovuti->filter(function (AffittoCanoneDovuto $row) use ($start) {
|
|
$month = Carbon::create((int) $row->anno, (int) $row->mese, 1);
|
|
return $month->gte($start);
|
|
})->values();
|
|
$pagati = $pagati->filter(function (AffittoCanonePagato $row) use ($start) {
|
|
$month = Carbon::create((int) $row->anno, (int) $row->mese, 1);
|
|
return $month->gte($start);
|
|
})->values();
|
|
}
|
|
|
|
$this->canoniDovuti = $dovuti->toArray();
|
|
$this->canoniPagati = $pagati->toArray();
|
|
$this->canoniRighe = $this->buildCanoniRows($dovuti->toArray(), $pagati->toArray());
|
|
|
|
$this->inquilini = $record->inquilini()->orderBy('id')->get()->map(function (AffittoInquilino $row) {
|
|
return [
|
|
'id' => $row->id,
|
|
'rubrica_inquilino_id' => $row->rubrica_inquilino_id,
|
|
'nome_visualizzato' => $row->nome_visualizzato,
|
|
'email' => $row->email,
|
|
'telefono' => $row->telefono,
|
|
'documenti_note' => $row->documenti_note,
|
|
'attivo' => (bool) $row->attivo,
|
|
];
|
|
})->all();
|
|
|
|
if (empty($this->inquilini) && ($record->nome_inquilino || $record->rubrica_inquilino_id)) {
|
|
$this->inquilini[] = [
|
|
'id' => null,
|
|
'rubrica_inquilino_id' => $record->rubrica_inquilino_id,
|
|
'nome_visualizzato' => $record->nome_inquilino,
|
|
'email' => null,
|
|
'telefono' => null,
|
|
'documenti_note' => null,
|
|
'attivo' => true,
|
|
];
|
|
}
|
|
|
|
$this->clausoleContratto = $record->clausoleContratto()->orderBy('ordine')->orderBy('id')->get()->map(function (AffittoContrattoClausola $row) {
|
|
return [
|
|
'id' => $row->id,
|
|
'clausola_id' => $row->clausola_id,
|
|
'titolo' => $row->titolo,
|
|
'testo' => $row->testo,
|
|
'periodicita' => $row->periodicita,
|
|
'importo' => $row->importo,
|
|
'ordine' => $row->ordine,
|
|
'attivo' => (bool) $row->attivo,
|
|
];
|
|
})->all();
|
|
|
|
$sumDovuti = $dovuti->sum('totale');
|
|
$sumPagati = $pagati->sum('totale');
|
|
$this->totali = [
|
|
'dovuti' => $sumDovuti,
|
|
'pagati' => $sumPagati,
|
|
'saldo' => $sumDovuti - $sumPagati,
|
|
];
|
|
|
|
$this->selectedContractTemplate = $this->selectedContractTemplate ?: 'abitativo_431_2';
|
|
}
|
|
|
|
private function ensureUnitLink(AffittoImmobile $record): void
|
|
{
|
|
if ($record->unita_immobiliare_id) {
|
|
return;
|
|
}
|
|
|
|
$candidate = $this->resolveCandidateUnita($record);
|
|
if (! $candidate) {
|
|
return;
|
|
}
|
|
|
|
$record->unita_immobiliare_id = $candidate->id;
|
|
$record->save();
|
|
}
|
|
|
|
private function resolveCandidateUnita(AffittoImmobile $record): ?UnitaImmobiliare
|
|
{
|
|
$stabileId = $record->stabile_id ?: $this->resolveStabileId();
|
|
if (! $stabileId) {
|
|
return null;
|
|
}
|
|
|
|
$query = UnitaImmobiliare::withTrashed()->where('stabile_id', $stabileId);
|
|
|
|
if ($record->unita_immobiliare_id) {
|
|
return $query->where('id', $record->unita_immobiliare_id)->first();
|
|
}
|
|
|
|
$code = trim((string) ($record->codice_appartamento ?? ''));
|
|
$desc = trim((string) ($record->descrizione_immobile ?? ''));
|
|
|
|
if ($code !== '') {
|
|
$byCode = (clone $query)->where(function ($q) use ($code) {
|
|
$q->where('codice_unita', $code)
|
|
->orWhere('denominazione', $code)
|
|
->orWhere('interno', $code);
|
|
})->first();
|
|
if ($byCode) {
|
|
return $byCode;
|
|
}
|
|
}
|
|
|
|
if ($desc !== '') {
|
|
$byDesc = (clone $query)->where('denominazione', 'like', '%' . $desc . '%')->first();
|
|
if ($byDesc) {
|
|
return $byDesc;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function isEmptyValue(mixed $value): bool
|
|
{
|
|
if ($value === null) {
|
|
return true;
|
|
}
|
|
if (is_string($value)) {
|
|
return trim($value) === '';
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private function resolveStabileId(): ?int
|
|
{
|
|
if ($this->stabileFilterId) {
|
|
return $this->stabileFilterId;
|
|
}
|
|
|
|
if ($this->forceAll) {
|
|
return null;
|
|
}
|
|
|
|
return $this->resolveStabileIdFromContext();
|
|
}
|
|
|
|
private function resolveStabileIdFromContext(): ?int
|
|
{
|
|
$user = Auth::user();
|
|
if ($user instanceof \App\Models\User) {
|
|
return StabileContext::resolveActiveStabileId($user);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private function resolveStabileLegacyCode(?int $stabileId): ?string
|
|
{
|
|
if (! $stabileId) {
|
|
return null;
|
|
}
|
|
|
|
return Stabile::where('id', $stabileId)->value('codice_stabile');
|
|
}
|
|
|
|
private function loadRubricaOptions(): void
|
|
{
|
|
$this->rubricaOptions = RubricaUniversale::query()
|
|
->orderBy('ragione_sociale')
|
|
->orderBy('cognome')
|
|
->get()
|
|
->mapWithKeys(function (RubricaUniversale $r) {
|
|
$label = trim(($r->nome_completo ?? $r->ragione_sociale ?? 'Anagrafica') . ' · ' . ($r->codice_fiscale ?? $r->partita_iva ?? ''));
|
|
return [$r->id => $label];
|
|
})
|
|
->all();
|
|
}
|
|
|
|
private function loadContoOptions(): void
|
|
{
|
|
$query = DatiBancari::query()->with('contatto');
|
|
$this->contoOptions = $query->orderBy('denominazione_banca')
|
|
->get()
|
|
->mapWithKeys(function (DatiBancari $c) {
|
|
$name = $c->contatto?->nome_completo ?? $c->intestazione_conto ?? 'Conto';
|
|
$label = trim(($c->denominazione_banca ?? 'Banca') . ' · ' . ($c->identificativo_conto ?? '') . ' · ' . $name);
|
|
return [$c->id => $label];
|
|
})
|
|
->all();
|
|
}
|
|
|
|
private function loadClausoleCatalogo(): void
|
|
{
|
|
$this->clausoleCatalogo = AffittoClausola::query()
|
|
->where('attivo', true)
|
|
->orderBy('ordine')
|
|
->orderBy('titolo')
|
|
->get()
|
|
->mapWithKeys(function (AffittoClausola $c) {
|
|
$label = trim(($c->titolo ?: 'Clausola') . ' · #' . $c->id);
|
|
return [$c->id => $label];
|
|
})
|
|
->all();
|
|
}
|
|
|
|
private function initContractTemplates(): void
|
|
{
|
|
$this->contractTemplates = [
|
|
'abitativo_431_2' => 'Contratto abitativo art.2 L.431/1998 (base)',
|
|
];
|
|
}
|
|
|
|
private function saveInquilini(AffittoImmobile $record): void
|
|
{
|
|
$keepIds = [];
|
|
foreach ($this->inquilini as $row) {
|
|
$nome = trim((string) ($row['nome_visualizzato'] ?? ''));
|
|
$rubricaId = (int) ($row['rubrica_inquilino_id'] ?? 0);
|
|
if ($nome === '' && $rubricaId <= 0) {
|
|
continue;
|
|
}
|
|
|
|
$payload = [
|
|
'rubrica_inquilino_id' => $rubricaId > 0 ? $rubricaId : null,
|
|
'nome_visualizzato' => $nome !== '' ? $nome : null,
|
|
'email' => trim((string) ($row['email'] ?? '')) ?: null,
|
|
'telefono' => trim((string) ($row['telefono'] ?? '')) ?: null,
|
|
'documenti_note' => trim((string) ($row['documenti_note'] ?? '')) ?: null,
|
|
'attivo' => (bool) ($row['attivo'] ?? true),
|
|
];
|
|
|
|
$id = (int) ($row['id'] ?? 0);
|
|
if ($id > 0) {
|
|
$entity = AffittoInquilino::query()->where('affitto_id', $record->id)->where('id', $id)->first();
|
|
if ($entity) {
|
|
$entity->fill($payload)->save();
|
|
$keepIds[] = $entity->id;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
$entity = $record->inquilini()->create($payload);
|
|
$keepIds[] = $entity->id;
|
|
}
|
|
|
|
$record->inquilini()->whereNotIn('id', $keepIds ?: [0])->delete();
|
|
|
|
$primary = $record->inquilini()->where('attivo', true)->orderBy('id')->first();
|
|
if ($primary) {
|
|
$record->nome_inquilino = $primary->nome_visualizzato ?: ($primary->rubrica?->nome_completo ?? $record->nome_inquilino);
|
|
$record->rubrica_inquilino_id = $primary->rubrica_inquilino_id;
|
|
$record->save();
|
|
}
|
|
}
|
|
|
|
private function saveClausoleContratto(AffittoImmobile $record): void
|
|
{
|
|
$keepIds = [];
|
|
foreach ($this->clausoleContratto as $index => $row) {
|
|
$titolo = trim((string) ($row['titolo'] ?? ''));
|
|
$testo = trim((string) ($row['testo'] ?? ''));
|
|
$clausolaId = (int) ($row['clausola_id'] ?? 0);
|
|
if ($titolo === '' && $testo === '' && $clausolaId <= 0) {
|
|
continue;
|
|
}
|
|
|
|
$payload = [
|
|
'clausola_id' => $clausolaId > 0 ? $clausolaId : null,
|
|
'titolo' => $titolo !== '' ? $titolo : null,
|
|
'testo' => $testo !== '' ? $testo : null,
|
|
'periodicita' => in_array(($row['periodicita'] ?? null), ['mensile', 'annuale', 'una_tantum'], true) ? $row['periodicita'] : null,
|
|
'importo' => is_numeric($row['importo'] ?? null) ? (float) $row['importo'] : null,
|
|
'ordine' => is_numeric($row['ordine'] ?? null) ? (int) $row['ordine'] : ($index + 1),
|
|
'attivo' => (bool) ($row['attivo'] ?? true),
|
|
];
|
|
|
|
$id = (int) ($row['id'] ?? 0);
|
|
if ($id > 0) {
|
|
$entity = AffittoContrattoClausola::query()->where('affitto_id', $record->id)->where('id', $id)->first();
|
|
if ($entity) {
|
|
$entity->fill($payload)->save();
|
|
$keepIds[] = $entity->id;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
$entity = $record->clausoleContratto()->create($payload);
|
|
$keepIds[] = $entity->id;
|
|
}
|
|
|
|
$record->clausoleContratto()->whereNotIn('id', $keepIds ?: [0])->delete();
|
|
}
|
|
|
|
private function buildCanoniRows(array $dovuti, array $pagati): array
|
|
{
|
|
$today = Carbon::today();
|
|
$rows = [];
|
|
foreach ($dovuti as $r) {
|
|
$key = ($r['anno'] ?? '0') . '-' . str_pad((string) ($r['mese'] ?? '0'), 2, '0', STR_PAD_LEFT);
|
|
if (! isset($rows[$key])) {
|
|
$rows[$key] = [
|
|
'anno' => $r['anno'] ?? null,
|
|
'mese' => $r['mese'] ?? null,
|
|
'mese_descrizione' => $r['mese_descrizione'] ?? null,
|
|
'totale_dovuto' => 0.0,
|
|
'totale_pagato' => 0.0,
|
|
'data_pagamento' => null,
|
|
'data_emissione' => null,
|
|
'data_scadenza' => null,
|
|
'saldo_progressivo' => 0.0,
|
|
'stato' => 'non_emesso',
|
|
];
|
|
}
|
|
$rows[$key]['totale_dovuto'] = (float) ($r['totale'] ?? 0);
|
|
$rows[$key]['data_emissione'] = $r['data_emissione'] ?? null;
|
|
$rows[$key]['data_scadenza'] = $r['data_scadenza'] ?? null;
|
|
if (empty($rows[$key]['mese_descrizione']) && ! empty($r['mese_descrizione'])) {
|
|
$rows[$key]['mese_descrizione'] = $r['mese_descrizione'];
|
|
}
|
|
}
|
|
|
|
foreach ($pagati as $r) {
|
|
$key = ($r['anno'] ?? '0') . '-' . str_pad((string) ($r['mese'] ?? '0'), 2, '0', STR_PAD_LEFT);
|
|
if (! isset($rows[$key])) {
|
|
$rows[$key] = [
|
|
'anno' => $r['anno'] ?? null,
|
|
'mese' => $r['mese'] ?? null,
|
|
'mese_descrizione' => $r['mese_descrizione'] ?? null,
|
|
'totale_dovuto' => 0.0,
|
|
'totale_pagato' => 0.0,
|
|
'data_pagamento' => null,
|
|
'data_emissione' => null,
|
|
'data_scadenza' => null,
|
|
'saldo_progressivo' => 0.0,
|
|
'stato' => 'non_emesso',
|
|
];
|
|
}
|
|
$rows[$key]['totale_pagato'] = (float) ($r['totale'] ?? 0);
|
|
$rows[$key]['data_pagamento'] = $r['data_pagamento'] ?? null;
|
|
if (empty($rows[$key]['mese_descrizione']) && ! empty($r['mese_descrizione'])) {
|
|
$rows[$key]['mese_descrizione'] = $r['mese_descrizione'];
|
|
}
|
|
}
|
|
|
|
ksort($rows);
|
|
|
|
$progressive = 0.0;
|
|
foreach ($rows as $key => $row) {
|
|
$delta = (float) ($row['totale_dovuto'] ?? 0) - (float) ($row['totale_pagato'] ?? 0);
|
|
$progressive += $delta;
|
|
|
|
$status = 'in_regola';
|
|
$scadenza = ! empty($row['data_scadenza']) ? Carbon::parse($row['data_scadenza']) : null;
|
|
if (($row['totale_dovuto'] ?? 0) <= 0) {
|
|
$status = 'non_emesso';
|
|
} elseif ($progressive > 0.0001 && $scadenza && $scadenza->lt($today)) {
|
|
$status = 'scaduto';
|
|
} elseif ($progressive > 0.0001) {
|
|
$status = 'da_pagare';
|
|
}
|
|
|
|
$rows[$key]['saldo_progressivo'] = $progressive;
|
|
$rows[$key]['stato'] = $status;
|
|
}
|
|
|
|
return array_values(array_reverse($rows));
|
|
}
|
|
|
|
private function emitCanoniAutomaticiForRecord(AffittoImmobile $record): int
|
|
{
|
|
if ((bool) ($record->obsoleto ?? false)) {
|
|
return 0;
|
|
}
|
|
|
|
$managementStart = $this->resolveCanoniManagementStart($record);
|
|
if (! $managementStart) {
|
|
return 0;
|
|
}
|
|
|
|
$start = $managementStart->copy()->startOfMonth();
|
|
$todayMonth = Carbon::today()->startOfMonth();
|
|
$end = $todayMonth;
|
|
|
|
if (! empty($record->prossima_scadenza)) {
|
|
$scadenza = Carbon::parse($record->prossima_scadenza)->startOfMonth();
|
|
if ($scadenza->lt($end)) {
|
|
$end = $scadenza;
|
|
}
|
|
}
|
|
|
|
if (! empty($record->data_fine_contratto)) {
|
|
$fineContratto = Carbon::parse($record->data_fine_contratto)->startOfMonth();
|
|
if ($fineContratto->lt($end)) {
|
|
$end = $fineContratto;
|
|
}
|
|
}
|
|
|
|
if ($end->lt($start)) {
|
|
return 0;
|
|
}
|
|
|
|
$existing = AffittoCanoneDovuto::query()
|
|
->where('affitto_id', $record->id)
|
|
->orderBy('anno')
|
|
->orderBy('mese')
|
|
->get()
|
|
->keyBy(fn(AffittoCanoneDovuto $r) => sprintf('%04d-%02d', (int) ($r->anno ?? 0), (int) ($r->mese ?? 0)));
|
|
|
|
$clausole = $record->clausoleContratto()
|
|
->where('attivo', true)
|
|
->orderBy('ordine')
|
|
->orderBy('id')
|
|
->get();
|
|
|
|
$istatService = app(IstatRivalutazioneService::class);
|
|
$istatSeries = strtoupper((string) config('services.istat.series', 'FOI'));
|
|
|
|
$annualBase = is_numeric($record->importo_fitto) ? (float) $record->importo_fitto : 0.0;
|
|
$currentBase = round($annualBase / 12, 2);
|
|
$meseAdempimenti = (int) ($record->mese_adempimenti_annuali ?? 1);
|
|
if ($meseAdempimenti < 1 || $meseAdempimenti > 12) {
|
|
$meseAdempimenti = 1;
|
|
}
|
|
$giornoAdempimenti = (int) ($record->giorno_adempimenti_annuali ?? 30);
|
|
if ($giornoAdempimenti < 1 || $giornoAdempimenti > 31) {
|
|
$giornoAdempimenti = 30;
|
|
}
|
|
$impostaRegistroAnnuale = is_numeric($record->imposta_registro_annuale) ? (float) $record->imposta_registro_annuale : 0.0;
|
|
$quotaImpostaInquilino = is_numeric($record->quota_imposta_inquilino_percent) ? (float) $record->quota_imposta_inquilino_percent : 50.0;
|
|
$quotaImpostaInquilino = max(0.0, min(100.0, $quotaImpostaInquilino));
|
|
$impostaBolloAnnuale = is_numeric($record->imposta_bollo_annuale) ? (float) $record->imposta_bollo_annuale : 0.0;
|
|
$created = 0;
|
|
|
|
for ($cursor = $start->copy(); $cursor->lte($end); $cursor->addMonth()) {
|
|
$key = $cursor->format('Y-m');
|
|
$existingRow = $existing->get($key);
|
|
|
|
if ($existingRow) {
|
|
$fittoExisting = is_numeric($existingRow->fitto) ? (float) $existingRow->fitto : $currentBase;
|
|
$istatExisting = is_numeric($existingRow->istat_importo) ? (float) $existingRow->istat_importo : 0.0;
|
|
$currentBase = $fittoExisting + max(0.0, $istatExisting);
|
|
continue;
|
|
}
|
|
|
|
$giornoEmissione = (int) ($record->giorno_emissione_canone ?? 5);
|
|
if ($giornoEmissione < 1 || $giornoEmissione > 28) {
|
|
$giornoEmissione = 5;
|
|
}
|
|
$emissione = Carbon::create((int) $cursor->year, (int) $cursor->month, min($giornoEmissione, $cursor->daysInMonth));
|
|
$scadenza = $emissione->copy();
|
|
|
|
$istatImporto = 0.0;
|
|
$istatPercentuale = null;
|
|
$extras = [];
|
|
|
|
foreach ($clausole as $clausola) {
|
|
$periodicita = $clausola->periodicita;
|
|
$applies = false;
|
|
|
|
if ($periodicita === 'mensile') {
|
|
$applies = true;
|
|
} elseif ($periodicita === 'annuale') {
|
|
$applies = (int) $cursor->month === (int) $start->month;
|
|
} elseif ($periodicita === 'una_tantum') {
|
|
$applies = $cursor->isSameMonth($start);
|
|
}
|
|
|
|
if (! $applies) {
|
|
continue;
|
|
}
|
|
|
|
$importo = is_numeric($clausola->importo) ? (float) $clausola->importo : 0.0;
|
|
$titolo = trim((string) ($clausola->titolo ?? ''));
|
|
if (Str::contains(Str::lower($titolo), 'istat')) {
|
|
if ($importo != 0.0) {
|
|
$istatImporto += $importo;
|
|
} else {
|
|
$adeguamento = $istatService->calcolaAdeguamentoAnnuale($annualBase, $cursor, $istatSeries);
|
|
if ($adeguamento) {
|
|
$quota = $this->resolveIstatQuotaFromClause($clausola);
|
|
$calcImporto = (float) ($adeguamento['importo_adeguamento'] ?? 0) / 12;
|
|
$calcPercent = (float) ($adeguamento['variazione_percentuale'] ?? 0);
|
|
if ($calcImporto > 0) {
|
|
$istatImporto += round($calcImporto * $quota, 2);
|
|
$istatPercentuale = round($calcPercent * $quota, 4);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if ($importo == 0.0) {
|
|
continue;
|
|
}
|
|
$extras[] = ['titolo' => $titolo !== '' ? $titolo : 'Voce contratto', 'importo' => $importo];
|
|
}
|
|
}
|
|
|
|
if ((int) $cursor->month === $meseAdempimenti) {
|
|
$registroQuotaInquilino = round($impostaRegistroAnnuale * ($quotaImpostaInquilino / 100), 2);
|
|
if ($registroQuotaInquilino > 0) {
|
|
$extras[] = [
|
|
'titolo' => 'Imposta registro annua (quota inquilino ' . rtrim(rtrim(number_format($quotaImpostaInquilino, 2, '.', ''), '0'), '.') . '%)',
|
|
'importo' => $registroQuotaInquilino,
|
|
];
|
|
}
|
|
|
|
if ($impostaBolloAnnuale > 0) {
|
|
$extras[] = [
|
|
'titolo' => 'Imposta di bollo annua',
|
|
'importo' => round($impostaBolloAnnuale, 2),
|
|
];
|
|
}
|
|
|
|
$scadenza = Carbon::create((int) $cursor->year, (int) $cursor->month, min($giornoAdempimenti, $cursor->daysInMonth));
|
|
}
|
|
|
|
$fitto = $currentBase;
|
|
$totale = $fitto + $istatImporto;
|
|
|
|
$payload = [
|
|
'affitto_id' => $record->id,
|
|
'anno' => (int) $cursor->year,
|
|
'mese' => (int) $cursor->month,
|
|
'mese_descrizione' => $cursor->locale('it')->translatedFormat('F'),
|
|
'fitto' => $fitto,
|
|
'istat_percentuale' => $istatPercentuale,
|
|
'istat_importo' => $istatImporto > 0 ? $istatImporto : null,
|
|
'bollo' => 0,
|
|
'totale' => 0,
|
|
'data_emissione' => $emissione->toDateString(),
|
|
'data_scadenza' => $scadenza->toDateString(),
|
|
'saldo_progressivo' => 0,
|
|
];
|
|
|
|
for ($i = 1; $i <= 4; $i++) {
|
|
$payload['descrizione_' . $i] = null;
|
|
$payload['importo_' . $i] = null;
|
|
}
|
|
|
|
foreach (array_values($extras) as $idx => $extra) {
|
|
if ($idx >= 4) {
|
|
break;
|
|
}
|
|
$col = $idx + 1;
|
|
$payload['descrizione_' . $col] = $extra['titolo'];
|
|
$payload['importo_' . $col] = $extra['importo'];
|
|
$totale += (float) $extra['importo'];
|
|
}
|
|
|
|
$payload['totale'] = $totale;
|
|
|
|
AffittoCanoneDovuto::create($payload);
|
|
$created++;
|
|
|
|
if ($istatImporto > 0) {
|
|
$annualBase += ($istatImporto * 12);
|
|
$currentBase += $istatImporto;
|
|
}
|
|
}
|
|
|
|
return $created;
|
|
}
|
|
|
|
private function resolveCanoniManagementStart(AffittoImmobile $record): ?Carbon
|
|
{
|
|
$dates = [];
|
|
|
|
if (! empty($record->inizio_contratto)) {
|
|
$dates[] = Carbon::parse($record->inizio_contratto)->startOfMonth();
|
|
}
|
|
|
|
if (! empty($record->presa_in_carico_operativa_dal)) {
|
|
$dates[] = Carbon::parse($record->presa_in_carico_operativa_dal)->startOfMonth();
|
|
}
|
|
|
|
if ($record->stabile && $record->stabile->latestAmministratoreTransfer?->created_at) {
|
|
$dates[] = $record->stabile->latestAmministratoreTransfer->created_at->copy()->startOfMonth();
|
|
}
|
|
|
|
if ($dates === []) {
|
|
return null;
|
|
}
|
|
|
|
usort($dates, fn(Carbon $left, Carbon $right): int => $left->getTimestamp() <=> $right->getTimestamp());
|
|
|
|
return $dates[count($dates) - 1];
|
|
}
|
|
|
|
private function resolveIstatQuotaFromClause(AffittoContrattoClausola $clausola): float
|
|
{
|
|
$content = trim((string) ($clausola->titolo ?? '') . ' ' . (string) ($clausola->testo ?? ''));
|
|
if ($content === '') {
|
|
return 1.0;
|
|
}
|
|
|
|
if (! preg_match('/(\d{1,3}(?:[\.,]\d{1,2})?)\s*%/', $content, $m)) {
|
|
return 1.0;
|
|
}
|
|
|
|
$raw = str_replace(',', '.', $m[1]);
|
|
if (! is_numeric($raw)) {
|
|
return 1.0;
|
|
}
|
|
|
|
$pct = (float) $raw;
|
|
if ($pct <= 0) {
|
|
return 1.0;
|
|
}
|
|
|
|
return min(1.0, $pct / 100);
|
|
}
|
|
|
|
private function deleteCanoniDuplicates(array $rows): int
|
|
{
|
|
$removed = 0;
|
|
$grouped = [];
|
|
foreach ($rows as $row) {
|
|
$key = implode('|', [
|
|
(int) ($row->affitto_id ?? 0),
|
|
(int) ($row->anno ?? 0),
|
|
(int) ($row->mese ?? 0),
|
|
number_format((float) ($row->totale ?? 0), 2, '.', ''),
|
|
]);
|
|
$grouped[$key][] = $row;
|
|
}
|
|
|
|
foreach ($grouped as $items) {
|
|
if (count($items) <= 1) {
|
|
continue;
|
|
}
|
|
usort($items, fn($a, $b) => (int) $a->id <=> (int) $b->id);
|
|
$toDelete = array_slice($items, 1);
|
|
foreach ($toDelete as $dup) {
|
|
$dup->delete();
|
|
$removed++;
|
|
}
|
|
}
|
|
|
|
return $removed;
|
|
}
|
|
|
|
private function ensureRicevute(AffittoImmobile $affitto, $dovuti): void
|
|
{
|
|
foreach ($dovuti as $row) {
|
|
if (! empty($row->n_ricevuta)) {
|
|
continue;
|
|
}
|
|
|
|
$row->n_ricevuta = $row->generateNumeroRicevuta();
|
|
$row->save();
|
|
}
|
|
}
|
|
}
|