netgescon-day0/app/Filament/Pages/Contabilita/CasseBancheRiepilogo.php

626 lines
23 KiB
PHP

<?php
namespace App\Filament\Pages\Contabilita;
use App\Models\DatiBancari;
use App\Models\GestioneContabile;
use App\Models\Stabile;
use App\Models\User;
use App\Modules\Contabilita\Models\MovimentoBanca;
use App\Modules\Contabilita\Models\SaldoConto;
use App\Services\Contabilita\MovimentiBancaImporter;
use App\Services\Contabilita\UnicreditWriParser;
use App\Services\Contabilita\IntesaCsvParser;
use App\Services\Contabilita\ExcelMovimentiParser;
use App\Support\AnnoGestioneContext;
use App\Support\GestioneContext;
use App\Support\StabileContext;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Repeater;
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\TextColumn;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Filters\Filter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use UnitEnum;
class CasseBancheRiepilogo extends Page implements HasTable
{
use InteractsWithTable;
protected static ?bool $supportsWindowFunctions = null;
protected static ?string $navigationLabel = 'Casse e banche';
protected static ?string $title = 'Casse e banche';
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-banknotes';
protected static UnitEnum|string|null $navigationGroup = 'Contabilità';
protected static ?int $navigationSort = 10;
protected static ?string $slug = 'contabilita/casse-banche';
protected string $view = 'filament.pages.contabilita.casse-banche-riepilogo';
public static function canAccess(): bool
{
$user = Auth::user();
if (! $user instanceof User) {
return false;
}
if ($user->hasAnyRole(['super-admin', 'admin'])) {
return true;
}
return $user->can('contabilita.casse-banche');
}
public function mount(): void
{
$this->mountInteractsWithTable();
}
public function getActiveStabile(): ?Stabile
{
$user = Auth::user();
if (! $user instanceof User) {
return null;
}
return StabileContext::getActiveStabile($user);
}
protected function getHeaderActions(): array
{
return [
Action::make('importa_unicredit_wri')
->label('Importa estratto (Unicredit WRI)')
->icon('heroicon-o-arrow-up-tray')
->modalWidth('7xl')
->form([
Select::make('iban')
->label('Conto (IBAN)')
->native(false)
->searchable()
->options(fn(): array => $this->getIbanOptions())
->helperText('Opzionale: se non selezionato, l\'import prova a rilevare il conto dai dati.'),
Select::make('gestione_id')
->label('Gestione (opzionale)')
->native(false)
->searchable()
->options(fn(): array => $this->getGestioniOptions())
->default(fn(): ?string => $this->resolveDefaultGestioneId()),
FileUpload::make('file')
->label('File estratto')
->directory('tmp/banca')
->disk('local')
->required(),
Toggle::make('replace')
->label('Sostituisci import nel periodo (consigliato se hai già importato con valori “slittati”)')
->helperText('Elimina SOLO i movimenti importati e NON riconciliati (registrazione vuota) per lo stesso conto e per l\'intervallo date del file, poi reimporta.')
->default(false),
])
->action(function (array $data): void {
$user = Auth::user();
if (! $user instanceof User) {
Notification::make()->title('Utente non valido')->danger()->send();
return;
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId) {
Notification::make()->title('Seleziona uno stabile')->danger()->send();
return;
}
$path = (string) ($data['file'] ?? '');
if ($path === '') {
Notification::make()->title('Seleziona un file')->danger()->send();
return;
}
try {
$content = Storage::disk('local')->get($path);
$importer = new MovimentiBancaImporter(new UnicreditWriParser(), new IntesaCsvParser(), new ExcelMovimentiParser());
$res = $importer->importUnicreditWri(
$content,
$stabileId,
basename($path),
isset($data['iban']) && is_string($data['iban']) && trim($data['iban']) !== '' ? trim((string) $data['iban']) : null,
isset($data['gestione_id']) && is_numeric($data['gestione_id']) ? (int) $data['gestione_id'] : null,
isset($data['replace']) ? (bool) $data['replace'] : false,
);
Notification::make()
->title('Import completato')
->body('Importati: ' . ($res['imported'] ?? 0) . ' · Duplicati: ' . ($res['duplicates'] ?? 0))
->success()
->send();
} catch (\Throwable $e) {
Notification::make()->title('Errore import')->body($e->getMessage())->danger()->send();
} finally {
try {
Storage::disk('local')->delete($path);
} catch (\Throwable) {
// ignore
}
}
}),
Action::make('inserisci_saldi')
->label('Inserisci saldi (periodo)')
->icon('heroicon-o-scale')
->modalWidth('7xl')
->form([
Select::make('iban')
->label('Conto (IBAN)')
->native(false)
->searchable()
->options(fn(): array => $this->getIbanOptions())
->required(),
Textarea::make('note')
->label('Note (opzionale)')
->rows(2)
->maxLength(255),
Repeater::make('saldi')
->label('Saldi')
->defaultItems(1)
->reorderable(true)
->schema([
DatePicker::make('data_saldo')
->label('Data')
->required(),
TextInput::make('saldo')
->label('Saldo')
->numeric()
->required(),
])
->columns(2)
->required(),
])
->action(function (array $data): void {
$user = Auth::user();
if (! $user instanceof User) {
Notification::make()->title('Utente non valido')->danger()->send();
return;
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId) {
Notification::make()->title('Seleziona uno stabile')->danger()->send();
return;
}
$iban = isset($data['iban']) && is_string($data['iban']) ? trim((string) $data['iban']) : '';
if ($iban === '') {
Notification::make()->title('Seleziona un IBAN')->danger()->send();
return;
}
$rows = $data['saldi'] ?? [];
if (! is_array($rows) || $rows === []) {
Notification::make()->title('Inserisci almeno un saldo')->danger()->send();
return;
}
$note = isset($data['note']) && is_string($data['note']) ? trim((string) $data['note']) : '';
$contoId = null;
try {
if (DB::getSchemaBuilder()->hasTable('dati_bancari') && DB::getSchemaBuilder()->hasColumn('contabilita_saldi_conti', 'conto_id')) {
$contoId = DatiBancari::query()
->where('stabile_id', (int) $stabileId)
->where('iban', $iban)
->orderBy('id')
->value('id');
$contoId = is_numeric($contoId) ? (int) $contoId : null;
}
} catch (\Throwable) {
$contoId = null;
}
$created = 0;
DB::transaction(function () use ($rows, $stabileId, $contoId, $iban, $note, &$created, $user): void {
foreach ($rows as $row) {
$date = $row['data_saldo'] ?? null;
$saldo = $row['saldo'] ?? null;
if (! $date || ! is_numeric($saldo)) {
continue;
}
SaldoConto::query()->create([
'stabile_id' => (int) $stabileId,
'conto_id' => $contoId,
'iban' => $iban,
'data_saldo' => $date,
'saldo' => (float) $saldo,
'note' => $note !== '' ? $note : null,
'created_by' => (int) ($user->id ?? 0) ?: null,
'updated_by' => (int) ($user->id ?? 0) ?: null,
]);
$created++;
}
});
Notification::make()
->title('Saldi salvati')
->body('Inseriti: ' . $created)
->success()
->send();
}),
];
}
protected function getTableQuery(): Builder
{
$user = Auth::user();
if (! $user instanceof User) {
return DatiBancari::query()->whereRaw('1 = 0');
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId) {
return DatiBancari::query()->whereRaw('1 = 0');
}
return DatiBancari::query()
->where('stabile_id', $stabileId)
->orderBy('stato_conto')
->orderBy('denominazione_banca')
->orderBy('iban')
->orderBy('id');
}
public function table(Table $table): Table
{
return $table
->striped()
->filters([
Filter::make('periodo')
->label('Periodo')
->form([
DatePicker::make('from')->label('Dal'),
DatePicker::make('to')->label('Al'),
]),
Filter::make('gestioni')
->label('Gestioni')
->form([
Select::make('gestione_ids')
->label('Gestione')
->multiple()
->options(fn(): array => $this->getGestioniOptions())
->searchable(),
]),
])
->columns([
TextColumn::make('denominazione_banca')
->label('Banca')
->searchable()
->wrap(),
TextColumn::make('tipo_conto')
->label('Tipo')
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('intestazione_conto')
->label('Intestazione')
->wrap()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('iban')
->label('IBAN')
->searchable()
->copyable(),
TextColumn::make('saldo_calcolato')
->label('Saldo')
->alignEnd()
->getStateUsing(fn(DatiBancari $record): ?float => $this->resolveSaldoConto($record))
->formatStateUsing(fn($state) => $state !== null ? ('€ ' . number_format((float) $state, 2, ',', '.')) : '—'),
TextColumn::make('stato_conto')
->label('Stato')
->toggleable(isToggledHiddenByDefault: true),
])
->actions([
Action::make('movimenti')
->label('Movimenti')
->icon('heroicon-o-arrow-right')
->url(function (DatiBancari $record): string {
return CasseBancheMovimenti::getUrl(panel: 'admin-filament') . '?conto_id=' . (int) $record->id;
}),
]);
}
public function getTotaleDisponibilita(): ?float
{
$user = Auth::user();
if (! $user instanceof User) {
return null;
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId) {
return null;
}
$periodo = $this->getTableFilterState('periodo');
$from = is_array($periodo) ? ($periodo['from'] ?? null) : null;
$to = is_array($periodo) ? ($periodo['to'] ?? null) : null;
$gestioni = $this->getTableFilterState('gestioni');
$gestioneIds = is_array($gestioni) ? ($gestioni['gestione_ids'] ?? null) : null;
$gestioneIds = is_array($gestioneIds) ? array_values(array_filter(array_map('intval', $gestioneIds), fn(int $v) => $v > 0)) : [];
$conti = DatiBancari::query()
->where('stabile_id', $stabileId)
->whereNotNull('iban')
->where('iban', '!=', '')
->get(['iban', 'saldo_iniziale']);
if ($conti->isEmpty()) {
return 0.0;
}
$ibans = $conti->pluck('iban')->map(fn($v) => trim((string) $v))->filter()->values()->all();
if ($ibans === []) {
return 0.0;
}
$movQ = MovimentoBanca::query()
->where('stabile_id', $stabileId)
->whereIn('iban', $ibans);
if ($from) {
$movQ->whereDate('data', '>=', $from);
}
if ($to) {
$movQ->whereDate('data', '<=', $to);
}
if ($gestioneIds !== []) {
$movQ->whereIn('gestione_id', $gestioneIds);
}
$sums = $movQ
->groupBy('iban')
->selectRaw('iban, COALESCE(SUM(importo), 0) AS saldo_movimenti')
->pluck('saldo_movimenti', 'iban')
->map(fn($v) => (float) $v)
->all();
$total = 0.0;
foreach ($conti as $c) {
$iban = trim((string) ($c->iban ?? ''));
if ($iban === '') {
continue;
}
$saldoIniziale = is_numeric($c->saldo_iniziale) ? (float) $c->saldo_iniziale : 0.0;
if ($from) {
$alt = $this->resolveSaldoIniziale($stabileId, $iban, (string) $from);
if ($alt !== null) {
$saldoIniziale = $alt;
}
}
$total += $saldoIniziale + ($sums[$iban] ?? 0.0);
}
return $total;
}
protected function resolveSaldoConto(DatiBancari $record): ?float
{
$user = Auth::user();
if (! $user instanceof User) {
return null;
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId) {
return null;
}
$iban = is_string($record->iban) ? trim((string) $record->iban) : '';
$periodo = $this->getTableFilterState('periodo');
$from = is_array($periodo) ? ($periodo['from'] ?? null) : null;
$to = is_array($periodo) ? ($periodo['to'] ?? null) : null;
$gestioni = $this->getTableFilterState('gestioni');
$gestioneIds = is_array($gestioni) ? ($gestioni['gestione_ids'] ?? null) : null;
$gestioneIds = is_array($gestioneIds) ? array_values(array_filter(array_map('intval', $gestioneIds), fn(int $v) => $v > 0)) : [];
$saldoIniziale = is_numeric($record->saldo_iniziale) ? (float) $record->saldo_iniziale : 0.0;
$q = MovimentoBanca::query()
->where('stabile_id', $stabileId)
->when($record->id, fn($q) => $q->where('conto_id', (int) $record->id))
->when($iban !== '', fn($q) => $q->where('iban', $iban));
if ($from) {
$q->whereDate('data', '>=', $from);
}
if ($to) {
$q->whereDate('data', '<=', $to);
}
if ($gestioneIds !== []) {
$q->whereIn('gestione_id', $gestioneIds);
}
// Base da saldi intermedi: se c'è un saldo <= from (se filtrato) o altrimenti ultimo saldo disponibile.
$baseSaldo = null;
$baseDate = null;
if (DB::getSchemaBuilder()->hasTable('contabilita_saldi_conti')) {
$qSaldo = SaldoConto::query()
->where('stabile_id', $stabileId)
->where('conto_id', (int) $record->id);
if ($from) {
$qSaldo->whereDate('data_saldo', '<=', (string) $from);
}
$row = $qSaldo->orderByDesc('data_saldo')->orderByDesc('id')->first(['data_saldo', 'saldo']);
if ($row) {
$baseSaldo = (float) $row->saldo;
$baseDate = $row->data_saldo->toDateString();
}
}
if ($from && $baseSaldo !== null) {
// Se filtrato da una data, la base è già il saldo al from (o più vicino precedente).
$saldoIniziale = $baseSaldo;
}
if ($baseSaldo !== null && ! $from) {
// Non filtrato: usa ultimo saldo inserito come base.
$saldoIniziale = $baseSaldo;
}
if ($baseDate && ! $from) {
$q->whereDate('data', '>', $baseDate);
}
$sum = (float) ($q->sum('importo') ?? 0.0);
return $saldoIniziale + $sum;
}
protected function resolveSaldoIniziale(int $stabileId, string $iban, string $fromDate): ?float
{
if (! DB::getSchemaBuilder()->hasTable('contabilita_saldi_conti')) {
return null;
}
$row = SaldoConto::query()
->where('stabile_id', $stabileId)
->where('iban', $iban)
->whereDate('data_saldo', '<=', $fromDate)
->orderByDesc('data_saldo')
->orderByDesc('id')
->first();
return $row ? (float) $row->saldo : null;
}
/** @return array<string,string> */
protected function getIbanOptions(): array
{
$user = Auth::user();
if (! $user instanceof User) {
return [];
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId) {
return [];
}
return DatiBancari::query()
->where('stabile_id', $stabileId)
->whereNotNull('iban')
->where('iban', '!=', '')
->orderBy('denominazione_banca')
->orderBy('iban')
->limit(500)
->get(['iban', 'denominazione_banca'])
->mapWithKeys(fn(DatiBancari $r) => [
trim((string) $r->iban) => trim((string) ($r->denominazione_banca ?: 'Conto') . ' · ' . (string) $r->iban),
])
->all();
}
/** @return array<string,string> */
protected function getGestioniOptions(): array
{
$user = Auth::user();
if (! $user instanceof User) {
return [];
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId) {
return [];
}
if (! DB::getSchemaBuilder()->hasTable('gestioni_contabili')) {
return [];
}
return GestioneContabile::query()
->where('stabile_id', $stabileId)
->orderByDesc('anno_gestione')
->orderBy('tipo_gestione')
->orderByDesc('id')
->get(['id', 'denominazione', 'protocollo_prefix', 'stato'])
->mapWithKeys(fn(GestioneContabile $g) => [
(string) $g->id => trim(($g->protocollo_prefix ? $g->protocollo_prefix . ' · ' : '') . $g->denominazione) . ($g->stato !== 'aperta' ? (' (' . $g->stato . ')') : ''),
])
->all();
}
protected function resolveDefaultGestioneId(): ?string
{
$user = Auth::user();
if (! $user instanceof User) {
return null;
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId) {
return null;
}
if (! DB::getSchemaBuilder()->hasTable('gestioni_contabili')) {
return null;
}
$anno = AnnoGestioneContext::resolveActiveAnno();
$tipo = GestioneContext::resolveActiveGestione();
$match = GestioneContabile::query()
->where('stabile_id', $stabileId)
->where('anno_gestione', $anno)
->where('tipo_gestione', $tipo)
->where('stato', 'aperta')
->orderByDesc('gestione_attiva')
->orderByDesc('id')
->first();
if (! $match) {
$match = GestioneContabile::query()
->where('stabile_id', $stabileId)
->where('stato', 'aperta')
->orderByDesc('gestione_attiva')
->orderByDesc('id')
->first();
}
return $match ? (string) $match->id : null;
}
}