}> */ protected array $periodoRateMap = []; protected static ?bool $supportsWindowFunctions = null; protected static ?string $navigationLabel = 'Movimenti banca'; protected static ?string $title = 'Movimenti banca'; protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-banknotes'; protected static UnitEnum|string|null $navigationGroup = 'Contabilità'; protected static ?int $navigationSort = 11; protected static ?string $slug = 'contabilita/casse-banche/movimenti'; protected string $view = 'filament.pages.contabilita.casse-banche-movimenti'; 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 { $today = now(); $this->periodoAnno = (int) $today->year; $this->periodoValore = (int) $today->month; $this->contoId = is_numeric(request()->query('conto_id')) ? (int) request()->query('conto_id') : null; $this->contoId = $this->contoId && $this->contoId > 0 ? $this->contoId : null; $this->iban = is_string(request()->query('iban')) ? trim((string) request()->query('iban')) : null; $this->iban = $this->iban !== '' ? $this->iban : null; $this->mountInteractsWithTable(); // Risolvi conto selezionato: preferisci conto_id, altrimenti iban->conto_id. $user = Auth::user(); if ($user instanceof User) { $stabileId = StabileContext::resolveActiveStabileId($user); if ($stabileId) { if ($this->contoId) { $conto = DatiBancari::query() ->where('stabile_id', $stabileId) ->where('id', $this->contoId) ->first(['id', 'iban']); if (! $conto) { $this->contoId = null; } else { $iban = is_string($conto->iban) ? trim((string) $conto->iban) : ''; $this->iban = $iban !== '' ? $iban : null; } } if (! $this->contoId && $this->iban) { $conto = DatiBancari::query() ->where('stabile_id', $stabileId) ->where('iban', $this->iban) ->orderBy('id') ->first(['id']); if ($conto) { $this->contoId = (int) $conto->id; } } } } // Default: primo conto con IBAN (mantiene comportamento "movimenti banca" predefinito). if ($this->contoId === null && $this->iban === null) { $first = $this->getDefaultIban(); $this->iban = $first !== '' ? $first : null; if ($user instanceof User) { $stabileId = StabileContext::resolveActiveStabileId($user); if ($stabileId && $this->iban) { $conto = DatiBancari::query() ->where('stabile_id', $stabileId) ->where('iban', $this->iban) ->orderBy('id') ->first(['id']); if ($conto) { $this->contoId = (int) $conto->id; } } } } } public function getPeriodoTotaliProperty(): array { $user = Auth::user(); if (! $user instanceof User) { return ['entrate' => 0, 'uscite' => 0, 'saldo' => 0, 'count' => 0, 'label' => '']; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { return ['entrate' => 0, 'uscite' => 0, 'saldo' => 0, 'count' => 0, 'label' => '']; } $anno = (int) ($this->periodoAnno ?: now()->year); $val = (int) ($this->periodoValore ?: now()->month); $start = null; $end = null; $label = ''; if ($this->periodoTipo === 'trimestre') { $q = max(1, min(4, $val)); $start = Carbon::create($anno, (($q - 1) * 3) + 1, 1)->startOfDay(); $end = (clone $start)->addMonths(3)->subDay()->endOfDay(); $label = 'Trimestre ' . $q . '/' . $anno; } else { $m = max(1, min(12, $val)); $start = Carbon::create($anno, $m, 1)->startOfDay(); $end = (clone $start)->endOfMonth()->endOfDay(); $label = $start->format('m/Y'); } $query = MovimentoBanca::query() ->where('stabile_id', $stabileId) ->when($this->contoId !== null, fn(Builder $q): Builder => $q->where('conto_id', $this->contoId)) ->when($this->contoId === null && $this->iban !== null, fn(Builder $q): Builder => $q->where('iban', $this->iban)) ->whereDate('data', '>=', $start->toDateString()) ->whereDate('data', '<=', $end->toDateString()); $entrate = (float) $query->sum(DB::raw('CASE WHEN importo > 0 THEN importo ELSE 0 END')); $uscite = (float) $query->sum(DB::raw('CASE WHEN importo < 0 THEN ABS(importo) ELSE 0 END')); $count = (int) $query->count(); return [ 'entrate' => $entrate, 'uscite' => $uscite, 'saldo' => $entrate - $uscite, 'count' => $count, 'label' => $label, ]; } public function getPeriodoOptionsProperty(): array { $months = []; for ($m = 1; $m <= 12; $m++) { $months[$m] = str_pad((string) $m, 2, '0', STR_PAD_LEFT); } return [ 'mese' => $months, 'trimestre' => [1 => 'T1', 2 => 'T2', 3 => 'T3', 4 => 'T4'], ]; } public function getPeriodoRateOptionsProperty(): array { $loaded = $this->loadPeriodoRateOptions(); $options = $loaded['options']; if ($this->periodoRate === null && ! empty($options)) { $this->periodoRate = array_key_first($options); } return $options; } public function getPeriodoRateTotaliProperty(): array { $user = Auth::user(); if (! $user instanceof User) { return ['entrate' => 0, 'uscite' => 0, 'saldo' => 0, 'count' => 0, 'label' => '']; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { return ['entrate' => 0, 'uscite' => 0, 'saldo' => 0, 'count' => 0, 'label' => '']; } $loaded = $this->loadPeriodoRateOptions(); $options = $loaded['options']; $map = $loaded['map']; $selected = $this->periodoRate; if (! $selected || ! isset($map[$selected])) { return ['entrate' => 0, 'uscite' => 0, 'saldo' => 0, 'count' => 0, 'label' => '']; } $rawValues = $map[$selected]['raw'] ?? []; if (empty($rawValues)) { return ['entrate' => 0, 'uscite' => 0, 'saldo' => 0, 'count' => 0, 'label' => (string) ($map[$selected]['label'] ?? $selected)]; } $query = MovimentoBanca::query() ->where('stabile_id', $stabileId) ->when($this->contoId !== null, fn(Builder $q): Builder => $q->where('conto_id', $this->contoId)) ->when($this->contoId === null && $this->iban !== null, fn(Builder $q): Builder => $q->where('iban', $this->iban)) ->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(match_data, '$.periodo_rate')) in (" . implode(',', array_fill(0, count($rawValues), '?')) . ")", $rawValues); $entrate = (float) $query->sum(DB::raw('CASE WHEN importo > 0 THEN importo ELSE 0 END')); $uscite = (float) $query->sum(DB::raw('CASE WHEN importo < 0 THEN ABS(importo) ELSE 0 END')); $count = (int) $query->count(); return [ 'entrate' => $entrate, 'uscite' => $uscite, 'saldo' => $entrate - $uscite, 'count' => $count, 'label' => (string) ($map[$selected]['label'] ?? $selected), ]; } /** @return array{options:array, map:array}>} */ protected function loadPeriodoRateOptions(): array { if (! empty($this->periodoRateMap)) { $options = []; foreach ($this->periodoRateMap as $key => $payload) { $options[$key] = $payload['label']; } return ['options' => $options, 'map' => $this->periodoRateMap]; } $user = Auth::user(); if (! $user instanceof User) { return ['options' => [], 'map' => []]; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { return ['options' => [], 'map' => []]; } $values = MovimentoBanca::query() ->where('stabile_id', $stabileId) ->when($this->contoId !== null, fn(Builder $q): Builder => $q->where('conto_id', $this->contoId)) ->when($this->contoId === null && $this->iban !== null, fn(Builder $q): Builder => $q->where('iban', $this->iban)) ->whereRaw("JSON_EXTRACT(match_data, '$.periodo_rate') IS NOT NULL") ->selectRaw("DISTINCT JSON_UNQUOTE(JSON_EXTRACT(match_data, '$.periodo_rate')) AS periodo_rate") ->pluck('periodo_rate') ->filter(fn($v) => is_string($v) && trim($v) !== '') ->values() ->all(); $map = []; foreach ($values as $val) { $norm = $this->normalizePeriodoRate($val); if ($norm === null) { continue; } if (! isset($map[$norm])) { $map[$norm] = ['label' => $norm, 'raw' => []]; } $map[$norm]['raw'][] = $val; } ksort($map); $this->periodoRateMap = $map; $options = []; foreach ($map as $key => $payload) { $options[$key] = $payload['label']; } return ['options' => $options, 'map' => $map]; } protected function normalizePeriodoRate(?string $value): ?string { $v = trim((string) $value); if ($v === '') { return null; } $v = preg_replace('/\b(20)?\d{2}\b/', '', $v) ?? $v; $v = preg_replace('/\s{2,}/', ' ', $v) ?? $v; $v = trim($v, " \t\n\r\0\x0B-/"); return $v !== '' ? $v : null; } protected function getHeaderActions(): array { return [ Action::make('torna_riepilogo') ->label('Torna a Casse e banche') ->icon('heroicon-o-arrow-left') ->url(fn() => CasseBancheRiepilogo::getUrl(panel: 'admin-filament')), Action::make('saldi_conti') ->label('Saldi conti') ->icon('heroicon-o-scale') ->url(fn() => SaldiContiArchivio::getUrl(panel: 'admin-filament')), 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(function (): array { $opts = []; foreach ($this->getDisponibilitaFinanziarie() as $c) { $iban = is_string($c['iban'] ?? null) ? trim((string) $c['iban']) : ''; if ($iban === '') { continue; } $opts[$iban] = (string) ($c['label'] ?? $iban); } return $opts; }) ->default(fn(): ?string => (is_string($this->iban) && trim($this->iban) !== '') ? trim($this->iban) : null) ->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(function (): 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(); }) ->default(function (): ?string { $user = Auth::user(); if (! $user instanceof User) { return null; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { return null; } return $this->resolveDefaultGestioneId($stabileId); }), 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']) && $data['iban'] !== '' ? $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'] . ' · Duplicati: ' . $res['duplicates']) ->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('importa_intesa_csv') ->label('Importa estratto (Intesa CSV)') ->icon('heroicon-o-arrow-up-tray') ->modalWidth('7xl') ->form([ Select::make('iban') ->label('Conto (IBAN)') ->native(false) ->searchable() ->options(function (): array { $opts = []; foreach ($this->getDisponibilitaFinanziarie() as $c) { $iban = is_string($c['iban'] ?? null) ? trim((string) $c['iban']) : ''; if ($iban === '') { continue; } $opts[$iban] = (string) ($c['label'] ?? $iban); } return $opts; }) ->default(fn(): ?string => (is_string($this->iban) && trim($this->iban) !== '') ? trim($this->iban) : null) ->helperText('Consigliato: seleziona il conto per associare i movimenti. Se non selezionato, l\'import prova a rilevare l\'IBAN (quando presente).'), Select::make('gestione_id') ->label('Gestione (opzionale)') ->native(false) ->searchable() ->options(function (): 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(); }) ->default(function (): ?string { $user = Auth::user(); if (! $user instanceof User) { return null; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { return null; } return $this->resolveDefaultGestioneId($stabileId); }), FileUpload::make('file') ->label('File estratto') ->directory('tmp/banca') ->disk('local') ->required(), Toggle::make('replace') ->label('Sostituisci import nel periodo') ->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->importIntesaCsv( $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'] . ' · Duplicati: ' . $res['duplicates']) ->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('importa_excel_xlsx') ->label('Importa estratto (Excel XLSX)') ->icon('heroicon-o-arrow-up-tray') ->modalWidth('7xl') ->form([ Select::make('conto_id') ->label('Conto') ->native(false) ->searchable() ->options(function (): array { $opts = []; foreach ($this->getDisponibilitaFinanziarie() as $c) { if (! empty($c['id'])) { $opts[$c['id']] = (string) ($c['label'] ?? ('Conto #' . $c['id'])); } } return $opts; }) ->default(fn(): ?int => $this->contoId) ->required(), Select::make('gestione_id') ->label('Gestione (opzionale)') ->native(false) ->searchable() ->options(function (): 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(); }) ->default(function (): ?string { $user = Auth::user(); if (! $user instanceof User) { return null; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { return null; } return $this->resolveDefaultGestioneId($stabileId); }), FileUpload::make('file') ->label('File XLSX') ->directory('tmp/banca') ->disk('local') ->required(), Toggle::make('replace') ->label('Sostituisci import nel periodo') ->helperText('Elimina SOLO i movimenti importati e NON riconciliati 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; } $contoId = isset($data['conto_id']) && is_numeric($data['conto_id']) ? (int) $data['conto_id'] : null; if (! $contoId) { Notification::make()->title('Seleziona un conto')->danger()->send(); return; } $path = (string) ($data['file'] ?? ''); if ($path === '') { Notification::make()->title('Seleziona un file')->danger()->send(); return; } try { $full = Storage::disk('local')->path($path); $importer = new MovimentiBancaImporter(new UnicreditWriParser(), new IntesaCsvParser(), new ExcelMovimentiParser()); $res = $importer->importExcelXlsxForConto( $full, $stabileId, $contoId, basename($path), 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'] . ' · Duplicati: ' . $res['duplicates']) ->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 } } }), ]; } protected function getTableQuery(): Builder { $user = Auth::user(); if (! $user instanceof User) { return MovimentoBanca::query()->whereRaw('1 = 0'); } $activeStabileId = StabileContext::resolveActiveStabileId($user); if (! $activeStabileId) { return MovimentoBanca::query()->whereRaw('1 = 0'); } $query = MovimentoBanca::query() ->with('gestioneContabile') ->where('stabile_id', $activeStabileId) ->when($this->contoId !== null, fn(Builder $q): Builder => $q->where('conto_id', $this->contoId)) ->when($this->contoId === null && $this->iban !== null, fn(Builder $q): Builder => $q->where('iban', $this->iban)) ->orderBy('data') ->orderBy('id'); if ($this->dbSupportsWindowFunctions()) { $query->select('contabilita_movimenti_banca.*') ->selectRaw( "SUM(importo) OVER (PARTITION BY stabile_id, COALESCE(iban, '') ORDER BY data, id) AS saldo_progressivo" ); } return $query; } public function table(Table $table): Table { return $table ->striped() ->filters([ Filter::make('ricerca_descrizione') ->label('Ricerca') ->form([ TextInput::make('q') ->label('Descrizione') ->placeholder('Es. Basta Monica') ->autocomplete(false), ]) ->query(function (Builder $query, array $data): Builder { $q = trim((string) ($data['q'] ?? '')); if ($q === '') { return $query; } $qLike = '%' . mb_strtolower($q) . '%'; return $query->where(function (Builder $sub) use ($qLike): Builder { $sub->whereRaw('LOWER(descrizione) LIKE ?', [$qLike]) ->orWhereRaw('LOWER(descrizione_estesa) LIKE ?', [$qLike]) ->orWhereRaw('LOWER(note) LIKE ?', [$qLike]); if (Schema::hasColumn('contabilita_movimenti_banca', 'mittente')) { $sub->orWhereRaw('LOWER(mittente) LIKE ?', [$qLike]); } if (Schema::hasColumn('contabilita_movimenti_banca', 'intestatario')) { $sub->orWhereRaw('LOWER(intestatario) LIKE ?', [$qLike]); } if (Schema::hasColumn('contabilita_movimenti_banca', 'ordinante')) { $sub->orWhereRaw('LOWER(ordinante) LIKE ?', [$qLike]); } return $sub; }); }), Filter::make('periodo') ->label('Periodo') ->form([ DatePicker::make('from')->label('Dal'), DatePicker::make('to')->label('Al'), ]) ->query(function (Builder $query, array $data): Builder { $from = $data['from'] ?? null; $to = $data['to'] ?? null; return $query ->when($from, fn(Builder $q) => $q->whereDate('data', '>=', $from)) ->when($to, fn(Builder $q) => $q->whereDate('data', '<=', $to)); }), Filter::make('causale_bancaria') ->label('Causale') ->form([ Select::make('causale') ->label('Causale') ->native(false) ->searchable() ->options(function (): array { if (! DB::getSchemaBuilder()->hasTable('contabilita_causali_bancarie')) { return []; } return CausaleBancaria::query() ->where('attivo', true) ->orderBy('banca') ->orderBy('codice') ->get(['banca', 'codice', 'descrizione']) ->mapWithKeys(fn(CausaleBancaria $c) => [ (string) $c->codice => (string) $c->banca . ' · ' . (string) $c->codice . ' · ' . (string) $c->descrizione, ]) ->all(); }), ]) ->query(function (Builder $query, array $data): Builder { $code = $data['causale'] ?? null; $code = is_string($code) ? trim($code) : ''; if ($code === '') { return $query; } return $query->where('causale', $code); }), Filter::make('gestioni') ->label('Gestioni') ->form([ Select::make('gestione_ids') ->label('Gestione') ->multiple() ->options(function (): 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(); }) ->searchable(), ]) ->query(function (Builder $query, array $data): Builder { $ids = $data['gestione_ids'] ?? null; if (! is_array($ids) || $ids === []) { return $query; } $ids = array_values(array_filter(array_map('intval', $ids), fn(int $v) => $v > 0)); if ($ids === []) { return $query; } return $query->whereIn('gestione_id', $ids); }), ]) ->columns([ TextColumn::make('codice_descrizione') ->label('Codice') ->state(function (MovimentoBanca $record): string { $descr = (string) ($record->descrizione ?? ''); $raw = strtoupper($descr); if (str_contains($raw, 'ACCREDITO_BEU_CON_CONTABILE')) { return 'ACCBEUCONCON'; } if (str_contains($raw, 'COMBONMYB')) { return 'COMBONMYB'; } $raw = preg_replace('/[^A-Z0-9]+/', ' ', $raw) ?? $raw; $parts = array_filter(explode(' ', $raw), fn($v) => $v !== ''); if (! empty($parts)) { $code = ''; foreach ($parts as $p) { $chunk = substr($p, 0, 3); $code .= $chunk; if (strlen($code) >= 12) { break; } } $code = substr($code, 0, 12); if ($code !== '') { return $code; } } $raw = preg_replace('/[^A-Z0-9]+/', '_', $raw) ?? $raw; $raw = trim($raw, '_'); $raw = preg_replace('/_+/', '_', $raw) ?? $raw; return mb_substr($raw, 0, 32); }) ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('disp') ->label('Disp') ->state(function (MovimentoBanca $record): string { $match = is_array($record->match_data ?? null) ? $record->match_data : []; $disp = $match['cod_disp'] ?? null; return is_string($disp) ? trim($disp) : ''; }) ->toggleable(), TextColumn::make('cash') ->label('Cash') ->state(function (MovimentoBanca $record): string { $match = is_array($record->match_data ?? null) ? $record->match_data : []; $cash = $match['cash'] ?? null; return is_string($cash) ? trim($cash) : ''; }) ->toggleable(), TextColumn::make('mittente') ->label('Mittente') ->state(function (MovimentoBanca $record): string { $match = is_array($record->match_data ?? null) ? $record->match_data : []; $mitt = $match['mittente'] ?? null; return is_string($mitt) ? trim($mitt) : ''; }) ->wrap() ->toggleable(), BadgeColumn::make('commissioni') ->label('Comm.') ->getStateUsing(function (MovimentoBanca $record): string { $raw = strtoupper((string) ($record->descrizione ?? '')); return str_contains($raw, 'COMBONMYB') ? 'si' : 'no'; }) ->colors([ 'success' => 'si', 'gray' => 'no', ]) ->formatStateUsing(fn(string $state): string => $state === 'si' ? 'SI' : '—') ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('disp') ->label('Disp') ->state(function (MovimentoBanca $record): string { $match = is_array($record->match_data ?? null) ? $record->match_data : []; $disp = $match['cod_disp'] ?? null; return is_string($disp) ? trim($disp) : ''; }) ->toggleable(), BadgeColumn::make('da_confermare') ->label('Conferma') ->getStateUsing(fn(MovimentoBanca $record): string => ! empty($record->da_confermare) ? 'da_confermare' : 'ok') ->colors([ 'warning' => 'da_confermare', 'success' => 'ok', ]) ->formatStateUsing(fn(string $state): string => $state === 'da_confermare' ? 'DA CONF.' : 'OK') ->toggleable(isToggledHiddenByDefault: true), BadgeColumn::make('stato_quadratura') ->label('Stato') ->getStateUsing(fn(MovimentoBanca $record): string => $this->resolveStatoQuadratura($record)) ->colors([ 'gray' => 'non_collegato', 'warning' => 'collegato_non_quadrato', 'success' => 'quadrato', ]) ->formatStateUsing(function (string $state): string { return match ($state) { 'quadrato' => 'OK', 'collegato_non_quadrato' => 'ATT', default => 'NO', }; }) ->sortable(false), TextColumn::make('voci_collegate') ->label('Voce spesa') ->state(function (MovimentoBanca $record): string { if (! $record->registrazione_id || ! Schema::hasColumn('contabilita_movimenti', 'voce_spesa_id')) { return '—'; } static $cache = []; $regId = (int) $record->registrazione_id; if (array_key_exists($regId, $cache)) { return $cache[$regId]; } $rows = DB::table('contabilita_movimenti as m') ->leftJoin('voci_spesa as v', 'v.id', '=', 'm.voce_spesa_id') ->where('m.registrazione_id', $regId) ->whereNotNull('m.voce_spesa_id') ->select('v.codice', 'v.descrizione') ->distinct() ->limit(5) ->get(); if ($rows->isEmpty()) { return $cache[$regId] = '—'; } $labels = []; foreach ($rows as $row) { $code = trim((string) ($row->codice ?? '')); $desc = trim((string) ($row->descrizione ?? '')); $labels[] = $code !== '' ? ($code . ($desc !== '' ? ' · ' . $desc : '')) : ($desc !== '' ? $desc : 'Voce'); } return $cache[$regId] = implode(' | ', array_values(array_unique($labels))); }) ->wrap() ->toggleable(), TextColumn::make('data') ->label('Data') ->date('d/m/Y') ->sortable(), TextColumn::make('valuta') ->label('Valuta') ->date('d/m/Y') ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('descrizione') ->label('Descrizione') ->state(fn(MovimentoBanca $record): string => (string) ($record->descrizione_estesa_pulita ?? $record->descrizione_estesa ?? $record->descrizione ?? '')) ->wrap() ->searchable(), TextColumn::make('gestioneContabile.denominazione') ->label('Gestione') ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('entrate') ->label('Entrate') ->alignEnd() ->state(fn(MovimentoBanca $record): float => max(0, (float) $record->importo)) ->formatStateUsing(fn($state) => $state > 0 ? ('€ ' . number_format((float) $state, 2, ',', '.')) : ''), TextColumn::make('uscite') ->label('Uscite') ->alignEnd() ->state(fn(MovimentoBanca $record): float => max(0, abs((float) $record->importo) * ((float) $record->importo < 0 ? 1 : 0))) ->formatStateUsing(fn($state) => $state > 0 ? ('€ ' . number_format((float) $state, 2, ',', '.')) : ''), TextColumn::make('saldo_progressivo') ->label('Saldo') ->alignEnd() ->getStateUsing(fn(MovimentoBanca $record): ?float => $this->resolveSaldoConSaldi($record)) ->formatStateUsing(fn($state) => $state !== null ? ('€ ' . number_format((float) $state, 2, ',', '.')) : ''), TextColumn::make('importo') ->label('Importo (raw)') ->alignEnd() ->toggleable(isToggledHiddenByDefault: true) ->formatStateUsing(fn($state) => '€ ' . number_format((float) $state, 2, ',', '.')), TextColumn::make('causale') ->label('Caus.') ->toggleable(), TextColumn::make('iban') ->label('IBAN') ->toggleable(isToggledHiddenByDefault: true), ]) ->actions([ Action::make('dettaglio_movimento') ->label('Dettaglio') ->icon('heroicon-o-eye') ->modalWidth('7xl') ->action(function (MovimentoBanca $record): void { $this->detailMovementId = $record->id; }) ->modalHeading(fn(): string => 'Dettaglio movimento') ->modalContent(function () { $record = $this->getDetailMovement(); return view('filament.pages.contabilita.partials.movimento-banca-dettaglio', [ 'record' => $record, 'prevId' => $this->getAdjacentMovementId('prev'), 'nextId' => $this->getAdjacentMovementId('next'), ]); }), Action::make('conferma_movimento') ->label('Conferma') ->icon('heroicon-o-check-circle') ->visible(fn(MovimentoBanca $record): bool => (bool) ($record->da_confermare ?? false)) ->action(function (MovimentoBanca $record): void { if (! Schema::hasColumn('contabilita_movimenti_banca', 'da_confermare')) { return; } $record->da_confermare = false; $record->save(); Notification::make()->title('Movimento confermato')->success()->send(); $this->resetTable(); }), Action::make('genera_prima_nota') ->label('Genera prima nota') ->icon('heroicon-o-book-open') ->visible(function (MovimentoBanca $record): bool { if (! Schema::hasColumn('contabilita_movimenti_banca', 'registrazione_id')) { return false; } return empty($record->registrazione_id); }) ->form([ Select::make('gestione_id') ->label('Gestione') ->options(function (): array { if (! DB::getSchemaBuilder()->hasTable('gestioni_contabili')) { return []; } $user = Auth::user(); if (! $user instanceof User) { return []; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { return []; } return GestioneContabile::query() ->where('stabile_id', $stabileId) ->orderByDesc('anno_gestione') ->orderBy('tipo_gestione') ->orderBy('numero_straordinaria') ->get(['id', 'denominazione', 'tipo_gestione', 'anno_gestione', 'numero_straordinaria']) ->mapWithKeys(fn(GestioneContabile $g) => [ (string) $g->id => $g->denominazione . ' (' . $g->tipo_gestione . ' ' . $g->anno_gestione . ($g->numero_straordinaria ? ' #' . $g->numero_straordinaria : '') . ')', ]) ->all(); }) ->searchable() ->visible(fn(MovimentoBanca $record) => empty($record->gestione_id)) ->required(fn(MovimentoBanca $record) => empty($record->gestione_id)), ]) ->action(function (MovimentoBanca $record, array $data): void { $user = Auth::user(); if (! $user instanceof User) { Notification::make()->title('Utente non valido')->danger()->send(); return; } if (empty($record->gestione_id) && isset($data['gestione_id']) && is_numeric($data['gestione_id'])) { $record->gestione_id = (int) $data['gestione_id']; $record->save(); } try { $service = app(MovimentoBancaPrimaNotaService::class); $service->generaRegistrazioneDaMovimento($user, $record); Notification::make()->title('Prima nota generata')->success()->send(); $this->resetTable(); } catch (\Throwable $e) { Notification::make()->title('Errore')->body($e->getMessage())->danger()->send(); } }), Action::make('paga_fornitore') ->label('Paga fornitore') ->icon('heroicon-o-banknotes') ->visible(function (MovimentoBanca $record): bool { if (! Schema::hasColumn('contabilita_movimenti_banca', 'registrazione_id')) { return false; } if (! Schema::hasTable('contabilita_registrazioni') || ! Schema::hasTable('contabilita_movimenti')) { return false; } $importo = (float) ($record->importo ?? 0); if ($importo >= 0) { return false; } return empty($record->registrazione_id); }) ->form([ Select::make('gestione_id') ->label('Gestione') ->options(function (): array { if (! DB::getSchemaBuilder()->hasTable('gestioni_contabili')) { return []; } $user = Auth::user(); if (! $user instanceof User) { return []; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { return []; } return GestioneContabile::query() ->where('stabile_id', $stabileId) ->orderByDesc('anno_gestione') ->orderBy('tipo_gestione') ->orderBy('numero_straordinaria') ->get(['id', 'denominazione', 'tipo_gestione', 'anno_gestione', 'numero_straordinaria']) ->mapWithKeys(fn(GestioneContabile $g) => [ (string) $g->id => $g->denominazione . ' (' . $g->tipo_gestione . ' ' . $g->anno_gestione . ($g->numero_straordinaria ? ' #' . $g->numero_straordinaria : '') . ')', ]) ->all(); }) ->searchable() ->visible(fn(MovimentoBanca $record) => empty($record->gestione_id)) ->required(fn(MovimentoBanca $record) => empty($record->gestione_id)), Select::make('fornitore_id') ->label('Fornitore') ->options(function (): array { $q = Fornitore::query(); if (Schema::hasColumn('fornitori', 'ragione_sociale')) { $q->orderBy('ragione_sociale'); } elseif (Schema::hasColumn('fornitori', 'cognome')) { $q->orderBy('cognome'); } return $q ->limit(500) ->get(['id', 'ragione_sociale', 'nome', 'cognome']) ->mapWithKeys(function (Fornitore $f): array { $label = is_string($f->ragione_sociale ?? null) && trim((string) $f->ragione_sociale) !== '' ? trim((string) $f->ragione_sociale) : trim(((string) ($f->cognome ?? '')) . ' ' . ((string) ($f->nome ?? ''))); $label = $label !== '' ? $label : ('Fornitore #' . (string) $f->id); return [(string) $f->id => $label]; }) ->all(); }) ->searchable() ->required(), Select::make('conto_finanziario_id') ->label('Conto finanziario (Banca/Cassa)') ->options(function (): array { $base = PianoConti::query(); if (Schema::hasColumn('contabilita_piano_conti', 'tipo')) { $base->where('tipo', 'Attività'); } $q = (clone $base)->where(function ($w) { $w->where('descrizione', 'like', '%Banca%') ->orWhere('descrizione', 'like', '%banca%') ->orWhere('descrizione', 'like', '%Cassa%') ->orWhere('descrizione', 'like', '%cassa%'); }); $rows = $q->orderBy('codice')->limit(500)->get(['id', 'codice', 'descrizione']); if ($rows->count() === 0) { $rows = $base->orderBy('codice')->limit(500)->get(['id', 'codice', 'descrizione']); } return $rows ->mapWithKeys(fn(PianoConti $c) => [(string) $c->id => $c->codice . ' - ' . $c->descrizione]) ->all(); }) ->searchable() ->required(), ]) ->action(function (MovimentoBanca $record, array $data): void { $user = Auth::user(); if (! $user instanceof User) { Notification::make()->title('Utente non valido')->danger()->send(); return; } $gestioneId = (empty($record->gestione_id) && isset($data['gestione_id']) && is_numeric($data['gestione_id'])) ? (int) $data['gestione_id'] : (int) ($record->gestione_id ?? 0); $fornitoreId = isset($data['fornitore_id']) && is_numeric($data['fornitore_id']) ? (int) $data['fornitore_id'] : 0; $contoFinId = isset($data['conto_finanziario_id']) && is_numeric($data['conto_finanziario_id']) ? (int) $data['conto_finanziario_id'] : 0; try { $service = app(PagamentoFornitorePrimaNotaService::class); $service->generaPagamentoDaMovimento($user, $record, $fornitoreId, [ 'gestione_id' => $gestioneId, 'conto_finanziario_id' => $contoFinId, ]); Notification::make()->title('Pagamento contabilizzato')->success()->send(); $this->resetTable(); } catch (\Throwable $e) { Notification::make()->title('Errore')->body($e->getMessage())->danger()->send(); } }), ]); } public function getDetailMovement(): ?MovimentoBanca { if (! $this->detailMovementId) { return null; } return MovimentoBanca::query()->where('id', $this->detailMovementId)->first(); } public function setDetailMovement(int $id): void { $this->detailMovementId = $id; } private function getAdjacentMovementId(string $dir): ?int { if (! $this->detailMovementId) { return null; } $current = MovimentoBanca::query()->where('id', $this->detailMovementId)->first(['id', 'data']); if (! $current) { return null; } $query = MovimentoBanca::query() ->when($this->contoId !== null, fn(Builder $q): Builder => $q->where('conto_id', $this->contoId)) ->when($this->contoId === null && $this->iban !== null, fn(Builder $q): Builder => $q->where('iban', $this->iban)); if ($dir === 'prev') { $query->where(function (Builder $q) use ($current): void { $q->where('data', '<', $current->data) ->orWhere(function (Builder $q2) use ($current): void { $q2->where('data', $current->data)->where('id', '<', $current->id); }); })->orderByDesc('data')->orderByDesc('id'); } else { $query->where(function (Builder $q) use ($current): void { $q->where('data', '>', $current->data) ->orWhere(function (Builder $q2) use ($current): void { $q2->where('data', $current->data)->where('id', '>', $current->id); }); })->orderBy('data')->orderBy('id'); } return $query->value('id'); } /** @return array */ protected function getActiveStabileIbans(): array { $user = Auth::user(); if (! $user instanceof User) { return []; } $stabile = StabileContext::getActiveStabile($user); if (! $stabile) { return []; } $ibans = []; foreach ( [ $stabile->iban_principale ?? null, $stabile->iban_secondario ?? null, $stabile->iban_condominio ?? null, ] as $iban ) { $iban = is_string($iban) ? trim($iban) : ''; if ($iban !== '') { $ibans[$iban] = $iban; } } return $ibans; } protected function resolveSaldoProgressivoConSaldoIniziale(MovimentoBanca $record): ?float { $base = $this->resolveSaldoProgressivo($record); if ($base === null) { return null; } $statePeriodo = $this->getTableFilterState('periodo'); $from = is_array($statePeriodo) ? ($statePeriodo['from'] ?? null) : null; if (! $from) { return $base; } $iban = is_string($this->iban) ? trim($this->iban) : ''; if ($iban === '') { return $base; } $saldoIniziale = $this->resolveSaldoIniziale((int) $record->stabile_id, $iban, (string) $from); if ($saldoIniziale === null) { return $base; } return $saldoIniziale + $base; } protected function resolveSaldoConSaldi(MovimentoBanca $record): ?float { $baseProgressivo = $this->resolveSaldoProgressivo($record); if ($baseProgressivo === null) { return null; } $stabileId = (int) $record->stabile_id; $iban = is_string($record->iban) ? trim((string) $record->iban) : ''; if ($iban === '') { // Per conti senza IBAN, al momento i movimenti non sono importati: saldo progressivo = somma importi. return (float) $baseProgressivo; } $saldoIniziale = $this->resolveSaldoInizialeDaDatiBancari($stabileId, $this->contoId, $iban); // Offset da eventuali saldi intermedi: saldo atteso alla data_saldo meno saldo calcolato fino a quella data. $offset = $this->resolveSaldoOffsetDaSaldi($stabileId, $this->contoId, $iban, $record->data); return $saldoIniziale + (float) $baseProgressivo + $offset; } protected function resolveSaldoInizialeDaDatiBancari(int $stabileId, ?int $contoId, string $iban): float { static $cache = []; $key = $stabileId . '|' . ($contoId ?: 0) . '|' . $iban; if (array_key_exists($key, $cache)) { return (float) $cache[$key]; } $q = DatiBancari::query()->where('stabile_id', $stabileId); if ($contoId) { $q->where('id', $contoId); } else { $q->where('iban', $iban); } $row = $q->first(['saldo_iniziale']); $cache[$key] = $row && is_numeric($row->saldo_iniziale) ? (float) $row->saldo_iniziale : 0.0; return (float) $cache[$key]; } protected function resolveSaldoOffsetDaSaldi(int $stabileId, ?int $contoId, string $iban, Carbon $recordDate): float { if (! DB::getSchemaBuilder()->hasTable('contabilita_saldi_conti')) { return 0.0; } static $saldiCache = []; $cacheKey = $stabileId . '|' . ($contoId ?: 0) . '|' . $iban; if (! array_key_exists($cacheKey, $saldiCache)) { $q = SaldoConto::query()->where('stabile_id', $stabileId); if ($contoId) { $q->where('conto_id', $contoId); } else { $q->where('iban', $iban); } $saldiCache[$cacheKey] = $q ->orderBy('data_saldo') ->orderBy('id') ->get(['id', 'data_saldo', 'saldo']) ->all(); } $saldi = $saldiCache[$cacheKey]; if (! is_array($saldi) || $saldi === []) { return 0.0; } $match = null; foreach ($saldi as $s) { if (! $s || ! isset($s->data_saldo)) { continue; } if ($s->data_saldo->toDateString() <= $recordDate->toDateString()) { $match = $s; } else { break; } } if (! $match) { return 0.0; } static $sumToDateCache = []; $sumKey = $cacheKey . '|' . $match->data_saldo->toDateString() . '|' . (int) $match->id; if (! array_key_exists($sumKey, $sumToDateCache)) { $q = MovimentoBanca::query() ->where('stabile_id', $stabileId) ->where('iban', $iban) ->whereDate('data', '<=', $match->data_saldo->toDateString()); if ($contoId) { $q->where('conto_id', $contoId); } $sumToDateCache[$sumKey] = (float) ($q->sum('importo') ?? 0.0); } $saldoIniziale = $this->resolveSaldoInizialeDaDatiBancari($stabileId, $contoId, $iban); $saldoAttesoAllaDataSaldo = (float) $match->saldo; $saldoCalcolatoAllaDataSaldo = $saldoIniziale + (float) $sumToDateCache[$sumKey]; return $saldoAttesoAllaDataSaldo - $saldoCalcolatoAllaDataSaldo; } protected function resolveSaldoIniziale(int $stabileId, string $iban, string $fromDate): ?float { if (! DB::getSchemaBuilder()->hasTable('contabilita_saldi_conti')) { return null; } static $cache = []; $key = $stabileId . '|' . $iban . '|' . $fromDate; if (array_key_exists($key, $cache)) { return $cache[$key]; } $row = SaldoConto::query() ->where('stabile_id', $stabileId) ->where('iban', $iban) ->whereDate('data_saldo', '<=', $fromDate) ->orderByDesc('data_saldo') ->orderByDesc('id') ->first(); $cache[$key] = $row ? (float) $row->saldo : null; return $cache[$key]; } protected function resolveStatoQuadratura(MovimentoBanca $record): string { if ($record->registrazione_id === null) { return 'non_collegato'; } try { $row = DB::table('contabilita_movimenti') ->where('registrazione_id', (int) $record->registrazione_id) ->selectRaw("SUM(CASE WHEN tipo = 'dare' THEN importo ELSE 0 END) AS dare") ->selectRaw("SUM(CASE WHEN tipo = 'avere' THEN importo ELSE 0 END) AS avere") ->first(); $dare = $row && isset($row->dare) ? (float) $row->dare : 0.0; $avere = $row && isset($row->avere) ? (float) $row->avere : 0.0; if (abs($dare - $avere) <= 0.01) { return 'quadrato'; } return 'collegato_non_quadrato'; } catch (\Throwable) { return 'collegato_non_quadrato'; } } protected function resolveDefaultGestioneId(int $stabileId): ?string { 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; } protected function resolveSaldoProgressivo(MovimentoBanca $record): ?float { if (isset($record->saldo_progressivo)) { return is_numeric($record->saldo_progressivo) ? (float) $record->saldo_progressivo : null; } // Fallback (DB senza window functions): calcolo best-effort sul conto (IBAN) fino a questa riga. $q = MovimentoBanca::query() ->where('stabile_id', (int) $record->stabile_id) ->where(function (Builder $q) use ($record) { if ($record->iban === null) { $q->whereNull('iban'); } else { $q->where('iban', $record->iban); } }) ->where(function (Builder $q) use ($record) { $q->whereDate('data', '<', $record->data->toDateString()) ->orWhere(function (Builder $q) use ($record) { $q->whereDate('data', '=', $record->data->toDateString()) ->where('id', '<=', (int) $record->id); }); }); return (float) ($q->sum('importo') ?? 0); } protected function dbSupportsWindowFunctions(): bool { if (self::$supportsWindowFunctions !== null) { return self::$supportsWindowFunctions; } try { $driver = DB::getDriverName(); if ($driver === 'pgsql' || $driver === 'sqlite') { return self::$supportsWindowFunctions = true; } if ($driver !== 'mysql') { return self::$supportsWindowFunctions = false; } $row = DB::selectOne('select version() as v'); $v = is_object($row) && isset($row->v) ? (string) $row->v : ''; $vLower = strtolower($v); if (str_contains($vLower, 'mariadb')) { // MariaDB window functions: 10.2+ if (preg_match('/(\d+)\.(\d+)\.(\d+)/', $v, $m)) { $major = (int) $m[1]; $minor = (int) $m[2]; return self::$supportsWindowFunctions = ($major > 10) || ($major === 10 && $minor >= 2); } return self::$supportsWindowFunctions = false; } // MySQL window functions: 8.0+ if (preg_match('/(\d+)\.(\d+)\.(\d+)/', $v, $m)) { $major = (int) $m[1]; $minor = (int) $m[2]; return self::$supportsWindowFunctions = ($major > 8) || ($major === 8 && $minor >= 0); } return self::$supportsWindowFunctions = false; } catch (\Throwable) { return self::$supportsWindowFunctions = false; } } public function getActiveStabile(): ?Stabile { $user = Auth::user(); if (! $user instanceof User) { return null; } return StabileContext::getActiveStabile($user); } /** @return array */ public function getDisponibilitaFinanziarie(): array { $user = Auth::user(); if (! $user instanceof User) { return []; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { return []; } $conti = []; if (DB::getSchemaBuilder()->hasTable('dati_bancari')) { $conti = DatiBancari::query() ->where('stabile_id', $stabileId) ->whereNotNull('iban') ->where('iban', '!=', '') ->orderBy('denominazione_banca') ->orderBy('iban') ->get(['id', 'denominazione_banca', 'iban', 'saldo_iniziale']) ->map(fn(DatiBancari $r) => [ 'id' => (int) $r->id, 'iban' => trim((string) $r->iban), 'label' => trim((string) ($r->denominazione_banca ?: 'Conto') . ' · ' . (string) $r->iban), 'saldo_iniziale' => is_numeric($r->saldo_iniziale) ? (float) $r->saldo_iniziale : 0.0, ]) ->filter(fn(array $r) => $r['iban'] !== '') ->values() ->all(); } $sums = MovimentoBanca::query() ->where('stabile_id', $stabileId) ->whereNotNull('iban') ->where('iban', '!=', '') ->groupBy('iban') ->selectRaw('iban, COALESCE(SUM(importo), 0) AS saldo_movimenti') ->pluck('saldo_movimenti', 'iban') ->map(fn($v) => (float) $v) ->all(); $byIban = []; foreach ($conti as $c) { $iban = $c['iban']; $saldo = ($c['saldo_iniziale'] ?? 0.0) + ($sums[$iban] ?? 0.0); $byIban[$iban] = [ 'id' => $c['id'] ?? null, 'iban' => $iban, 'label' => (string) $c['label'], 'saldo' => $saldo, ]; } foreach ($sums as $iban => $saldoMov) { if (! isset($byIban[$iban])) { $byIban[$iban] = [ 'id' => null, 'iban' => (string) $iban, 'label' => (string) $iban, 'saldo' => (float) $saldoMov, ]; } } uasort($byIban, fn($a, $b) => strcmp((string) $a['label'], (string) $b['label'])); return array_values($byIban); } public function getSelectedIbanLabel(): ?string { $iban = is_string($this->iban) ? trim($this->iban) : ''; if ($iban === '') { return null; } foreach ($this->getDisponibilitaFinanziarie() as $c) { if (($c['iban'] ?? '') === $iban) { return (string) ($c['label'] ?? $iban); } } return $iban; } protected function getDefaultIban(): string { foreach ($this->getDisponibilitaFinanziarie() as $c) { $iban = is_string($c['iban'] ?? null) ? trim((string) $c['iban']) : ''; if ($iban !== '') { return $iban; } } return ''; } /** @return array{label:string,data:?string,saldo:?float} */ public function getHeaderSaldoInfo(): array { $user = Auth::user(); if (! $user instanceof User) { return ['label' => 'Movimenti', 'data' => null, 'saldo' => null]; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { return ['label' => 'Movimenti', 'data' => null, 'saldo' => null]; } $conto = null; if ($this->contoId) { $conto = DatiBancari::query() ->where('stabile_id', $stabileId) ->where('id', $this->contoId) ->first(['id', 'denominazione_banca', 'iban']); } elseif ($this->iban) { $conto = DatiBancari::query() ->where('stabile_id', $stabileId) ->where('iban', $this->iban) ->orderBy('id') ->first(['id', 'denominazione_banca', 'iban']); } $label = $conto ? trim((string) ($conto->denominazione_banca ?: 'Conto') . ($conto->iban ? (' · ' . $conto->iban) : '')) : (string) ($this->getSelectedIbanLabel() ?: 'Movimenti'); $movQ = MovimentoBanca::query()->where('stabile_id', $stabileId); if ($this->contoId) { $movQ->where('conto_id', $this->contoId); } elseif ($this->iban) { $movQ->where('iban', $this->iban); } $lastMov = $movQ->orderByDesc('data')->orderByDesc('id')->first(['data']); // Saldo attuale: usa l'ultimo saldo inserito come base (se presente), poi somma i movimenti successivi. $iban = $this->iban; if (! is_string($iban) || trim($iban) === '') { return ['label' => $label, 'data' => $lastMov ? $lastMov->data->format('d/m/Y') : null, 'saldo' => null]; } $iban = trim($iban); $saldoBase = null; $dataBase = null; $periodFrom = null; if (is_array($this->tableFilters ?? null)) { $periodFrom = $this->tableFilters['periodo']['from'] ?? null; $periodFrom = is_string($periodFrom) && trim($periodFrom) !== '' ? trim($periodFrom) : null; } if (DB::getSchemaBuilder()->hasTable('contabilita_saldi_conti')) { $qSaldo = SaldoConto::query()->where('stabile_id', $stabileId); if ($this->contoId) { $qSaldo->where('conto_id', $this->contoId); } else { $qSaldo->where('iban', $iban); } if ($periodFrom) { $qSaldo->whereDate('data_saldo', '<=', $periodFrom); } $row = $qSaldo->orderByDesc('data_saldo')->orderByDesc('id')->first(['data_saldo', 'saldo']); if ($row) { $saldoBase = (float) $row->saldo; $dataBase = $row->data_saldo->toDateString(); } } $saldoIniziale = $this->resolveSaldoInizialeDaDatiBancari($stabileId, $this->contoId, $iban); $sumQ = MovimentoBanca::query()->where('stabile_id', $stabileId)->where('iban', $iban); if ($this->contoId) { $sumQ->where('conto_id', $this->contoId); } if ($dataBase) { $sumQ->whereDate('data', '>', $dataBase); } $sumAfter = (float) ($sumQ->sum('importo') ?? 0.0); $saldoAttuale = ($saldoBase !== null ? $saldoBase : $saldoIniziale) + $sumAfter; $dataLabel = $lastMov ? $lastMov->data->format('d/m/Y') : ($dataBase ? Carbon::parse($dataBase)->format('d/m/Y') : null); return ['label' => $label, 'data' => $dataLabel, 'saldo' => $saldoAttuale]; } }