From f1836763ae5f8691661b1e1a247a0d439f578225 Mon Sep 17 00:00:00 2001 From: michele Date: Wed, 15 Apr 2026 21:46:33 +0000 Subject: [PATCH] Unify bank import flow and add MPS QIF support --- .../Contabilita/CasseBancheMovimenti.php | 492 ++++++++---------- .../Contabilita/MovimentiBancaImporter.php | 75 +++ app/Services/Contabilita/MpsQifParser.php | 162 ++++++ .../casse-banche-movimenti.blade.php | 20 + 4 files changed, 483 insertions(+), 266 deletions(-) create mode 100644 app/Services/Contabilita/MpsQifParser.php diff --git a/app/Filament/Pages/Contabilita/CasseBancheMovimenti.php b/app/Filament/Pages/Contabilita/CasseBancheMovimenti.php index c6ecf5f..dafd2d0 100644 --- a/app/Filament/Pages/Contabilita/CasseBancheMovimenti.php +++ b/app/Filament/Pages/Contabilita/CasseBancheMovimenti.php @@ -14,6 +14,7 @@ use App\Modules\Contabilita\Services\PagamentoFornitorePrimaNotaService; use App\Services\Contabilita\ExcelMovimentiParser; use App\Services\Contabilita\IntesaCsvParser; +use App\Services\Contabilita\MpsQifParser; use App\Services\Contabilita\MovimentiBancaImporter; use App\Services\Contabilita\UnicreditWriParser; use App\Support\AnnoGestioneContext; @@ -348,272 +349,45 @@ protected function getHeaderActions(): array ->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)') + Action::make('importa_estratto_unificato') + ->label('Importa estratto') ->icon('heroicon-o-arrow-up-tray') ->modalWidth('7xl') ->form([ Select::make('conto_id') - ->label('Conto') + ->label('Conto / cassa') ->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'])); + foreach ($this->getContiImportabili() as $c) { + $contoId = isset($c['id']) && is_numeric($c['id']) ? (int) $c['id'] : null; + if (! $contoId) { + continue; } + $opts[$contoId] = (string) ($c['label'] ?? ('Conto #' . $contoId)); } return $opts; }) ->default(fn(): ?int => $this->contoId) - ->required(), + ->required() + ->helperText('Obbligatorio: l\'import viene sempre legato al conto/cassa selezionato, usando il codice legacy quando presente.'), + + Select::make('formato') + ->label('Formato file') + ->native(false) + ->options(function (): array { + return [ + 'auto_csv' => 'CSV banca (rilevamento automatico)', + 'mps_qif' => 'MPS QIF', + 'unicredit_wri' => 'Unicredit WRI', + 'intesa_csv' => 'Intesa CSV', + 'xlsx' => 'Excel XLSX', + ]; + }) + ->default(fn(): string => $this->resolvePreferredImportFormatForConto($this->contoId)) + ->required() + ->helperText('Un solo flusso di import: scegli il formato corretto per il conto selezionato. Per Monte dei Paschi usa QIF.'), Select::make('gestione_id') ->label('Gestione (opzionale)') @@ -658,9 +432,20 @@ protected function getHeaderActions(): array }), FileUpload::make('file') - ->label('File XLSX') + ->label('File estratto') ->directory('tmp/banca') ->disk('local') + ->acceptedFileTypes([ + 'text/plain', + 'text/csv', + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + '.csv', + '.txt', + '.wri', + '.qif', + '.xlsx', + ]) ->required(), Toggle::make('replace') @@ -683,7 +468,17 @@ protected function getHeaderActions(): array $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(); + Notification::make()->title('Seleziona un conto o una cassa')->danger()->send(); + return; + } + + $conto = DatiBancari::query() + ->where('stabile_id', $stabileId) + ->where('id', $contoId) + ->first(['id', 'iban']); + + if (! $conto) { + Notification::make()->title('Conto non valido per lo stabile attivo')->danger()->send(); return; } @@ -693,21 +488,69 @@ protected function getHeaderActions(): array return; } + $format = isset($data['formato']) && is_string($data['formato']) ? trim((string) $data['formato']) : ''; + if ($format === '') { + Notification::make()->title('Seleziona il formato del file')->danger()->send(); + return; + } + + $gestioneId = isset($data['gestione_id']) && is_numeric($data['gestione_id']) ? (int) $data['gestione_id'] : null; + $replace = isset($data['replace']) ? (bool) $data['replace'] : false; + 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, - ); + $importer = $this->makeMovimentiImporter(); + $filename = basename($path); + + $res = match ($format) { + 'xlsx' => $importer->importExcelXlsxForConto( + Storage::disk('local')->path($path), + $stabileId, + $contoId, + $filename, + $gestioneId, + $replace, + ), + 'unicredit_wri' => $importer->importUnicreditWriForConto( + Storage::disk('local')->get($path), + $stabileId, + $contoId, + $filename, + $gestioneId, + $replace, + ), + 'intesa_csv' => $importer->importIntesaCsvForConto( + Storage::disk('local')->get($path), + $stabileId, + $contoId, + $filename, + $gestioneId, + $replace, + ), + 'mps_qif' => $importer->importMpsQifForConto( + Storage::disk('local')->get($path), + $stabileId, + $contoId, + $filename, + $gestioneId, + $replace, + ), + default => $importer->importCsvAutoForConto( + Storage::disk('local')->get($path), + $stabileId, + $contoId, + $filename, + $gestioneId, + $replace, + ), + }; + + $this->contoId = $contoId; + $iban = is_string($conto->iban) ? trim((string) $conto->iban) : ''; + $this->iban = $iban !== '' ? $iban : null; Notification::make() ->title('Import completato') - ->body('Importati: ' . $res['imported'] . ' · Duplicati: ' . $res['duplicates']) + ->body('Formato: ' . $format . ' · Importati: ' . $res['imported'] . ' · Duplicati: ' . $res['duplicates']) ->success() ->send(); } catch (\Throwable $e) { @@ -1751,6 +1594,123 @@ public function getDisponibilitaFinanziarie(): array return array_values($byIban); } + /** @return array */ + public function getContiImportabili(): array + { + $user = Auth::user(); + if (! $user instanceof User) { + return []; + } + + $stabileId = StabileContext::resolveActiveStabileId($user); + if (! $stabileId || ! DB::getSchemaBuilder()->hasTable('dati_bancari')) { + return []; + } + + return DatiBancari::query() + ->where('stabile_id', $stabileId) + ->orderBy('denominazione_banca') + ->orderBy('legacy_cod_cassa') + ->orderBy('iban') + ->orderBy('numero_conto') + ->get(['id', 'denominazione_banca', 'iban', 'numero_conto', 'legacy_cod_cassa']) + ->map(function (DatiBancari $conto): array { + $iban = is_string($conto->iban) ? trim((string) $conto->iban) : null; + $iban = $iban !== '' ? $iban : null; + $numeroConto = is_string($conto->numero_conto) ? trim((string) $conto->numero_conto) : null; + $numeroConto = $numeroConto !== '' ? $numeroConto : null; + $legacyCodCassa = is_string($conto->legacy_cod_cassa) ? strtoupper(trim((string) $conto->legacy_cod_cassa)) : null; + $legacyCodCassa = $legacyCodCassa !== '' ? $legacyCodCassa : null; + $denominazione = is_string($conto->denominazione_banca) ? trim((string) $conto->denominazione_banca) : null; + $denominazione = $denominazione !== '' ? $denominazione : null; + + $parts = []; + $parts[] = $denominazione ?: 'Conto'; + if ($legacyCodCassa) { + $parts[] = 'cassa ' . $legacyCodCassa; + } + if ($iban) { + $parts[] = $iban; + } elseif ($numeroConto) { + $parts[] = 'n. ' . $numeroConto; + } + + return [ + 'id' => (int) $conto->id, + 'iban' => $iban, + 'label' => implode(' · ', $parts), + 'legacy_cod_cassa' => $legacyCodCassa, + 'numero_conto' => $numeroConto, + 'denominazione_banca' => $denominazione, + ]; + }) + ->values() + ->all(); + } + + /** @return array{id:int,iban:?string,label:string,legacy_cod_cassa:?string,numero_conto:?string,denominazione_banca:?string}|null */ + public function getSelectedContoImportInfo(): ?array + { + if (! $this->contoId) { + return null; + } + + foreach ($this->getContiImportabili() as $conto) { + if ((int) ($conto['id'] ?? 0) === $this->contoId) { + return $conto; + } + } + + return null; + } + + protected function resolvePreferredImportFormatForConto(?int $contoId): string + { + if (! $contoId) { + return 'auto_csv'; + } + + foreach ($this->getContiImportabili() as $conto) { + if ((int) ($conto['id'] ?? 0) !== $contoId) { + continue; + } + + $haystack = strtolower(implode(' ', array_filter([ + $conto['denominazione_banca'] ?? null, + $conto['legacy_cod_cassa'] ?? null, + $conto['numero_conto'] ?? null, + ], static fn($value): bool => is_string($value) && trim($value) !== ''))); + + if (str_contains($haystack, 'mps') || str_contains($haystack, 'monte dei paschi') || str_contains($haystack, 'paschi')) { + return 'mps_qif'; + } + + if (str_contains($haystack, 'intesa')) { + return 'intesa_csv'; + } + + if (str_contains($haystack, 'unicredit')) { + return 'unicredit_wri'; + } + + return 'auto_csv'; + } + + return 'auto_csv'; + } + + protected function makeMovimentiImporter(): MovimentiBancaImporter + { + return new MovimentiBancaImporter( + new UnicreditWriParser(), + new IntesaCsvParser(), + new ExcelMovimentiParser(), + null, + null, + new MpsQifParser(), + ); + } + public function getSelectedIbanLabel(): ?string { $iban = is_string($this->iban) ? trim($this->iban) : ''; diff --git a/app/Services/Contabilita/MovimentiBancaImporter.php b/app/Services/Contabilita/MovimentiBancaImporter.php index 2a65a81..929ab09 100644 --- a/app/Services/Contabilita/MovimentiBancaImporter.php +++ b/app/Services/Contabilita/MovimentiBancaImporter.php @@ -13,6 +13,7 @@ class MovimentiBancaImporter { private UnicreditSemicolonCsvParser $unicreditSemicolonCsvParser; private CedhouseCsvParser $cedhouseCsvParser; + private MpsQifParser $mpsQifParser; public function __construct( private readonly UnicreditWriParser $unicreditParser, @@ -20,9 +21,11 @@ public function __construct( private readonly ExcelMovimentiParser $excelParser, ?UnicreditSemicolonCsvParser $unicreditSemicolonCsvParser = null, ?CedhouseCsvParser $cedhouseCsvParser = null, + ?MpsQifParser $mpsQifParser = null, ) { $this->unicreditSemicolonCsvParser = $unicreditSemicolonCsvParser ?? new UnicreditSemicolonCsvParser(); $this->cedhouseCsvParser = $cedhouseCsvParser ?? new CedhouseCsvParser(); + $this->mpsQifParser = $mpsQifParser ?? new MpsQifParser(); } /** @@ -256,6 +259,78 @@ public function importCsvAutoForConto( throw new \RuntimeException('Formato CSV non riconosciuto. Tentativi: ' . implode(' | ', $errors)); } + /** + * @return array{imported:int, duplicates:int, meta?:array} + */ + public function importUnicreditWriForConto( + string $content, + int $stabileId, + int $contoId, + ?string $sourceFile = null, + ?int $gestioneId = null, + bool $replace = false, + ): array { + $parsed = $this->unicreditParser->parse($content); + + return $this->importParsedRowsForConto( + $parsed, + $stabileId, + $contoId, + $sourceFile, + $gestioneId, + $replace, + 'unicredit_wri', + ); + } + + /** + * @return array{imported:int, duplicates:int, meta?:array} + */ + public function importIntesaCsvForConto( + string $content, + int $stabileId, + int $contoId, + ?string $sourceFile = null, + ?int $gestioneId = null, + bool $replace = false, + ): array { + $parsed = $this->intesaParser->parse($content); + + return $this->importParsedRowsForConto( + $parsed, + $stabileId, + $contoId, + $sourceFile, + $gestioneId, + $replace, + 'intesa_csv', + ); + } + + /** + * @return array{imported:int, duplicates:int, meta?:array} + */ + public function importMpsQifForConto( + string $content, + int $stabileId, + int $contoId, + ?string $sourceFile = null, + ?int $gestioneId = null, + bool $replace = false, + ): array { + $parsed = $this->mpsQifParser->parse($content); + + return $this->importParsedRowsForConto( + $parsed, + $stabileId, + $contoId, + $sourceFile, + $gestioneId, + $replace, + 'mps_qif', + ); + } + /** * @param array{rows?:array, meta?:array} $parsed * @return array{imported:int, duplicates:int, meta?:array} diff --git a/app/Services/Contabilita/MpsQifParser.php b/app/Services/Contabilita/MpsQifParser.php new file mode 100644 index 0000000..e892390 --- /dev/null +++ b/app/Services/Contabilita/MpsQifParser.php @@ -0,0 +1,162 @@ +, meta: array} + */ + public function parse(string $content): array + { + $content = str_replace(["\r\n", "\r"], "\n", $content); + $lines = array_values(array_filter(explode("\n", $content), static fn(string $line): bool => trim($line) !== '')); + + if ($lines === []) { + throw new \RuntimeException('File QIF vuoto.'); + } + + $type = null; + $rows = []; + $transaction = []; + + foreach ($lines as $line) { + $line = preg_replace('/^\xEF\xBB\xBF/', '', $line) ?? $line; + $trimmed = trim($line); + + if ($trimmed === '') { + continue; + } + + if (str_starts_with($trimmed, '!Type:')) { + $type = substr($trimmed, 6) ?: null; + continue; + } + + if ($trimmed === '^') { + $row = $this->buildRow($transaction, $type); + if ($row !== null) { + $rows[] = $row; + } + $transaction = []; + continue; + } + + $code = substr($trimmed, 0, 1); + $value = trim(substr($trimmed, 1)); + + if (! isset($transaction[$code])) { + $transaction[$code] = []; + } + + $transaction[$code][] = $value; + } + + if ($transaction !== []) { + $row = $this->buildRow($transaction, $type); + if ($row !== null) { + $rows[] = $row; + } + } + + if ($rows === []) { + throw new \RuntimeException('Nessun movimento valido trovato nel file QIF.'); + } + + return [ + 'rows' => $rows, + 'meta' => [ + 'format' => 'mps_qif', + 'qif_type' => $type, + ], + ]; + } + + /** + * @param array> $transaction + * @return array{data: Carbon, valuta: Carbon|null, descrizione: string, descrizione_estesa?: string|null, importo: float, causale: string|null, raw_line: string}|null + */ + private function buildRow(array $transaction, ?string $type): ?array + { + $date = $this->parseDate($transaction['D'][0] ?? null); + $amount = $this->parseMoney($transaction['T'][0] ?? null); + + if (! $date || $amount === null) { + return null; + } + + $payee = $this->joinParts($transaction['P'] ?? []); + $memo = $this->joinParts($transaction['M'] ?? []); + $description = $payee !== '' ? $payee : ($memo !== '' ? mb_substr($memo, 0, 255) : 'Movimento QIF'); + $extended = trim($payee . ($payee !== '' && $memo !== '' ? ' · ' : '') . $memo); + + $rawLines = []; + foreach ($transaction as $code => $values) { + foreach ($values as $value) { + $rawLines[] = $code . $value; + } + } + + return [ + 'data' => $date, + 'valuta' => $date->copy(), + 'descrizione' => $description, + 'descrizione_estesa' => $extended !== '' ? $extended : null, + 'importo' => $amount, + 'causale' => $type ? ('QIF:' . strtoupper($type)) : 'QIF', + 'raw_line' => implode("\n", $rawLines), + ]; + } + + /** + * @param array $parts + */ + private function joinParts(array $parts): string + { + $parts = array_map(static fn(string $value): string => trim($value), $parts); + $parts = array_values(array_filter($parts, static fn(string $value): bool => $value !== '')); + + return trim(implode(' ', $parts)); + } + + private function parseDate(?string $value): ?Carbon + { + $value = trim((string) $value); + if ($value === '') { + return null; + } + + foreach (['d/m/Y', 'd/m/y', 'm/d/Y', 'm/d/y'] as $format) { + try { + return Carbon::createFromFormat($format, $value)->startOfDay(); + } catch (\Throwable) { + // Try next format. + } + } + + return null; + } + + private function parseMoney(?string $value): ?float + { + $value = trim((string) $value); + if ($value === '') { + return null; + } + + $value = str_replace(["\u{00A0}", "\t", ' '], '', $value); + $value = str_ireplace(['EUR', 'EURO', '€'], '', $value); + $value = preg_replace('/[^0-9,\.\-\+]/', '', $value) ?? $value; + + if (str_contains($value, ',') && str_contains($value, '.')) { + $value = str_replace('.', '', $value); + $value = str_replace(',', '.', $value); + } elseif (str_contains($value, ',')) { + $value = str_replace(',', '.', $value); + } + + return is_numeric($value) ? (float) $value : null; + } +} \ No newline at end of file diff --git a/resources/views/filament/pages/contabilita/casse-banche-movimenti.blade.php b/resources/views/filament/pages/contabilita/casse-banche-movimenti.blade.php index 40f30c2..f5f455f 100644 --- a/resources/views/filament/pages/contabilita/casse-banche-movimenti.blade.php +++ b/resources/views/filament/pages/contabilita/casse-banche-movimenti.blade.php @@ -19,8 +19,28 @@ $periodoOptions = $this->periodoOptions; $periodoRateOptions = $this->periodoRateOptions; $periodoRate = $this->periodoRateTotali; + $contoImport = $this->getSelectedContoImportInfo(); @endphp + +
+
+
Import unico estratti
+
+ Seleziona il conto o la cassa corretta e importa il file nel formato relativo. Il collegamento al conto resta esplicito, senza inferenze dal file. +
+ @if($contoImport) +
+ Conto selezionato: {{ $contoImport['label'] }} +
+ @endif +
+ + Importa estratto + +
+
+