'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, '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'; } } $scadutiNonPagati = AffittoCanoneDovuto::query() ->where('affitto_id', $affitto->id) ->whereDate('data_scadenza', '<', $today->toDateString()) ->sum('totale') - AffittoCanonePagato::query() ->where('affitto_id', $affitto->id) ->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, '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(); if (! empty($record->inizio_contratto)) { $start = Carbon::parse($record->inizio_contratto)->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; } if (empty($record->inizio_contratto)) { return 0; } $start = Carbon::parse($record->inizio_contratto)->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 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(); } } }