hasAnyRole(['super-admin', 'admin'])) { return true; } return $user->can('contabilita.gestioni'); } public function mount(): void { $user = Auth::user(); if (! $user instanceof User) { return; } $activeId = StabileContext::resolveActiveStabileId($user); if (! $activeId) { $this->stabileAttivo = null; return; } $this->stabileAttivo = StabileContext::accessibleStabili($user) ->firstWhere('id', $activeId); $this->getSchema('form')?->fill([ 'tipo_spesa' => 'singola', 'anno_gestione' => (int) now()->format('Y'), ]); } public function form(Schema $schema): Schema { return $schema ->statePath('data') ->components($this->getFormSchema()); } protected function getFormSchema(): array { return [ Grid::make(3)->schema([ Section::make('Registrazione') ->columnSpan(2) ->schema([ Select::make('tipo_spesa') ->label('Tipo') ->options([ 'singola' => 'Spesa singola', 'aggregata' => 'Acqua / Riscaldamento (aggregata)', ]) ->required() ->live(), Select::make('legacy_voce_id') ->label('Voce (GESCON)') ->options([]) ->searchable() ->getSearchResultsUsing(fn(string $search): array=> $this->searchLegacyVoci($search)) ->getOptionLabelUsing(fn($value): ?string => $this->getLegacyVoceLabel($value)) ->live() ->afterStateUpdated(function ($state): void { if (! $state) { return; } $this->loadLegacyVoce((int) $state); }) ->required(), Grid::make(3)->schema([ Placeholder::make('legacy_tabella') ->label('Tabella') ->content(fn(callable $get) => $get('legacy_tabella') ?: '—'), Placeholder::make('legacy_prev_euro') ->label('Prev. (€)') ->content(fn(callable $get) => is_numeric($get('legacy_prev_euro')) ? number_format((float) $get('legacy_prev_euro'), 2, ',', '.') : '—'), Placeholder::make('legacy_cons_euro') ->label('Cons. (€)') ->content(fn(callable $get) => is_numeric($get('legacy_cons_euro')) ? number_format((float) $get('legacy_cons_euro'), 2, ',', '.') : '—'), ]), Select::make('tabella_millesimale_id') ->label('Tabella millesimale') ->options(fn() => $this->getTabelleOptions()) ->searchable() ->required(), TextInput::make('descrizione') ->label('Descrizione') ->maxLength(255) ->required(), Grid::make(2)->schema([ TextInput::make('anno_gestione') ->label('Anno gestione') ->numeric() ->required(), TextInput::make('importo_consuntivo') ->label('Importo consuntivo (€)') ->numeric() ->required(), ]), ]), Section::make('Fattura elettronica (XML)') ->columnSpan(1) ->schema([ Select::make('fattura_elettronica_id') ->label('Seleziona fattura') ->options(fn() => $this->getFattureOptions()) ->searchable() ->visible(fn(callable $get) => $get('tipo_spesa') === 'singola') ->live(), Select::make('fatture_elettroniche_ids') ->label('Seleziona fatture (multiple)') ->options(fn() => $this->getFattureOptions()) ->searchable() ->multiple() ->visible(fn(callable $get) => $get('tipo_spesa') === 'aggregata') ->live(), Placeholder::make('xml_preview_supplier') ->label('Fornitore') ->content(fn(callable $get) => $get('fornitore_id') ? (string) ($this->getFornitoreDenominazione((int) $get('fornitore_id')) ?: '—'): '—'), Placeholder::make('xml_preview_doc') ->label('Documento') ->content(fn(callable $get) => trim(((string) ($get('numero_documento') ?: '—')) . ' • ' . ((string) ($get('data_documento') ?: '—')))), Placeholder::make('xml_preview_totale') ->label('Totale') ->content(fn(callable $get) => is_numeric($get('totale')) ? number_format((float) $get('totale'), 2, ',', '.') . ' €' : '—'), Placeholder::make('xml_preview_pagamento') ->label('Pagamento') ->content(fn(callable $get) => trim(((string) ($get('modalita_pagamento') ?: '—')) . ' • ' . ((string) ($get('iban') ?: '—')))), Placeholder::make('xml_preview_consumo') ->label('Consumo/contatore') ->content(function (callable $get) { $val = $get('consumo_valore'); $rif = $get('consumo_riferimento'); if (! is_numeric($val) && ! $rif) { return '—'; } $a = is_numeric($val) ? (string) $val : ''; $b = (string) ($get('consumo_unita') ?: ''); $c = (string) ($rif ?: ''); return trim(trim($a . ' ' . $b) . ' ' . $c); }), ]), ]), ]; } public function applyXml(): void { if (! $this->stabileAttivo) { Notification::make()->title('Seleziona uno stabile')->danger()->send(); return; } $state = $this->getSchema('form')?->getState() ?? []; $fatture = $this->getSelectedFatture($state); if ($fatture->isEmpty()) { Notification::make()->title('Seleziona una fattura XML')->warning()->send(); return; } $totale = (float) $fatture->sum('totale'); $imponibile = (float) $fatture->sum('imponibile'); $iva = (float) $fatture->sum('iva'); $first = $fatture->first(); if (! $first instanceof FatturaElettronica) { return; } $data = $this->getParsedOrStoredXmlData($first); $fornitoreId = $first->fornitore_id; $numero = $first->numero_fattura; $dataDoc = optional($first->data_fattura)->format('Y-m-d'); $descr = (string) ($state['descrizione'] ?? ''); if ($descr === '') { $descr = trim('Fattura ' . ($numero ?: '') . ' ' . ($fornitoreId ? ($this->getFornitoreDenominazione((int) $fornitoreId) ?: ''): '')); } $this->getSchema('form')?->fill([ 'fornitore_id' => $fornitoreId, 'numero_documento' => $numero, 'data_documento' => $dataDoc, 'data_scadenza' => optional($first->data_scadenza)->format('Y-m-d'), 'modalita_pagamento' => $data['pagamento_modalita'] ?? null, 'iban' => $data['pagamento_iban'] ?? null, 'bic' => $data['pagamento_bic'] ?? null, 'imponibile' => $imponibile, 'iva' => $iva, 'totale' => $totale, 'importo_consuntivo' => $totale, 'consumo_valore' => isset($data['consumo_valore']) && is_numeric($data['consumo_valore']) ? (float) $data['consumo_valore'] : null, 'consumo_unita' => $data['consumo_unita'] ?? null, 'consumo_riferimento' => $data['consumo_riferimento'] ?? null, 'descrizione' => $descr, ]); Notification::make()->title('Dati XML applicati')->success()->send(); } public function creaRegistrazione(): void { if (! $this->stabileAttivo) { Notification::make()->title('Seleziona uno stabile')->danger()->send(); return; } $state = $this->getSchema('form')?->getState() ?? []; validator($state, [ 'tipo_spesa' => 'required|in:singola,aggregata', 'legacy_voce_id' => 'required|integer', 'tabella_millesimale_id' => 'required|integer', 'descrizione' => 'required|string|max:255', 'anno_gestione' => 'required|integer|min:2000|max:2100', 'importo_consuntivo' => 'required|numeric|min:0', ])->validate(); $fatture = $this->getSelectedFatture($state); if ($fatture->isEmpty()) { Notification::make()->title('Seleziona una fattura XML')->warning()->send(); return; } $legacyVoceId = (int) ($state['legacy_voce_id'] ?? 0); $legacy = $this->fetchLegacyVoce($legacyVoceId); if (! $legacy) { Notification::make()->title('Voce GESCON non trovata')->danger()->send(); return; } $tabellaId = (int) ($state['tabella_millesimale_id'] ?? 0); $voceSpesa = $this->firstOrCreateVoceSpesaFromLegacy($legacy, $tabellaId); DB::transaction(function () use ($fatture, $legacy, $tabellaId, $voceSpesa, $state) { // PREVENTIVO: usa sempre prev_euro se presente if (isset($legacy['preventivo_euro']) && is_numeric($legacy['preventivo_euro']) && (float) $legacy['preventivo_euro'] > 0) { $preventivo = Preventivo::query()->firstOrCreate( [ 'stabile_id' => $this->stabileAttivo->id, 'anno_gestione' => (int) ($state['anno_gestione'] ?? now()->format('Y')), 'tipo_gestione' => 'ordinaria', ], [ 'descrizione' => 'Preventivo ordinario ' . ($state['anno_gestione'] ?? now()->format('Y')), 'stato' => 'bozza', 'data_creazione' => now()->toDateString(), 'versione' => 1, ], ); $vp = VocePreventivo::query()->firstOrNew([ 'preventivo_id' => $preventivo->id, 'voce_spesa_id' => $voceSpesa->id, ]); $vp->fill([ 'codice' => (string) ($legacy['cod'] ?? $voceSpesa->codice ?? ''), 'descrizione' => (string) ($legacy['descriz'] ?? $voceSpesa->descrizione), 'importo_preventivato' => (float) $legacy['preventivo_euro'], 'tabella_millesimale_id' => $tabellaId, 'ordinamento' => 0, ]); $vp->save(); $preventivo->calcolaImportoTotale(); } // CONSUNTIVO: registra fattura fornitore + ripartizione $totale = (float) ($state['importo_consuntivo'] ?? 0); $imponibile = (float) ($state['imponibile'] ?? $fatture->sum('imponibile')); $iva = (float) ($state['iva'] ?? $fatture->sum('iva')); $fatturaElettronicaId = null; if (($state['tipo_spesa'] ?? 'singola') === 'singola' && ! empty($state['fattura_elettronica_id'])) { $fatturaElettronicaId = (int) $state['fattura_elettronica_id']; } $fattura = \App\Modules\Contabilita\Models\FatturaFornitore::query()->create([ 'stabile_id' => $this->stabileAttivo->id, 'fattura_elettronica_id' => $fatturaElettronicaId, 'fornitore_id' => $state['fornitore_id'] ?? null, 'data_registrazione' => now()->toDateString(), 'data_documento' => $state['data_documento'] ?? null, 'numero_documento' => $state['numero_documento'] ?? null, 'descrizione' => $state['descrizione'] ?? null, 'imponibile' => $imponibile, 'iva' => $iva, 'totale' => $totale, 'data_scadenza' => $state['data_scadenza'] ?? null, 'modalita_pagamento' => $state['modalita_pagamento'] ?? null, 'netto_da_pagare' => $totale, 'stato' => 'inserito', ]); \App\Modules\Contabilita\Models\FatturaFornitoreRiga::query()->create([ 'fattura_id' => $fattura->id, 'riga' => 1, 'descrizione' => (string) (($state['descrizione'] ?? '') ?: ($legacy['descriz'] ?? 'Spesa')), 'imponibile_euro' => $imponibile, 'iva_euro' => $iva, 'totale_euro' => $totale, 'tabella_millesimale_id' => $tabellaId, ]); app(RipartizioneSpesaService::class)->calcolaRipartizione( $voceSpesa, $totale, $tabellaId, [ 'tipo_ripartizione' => 'millesimale', 'note' => $state['descrizione'] ?? null, ], ); }); Notification::make()->title('Registrazione creata')->success()->send(); } private function getLegacyVociOptions(): array { if (! $this->stabileAttivo) { return []; } try { $rows = DB::connection('gescon_import') ->table('voc_spe') ->where('cod_stabile', $this->stabileAttivo->codice_stabile) ->orderBy('descriz') ->get(['id_vocspe', 'cod', 'descriz', 'tabella']); $out = []; foreach ($rows as $r) { $out[(int) $r->id_vocspe] = trim((string) $r->cod) . ' — ' . trim((string) $r->descriz) . ' (TAB ' . trim((string) $r->tabella) . ')'; } return $out; } catch (\Throwable $e) { return []; } } private function searchLegacyVoci(string $search): array { if (! $this->stabileAttivo) { return []; } $search = trim($search); if ($search === '') { return []; } try { $rows = DB::connection('gescon_import') ->table('voc_spe') ->where('cod_stabile', $this->stabileAttivo->codice_stabile) ->where(function ($q) use ($search) { $like = '%' . str_replace(['%', '_'], ['\\%', '\\_'], $search) . '%'; $q->where('cod', 'like', $like) ->orWhere('descriz', 'like', $like) ->orWhere('tabella', 'like', $like); }) ->orderBy('descriz') ->limit(50) ->get(['id_vocspe', 'cod', 'descriz', 'tabella']); $out = []; foreach ($rows as $r) { $out[(int) $r->id_vocspe] = trim((string) $r->cod) . ' — ' . trim((string) $r->descriz) . ' (TAB ' . trim((string) $r->tabella) . ')'; } return $out; } catch (\Throwable $e) { return []; } } private function getLegacyVoceLabel($value): ?string { $id = is_numeric($value) ? (int) $value : null; if (! $id || ! $this->stabileAttivo) { return null; } try { $row = DB::connection('gescon_import') ->table('voc_spe') ->where('cod_stabile', $this->stabileAttivo->codice_stabile) ->where('id_vocspe', $id) ->first(['cod', 'descriz', 'tabella']); if (! $row) { return null; } return trim((string) $row->cod) . ' — ' . trim((string) $row->descriz) . ' (TAB ' . trim((string) $row->tabella) . ')'; } catch (\Throwable $e) { return null; } } private function fetchLegacyVoce(?int $id): ?array { if (! $this->stabileAttivo || ! $id) { return null; } try { $row = DB::connection('gescon_import') ->table('voc_spe') ->where('cod_stabile', $this->stabileAttivo->codice_stabile) ->where('id_vocspe', $id) ->first(); if (! $row) { return null; } return (array) $row; } catch (\Throwable $e) { return null; } } private function loadLegacyVoce(int $id): void { $legacy = $this->fetchLegacyVoce($id); if (! $legacy) { return; } $legacyTabella = isset($legacy['tabella']) ? (string) $legacy['tabella'] : null; $mapped = $this->mapLegacyTabellaToTabellaMillesimaleId($legacyTabella); $current = $this->getSchema('form')?->getState() ?? []; $descr = (string) ($current['descrizione'] ?? ''); if ($descr === '') { $descr = (string) ($legacy['descriz'] ?? ''); } $fill = [ 'legacy_tabella' => $legacyTabella, 'legacy_prev_euro' => isset($legacy['preventivo_euro']) && is_numeric($legacy['preventivo_euro']) ? (float) $legacy['preventivo_euro'] : null, 'legacy_cons_euro' => isset($legacy['consuntivo_euro']) && is_numeric($legacy['consuntivo_euro']) ? (float) $legacy['consuntivo_euro'] : null, 'descrizione' => $descr, ]; if ($mapped) { $fill['tabella_millesimale_id'] = $mapped; } $this->getSchema('form')?->fill($fill); } private function mapLegacyTabellaToTabellaMillesimaleId(?string $legacyTabella): ?int { if (! $this->stabileAttivo || ! $legacyTabella) { return null; } $raw = strtoupper(trim($legacyTabella)); $candidates = [$raw]; if (! str_starts_with($raw, 'TAB')) { $candidates[] = 'TAB.' . $raw; } $q = TabellaMillesimale::query()->where('stabile_id', $this->stabileAttivo->id); foreach ($candidates as $c) { $found = (clone $q) ->where(function ($qq) use ($c) { $qq->where('codice_tabella', $c) ->orWhere('nome_tabella', $c); }) ->value('id'); if ($found) { return (int) $found; } } return null; } private function getTabelleOptions(): array { if (! $this->stabileAttivo) { return []; } return TabellaMillesimale::query() ->where('stabile_id', $this->stabileAttivo->id) ->orderBy('codice_tabella') ->get(['id', 'codice_tabella', 'nome_tabella', 'denominazione']) ->mapWithKeys(function (TabellaMillesimale $t) { $label = $t->codice_tabella ?: ($t->denominazione ?: ($t->nome_tabella ?: null)); return [$t->id => ($label ?: ('Tabella #' . (int) $t->id))]; }) ->all(); } private function getFattureOptions(): array { if (! $this->stabileAttivo) { return []; } return FatturaElettronica::query() ->where('stabile_id', $this->stabileAttivo->id) ->orderByDesc('data_fattura') ->limit(200) ->get() ->mapWithKeys(function (FatturaElettronica $f) { $label = trim(($f->data_fattura?->format('Y-m-d') ?: '') . ' — ' . ($f->fornitore_denominazione ?: 'ND') . ' — ' . ($f->numero_fattura ?: 'ND') . ' — ' . number_format((float) $f->totale, 2, ',', '.') . '€'); return [$f->id => $label]; }) ->all(); } private function getSelectedFatture(array $state) { if (! $this->stabileAttivo) { return collect(); } if (($state['tipo_spesa'] ?? 'singola') === 'aggregata') { $ids = array_values(array_filter(array_map('intval', (array) ($state['fatture_elettroniche_ids'] ?? [])))); if (count($ids) === 0) { return collect(); } return FatturaElettronica::query() ->where('stabile_id', $this->stabileAttivo->id) ->whereIn('id', $ids) ->get(); } $id = (int) ($state['fattura_elettronica_id'] ?? 0); if (! $id) { return collect(); } return FatturaElettronica::query() ->where('stabile_id', $this->stabileAttivo->id) ->where('id', $id) ->get(); } private function getParsedOrStoredXmlData(FatturaElettronica $fattura): array { $data = [ 'pagamento_modalita' => $fattura->pagamento_modalita, 'pagamento_iban' => $fattura->pagamento_iban, 'pagamento_bic' => $fattura->pagamento_bic, 'consumo_valore' => $fattura->consumo_valore, 'consumo_unita' => $fattura->consumo_unita, 'consumo_riferimento' => $fattura->consumo_riferimento, ]; $needsParse = empty($data['pagamento_modalita']) && empty($data['pagamento_iban']) && empty($data['consumo_valore']) && empty($data['consumo_riferimento']); if ($needsParse && is_string($fattura->xml_content) && $fattura->xml_content !== '') { try { return app(FatturaElettronicaXmlParser::class)->parse($fattura->xml_content); } catch (\Throwable $e) { return $data; } } return $data; } private function getFornitoreDenominazione(int $fornitoreId): ?string { return (string) (\App\Models\Fornitore::query()->whereKey($fornitoreId)->value('ragione_sociale') ?: \App\Models\Fornitore::query()->whereKey($fornitoreId)->value('nome')); } private function firstOrCreateVoceSpesaFromLegacy(array $legacy, int $tabellaId): VoceSpesa { $descr = trim((string) ($legacy['descriz'] ?? 'Spesa')); $existing = VoceSpesa::query() ->where('stabile_id', $this->stabileAttivo->id) ->where('descrizione', $descr) ->first(); if ($existing) { if (! $existing->tabella_millesimale_default_id) { $existing->tabella_millesimale_default_id = $tabellaId; $existing->save(); } return $existing; } $categoria = $this->guessCategoria($descr); return VoceSpesa::query()->create([ 'stabile_id' => $this->stabileAttivo->id, 'descrizione' => $descr, 'tipo_gestione' => 'ordinaria', 'categoria' => $categoria, 'tabella_millesimale_default_id' => $tabellaId, 'ritenuta_acconto_default' => 0, 'attiva' => true, 'ordinamento' => 0, ]); } private function guessCategoria(string $descrizione): string { $d = mb_strtolower($descrizione); if (str_contains($d, 'acqua') || str_contains($d, 'idrico')) { return 'acqua'; } if (str_contains($d, 'riscald')) { return 'riscaldamento'; } if (str_contains($d, 'energia') || str_contains($d, 'luce') || str_contains($d, 'elettr')) { return 'energia'; } return 'generale'; } }