netgescon-day0/app/Filament/Pages/Gescon/FornitoreScheda.php

1525 lines
70 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Filament\Pages\Gescon;
use App\Models\Documento;
use App\Models\FatturaElettronica;
use App\Models\Fornitore;
use App\Models\FornitoreDipendente;
use App\Models\FornitoreStabileImpostazione;
use App\Models\RegistroRitenuteAcconto;
use App\Models\RubricaUniversale;
use App\Models\Stabile;
use App\Models\StabileServizio;
use App\Models\StgFatturaAde;
use App\Models\User;
use App\Models\VoceSpesa;
use App\Modules\Contabilita\Models\FatturaFornitore as ContabilitaFatturaFornitore;
use App\Modules\Contabilita\Models\PianoConti;
use App\Services\Consumi\AcquaPdfTextParser;
use App\Services\Consumi\ConsumiAcquaIngestionService;
use App\Services\Consumi\ConsumiAcquaTariffeIngestionService;
use App\Services\Documenti\PdfTextExtractionService;
use App\Services\FattureElettroniche\FatturaElettronicaProtocolloService;
use App\Support\StabileContext;
use Filament\Actions\Action;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
class FornitoreScheda extends Page
{
protected static ?string $title = 'Scheda fornitore';
protected static ?string $slug = 'anagrafica/fornitori/{record}';
protected static bool $shouldRegisterNavigation = false;
protected string $view = 'filament.pages.gescon.fornitore-scheda';
public Fornitore $fornitore;
/** @var array<string, mixed> */
public array $box = [];
/** @var array<int, array<string, mixed>> */
public array $anteprimaAde = [];
/** @var array<int, array<string, mixed>> */
public array $anteprimaFe = [];
/** @var array<int, array<string, mixed>> */
public array $anteprimaContabilita = [];
/** @var array<int, array<string, mixed>> */
public array $fattureAssociate = [];
/** @var array<int, array<string, mixed>> */
public array $raVersateRows = [];
public string $tagsInput = '';
/** @var array<int, string> */
public array $tagSuggestions = [];
/** @var array<int, array<string, mixed>> */
public array $dipendentiRows = [];
public string $nuovoDipendenteNome = '';
public string $nuovoDipendenteCognome = '';
public string $nuovoDipendenteEmail = '';
public string $nuovoDipendenteTelefono = '';
public ?string $lastGeneratedPassword = null;
public function cleanDisplayValue(?string $value, string $fallback = '-'): string
{
$v = trim((string) $value);
if ($v === '') {
return $fallback;
}
$upper = strtoupper($v);
if (in_array($upper, ['NULL', 'N/D', 'ND', 'N.A.', '-', '--', '[]', '{}'], true)) {
return $fallback;
}
return $v;
}
public function mount(int | string $record): void
{
$user = Auth::user();
if (! $user instanceof User) {
abort(403);
}
if (! $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore'])) {
abort(403);
}
$this->fornitore = Fornitore::query()->with(['rubrica', 'amministratore'])->findOrFail((int) $record);
// Tenant guard: impedisce accesso cross-amministratore via URL.
if (! $user->hasAnyRole(['super-admin', 'admin'])) {
$allowedAdminIds = [];
$activeStabile = StabileContext::getActiveStabile($user);
$activeAdminId = (int) ($activeStabile?->amministratore_id ?: 0);
if ($activeAdminId > 0) {
$allowedAdminIds[] = $activeAdminId;
}
$userAdminId = (int) ($user->amministratore?->id ?: 0);
if ($userAdminId > 0) {
$allowedAdminIds[] = $userAdminId;
}
$ownerAdminId = (int) ($user->amministratoreOwner?->id ?: 0);
if ($ownerAdminId > 0) {
$allowedAdminIds[] = $ownerAdminId;
}
// Collaboratori: includi anche gli amministratori degli stabili assegnati.
try {
$assigned = $user->stabiliAssegnati()->pluck('amministratore_id')->all();
foreach ($assigned as $aid) {
$aid = (int) $aid;
if ($aid > 0) {
$allowedAdminIds[] = $aid;
}
}
} catch (\Throwable) {
// ignore
}
$allowedAdminIds = array_values(array_unique(array_filter($allowedAdminIds, fn($v) => (int) $v > 0)));
$fornitoreAdminIds = array_values(array_unique(array_filter([
(int) ($this->fornitore->amministratore_id ?? 0),
(int) ($this->fornitore->rubrica?->amministratore_id ?? 0),
], fn($v) => (int) $v > 0)));
if ($fornitoreAdminIds === []) {
if (! $user->hasAnyRole(['admin', 'amministratore'])) {
abort(404);
}
} else {
$hasIntersection = count(array_intersect($fornitoreAdminIds, $allowedAdminIds)) > 0;
if (! $hasIntersection) {
abort(404);
}
}
}
$this->hydrateBoxData($user);
$this->tagsInput = (string) ($this->fornitore->tags ?? '');
$this->loadTagSuggestions();
$this->refreshDipendentiRows();
}
public function creaDipendenteFornitore(): void
{
$nome = trim($this->nuovoDipendenteNome);
$email = mb_strtolower(trim($this->nuovoDipendenteEmail));
if ($nome === '' || $email === '') {
Notification::make()->title('Nome ed email sono obbligatori')->warning()->send();
return;
}
$exists = FornitoreDipendente::query()
->where('fornitore_id', (int) $this->fornitore->id)
->whereRaw('LOWER(email) = ?', [$email])
->exists();
if ($exists) {
Notification::make()->title('Dipendente gia presente per questo fornitore')->warning()->send();
return;
}
FornitoreDipendente::query()->create([
'fornitore_id' => (int) $this->fornitore->id,
'nome' => $nome,
'cognome' => trim($this->nuovoDipendenteCognome) ?: null,
'email' => $email,
'telefono' => trim($this->nuovoDipendenteTelefono) ?: null,
'attivo' => true,
'created_by_user_id' => Auth::id(),
'updated_by_user_id' => Auth::id(),
]);
$this->nuovoDipendenteNome = '';
$this->nuovoDipendenteCognome = '';
$this->nuovoDipendenteEmail = '';
$this->nuovoDipendenteTelefono = '';
$this->refreshDipendentiRows();
Notification::make()->title('Dipendente aggiunto')->success()->send();
}
public function abilitaAccessoDipendente(int $dipendenteId): void
{
$dipendente = FornitoreDipendente::query()
->where('fornitore_id', (int) $this->fornitore->id)
->find($dipendenteId);
if (! $dipendente instanceof FornitoreDipendente) {
Notification::make()->title('Dipendente non trovato')->danger()->send();
return;
}
$email = trim((string) ($dipendente->email ?? ''));
if ($email === '') {
Notification::make()->title('Email dipendente mancante')->warning()->send();
return;
}
$targetUser = User::query()->whereRaw('LOWER(email) = ?', [mb_strtolower($email)])->first();
$created = false;
if (! $targetUser instanceof User) {
$generatedPassword = Str::random(12);
$targetUser = User::query()->create([
'name' => trim((string) ($dipendente->nome_completo ?: 'Utente fornitore')),
'email' => $email,
'password' => Hash::make($generatedPassword),
'email_verified_at' => now(),
'is_active' => true,
]);
$this->lastGeneratedPassword = $generatedPassword;
$created = true;
}
$targetUser->assignRole('fornitore');
$dipendente->user_id = (int) $targetUser->id;
$dipendente->attivo = true;
$dipendente->updated_by_user_id = Auth::id();
$dipendente->save();
$this->refreshDipendentiRows();
Notification::make()
->title('Accesso dipendente abilitato')
->body($created
? 'Accesso creato. Password temporanea: ' . ($this->lastGeneratedPassword ?? '(non disponibile)')
: 'Accesso collegato ad utente esistente.')
->success()
->send();
}
public function resetPasswordDipendenteUser(int $userId): void
{
$dipendente = FornitoreDipendente::query()
->where('fornitore_id', (int) $this->fornitore->id)
->where('user_id', $userId)
->first();
if (! $dipendente instanceof FornitoreDipendente) {
Notification::make()->title('Utente non collegato a questo fornitore')->danger()->send();
return;
}
$target = User::query()->find($userId);
if (! $target instanceof User) {
Notification::make()->title('Utente non trovato')->danger()->send();
return;
}
$newPassword = Str::random(12);
$target->password = Hash::make($newPassword);
$target->save();
$this->lastGeneratedPassword = $newPassword;
Notification::make()
->title('Password dipendente resettata')
->body('Nuova password temporanea: ' . $newPassword)
->success()
->send();
}
public function toggleDipendenteAttivo(int $dipendenteId): void
{
$dipendente = FornitoreDipendente::query()
->where('fornitore_id', (int) $this->fornitore->id)
->find($dipendenteId);
if (! $dipendente instanceof FornitoreDipendente) {
Notification::make()->title('Dipendente non trovato')->danger()->send();
return;
}
$dipendente->attivo = ! (bool) $dipendente->attivo;
$dipendente->updated_by_user_id = Auth::id();
$dipendente->save();
$this->refreshDipendentiRows();
Notification::make()->title('Stato dipendente aggiornato')->success()->send();
}
public function saveTags(): void
{
if (! Schema::hasColumn('fornitori', 'tags')) {
Notification::make()->title('Colonna tags non presente')->warning()->send();
return;
}
$normalized = $this->normalizeTags($this->tagsInput);
$this->fornitore->tags = implode(', ', $normalized);
$this->fornitore->save();
$this->tagsInput = (string) ($this->fornitore->tags ?? '');
$this->loadTagSuggestions();
Notification::make()->title('Tag fornitore salvati')->success()->send();
}
public function importLegacyTags(): void
{
if (! Schema::hasColumn('fornitori', 'tags')) {
Notification::make()->title('Colonna tags non presente')->warning()->send();
return;
}
try {
Artisan::call('fornitori:import-legacy-tags', [
'--fornitore-id' => [(int) $this->fornitore->id],
]);
} catch (\Throwable $e) {
Notification::make()->title('Import tag legacy fallito')->body($e->getMessage())->danger()->send();
return;
}
$this->fornitore->refresh();
$this->tagsInput = (string) ($this->fornitore->tags ?? '');
$this->loadTagSuggestions();
Notification::make()->title('Tag legacy importati')->success()->send();
}
public function syncRubricaFromFornitore(): void
{
$payload = [
'tipo_contatto' => 'persona_giuridica',
'categoria' => 'fornitore',
'stato' => 'attivo',
'ragione_sociale' => $this->fornitore->ragione_sociale ?: trim(($this->fornitore->nome ?? '') . ' ' . ($this->fornitore->cognome ?? '')),
'nome' => $this->fornitore->nome,
'cognome' => $this->fornitore->cognome,
'partita_iva' => $this->fornitore->partita_iva,
'codice_fiscale' => $this->fornitore->codice_fiscale,
'indirizzo' => $this->fornitore->indirizzo,
'civico' => $this->fornitore->civico,
'cap' => $this->fornitore->cap,
'citta' => $this->fornitore->citta,
'provincia' => $this->fornitore->provincia,
'nazione' => $this->fornitore->nazione,
'email' => $this->fornitore->email,
'pec' => $this->fornitore->pec,
'telefono_ufficio' => $this->fornitore->telefono,
'telefono_cellulare' => $this->fornitore->cellulare,
'sito_web' => $this->fornitore->sito_web,
'note' => $this->fornitore->note,
'data_ultima_modifica' => now()->toDateString(),
];
$rubrica = null;
if ((int) ($this->fornitore->rubrica_id ?? 0) > 0) {
$rubrica = RubricaUniversale::query()->find((int) $this->fornitore->rubrica_id);
}
if (! $rubrica instanceof RubricaUniversale) {
$rubrica = RubricaUniversale::query()->create(array_merge($payload, [
'data_inserimento' => now()->toDateString(),
]));
} else {
$rubrica->fill($payload);
$rubrica->save();
}
if ((int) ($this->fornitore->rubrica_id ?? 0) !== (int) $rubrica->id) {
$this->fornitore->rubrica_id = (int) $rubrica->id;
$this->fornitore->save();
}
$this->fornitore->refresh();
Notification::make()->title('Rubrica aggiornata dal fornitore')->success()->send();
}
private function loadTagSuggestions(): void
{
if (! Schema::hasColumn('fornitori', 'tags')) {
$this->tagSuggestions = [];
return;
}
$all = Fornitore::query()
->whereNotNull('tags')
->pluck('tags')
->all();
$pool = [];
foreach ($all as $row) {
foreach ($this->splitTags((string) $row) as $tag) {
$pool[] = $tag;
}
}
$pool = array_values(array_unique($pool));
sort($pool, SORT_NATURAL | SORT_FLAG_CASE);
$this->tagSuggestions = array_slice($pool, 0, 300);
}
private function refreshDipendentiRows(): void
{
$this->dipendentiRows = FornitoreDipendente::query()
->with('user:id,name,email')
->where('fornitore_id', (int) $this->fornitore->id)
->orderByDesc('attivo')
->orderBy('nome')
->orderBy('cognome')
->get()
->map(function (FornitoreDipendente $d): array {
$userId = (int) ($d->user_id ?? 0);
return [
'id' => (int) $d->id,
'nome' => (string) ($d->nome_completo ?: 'Dipendente #' . $d->id),
'email' => (string) ($d->email ?? ''),
'telefono' => (string) ($d->telefono ?? ''),
'attivo' => (bool) $d->attivo,
'user_id' => $userId,
'user_label' => $userId > 0 ? ((string) ($d->user?->name ?: ('Utente #' . $userId))): '-',
];
})
->all();
}
/**
* @return array<int, string>
*/
private function normalizeTags(string $input): array
{
$tags = [];
foreach ($this->splitTags($input) as $tag) {
$normalized = $this->canonicalizeTag($tag);
if ($normalized !== null) {
$tags[] = $normalized;
}
}
$tags = array_values(array_unique($tags));
sort($tags, SORT_NATURAL | SORT_FLAG_CASE);
return $tags;
}
/**
* @return array<int, string>
*/
private function splitTags(string $value): array
{
$parts = preg_split('/[,;|\n\r\/]+/', $value) ?: [];
return array_values(array_filter(array_map(function (string $part): string {
$clean = trim($part);
$clean = preg_replace('/\s+/', ' ', $clean) ?? '';
return $clean;
}, $parts), fn(string $p): bool => $p !== ''));
}
private function canonicalizeTag(string $raw): ?string
{
$clean = trim(mb_strtolower($raw));
if ($clean === '' || mb_strlen($clean) < 3) {
return null;
}
$map = [
'idr' => 'idraulico',
'idraul' => 'idraulico',
'elett' => 'elettricista',
'elettric' => 'elettricista',
'ascens' => 'ascensorista',
'puliz' => 'pulizie',
'giardin' => 'giardiniere',
'assicur' => 'assicurazione',
'manut' => 'manutenzione',
];
foreach ($map as $prefix => $canonical) {
if (str_starts_with($clean, $prefix)) {
return $canonical;
}
}
return $clean;
}
private function hydrateBoxData(User $user): void
{
$this->fattureAssociate = [];
$this->raVersateRows = [];
$this->box = [
'stabile_id' => null,
'fornitore_features' => [],
'fornitore_defaults' => [
'voce_spesa_default_id' => null,
'conto_costo_default_id' => null,
],
'ade' => ['count' => 0, 'lordo' => 0.0],
'fe' => ['count' => 0, 'totale' => 0.0, 'contabilizzate' => 0],
'contabilita' => [
'aperte_count' => 0,
'aperte_netto' => 0.0,
'pagate_count' => 0,
'pagate_netto' => 0.0,
],
'da_pagare' => ['source' => null, 'importo' => 0.0],
'ra' => [
'da_versare' => 0.0,
'versata' => 0.0,
'compensata' => 0.0,
],
];
$activeStabileId = StabileContext::resolveActiveStabileId($user);
if (! $activeStabileId) {
return;
}
$stabile = Stabile::query()->select(['id', 'amministratore_id'])->find($activeStabileId);
$amministratoreId = (int) ($stabile?->amministratore_id ?: 0);
if ($amministratoreId <= 0) {
return;
}
$this->box['stabile_id'] = $activeStabileId;
// Feature flags & defaults (per stabile) + fallback (per fornitore)
$settings = null;
if (Schema::hasTable('fornitore_stabile_impostazioni')) {
$settings = FornitoreStabileImpostazione::query()
->where('stabile_id', (int) $activeStabileId)
->where('fornitore_id', (int) $this->fornitore->id)
->first();
if ($settings) {
$this->box['fornitore_defaults']['voce_spesa_default_id'] = $settings->voce_spesa_default_id ? (int) $settings->voce_spesa_default_id : null;
$this->box['fornitore_defaults']['conto_costo_default_id'] = $settings->conto_costo_default_id ? (int) $settings->conto_costo_default_id : null;
}
}
$features = [];
if (Schema::hasColumn('fornitori', 'fe_features')) {
$features = is_array($this->fornitore->fe_features ?? null) ? $this->fornitore->fe_features : [];
} elseif ($settings && is_array($settings->meta ?? null)) {
$features = is_array($settings->meta['fe_features'] ?? null) ? $settings->meta['fe_features'] : [];
}
$this->box['fornitore_features'] = $features;
$normalize = static function (?string $value): string {
$value = strtoupper(trim((string) $value));
$value = str_replace(' ', '', $value);
return $value;
};
$ids = [];
$piva = $normalize($this->fornitore->partita_iva);
$cf = $normalize($this->fornitore->codice_fiscale);
if ($piva !== '') {
$ids[] = $piva;
if (str_starts_with($piva, 'IT') && strlen($piva) > 2) {
$ids[] = substr($piva, 2);
}
}
if ($cf !== '') {
$ids[] = $cf;
}
$ids = array_values(array_unique(array_filter($ids, fn($v) => $v !== '')));
// AdE (staging)
if (! empty($ids) && Schema::hasTable('stg_fatture_ade')) {
$adeBase = StgFatturaAde::query()
->where('amministratore_id', $amministratoreId)
->where(function (\Illuminate\Database\Eloquent\Builder $q) use ($activeStabileId): void {
$q->where('stabile_id', $activeStabileId)->orWhereNull('stabile_id');
})
->where(function (\Illuminate\Database\Eloquent\Builder $q) use ($ids): void {
foreach ($ids as $id) {
$q->orWhereRaw("REPLACE(UPPER(identificativo_fornitore), ' ', '') = ?", [$id]);
}
});
$this->box['ade']['count'] = (int) (clone $adeBase)->count();
$lordo = (float) (clone $adeBase)->selectRaw('COALESCE(SUM(imponibile + imposta), 0) as lordo')->value('lordo');
$this->box['ade']['lordo'] = $lordo;
$this->anteprimaAde = (clone $adeBase)
->orderByDesc('data_emissione')
->orderByDesc('id')
->limit(8)
->get(['id', 'data_emissione', 'numero_documento', 'imponibile', 'imposta', 'sdi_file'])
->map(fn(StgFatturaAde $r) => [
'id' => $r->id,
'data' => $r->data_emissione?->format('d/m/Y'),
'numero' => (string) ($r->numero_documento ?? ''),
'lordo' => (float) ($r->imponibile ?? 0) + (float) ($r->imposta ?? 0),
'sdi_file' => (string) ($r->sdi_file ?? ''),
])
->all();
}
// FE importate
if (Schema::hasTable('fatture_elettroniche')) {
$feBase = FatturaElettronica::query()
->where('stabile_id', $activeStabileId)
->where(function (\Illuminate\Database\Eloquent\Builder $q) use ($ids): void {
$q->where('fornitore_id', $this->fornitore->id);
if (! empty($ids)) {
$q->orWhere(function (\Illuminate\Database\Eloquent\Builder $qq) use ($ids): void {
foreach ($ids as $id) {
$qq->orWhereRaw("REPLACE(UPPER(fornitore_piva), ' ', '') = ?", [$id])
->orWhereRaw("REPLACE(UPPER(fornitore_cf), ' ', '') = ?", [$id]);
}
});
}
});
$this->box['fe']['count'] = (int) (clone $feBase)->count();
$this->box['fe']['totale'] = (float) (clone $feBase)->selectRaw('COALESCE(SUM(totale), 0) as totale')->value('totale');
$this->box['fe']['contabilizzate'] = (int) (clone $feBase)
->where(function (\Illuminate\Database\Eloquent\Builder $q): void {
$q->whereNotNull('registrazione_contabile_id')->orWhere('stato', 'contabilizzata');
})
->count();
$this->anteprimaFe = (clone $feBase)
->orderByDesc('data_fattura')
->orderByDesc('id')
->limit(8)
->get(['id', 'data_fattura', 'numero_fattura', 'totale', 'sdi_file', 'stato'])
->map(fn(FatturaElettronica $r) => [
'id' => $r->id,
'data' => $r->data_fattura?->format('d/m/Y'),
'numero' => (string) ($r->numero_fattura ?? ''),
'totale' => (float) ($r->totale ?? 0),
'sdi_file' => (string) ($r->sdi_file ?? ''),
'stato' => (string) ($r->stato ?? ''),
])
->all();
}
// Contabilità (fatture fornitori)
if (Schema::hasTable('contabilita_fatture_fornitori')) {
$contabBase = ContabilitaFatturaFornitore::query()
->where('stabile_id', $activeStabileId)
->where('fornitore_id', (int) $this->fornitore->id);
// Coerente con la logica della scheda contabile: aperto = totale - pagato.
$totNetto = (float) (clone $contabBase)->selectRaw('COALESCE(SUM(netto_da_pagare), 0) as netto')->value('netto');
$this->box['contabilita']['pagate_count'] = (int) (clone $contabBase)->where('stato', 'pagato')->count();
$this->box['contabilita']['pagate_netto'] = (float) (clone $contabBase)->where('stato', 'pagato')->selectRaw('COALESCE(SUM(netto_da_pagare), 0) as netto')->value('netto');
$this->box['contabilita']['aperte_netto'] = round($totNetto - (float) $this->box['contabilita']['pagate_netto'], 2);
$this->box['contabilita']['aperte_count'] = (int) (clone $contabBase)->where('stato', '!=', 'pagato')->count();
$this->anteprimaContabilita = (clone $contabBase)
->orderByDesc('data_documento')
->orderByDesc('id')
->limit(8)
->get(['id', 'data_documento', 'numero_documento', 'netto_da_pagare', 'stato'])
->map(fn(ContabilitaFatturaFornitore $r) => [
'id' => $r->id,
'data' => $r->data_documento?->format('d/m/Y'),
'numero' => (string) ($r->numero_documento ?? ''),
'netto' => (float) ($r->netto_da_pagare ?? 0),
'stato' => (string) ($r->stato ?? ''),
])
->all();
$this->fattureAssociate = (clone $contabBase)
->orderByDesc('data_documento')
->orderByDesc('id')
->limit(30)
->get(['id', 'data_documento', 'numero_documento', 'totale', 'netto_da_pagare', 'stato', 'fattura_elettronica_id'])
->map(fn(ContabilitaFatturaFornitore $r) => [
'id' => (int) $r->id,
'data' => $r->data_documento?->format('d/m/Y'),
'numero' => (string) ($r->numero_documento ?? ''),
'totale' => (float) ($r->totale ?? 0),
'netto' => (float) ($r->netto_da_pagare ?? 0),
'stato' => (string) ($r->stato ?? ''),
'source' => ((int) ($r->fattura_elettronica_id ?? 0) > 0) ? 'FE' : 'manuale',
])
->all();
}
// Registro RA
if (Schema::hasTable('registro_ritenute_acconto')) {
$raBase = RegistroRitenuteAcconto::query()
->where('fornitore_id', (int) $this->fornitore->id)
->whereHas('gestione', fn(\Illuminate\Database\Eloquent\Builder $q) => $q->where('stabile_id', (int) $activeStabileId));
foreach (['da_versare', 'versata', 'compensata'] as $stato) {
$this->box['ra'][$stato] = (float) (clone $raBase)
->where('stato_versamento', $stato)
->selectRaw('COALESCE(SUM(importo_ritenuta), 0) as ra')
->value('ra');
}
$this->raVersateRows = (clone $raBase)
->whereIn('stato_versamento', ['versata', 'compensata'])
->orderByDesc('data_versamento')
->orderByDesc('id')
->limit(30)
->get(['id', 'data_versamento', 'importo_ritenuta', 'stato_versamento', 'codice_tributo', 'f24_riferimento'])
->map(fn(RegistroRitenuteAcconto $r) => [
'id' => (int) $r->id,
'data_versamento' => $r->data_versamento?->format('d/m/Y'),
'importo' => (float) ($r->importo_ritenuta ?? 0),
'stato' => (string) ($r->stato_versamento ?? ''),
'tributo' => (string) ($r->codice_tributo ?? ''),
'f24' => (string) ($r->f24_riferimento ?? ''),
])
->all();
}
// Totale da pagare (priorità: contabilità → FE → AdE)
if ((float) ($this->box['contabilita']['aperte_netto'] ?? 0) > 0) {
$this->box['da_pagare'] = ['source' => 'contabilita', 'importo' => (float) $this->box['contabilita']['aperte_netto']];
} elseif ((int) ($this->box['fe']['count'] ?? 0) > 0) {
$this->box['da_pagare'] = ['source' => 'fe', 'importo' => (float) ($this->box['fe']['totale'] ?? 0)];
} elseif ((int) ($this->box['ade']['count'] ?? 0) > 0) {
$this->box['da_pagare'] = ['source' => 'ade', 'importo' => (float) ($this->box['ade']['lordo'] ?? 0)];
}
}
protected function getHeaderActions(): array
{
return [
Action::make('apri_rubrica')
->label('Apri anagrafica unica')
->icon('heroicon-o-user')
->visible(fn(): bool => (int) ($this->fornitore->rubrica_id ?? 0) > 0)
->url(fn() => RubricaUniversaleScheda::getUrl([
'record' => (int) $this->fornitore->rubrica_id,
], panel: 'admin-filament')),
Action::make('sync_rubrica')
->label('Sincronizza Rubrica')
->icon('heroicon-o-arrow-path')
->action(fn() => $this->syncRubricaFromFornitore()),
Action::make('importa_tag_legacy')
->label('Importa TAG legacy')
->icon('heroicon-o-tag')
->action(fn() => $this->importLegacyTags()),
Action::make('impostazioni_fe')
->label('Impostazioni FE')
->icon('heroicon-o-adjustments-horizontal')
->visible(fn(): bool => Schema::hasColumn('fornitori', 'escludi_righe_fe'))
->form([
Toggle::make('escludi_righe_fe')
->label('Escludi righe FE (import e visualizzazione)')
->helperText('Utile per utenze (acqua/luce/gas): si userà il PDF per i dati di consumo invece delle righe XML.')
->default((bool) ($this->fornitore->escludi_righe_fe ?? false)),
Select::make('conto_costo_default_id')
->label('Voce spesa predefinita (sottoconto costo)')
->helperText('Valore suggerito per imputare i costi quando le righe FE non hanno sottoconto o sono assenti.')
->options(function (): array {
if (! Schema::hasTable('contabilita_piano_conti')) {
return [];
}
return PianoConti::query()
->orderBy('codice')
->limit(1000)
->get(['id', 'codice', 'descrizione'])
->mapWithKeys(fn(PianoConti $c) => [(string) $c->id => $c->codice . ' - ' . $c->descrizione])
->all();
})
->searchable()
->nullable()
->default(fn(): ?int => $this->box['fornitore_defaults']['conto_costo_default_id'] ? (int) $this->box['fornitore_defaults']['conto_costo_default_id'] : null),
Select::make('voce_spesa_default_id')
->label('Voce di spesa predefinita (ripartizione)')
->helperText('Collegamento “logico” alla voce (utile per consumi/statistiche/ripartizione; es. acqua).')
->options(function (): array {
$user = Auth::user();
if (! $user instanceof User) {
return [];
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId || ! Schema::hasTable('voci_spesa')) {
return [];
}
return VoceSpesa::query()
->where('stabile_id', (int) $stabileId)
->orderBy('descrizione')
->limit(800)
->get(['id', 'codice', 'descrizione'])
->mapWithKeys(function (VoceSpesa $v) {
$label = trim((string) ($v->codice ?? ''));
if ($label !== '') {
$label .= ' — ';
}
$label .= (string) ($v->descrizione ?? ('Voce #' . $v->id));
return [(string) $v->id => $label];
})
->all();
})
->searchable()
->nullable()
->default(fn(): ?int => $this->box['fornitore_defaults']['voce_spesa_default_id'] ? (int) $this->box['fornitore_defaults']['voce_spesa_default_id'] : null),
])
->action(function (array $data): void {
if (! Schema::hasColumn('fornitori', 'escludi_righe_fe')) {
Notification::make()->title('Colonna mancante: escludi_righe_fe')->warning()->send();
return;
}
$this->fornitore->escludi_righe_fe = (bool) ($data['escludi_righe_fe'] ?? false);
$this->fornitore->save();
$user = Auth::user();
if (! $user instanceof User) {
return;
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId) {
return;
}
if (! Schema::hasTable('fornitore_stabile_impostazioni')) {
return;
}
$voceSpesaId = isset($data['voce_spesa_default_id']) && is_numeric($data['voce_spesa_default_id']) ? (int) $data['voce_spesa_default_id'] : null;
$contoCostoId = isset($data['conto_costo_default_id']) && is_numeric($data['conto_costo_default_id']) ? (int) $data['conto_costo_default_id'] : null;
FornitoreStabileImpostazione::query()->updateOrCreate(
[
'stabile_id' => (int) $stabileId,
'fornitore_id' => (int) $this->fornitore->id,
],
[
'voce_spesa_default_id' => $voceSpesaId ?: null,
'conto_costo_default_id' => $contoCostoId ?: null,
]
);
$this->hydrateBoxData($user);
})
->successNotificationTitle('Impostazioni FE salvate'),
Action::make('acqua_config')
->label('Configura acqua')
->icon('heroicon-o-beaker')
->form([
Toggle::make('acqua_enabled')
->label('Abilita acquisizione dati acqua (PDF)')
->default((bool) ((is_array($this->box['fornitore_features'] ?? null) ? ($this->box['fornitore_features']['acqua']['enabled'] ?? false) : false)))
->helperText('Attiva le funzioni acqua su questo fornitore (scan + letture/periodi).'),
Toggle::make('acqua_scan_enabled')
->label('Abilita scansione massiva su FE già importate')
->default((bool) ((is_array($this->box['fornitore_features'] ?? null) ? ($this->box['fornitore_features']['acqua']['scan_enabled'] ?? false) : false))),
Toggle::make('acqua_ticket_on_anomaly')
->label('Apri ticket su anomalie/mismatch')
->default(true)
->helperText('Se il parser non trova dati o cambia utenza/contatore, apre un ticket interno.'),
Repeater::make('utenze_acqua')
->label('Utenze acqua (stabile attivo)')
->helperText('Compila i campi per ogni contatore/utenza (lo stabile può averne più di uno).')
->default(function (): array {
$user = Auth::user();
if (! $user instanceof User) {
return [];
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId || ! Schema::hasTable('stabile_servizi')) {
return [];
}
return StabileServizio::query()
->where('stabile_id', (int) $stabileId)
->where('tipo', 'acqua')
->orderByDesc('attivo')
->orderBy('nome')
->limit(50)
->get([
'id',
'nome',
'attivo',
'voce_spesa_id',
'contatore_matricola',
'codice_utenza',
'codice_cliente',
'codice_contratto',
])
->map(function (StabileServizio $s): array {
return [
'id' => (int) $s->id,
'nome' => (string) ($s->nome ?? ''),
'attivo' => (bool) $s->attivo,
'voce_spesa_id' => $s->voce_spesa_id ? (int) $s->voce_spesa_id : null,
'contatore_matricola' => (string) ($s->contatore_matricola ?? ''),
'codice_utenza' => (string) ($s->codice_utenza ?? ''),
'codice_cliente' => (string) ($s->codice_cliente ?? ''),
'codice_contratto' => (string) ($s->codice_contratto ?? ''),
];
})
->all();
})
->schema([
Hidden::make('id'),
TextInput::make('nome')
->label('Nome utenza')
->maxLength(255)
->nullable(),
Toggle::make('attivo')
->label('Attiva')
->default(true),
TextInput::make('contatore_matricola')
->label('Matricola contatore')
->maxLength(64)
->nullable(),
TextInput::make('codice_utenza')
->label('Codice utenza')
->maxLength(64)
->nullable(),
TextInput::make('codice_cliente')
->label('Codice cliente')
->maxLength(64)
->nullable(),
TextInput::make('codice_contratto')
->label('Codice contratto')
->maxLength(64)
->nullable(),
Select::make('voce_spesa_id')
->label('Voce di spesa (solo acqua)')
->options(function (): array {
$user = Auth::user();
if (! $user instanceof User) {
return [];
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId || ! Schema::hasTable('voci_spesa')) {
return [];
}
return VoceSpesa::query()
->where('stabile_id', (int) $stabileId)
->where('categoria', 'acqua')
->orderBy('descrizione')
->limit(300)
->get(['id', 'codice', 'descrizione'])
->mapWithKeys(function (VoceSpesa $v) {
$label = trim((string) ($v->codice ?? ''));
if ($label !== '') {
$label .= ' — ';
}
$label .= (string) ($v->descrizione ?? ('Voce #' . $v->id));
return [(string) $v->id => $label];
})
->all();
})
->searchable()
->nullable(),
])
->addActionLabel('Aggiungi utenza acqua')
->reorderable(false)
->collapsed(false),
])
->action(function (array $data): void {
$user = Auth::user();
if (! $user instanceof User) {
Notification::make()->title('Utente non valido')->danger()->send();
return;
}
// Salva toggle feature (preferenza: colonna fornitori.fe_features, fallback: meta per-stabile)
$features = is_array($this->box['fornitore_features'] ?? null) ? $this->box['fornitore_features'] : [];
$features['acqua'] = array_merge(is_array($features['acqua'] ?? null) ? $features['acqua'] : [], [
'enabled' => (bool) ($data['acqua_enabled'] ?? false),
'scan_enabled' => (bool) ($data['acqua_scan_enabled'] ?? false),
'ticket_on_anomaly' => (bool) ($data['acqua_ticket_on_anomaly'] ?? true),
]);
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId) {
Notification::make()->title('Seleziona uno stabile attivo')->warning()->send();
return;
}
if (Schema::hasColumn('fornitori', 'fe_features')) {
$this->fornitore->fe_features = $features;
$this->fornitore->save();
} else {
if (! Schema::hasTable('fornitore_stabile_impostazioni')) {
Notification::make()
->title('Impossibile salvare feature')
->body('Manca la colonna fornitori.fe_features e non esiste la tabella fornitore_stabile_impostazioni (eseguire migrazioni).')
->danger()
->send();
return;
}
$settings = FornitoreStabileImpostazione::query()->firstOrNew([
'stabile_id' => (int) $stabileId,
'fornitore_id' => (int) $this->fornitore->id,
]);
$meta = is_array($settings->meta ?? null) ? $settings->meta : [];
$meta['fe_features'] = $features;
$settings->meta = $meta;
$settings->save();
}
if (! Schema::hasTable('stabile_servizi')) {
Notification::make()->title('Tabella servizi non presente')->warning()->send();
return;
}
$utenze = is_array($data['utenze_acqua'] ?? null) ? $data['utenze_acqua'] : [];
foreach ($utenze as $row) {
$rowId = isset($row['id']) && is_numeric($row['id']) ? (int) $row['id'] : null;
$nome = is_string($row['nome'] ?? null) ? trim((string) $row['nome']) : '';
$attivo = (bool) ($row['attivo'] ?? true);
$voceSpesaId = isset($row['voce_spesa_id']) && is_numeric($row['voce_spesa_id']) ? (int) $row['voce_spesa_id'] : null;
$matricola = is_string($row['contatore_matricola'] ?? null) ? trim((string) $row['contatore_matricola']) : '';
$utenzaCod = is_string($row['codice_utenza'] ?? null) ? trim((string) $row['codice_utenza']) : '';
$cliente = is_string($row['codice_cliente'] ?? null) ? trim((string) $row['codice_cliente']) : '';
$contratto = is_string($row['codice_contratto'] ?? null) ? trim((string) $row['codice_contratto']) : '';
$hasAny = ($nome !== '') || ($matricola !== '') || ($utenzaCod !== '') || ($cliente !== '') || ($contratto !== '') || ($voceSpesaId && $voceSpesaId > 0);
if (! $hasAny) {
continue;
}
$servizio = null;
if ($rowId) {
$servizio = StabileServizio::query()
->where('stabile_id', (int) $stabileId)
->where('tipo', 'acqua')
->find($rowId);
}
if (! $servizio) {
$autoName = 'Acqua';
if ($matricola !== '') {
$autoName .= ' - Contatore ' . $matricola;
} elseif ($utenzaCod !== '') {
$autoName .= ' - Utenza ' . $utenzaCod;
}
$servizio = new StabileServizio([
'stabile_id' => (int) $stabileId,
'tipo' => 'acqua',
'nome' => $nome !== '' ? $nome : $autoName,
'attivo' => true,
]);
}
$servizio->fornitore_id = (int) $this->fornitore->id;
$servizio->attivo = $attivo;
if ($nome !== '') {
$servizio->nome = $nome;
}
$servizio->voce_spesa_id = $voceSpesaId ?: null;
$servizio->contatore_matricola = $matricola !== '' ? $matricola : null;
$servizio->codice_utenza = $utenzaCod !== '' ? $utenzaCod : null;
$servizio->codice_cliente = $cliente !== '' ? $cliente : null;
$servizio->codice_contratto = $contratto !== '' ? $contratto : null;
$servizio->save();
}
$this->hydrateBoxData($user);
Notification::make()->title('Configurazione acqua salvata')->success()->send();
}),
Action::make('acqua_scan')
->label('Scansiona FE acqua')
->icon('heroicon-o-magnifying-glass')
->requiresConfirmation()
->form([
Toggle::make('solo_stabile_attivo')
->label('Solo stabile attivo')
->default(true),
Toggle::make('solo_non_estratte')
->label('Solo FE non ancora estratte (acqua)')
->default(true)
->helperText('Processa solo fatture dove non risulta già salvato consumo acqua.'),
Select::make('voce_spesa_id')
->label('Voce di spesa (opzionale)')
->options(function (): array {
$user = Auth::user();
if (! $user instanceof User) {
return [];
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId || ! Schema::hasTable('voci_spesa')) {
return [];
}
return VoceSpesa::query()
->where('stabile_id', (int) $stabileId)
->orderBy('descrizione')
->where('categoria', 'acqua')
->limit(300)
->get(['id', 'codice', 'descrizione'])
->mapWithKeys(function (VoceSpesa $v) {
$label = trim((string) ($v->codice ?? ''));
if ($label !== '') {
$label .= ' — ';
}
$label .= (string) ($v->descrizione ?? ('Voce #' . $v->id));
return [(string) $v->id => $label];
})
->all();
})
->searchable()
->nullable(),
Select::make('force_servizio_id')
->label('Forza servizio acqua (opzionale)')
->helperText('Se impostato, blocca laggancio se il PDF indica un contatore/utenza diversa (apre ticket).')
->options(function (): array {
$user = Auth::user();
if (! $user instanceof User) {
return [];
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId || ! Schema::hasTable('stabile_servizi')) {
return [];
}
return StabileServizio::query()
->where('stabile_id', (int) $stabileId)
->where('tipo', 'acqua')
->orderByDesc('attivo')
->orderBy('nome')
->limit(200)
->get(['id', 'nome', 'contatore_matricola', 'codice_utenza'])
->mapWithKeys(function (StabileServizio $s) {
$label = (string) ($s->nome ?: ('Servizio #' . $s->id));
$bits = [];
if (is_string($s->contatore_matricola) && $s->contatore_matricola !== '') {
$bits[] = 'Contatore ' . $s->contatore_matricola;
}
if (is_string($s->codice_utenza) && $s->codice_utenza !== '') {
$bits[] = 'Utenza ' . $s->codice_utenza;
}
if (count($bits) > 0) {
$label .= ' — ' . implode(' · ', $bits);
}
return [(string) $s->id => $label];
})
->all();
})
->searchable()
->nullable(),
TextInput::make('limit')
->label('Limite FE da processare')
->numeric()
->default(50)
->minValue(1)
->maxValue(500),
Toggle::make('crea_ticket')
->label('Crea ticket su anomalie')
->default(true),
])
->action(function (array $data): void {
$user = Auth::user();
if (! $user instanceof User) {
Notification::make()->title('Utente non valido')->danger()->send();
return;
}
$features = is_array($this->box['fornitore_features'] ?? null) ? $this->box['fornitore_features'] : [];
$acqua = is_array($features['acqua'] ?? null) ? $features['acqua'] : [];
if (! (bool) ($acqua['enabled'] ?? false)) {
Notification::make()->title('Acqua non attiva su questo fornitore')->warning()->send();
return;
}
$soloStabileAttivo = (bool) ($data['solo_stabile_attivo'] ?? true);
$soloNonEstratte = (bool) ($data['solo_non_estratte'] ?? true);
$activeStabileId = StabileContext::resolveActiveStabileId($user);
if ($soloStabileAttivo && ! $activeStabileId) {
Notification::make()->title('Seleziona uno stabile attivo')->warning()->send();
return;
}
$stabileIds = [];
if ($soloStabileAttivo) {
$stabileIds = [(int) $activeStabileId];
} else {
$stabileIds = Stabile::query()
->where('amministratore_id', (int) $this->fornitore->amministratore_id)
->pluck('id')
->map(fn($v) => (int) $v)
->all();
}
if (count($stabileIds) < 1) {
Notification::make()->title('Nessuno stabile trovato')->warning()->send();
return;
}
$limit = isset($data['limit']) && is_numeric($data['limit']) ? (int) $data['limit'] : 50;
$limit = max(1, min(500, $limit));
$voceSpesaId = isset($data['voce_spesa_id']) && is_numeric($data['voce_spesa_id']) ? (int) $data['voce_spesa_id'] : null;
$forceServizioId = isset($data['force_servizio_id']) && is_numeric($data['force_servizio_id']) ? (int) $data['force_servizio_id'] : null;
$creaTicket = (bool) ($data['crea_ticket'] ?? true);
$normalize = static function (?string $value): string {
$value = strtoupper(trim((string) $value));
$value = str_replace(' ', '', $value);
return $value;
};
$ids = [];
$piva = $normalize($this->fornitore->partita_iva);
$cf = $normalize($this->fornitore->codice_fiscale);
if ($piva !== '') {
$ids[] = $piva;
if (str_starts_with($piva, 'IT') && strlen($piva) > 2) {
$ids[] = substr($piva, 2);
}
}
if ($cf !== '') {
$ids[] = $cf;
}
$ids = array_values(array_unique(array_filter($ids, fn($v) => $v !== '')));
$feQuery = FatturaElettronica::query()
->whereIn('stabile_id', $stabileIds)
->where(function (\Illuminate\Database\Eloquent\Builder $q) use ($ids): void {
$q->where('fornitore_id', (int) $this->fornitore->id);
if (! empty($ids)) {
$q->orWhere(function (\Illuminate\Database\Eloquent\Builder $qq) use ($ids): void {
foreach ($ids as $id) {
$qq->orWhereRaw("REPLACE(UPPER(fornitore_piva), ' ', '') = ?", [$id])
->orWhereRaw("REPLACE(UPPER(fornitore_cf), ' ', '') = ?", [$id]);
}
});
}
});
if ($soloNonEstratte) {
$feQuery->where(function (\Illuminate\Database\Eloquent\Builder $q): void {
$q->whereNull('consumo_raw')
->orWhere('consumo_raw', 'not like', '%"type":"acqua"%');
});
}
$feQuery
->orderByDesc('data_fattura')
->orderByDesc('id')
->limit($limit);
$fatture = $feQuery->get(['id', 'stabile_id', 'fornitore_id', 'sdi_file', 'totale', 'data_fattura', 'allegato_pdf_path', 'allegato_pdf_hash', 'xml_content', 'consumo_raw']);
$processed = 0;
$ok = 0;
$skipped = 0;
$tickets = 0;
foreach ($fatture as $fe) {
$processed++;
// Ensure Documento (best-effort)
try {
if ((int) $user->id > 0) {
app(FatturaElettronicaProtocolloService::class)->ensureDocumentoProtocollato($fe, (int) $user->id);
$fe->refresh();
}
} catch (\Throwable) {
// ignore
}
$doc = Documento::query()
->where('documentable_type', FatturaElettronica::class)
->where('documentable_id', (int) $fe->id)
->first();
if (! $doc) {
$skipped++;
if ($creaTicket) {
try {
$ing = app(ConsumiAcquaIngestionService::class)->ingest(
$fe,
['error' => 'Documento non presente', 'consumi' => []],
$voceSpesaId,
(int) $user->id,
true,
$forceServizioId
);
$tickets += (int) ($ing['tickets'] ?? 0);
} catch (\Throwable) {
// ignore
}
}
continue;
}
$text = is_string($doc->contenuto_ocr) ? trim($doc->contenuto_ocr) : '';
if ($text === '') {
$res = app(PdfTextExtractionService::class)->extractAndStore($doc, false);
if (($res['status'] ?? null) !== 'ok') {
$skipped++;
if ($creaTicket) {
try {
$ing = app(ConsumiAcquaIngestionService::class)->ingest(
$fe,
['error' => (string) ($res['message'] ?? 'OCR fallita'), 'consumi' => []],
$voceSpesaId,
(int) $user->id,
true,
$forceServizioId
);
$tickets += (int) ($ing['tickets'] ?? 0);
} catch (\Throwable) {
// ignore
}
}
continue;
}
$doc->refresh();
$text = is_string($doc->contenuto_ocr) ? trim($doc->contenuto_ocr) : '';
}
if ($text === '') {
$skipped++;
if ($creaTicket) {
try {
$ing = app(ConsumiAcquaIngestionService::class)->ingest(
$fe,
['error' => 'Testo OCR vuoto', 'consumi' => []],
$voceSpesaId,
(int) $user->id,
true,
$forceServizioId
);
$tickets += (int) ($ing['tickets'] ?? 0);
} catch (\Throwable) {
// ignore
}
}
continue;
}
$parsed = app(AcquaPdfTextParser::class)->parse($text);
$codici = is_array($parsed['codici'] ?? null) ? $parsed['codici'] : [];
$contatore = is_array($parsed['contatore'] ?? null) ? $parsed['contatore'] : [];
$consumi = is_array($parsed['consumi'] ?? null) ? $parsed['consumi'] : [];
$generale = is_array($parsed['generale'] ?? null) ? $parsed['generale'] : [];
$finestra = is_array($parsed['finestra_autolettura'] ?? null) ? $parsed['finestra_autolettura'] : [];
$letture = is_array($parsed['riepilogo_letture'] ?? null) ? $parsed['riepilogo_letture'] : [];
$quadro = is_array($parsed['quadro_dettaglio'] ?? null) ? $parsed['quadro_dettaglio'] : [];
$tariffe = is_array($parsed['tariffe'] ?? null) ? $parsed['tariffe'] : [];
$ivaInfo = is_array($parsed['iva'] ?? null) ? $parsed['iva'] : [];
$hasTariffData = count(array_filter([
$generale['periodicita_fatturazione'] ?? null,
$tariffe['profilo'] ?? null,
$ivaInfo['codice'] ?? null,
$quadro['quota_fissa'] ?? null,
], static fn($v): bool => $v !== null && $v !== '')) > 0;
$hasAny = false;
foreach ([$codici['utenza'] ?? null, $codici['cliente'] ?? null, $codici['contratto'] ?? null, $contatore['matricola'] ?? null] as $v) {
if (is_string($v) && trim($v) !== '') {
$hasAny = true;
break;
}
}
if (! $hasAny && count($consumi) < 1 && ! $hasTariffData) {
$skipped++;
if ($creaTicket) {
try {
$ing = app(ConsumiAcquaIngestionService::class)->ingest($fe, $parsed, $voceSpesaId, (int) $user->id, true, $forceServizioId);
$tickets += (int) ($ing['tickets'] ?? 0);
} catch (\Throwable) {
// ignore
}
}
continue;
}
// Salva anche su FE (consumo_raw + campi generici)
$payload = [
'type' => 'acqua',
'source' => 'pdf_ocr',
'codici' => [
'utenza' => $codici['utenza'] ?? null,
'cliente' => $codici['cliente'] ?? null,
'contratto' => $codici['contratto'] ?? null,
],
'contatore' => [
'matricola' => $contatore['matricola'] ?? null,
],
'consumi' => array_values($consumi),
'generale' => $generale,
'finestra_autolettura' => $finestra,
'riepilogo_letture' => array_values($letture),
'quadro_dettaglio' => $quadro,
'tariffe' => $tariffe,
'iva' => [
'codice' => $ivaInfo['codice'] ?? null,
'aliquota_percentuale' => $ivaInfo['aliquota_percentuale'] ?? null,
'descrizione' => $ivaInfo['descrizione'] ?? null,
],
'parsed_at' => now()->toISOString(),
];
$totMc = null;
$ref = null;
if (count($consumi) > 0) {
$sum = 0.0;
$has = false;
foreach ($consumi as $c) {
if (isset($c['valore']) && is_numeric($c['valore'])) {
$sum += (float) $c['valore'];
$has = true;
}
}
if ($has) {
$totMc = $sum;
}
$first = $consumi[0] ?? [];
$dal = $first['dal'] ?? null;
$al = $first['al'] ?? null;
if (is_string($dal) && is_string($al) && $dal !== '' && $al !== '') {
$ref = $dal . ' - ' . $al;
}
}
$fe->consumo_unita = 'mc';
$fe->consumo_valore = $totMc;
$fe->consumo_riferimento = $ref;
$fe->consumo_raw = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$fe->save();
try {
app(ConsumiAcquaTariffeIngestionService::class)->ingest($fe, $parsed, null);
} catch (\Throwable) {
// best-effort
}
$ing = app(ConsumiAcquaIngestionService::class)->ingest($fe, $parsed, $voceSpesaId, (int) $user->id, $creaTicket, $forceServizioId);
if (($ing['status'] ?? null) === 'ok') {
try {
app(ConsumiAcquaTariffeIngestionService::class)->ingest(
$fe,
$parsed,
isset($ing['servizio_id']) && is_numeric($ing['servizio_id']) ? (int) $ing['servizio_id'] : null
);
} catch (\Throwable) {
// best-effort
}
}
if (($ing['status'] ?? null) === 'ok') {
$ok++;
} elseif (($ing['status'] ?? null) === 'no-data' && $hasTariffData) {
$ok++;
} elseif (($ing['status'] ?? null) === 'mismatch') {
$tickets += (int) ($ing['tickets'] ?? 1);
$skipped++;
} elseif (($ing['status'] ?? null) === 'no-data') {
$tickets += (int) ($ing['tickets'] ?? 0);
$skipped++;
} else {
$skipped++;
}
}
$body = 'Processate: ' . $processed . ' · OK: ' . $ok . ' · Skipped: ' . $skipped;
if ($tickets > 0) {
$body .= ' · Ticket: ' . $tickets;
}
Notification::make()->title('Scansione acqua completata')->body($body)->success()->send();
}),
Action::make('torna')
->label('Torna')
->icon('heroicon-o-arrow-left')
->url(fn() => url()->previous()),
];
}
}