4191 lines
185 KiB
PHP
Executable File
4191 lines
185 KiB
PHP
Executable File
<?php
|
|
namespace App\Filament\Pages\Condomini;
|
|
|
|
use App\Filament\Pages\Contabilita\FatturaElettronicaScheda;
|
|
use App\Filament\Pages\Contabilita\FattureElettronicheP7mRicevute;
|
|
use App\Filament\Pages\Supporto\TicketRiscaldamento;
|
|
use App\Filament\Pages\Supporto\TicketRiscaldamentoGenerale;
|
|
use App\Models\Documento;
|
|
use App\Models\FatturaElettronica;
|
|
use App\Models\Fornitore;
|
|
use App\Models\Stabile;
|
|
use App\Models\StabileServizio;
|
|
use App\Models\StabileServizioLettura;
|
|
use App\Models\StabileServizioTariffa;
|
|
use App\Models\UnitaImmobiliare;
|
|
use App\Models\User;
|
|
use App\Models\VoceSpesa;
|
|
use App\Services\Consumi\GasLucePdfTextParser;
|
|
use App\Services\Consumi\ConsumiRiscaldamentoIngestionService;
|
|
use App\Services\Consumi\ConsumiRiscaldamentoTariffeIngestionService;
|
|
use App\Services\Documenti\PdfTextExtractionService;
|
|
use App\Services\FattureElettroniche\FatturaElettronicaProtocolloService;
|
|
use App\Support\AnnoGestioneContext;
|
|
use App\Support\StabileContext;
|
|
use BackedEnum;
|
|
use Filament\Actions\Action;
|
|
use Filament\Forms\Components\Select;
|
|
use Filament\Forms\Components\Textarea;
|
|
use Filament\Forms\Components\TextInput;
|
|
use Filament\Forms\Components\Toggle;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Pages\Page;
|
|
use Filament\Tables\Columns\IconColumn;
|
|
use Filament\Tables\Columns\TextColumn;
|
|
use Filament\Tables\Concerns\InteractsWithTable;
|
|
use Filament\Tables\Contracts\HasTable;
|
|
use Filament\Tables\Filters\SelectFilter;
|
|
use Filament\Tables\Table;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Support\Facades\URL;
|
|
use Livewire\WithFileUploads;
|
|
use UnitEnum;
|
|
|
|
class RiscaldamentoStabileArchivio extends Page implements HasTable
|
|
{
|
|
use InteractsWithTable;
|
|
use WithFileUploads;
|
|
|
|
protected static ?string $navigationLabel = 'Riscaldamento / Caldaia';
|
|
|
|
protected static ?string $title = 'Riscaldamento / Caldaia';
|
|
|
|
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-rectangle-stack';
|
|
|
|
protected static UnitEnum|string|null $navigationGroup = 'Stabile';
|
|
|
|
protected static ?int $navigationSort = 34;
|
|
|
|
protected static ?string $slug = 'condomini/riscaldamento-utenze';
|
|
|
|
protected string $view = 'filament.pages.condomini.riscaldamento-stabile-archivio';
|
|
|
|
public string $riscaldamentoTab = 'dashboard';
|
|
|
|
public string $riscaldamentoBackfillGestione = 'ordinaria';
|
|
|
|
public bool $riscaldamentoBackfillOnlyMissing = true;
|
|
|
|
/** @var array<int, int> */
|
|
public array $riscaldamentoSelectedFeIds = [];
|
|
|
|
public ?int $riscaldamentoUiEditingId = null;
|
|
|
|
/** @var array<string,mixed> */
|
|
public array $riscaldamentoUiReadingForm = [];
|
|
|
|
public string $riscaldamentoCampagnaSort = 'scala_interno';
|
|
|
|
/** @var array<int, array<string, mixed>> */
|
|
public array $riscaldamentoCampagnaEditRows = [];
|
|
|
|
public ?string $riscaldamentoDataInizioFiltro = null;
|
|
public ?string $riscaldamentoDataFineFiltro = null;
|
|
|
|
public bool $showSendEmailModal = false;
|
|
public string $emailDestinatario = '';
|
|
public string $emailOggetto = '';
|
|
public string $emailCorpo = '';
|
|
|
|
public $riscaldamentoReadingsFile;
|
|
|
|
public static function canAccess(): bool
|
|
{
|
|
$user = Auth::user();
|
|
|
|
return $user instanceof User
|
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore'])
|
|
&& Schema::hasTable('stabile_servizi');
|
|
}
|
|
|
|
public function mount(): void
|
|
{
|
|
$this->riscaldamentoTab = $this->normalizeRiscaldamentoTab((string) request()->query('tab', 'dashboard'));
|
|
$this->resetRiscaldamentoUiReadingForm();
|
|
$this->mountInteractsWithTable();
|
|
$this->loadRiscaldamentoFilterDates();
|
|
$this->initializeEmailDefaults();
|
|
|
|
$stabileId = $this->resolveActiveStabileId();
|
|
if ($stabileId > 0) {
|
|
$this->ensureFornitoreStabileDefaultVoceSpesa($stabileId);
|
|
}
|
|
}
|
|
|
|
public function setRiscaldamentoTab(string $tab): void
|
|
{
|
|
$this->riscaldamentoTab = $this->normalizeRiscaldamentoTab($tab);
|
|
$this->loadRiscaldamentoFilterDates();
|
|
}
|
|
|
|
private function loadRiscaldamentoFilterDates(): void
|
|
{
|
|
$stabileId = $this->resolveActiveStabileId();
|
|
$year = $this->resolveActiveAnnoGestione();
|
|
$service = $this->resolveActiveRiscaldamentoServizio();
|
|
|
|
$this->riscaldamentoDataInizioFiltro = null;
|
|
$this->riscaldamentoDataFineFiltro = null;
|
|
|
|
if ($service instanceof StabileServizio) {
|
|
$periods = $service->meta['periodi_esercizio'] ?? [];
|
|
if (!empty($periods[$year]['dal'])) {
|
|
$this->riscaldamentoDataInizioFiltro = $periods[$year]['dal'];
|
|
}
|
|
if (!empty($periods[$year]['al'])) {
|
|
$this->riscaldamentoDataFineFiltro = $periods[$year]['al'];
|
|
}
|
|
}
|
|
}
|
|
|
|
private function initializeEmailDefaults(): void
|
|
{
|
|
$stabileId = $this->resolveActiveStabileId();
|
|
$stabile = $stabileId ? Stabile::query()->find($stabileId) : null;
|
|
|
|
$this->emailOggetto = 'Riepilogo Ripartizione Riscaldamento - Stabile ' . ($stabile?->denominazione ?? '');
|
|
$this->emailCorpo = "Gentile collaboratore,\nin allegato trovi il PDF del riepilogo contatori riscaldamento per lo stabile in oggetto.\n\nCordiali saluti,\nL'Amministrazione";
|
|
}
|
|
|
|
public function toggleRiscaldamentoFeSelection(int $fatturaId): void
|
|
{
|
|
$selected = $this->normalizeRiscaldamentoSelectedFeIds();
|
|
|
|
if (in_array($fatturaId, $selected, true)) {
|
|
$selected = array_values(array_filter($selected, static fn(int $value): bool => $value !== $fatturaId));
|
|
} else {
|
|
$selected[] = $fatturaId;
|
|
}
|
|
|
|
$this->riscaldamentoSelectedFeIds = array_values(array_unique($selected));
|
|
}
|
|
|
|
public function selectAllRiscaldamentoFe(): void
|
|
{
|
|
$this->riscaldamentoSelectedFeIds = $this->resolveRiscaldamentoCandidateInvoices()
|
|
->pluck('id')
|
|
->map(fn($value): int => (int) $value)
|
|
->filter(fn(int $value): bool => $value > 0)
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
public function clearRiscaldamentoFeSelection(): void
|
|
{
|
|
$this->riscaldamentoSelectedFeIds = [];
|
|
}
|
|
|
|
public function startCreateRiscaldamentoUiReading(): void
|
|
{
|
|
$this->resetRiscaldamentoUiReadingForm();
|
|
}
|
|
|
|
public function startEditRiscaldamentoUiReading(int $readingId): void
|
|
{
|
|
$stabileId = $this->resolveActiveStabileId();
|
|
if (! $stabileId) {
|
|
return;
|
|
}
|
|
|
|
$record = StabileServizioLettura::query()
|
|
->where('id', $readingId)
|
|
->where('stabile_id', $stabileId)
|
|
->whereNotNull('unita_immobiliare_id')
|
|
->whereHas('servizio', fn(Builder $q) => $q->where('tipo', 'riscaldamento'))
|
|
->first();
|
|
|
|
if (! $record instanceof StabileServizioLettura) {
|
|
Notification::make()->title('Lettura UI non trovata')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$this->riscaldamentoUiEditingId = (int) $record->id;
|
|
$this->riscaldamentoUiReadingForm = [
|
|
'stabile_servizio_id' => (int) $record->stabile_servizio_id,
|
|
'unita_immobiliare_id' => (int) ($record->unita_immobiliare_id ?? 0) ?: null,
|
|
'periodo_dal' => optional($record->periodo_dal)->toDateString(),
|
|
'periodo_al' => optional($record->periodo_al)->toDateString(),
|
|
'canale_acquisizione' => (string) ($record->canale_acquisizione ?? 'manuale'),
|
|
'riferimento_acquisizione' => (string) ($record->riferimento_acquisizione ?? ''),
|
|
'workflow_stato' => (string) ($record->workflow_stato ?? 'ricevuta'),
|
|
'rilevatore_tipo' => (string) ($record->rilevatore_tipo ?? 'amministrazione'),
|
|
'rilevatore_nome' => (string) ($record->rilevatore_nome ?? ''),
|
|
'lettura_precedente_valore' => $record->lettura_precedente_valore,
|
|
'lettura_inizio' => $record->lettura_inizio,
|
|
'lettura_fine' => $record->lettura_fine,
|
|
'consumo_valore' => $record->consumo_valore,
|
|
'consumo_unita' => (string) ($record->consumo_unita ?: 'mc'),
|
|
'deadline_lettura_at' => optional($record->deadline_lettura_at)->toDateString(),
|
|
'note' => trim((string) data_get($record->raw, 'inline_ui_editor.note', '')),
|
|
];
|
|
}
|
|
|
|
public function cancelRiscaldamentoUiEditing(): void
|
|
{
|
|
$this->resetRiscaldamentoUiReadingForm();
|
|
}
|
|
|
|
public function saveRiscaldamentoUiReading(): void
|
|
{
|
|
$stabileId = $this->resolveActiveStabileId();
|
|
$user = Auth::user();
|
|
if (! $stabileId || ! $user instanceof User) {
|
|
return;
|
|
}
|
|
|
|
$validated = $this->validate([
|
|
'riscaldamentoUiReadingForm.stabile_servizio_id' => ['required', 'integer'],
|
|
'riscaldamentoUiReadingForm.unita_immobiliare_id' => ['required', 'integer'],
|
|
'riscaldamentoUiReadingForm.periodo_dal' => ['nullable', 'date'],
|
|
'riscaldamentoUiReadingForm.periodo_al' => ['nullable', 'date', 'after_or_equal:riscaldamentoUiReadingForm.periodo_dal'],
|
|
'riscaldamentoUiReadingForm.canale_acquisizione' => ['nullable', 'string', 'max:50'],
|
|
'riscaldamentoUiReadingForm.riferimento_acquisizione' => ['nullable', 'string', 'max:191'],
|
|
'riscaldamentoUiReadingForm.workflow_stato' => ['nullable', 'string', 'max:50'],
|
|
'riscaldamentoUiReadingForm.rilevatore_tipo' => ['nullable', 'string', 'max:80'],
|
|
'riscaldamentoUiReadingForm.rilevatore_nome' => ['nullable', 'string', 'max:191'],
|
|
'riscaldamentoUiReadingForm.lettura_precedente_valore' => ['nullable', 'numeric', 'min:0'],
|
|
'riscaldamentoUiReadingForm.lettura_inizio' => ['nullable', 'numeric', 'min:0'],
|
|
'riscaldamentoUiReadingForm.lettura_fine' => ['nullable', 'numeric', 'min:0'],
|
|
'riscaldamentoUiReadingForm.consumo_valore' => ['nullable', 'numeric'],
|
|
'riscaldamentoUiReadingForm.consumo_unita' => ['nullable', 'string', 'max:16'],
|
|
'riscaldamentoUiReadingForm.deadline_lettura_at' => ['nullable', 'date'],
|
|
'riscaldamentoUiReadingForm.note' => ['nullable', 'string', 'max:2000'],
|
|
]);
|
|
|
|
$data = $validated['riscaldamentoUiReadingForm'] ?? [];
|
|
|
|
$servizio = StabileServizio::query()
|
|
->where('id', (int) ($data['stabile_servizio_id'] ?? 0))
|
|
->where('stabile_id', $stabileId)
|
|
->where('tipo', 'riscaldamento')
|
|
->first();
|
|
|
|
$unita = UnitaImmobiliare::query()
|
|
->where('id', (int) ($data['unita_immobiliare_id'] ?? 0))
|
|
->where('stabile_id', $stabileId)
|
|
->first();
|
|
|
|
if (! $servizio instanceof StabileServizio || ! $unita instanceof UnitaImmobiliare) {
|
|
Notification::make()->title('Servizio o unità non validi')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$record = $this->riscaldamentoUiEditingId
|
|
? StabileServizioLettura::query()
|
|
->where('id', (int) $this->riscaldamentoUiEditingId)
|
|
->where('stabile_id', $stabileId)
|
|
->whereNotNull('unita_immobiliare_id')
|
|
->whereHas('servizio', fn(Builder $q) => $q->where('tipo', 'riscaldamento'))
|
|
->first()
|
|
: null;
|
|
|
|
if ($this->riscaldamentoUiEditingId && ! $record instanceof StabileServizioLettura) {
|
|
Notification::make()->title('Lettura UI non trovata')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$previous = StabileServizioLettura::query()
|
|
->where('stabile_servizio_id', (int) $servizio->id)
|
|
->where('unita_immobiliare_id', (int) $unita->id)
|
|
->when($record instanceof StabileServizioLettura, fn(Builder $q) => $q->where('id', '!=', (int) $record->id))
|
|
->whereNotNull('lettura_fine')
|
|
->orderByDesc('periodo_al')
|
|
->orderByDesc('id')
|
|
->first();
|
|
|
|
$letturaPrecedente = is_numeric($data['lettura_precedente_valore'] ?? null)
|
|
? round((float) $data['lettura_precedente_valore'], 3)
|
|
: ($previous?->lettura_fine !== null ? (float) $previous->lettura_fine : null);
|
|
$letturaInizio = is_numeric($data['lettura_inizio'] ?? null)
|
|
? round((float) $data['lettura_inizio'], 3)
|
|
: $letturaPrecedente;
|
|
$letturaFine = is_numeric($data['lettura_fine'] ?? null)
|
|
? round((float) $data['lettura_fine'], 3)
|
|
: null;
|
|
$consumo = is_numeric($data['consumo_valore'] ?? null)
|
|
? round((float) $data['consumo_valore'], 3)
|
|
: (($letturaFine !== null && $letturaInizio !== null) ? round($letturaFine - $letturaInizio, 3) : null);
|
|
|
|
if ($consumo !== null && $consumo < 0) {
|
|
$this->addError('riscaldamentoUiReadingForm.lettura_fine', 'La lettura finale non può essere inferiore alla precedente.');
|
|
return;
|
|
}
|
|
|
|
$raw = is_array($record?->raw ?? null) ? $record->raw : [];
|
|
$raw['inline_ui_editor'] = [
|
|
'updated_by' => (int) $user->id,
|
|
'updated_at' => now()->toIso8601String(),
|
|
'note' => trim((string) ($data['note'] ?? '')),
|
|
];
|
|
|
|
$payload = [
|
|
'stabile_id' => $stabileId,
|
|
'stabile_servizio_id' => (int) $servizio->id,
|
|
'unita_immobiliare_id' => (int) $unita->id,
|
|
'fornitore_id' => $servizio->fornitore_id,
|
|
'periodo_dal' => $data['periodo_dal'] ?? null,
|
|
'periodo_al' => $data['periodo_al'] ?? null,
|
|
'tipologia_lettura' => 'manuale_ui',
|
|
'canale_acquisizione' => trim((string) ($data['canale_acquisizione'] ?? 'manuale')) ?: 'manuale',
|
|
'riferimento_acquisizione' => trim((string) ($data['riferimento_acquisizione'] ?? '')) ?: null,
|
|
'workflow_stato' => trim((string) ($data['workflow_stato'] ?? 'ricevuta')) ?: 'ricevuta',
|
|
'deadline_lettura_at' => $data['deadline_lettura_at'] ?? null,
|
|
'rilevatore_tipo' => trim((string) ($data['rilevatore_tipo'] ?? 'amministrazione')) ?: 'amministrazione',
|
|
'rilevatore_nome' => trim((string) ($data['rilevatore_nome'] ?? '')) ?: null,
|
|
'lettura_precedente_valore' => $letturaPrecedente,
|
|
'lettura_inizio' => $letturaInizio,
|
|
'lettura_fine' => $letturaFine,
|
|
'consumo_valore' => $consumo,
|
|
'consumo_unita' => trim((string) ($data['consumo_unita'] ?? 'mc')) ?: 'mc',
|
|
'raw' => $raw,
|
|
];
|
|
|
|
if ($record instanceof StabileServizioLettura) {
|
|
$record->fill($payload);
|
|
$record->save();
|
|
} else {
|
|
$payload['created_by'] = (int) $user->id;
|
|
StabileServizioLettura::query()->create($payload);
|
|
}
|
|
|
|
Notification::make()
|
|
->title($record instanceof StabileServizioLettura ? 'Lettura UI aggiornata' : 'Lettura UI creata')
|
|
->success()
|
|
->send();
|
|
|
|
$this->resetRiscaldamentoUiReadingForm();
|
|
}
|
|
|
|
public function deleteRiscaldamentoUiReading(int $readingId): void
|
|
{
|
|
$stabileId = $this->resolveActiveStabileId();
|
|
if (! $stabileId) {
|
|
return;
|
|
}
|
|
|
|
$record = StabileServizioLettura::query()
|
|
->where('id', $readingId)
|
|
->where('stabile_id', $stabileId)
|
|
->whereNotNull('unita_immobiliare_id')
|
|
->whereHas('servizio', fn(Builder $q) => $q->where('tipo', 'riscaldamento'))
|
|
->first();
|
|
|
|
if (! $record instanceof StabileServizioLettura) {
|
|
Notification::make()->title('Lettura UI non trovata')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$record->delete();
|
|
if ((int) $this->riscaldamentoUiEditingId === (int) $readingId) {
|
|
$this->resetRiscaldamentoUiReadingForm();
|
|
}
|
|
|
|
Notification::make()->title('Lettura UI eliminata')->success()->send();
|
|
}
|
|
|
|
public function saveRiscaldamentoFilterDates(): void
|
|
{
|
|
$stabileId = $this->resolveActiveStabileId();
|
|
$year = $this->resolveActiveAnnoGestione();
|
|
$service = $this->resolveActiveRiscaldamentoServizio();
|
|
|
|
if (! $stabileId || ! $service instanceof StabileServizio) {
|
|
Notification::make()->title('Configura prima un servizio riscaldamento attivo.')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$meta = $service->meta ?? [];
|
|
$periods = $meta['periodi_esercizio'] ?? [];
|
|
|
|
$dal = trim((string) $this->riscaldamentoDataInizioFiltro) ?: null;
|
|
$al = trim((string) $this->riscaldamentoDataFineFiltro) ?: null;
|
|
|
|
if ($dal || $al) {
|
|
$periods[$year] = [
|
|
'dal' => $dal,
|
|
'al' => $al
|
|
];
|
|
|
|
// PROPAGAZIONE AUTOMATICA ANNI PRECEDENTI E SUCCESSIVI
|
|
$dalCarbon = \Illuminate\Support\Carbon::parse($dal);
|
|
$alCarbon = \Illuminate\Support\Carbon::parse($al);
|
|
|
|
$otherYears = \App\Models\GestioneContabile::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('tipo_gestione', 'riscaldamento')
|
|
->pluck('anno_gestione')
|
|
->toArray();
|
|
|
|
if (empty($otherYears)) {
|
|
$otherYears = \App\Models\GestioneContabile::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('tipo_gestione', 'ordinaria')
|
|
->pluck('anno_gestione')
|
|
->toArray();
|
|
}
|
|
|
|
foreach ($otherYears as $oy) {
|
|
if ($oy != $year && (! isset($periods[$oy]) || empty($periods[$oy]['dal']) || empty($periods[$oy]['al']))) {
|
|
$diff = $oy - $year;
|
|
$periods[$oy] = [
|
|
'dal' => $dalCarbon->copy()->addYears($diff)->format('Y-m-d'),
|
|
'al' => $alCarbon->copy()->addYears($diff)->format('Y-m-d'),
|
|
];
|
|
}
|
|
}
|
|
} else {
|
|
unset($periods[$year]);
|
|
}
|
|
|
|
$meta['periodi_esercizio'] = $periods;
|
|
$service->meta = $meta;
|
|
$service->save();
|
|
|
|
// Spostamento automatico delle fatture (Aggiornamento gestione_id)
|
|
if ($dal && $al) {
|
|
$gestione = \App\Models\GestioneContabile::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('anno_gestione', $year)
|
|
->where('tipo_gestione', 'ordinaria')
|
|
->first();
|
|
|
|
if ($gestione) {
|
|
$fornitoreIds = StabileServizio::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('tipo', 'riscaldamento')
|
|
->whereNotNull('fornitore_id')
|
|
->pluck('fornitore_id')
|
|
->map(fn($value): int => (int) $value)
|
|
->filter(fn(int $value): bool => $value > 0)
|
|
->values()
|
|
->all();
|
|
|
|
if ($fornitoreIds === []) {
|
|
$preferredSupplier = $this->resolvePreferredRiscaldamentoSupplier($stabileId);
|
|
if ($preferredSupplier instanceof Fornitore) {
|
|
$fornitoreIds = [(int) $preferredSupplier->id];
|
|
}
|
|
}
|
|
|
|
$fornitoreIds = $this->resolveEquivalentFornitoreIds($fornitoreIds);
|
|
|
|
// Aggiorna le fatture in contabilita_fatture_fornitori
|
|
if (Schema::hasTable('contabilita_fatture_fornitori')) {
|
|
$q = DB::table('contabilita_fatture_fornitori')
|
|
->where('stabile_id', $stabileId)
|
|
->whereBetween('data_documento', [$dal, $al]);
|
|
|
|
if ($fornitoreIds !== []) {
|
|
$q->whereIn('fornitore_id', $fornitoreIds);
|
|
}
|
|
|
|
$q->update(['gestione_id' => $gestione->id]);
|
|
|
|
// Inoltre, aggiorna le FE associate
|
|
$feIds = DB::table('fatture_elettroniche')
|
|
->where('stabile_id', $stabileId)
|
|
->whereBetween('data_fattura', [$dal, $al])
|
|
->when($fornitoreIds !== [], fn($b) => $b->whereIn('fornitore_id', $fornitoreIds))
|
|
->pluck('id')
|
|
->all();
|
|
|
|
if (!empty($feIds)) {
|
|
DB::table('contabilita_fatture_fornitori')
|
|
->where('stabile_id', $stabileId)
|
|
->whereIn('fattura_elettronica_id', $feIds)
|
|
->update(['gestione_id' => $gestione->id]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Notification::make()->title('Filtro date salvato e fatture allineate con successo.')->success()->send();
|
|
|
|
// Refresh properties
|
|
$this->loadRiscaldamentoFilterDates();
|
|
}
|
|
|
|
public function sendRipartizioneEmail(): void
|
|
{
|
|
$email = trim($this->emailDestinatario);
|
|
if ($email === '') {
|
|
Notification::make()->title('Destinatario mancante')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$stabileId = $this->resolveActiveStabileId();
|
|
$year = $this->resolveActiveAnnoGestione();
|
|
|
|
try {
|
|
$request = new \Illuminate\Http\Request([
|
|
'stabile_id' => $stabileId,
|
|
'year' => $year,
|
|
]);
|
|
$selectedIds = $this->normalizeRiscaldamentoSelectedFeIds();
|
|
if ($selectedIds !== []) {
|
|
$request->merge(['ids' => implode(',', $selectedIds)]);
|
|
}
|
|
|
|
// Imposta l'utente loggato sulla request
|
|
$request->setUserResolver(fn() => Auth::user());
|
|
|
|
$printController = new \App\Http\Controllers\Admin\RiscaldamentoRipartizionePrintController();
|
|
$reportResponse = $printController->pdf($request);
|
|
$pdfContent = $reportResponse->getContent();
|
|
|
|
Mail::send([], [], function ($message) use ($email, $pdfContent, $year): void {
|
|
$message->to($email)
|
|
->subject($this->emailOggetto)
|
|
->html(nl2br(e($this->emailCorpo)))
|
|
->attachData($pdfContent, 'riepilogo-riscaldamento-' . $year . '.pdf', [
|
|
'mime' => 'application/pdf',
|
|
]);
|
|
});
|
|
|
|
$this->showSendEmailModal = false;
|
|
Notification::make()->title('Email inviata con successo.')->success()->send();
|
|
} catch (\Throwable $e) {
|
|
Notification::make()->title('Errore durante l invio dell email')->danger()->body($e->getMessage())->send();
|
|
}
|
|
}
|
|
|
|
public function importElectronicReadings(): void
|
|
{
|
|
if (! $this->riscaldamentoReadingsFile) {
|
|
Notification::make()->title('Carica prima un file CSV.')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$stabileId = $this->resolveActiveStabileId();
|
|
$servizio = $this->resolveActiveRiscaldamentoServizio();
|
|
if (! $stabileId || ! $servizio) {
|
|
Notification::make()->title('Nessun servizio riscaldamento configurato per lo stabile attivo.')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$user = Auth::user();
|
|
$filePath = $this->riscaldamentoReadingsFile->getRealPath();
|
|
|
|
$handle = fopen($filePath, 'r');
|
|
if (! $handle) {
|
|
Notification::make()->title('Impossibile aprire il file caricato.')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$headers = fgetcsv($handle, 0, ',');
|
|
if (! $headers) {
|
|
fclose($handle);
|
|
Notification::make()->title('File CSV vuoto o non valido.')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
// Trova gli indici dei campi
|
|
$idIdx = -1;
|
|
$internoIdx = -1;
|
|
$letturaIdx = -1;
|
|
|
|
foreach ($headers as $idx => $header) {
|
|
$headerClean = strtolower(trim((string) $header));
|
|
if ($headerClean === '#id' || $headerClean === 'id' || $idx === 0) {
|
|
if ($idIdx === -1) $idIdx = $idx;
|
|
}
|
|
if ($headerClean === 'interno' || $headerClean === 'int') {
|
|
$internoIdx = $idx;
|
|
}
|
|
if ($headerClean === 'lettura' || $headerClean === 'valore') {
|
|
$letturaIdx = $idx;
|
|
}
|
|
}
|
|
|
|
// Fallbacks se non trova per nome
|
|
if ($idIdx === -1) $idIdx = 0;
|
|
if ($internoIdx === -1) $internoIdx = 6; // Default standard del tracciato
|
|
if ($letturaIdx === -1) $letturaIdx = 7; // Default standard del tracciato
|
|
|
|
$imported = 0;
|
|
$linked = 0;
|
|
$errors = 0;
|
|
$year = $this->resolveActiveAnnoGestione();
|
|
|
|
while (($row = fgetcsv($handle, 0, ',')) !== false) {
|
|
if (count($row) <= max($idIdx, $internoIdx, $letturaIdx)) {
|
|
continue;
|
|
}
|
|
|
|
$deviceId = trim((string) ($row[$idIdx] ?? ''));
|
|
$internoCsv = trim((string) ($row[$internoIdx] ?? ''));
|
|
$letturaStr = trim((string) ($row[$letturaIdx] ?? ''));
|
|
|
|
if ($deviceId === '' && $internoCsv === '') {
|
|
continue;
|
|
}
|
|
|
|
// Pulisci il valore della lettura
|
|
$letturaClean = preg_replace('/[^\d,\.]/', '', $letturaStr);
|
|
$letturaClean = str_replace(',', '.', $letturaClean);
|
|
if (! is_numeric($letturaClean)) {
|
|
$errors++;
|
|
continue;
|
|
}
|
|
$finalValue = round((float) $letturaClean, 3);
|
|
|
|
// Cerca l'unità immobiliare
|
|
$unit = null;
|
|
if ($deviceId !== '') {
|
|
$unit = UnitaImmobiliare::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('riscaldamento_gateway_device_id', $deviceId)
|
|
->whereNull('deleted_at')
|
|
->first();
|
|
}
|
|
|
|
if (! $unit && $internoCsv !== '') {
|
|
$unit = UnitaImmobiliare::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where(function($query) use ($internoCsv) {
|
|
$query->where('interno', $internoCsv)
|
|
->orWhere('codice_unita', $internoCsv);
|
|
})
|
|
->whereNull('deleted_at')
|
|
->first();
|
|
|
|
if ($unit && $deviceId !== '') {
|
|
$unit->riscaldamento_gateway_device_id = $deviceId;
|
|
$unit->save();
|
|
$linked++;
|
|
}
|
|
}
|
|
|
|
if (! $unit) {
|
|
$errors++;
|
|
continue;
|
|
}
|
|
|
|
// Trova la lettura precedente
|
|
$previous = StabileServizioLettura::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('stabile_servizio_id', (int) $servizio->id)
|
|
->where('unita_immobiliare_id', $unit->id)
|
|
->whereNotNull('lettura_fine')
|
|
->orderByDesc('periodo_al')
|
|
->orderByDesc('id')
|
|
->first();
|
|
|
|
$previousValue = $previous?->lettura_fine !== null ? (float) $previous->lettura_fine : null;
|
|
$consumo = ($previousValue !== null && $finalValue >= $previousValue) ? round($finalValue - $previousValue, 3) : null;
|
|
|
|
// Cerca se esiste già una lettura per quest'anno
|
|
$current = StabileServizioLettura::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('stabile_servizio_id', (int) $servizio->id)
|
|
->where('unita_immobiliare_id', $unit->id)
|
|
->whereYear('created_at', $year)
|
|
->first();
|
|
|
|
$payload = [
|
|
'stabile_id' => $stabileId,
|
|
'stabile_servizio_id' => (int) $servizio->id,
|
|
'unita_immobiliare_id' => $unit->id,
|
|
'fornitore_id' => $servizio->fornitore_id,
|
|
'periodo_dal' => $previous?->periodo_al,
|
|
'periodo_al' => now()->toDateString(),
|
|
'tipologia_lettura' => 'elettronica_remota',
|
|
'canale_acquisizione' => 'dispositivo_remoto',
|
|
'riferimento_acquisizione' => 'Gateway ID ' . $deviceId,
|
|
'workflow_stato' => 'ricevuta',
|
|
'rilevatore_tipo' => 'sistema_remoto',
|
|
'rilevatore_nome' => 'Gateway Elettronico',
|
|
'lettura_precedente_valore' => $previousValue,
|
|
'lettura_inizio' => $previousValue,
|
|
'lettura_fine' => $finalValue,
|
|
'consumo_valore' => $consumo,
|
|
'consumo_unita' => 'mc',
|
|
'raw' => [
|
|
'csv_import' => [
|
|
'device_id' => $deviceId,
|
|
'interno' => $internoCsv,
|
|
'original' => $row,
|
|
'imported_at' => now()->toIso8601String(),
|
|
'imported_by' => $user?->id,
|
|
]
|
|
]
|
|
];
|
|
|
|
if ($current) {
|
|
$current->fill($payload);
|
|
$current->save();
|
|
} else {
|
|
$payload['created_by'] = $user?->id;
|
|
StabileServizioLettura::query()->create($payload);
|
|
}
|
|
|
|
$imported++;
|
|
}
|
|
|
|
fclose($handle);
|
|
$this->riscaldamentoReadingsFile = null;
|
|
|
|
$msg = "Caricate con successo $imported letture.";
|
|
if ($linked > 0) {
|
|
$msg .= " Associate $linked nuove unità via interno.";
|
|
}
|
|
if ($errors > 0) {
|
|
$msg .= " Saltate/non abbinate $errors righe.";
|
|
}
|
|
|
|
Notification::make()->title('Importazione completata')->body($msg)->success()->send();
|
|
|
|
$this->dispatch('refresh-riscaldamento-fe-selections');
|
|
}
|
|
|
|
private function normalizeRiscaldamentoTab(string $tab): string
|
|
{
|
|
$allowed = ['dashboard', 'fatture', 'letture', 'generale', 'tariffe', 'servizi'];
|
|
|
|
return in_array($tab, $allowed, true) ? $tab : 'dashboard';
|
|
}
|
|
|
|
private function resetRiscaldamentoUiReadingForm(): void
|
|
{
|
|
$defaultServiceId = collect($this->riscaldamentoUiServiceOptions)->keys()->map(fn($value) => (int) $value)->first();
|
|
|
|
$this->riscaldamentoUiEditingId = null;
|
|
$this->riscaldamentoUiReadingForm = [
|
|
'stabile_servizio_id' => $defaultServiceId,
|
|
'unita_immobiliare_id' => null,
|
|
'periodo_dal' => null,
|
|
'periodo_al' => null,
|
|
'canale_acquisizione' => 'manuale',
|
|
'riferimento_acquisizione' => null,
|
|
'workflow_stato' => 'ricevuta',
|
|
'rilevatore_tipo' => 'amministrazione',
|
|
'rilevatore_nome' => null,
|
|
'lettura_precedente_valore' => null,
|
|
'lettura_inizio' => null,
|
|
'lettura_fine' => null,
|
|
'consumo_valore' => null,
|
|
'consumo_unita' => 'mc',
|
|
'deadline_lettura_at' => null,
|
|
'note' => null,
|
|
];
|
|
}
|
|
|
|
public function backfillRiscaldamentoFromFatturePregresse(): void
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
Notification::make()->title('Utente non autenticato.')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$stabileId = $this->resolveActiveStabileId();
|
|
$servizio = $this->resolveActiveRiscaldamentoServizio();
|
|
if (! $stabileId || ! $servizio instanceof StabileServizio) {
|
|
Notification::make()->title('Configura prima un servizio riscaldamento attivo per lo stabile selezionato.')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$year = $this->resolveActiveAnnoGestione();
|
|
$tipoGestione = in_array($this->riscaldamentoBackfillGestione, ['ordinaria', 'riscaldamento', 'straordinaria', 'tutte'], true)
|
|
? $this->riscaldamentoBackfillGestione
|
|
: 'ordinaria';
|
|
|
|
$fatture = $this->resolveRiscaldamentoCandidateInvoices($tipoGestione)
|
|
->when($this->normalizeRiscaldamentoSelectedFeIds() !== [], fn($collection) => $collection->whereIn('id', $this->normalizeRiscaldamentoSelectedFeIds()));
|
|
|
|
if ($fatture->isEmpty()) {
|
|
Notification::make()->title('Nessuna FE riscaldamento trovata per lo stabile/gestione selezionati.')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
if ($this->riscaldamentoBackfillOnlyMissing) {
|
|
$fatture = $fatture->filter(function (FatturaElettronica $fattura): bool {
|
|
$storedRaw = is_string($fattura->consumo_raw ?? null) ? trim((string) $fattura->consumo_raw) : '';
|
|
if ($storedRaw === '') {
|
|
return true;
|
|
}
|
|
|
|
$decoded = json_decode($storedRaw, true);
|
|
|
|
return ! $this->payloadLooksLikeRiscaldamento(is_array($decoded) ? $decoded : null);
|
|
})->values();
|
|
}
|
|
|
|
if ($fatture->isEmpty()) {
|
|
Notification::make()->title('Nessuna FE riscaldamento da rielaborare con i filtri correnti.')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$processed = 0;
|
|
$ok = 0;
|
|
$skipped = 0;
|
|
$tickets = 0;
|
|
$tariffe = 0;
|
|
|
|
foreach ($fatture as $fattura) {
|
|
$processed++;
|
|
|
|
$parsed = $this->resolveRiscaldamentoParsedPayload($fattura, (int) $user->id);
|
|
if (! is_array($parsed)) {
|
|
$skipped++;
|
|
continue;
|
|
}
|
|
|
|
$consumi = array_values(is_array($parsed['consumi'] ?? null) ? $parsed['consumi'] : []);
|
|
$fattura->consumo_unita = 'mc';
|
|
$fattura->consumo_valore = $this->sumRiscaldamentoConsumi($consumi);
|
|
$fattura->consumo_riferimento = $this->buildRiscaldamentoConsumoReference($consumi);
|
|
$fattura->consumo_raw = json_encode([
|
|
'type' => 'riscaldamento',
|
|
'source' => 'pdf_ocr',
|
|
'codici' => is_array($parsed['codici'] ?? null) ? $parsed['codici'] : [],
|
|
'contatore' => is_array($parsed['contatore'] ?? null) ? $parsed['contatore'] : [],
|
|
'consumi' => $consumi,
|
|
'generale' => is_array($parsed['generale'] ?? null) ? $parsed['generale'] : [],
|
|
'pagamento' => is_array($parsed['pagamento'] ?? null) ? $parsed['pagamento'] : [],
|
|
'finestra_autolettura' => is_array($parsed['finestra_autolettura'] ?? null) ? $parsed['finestra_autolettura'] : [],
|
|
'riepilogo_letture' => array_values(is_array($parsed['riepilogo_letture'] ?? null) ? $parsed['riepilogo_letture'] : []),
|
|
'quadro_dettaglio' => is_array($parsed['quadro_dettaglio'] ?? null) ? $parsed['quadro_dettaglio'] : [],
|
|
'tariffe' => is_array($parsed['tariffe'] ?? null) ? $parsed['tariffe'] : [],
|
|
'iva' => is_array($parsed['iva'] ?? null) ? $parsed['iva'] : [],
|
|
'parsed_at' => now()->toISOString(),
|
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
$fattura->save();
|
|
|
|
try {
|
|
app(ConsumiRiscaldamentoTariffeIngestionService::class)->ingest($fattura, $parsed, (int) $servizio->id);
|
|
$tariffe++;
|
|
} catch (\Throwable) {
|
|
// best-effort
|
|
}
|
|
|
|
$ingestion = app(ConsumiRiscaldamentoIngestionService::class)->ingest(
|
|
$fattura,
|
|
$parsed,
|
|
(int) ($servizio->voce_spesa_id ?? 0) ?: null,
|
|
(int) $user->id,
|
|
true,
|
|
(int) $servizio->id,
|
|
);
|
|
|
|
if (($ingestion['status'] ?? null) === 'ok') {
|
|
$ok++;
|
|
} else {
|
|
$skipped++;
|
|
}
|
|
|
|
$tickets += (int) ($ingestion['tickets'] ?? 0);
|
|
}
|
|
|
|
Notification::make()
|
|
->title('Backfill FE riscaldamento completato')
|
|
->body("Processate: {$processed} · Agganciate: {$ok} · Saltate: {$skipped} · Ticket: {$tickets} · Tariffe aggiornate: {$tariffe}")
|
|
->success()
|
|
->send();
|
|
}
|
|
|
|
public function prepareRiscaldamentoReadingCampaign(): void
|
|
{
|
|
$stabileId = $this->resolveActiveStabileId();
|
|
$servizio = $this->resolveActiveRiscaldamentoServizio();
|
|
|
|
if (! $stabileId || ! $servizio) {
|
|
Notification::make()->title('Servizio riscaldamento attivo non disponibile per lo stabile selezionato.')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$units = UnitaImmobiliare::query()
|
|
->where('stabile_id', $stabileId)
|
|
->whereNull('deleted_at')
|
|
->orderBy('scala')
|
|
->orderBy('interno')
|
|
->orderBy('id')
|
|
->get(['id', 'codice_unita', 'scala', 'interno']);
|
|
|
|
if ($units->isEmpty()) {
|
|
Notification::make()->title('Nessuna unità immobiliare disponibile per avviare la campagna letture.')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$year = $this->resolveActiveAnnoGestione();
|
|
$created = 0;
|
|
$updated = 0;
|
|
|
|
foreach ($units as $unit) {
|
|
$completed = StabileServizioLettura::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('stabile_servizio_id', (int) $servizio->id)
|
|
->where('unita_immobiliare_id', (int) $unit->id)
|
|
->whereYear('created_at', $year)
|
|
->whereNotNull('lettura_fine')
|
|
->exists();
|
|
|
|
if ($completed) {
|
|
continue;
|
|
}
|
|
|
|
$previous = StabileServizioLettura::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('stabile_servizio_id', (int) $servizio->id)
|
|
->where('unita_immobiliare_id', (int) $unit->id)
|
|
->whereNotNull('lettura_fine')
|
|
->orderByDesc('periodo_al')
|
|
->orderByDesc('id')
|
|
->first();
|
|
|
|
$requestRow = StabileServizioLettura::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('stabile_servizio_id', (int) $servizio->id)
|
|
->where('unita_immobiliare_id', (int) $unit->id)
|
|
->whereYear('created_at', $year)
|
|
->whereNull('lettura_fine')
|
|
->orderByDesc('id')
|
|
->first();
|
|
|
|
$requestReference = 'PUBREQ:' . (int) $servizio->id . ':' . (int) $unit->id . ':' . $year;
|
|
$requestPayload = [
|
|
'stabile_id' => $stabileId,
|
|
'stabile_servizio_id' => (int) $servizio->id,
|
|
'unita_immobiliare_id' => (int) $unit->id,
|
|
'fornitore_id' => $servizio->fornitore_id,
|
|
'periodo_dal' => $previous?->periodo_al,
|
|
'periodo_al' => null,
|
|
'tipologia_lettura' => 'richiesta_autolettura',
|
|
'canale_acquisizione' => 'portale_pubblico',
|
|
'riferimento_acquisizione' => $requestReference,
|
|
'workflow_stato' => 'richiesta_inviata',
|
|
'richiesta_lettura_inviata_at' => now(),
|
|
'deadline_lettura_at' => now()->addDays(7),
|
|
'lettura_precedente_valore' => $previous?->lettura_fine,
|
|
'lettura_inizio' => $previous?->lettura_fine,
|
|
'raw' => [
|
|
'source' => 'campagna_letture_riscaldamento',
|
|
'year' => $year,
|
|
'unit_label' => trim((string) ($unit->codice_unita ?? '') . ' ' . (string) ($unit->interno ?? '')),
|
|
],
|
|
];
|
|
|
|
if ($requestRow) {
|
|
$requestRow->fill($requestPayload);
|
|
$requestRow->save();
|
|
$updated++;
|
|
continue;
|
|
}
|
|
|
|
StabileServizioLettura::query()->create($requestPayload);
|
|
$created++;
|
|
}
|
|
|
|
Notification::make()
|
|
->title('Campagna letture riscaldamento preparata')
|
|
->body("Richieste create: {$created} · richieste aggiornate: {$updated}")
|
|
->success()
|
|
->send();
|
|
}
|
|
|
|
public function markRiscaldamentoReadingReminder(int $readingId): void
|
|
{
|
|
$reading = StabileServizioLettura::query()->find($readingId);
|
|
if (! $reading) {
|
|
return;
|
|
}
|
|
|
|
$email = DB::table('unita_recapiti_servizio')
|
|
->where('unita_id', (int) ($reading->unita_immobiliare_id ?? 0))
|
|
->where('attivo', true)
|
|
->whereNotNull('email')
|
|
->where('email', '!=', '')
|
|
->orderByRaw("CASE WHEN tipo_servizio = 'riscaldamento' THEN 0 ELSE 1 END")
|
|
->orderByDesc('is_default')
|
|
->orderBy('ordine')
|
|
->value('email');
|
|
|
|
if (is_string($email) && trim($email) !== '') {
|
|
$email = trim($email);
|
|
$publicUrl = $this->buildPublicRiscaldamentoReadingUrl((int) $reading->stabile_servizio_id, (int) $reading->unita_immobiliare_id, (int) $reading->id);
|
|
$body = implode("\n", array_filter([
|
|
'Oggetto: sollecito lettura riscaldamento',
|
|
'',
|
|
'Gentile condomino,',
|
|
'ti ricordiamo di comunicare la lettura del contatore riscaldamento tramite il link dedicato qui sotto.',
|
|
$publicUrl,
|
|
'',
|
|
'Se hai già inviato la lettura puoi ignorare questo messaggio.',
|
|
'In caso di problemi contatta l amministrazione.',
|
|
]));
|
|
|
|
try {
|
|
Mail::raw($body, function ($message) use ($email): void {
|
|
$message->to($email)->subject('Sollecito lettura riscaldamento');
|
|
});
|
|
} catch (\Throwable) {
|
|
Notification::make()->title('Invio email non riuscito')->warning()->body('Il recapito esiste ma l invio del sollecito non è riuscito.')->send();
|
|
return;
|
|
}
|
|
} else {
|
|
Notification::make()->title('Nessuna email disponibile per questa unità')->warning()->body('Compila un recapito in unita_recapiti_servizio per inviare il sollecito diretto.')->send();
|
|
return;
|
|
}
|
|
|
|
$reading->fill([
|
|
'sollecito_inviato_at' => now(),
|
|
'workflow_stato' => 'sollecito_inviato',
|
|
]);
|
|
$reading->save();
|
|
|
|
Notification::make()->title('Sollecito inviato e registrato')->success()->send();
|
|
}
|
|
|
|
public function setRiscaldamentoCampagnaSort(string $sort): void
|
|
{
|
|
$this->riscaldamentoCampagnaSort = in_array($sort, ['scala_interno', 'nominativo'], true) ? $sort : 'scala_interno';
|
|
}
|
|
|
|
public function saveRiscaldamentoCampagnaRow(int $unitId): void
|
|
{
|
|
$this->persistRiscaldamentoCampagnaRows([$unitId]);
|
|
}
|
|
|
|
public function saveAllRiscaldamentoCampagnaRows(): void
|
|
{
|
|
$this->persistRiscaldamentoCampagnaRows(array_map('intval', array_keys($this->riscaldamentoCampagnaEditRows)));
|
|
}
|
|
|
|
public function ensureRiscaldamentoAceaContract(): void
|
|
{
|
|
$stabileId = $this->resolveActiveStabileId();
|
|
if (! $stabileId) {
|
|
Notification::make()->title('Stabile attivo non disponibile.')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$fornitoreAcea = $this->resolvePreferredRiscaldamentoSupplier($stabileId);
|
|
|
|
if (! $fornitoreAcea) {
|
|
Notification::make()->title('Fornitore Riscaldamento/Gas non trovato in anagrafica fornitori.')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$existing = StabileServizio::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('tipo', 'riscaldamento')
|
|
->orderByDesc('attivo')
|
|
->orderBy('id')
|
|
->first();
|
|
|
|
if ($existing) {
|
|
$existing->fornitore_id = (int) $fornitoreAcea->id;
|
|
if (trim((string) ($existing->nome ?? '')) === '') {
|
|
$existing->nome = 'Utenza riscaldamento - Fornitore Gas/Caldaia';
|
|
}
|
|
if (! $existing->attivo) {
|
|
$existing->attivo = true;
|
|
}
|
|
$existing->save();
|
|
|
|
Notification::make()->title('Contratto RISCALDAMENTO aggiornato.')->success()->send();
|
|
return;
|
|
}
|
|
|
|
StabileServizio::query()->create([
|
|
'stabile_id' => $stabileId,
|
|
'tipo' => 'riscaldamento',
|
|
'nome' => 'Utenza riscaldamento - Fornitore Gas/Caldaia',
|
|
'fornitore_id' => (int) $fornitoreAcea->id,
|
|
'attivo' => true,
|
|
'meta' => [
|
|
'canale_acquisizione' => 'manuale',
|
|
],
|
|
]);
|
|
|
|
Notification::make()->title('Contratto RISCALDAMENTO creato.')->success()->send();
|
|
}
|
|
|
|
protected function getTableQuery(): Builder
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return StabileServizio::query()->whereRaw('1 = 0');
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
return StabileServizio::query()->whereRaw('1 = 0');
|
|
}
|
|
|
|
return StabileServizio::query()
|
|
->where('stabile_id', (int) $stabileId)
|
|
->with([
|
|
'fornitore:id,ragione_sociale',
|
|
'voceSpesa:id,descrizione,tipo_gestione',
|
|
])
|
|
->withCount('letture')
|
|
->orderBy('tipo')
|
|
->orderBy('nome')
|
|
->orderByDesc('id');
|
|
}
|
|
|
|
public function table(Table $table): Table
|
|
{
|
|
$tipoOptions = $this->getServiceTypeOptions();
|
|
$commonScopeOptions = $this->getCommonAssetScopeOptions();
|
|
$allocationOptions = $this->getAllocationScopeOptions();
|
|
$fiscalScopeOptions = $this->getFiscalScopeOptions();
|
|
$counterScopeOptions = $this->getCounterScopeOptions();
|
|
|
|
$gestioneOptions = [
|
|
'ordinaria' => 'Ordinaria',
|
|
'riscaldamento' => 'Riscaldamento',
|
|
'straordinaria' => 'Straordinaria',
|
|
];
|
|
|
|
return $table
|
|
->striped()
|
|
->filters([
|
|
SelectFilter::make('tipo')
|
|
->label('Tipo servizio')
|
|
->options($tipoOptions),
|
|
|
|
SelectFilter::make('common_asset_scope')
|
|
->label('Ambito bene comune')
|
|
->options($commonScopeOptions)
|
|
->query(function (Builder $query, array $data): Builder {
|
|
$value = trim((string) ($data['value'] ?? ''));
|
|
if ($value === '') {
|
|
return $query;
|
|
}
|
|
|
|
return $query->where('meta->common_asset_scope', $value);
|
|
}),
|
|
|
|
SelectFilter::make('counter_scope')
|
|
->label('Tipo contatore')
|
|
->options($counterScopeOptions)
|
|
->query(function (Builder $query, array $data): Builder {
|
|
$value = trim((string) ($data['value'] ?? ''));
|
|
if ($value === '') {
|
|
return $query;
|
|
}
|
|
|
|
return $query->where('meta->counter_scope', $value);
|
|
}),
|
|
|
|
SelectFilter::make('tipo_gestione')
|
|
->label('Gestione (O/R/S)')
|
|
->options($gestioneOptions)
|
|
->query(function (Builder $query, array $data): Builder {
|
|
$value = (string) ($data['value'] ?? '');
|
|
if ($value === '') {
|
|
return $query;
|
|
}
|
|
|
|
return $query->whereHas('voceSpesa', fn(Builder $q) => $q->where('tipo_gestione', $value));
|
|
}),
|
|
])
|
|
->columns([
|
|
TextColumn::make('tipo')
|
|
->label('Tipo')
|
|
->formatStateUsing(fn($state) => $this->formatMetaOptionLabel($tipoOptions, $state))
|
|
->sortable(),
|
|
|
|
TextColumn::make('nome')->label('Nome')->searchable()->wrap()->sortable(),
|
|
|
|
TextColumn::make('meta.common_asset_label')
|
|
->label('Bene / locale')
|
|
->formatStateUsing(fn($state): string => trim((string) $state) !== '' ? trim((string) $state) : '—')
|
|
->searchable()
|
|
->wrap()
|
|
->toggleable(),
|
|
|
|
TextColumn::make('meta.common_asset_scope')
|
|
->label('Ambito')
|
|
->formatStateUsing(fn($state) => $this->formatMetaOptionLabel($commonScopeOptions, $state))
|
|
->toggleable(),
|
|
|
|
TextColumn::make('meta.allocation_scope')
|
|
->label('Addebito')
|
|
->formatStateUsing(fn($state) => $this->formatMetaOptionLabel($allocationOptions, $state))
|
|
->wrap()
|
|
->toggleable(),
|
|
|
|
TextColumn::make('meta.fiscal_scope')
|
|
->label('Trattamento')
|
|
->formatStateUsing(fn($state) => $this->formatMetaOptionLabel($fiscalScopeOptions, $state))
|
|
->wrap()
|
|
->toggleable(isToggledHiddenByDefault: true),
|
|
|
|
TextColumn::make('fornitore.ragione_sociale')->label('Fornitore')->searchable()->wrap()->toggleable(),
|
|
|
|
TextColumn::make('voceSpesa.descrizione')->label('Voce spesa')->searchable()->wrap()->toggleable(),
|
|
|
|
TextColumn::make('voceSpesa.tipo_gestione')
|
|
->label('Gestione')
|
|
->formatStateUsing(fn($state) => $gestioneOptions[strtolower(trim((string) $state))] ?? (string) ($state ?? '—'))
|
|
->toggleable(isToggledHiddenByDefault: true),
|
|
|
|
TextColumn::make('letture_count')->label('Letture')->sortable(),
|
|
|
|
TextColumn::make('meta.canale_acquisizione')
|
|
->label('Canale')
|
|
->formatStateUsing(function ($state): string {
|
|
$v = strtolower(trim((string) $state));
|
|
return match ($v) {
|
|
'manuale' => 'Manuale',
|
|
'email' => 'Email',
|
|
'whatsapp' => 'WhatsApp',
|
|
'import_file' => 'Import file',
|
|
'm_bus', 'mbus' => 'Contatore elettronico',
|
|
default => $v !== '' ? strtoupper($v) : '—',
|
|
};
|
|
})
|
|
->toggleable(isToggledHiddenByDefault: true),
|
|
|
|
TextColumn::make('meta.email_raccolta_letture')->label('Email raccolta')->toggleable(isToggledHiddenByDefault: true),
|
|
TextColumn::make('meta.whatsapp_raccolta_letture')->label('WhatsApp raccolta')->toggleable(isToggledHiddenByDefault: true),
|
|
TextColumn::make('meta.costo_lettura_unitario')->label('Costo lettura')->money('EUR')->toggleable(isToggledHiddenByDefault: true),
|
|
TextColumn::make('meta.costo_ripartizione')->label('Costo ripartizione')->money('EUR')->toggleable(isToggledHiddenByDefault: true),
|
|
|
|
TextColumn::make('codice_utenza')->label('Utenza')->searchable()->toggleable(isToggledHiddenByDefault: true),
|
|
TextColumn::make('codice_cliente')->label('Cliente')->searchable()->toggleable(isToggledHiddenByDefault: true),
|
|
TextColumn::make('codice_contratto')->label('Contratto')->searchable()->toggleable(isToggledHiddenByDefault: true),
|
|
TextColumn::make('contatore_matricola')->label('Matricola')->searchable()->toggleable(),
|
|
|
|
TextColumn::make('meta.counter_scope')
|
|
->label('Contatore')
|
|
->formatStateUsing(fn($state) => $this->formatMetaOptionLabel($counterScopeOptions, $state))
|
|
->toggleable(),
|
|
|
|
IconColumn::make('meta.has_dedicated_meter')
|
|
->label('Ded.')
|
|
->boolean()
|
|
->toggleable(isToggledHiddenByDefault: true),
|
|
|
|
TextColumn::make('meta.served_area_label')
|
|
->label('Area servita')
|
|
->wrap()
|
|
->toggleable(isToggledHiddenByDefault: true),
|
|
|
|
IconColumn::make('attivo')->label('Attivo')->boolean()->sortable(),
|
|
TextColumn::make('updated_at')->label('Aggiornato')->dateTime('d/m/Y H:i')->toggleable(isToggledHiddenByDefault: true),
|
|
])
|
|
->headerActions([
|
|
Action::make('create')
|
|
->label('Nuovo')
|
|
->icon('heroicon-o-plus')
|
|
->form([
|
|
Select::make('tipo')
|
|
->label('Tipo servizio')
|
|
->options($tipoOptions)
|
|
->required(),
|
|
TextInput::make('nome')->label('Nome')->maxLength(255),
|
|
TextInput::make('common_asset_label')->label('Bene comune / locale / servizio')->maxLength(255),
|
|
Select::make('common_asset_scope')
|
|
->label('Ambito bene comune')
|
|
->options($commonScopeOptions),
|
|
Select::make('allocation_scope')
|
|
->label('Ripartizione / addebito')
|
|
->options($allocationOptions),
|
|
Select::make('fiscal_scope')
|
|
->label('Trattamento economico / fiscale')
|
|
->options($fiscalScopeOptions),
|
|
Select::make('fornitore_id')
|
|
->label('Fornitore')
|
|
->searchable()
|
|
->options(fn() => $this->getFornitoriOptions()),
|
|
Select::make('voce_spesa_id')
|
|
->label('Voce spesa')
|
|
->searchable()
|
|
->options(fn() => $this->getVociSpesaOptions()),
|
|
TextInput::make('codice_utenza')->label('Codice utenza')->maxLength(255),
|
|
TextInput::make('codice_cliente')->label('Codice cliente')->maxLength(255),
|
|
TextInput::make('codice_contratto')->label('Codice contratto')->maxLength(255),
|
|
TextInput::make('contatore_matricola')->label('Matricola contatore')->maxLength(255),
|
|
Toggle::make('has_dedicated_meter')->label('Contatore dedicato')->default(false),
|
|
Select::make('counter_scope')
|
|
->label('Tipo contatore')
|
|
->options($counterScopeOptions)
|
|
->default('assente'),
|
|
TextInput::make('served_area_label')->label('Area servita / utilizzo')->maxLength(255),
|
|
Select::make('canale_acquisizione')
|
|
->label('Canale acquisizione letture')
|
|
->options([
|
|
'manuale' => 'Manuale',
|
|
'email' => 'Email',
|
|
'whatsapp' => 'WhatsApp',
|
|
'import_file' => 'Import file',
|
|
'm_bus' => 'Contatore elettronico',
|
|
]),
|
|
TextInput::make('email_raccolta_letture')->label('Email raccolta letture')->email()->maxLength(255),
|
|
TextInput::make('whatsapp_raccolta_letture')->label('Numero WhatsApp raccolta')->maxLength(50),
|
|
TextInput::make('costo_lettura_unitario')->label('Costo lettura (unitario)')->numeric(),
|
|
TextInput::make('costo_ripartizione')->label('Costo ripartizione')->numeric(),
|
|
Toggle::make('ripartizione_esterna')->label('Ripartizione affidata a ditta esterna')->default(false),
|
|
Textarea::make('operative_notes')->label('Note operative')->rows(3)->maxLength(2000),
|
|
Toggle::make('attivo')->label('Attivo')->default(true),
|
|
])
|
|
->action(function (array $data): void {
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return;
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
return;
|
|
}
|
|
|
|
StabileServizio::query()->create([
|
|
'stabile_id' => (int) $stabileId,
|
|
'tipo' => strtolower(trim((string) ($data['tipo'] ?? ''))),
|
|
'nome' => $this->resolveServiceDisplayName($data),
|
|
'fornitore_id' => (int) ($data['fornitore_id'] ?? 0) ?: null,
|
|
'voce_spesa_id' => (int) ($data['voce_spesa_id'] ?? 0) ?: null,
|
|
'codice_utenza' => trim((string) ($data['codice_utenza'] ?? '')) ?: null,
|
|
'codice_cliente' => trim((string) ($data['codice_cliente'] ?? '')) ?: null,
|
|
'codice_contratto' => trim((string) ($data['codice_contratto'] ?? '')) ?: null,
|
|
'contatore_matricola' => trim((string) ($data['contatore_matricola'] ?? '')) ?: null,
|
|
'attivo' => (bool) ($data['attivo'] ?? true),
|
|
'meta' => $this->buildServiceMetaPayload([], $data),
|
|
]);
|
|
}),
|
|
Action::make('propaga_impostazioni_riscaldamento')
|
|
->label('Propaga impostazioni riscaldamento')
|
|
->icon('heroicon-o-arrow-path')
|
|
->color('info')
|
|
->requiresConfirmation()
|
|
->modalHeading('Propaga template riscaldamento agli altri stabili')
|
|
->modalDescription('Copia il template operativo riscaldamento dello stabile attivo sugli altri stabili accessibili senza sovrascrivere utenza, cliente, contratto e matricola già presenti.')
|
|
->action(function (): void {
|
|
$stats = $this->propagateRiscaldamentoSettingsToOtherStabili();
|
|
|
|
Notification::make()
|
|
->title('Propagazione riscaldamento completata')
|
|
->body('Stabili toccati: ' . $stats['touched'] . ' · creati: ' . $stats['created'] . ' · aggiornati: ' . $stats['updated'] . ' · saltati: ' . $stats['skipped'])
|
|
->success()
|
|
->send();
|
|
}),
|
|
])
|
|
->actions([
|
|
Action::make('letture')
|
|
->label('Letture')
|
|
->icon('heroicon-o-clipboard-document-list')
|
|
->url(fn(StabileServizio $record): string => LettureServiziArchivio::getUrl(['servizio' => (int) $record->id], panel: 'admin-filament')),
|
|
|
|
Action::make('fatture')
|
|
->label('Fatture')
|
|
->icon('heroicon-o-document-text')
|
|
->url(fn() => FattureElettronicheP7mRicevute::getUrl(panel: 'admin-filament')),
|
|
|
|
Action::make('edit')
|
|
->label('Modifica')
|
|
->icon('heroicon-o-pencil-square')
|
|
->form([
|
|
Select::make('tipo')
|
|
->label('Tipo servizio')
|
|
->options($tipoOptions)
|
|
->required(),
|
|
TextInput::make('nome')->label('Nome')->maxLength(255),
|
|
TextInput::make('common_asset_label')->label('Bene comune / locale / servizio')->maxLength(255),
|
|
Select::make('common_asset_scope')
|
|
->label('Ambito bene comune')
|
|
->options($commonScopeOptions),
|
|
Select::make('allocation_scope')
|
|
->label('Ripartizione / addebito')
|
|
->options($allocationOptions),
|
|
Select::make('fiscal_scope')
|
|
->label('Trattamento economico / fiscale')
|
|
->options($fiscalScopeOptions),
|
|
Select::make('fornitore_id')
|
|
->label('Fornitore')
|
|
->searchable()
|
|
->options(fn() => $this->getFornitoriOptions()),
|
|
Select::make('voce_spesa_id')
|
|
->label('Voce spesa')
|
|
->searchable()
|
|
->options(fn() => $this->getVociSpesaOptions()),
|
|
TextInput::make('codice_utenza')->label('Codice utenza')->maxLength(255),
|
|
TextInput::make('codice_cliente')->label('Codice cliente')->maxLength(255),
|
|
TextInput::make('codice_contratto')->label('Codice contratto')->maxLength(255),
|
|
TextInput::make('contatore_matricola')->label('Matricola contatore')->maxLength(255),
|
|
Toggle::make('has_dedicated_meter')->label('Contatore dedicato')->default(false),
|
|
Select::make('counter_scope')
|
|
->label('Tipo contatore')
|
|
->options($counterScopeOptions),
|
|
TextInput::make('served_area_label')->label('Area servita / utilizzo')->maxLength(255),
|
|
Select::make('canale_acquisizione')
|
|
->label('Canale acquisizione letture')
|
|
->options([
|
|
'manuale' => 'Manuale',
|
|
'email' => 'Email',
|
|
'whatsapp' => 'WhatsApp',
|
|
'import_file' => 'Import file',
|
|
'm_bus' => 'Contatore elettronico',
|
|
]),
|
|
TextInput::make('email_raccolta_letture')->label('Email raccolta letture')->email()->maxLength(255),
|
|
TextInput::make('whatsapp_raccolta_letture')->label('Numero WhatsApp raccolta')->maxLength(50),
|
|
TextInput::make('costo_lettura_unitario')->label('Costo lettura (unitario)')->numeric(),
|
|
TextInput::make('costo_ripartizione')->label('Costo ripartizione')->numeric(),
|
|
Toggle::make('ripartizione_esterna')->label('Ripartizione affidata a ditta esterna')->default(false),
|
|
Textarea::make('operative_notes')->label('Note operative')->rows(3)->maxLength(2000),
|
|
Toggle::make('attivo')->label('Attivo')->default(true),
|
|
])
|
|
->fillForm(fn(StabileServizio $record): array=> [
|
|
'tipo' => (string) ($record->tipo ?? ''),
|
|
'nome' => (string) ($record->nome ?? ''),
|
|
'common_asset_label' => (string) (($record->meta['common_asset_label'] ?? '') ?: ''),
|
|
'common_asset_scope' => (string) (($record->meta['common_asset_scope'] ?? '') ?: ''),
|
|
'allocation_scope' => (string) (($record->meta['allocation_scope'] ?? '') ?: ''),
|
|
'fiscal_scope' => (string) (($record->meta['fiscal_scope'] ?? '') ?: ''),
|
|
'fornitore_id' => $record->fornitore_id,
|
|
'voce_spesa_id' => $record->voce_spesa_id,
|
|
'codice_utenza' => (string) ($record->codice_utenza ?? ''),
|
|
'codice_cliente' => (string) ($record->codice_cliente ?? ''),
|
|
'codice_contratto' => (string) ($record->codice_contratto ?? ''),
|
|
'contatore_matricola' => (string) ($record->contatore_matricola ?? ''),
|
|
'has_dedicated_meter' => (bool) ($record->meta['has_dedicated_meter'] ?? false),
|
|
'counter_scope' => (string) (($record->meta['counter_scope'] ?? '') ?: ''),
|
|
'served_area_label' => (string) (($record->meta['served_area_label'] ?? '') ?: ''),
|
|
'canale_acquisizione' => (string) (($record->meta['canale_acquisizione'] ?? '') ?: ''),
|
|
'email_raccolta_letture' => (string) (($record->meta['email_raccolta_letture'] ?? '') ?: ''),
|
|
'whatsapp_raccolta_letture' => (string) (($record->meta['whatsapp_raccolta_letture'] ?? '') ?: ''),
|
|
'costo_lettura_unitario' => $record->meta['costo_lettura_unitario'] ?? null,
|
|
'costo_ripartizione' => $record->meta['costo_ripartizione'] ?? null,
|
|
'ripartizione_esterna' => (bool) ($record->meta['ripartizione_esterna'] ?? false),
|
|
'operative_notes' => (string) (($record->meta['operative_notes'] ?? '') ?: ''),
|
|
'attivo' => (bool) ($record->attivo ?? true),
|
|
])
|
|
->action(function (StabileServizio $record, array $data): void {
|
|
$meta = $this->buildServiceMetaPayload(is_array($record->meta ?? null) ? $record->meta : [], $data);
|
|
|
|
$record->fill([
|
|
'tipo' => strtolower(trim((string) ($data['tipo'] ?? ''))),
|
|
'nome' => $this->resolveServiceDisplayName($data, (string) ($record->nome ?? '')),
|
|
'fornitore_id' => (int) ($data['fornitore_id'] ?? 0) ?: null,
|
|
'voce_spesa_id' => (int) ($data['voce_spesa_id'] ?? 0) ?: null,
|
|
'codice_utenza' => trim((string) ($data['codice_utenza'] ?? '')) ?: null,
|
|
'codice_cliente' => trim((string) ($data['codice_cliente'] ?? '')) ?: null,
|
|
'codice_contratto' => trim((string) ($data['codice_contratto'] ?? '')) ?: null,
|
|
'contatore_matricola' => trim((string) ($data['contatore_matricola'] ?? '')) ?: null,
|
|
'attivo' => (bool) ($data['attivo'] ?? true),
|
|
'meta' => $meta,
|
|
]);
|
|
$record->save();
|
|
}),
|
|
|
|
Action::make('delete')
|
|
->label('Elimina')
|
|
->icon('heroicon-o-trash')
|
|
->color('danger')
|
|
->requiresConfirmation()
|
|
->action(fn(StabileServizio $record) => $record->delete()),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
private function getServiceTypeOptions(): array
|
|
{
|
|
return [
|
|
'riscaldamento' => 'Riscaldamento',
|
|
'energia' => 'Energia',
|
|
'gas' => 'Gas',
|
|
'riscaldamento' => 'Riscaldamento',
|
|
'ascensore' => 'Ascensore',
|
|
'pulizie' => 'Pulizie',
|
|
'assicurazione' => 'Assicurazione',
|
|
'privacy' => 'Privacy',
|
|
'antenna' => 'Antenna',
|
|
'antincendio' => 'Antincendio',
|
|
'citofonia' => 'Citofonia',
|
|
'locale_comune' => 'Locale comune',
|
|
'spazio_comune' => 'Spazio / area comune',
|
|
'impianto' => 'Impianto comune',
|
|
'altro' => 'Altro',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
private function getCommonAssetScopeOptions(): array
|
|
{
|
|
return [
|
|
'locale' => 'Locale',
|
|
'spazio' => 'Spazio comune',
|
|
'impianto' => 'Impianto',
|
|
'area_esterna' => 'Area esterna / giardino',
|
|
'copertura' => 'Terrazzo / copertura',
|
|
'accesso' => 'Portone / accesso',
|
|
'servizio' => 'Servizio comune',
|
|
'sicurezza' => 'Sicurezza / antincendio',
|
|
'amministrativo' => 'Privacy / assicurazione / studio',
|
|
'altro' => 'Altro',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
private function getAllocationScopeOptions(): array
|
|
{
|
|
return [
|
|
'tutti_millesimi' => 'Tutti per millesimi',
|
|
'condomini_utilizzatori' => 'Condomini utilizzatori',
|
|
'inquilini_utilizzatori' => 'Inquilini utilizzatori',
|
|
'condomini_inquilini_utilizzatori' => 'Condomini + inquilini utilizzatori',
|
|
'rimborso_spese' => 'Rimborso spese uso bene comune',
|
|
'gestione_studio' => 'Gestione studio / amministrazione',
|
|
'altro' => 'Altro criterio',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
private function getFiscalScopeOptions(): array
|
|
{
|
|
return [
|
|
'spesa_condominiale' => 'Spesa condominiale',
|
|
'rimborso_non_fiscale' => 'Rimborso spese non fiscale',
|
|
'da_fatturare' => 'Da fatturare',
|
|
'polizza_o_servizio' => 'Polizza / servizio',
|
|
'non_applicabile' => 'Non applicabile',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
private function getCounterScopeOptions(): array
|
|
{
|
|
return [
|
|
'generale' => 'Generale',
|
|
'particolare' => 'Particolare / dedicato',
|
|
'assente' => 'Senza contatore',
|
|
];
|
|
}
|
|
|
|
private function formatMetaOptionLabel(array $options, mixed $state): string
|
|
{
|
|
$value = strtolower(trim((string) $state));
|
|
|
|
return $options[$value] ?? ($value !== '' ? (string) $state : '—');
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $currentMeta
|
|
* @param array<string, mixed> $data
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function buildServiceMetaPayload(array $currentMeta, array $data): array
|
|
{
|
|
$meta = $currentMeta;
|
|
$meta['common_asset_label'] = trim((string) ($data['common_asset_label'] ?? '')) ?: null;
|
|
$meta['common_asset_scope'] = trim((string) ($data['common_asset_scope'] ?? '')) ?: null;
|
|
$meta['allocation_scope'] = trim((string) ($data['allocation_scope'] ?? '')) ?: null;
|
|
$meta['fiscal_scope'] = trim((string) ($data['fiscal_scope'] ?? '')) ?: null;
|
|
$meta['has_dedicated_meter'] = (bool) ($data['has_dedicated_meter'] ?? false);
|
|
$meta['counter_scope'] = trim((string) ($data['counter_scope'] ?? '')) ?: (($meta['has_dedicated_meter'] ?? false) ? 'particolare' : 'assente');
|
|
$meta['served_area_label'] = trim((string) ($data['served_area_label'] ?? '')) ?: null;
|
|
$meta['canale_acquisizione'] = trim((string) ($data['canale_acquisizione'] ?? '')) ?: null;
|
|
$meta['email_raccolta_letture'] = trim((string) ($data['email_raccolta_letture'] ?? '')) ?: null;
|
|
$meta['whatsapp_raccolta_letture'] = trim((string) ($data['whatsapp_raccolta_letture'] ?? '')) ?: null;
|
|
$meta['costo_lettura_unitario'] = is_numeric($data['costo_lettura_unitario'] ?? null) ? (float) $data['costo_lettura_unitario'] : null;
|
|
$meta['costo_ripartizione'] = is_numeric($data['costo_ripartizione'] ?? null) ? (float) $data['costo_ripartizione'] : null;
|
|
$meta['ripartizione_esterna'] = (bool) ($data['ripartizione_esterna'] ?? false);
|
|
$meta['operative_notes'] = trim((string) ($data['operative_notes'] ?? '')) ?: null;
|
|
|
|
return $meta;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $data
|
|
*/
|
|
private function resolveServiceDisplayName(array $data, string $fallback = ''): string
|
|
{
|
|
$name = trim((string) ($data['nome'] ?? ''));
|
|
if ($name !== '') {
|
|
return $name;
|
|
}
|
|
|
|
$commonLabel = trim((string) ($data['common_asset_label'] ?? ''));
|
|
if ($commonLabel !== '') {
|
|
return $commonLabel;
|
|
}
|
|
|
|
return trim($fallback);
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
private function getFornitoriOptions(): array
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return [];
|
|
}
|
|
|
|
$query = Fornitore::query();
|
|
|
|
if (! $user->hasAnyRole(['super-admin', 'admin'])) {
|
|
$activeStabile = StabileContext::getActiveStabile($user);
|
|
$adminId = (int) ($activeStabile?->amministratore_id ?: 0);
|
|
if ($adminId > 0) {
|
|
$query->where('amministratore_id', $adminId);
|
|
}
|
|
}
|
|
|
|
return $query
|
|
->orderBy('ragione_sociale')
|
|
->get(['id', 'ragione_sociale'])
|
|
->mapWithKeys(fn(Fornitore $f) => [(int) $f->id => (string) ($f->ragione_sociale ?? ('Fornitore #' . $f->id))])
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
private function getVociSpesaOptions(): array
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return [];
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
return [];
|
|
}
|
|
|
|
return VoceSpesa::query()
|
|
->where('stabile_id', (int) $stabileId)
|
|
->orderBy('tipo_gestione')
|
|
->orderBy('descrizione')
|
|
->get(['id', 'descrizione', 'tipo_gestione'])
|
|
->mapWithKeys(function (VoceSpesa $v): array {
|
|
$tipo = strtolower(trim((string) ($v->tipo_gestione ?? '')));
|
|
$prefix = $tipo !== '' ? strtoupper(substr($tipo, 0, 1)) . ' - ' : '';
|
|
return [(int) $v->id => $prefix . (string) ($v->descrizione ?? ('Voce #' . $v->id))];
|
|
})
|
|
->all();
|
|
}
|
|
|
|
private function resolveActiveStabileId(): ?int
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return null;
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
return $stabileId ? (int) $stabileId : null;
|
|
}
|
|
|
|
private function resolveActiveAnnoGestione(): int
|
|
{
|
|
$user = Auth::user();
|
|
|
|
return AnnoGestioneContext::resolveActiveAnno($user instanceof User ? $user : null);
|
|
}
|
|
|
|
private function resolveActiveRiscaldamentoServizio(): ?StabileServizio
|
|
{
|
|
$stabileId = $this->resolveActiveStabileId();
|
|
if (! $stabileId) {
|
|
return null;
|
|
}
|
|
|
|
return StabileServizio::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('tipo', 'riscaldamento')
|
|
->orderByDesc('attivo')
|
|
->orderBy('id')
|
|
->first();
|
|
}
|
|
|
|
/** @return array<int, int> */
|
|
private function normalizeRiscaldamentoSelectedFeIds(): array
|
|
{
|
|
return array_values(array_unique(array_filter(array_map('intval', $this->riscaldamentoSelectedFeIds), static fn(int $value): bool => $value > 0)));
|
|
}
|
|
|
|
private function resolveRiscaldamentoCandidateInvoices(?string $tipoGestione = null)
|
|
{
|
|
$stabileId = $this->resolveActiveStabileId();
|
|
if (! $stabileId) {
|
|
return collect();
|
|
}
|
|
|
|
$year = $this->resolveActiveAnnoGestione();
|
|
$fornitoreIds = StabileServizio::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('tipo', 'riscaldamento')
|
|
->whereNotNull('fornitore_id')
|
|
->pluck('fornitore_id')
|
|
->map(fn($value): int => (int) $value)
|
|
->filter(fn(int $value): bool => $value > 0)
|
|
->values()
|
|
->all();
|
|
|
|
if ($fornitoreIds === []) {
|
|
$preferredSupplier = $this->resolvePreferredRiscaldamentoSupplier($stabileId);
|
|
if ($preferredSupplier instanceof Fornitore) {
|
|
$fornitoreIds = [(int) $preferredSupplier->id];
|
|
}
|
|
}
|
|
|
|
$fornitoreIds = $this->resolveEquivalentFornitoreIds($fornitoreIds);
|
|
|
|
$dal = $this->riscaldamentoDataInizioFiltro;
|
|
$al = $this->riscaldamentoDataFineFiltro;
|
|
|
|
$query = FatturaElettronica::query()
|
|
->where('stabile_id', $stabileId)
|
|
->orderByDesc('data_fattura')
|
|
->orderByDesc('id');
|
|
|
|
if ($dal && $al) {
|
|
$query->whereBetween('data_fattura', [$dal, $al]);
|
|
} else {
|
|
$query->whereYear('data_fattura', $year);
|
|
}
|
|
|
|
if ($fornitoreIds !== []) {
|
|
$query->whereIn('fornitore_id', $fornitoreIds);
|
|
}
|
|
|
|
if ($tipoGestione !== null && $tipoGestione !== '' && $tipoGestione !== 'tutte' && Schema::hasTable('contabilita_fatture_fornitori')) {
|
|
$invoiceIds = DB::table('contabilita_fatture_fornitori as f')
|
|
->leftJoin('gestioni_contabili as g', 'g.id', '=', 'f.gestione_id')
|
|
->where('f.stabile_id', $stabileId)
|
|
->whereNotNull('f.fattura_elettronica_id')
|
|
->when($fornitoreIds !== [], fn($builder) => $builder->whereIn('f.fornitore_id', $fornitoreIds))
|
|
->where(function ($b) use ($tipoGestione) {
|
|
$b->where('g.tipo_gestione', $tipoGestione)
|
|
->orWhereNull('f.gestione_id');
|
|
})
|
|
->when($dal && $al, fn($builder) => $builder->whereBetween('f.data_documento', [$dal, $al]))
|
|
->when(!($dal && $al), function($builder) use ($year) {
|
|
$builder->when(Schema::hasColumn('gestioni_contabili', 'anno_gestione'), fn($b) => $b->where('g.anno_gestione', $year))
|
|
->when(! Schema::hasColumn('gestioni_contabili', 'anno_gestione') && Schema::hasColumn('contabilita_fatture_fornitori', 'data_documento'), fn($b) => $b->whereYear('f.data_documento', $year));
|
|
})
|
|
->pluck('f.fattura_elettronica_id')
|
|
->map(fn($value): int => (int) $value)
|
|
->filter(fn(int $value): bool => $value > 0)
|
|
->unique()
|
|
->values()
|
|
->all();
|
|
|
|
if ($invoiceIds === []) {
|
|
return collect();
|
|
}
|
|
|
|
$query->whereIn('id', $invoiceIds);
|
|
}
|
|
|
|
$userId = (int) (Auth::id() ?? 0);
|
|
|
|
return $query->get()->filter(function (FatturaElettronica $fattura) use ($fornitoreIds, $userId): bool {
|
|
if ($fornitoreIds !== [] && in_array((int) ($fattura->fornitore_id ?? 0), $fornitoreIds, true)) {
|
|
return true;
|
|
}
|
|
|
|
return $this->payloadLooksLikeRiscaldamento($this->resolveRiscaldamentoParsedPayload($fattura, $userId));
|
|
})->values();
|
|
}
|
|
|
|
private function payloadLooksLikeRiscaldamento(?array $payload): bool
|
|
{
|
|
if (! is_array($payload) || $payload === []) {
|
|
return false;
|
|
}
|
|
|
|
if (($payload['type'] ?? null) === 'riscaldamento') {
|
|
return true;
|
|
}
|
|
|
|
if (! empty($payload['consumi']) || ! empty($payload['riepilogo_letture']) || ! empty($payload['finestra_autolettura'])) {
|
|
return true;
|
|
}
|
|
|
|
$quadro = is_array($payload['quadro_dettaglio'] ?? null) ? $payload['quadro_dettaglio'] : [];
|
|
foreach (['quota_fissa', 'acquedotto', 'fognatura', 'depurazione', 'oneri_perequazione', 'restituzione_acconti'] as $key) {
|
|
if (isset($quadro[$key]) && is_numeric($quadro[$key])) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return ! empty($payload['tariffe']);
|
|
}
|
|
|
|
private function resolveRiscaldamentoParsedPayload(FatturaElettronica $fattura, int $userId): ?array
|
|
{
|
|
$storedRaw = is_string($fattura->consumo_raw ?? null) ? trim((string) $fattura->consumo_raw) : '';
|
|
if ($storedRaw !== '') {
|
|
$decoded = json_decode($storedRaw, true);
|
|
if (is_array($decoded) && $this->payloadLooksLikeRiscaldamento($decoded)) {
|
|
return $decoded;
|
|
}
|
|
|
|
$parsedStoredText = app(GasLucePdfTextParser::class)->parse($storedRaw);
|
|
if ($this->payloadLooksLikeRiscaldamento($parsedStoredText)) {
|
|
return $parsedStoredText;
|
|
}
|
|
}
|
|
|
|
try {
|
|
if ($userId > 0) {
|
|
app(FatturaElettronicaProtocolloService::class)->ensureDocumentoProtocollato($fattura, $userId);
|
|
$fattura->refresh();
|
|
}
|
|
} catch (\Throwable) {
|
|
// ignore
|
|
}
|
|
|
|
$documento = Documento::query()
|
|
->where('documentable_type', FatturaElettronica::class)
|
|
->where('documentable_id', (int) $fattura->id)
|
|
->first();
|
|
|
|
if (! $documento instanceof Documento) {
|
|
return null;
|
|
}
|
|
|
|
$text = is_string($documento->contenuto_ocr ?? null) ? trim((string) $documento->contenuto_ocr) : '';
|
|
if ($text === '') {
|
|
try {
|
|
$result = app(PdfTextExtractionService::class)->extractAndStore($documento, false);
|
|
if (($result['status'] ?? null) === 'ok') {
|
|
$documento->refresh();
|
|
$text = is_string($documento->contenuto_ocr ?? null) ? trim((string) $documento->contenuto_ocr) : '';
|
|
}
|
|
} catch (\Throwable) {
|
|
$text = '';
|
|
}
|
|
}
|
|
|
|
if ($text === '') {
|
|
return null;
|
|
}
|
|
|
|
$parsed = app(GasLucePdfTextParser::class)->parse($text);
|
|
|
|
return $this->payloadLooksLikeRiscaldamento($parsed) ? $parsed : null;
|
|
}
|
|
|
|
/** @param array<int, array<string, mixed>> $consumi */
|
|
private function sumRiscaldamentoConsumi(array $consumi): ?float
|
|
{
|
|
$sum = 0.0;
|
|
$hasValues = false;
|
|
|
|
foreach ($consumi as $consumo) {
|
|
if (! is_array($consumo) || ! is_numeric($consumo['valore'] ?? null)) {
|
|
continue;
|
|
}
|
|
|
|
$sum += (float) $consumo['valore'];
|
|
$hasValues = true;
|
|
}
|
|
|
|
return $hasValues ? round($sum, 3) : null;
|
|
}
|
|
|
|
/** @param array<int, array<string, mixed>> $consumi */
|
|
private function buildRiscaldamentoConsumoReference(array $consumi): ?string
|
|
{
|
|
$first = $consumi[0] ?? null;
|
|
if (! is_array($first)) {
|
|
return null;
|
|
}
|
|
|
|
$dal = is_string($first['dal'] ?? null) ? trim((string) $first['dal']) : '';
|
|
$al = is_string($first['al'] ?? null) ? trim((string) $first['al']) : '';
|
|
|
|
if ($dal === '' || $al === '') {
|
|
return null;
|
|
}
|
|
|
|
return $dal . ' - ' . $al;
|
|
}
|
|
|
|
private function buildPublicRiscaldamentoReadingUrl(int $servizioId, int $unitaId, ?int $requestId = null): string
|
|
{
|
|
return URL::temporarySignedRoute(
|
|
'public.water-reading.show',
|
|
now()->addDays(30),
|
|
array_filter([
|
|
'servizio' => $servizioId,
|
|
'unita' => $unitaId,
|
|
'request' => $requestId,
|
|
], fn($value) => $value !== null)
|
|
);
|
|
}
|
|
|
|
/** @return array<int, string> */
|
|
private function resolveLegacyCodiciStabile(): array
|
|
{
|
|
$user = Auth::user();
|
|
|
|
return StabileContext::legacyCodeCandidates(StabileContext::getActiveStabile($user instanceof User ? $user : null));
|
|
}
|
|
|
|
private function applyActiveYearFilterToReadingQuery(Builder $query): Builder
|
|
{
|
|
$year = $this->resolveActiveAnnoGestione();
|
|
|
|
return $query->where(function (Builder $inner) use ($year): void {
|
|
$inner->whereYear('periodo_al', $year)
|
|
->orWhere(function (Builder $fallback) use ($year): void {
|
|
$fallback->whereNull('periodo_al')
|
|
->whereYear('periodo_dal', $year);
|
|
})
|
|
->orWhere(function (Builder $fallback) use ($year): void {
|
|
$fallback->whereNull('periodo_al')
|
|
->whereNull('periodo_dal')
|
|
->whereYear('created_at', $year);
|
|
});
|
|
});
|
|
}
|
|
|
|
/** @return array{totale_fatture: float, totale_operazioni_ac12_legacy: float, delta_operazioni_vs_fatture: float, totale_letture: int, totale_letture_con_riferimento: int, totale_consumi_mc: float, totale_tariffe: int} */
|
|
public function getRiscaldamentoDashboardStatsProperty(): array
|
|
{
|
|
$stabileId = $this->resolveActiveStabileId();
|
|
if (! $stabileId) {
|
|
return [
|
|
'totale_fatture' => 0.0,
|
|
'totale_operazioni_ac12_legacy' => 0.0,
|
|
'delta_operazioni_vs_fatture' => 0.0,
|
|
'totale_letture' => 0,
|
|
'totale_letture_con_riferimento' => 0,
|
|
'totale_consumi_mc' => 0.0,
|
|
'totale_tariffe' => 0,
|
|
];
|
|
}
|
|
|
|
$fattureRows = $this->riscaldamentoFatturePerGestione;
|
|
$totaleFatture = (float) collect($fattureRows)->sum(fn(array $r): float => (float) ($r['totale_fatture'] ?? 0));
|
|
|
|
$totaleLetture = (int) StabileServizioLettura::query()
|
|
->where('stabile_id', $stabileId)
|
|
->whereHas('servizio', fn(Builder $q) => $q->where('tipo', 'riscaldamento'))
|
|
->count();
|
|
|
|
$totaleLettureConRiferimento = (int) StabileServizioLettura::query()
|
|
->where('stabile_id', $stabileId)
|
|
->whereHas('servizio', fn(Builder $q) => $q->where('tipo', 'riscaldamento'))
|
|
->whereNotNull('riferimento_acquisizione')
|
|
->where('riferimento_acquisizione', '!=', '')
|
|
->count();
|
|
|
|
$totaleConsumi = (float) StabileServizioLettura::query()
|
|
->where('stabile_id', $stabileId)
|
|
->whereHas('servizio', fn(Builder $q) => $q->where('tipo', 'riscaldamento'))
|
|
->sum('consumo_valore');
|
|
|
|
$totaleTariffe = (int) StabileServizioTariffa::query()
|
|
->where('stabile_id', $stabileId)
|
|
->count();
|
|
|
|
$legacySummary = $this->riscaldamentoLegacyOperazioniSummary;
|
|
$totaleLegacyAc12 = (float) ($legacySummary['totale'] ?? 0);
|
|
|
|
return [
|
|
'totale_fatture' => round($totaleFatture, 2),
|
|
'totale_operazioni_ac12_legacy' => round($totaleLegacyAc12, 2),
|
|
'delta_operazioni_vs_fatture' => round($totaleFatture - $totaleLegacyAc12, 2),
|
|
'totale_letture' => $totaleLetture,
|
|
'totale_letture_con_riferimento' => $totaleLettureConRiferimento,
|
|
'totale_consumi_mc' => round($totaleConsumi, 3),
|
|
'totale_tariffe' => $totaleTariffe,
|
|
];
|
|
}
|
|
|
|
private function normalizeInvoiceNumber(mixed $number): ?string
|
|
{
|
|
if ($number === null) {
|
|
return null;
|
|
}
|
|
|
|
$raw = strtoupper(trim((string) $number));
|
|
if ($raw === '') {
|
|
return null;
|
|
}
|
|
|
|
$normalized = preg_replace('/[^A-Z0-9]/', '', $raw);
|
|
return $normalized !== '' ? $normalized : null;
|
|
}
|
|
|
|
private function parseLegacyDate(mixed $value): ?string
|
|
{
|
|
if ($value === null || $value === '') {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return (string) \Carbon\Carbon::parse((string) $value)->format('Y-m-d');
|
|
} catch (\Throwable) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** @return array{totale: float, voci: array<string, float>, righe: int, scope_note?: string} */
|
|
public function getRiscaldamentoLegacyOperazioniSummaryProperty(): array
|
|
{
|
|
if (! Schema::connection('gescon_import')->hasTable('operazioni')) {
|
|
return ['totale' => 0.0, 'voci' => [], 'righe' => 0, 'scope_note' => 'Archivio legacy non disponibile.'];
|
|
}
|
|
|
|
if (! Schema::connection('gescon_import')->hasColumn('operazioni', 'cod_stabile')) {
|
|
$rows = $this->riscaldamentoLegacyOperazioniRows;
|
|
$voci = [];
|
|
$totale = 0.0;
|
|
|
|
foreach ($rows as $row) {
|
|
$cod = strtoupper(trim((string) ($row['cod_spe'] ?? '')));
|
|
if ($cod === '') {
|
|
continue;
|
|
}
|
|
|
|
$importo = (float) ($row['importo'] ?? 0);
|
|
$voci[$cod] = round(($voci[$cod] ?? 0.0) + $importo, 2);
|
|
$totale += $importo;
|
|
}
|
|
|
|
return [
|
|
'totale' => round($totale, 2),
|
|
'voci' => $voci,
|
|
'righe' => count($rows),
|
|
'scope_note' => 'Riepilogo filtrato solo sulle righe legacy collegate alle fatture locali dello stabile corrente.',
|
|
];
|
|
}
|
|
|
|
$legacyCodes = $this->resolveLegacyCodiciStabile();
|
|
$year = $this->resolveActiveAnnoGestione();
|
|
|
|
$query = DB::connection('gescon_import')
|
|
->table('operazioni')
|
|
->whereIn('gestione', ['O', 'R'])
|
|
->where('cod_spe', 'like', 'R%')
|
|
->where('cod_spe', '!=', 'RAO');
|
|
|
|
if ($legacyCodes !== [] && Schema::connection('gescon_import')->hasColumn('operazioni', 'cod_stabile')) {
|
|
$query->whereIn('cod_stabile', $legacyCodes);
|
|
}
|
|
|
|
if (Schema::connection('gescon_import')->hasColumn('operazioni', 'dt_spe')) {
|
|
$query->whereYear('dt_spe', $year);
|
|
}
|
|
|
|
$columns = Schema::connection('gescon_import')->getColumnListing('operazioni');
|
|
$sumFields = [];
|
|
if (in_array('importo_euro', $columns, true)) {
|
|
$sumFields[] = 'importo_euro';
|
|
}
|
|
if (in_array('importo', $columns, true)) {
|
|
$sumFields[] = 'importo';
|
|
}
|
|
$sumSql = '0';
|
|
if ($sumFields !== []) {
|
|
$sumSql = 'COALESCE(' . implode(', ', $sumFields) . ', 0)';
|
|
}
|
|
|
|
$rows = $query
|
|
->select('cod_spe')
|
|
->selectRaw('COUNT(*) as righe')
|
|
->selectRaw("SUM($sumSql) as totale")
|
|
->groupBy('cod_spe')
|
|
->get();
|
|
|
|
$voci = [];
|
|
$totale = 0.0;
|
|
$righe = 0;
|
|
foreach ($rows as $row) {
|
|
$cod = strtoupper(trim((string) ($row->cod_spe ?? '')));
|
|
if ($cod === '') {
|
|
continue;
|
|
}
|
|
|
|
$imp = (float) ($row->totale ?? 0);
|
|
$voci[$cod] = round($imp, 2);
|
|
$totale += $imp;
|
|
$righe += (int) ($row->righe ?? 0);
|
|
}
|
|
|
|
return [
|
|
'totale' => round($totale, 2),
|
|
'voci' => $voci,
|
|
'righe' => $righe,
|
|
'scope_note' => 'Riepilogo filtrato per stabile e anno sull archivio legacy.',
|
|
];
|
|
}
|
|
|
|
/** @return array<int, array<string,mixed>> */
|
|
public function getRiscaldamentoLegacyOperazioniRowsProperty(): array
|
|
{
|
|
if (! Schema::connection('gescon_import')->hasTable('operazioni')) {
|
|
return [];
|
|
}
|
|
|
|
$stabileId = $this->resolveActiveStabileId();
|
|
if (! $stabileId) {
|
|
return [];
|
|
}
|
|
|
|
$legacyCodes = $this->resolveLegacyCodiciStabile();
|
|
$year = $this->resolveActiveAnnoGestione();
|
|
$hasCodStabile = Schema::connection('gescon_import')->hasColumn('operazioni', 'cod_stabile');
|
|
|
|
$query = DB::connection('gescon_import')
|
|
->table('operazioni')
|
|
->whereIn('gestione', ['O', 'R'])
|
|
->where('cod_spe', 'like', 'R%')
|
|
->where('cod_spe', '!=', 'RAO');
|
|
|
|
if ($legacyCodes !== [] && $hasCodStabile) {
|
|
$query->whereIn('cod_stabile', $legacyCodes);
|
|
}
|
|
|
|
if (! $hasCodStabile && Schema::connection('gescon_import')->hasColumn('operazioni', 'cod_for')) {
|
|
$legacySupplierCodes = $this->resolveLegacyOperazioniSupplierCodesForCurrentStabile();
|
|
if ($legacySupplierCodes === []) {
|
|
return [];
|
|
}
|
|
|
|
$query->whereIn('cod_for', $legacySupplierCodes);
|
|
}
|
|
|
|
if (Schema::connection('gescon_import')->hasColumn('operazioni', 'dt_spe')) {
|
|
$query->whereYear('dt_spe', $year);
|
|
}
|
|
|
|
$legacyRows = $query
|
|
->orderByDesc('dt_spe')
|
|
->orderByDesc('id_operaz')
|
|
->limit(120)
|
|
->get([
|
|
'id_operaz',
|
|
'n_spe',
|
|
'dt_spe',
|
|
'cod_spe',
|
|
'cod_for',
|
|
'num_fat',
|
|
'dt_fat',
|
|
'benef',
|
|
'importo_euro',
|
|
'importo',
|
|
'fe_uid',
|
|
]);
|
|
|
|
if ($legacyRows->isEmpty()) {
|
|
return [];
|
|
}
|
|
|
|
$codForList = $legacyRows
|
|
->pluck('cod_for')
|
|
->filter()
|
|
->map(fn($v) => trim((string) $v))
|
|
->filter()
|
|
->unique()
|
|
->values()
|
|
->all();
|
|
|
|
$codForNoZero = array_values(array_filter(array_unique(array_map(fn($v) => ltrim((string) $v, '0'), $codForList))));
|
|
$codForAll = array_values(array_unique(array_merge($codForList, $codForNoZero)));
|
|
|
|
$legacyFornitoriMap = [];
|
|
$codFornToLegacyId = [];
|
|
if (! empty($codForAll)) {
|
|
$useStg = Schema::hasTable('stg_fornitori_gescon');
|
|
if ($useStg) {
|
|
$fornRows = DB::table('stg_fornitori_gescon')
|
|
->whereIn('cod_forn', $codForAll)
|
|
->get(['cod_forn', 'legacy_id_fornitore', 'denominazione', 'cognome', 'nome']);
|
|
} else {
|
|
$stagingConn = DB::connection('gescon_import');
|
|
$tableName = $stagingConn->getSchemaBuilder()->hasTable('mdb_fornitori') ? 'mdb_fornitori' : 'fornitori';
|
|
$hasIdForn = $stagingConn->getSchemaBuilder()->hasColumn($tableName, 'id_fornitore');
|
|
$hasDenom = $stagingConn->getSchemaBuilder()->hasColumn($tableName, 'denominazione');
|
|
$hasRagione = $stagingConn->getSchemaBuilder()->hasColumn($tableName, 'ragione_sociale');
|
|
|
|
$selectFields = ['cod_forn'];
|
|
if ($hasIdForn) {
|
|
$selectFields[] = 'id_fornitore';
|
|
} else {
|
|
$selectFields[] = 'id as id_fornitore';
|
|
}
|
|
if ($hasDenom) {
|
|
$selectFields[] = 'denominazione';
|
|
} elseif ($hasRagione) {
|
|
$selectFields[] = 'ragione_sociale as denominazione';
|
|
}
|
|
if ($stagingConn->getSchemaBuilder()->hasColumn($tableName, 'cognome')) {
|
|
$selectFields[] = 'cognome';
|
|
}
|
|
if ($stagingConn->getSchemaBuilder()->hasColumn($tableName, 'nome')) {
|
|
$selectFields[] = 'nome';
|
|
}
|
|
|
|
$fornRows = $stagingConn->table($tableName)
|
|
->whereIn('cod_forn', $codForAll)
|
|
->get($selectFields);
|
|
}
|
|
|
|
foreach ($fornRows as $f) {
|
|
$code = trim((string) ($f->cod_forn ?? ''));
|
|
if ($code === '') {
|
|
continue;
|
|
}
|
|
|
|
$label = trim((string) ($f->denominazione ?? ''));
|
|
if ($label === '') {
|
|
$label = trim((string) (($f->nome ?? '') . ' ' . ($f->cognome ?? '')));
|
|
}
|
|
|
|
$legacyId = $useStg ? ($f->cod_forn ?? null) : ($f->id_fornitore ?? null);
|
|
$legacyFornitoriMap[$code] = $label !== '' ? $label : null;
|
|
$codFornToLegacyId[$code] = $legacyId;
|
|
|
|
$noZero = ltrim($code, '0');
|
|
if ($noZero !== '') {
|
|
$legacyFornitoriMap[$noZero] = $legacyFornitoriMap[$code];
|
|
$codFornToLegacyId[$noZero] = $legacyId;
|
|
}
|
|
}
|
|
}
|
|
|
|
$legacyIds = array_values(array_filter(array_unique(array_values($codFornToLegacyId))));
|
|
$fornitoriIdMap = [];
|
|
if (! empty($legacyIds) && Schema::hasTable('fornitori')) {
|
|
$fornitoriRows = DB::table('fornitori')
|
|
->whereIn('old_id', $legacyIds)
|
|
->get(['id', 'old_id']);
|
|
|
|
foreach ($fornitoriRows as $fr) {
|
|
$fornitoriIdMap[(string) ($fr->old_id ?? '')] = (int) ($fr->id ?? 0);
|
|
}
|
|
}
|
|
|
|
$fattureByFornitore = [];
|
|
$fattureByStrict = [];
|
|
$fattureByGlobal = [];
|
|
if (! empty($fornitoriIdMap) && Schema::hasTable('contabilita_fatture_fornitori')) {
|
|
$fornitoreIds = array_values(array_unique(array_filter(array_values($fornitoriIdMap))));
|
|
if (! empty($fornitoreIds)) {
|
|
$fatture = DB::table('contabilita_fatture_fornitori')
|
|
->where('stabile_id', $stabileId)
|
|
->whereIn('fornitore_id', $fornitoreIds)
|
|
->get([
|
|
'id',
|
|
'fornitore_id',
|
|
'fattura_elettronica_id',
|
|
'numero_documento',
|
|
'data_documento',
|
|
'totale',
|
|
'netto_da_pagare',
|
|
]);
|
|
|
|
foreach ($fatture as $f) {
|
|
$fid = (int) ($f->fornitore_id ?? 0);
|
|
if ($fid <= 0) {
|
|
continue;
|
|
}
|
|
|
|
$fattureByFornitore[$fid][] = $f;
|
|
$docNumber = $this->normalizeInvoiceNumber($f->numero_documento ?? null);
|
|
$docDate = $this->parseLegacyDate($f->data_documento ?? null);
|
|
if ($docNumber && $docDate) {
|
|
$strictKey = $fid . '|' . $docNumber . '|' . $docDate;
|
|
$fattureByStrict[$strictKey][] = $f;
|
|
$globalKey = $docNumber . '|' . $docDate;
|
|
$fattureByGlobal[$globalKey][] = $f;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$rows = $legacyRows->map(function ($row) use ($legacyFornitoriMap, $codFornToLegacyId, $fornitoriIdMap, $fattureByFornitore, $fattureByStrict, $fattureByGlobal): array {
|
|
$codFor = trim((string) ($row->cod_for ?? ''));
|
|
$codForNoZero = ltrim($codFor, '0');
|
|
$legacyId = $codFornToLegacyId[$codFor] ?? ($codForNoZero !== '' ? ($codFornToLegacyId[$codForNoZero] ?? null) : null);
|
|
$fornitoreId = ($legacyId !== null && isset($fornitoriIdMap[(string) $legacyId])) ? (int) $fornitoriIdMap[(string) $legacyId] : null;
|
|
$fornitoreLegacy = $legacyFornitoriMap[$codFor] ?? ($codForNoZero !== '' ? ($legacyFornitoriMap[$codForNoZero] ?? null) : null);
|
|
|
|
$numFatNorm = $this->normalizeInvoiceNumber($row->num_fat ?? null);
|
|
$dtFatIso = $this->parseLegacyDate($row->dt_fat ?? null);
|
|
$importo = (float) ($row->importo_euro ?? $row->importo ?? 0);
|
|
|
|
$matched = null;
|
|
$matchType = 'none';
|
|
|
|
if ($fornitoreId && $numFatNorm && $dtFatIso) {
|
|
$key = $fornitoreId . '|' . $numFatNorm . '|' . $dtFatIso;
|
|
if (! empty($fattureByStrict[$key])) {
|
|
$matched = $fattureByStrict[$key][0];
|
|
$matchType = 'strict';
|
|
}
|
|
}
|
|
|
|
if (! $matched && $numFatNorm && $dtFatIso) {
|
|
$gKey = $numFatNorm . '|' . $dtFatIso;
|
|
$globalMatches = $fattureByGlobal[$gKey] ?? [];
|
|
if (count($globalMatches) === 1) {
|
|
$matched = $globalMatches[0];
|
|
$matchType = 'global';
|
|
}
|
|
}
|
|
|
|
if (! $matched && $fornitoreId && ! empty($fattureByFornitore[$fornitoreId])) {
|
|
foreach ($fattureByFornitore[$fornitoreId] as $f) {
|
|
$tot = (float) ($f->totale ?? 0);
|
|
$net = (float) ($f->netto_da_pagare ?? 0);
|
|
if (abs(abs($importo) - abs($tot)) <= 0.01 || abs(abs($importo) - abs($net)) <= 0.01) {
|
|
$matched = $f;
|
|
$matchType = 'amount';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return [
|
|
'id_operaz' => (int) ($row->id_operaz ?? 0),
|
|
'n_spe' => isset($row->n_spe) ? (int) $row->n_spe : null,
|
|
'data_operazione' => $this->parseLegacyDate($row->dt_spe ?? null),
|
|
'cod_spe' => strtoupper(trim((string) ($row->cod_spe ?? ''))),
|
|
'cod_for' => $codFor,
|
|
'fornitore_legacy' => $fornitoreLegacy,
|
|
'num_fat' => (string) ($row->num_fat ?? ''),
|
|
'dt_fat' => $dtFatIso,
|
|
'benef' => (string) ($row->benef ?? ''),
|
|
'importo' => round($importo, 2),
|
|
'fe_uid' => (string) ($row->fe_uid ?? ''),
|
|
'match_type' => $matchType,
|
|
'matched_fattura_id' => $matched ? (int) ($matched->id ?? 0) : null,
|
|
'matched_fe_id' => $matched ? ((int) ($matched->fattura_elettronica_id ?? 0) ?: null): null,
|
|
'matched_numero' => $matched ? (string) ($matched->numero_documento ?? '') : null,
|
|
'matched_data' => $matched ? $this->parseLegacyDate($matched->data_documento ?? null) : null,
|
|
'matched_totale' => $matched ? (float) ($matched->totale ?? $matched->netto_da_pagare ?? 0) : null,
|
|
];
|
|
});
|
|
|
|
if (! $hasCodStabile) {
|
|
$rows = $rows->filter(fn(array $row): bool => (int) ($row['matched_fattura_id'] ?? 0) > 0);
|
|
}
|
|
|
|
return $rows->values()->all();
|
|
}
|
|
|
|
/** @return array<int, string> */
|
|
private function resolveLegacyOperazioniSupplierCodesForCurrentStabile(): array
|
|
{
|
|
if (! Schema::hasTable('fornitori')) {
|
|
return [];
|
|
}
|
|
|
|
$stabileId = $this->resolveActiveStabileId();
|
|
if (! $stabileId) {
|
|
return [];
|
|
}
|
|
|
|
$fornitoreIds = collect($this->resolveRiscaldamentoCandidateInvoices())
|
|
->pluck('fornitore_id')
|
|
->map(fn($value): int => (int) $value)
|
|
->filter(fn(int $value): bool => $value > 0)
|
|
->unique()
|
|
->values()
|
|
->all();
|
|
|
|
if ($fornitoreIds === []) {
|
|
$fornitoreIds = DB::table('contabilita_fatture_fornitori')
|
|
->where('stabile_id', $stabileId)
|
|
->whereNotNull('fornitore_id')
|
|
->pluck('fornitore_id')
|
|
->map(fn($value): int => (int) $value)
|
|
->filter(fn(int $value): bool => $value > 0)
|
|
->unique()
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
if ($fornitoreIds === []) {
|
|
return [];
|
|
}
|
|
|
|
$legacyIds = DB::table('fornitori')
|
|
->whereIn('id', $fornitoreIds)
|
|
->whereNotNull('old_id')
|
|
->pluck('old_id')
|
|
->map(fn($value): string => trim((string) $value))
|
|
->filter(fn(string $value): bool => $value !== '')
|
|
->unique()
|
|
->values()
|
|
->all();
|
|
|
|
if ($legacyIds === []) {
|
|
return [];
|
|
}
|
|
|
|
$useStg = Schema::hasTable('stg_fornitori_gescon');
|
|
if ($useStg) {
|
|
$rows = DB::table('stg_fornitori_gescon')->whereIn('legacy_id_fornitore', $legacyIds)->get(['cod_forn']);
|
|
} else {
|
|
$stagingConn = DB::connection('gescon_import');
|
|
$tableName = $stagingConn->getSchemaBuilder()->hasTable('mdb_fornitori') ? 'mdb_fornitori' : 'fornitori';
|
|
$idCol = $stagingConn->getSchemaBuilder()->hasColumn($tableName, 'id_fornitore') ? 'id_fornitore' : 'id';
|
|
$rows = $stagingConn->table($tableName)->whereIn($idCol, $legacyIds)->get(['cod_forn']);
|
|
}
|
|
|
|
return $rows
|
|
->pluck('cod_forn')
|
|
->map(fn($value): string => trim((string) $value))
|
|
->filter(fn(string $value): bool => $value !== '')
|
|
->unique()
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
/** @return array{servizio_id: int|null, servizio_nome: string|null, fornitore_id: int|null, fornitore_nome: string|null, codice_utenza: string|null, codice_cliente: string|null, codice_contratto: string|null, matricola: string|null, suggerito_fornitore: string|null} */
|
|
public function getRiscaldamentoContrattoStabileProperty(): array
|
|
{
|
|
$stabileId = $this->resolveActiveStabileId();
|
|
if (! $stabileId) {
|
|
return [
|
|
'servizio_id' => null,
|
|
'servizio_nome' => null,
|
|
'fornitore_id' => null,
|
|
'fornitore_nome' => null,
|
|
'codice_utenza' => null,
|
|
'codice_cliente' => null,
|
|
'codice_contratto' => null,
|
|
'matricola' => null,
|
|
'suggerito_fornitore' => null,
|
|
];
|
|
}
|
|
|
|
$servizio = StabileServizio::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('tipo', 'riscaldamento')
|
|
->with('fornitore:id,ragione_sociale')
|
|
->orderByDesc('attivo')
|
|
->orderBy('id')
|
|
->first();
|
|
|
|
$fornitoreSuggerito = $this->resolvePreferredRiscaldamentoSupplier($stabileId);
|
|
|
|
return [
|
|
'servizio_id' => $servizio?->id,
|
|
'servizio_nome' => $servizio?->nome,
|
|
'fornitore_id' => $servizio?->fornitore_id,
|
|
'fornitore_nome' => $servizio?->fornitore?->ragione_sociale,
|
|
'codice_utenza' => $servizio?->codice_utenza,
|
|
'codice_cliente' => $servizio?->codice_cliente,
|
|
'codice_contratto' => $servizio?->codice_contratto,
|
|
'matricola' => $servizio?->contatore_matricola,
|
|
'suggerito_fornitore' => $fornitoreSuggerito?->ragione_sociale,
|
|
];
|
|
}
|
|
|
|
/** @return array<int, array{id_cond:int|null, ruolo:string, nominativo:string, scala:string, interno:string, quota_euro:float, lettura_iniziale:?float, consumo_mc:?float}> */
|
|
public function getRiscaldamentoRipartoNominativiLegacyProperty(): array
|
|
{
|
|
if (! Schema::connection('gescon_import')->hasTable('dett_tab') || ! Schema::connection('gescon_import')->hasTable('condomin')) {
|
|
return [];
|
|
}
|
|
|
|
$legacyCodes = $this->resolveLegacyCodiciStabile();
|
|
if ($legacyCodes === []) {
|
|
return [];
|
|
}
|
|
|
|
$legacyYear = (string) $this->resolveActiveAnnoGestione();
|
|
|
|
$condQuery = DB::connection('gescon_import')
|
|
->table('condomin')
|
|
->whereIn('cod_stabile', $legacyCodes);
|
|
|
|
if ($legacyYear !== '' && Schema::connection('gescon_import')->hasColumn('condomin', 'legacy_year')) {
|
|
$condQuery->where('legacy_year', $legacyYear);
|
|
}
|
|
|
|
$condominCols = Schema::connection('gescon_import')->getColumnListing('condomin');
|
|
$desired = [
|
|
'id_cond',
|
|
'scala',
|
|
'interno',
|
|
'cognome',
|
|
'nome',
|
|
'proprietario_denominazione',
|
|
'inquilino_denominazione',
|
|
'inquil_nome',
|
|
];
|
|
$actual = array_intersect($desired, $condominCols);
|
|
$condRows = $condQuery->get($actual)
|
|
->keyBy('id_cond');
|
|
|
|
$rowsQuery = DB::connection('gescon_import')
|
|
->table('dett_tab')
|
|
->where('cod_tab', 'RISCALDAMENTO')
|
|
->whereIn('cod_stabile', $legacyCodes);
|
|
|
|
if ($legacyYear !== '' && Schema::connection('gescon_import')->hasColumn('dett_tab', 'legacy_year')) {
|
|
$rowsQuery->where('legacy_year', $legacyYear);
|
|
}
|
|
|
|
$rows = $rowsQuery
|
|
->orderBy('id_cond')
|
|
->orderBy('cond_inquil')
|
|
->get(['id_cond', 'cond_inquil', 'cons_euro']);
|
|
|
|
$stabileId = $this->resolveActiveStabileId();
|
|
|
|
$letturaByUi = [];
|
|
if ($stabileId) {
|
|
$lettureRows = StabileServizioLettura::query()
|
|
->where('stabile_id', $stabileId)
|
|
->whereHas('servizio', fn(Builder $q) => $q->where('tipo', 'riscaldamento'))
|
|
->whereNotNull('unita_immobiliare_id')
|
|
->orderByDesc('created_at')
|
|
->get(['unita_immobiliare_id', 'lettura_inizio', 'consumo_valore']);
|
|
|
|
foreach ($lettureRows as $lr) {
|
|
$uiId = (int) ($lr->unita_immobiliare_id ?? 0);
|
|
if ($uiId <= 0 || isset($letturaByUi[$uiId])) {
|
|
continue;
|
|
}
|
|
|
|
$letturaByUi[$uiId] = [
|
|
'lettura_iniziale' => is_numeric($lr->lettura_inizio) ? (float) $lr->lettura_inizio : null,
|
|
'consumo_mc' => is_numeric($lr->consumo_valore) ? (float) $lr->consumo_valore : null,
|
|
];
|
|
}
|
|
}
|
|
|
|
$uiByLegacyCond = [];
|
|
$uiByScalaInt = [];
|
|
if ($stabileId && Schema::hasTable('unita_immobiliari')) {
|
|
$uiRows = DB::table('unita_immobiliari')
|
|
->where('stabile_id', $stabileId)
|
|
->get(['id', 'legacy_cond_id', 'scala', 'interno']);
|
|
|
|
foreach ($uiRows as $ui) {
|
|
$uiId = (int) ($ui->id ?? 0);
|
|
if ($uiId <= 0) {
|
|
continue;
|
|
}
|
|
|
|
if (is_numeric($ui->legacy_cond_id ?? null)) {
|
|
$uiByLegacyCond[(int) $ui->legacy_cond_id] = $uiId;
|
|
}
|
|
|
|
$scala = strtoupper(trim((string) ($ui->scala ?? '')));
|
|
$interno = strtoupper(trim((string) ($ui->interno ?? '')));
|
|
if ($scala !== '' || $interno !== '') {
|
|
$uiByScalaInt[$scala . '|' . $interno] = $uiId;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $rows->map(function ($r) use ($condRows, $uiByLegacyCond, $uiByScalaInt, $letturaByUi): ?array {
|
|
$cond = $condRows[$r->id_cond] ?? null;
|
|
$ruolo = strtoupper(trim((string) ($r->cond_inquil ?? '')));
|
|
$nominativo = '';
|
|
if ($cond) {
|
|
if ($ruolo === 'I') {
|
|
$nominativo = trim((string) ($cond->inquilino_denominazione ?? ''));
|
|
if ($nominativo === '') {
|
|
$nominativo = trim((string) ($cond->inquil_nome ?? ''));
|
|
}
|
|
} else {
|
|
$nominativo = trim((string) ($cond->proprietario_denominazione ?? ''));
|
|
if ($nominativo === '') {
|
|
$nominativo = trim((string) (($cond->cognome ?? '') . ' ' . ($cond->nome ?? '')));
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($nominativo === '') {
|
|
return null;
|
|
}
|
|
|
|
$uiId = null;
|
|
if (is_numeric($r->id_cond ?? null)) {
|
|
$uiId = $uiByLegacyCond[(int) $r->id_cond] ?? null;
|
|
}
|
|
|
|
if (! $uiId) {
|
|
$scala = strtoupper(trim((string) ($cond->scala ?? '')));
|
|
$interno = strtoupper(trim((string) ($cond->interno ?? '')));
|
|
$uiId = $uiByScalaInt[$scala . '|' . $interno] ?? null;
|
|
}
|
|
|
|
$lettura = ($uiId && isset($letturaByUi[$uiId]))
|
|
? $letturaByUi[$uiId]
|
|
: ['lettura_iniziale' => null, 'consumo_mc' => null];
|
|
|
|
return [
|
|
'id_cond' => is_numeric($r->id_cond ?? null) ? (int) $r->id_cond : null,
|
|
'ruolo' => $ruolo !== '' ? $ruolo : 'C',
|
|
'nominativo' => $nominativo,
|
|
'scala' => trim((string) ($cond->scala ?? '')),
|
|
'interno' => trim((string) ($cond->interno ?? '')),
|
|
'quota_euro' => round((float) ($r->cons_euro ?? 0), 2),
|
|
'lettura_iniziale' => isset($lettura['lettura_iniziale']) && is_numeric($lettura['lettura_iniziale']) ? (float) $lettura['lettura_iniziale'] : null,
|
|
'consumo_mc' => isset($lettura['consumo_mc']) && is_numeric($lettura['consumo_mc']) ? (float) $lettura['consumo_mc'] : null,
|
|
];
|
|
})->filter()->values()->all();
|
|
}
|
|
|
|
/** @return array<int, array{gestione: string, fatture_count: int, totale_fatture: float}> */
|
|
public function getRiscaldamentoFatturePerGestioneProperty(): array
|
|
{
|
|
$stabileId = $this->resolveActiveStabileId();
|
|
if (! $stabileId) {
|
|
return [];
|
|
}
|
|
|
|
$fatture = $this->resolveRiscaldamentoCandidateInvoices();
|
|
if ($fatture->isEmpty()) {
|
|
return [];
|
|
}
|
|
|
|
$fattureById = $fatture->keyBy(fn(FatturaElettronica $fattura): int => (int) $fattura->id);
|
|
$fatturaIds = $fattureById->keys()->map(fn($value): int => (int) $value)->all();
|
|
$rowsByFatturaId = collect($this->riscaldamentoFeEstratteRows)->keyBy(fn(array $row): int => (int) ($row['fattura_id'] ?? 0));
|
|
|
|
$enrichGroup = function (array $row, array $ids) use ($rowsByFatturaId): array {
|
|
$details = collect($ids)
|
|
->map(fn(int $fatturaId): ?array=> $rowsByFatturaId->get($fatturaId))
|
|
->filter(fn(?array $detail): bool => is_array($detail));
|
|
|
|
$row['totale_mc'] = round((float) $details->sum(fn(array $detail): float => (float) ($detail['totale_mc'] ?? 0)), 3);
|
|
$row['contratti'] = $details->pluck('codice_contratto')->filter()->unique()->values()->all();
|
|
$row['utenze'] = $details->pluck('codice_utenza')->filter()->unique()->values()->all();
|
|
$row['matricole'] = $details->pluck('matricola')->filter()->unique()->values()->all();
|
|
$row['cbill_codes'] = $details->pluck('cbill')->filter()->unique()->values()->all();
|
|
|
|
return $row;
|
|
};
|
|
|
|
if (Schema::hasTable('contabilita_fatture_fornitori')) {
|
|
$grouped = [];
|
|
$seen = [];
|
|
$groupIds = [];
|
|
|
|
$contabili = DB::table('contabilita_fatture_fornitori as f')
|
|
->leftJoin('gestioni_contabili as g', 'g.id', '=', 'f.gestione_id')
|
|
->where('f.stabile_id', $stabileId)
|
|
->whereIn('f.fattura_elettronica_id', $fatturaIds)
|
|
->get(['f.fattura_elettronica_id', 'g.tipo_gestione']);
|
|
|
|
foreach ($contabili as $row) {
|
|
$fatturaId = (int) ($row->fattura_elettronica_id ?? 0);
|
|
if ($fatturaId <= 0 || ! isset($fattureById[$fatturaId])) {
|
|
continue;
|
|
}
|
|
|
|
$gestione = strtolower(trim((string) ($row->tipo_gestione ?? '')));
|
|
if ($gestione === '') {
|
|
$gestione = 'selezione fe riscaldamento';
|
|
}
|
|
|
|
$bucket = $gestione . '|' . $fatturaId;
|
|
if (isset($seen[$bucket])) {
|
|
continue;
|
|
}
|
|
|
|
if (! isset($grouped[$gestione])) {
|
|
$grouped[$gestione] = [
|
|
'gestione' => $gestione,
|
|
'fatture_count' => 0,
|
|
'totale_fatture' => 0.0,
|
|
'fe_count' => 0,
|
|
'manual_count' => 0,
|
|
];
|
|
}
|
|
|
|
$grouped[$gestione]['fatture_count']++;
|
|
$grouped[$gestione]['fe_count']++;
|
|
$grouped[$gestione]['totale_fatture'] += (float) ($fattureById[$fatturaId]->totale ?? 0);
|
|
$seen[$bucket] = true;
|
|
$groupIds[$gestione][] = $fatturaId;
|
|
}
|
|
|
|
$matchedIds = array_values(array_unique(array_map(static fn(string $bucket): int => (int) explode('|', $bucket)[1], array_keys($seen))));
|
|
foreach ($fatture as $fattura) {
|
|
if (in_array((int) $fattura->id, $matchedIds, true)) {
|
|
continue;
|
|
}
|
|
|
|
if (! isset($grouped['selezione fe riscaldamento'])) {
|
|
$grouped['selezione fe riscaldamento'] = [
|
|
'gestione' => 'selezione fe riscaldamento',
|
|
'fatture_count' => 0,
|
|
'totale_fatture' => 0.0,
|
|
'fe_count' => 0,
|
|
'manual_count' => 0,
|
|
];
|
|
}
|
|
|
|
$grouped['selezione fe riscaldamento']['fatture_count']++;
|
|
$grouped['selezione fe riscaldamento']['fe_count']++;
|
|
$grouped['selezione fe riscaldamento']['totale_fatture'] += (float) ($fattura->totale ?? 0);
|
|
$groupIds['selezione fe riscaldamento'][] = (int) $fattura->id;
|
|
}
|
|
|
|
if ($grouped !== []) {
|
|
$out = array_values($grouped);
|
|
usort($out, fn($left, $right) => strcmp((string) $left['gestione'], (string) $right['gestione']));
|
|
foreach ($out as &$row) {
|
|
$row['totale_fatture'] = round((float) $row['totale_fatture'], 2);
|
|
$row = $enrichGroup($row, array_values(array_unique(array_map('intval', $groupIds[(string) $row['gestione']] ?? []))));
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
}
|
|
|
|
return [$enrichGroup([
|
|
'gestione' => 'selezione fe riscaldamento',
|
|
'fatture_count' => $fatture->count(),
|
|
'totale_fatture' => round((float) $fatture->sum(fn(FatturaElettronica $fattura): float => (float) ($fattura->totale ?? 0)), 2),
|
|
'fe_count' => $fatture->count(),
|
|
'manual_count' => 0,
|
|
], $fatturaIds)];
|
|
}
|
|
|
|
/** @return array{fatture:int,periodi:int,totale_mc:float,totale_spesa:float,con_finestra:int,ultima_data:?string} */
|
|
public function getRiscaldamentoFeEstratteSummaryProperty(): array
|
|
{
|
|
$rows = $this->riscaldamentoFeEstratteRows;
|
|
|
|
if ($rows === []) {
|
|
return [
|
|
'fatture' => 0,
|
|
'periodi' => 0,
|
|
'totale_mc' => 0.0,
|
|
'totale_spesa' => 0.0,
|
|
'con_finestra' => 0,
|
|
'ultima_data' => null,
|
|
];
|
|
}
|
|
|
|
$dateValues = array_values(array_filter(array_map(
|
|
static fn(array $row): ?string => is_string($row['data_fattura_iso'] ?? null) && $row['data_fattura_iso'] !== ''
|
|
? (string) $row['data_fattura_iso']
|
|
: null,
|
|
$rows,
|
|
)));
|
|
|
|
return [
|
|
'fatture' => count($rows),
|
|
'periodi' => (int) array_sum(array_map(static fn(array $row): int => (int) ($row['periodi_count'] ?? 0), $rows)),
|
|
'totale_mc' => round((float) array_sum(array_map(static fn(array $row): float => (float) ($row['totale_mc'] ?? 0), $rows)), 3),
|
|
'totale_spesa' => round((float) array_sum(array_map(static fn(array $row): float => (float) ($row['totale_spesa'] ?? 0), $rows)), 2),
|
|
'con_finestra' => count(array_filter($rows, static fn(array $row): bool => (bool) ($row['ha_finestra'] ?? false))),
|
|
'ultima_data' => $dateValues !== [] ? max($dateValues) : null,
|
|
];
|
|
}
|
|
|
|
/** @return array{fatture:int,totale_spesa:float,totale_mc:float} */
|
|
public function getRiscaldamentoFeSelezioneSummaryProperty(): array
|
|
{
|
|
$selectedIds = $this->normalizeRiscaldamentoSelectedFeIds();
|
|
$rows = collect($this->riscaldamentoFeEstratteRows)
|
|
->when($selectedIds !== [], fn($collection) => $collection->whereIn('fattura_id', $selectedIds));
|
|
|
|
return [
|
|
'fatture' => $rows->count(),
|
|
'totale_spesa' => round((float) $rows->sum(fn(array $row): float => (float) ($row['totale_spesa'] ?? 0)), 2),
|
|
'totale_mc' => round((float) $rows->sum(fn(array $row): float => (float) ($row['totale_mc'] ?? 0)), 3),
|
|
];
|
|
}
|
|
|
|
/** @return array<int, array<string,mixed>> */
|
|
public function getRiscaldamentoFeEstratteRowsProperty(): array
|
|
{
|
|
$stabileId = $this->resolveActiveStabileId();
|
|
if (! $stabileId) {
|
|
return [];
|
|
}
|
|
|
|
$servizi = StabileServizio::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('tipo', 'riscaldamento')
|
|
->orderByDesc('attivo')
|
|
->orderBy('id')
|
|
->get(['id', 'nome', 'contatore_matricola', 'codice_utenza', 'codice_cliente', 'codice_contratto']);
|
|
|
|
$defaultServizio = $servizi->first();
|
|
|
|
$userId = (int) (Auth::id() ?? 0);
|
|
$selectedIds = $this->normalizeRiscaldamentoSelectedFeIds();
|
|
$fatture = $this->resolveRiscaldamentoCandidateInvoices();
|
|
|
|
if ($fatture->isEmpty()) {
|
|
return [];
|
|
}
|
|
|
|
return $fatture
|
|
->map(function (FatturaElettronica $fattura) use ($selectedIds, $servizi, $defaultServizio, $stabileId, $userId): array {
|
|
$payload = $this->resolveRiscaldamentoParsedPayload($fattura, $userId) ?? [];
|
|
$consumi = is_array($payload['consumi'] ?? null) ? $payload['consumi'] : [];
|
|
$codici = is_array($payload['codici'] ?? null) ? $payload['codici'] : [];
|
|
$contatore = is_array($payload['contatore'] ?? null) ? $payload['contatore'] : [];
|
|
$generale = is_array($payload['generale'] ?? null) ? $payload['generale'] : [];
|
|
$pagamento = is_array($payload['pagamento'] ?? null) ? $payload['pagamento'] : [];
|
|
$riepilogoLetture = is_array($payload['riepilogo_letture'] ?? null) ? $payload['riepilogo_letture'] : [];
|
|
|
|
$servizio = $this->resolveRiscaldamentoServizioForIdentifiers($servizi, $codici, $contatore, $fattura);
|
|
|
|
[$periodoDal, $periodoAl] = $this->resolveRiscaldamentoPeriodsFromPayload($consumi, is_string($fattura->consumo_riferimento ?? null) ? (string) $fattura->consumo_riferimento : null);
|
|
$windowFrom = $this->extractRiscaldamentoWindowDate($payload, ['dal', 'from', 'inizio', 'start']);
|
|
$windowTo = $this->extractRiscaldamentoWindowDate($payload, ['al', 'to', 'fine', 'end']);
|
|
|
|
$matricola = trim((string) ($contatore['matricola'] ?? $servizio?->contatore_matricola ?? ''));
|
|
$codiceUtenza = trim((string) ($codici['utenza'] ?? $servizio?->codice_utenza ?? ''));
|
|
$codiceCliente = trim((string) ($codici['cliente'] ?? $servizio?->codice_cliente ?? ''));
|
|
$codiceContratto = trim((string) ($codici['contratto'] ?? $servizio?->codice_contratto ?? ''));
|
|
$cbill = trim((string) ($pagamento['cbill'] ?? $fattura->pagamento_cbill ?? ''));
|
|
$codiceAvviso = trim((string) ($pagamento['codice_avviso'] ?? $fattura->pagamento_codice_avviso ?? ''));
|
|
$sia = trim((string) ($pagamento['sia'] ?? $fattura->pagamento_codice_sia ?? ''));
|
|
$totaleMc = $this->sumRiscaldamentoConsumi($consumi);
|
|
if ($totaleMc === null && is_numeric($fattura->consumo_valore ?? null) && strtolower(trim((string) ($fattura->consumo_unita ?? ''))) === 'mc') {
|
|
$totaleMc = round((float) $fattura->consumo_valore, 3);
|
|
}
|
|
|
|
$letturePreview = collect($riepilogoLetture)
|
|
->filter(fn($lettura): bool => is_array($lettura))
|
|
->map(function (array $lettura): string {
|
|
$tipologia = trim((string) ($lettura['tipologia'] ?? 'Lettura'));
|
|
$consumo = is_numeric($lettura['consumo'] ?? null) ? number_format((float) $lettura['consumo'], 3, ',', '.') . ' ' . trim((string) ($lettura['unita'] ?? 'mc')) : null;
|
|
$dataA = is_string($lettura['data_a'] ?? null) && trim((string) $lettura['data_a']) !== '' ? $this->formatRiscaldamentoPeriodLabel(null, (string) $lettura['data_a']) : null;
|
|
|
|
return trim(implode(' · ', array_filter([$tipologia !== '' ? $tipologia : null, $consumo, $dataA])));
|
|
})
|
|
->filter()
|
|
->take(2)
|
|
->values()
|
|
->all();
|
|
|
|
$legacyMatch = null;
|
|
$numFatNorm = $this->normalizeInvoiceNumber($fattura->numero_fattura ?? null);
|
|
$dtFatIso = optional($fattura->data_fattura)->format('Y-m-d');
|
|
$legacyOps = $this->riscaldamentoLegacyOperazioniRows;
|
|
|
|
foreach ($legacyOps as $opRow) {
|
|
$opNumFatNorm = $this->normalizeInvoiceNumber($opRow['num_fat'] ?? null);
|
|
$opDtFatIso = $opRow['data_operazione'] ?? null;
|
|
|
|
if ($numFatNorm && $opNumFatNorm && $numFatNorm === $opNumFatNorm && $dtFatIso && $opDtFatIso && $dtFatIso === $opDtFatIso) {
|
|
$legacyMatch = $opRow;
|
|
break;
|
|
}
|
|
|
|
if ($opRow['fornitore_legacy'] && $fattura->fornitore && $opRow['fornitore_legacy'] === $fattura->fornitore->ragione_sociale) {
|
|
$opImporto = (float) ($opRow['importo'] ?? 0);
|
|
if (abs(abs($opImporto) - abs((float) $fattura->totale)) <= 0.01) {
|
|
$legacyMatch = $opRow;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
$regFattura = \Illuminate\Support\Facades\DB::table('contabilita_fatture_fornitori')
|
|
->where('stabile_id', $stabileId)
|
|
->where('fattura_elettronica_id', $fattura->id)
|
|
->first();
|
|
|
|
$imp = \App\Models\FornitoreStabileImpostazione::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('fornitore_id', $fattura->fornitore_id)
|
|
->first();
|
|
|
|
$voceSpesaDefaultName = 'Non impostata';
|
|
if ($imp && $imp->voce_spesa_default_id) {
|
|
$voceSpesaDefaultName = \App\Models\VoceSpesa::query()
|
|
->where('id', $imp->voce_spesa_default_id)
|
|
->value('codice') ?: 'Impostata';
|
|
}
|
|
|
|
return [
|
|
'fattura_id' => (int) $fattura->id,
|
|
'numero_fattura' => trim((string) ($fattura->numero_fattura ?? $fattura->sdi_file ?? ('FE #' . (int) $fattura->id))),
|
|
'data_fattura' => optional($fattura->data_fattura)?->format('d/m/Y'),
|
|
'data_fattura_iso' => optional($fattura->data_fattura)?->format('Y-m-d'),
|
|
'servizio' => $servizio
|
|
? (string) $servizio->nome
|
|
: ($fattura->fornitore?->ragione_sociale ? 'Non abbinato: ' . $fattura->fornitore->ragione_sociale : 'Non abbinata / Fornitore sconosciuto'),
|
|
'servizio_id' => (int) ($servizio?->id ?? 0),
|
|
'matricola' => $matricola,
|
|
'codice_utenza' => $codiceUtenza,
|
|
'codice_cliente' => $codiceCliente,
|
|
'codice_contratto' => $codiceContratto,
|
|
'cbill' => $cbill,
|
|
'codice_avviso' => $codiceAvviso,
|
|
'sia' => $sia,
|
|
'cbill_display' => $cbill !== '' ? $cbill : ($codiceAvviso !== '' ? $codiceAvviso : ''),
|
|
'numero_componenti' => is_numeric($generale['numero_componenti_totali'] ?? null) ? (int) $generale['numero_componenti_totali'] : null,
|
|
'numero_unita_abitative' => is_numeric($generale['numero_unita_abitative'] ?? null) ? (int) $generale['numero_unita_abitative'] : null,
|
|
'periodicita_fatturazione' => trim((string) ($generale['periodicita_fatturazione'] ?? '')),
|
|
'periodo_label' => $this->formatRiscaldamentoPeriodLabel($periodoDal, $periodoAl),
|
|
'periodi_count' => count($consumi),
|
|
'letture_count' => count($riepilogoLetture),
|
|
'letture_preview' => $letturePreview,
|
|
'totale_mc' => $totaleMc ?? 0.0,
|
|
'totale_spesa' => round((float) ($fattura->totale ?? 0), 2),
|
|
'ha_finestra' => $windowFrom !== null || $windowTo !== null,
|
|
'finestra_label' => $this->formatRiscaldamentoPeriodLabel($windowFrom, $windowTo),
|
|
'selected' => in_array((int) $fattura->id, $selectedIds, true),
|
|
'identificativi_noti' => $matricola !== '' || $codiceUtenza !== '' || $codiceCliente !== '' || $codiceContratto !== '' || $cbill !== '' || $codiceAvviso !== '' || $sia !== '',
|
|
'scheda_fe_url' => FatturaElettronicaScheda::getUrl(panel: 'admin-filament', parameters: ['record' => (int) $fattura->id]),
|
|
'ticket_generale_url' => TicketRiscaldamentoGenerale::getUrl(panel: 'admin-filament', parameters: ['stabile' => $stabileId, 'servizio' => (int) ($servizio?->id ?? 0)]),
|
|
'contratti_url' => ContrattiHub::getUrl(panel: 'admin-filament'),
|
|
'letture_servizio_url' => LettureServiziArchivio::getUrl(panel: 'admin-filament', parameters: ['servizio' => (int) ($servizio?->id ?? 0)]),
|
|
|
|
'legacy_n_spe' => $legacyMatch ? ($legacyMatch['n_spe'] ?? null) : null,
|
|
'legacy_id' => $legacyMatch ? ($legacyMatch['id_operaz'] ?? null) : null,
|
|
'legacy_cod_spe' => $legacyMatch ? ($legacyMatch['cod_spe'] ?? null) : null,
|
|
'legacy_benef' => $legacyMatch ? ($legacyMatch['benef'] ?? null) : null,
|
|
'legacy_importo' => $legacyMatch ? ($legacyMatch['importo'] ?? null) : null,
|
|
'legacy_match_type' => $legacyMatch ? 'Abbinato' : 'Non abbinato',
|
|
|
|
'registrata_id' => $regFattura ? (int) $regFattura->id : null,
|
|
'registrata_stato' => $regFattura ? (string) $regFattura->stato : 'non_registrata',
|
|
'registrata_data_pagamento'=> $regFattura && $regFattura->data_pagamento ? \Carbon\Carbon::parse($regFattura->data_pagamento)->format('d/m/Y') : null,
|
|
'registrata_netto_da_pagare'=> $regFattura ? (float) $regFattura->netto_da_pagare : 0.0,
|
|
'voce_spesa_default_name' => $voceSpesaDefaultName,
|
|
];
|
|
})
|
|
->sortByDesc('data_fattura_iso')
|
|
->take(24)
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
private function decodeRiscaldamentoPayload(mixed $value): array
|
|
{
|
|
if (is_array($value)) {
|
|
return $value;
|
|
}
|
|
|
|
if (is_string($value) && trim($value) !== '') {
|
|
$decoded = json_decode($value, true);
|
|
if (is_array($decoded)) {
|
|
return $decoded;
|
|
}
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
private function extractRiscaldamentoWindowDate(array $payload, array $keys): ?string
|
|
{
|
|
$window = is_array($payload['finestra_autolettura'] ?? null) ? $payload['finestra_autolettura'] : [];
|
|
|
|
foreach ($keys as $key) {
|
|
$value = $window[$key] ?? null;
|
|
if (! is_string($value) || trim($value) === '') {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
return (string) \Carbon\Carbon::parse($value)->format('Y-m-d');
|
|
} catch (\Throwable) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function formatRiscaldamentoPeriodLabel(?string $from, ?string $to): string
|
|
{
|
|
$fromLabel = $from ? \Carbon\Carbon::parse($from)->format('d/m/Y') : null;
|
|
$toLabel = $to ? \Carbon\Carbon::parse($to)->format('d/m/Y') : null;
|
|
|
|
if ($fromLabel && $toLabel) {
|
|
return $fromLabel . ' - ' . $toLabel;
|
|
}
|
|
|
|
if ($fromLabel) {
|
|
return 'Dal ' . $fromLabel;
|
|
}
|
|
|
|
if ($toLabel) {
|
|
return 'Fino al ' . $toLabel;
|
|
}
|
|
|
|
return '—';
|
|
}
|
|
|
|
/** @param \Illuminate\Support\Collection<int, StabileServizio> $servizi */
|
|
private function resolveRiscaldamentoServizioForIdentifiers($servizi, array $codici, array $contatore, ?FatturaElettronica $fattura = null): ?StabileServizio
|
|
{
|
|
if (! $servizi instanceof \Illuminate\Support\Collection || $servizi->isEmpty()) {
|
|
return null;
|
|
}
|
|
|
|
$targetMatricola = $this->normalizeRiscaldamentoIdentifier($contatore['matricola'] ?? null);
|
|
$targetUtenza = $this->normalizeRiscaldamentoIdentifier($codici['utenza'] ?? null);
|
|
$targetCliente = $this->normalizeRiscaldamentoIdentifier($codici['cliente'] ?? null);
|
|
$targetContratto = $this->normalizeRiscaldamentoIdentifier($codici['contratto'] ?? null);
|
|
$fornitoreId = $fattura?->fornitore_id;
|
|
|
|
return $servizi
|
|
->map(function (StabileServizio $servizio) use ($targetMatricola, $targetUtenza, $targetCliente, $targetContratto, $fornitoreId): array {
|
|
$score = 0;
|
|
if ($targetMatricola !== '' && $this->normalizeRiscaldamentoIdentifier($servizio->contatore_matricola) === $targetMatricola) {
|
|
$score += 10;
|
|
}
|
|
if ($targetUtenza !== '' && $this->normalizeRiscaldamentoIdentifier($servizio->codice_utenza) === $targetUtenza) {
|
|
$score += 8;
|
|
}
|
|
if ($targetContratto !== '' && $this->normalizeRiscaldamentoIdentifier($servizio->codice_contratto) === $targetContratto) {
|
|
$score += 6;
|
|
}
|
|
if ($targetCliente !== '' && $this->normalizeRiscaldamentoIdentifier($servizio->codice_cliente) === $targetCliente) {
|
|
$score += 4;
|
|
}
|
|
if ($fornitoreId && (int) $servizio->fornitore_id === (int) $fornitoreId) {
|
|
$score += 2;
|
|
}
|
|
|
|
return ['servizio' => $servizio, 'score' => $score];
|
|
})
|
|
->sortByDesc('score')
|
|
->first(fn(array $candidate): bool => (int) ($candidate['score'] ?? 0) > 0)['servizio'] ?? null;
|
|
}
|
|
|
|
private function normalizeRiscaldamentoIdentifier(mixed $value): string
|
|
{
|
|
return strtoupper(preg_replace('/[^A-Z0-9]/', '', trim((string) $value)) ?: '');
|
|
}
|
|
|
|
/** @param array<int, array<string,mixed>> $consumi */
|
|
private function resolveRiscaldamentoPeriodsFromPayload(array $consumi, ?string $fallbackRange): array
|
|
{
|
|
$periodoDal = collect($consumi)->pluck('dal')->filter()->sort()->first();
|
|
$periodoAl = collect($consumi)->pluck('al')->filter()->sort()->last();
|
|
|
|
if (is_string($periodoDal) && $periodoDal !== '' && is_string($periodoAl) && $periodoAl !== '') {
|
|
return [$periodoDal, $periodoAl];
|
|
}
|
|
|
|
if (! is_string($fallbackRange) || trim($fallbackRange) === '') {
|
|
return [$periodoDal, $periodoAl];
|
|
}
|
|
|
|
if (preg_match('/(\d{4}-\d{2}-\d{2})\s*-\s*(\d{4}-\d{2}-\d{2})/', $fallbackRange, $matches)) {
|
|
return [$matches[1], $matches[2]];
|
|
}
|
|
|
|
return [$periodoDal, $periodoAl];
|
|
}
|
|
|
|
private function resolvePreferredRiscaldamentoSupplier(?int $stabileId = null): ?Fornitore
|
|
{
|
|
if (! Schema::hasTable('fornitori')) {
|
|
return null;
|
|
}
|
|
|
|
$serviceSupplierId = null;
|
|
if ($stabileId) {
|
|
$serviceSupplierId = StabileServizio::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('tipo', 'riscaldamento')
|
|
->whereNotNull('fornitore_id')
|
|
->orderByDesc('attivo')
|
|
->orderBy('id')
|
|
->value('fornitore_id');
|
|
}
|
|
|
|
$candidates = Fornitore::query()
|
|
->where(function (Builder $query): Builder {
|
|
return $query->whereRaw('LOWER(ragione_sociale) like ?', ['%eni%'])
|
|
->orWhereRaw('LOWER(ragione_sociale) like ?', ['%enel%'])
|
|
->orWhereRaw('LOWER(ragione_sociale) like ?', ['%gas%'])
|
|
->orWhereRaw('LOWER(ragione_sociale) like ?', ['%caldaia%'])
|
|
->orWhereRaw('LOWER(ragione_sociale) like ?', ['%combustibile%'])
|
|
->orWhereRaw('LOWER(ragione_sociale) like ?', ['%riscaldamento%']);
|
|
})
|
|
->get([
|
|
'id',
|
|
'amministratore_id',
|
|
'rubrica_id',
|
|
'ragione_sociale',
|
|
'partita_iva',
|
|
'codice_fiscale',
|
|
'old_id',
|
|
'fe_features',
|
|
]);
|
|
|
|
if ($candidates->isEmpty()) {
|
|
return null;
|
|
}
|
|
|
|
$userAdminId = null;
|
|
$user = Auth::user();
|
|
if ($user instanceof User && isset($user->amministratore_id) && is_numeric($user->amministratore_id)) {
|
|
$userAdminId = (int) $user->amministratore_id;
|
|
}
|
|
|
|
$stableInvoiceCounts = [];
|
|
if ($stabileId) {
|
|
$candidateIds = $candidates->pluck('id')
|
|
->map(fn($value) => (int) $value)
|
|
->filter(fn($value) => $value > 0)
|
|
->all();
|
|
|
|
if ($candidateIds !== []) {
|
|
$feCounts = DB::table('fatture_elettroniche')
|
|
->selectRaw('fornitore_id, COUNT(*) as aggregate_count')
|
|
->where('stabile_id', $stabileId)
|
|
->whereIn('fornitore_id', $candidateIds)
|
|
->groupBy('fornitore_id')
|
|
->pluck('aggregate_count', 'fornitore_id')
|
|
->all();
|
|
|
|
$contabiliCounts = DB::table('contabilita_fatture_fornitori')
|
|
->selectRaw('fornitore_id, COUNT(*) as aggregate_count')
|
|
->where('stabile_id', $stabileId)
|
|
->whereIn('fornitore_id', $candidateIds)
|
|
->groupBy('fornitore_id')
|
|
->pluck('aggregate_count', 'fornitore_id')
|
|
->all();
|
|
|
|
foreach ($candidateIds as $candidateId) {
|
|
$stableInvoiceCounts[$candidateId] = (int) ($feCounts[$candidateId] ?? 0) + (int) ($contabiliCounts[$candidateId] ?? 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
$sorted = $candidates->sort(function (Fornitore $left, Fornitore $right) use ($serviceSupplierId, $userAdminId, $stableInvoiceCounts): int {
|
|
$leftScore = $this->scorePreferredRiscaldamentoSupplier($left, $serviceSupplierId, $userAdminId, $stableInvoiceCounts);
|
|
$rightScore = $this->scorePreferredRiscaldamentoSupplier($right, $serviceSupplierId, $userAdminId, $stableInvoiceCounts);
|
|
|
|
if ($leftScore === $rightScore) {
|
|
return (int) $left->id <=> (int) $right->id;
|
|
}
|
|
|
|
return $rightScore <=> $leftScore;
|
|
});
|
|
|
|
return $sorted->first();
|
|
}
|
|
|
|
/** @return array<int, string> */
|
|
public function getRiscaldamentoUiServiceOptionsProperty(): array
|
|
{
|
|
$stabileId = $this->resolveActiveStabileId();
|
|
if (! $stabileId) {
|
|
return [];
|
|
}
|
|
|
|
return StabileServizio::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('tipo', 'riscaldamento')
|
|
->where('attivo', true)
|
|
->orderBy('nome')
|
|
->orderBy('contatore_matricola')
|
|
->get(['id', 'nome', 'contatore_matricola', 'codice_utenza'])
|
|
->mapWithKeys(function (StabileServizio $service): array {
|
|
$parts = array_filter([
|
|
trim((string) ($service->nome ?? '')) ?: ('Servizio #' . (int) $service->id),
|
|
trim((string) ($service->contatore_matricola ?? '')) !== '' ? 'Matr. ' . trim((string) $service->contatore_matricola) : null,
|
|
trim((string) ($service->codice_utenza ?? '')) !== '' ? 'Utenza ' . trim((string) $service->codice_utenza) : null,
|
|
]);
|
|
|
|
return [(int) $service->id => implode(' · ', $parts)];
|
|
})
|
|
->all();
|
|
}
|
|
|
|
/** @return array<int, string> */
|
|
public function getRiscaldamentoUiUnitOptionsProperty(): array
|
|
{
|
|
$stabileId = $this->resolveActiveStabileId();
|
|
if (! $stabileId) {
|
|
return [];
|
|
}
|
|
|
|
return UnitaImmobiliare::query()
|
|
->where('stabile_id', $stabileId)
|
|
->whereNull('deleted_at')
|
|
->orderBy('scala')
|
|
->orderBy('interno')
|
|
->orderBy('id')
|
|
->get(['id', 'codice_unita', 'scala', 'interno', 'denominazione'])
|
|
->mapWithKeys(function (UnitaImmobiliare $unit): array {
|
|
$label = trim(implode(' ', array_filter([
|
|
trim((string) ($unit->codice_unita ?? '')),
|
|
trim((string) ($unit->scala ?? '')) !== '' ? 'Scala ' . trim((string) $unit->scala) : null,
|
|
trim((string) ($unit->interno ?? '')) !== '' ? 'Int. ' . trim((string) $unit->interno) : null,
|
|
trim((string) ($unit->denominazione ?? '')),
|
|
])));
|
|
|
|
return [(int) $unit->id => ($label !== '' ? $label : ('UI #' . (int) $unit->id))];
|
|
})
|
|
->all();
|
|
}
|
|
|
|
public function getRiscaldamentoRipartizionePrintUrlProperty(): string
|
|
{
|
|
return $this->buildRiscaldamentoRipartizioneRoute('admin.riscaldamento-ripartizione.print');
|
|
}
|
|
|
|
public function getRiscaldamentoRipartizionePdfUrlProperty(): string
|
|
{
|
|
return $this->buildRiscaldamentoRipartizioneRoute('admin.riscaldamento-ripartizione.pdf');
|
|
}
|
|
|
|
private function buildRiscaldamentoRipartizioneRoute(string $routeName): string
|
|
{
|
|
$params = [
|
|
'stabile_id' => $this->resolveActiveStabileId(),
|
|
'year' => $this->resolveActiveAnnoGestione(),
|
|
];
|
|
|
|
$selectedIds = $this->normalizeRiscaldamentoSelectedFeIds();
|
|
if ($selectedIds !== []) {
|
|
$params['ids'] = implode(',', $selectedIds);
|
|
}
|
|
|
|
return route($routeName, $params);
|
|
}
|
|
|
|
/** @return array{touched:int,created:int,updated:int,skipped:int} */
|
|
private function propagateRiscaldamentoSettingsToOtherStabili(): array
|
|
{
|
|
$user = Auth::user();
|
|
$currentStabileId = $this->resolveActiveStabileId();
|
|
if (! $user instanceof User || ! $currentStabileId) {
|
|
return ['touched' => 0, 'created' => 0, 'updated' => 0, 'skipped' => 0];
|
|
}
|
|
|
|
$template = StabileServizio::query()
|
|
->where('stabile_id', $currentStabileId)
|
|
->where('tipo', 'riscaldamento')
|
|
->orderByDesc('attivo')
|
|
->orderBy('id')
|
|
->first();
|
|
|
|
if (! $template instanceof StabileServizio) {
|
|
return ['touched' => 0, 'created' => 0, 'updated' => 0, 'skipped' => 0];
|
|
}
|
|
|
|
$templateMeta = is_array($template->meta ?? null) ? $template->meta : [];
|
|
$targets = StabileContext::accessibleStabili($user)
|
|
->filter(fn(Stabile $stabile): bool => (int) $stabile->id !== (int) $currentStabileId)
|
|
->values();
|
|
|
|
$stats = ['touched' => 0, 'created' => 0, 'updated' => 0, 'skipped' => 0];
|
|
|
|
foreach ($targets as $target) {
|
|
$service = StabileServizio::query()
|
|
->where('stabile_id', (int) $target->id)
|
|
->where('tipo', 'riscaldamento')
|
|
->orderByDesc('attivo')
|
|
->orderBy('id')
|
|
->first();
|
|
|
|
$targetSupplier = $this->resolvePreferredRiscaldamentoSupplier((int) $target->id);
|
|
$payload = [
|
|
'tipo' => 'riscaldamento',
|
|
'nome' => trim((string) ($service?->nome ?: $template->nome ?: 'Utenza riscaldamento - Fornitore Gas/Caldaia')),
|
|
'fornitore_id' => (int) ($targetSupplier?->id ?: $service?->fornitore_id ?: $template->fornitore_id) ?: null,
|
|
'voce_spesa_id' => (int) ($service?->voce_spesa_id ?: $template->voce_spesa_id) ?: null,
|
|
'attivo' => true,
|
|
'meta' => array_merge(is_array($service?->meta ?? null) ? $service->meta : [], [
|
|
'common_asset_label' => $templateMeta['common_asset_label'] ?? null,
|
|
'common_asset_scope' => $templateMeta['common_asset_scope'] ?? null,
|
|
'allocation_scope' => $templateMeta['allocation_scope'] ?? null,
|
|
'fiscal_scope' => $templateMeta['fiscal_scope'] ?? null,
|
|
'has_dedicated_meter' => (bool) ($templateMeta['has_dedicated_meter'] ?? false),
|
|
'counter_scope' => $templateMeta['counter_scope'] ?? 'assente',
|
|
'served_area_label' => $templateMeta['served_area_label'] ?? null,
|
|
'canale_acquisizione' => $templateMeta['canale_acquisizione'] ?? null,
|
|
'email_raccolta_letture' => $templateMeta['email_raccolta_letture'] ?? null,
|
|
'whatsapp_raccolta_letture' => $templateMeta['whatsapp_raccolta_letture'] ?? null,
|
|
'costo_lettura_unitario' => $templateMeta['costo_lettura_unitario'] ?? null,
|
|
'costo_ripartizione' => $templateMeta['costo_ripartizione'] ?? null,
|
|
'ripartizione_esterna' => (bool) ($templateMeta['ripartizione_esterna'] ?? false),
|
|
'operative_notes' => $templateMeta['operative_notes'] ?? null,
|
|
'propagated_from_stabile_id' => (int) $currentStabileId,
|
|
'propagated_at' => now()->toIso8601String(),
|
|
]),
|
|
];
|
|
|
|
if ($service instanceof StabileServizio) {
|
|
$service->fill($payload);
|
|
$service->save();
|
|
$stats['updated']++;
|
|
} else {
|
|
StabileServizio::query()->create(array_merge($payload, [
|
|
'stabile_id' => (int) $target->id,
|
|
'codice_utenza' => null,
|
|
'codice_cliente' => null,
|
|
'codice_contratto' => null,
|
|
'contatore_matricola' => null,
|
|
]));
|
|
$stats['created']++;
|
|
}
|
|
|
|
$stats['touched']++;
|
|
}
|
|
|
|
return $stats;
|
|
}
|
|
|
|
/** @param array<int, int> $supplierIds */
|
|
private function resolveEquivalentFornitoreIds(array $supplierIds): array
|
|
{
|
|
$supplierIds = array_values(array_unique(array_filter(array_map('intval', $supplierIds), fn(int $value): bool => $value > 0)));
|
|
if ($supplierIds === [] || ! Schema::hasTable('fornitori')) {
|
|
return $supplierIds;
|
|
}
|
|
|
|
$suppliers = Fornitore::query()
|
|
->whereIn('id', $supplierIds)
|
|
->get(['id', 'rubrica_id', 'partita_iva', 'codice_fiscale', 'old_id']);
|
|
|
|
if ($suppliers->isEmpty()) {
|
|
return $supplierIds;
|
|
}
|
|
|
|
$rubricaIds = $suppliers->pluck('rubrica_id')
|
|
->filter(fn($value) => is_numeric($value) && (int) $value > 0)
|
|
->map(fn($value) => (int) $value)
|
|
->unique()
|
|
->values()
|
|
->all();
|
|
|
|
$partiteIva = $suppliers->pluck('partita_iva')
|
|
->map(fn($value) => trim((string) $value))
|
|
->filter(fn(string $value): bool => $value !== '')
|
|
->unique()
|
|
->values()
|
|
->all();
|
|
|
|
$codiciFiscali = $suppliers->pluck('codice_fiscale')
|
|
->map(fn($value) => trim((string) $value))
|
|
->filter(fn(string $value): bool => $value !== '')
|
|
->unique()
|
|
->values()
|
|
->all();
|
|
|
|
$legacyIds = $suppliers->pluck('old_id')
|
|
->filter(fn($value) => is_numeric($value) && (int) $value > 0)
|
|
->map(fn($value) => (int) $value)
|
|
->unique()
|
|
->values()
|
|
->all();
|
|
|
|
$matches = Fornitore::query()
|
|
->where(function (Builder $query) use ($supplierIds, $rubricaIds, $partiteIva, $codiciFiscali, $legacyIds): Builder {
|
|
$query->whereIn('id', $supplierIds);
|
|
|
|
if ($rubricaIds !== []) {
|
|
$query->orWhereIn('rubrica_id', $rubricaIds);
|
|
}
|
|
|
|
if ($partiteIva !== []) {
|
|
$query->orWhereIn('partita_iva', $partiteIva);
|
|
}
|
|
|
|
if ($codiciFiscali !== []) {
|
|
$query->orWhereIn('codice_fiscale', $codiciFiscali);
|
|
}
|
|
|
|
if ($legacyIds !== []) {
|
|
$query->orWhereIn('old_id', $legacyIds);
|
|
}
|
|
|
|
return $query;
|
|
})
|
|
->pluck('id')
|
|
->map(fn($value) => (int) $value)
|
|
->filter(fn($value) => $value > 0)
|
|
->unique()
|
|
->values()
|
|
->all();
|
|
|
|
return $matches !== [] ? $matches : $supplierIds;
|
|
}
|
|
|
|
/** @param array<int, int> $stableInvoiceCounts */
|
|
private function scorePreferredRiscaldamentoSupplier(Fornitore $supplier, ?int $serviceSupplierId, ?int $userAdminId, array $stableInvoiceCounts): int
|
|
{
|
|
$score = 0;
|
|
|
|
if ($serviceSupplierId && (int) $supplier->id === $serviceSupplierId) {
|
|
$score += 4000;
|
|
}
|
|
|
|
$waterFeatures = is_array($supplier->fe_features ?? null) ? $supplier->fe_features : [];
|
|
if ((bool) data_get($waterFeatures, 'riscaldamento.enabled', false)) {
|
|
$score += 1000;
|
|
}
|
|
|
|
if ($userAdminId && isset($supplier->amministratore_id) && (int) $supplier->amministratore_id === $userAdminId) {
|
|
$score += 500;
|
|
}
|
|
|
|
$score += ((int) ($stableInvoiceCounts[(int) $supplier->id] ?? 0)) * 25;
|
|
|
|
return $score;
|
|
}
|
|
|
|
/** @return array<int, array<string,mixed>> */
|
|
public function getRiscaldamentoLettureCondominiProperty(): array
|
|
{
|
|
$stabileId = $this->resolveActiveStabileId();
|
|
if (! $stabileId) {
|
|
return [];
|
|
}
|
|
|
|
return StabileServizioLettura::query()
|
|
->where('stabile_id', $stabileId)
|
|
->whereHas('servizio', fn(Builder $q) => $q->where('tipo', 'riscaldamento'))
|
|
->with([
|
|
'unitaImmobiliare:id,codice_unita,interno,scala',
|
|
'servizio:id,nome,contatore_matricola',
|
|
])
|
|
->tap(fn(Builder $query) => $this->applyActiveYearFilterToReadingQuery($query))
|
|
->orderByDesc('created_at')
|
|
->limit(60)
|
|
->get()
|
|
->map(function (StabileServizioLettura $row): array {
|
|
return [
|
|
'id' => (int) $row->id,
|
|
'data_ricezione' => optional($row->created_at)?->format('d/m/Y H:i'),
|
|
'servizio' => (string) ($row->servizio?->nome ?? 'Riscaldamento'),
|
|
'matricola' => (string) ($row->servizio?->contatore_matricola ?? '—'),
|
|
'unita' => trim((string) ($row->unitaImmobiliare?->codice_unita ?? '') . ' ' . (string) ($row->unitaImmobiliare?->interno ?? '')),
|
|
'canale' => (string) ($row->canale_acquisizione ?? '—'),
|
|
'riferimento' => (string) ($row->riferimento_acquisizione ?? '—'),
|
|
'periodo' => ((string) optional($row->periodo_dal)->format('d/m/Y')) . ' - ' . ((string) optional($row->periodo_al)->format('d/m/Y')),
|
|
'consumo_valore' => is_numeric($row->consumo_valore) ? (float) $row->consumo_valore : null,
|
|
'consumo_unita' => (string) ($row->consumo_unita ?: 'mc'),
|
|
];
|
|
})
|
|
->all();
|
|
}
|
|
|
|
/** @return array<int, array<string,mixed>> */
|
|
public function getRiscaldamentoLettureGeneraliProperty(): array
|
|
{
|
|
$stabileId = $this->resolveActiveStabileId();
|
|
if (! $stabileId) {
|
|
return [];
|
|
}
|
|
|
|
return StabileServizioLettura::query()
|
|
->where('stabile_id', $stabileId)
|
|
->whereNull('unita_immobiliare_id')
|
|
->whereHas('servizio', fn(Builder $q) => $q->where('tipo', 'riscaldamento'))
|
|
->with([
|
|
'servizio:id,nome,codice_utenza,codice_cliente,codice_contratto,contatore_matricola',
|
|
'fatturaElettronica:id,numero_fattura,data_fattura',
|
|
])
|
|
->tap(fn(Builder $query) => $this->applyActiveYearFilterToReadingQuery($query))
|
|
->orderByDesc('periodo_al')
|
|
->orderByDesc('id')
|
|
->limit(60)
|
|
->get()
|
|
->map(function (StabileServizioLettura $row): array {
|
|
$schedule = is_array(data_get($row->raw, 'acea_schedule')) ? data_get($row->raw, 'acea_schedule') : [];
|
|
$windowStart = data_get($schedule, 'window_start');
|
|
$windowEnd = data_get($schedule, 'window_end');
|
|
$windowLabel = trim(implode(' - ', array_filter([
|
|
$windowStart ? optional(now()->parse((string) $windowStart))->format('d/m/Y') : null,
|
|
$windowEnd ? optional(now()->parse((string) $windowEnd))->format('d/m/Y') : null,
|
|
])));
|
|
|
|
return [
|
|
'id' => (int) $row->id,
|
|
'data_ricezione' => optional($row->created_at)?->format('d/m/Y H:i'),
|
|
'servizio' => (string) ($row->servizio?->nome ?? 'Riscaldamento'),
|
|
'matricola' => (string) ($row->servizio?->contatore_matricola ?? '—'),
|
|
'codice_utenza' => (string) ($row->servizio?->codice_utenza ?? '—'),
|
|
'codice_cliente' => (string) ($row->servizio?->codice_cliente ?? '—'),
|
|
'codice_contratto' => (string) ($row->servizio?->codice_contratto ?? '—'),
|
|
'canale' => (string) ($row->canale_acquisizione ?? '—'),
|
|
'riferimento' => (string) ($row->riferimento_acquisizione ?? '—'),
|
|
'periodo' => ((string) optional($row->periodo_dal)->format('d/m/Y')) . ' - ' . ((string) optional($row->periodo_al)->format('d/m/Y')),
|
|
'lettura_fine' => is_numeric($row->lettura_fine) ? (float) $row->lettura_fine : null,
|
|
'consumo_valore' => is_numeric($row->consumo_valore) ? (float) $row->consumo_valore : null,
|
|
'consumo_unita' => (string) ($row->consumo_unita ?: 'mc'),
|
|
'protocollo' => (string) ($row->protocollo_numero ?? '—'),
|
|
'workflow' => (string) ($row->workflow_stato ?? '—'),
|
|
'reader' => (string) ($row->rilevatore_nome ?? '—'),
|
|
'window_label' => $windowLabel !== '' ? $windowLabel : null,
|
|
'visit_date' => data_get($schedule, 'visit_date'),
|
|
'fattura_label' => $row->fatturaElettronica
|
|
? trim((string) ($row->fatturaElettronica->numero_fattura ?? 'FE #' . $row->fatturaElettronica->id))
|
|
: null,
|
|
'fattura_url' => $row->fattura_elettronica_id
|
|
? FatturaElettronicaScheda::getUrl(['record' => (int) $row->fattura_elettronica_id], panel : 'admin-filament')
|
|
: null,
|
|
'ticket_url' => TicketRiscaldamentoGenerale::getUrl([
|
|
'stabile' => (int) $row->stabile_id,
|
|
'servizio' => (int) $row->stabile_servizio_id,
|
|
], panel: 'admin-filament'),
|
|
'archivio_url' => LettureServiziArchivio::getUrl([
|
|
'servizio' => (int) $row->stabile_servizio_id,
|
|
], panel: 'admin-filament'),
|
|
];
|
|
})
|
|
->all();
|
|
}
|
|
|
|
/** @return array{totale:int,completate:int,pendenti:int,sollecitate:int} */
|
|
public function getRiscaldamentoCampagnaStatsProperty(): array
|
|
{
|
|
$rows = $this->riscaldamentoCampagnaRows;
|
|
|
|
return [
|
|
'totale' => count($rows),
|
|
'completate' => count(array_filter($rows, fn(array $row): bool => $row['status'] === 'fatta')),
|
|
'pendenti' => count(array_filter($rows, fn(array $row): bool => in_array($row['status'], ['richiesta', 'non_richiesta'], true))),
|
|
'sollecitate' => count(array_filter($rows, fn(array $row): bool => $row['status'] === 'sollecito')),
|
|
];
|
|
}
|
|
|
|
/** @return array<int,array<string,mixed>> */
|
|
public function getRiscaldamentoCampagnaRowsProperty(): array
|
|
{
|
|
$stabileId = $this->resolveActiveStabileId();
|
|
$servizio = $this->resolveActiveRiscaldamentoServizio();
|
|
if (! $stabileId || ! $servizio) {
|
|
return [];
|
|
}
|
|
|
|
$year = $this->resolveActiveAnnoGestione();
|
|
|
|
$units = UnitaImmobiliare::query()
|
|
->where('stabile_id', $stabileId)
|
|
->whereNull('deleted_at')
|
|
->orderBy('scala')
|
|
->orderBy('interno')
|
|
->orderBy('id')
|
|
->get(['id', 'codice_unita', 'denominazione', 'scala', 'interno']);
|
|
|
|
$nominativi = DB::table('unita_immobiliare_nominativi')
|
|
->whereIn('unita_immobiliare_id', $units->pluck('id')->all())
|
|
->orderByDesc('updated_at')
|
|
->orderByDesc('id')
|
|
->get(['unita_immobiliare_id', 'nominativo']);
|
|
|
|
$nominativiByUnit = [];
|
|
foreach ($nominativi as $row) {
|
|
$unitId = (int) ($row->unita_immobiliare_id ?? 0);
|
|
if ($unitId > 0 && ! isset($nominativiByUnit[$unitId])) {
|
|
$nominativiByUnit[$unitId] = trim((string) ($row->nominativo ?? ''));
|
|
}
|
|
}
|
|
|
|
$recapitiRows = DB::table('unita_recapiti_servizio')
|
|
->whereIn('unita_id', $units->pluck('id')->all())
|
|
->where('attivo', true)
|
|
->whereNotNull('email')
|
|
->where('email', '!=', '')
|
|
->orderByRaw("CASE WHEN tipo_servizio = 'riscaldamento' THEN 0 ELSE 1 END")
|
|
->orderByDesc('is_default')
|
|
->orderBy('ordine')
|
|
->get(['unita_id', 'email']);
|
|
|
|
$emailsByUnit = [];
|
|
foreach ($recapitiRows as $row) {
|
|
$unitId = (int) ($row->unita_id ?? 0);
|
|
if ($unitId > 0 && ! isset($emailsByUnit[$unitId])) {
|
|
$emailsByUnit[$unitId] = trim((string) ($row->email ?? ''));
|
|
}
|
|
}
|
|
|
|
$readingRows = StabileServizioLettura::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('stabile_servizio_id', (int) $servizio->id)
|
|
->whereIn('unita_immobiliare_id', $units->pluck('id')->all())
|
|
->orderByDesc('created_at')
|
|
->orderByDesc('id')
|
|
->get();
|
|
|
|
$currentYearByUnit = [];
|
|
$latestRealByUnit = [];
|
|
foreach ($readingRows as $row) {
|
|
$unitId = (int) ($row->unita_immobiliare_id ?? 0);
|
|
if ($unitId <= 0) {
|
|
continue;
|
|
}
|
|
|
|
$rowYear = optional($row->created_at)->year ?: optional($row->periodo_al)->year;
|
|
if ((int) $rowYear === $year && ! isset($currentYearByUnit[$unitId])) {
|
|
$currentYearByUnit[$unitId] = $row;
|
|
}
|
|
|
|
if ($row->lettura_fine !== null && (int) $rowYear !== $year && ! isset($latestRealByUnit[$unitId])) {
|
|
$latestRealByUnit[$unitId] = $row;
|
|
}
|
|
}
|
|
|
|
$rows = $units->map(function (UnitaImmobiliare $unit) use ($stabileId, $currentYearByUnit, $latestRealByUnit, $nominativiByUnit, $emailsByUnit, $servizio): array {
|
|
$current = $currentYearByUnit[(int) $unit->id] ?? null;
|
|
$latestReal = $latestRealByUnit[(int) $unit->id] ?? null;
|
|
|
|
$status = 'non_richiesta';
|
|
if ($current && $current->lettura_fine !== null) {
|
|
$status = 'fatta';
|
|
} elseif ($current && $current->sollecito_inviato_at) {
|
|
$status = 'sollecito';
|
|
} elseif ($current && $current->richiesta_lettura_inviata_at) {
|
|
$status = 'richiesta';
|
|
}
|
|
|
|
return [
|
|
'unit_id' => (int) $unit->id,
|
|
'scala' => trim((string) ($unit->scala ?? '')),
|
|
'interno' => trim((string) ($unit->interno ?? '')),
|
|
'unit_label' => trim((string) ($unit->codice_unita ?? '') . ' ' . (string) ($unit->interno ?? '')) ?: ('UI #' . (int) $unit->id),
|
|
'nominativo' => $nominativiByUnit[(int) $unit->id] ?? (trim((string) ($unit->denominazione ?? '')) ?: 'Nominativo da completare'),
|
|
'email' => $emailsByUnit[(int) $unit->id] ?? null,
|
|
'previous_reading' => $latestReal?->lettura_fine,
|
|
'request_id' => $current?->id,
|
|
'status' => $status,
|
|
'requested_at' => optional($current?->richiesta_lettura_inviata_at)->format('d/m/Y H:i'),
|
|
'deadline_at' => optional($current?->deadline_lettura_at)->format('d/m/Y'),
|
|
'completed_at' => optional($current?->created_at)->format('d/m/Y H:i'),
|
|
'latest_value' => $current?->lettura_fine,
|
|
'edit' => $this->resolveRiscaldamentoCampagnaEditRow((int) $unit->id, $current, $latestReal),
|
|
'public_url' => $this->buildPublicRiscaldamentoReadingUrl((int) $servizio->id, (int) $unit->id, $current?->id ? (int) $current->id : null),
|
|
'ticket_url' => \App\Filament\Pages\Supporto\TicketRiscaldamentoLight::getUrl([
|
|
'stabile' => $stabileId,
|
|
'servizio' => (int) $servizio->id,
|
|
'unita' => (int) $unit->id,
|
|
], panel: 'admin-filament'),
|
|
];
|
|
})->all();
|
|
|
|
usort($rows, function (array $left, array $right): int {
|
|
if ($this->riscaldamentoCampagnaSort === 'nominativo') {
|
|
return strnatcasecmp((string) ($left['nominativo'] ?? ''), (string) ($right['nominativo'] ?? ''));
|
|
}
|
|
|
|
$leftKey = trim((string) ($left['scala'] ?? '') . ' ' . (string) ($left['interno'] ?? ''));
|
|
$rightKey = trim((string) ($right['scala'] ?? '') . ' ' . (string) ($right['interno'] ?? ''));
|
|
|
|
return strnatcasecmp($leftKey, $rightKey);
|
|
});
|
|
|
|
return $rows;
|
|
}
|
|
|
|
private function resolveRiscaldamentoCampagnaEditRow(int $unitId, ?StabileServizioLettura $current, ?StabileServizioLettura $latestReal): array
|
|
{
|
|
if (! isset($this->riscaldamentoCampagnaEditRows[$unitId])) {
|
|
$this->riscaldamentoCampagnaEditRows[$unitId] = [
|
|
'data_lettura' => optional($current?->periodo_al)->toDateString() ?: now()->toDateString(),
|
|
'deadline_at' => optional($current?->deadline_lettura_at)->toDateString(),
|
|
'canale' => (string) ($current->canale_acquisizione ?? 'manuale'),
|
|
'riferimento' => (string) ($current->riferimento_acquisizione ?? ''),
|
|
'lettura_finale' => $current?->lettura_fine,
|
|
'note' => trim((string) data_get($current?->raw ?? [], 'campagna_editor.note', '')),
|
|
'reader_name' => trim((string) ($current->rilevatore_nome ?? Auth::user()?->name ?? '')),
|
|
'previous_reading' => $latestReal?->lettura_fine,
|
|
];
|
|
}
|
|
|
|
return $this->riscaldamentoCampagnaEditRows[$unitId];
|
|
}
|
|
|
|
/** @param array<int> $unitIds */
|
|
private function persistRiscaldamentoCampagnaRows(array $unitIds): void
|
|
{
|
|
$stabileId = $this->resolveActiveStabileId();
|
|
$servizio = $this->resolveActiveRiscaldamentoServizio();
|
|
$user = Auth::user();
|
|
if (! $stabileId || ! $servizio || ! $user instanceof User) {
|
|
return;
|
|
}
|
|
|
|
$saved = 0;
|
|
foreach ($unitIds as $unitId) {
|
|
$unitId = (int) $unitId;
|
|
if ($unitId <= 0) {
|
|
continue;
|
|
}
|
|
|
|
$unit = UnitaImmobiliare::query()->where('stabile_id', $stabileId)->find($unitId);
|
|
if (! $unit instanceof UnitaImmobiliare) {
|
|
continue;
|
|
}
|
|
|
|
$edit = is_array($this->riscaldamentoCampagnaEditRows[$unitId] ?? null) ? $this->riscaldamentoCampagnaEditRows[$unitId] : [];
|
|
if ($edit === []) {
|
|
continue;
|
|
}
|
|
|
|
$current = StabileServizioLettura::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('stabile_servizio_id', (int) $servizio->id)
|
|
->where('unita_immobiliare_id', $unitId)
|
|
->whereYear('created_at', $this->resolveActiveAnnoGestione())
|
|
->orderByDesc('created_at')
|
|
->orderByDesc('id')
|
|
->first();
|
|
|
|
$previous = StabileServizioLettura::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('stabile_servizio_id', (int) $servizio->id)
|
|
->where('unita_immobiliare_id', $unitId)
|
|
->whereNotNull('lettura_fine')
|
|
->when($current instanceof StabileServizioLettura, fn(Builder $q) => $q->where('id', '!=', (int) $current->id))
|
|
->orderByDesc('periodo_al')
|
|
->orderByDesc('id')
|
|
->first();
|
|
|
|
$previousValue = is_numeric($edit['previous_reading'] ?? null)
|
|
? round((float) $edit['previous_reading'], 3)
|
|
: ($previous?->lettura_fine !== null ? (float) $previous->lettura_fine : null);
|
|
$finalValue = is_numeric($edit['lettura_finale'] ?? null) ? round((float) $edit['lettura_finale'], 3) : null;
|
|
$consumo = ($previousValue !== null && $finalValue !== null) ? round($finalValue - $previousValue, 3) : null;
|
|
if ($consumo !== null && $consumo < 0) {
|
|
continue;
|
|
}
|
|
|
|
$payload = [
|
|
'stabile_id' => $stabileId,
|
|
'stabile_servizio_id' => (int) $servizio->id,
|
|
'unita_immobiliare_id' => $unitId,
|
|
'fornitore_id' => $servizio->fornitore_id,
|
|
'periodo_dal' => $previous?->periodo_al,
|
|
'periodo_al' => $edit['data_lettura'] ?? null,
|
|
'tipologia_lettura' => $finalValue !== null ? 'manuale_ui' : 'richiesta_autolettura',
|
|
'canale_acquisizione' => trim((string) ($edit['canale'] ?? 'manuale')) ?: 'manuale',
|
|
'riferimento_acquisizione' => trim((string) ($edit['riferimento'] ?? '')) ?: null,
|
|
'workflow_stato' => $finalValue !== null ? 'ricevuta' : ($current?->workflow_stato ?: 'richiesta_inviata'),
|
|
'richiesta_lettura_inviata_at' => $current?->richiesta_lettura_inviata_at ?: now(),
|
|
'deadline_lettura_at' => $edit['deadline_at'] ?? null,
|
|
'rilevatore_tipo' => 'amministrazione',
|
|
'rilevatore_nome' => trim((string) ($edit['reader_name'] ?? $user->name)) ?: $user->name,
|
|
'lettura_precedente_valore' => $previousValue,
|
|
'lettura_inizio' => $previousValue,
|
|
'lettura_fine' => $finalValue,
|
|
'consumo_valore' => $consumo,
|
|
'consumo_unita' => 'mc',
|
|
'raw' => array_merge(is_array($current?->raw ?? null) ? $current->raw : [], [
|
|
'campagna_editor' => [
|
|
'note' => trim((string) ($edit['note'] ?? '')),
|
|
'updated_at' => now()->toIso8601String(),
|
|
'updated_by' => (int) $user->id,
|
|
],
|
|
]),
|
|
];
|
|
|
|
if ($current instanceof StabileServizioLettura) {
|
|
$current->fill($payload);
|
|
$current->save();
|
|
} else {
|
|
$payload['created_by'] = (int) $user->id;
|
|
StabileServizioLettura::query()->create($payload);
|
|
}
|
|
|
|
$saved++;
|
|
}
|
|
|
|
Notification::make()->title('Campagna letture aggiornata')->body('Righe salvate: ' . $saved)->success()->send();
|
|
}
|
|
|
|
protected function ensureRiscaldamentoServicesForStabile(int $stabileId): void
|
|
{
|
|
if (! Schema::hasTable('stabile_servizi')) {
|
|
return;
|
|
}
|
|
|
|
$exists = \App\Models\StabileServizio::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('tipo', 'riscaldamento')
|
|
->exists();
|
|
|
|
if ($exists) {
|
|
return;
|
|
}
|
|
|
|
$legacyCodes = $this->resolveLegacyCodiciStabile();
|
|
if ($legacyCodes === [] || ! Schema::connection('gescon_import')->hasTable('operazioni')) {
|
|
return;
|
|
}
|
|
|
|
$legacySupplierIds = \Illuminate\Support\Facades\DB::connection('gescon_import')
|
|
->table('operazioni')
|
|
->whereIn('cod_stabile', $legacyCodes)
|
|
->whereIn('gestione', ['O', 'R'])
|
|
->where('cod_spe', 'like', 'R%')
|
|
->distinct()
|
|
->pluck('cod_for')
|
|
->all();
|
|
|
|
if ($legacySupplierIds === []) {
|
|
return;
|
|
}
|
|
|
|
$fornitori = \App\Models\Fornitore::query()
|
|
->whereIn('old_id', $legacySupplierIds)
|
|
->get();
|
|
|
|
foreach ($fornitori as $fornitore) {
|
|
\App\Models\StabileServizio::query()->firstOrCreate([
|
|
'stabile_id' => $stabileId,
|
|
'tipo' => 'riscaldamento',
|
|
'fornitore_id' => $fornitore->id,
|
|
], [
|
|
'nome' => 'Riscaldamento ' . $fornitore->ragione_sociale,
|
|
'attivo' => true,
|
|
'meta' => [
|
|
'source' => 'legacy_import_auto_link',
|
|
],
|
|
]);
|
|
}
|
|
}
|
|
|
|
/** @return array<int, array<string,mixed>> */
|
|
public function getRiscaldamentoTariffeRowsProperty(): array
|
|
{
|
|
$stabileId = $this->resolveActiveStabileId();
|
|
if (! $stabileId) {
|
|
return [];
|
|
}
|
|
|
|
// Auto-sincronizza i servizi di riscaldamento dallo staging legacy
|
|
$this->ensureRiscaldamentoServicesForStabile($stabileId);
|
|
|
|
$activeYear = $this->resolveActiveAnnoGestione();
|
|
|
|
// Carica le voci di spesa collegate al riscaldamento per lo stabile
|
|
$voci = \App\Models\VoceSpesa::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('attiva', true)
|
|
->where(function ($query): void {
|
|
$query->where('tipo_gestione', 'riscaldamento')
|
|
->orWhere('tipo', 'riscaldamento')
|
|
->orWhere('categoria', 'riscaldamento')
|
|
->orWhere('categoria', 'gas')
|
|
->orWhere('codice', 'like', 'R%')
|
|
->orWhere('codice', 'like', 'RL%');
|
|
})
|
|
->where('codice', '!=', 'RAO')
|
|
->with(['tabellaMillesimaleDefault'])
|
|
->orderBy('codice')
|
|
->get();
|
|
|
|
// Trova le gestioni attive per questo stabile ed anno
|
|
$gestioneIds = \App\Models\GestioneContabile::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('anno_gestione', $activeYear)
|
|
->pluck('id')
|
|
->all();
|
|
|
|
$rows = [];
|
|
foreach ($voci as $voce) {
|
|
$consuntivo = 0.0;
|
|
if ($gestioneIds !== []) {
|
|
// 1) Somma dalle righe fatture fornitore
|
|
$invoiceSum = (float) \App\Modules\Contabilita\Models\FatturaFornitoreRiga::query()
|
|
->where('voce_spesa_id', $voce->id)
|
|
->whereHas('fattura', function ($q) use ($stabileId, $gestioneIds) {
|
|
$q->where('stabile_id', $stabileId)
|
|
->whereIn('gestione_id', $gestioneIds);
|
|
})
|
|
->sum('totale_euro');
|
|
|
|
// 2) Somma dalle operazioni contabili di prima nota (es. importate dal legacy)
|
|
$opSum = (float) \Illuminate\Support\Facades\DB::table('operazioni_contabili')
|
|
->whereIn('gestione_id', $gestioneIds)
|
|
->where(function ($query) use ($voce) {
|
|
$query->where('conto_contabile', (string) $voce->codice)
|
|
->when($voce->legacy_codice, fn($q) => $q->orWhere('conto_contabile', (string) $voce->legacy_codice))
|
|
->orWhere('voce_spesa_snapshot', 'like', '%"id":' . $voce->id . '%')
|
|
->orWhere('voce_spesa_snapshot', 'like', '%"id": "' . $voce->id . '"%');
|
|
})
|
|
->sum('dare');
|
|
|
|
$consuntivo = $invoiceSum + $opSum;
|
|
}
|
|
|
|
$preventivo = (float) ($voce->importo_default ?? 0.0);
|
|
$scostamento = $consuntivo - $preventivo;
|
|
|
|
$rows[] = [
|
|
'codice' => (string) $voce->codice,
|
|
'descrizione' => (string) $voce->descrizione,
|
|
'tabella' => $voce->tabellaMillesimaleDefault?->nome_tabella ?: ($voce->tabellaMillesimaleDefault?->denominazione ?: 'Nessuna'),
|
|
'preventivo' => $preventivo,
|
|
'consuntivo' => $consuntivo,
|
|
'scostamento' => $scostamento,
|
|
];
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
/** @return array{contratti_acea: bool, tariffe_acea_standard: bool, note: string} */
|
|
public function getRiscaldamentoLegacyArchiveAvailabilityProperty(): array
|
|
{
|
|
$hasContratti = Schema::connection('gescon_import')->hasTable('contratti_ACEA')
|
|
|| Schema::connection('gescon_import')->hasTable('contratti_acea');
|
|
$hasTariffe = Schema::connection('gescon_import')->hasTable('Tariffe_ACEA_Standard')
|
|
|| Schema::connection('gescon_import')->hasTable('tariffe_acea_standard');
|
|
|
|
return [
|
|
'contratti_acea' => $hasContratti,
|
|
'tariffe_acea_standard' => $hasTariffe,
|
|
'note' => ($hasContratti || $hasTariffe)
|
|
? 'Archivio legacy disponibile su connessione gescon_import.'
|
|
: 'Tabelle legacy non presenti ora su gescon_import: caricare import da parti_comuni.mdb per abilitarle.',
|
|
];
|
|
}
|
|
|
|
/** @return array<int, array{cod_spe: string, totale: float}> */
|
|
public function getRiscaldamentoAltreVociLegacyProperty(): array
|
|
{
|
|
if (! Schema::connection('gescon_import')->hasTable('operazioni')) {
|
|
return [];
|
|
}
|
|
|
|
$legacyCodes = $this->resolveLegacyCodiciStabile();
|
|
$year = $this->resolveActiveAnnoGestione();
|
|
|
|
$query = DB::connection('gescon_import')
|
|
->table('operazioni')
|
|
->where('gestione', 'O')
|
|
->where('cod_spe', 'like', 'AC%')
|
|
->where('cod_spe', '!=', 'AC1')
|
|
->where('cod_spe', '!=', 'AC2');
|
|
|
|
if ($legacyCodes !== [] && Schema::connection('gescon_import')->hasColumn('operazioni', 'cod_stabile')) {
|
|
$query->whereIn('cod_stabile', $legacyCodes);
|
|
}
|
|
|
|
if (Schema::connection('gescon_import')->hasColumn('operazioni', 'dt_spe')) {
|
|
$query->whereYear('dt_spe', $year);
|
|
}
|
|
|
|
$columns = Schema::connection('gescon_import')->getColumnListing('operazioni');
|
|
$sumFields = [];
|
|
if (in_array('importo_euro', $columns, true)) {
|
|
$sumFields[] = 'importo_euro';
|
|
}
|
|
if (in_array('importo', $columns, true)) {
|
|
$sumFields[] = 'importo';
|
|
}
|
|
$sumSql = '0';
|
|
if ($sumFields !== []) {
|
|
$sumSql = 'COALESCE(' . implode(', ', $sumFields) . ', 0)';
|
|
}
|
|
|
|
$rows = $query
|
|
->select('cod_spe')
|
|
->selectRaw("SUM($sumSql) as totale")
|
|
->groupBy('cod_spe')
|
|
->orderBy('cod_spe')
|
|
->get();
|
|
|
|
return $rows->map(fn($r): array=> [
|
|
'cod_spe' => (string) ($r->cod_spe ?? '—'),
|
|
'totale' => round((float) ($r->totale ?? 0), 2),
|
|
])->all();
|
|
}
|
|
|
|
protected function ensureFornitoreStabileDefaultVoceSpesa(int $stabileId): void
|
|
{
|
|
if (! Schema::hasTable('fornitore_stabile_impostazioni')) {
|
|
return;
|
|
}
|
|
|
|
$services = \App\Models\StabileServizio::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('tipo', 'riscaldamento')
|
|
->whereNotNull('fornitore_id')
|
|
->get();
|
|
|
|
$legacyCodes = $this->resolveLegacyCodiciStabile();
|
|
if ($legacyCodes === [] || ! Schema::connection('gescon_import')->hasTable('operazioni')) {
|
|
return;
|
|
}
|
|
|
|
foreach ($services as $service) {
|
|
$fornitoreId = (int) $service->fornitore_id;
|
|
$fornitore = \App\Models\Fornitore::query()->find($fornitoreId);
|
|
if (! $fornitore || ! $fornitore->old_id) {
|
|
continue;
|
|
}
|
|
|
|
$imp = \App\Models\FornitoreStabileImpostazione::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('fornitore_id', $fornitoreId)
|
|
->first();
|
|
|
|
if ($imp && $imp->voce_spesa_default_id) {
|
|
continue;
|
|
}
|
|
|
|
$mostCommonVoceCode = \Illuminate\Support\Facades\DB::connection('gescon_import')
|
|
->table('operazioni')
|
|
->whereIn('cod_stabile', $legacyCodes)
|
|
->where('cod_for', $fornitore->old_id)
|
|
->where('cod_spe', 'like', 'R%')
|
|
->where('cod_spe', '!=', 'RAO')
|
|
->groupBy('cod_spe')
|
|
->orderByRaw('count(*) desc')
|
|
->value('cod_spe');
|
|
|
|
if (! $mostCommonVoceCode) {
|
|
continue;
|
|
}
|
|
|
|
$voceSpesa = \App\Models\VoceSpesa::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where(function ($q) use ($mostCommonVoceCode) {
|
|
$q->where('codice', $mostCommonVoceCode)
|
|
->orWhere('legacy_codice', $mostCommonVoceCode);
|
|
})
|
|
->first();
|
|
|
|
if (! $voceSpesa) {
|
|
continue;
|
|
}
|
|
|
|
\App\Models\FornitoreStabileImpostazione::updateOrCreate([
|
|
'stabile_id' => $stabileId,
|
|
'fornitore_id' => $fornitoreId,
|
|
], [
|
|
'voce_spesa_default_id' => $voceSpesa->id,
|
|
'meta' => array_merge($imp?->meta ?? [], [
|
|
'source' => 'legacy_import_auto_link',
|
|
'most_common_legacy_code' => $mostCommonVoceCode,
|
|
]),
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function riconciliaAutomaticamentePagamenti(): void
|
|
{
|
|
$stabileId = $this->resolveActiveStabileId();
|
|
if (! $stabileId) {
|
|
Notification::make()->title('Errore')->body('Nessuno stabile attivo selezionato.')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$fornitoreIds = StabileServizio::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('tipo', 'riscaldamento')
|
|
->whereNotNull('fornitore_id')
|
|
->pluck('fornitore_id')
|
|
->map(fn($value): int => (int) $value)
|
|
->filter(fn(int $value): bool => $value > 0)
|
|
->values()
|
|
->all();
|
|
|
|
if (empty($fornitoreIds)) {
|
|
Notification::make()->title('Avviso')->body('Nessun fornitore di riscaldamento censito per questo stabile.')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$unpaidInvoices = DB::table('contabilita_fatture_fornitori')
|
|
->where('stabile_id', $stabileId)
|
|
->whereIn('fornitore_id', $fornitoreIds)
|
|
->where(function ($q) {
|
|
$q->where('stato', '!=', 'pagato')
|
|
->orWhereNull('stato')
|
|
->orWhereNull('data_pagamento');
|
|
})
|
|
->get();
|
|
|
|
if ($unpaidInvoices->isEmpty()) {
|
|
Notification::make()->title('Riconciliazione completata')->body('Tutte le fatture risultano già pagate o non ci sono fatture registrate.')->info()->send();
|
|
return;
|
|
}
|
|
|
|
$matchedCount = 0;
|
|
|
|
foreach ($unpaidInvoices as $invoice) {
|
|
$fornitoreRagioneSociale = DB::table('fornitori')
|
|
->where('id', $invoice->fornitore_id)
|
|
->value('ragione_sociale');
|
|
|
|
// 1. Prova prima con il riferimento preciso FAT-ID
|
|
$match = DB::table('contabilita_movimenti_banca')
|
|
->where('stabile_id', $stabileId)
|
|
->where('importo', '<', 0)
|
|
->where(function ($q) use ($invoice) {
|
|
$q->where('descrizione', 'like', '%FAT-ID:' . $invoice->id . '%')
|
|
->orWhere('descrizione_estesa', 'like', '%FAT-ID:' . $invoice->id . '%');
|
|
})
|
|
->first();
|
|
|
|
// 2. Fallback alla ricerca generica per importo e fornitore
|
|
if (! $match) {
|
|
$match = DB::table('contabilita_movimenti_banca')
|
|
->where('stabile_id', $stabileId)
|
|
->where('importo', '<', 0)
|
|
->where(function ($q) use ($invoice, $fornitoreRagioneSociale) {
|
|
$q->where('fornitore_id', $invoice->fornitore_id);
|
|
if ($fornitoreRagioneSociale) {
|
|
$q->orWhere('descrizione', 'like', '%' . $fornitoreRagioneSociale . '%')
|
|
->orWhere('descrizione_estesa', 'like', '%' . $fornitoreRagioneSociale . '%');
|
|
}
|
|
})
|
|
->where(function ($q) use ($invoice) {
|
|
$q->whereRaw('ABS(importo) = ?', [abs((float) $invoice->totale)])
|
|
->orWhereRaw('ABS(importo) = ?', [abs((float) ($invoice->netto_da_pagare ?? 0))]);
|
|
|
|
if (!empty($invoice->numero_documento)) {
|
|
$q->orWhere('descrizione', 'like', '%' . $invoice->numero_documento . '%')
|
|
->orWhere('descrizione_estesa', 'like', '%' . $invoice->numero_documento . '%');
|
|
}
|
|
})
|
|
->first();
|
|
}
|
|
|
|
if ($match) {
|
|
DB::table('contabilita_fatture_fornitori')
|
|
->where('id', $invoice->id)
|
|
->update([
|
|
'stato' => 'pagato',
|
|
'data_pagamento' => $match->data,
|
|
'movimento_pagamento_id' => $match->id,
|
|
]);
|
|
|
|
$matchData = is_string($match->match_data) ? json_decode($match->match_data, true) : (is_array($match->match_data) ? $match->match_data : []);
|
|
$matchData['riconciliato_operativo'] = true;
|
|
$matchData['riconciliazione_tipo'] = 'fattura_fornitore';
|
|
$matchData['riconciliazione_label'] = 'Pagamento fattura #' . $invoice->numero_documento;
|
|
$matchData['riconciliazione_at'] = now()->toDateTimeString();
|
|
$matchData['fattura_fornitore_id'] = $invoice->id;
|
|
|
|
DB::table('contabilita_movimenti_banca')
|
|
->where('id', $match->id)
|
|
->update([
|
|
'fornitore_id' => $invoice->fornitore_id,
|
|
'registrazione_id' => $invoice->registrazione_id,
|
|
'match_data' => json_encode($matchData),
|
|
]);
|
|
|
|
$matchedCount++;
|
|
}
|
|
}
|
|
|
|
if ($matchedCount > 0) {
|
|
Notification::make()->title('Riconciliazione completata')->body("Abbinati e registrati con successo {$matchedCount} pagamenti di fatture.")->success()->send();
|
|
} else {
|
|
Notification::make()->title('Riconciliazione completata')->body('Nessun movimento bancario corrispondente trovato per le fatture da pagare.')->warning()->send();
|
|
}
|
|
}
|
|
}
|