diff --git a/app/Console/Commands/GesconFornitoriDedupCommand.php b/app/Console/Commands/GesconFornitoriDedupCommand.php new file mode 100644 index 0000000..1616fef --- /dev/null +++ b/app/Console/Commands/GesconFornitoriDedupCommand.php @@ -0,0 +1,480 @@ + */ + private array $referenceCounts = []; + + /** @var array */ + private array $referenceColumns = []; + + public function handle(): int + { + if (! Schema::hasTable('fornitori')) { + $this->error('Tabella fornitori non disponibile.'); + return self::FAILURE; + } + + $apply = (bool) $this->option('apply'); + $limit = $this->option('limit') ? max(1, (int) $this->option('limit')) : null; + $preferredAdminId = $this->option('admin-id') !== null ? max(0, (int) $this->option('admin-id')) : null; + + $groups = $this->loadDuplicateGroups($limit); + if ($groups === []) { + $this->info('Nessun doppione fornitore rilevato.'); + return self::SUCCESS; + } + + $this->referenceColumns = $this->discoverSupplierReferenceColumns(); + $this->referenceCounts = $this->loadSupplierReferenceCounts(); + + $stats = [ + 'groups' => 0, + 'duplicates' => 0, + 'merged' => 0, + 'deleted' => 0, + 'moved_links' => 0, + 'skipped_delete' => 0, + 'errors' => 0, + ]; + + $previewRows = []; + + foreach ($groups as $groupKey => $suppliers) { + $stats['groups']++; + $stats['duplicates'] += count($suppliers) - 1; + + $master = $this->pickMaster($suppliers, $preferredAdminId); + $duplicates = array_values(array_filter($suppliers, fn(Fornitore $supplier): bool => (int) $supplier->id !== (int) $master->id)); + + $previewRows[] = [ + 'group' => $groupKey, + 'keep' => '#' . (int) $master->id . ' ' . trim((string) ($master->ragione_sociale ?? '')), + 'keep_code' => $master->cod_forn ?: '—', + 'drop' => implode(', ', array_map(fn(Fornitore $supplier): string => '#' . (int) $supplier->id, $duplicates)), + ]; + + if (! $apply) { + continue; + } + + try { + DB::transaction(function () use ($master, $duplicates, &$stats): void { + $masterModel = Fornitore::query()->findOrFail((int) $master->id); + $duplicatesModels = Fornitore::query() + ->whereIn('id', array_map(fn(Fornitore $supplier): int => (int) $supplier->id, $duplicates)) + ->get(); + + if ($duplicatesModels->isEmpty()) { + return; + } + + $masterPayload = $this->buildMasterPayload($masterModel, $duplicatesModels->all()); + $masterModel->fill($masterPayload); + if ($masterModel->isDirty()) { + $masterModel->save(); + } + + foreach ($duplicatesModels as $duplicate) { + $stats['moved_links'] += $this->moveSupplierReferences((int) $duplicate->id, (int) $masterModel->id); + + if ($this->hasRemainingReferences((int) $duplicate->id)) { + $stats['skipped_delete']++; + continue; + } + + $duplicate->delete(); + $stats['deleted']++; + $stats['merged']++; + } + }); + } catch (\Throwable $exception) { + $stats['errors']++; + $this->warn('Gruppo ' . $groupKey . ' non bonificato: ' . $exception->getMessage()); + } + } + + $this->table(['Gruppo', 'Canonico', 'cod_forn', 'Duplicati'], array_slice($previewRows, 0, 20)); + $this->line(json_encode($stats, JSON_UNESCAPED_UNICODE)); + $this->info($apply ? 'Bonifica fornitori completata.' : 'Dry-run bonifica fornitori completato.'); + + return self::SUCCESS; + } + + /** @return array> */ + private function loadDuplicateGroups(?int $limit = null): array + { + $suppliers = Fornitore::query()->get([ + 'id', + 'amministratore_id', + 'cod_forn', + 'old_id', + 'rubrica_id', + 'ragione_sociale', + 'partita_iva', + 'codice_fiscale', + 'email', + 'pec', + 'telefono', + 'cellulare', + 'iban', + 'codice_univoco', + 'fe_features', + 'operational_config', + 'note', + ]); + + $groups = []; + foreach ($suppliers as $supplier) { + $groupKey = $this->buildIdentityKey($supplier); + if ($groupKey === null) { + continue; + } + + $groups[$groupKey] ??= []; + $groups[$groupKey][] = $supplier; + } + + $groups = array_filter($groups, fn(array $items): bool => count($items) > 1); + ksort($groups); + + if ($limit !== null) { + $groups = array_slice($groups, 0, $limit, true); + } + + return $groups; + } + + private function buildIdentityKey(Fornitore $supplier): ?string + { + $rubricaId = (int) ($supplier->rubrica_id ?? 0); + if ($rubricaId > 0) { + return 'rubrica:' . $rubricaId; + } + + $piva = $this->normalizeNumericIdentity($supplier->partita_iva); + if ($piva !== null) { + return 'piva:' . $piva; + } + + $cf = $this->normalizeAlphaNumericIdentity($supplier->codice_fiscale); + if ($cf !== null) { + return 'cf:' . $cf; + } + + return null; + } + + /** @param array $suppliers */ + private function pickMaster(array $suppliers, ?int $preferredAdminId): Fornitore + { + $best = null; + $bestScore = null; + + foreach ($suppliers as $supplier) { + $score = 0; + + if ((int) ($supplier->cod_forn ?? 0) > 0) { + $score += 2000; + } + if ((int) ($supplier->rubrica_id ?? 0) > 0) { + $score += 400; + } + if ($preferredAdminId !== null && (int) ($supplier->amministratore_id ?? 0) === $preferredAdminId) { + $score += 300; + } + if (! empty($supplier->fe_features)) { + $score += 150; + } + if (! empty($supplier->operational_config)) { + $score += 100; + } + + foreach (['partita_iva', 'codice_fiscale', 'email', 'pec', 'telefono', 'cellulare', 'iban', 'codice_univoco'] as $field) { + if (trim((string) ($supplier->{$field} ?? '')) !== '') { + $score += 20; + } + } + + $score += ((int) ($this->referenceCounts[(int) $supplier->id] ?? 0)) * 25; + + if ($best === null || $bestScore === null || $score > $bestScore || ($score === $bestScore && (int) $supplier->id < (int) $best->id)) { + $best = $supplier; + $bestScore = $score; + } + } + + return $best ?? $suppliers[0]; + } + + /** @param array $duplicates */ + private function buildMasterPayload(Fornitore $master, array $duplicates): array + { + $payload = []; + + foreach (['rubrica_id', 'partita_iva', 'codice_fiscale', 'email', 'pec', 'telefono', 'cellulare', 'iban', 'codice_univoco'] as $field) { + if (trim((string) ($master->{$field} ?? '')) !== '') { + continue; + } + + foreach ($duplicates as $duplicate) { + $value = $duplicate->{$field} ?? null; + if (trim((string) $value) !== '') { + $payload[$field] = $value; + break; + } + } + } + + $masterFeFeatures = is_array($master->fe_features ?? null) ? $master->fe_features : []; + $masterOperationalConfig = is_array($master->operational_config ?? null) ? $master->operational_config : []; + + $legacyCodFornAliases = []; + $legacyOldIdAliases = []; + $mergedSupplierIds = []; + + if ((int) ($master->cod_forn ?? 0) > 0) { + $legacyCodFornAliases[] = (int) $master->cod_forn; + } + if ((int) ($master->old_id ?? 0) > 0) { + $legacyOldIdAliases[] = (int) $master->old_id; + } + + foreach ($duplicates as $duplicate) { + if ((int) ($duplicate->cod_forn ?? 0) > 0) { + $legacyCodFornAliases[] = (int) $duplicate->cod_forn; + } + if ((int) ($duplicate->old_id ?? 0) > 0) { + $legacyOldIdAliases[] = (int) $duplicate->old_id; + } + + $mergedSupplierIds[] = (int) $duplicate->id; + $masterFeFeatures = $this->mergeConfigRecursive($masterFeFeatures, is_array($duplicate->fe_features ?? null) ? $duplicate->fe_features : []); + $masterOperationalConfig = $this->mergeConfigRecursive($masterOperationalConfig, is_array($duplicate->operational_config ?? null) ? $duplicate->operational_config : []); + } + + $legacyCodFornAliases = array_values(array_unique(array_filter(array_map('intval', array_merge( + $legacyCodFornAliases, + array_map('intval', (array) ($masterOperationalConfig['legacy_cod_forn_aliases'] ?? [])) + ))))); + $legacyOldIdAliases = array_values(array_unique(array_filter(array_map('intval', array_merge( + $legacyOldIdAliases, + array_map('intval', (array) ($masterOperationalConfig['legacy_old_id_aliases'] ?? [])) + ))))); + $mergedSupplierIds = array_values(array_unique(array_filter(array_map('intval', array_merge( + $mergedSupplierIds, + array_map('intval', (array) ($masterOperationalConfig['merged_supplier_ids'] ?? [])) + ))))); + + $masterOperationalConfig['legacy_cod_forn_aliases'] = $legacyCodFornAliases; + $masterOperationalConfig['legacy_old_id_aliases'] = $legacyOldIdAliases; + $masterOperationalConfig['merged_supplier_ids'] = $mergedSupplierIds; + + $payload['fe_features'] = $masterFeFeatures; + $payload['operational_config'] = $masterOperationalConfig; + + if (trim((string) ($master->note ?? '')) === '' && $duplicates !== []) { + $payload['note'] = 'Fornitore canonico dopo bonifica duplicati.'; + } + + return $payload; + } + + /** @return array */ + private function discoverSupplierReferenceColumns(): array + { + $database = DB::getDatabaseName(); + $rows = DB::select( + "SELECT table_name, column_name + FROM information_schema.columns + WHERE table_schema = ? + AND table_name <> 'fornitori' + AND column_name IN ('fornitore_id', 'default_fornitore_id', 'assegnato_a_fornitore_id', 'fornitore_esterno_id') + ORDER BY table_name, column_name", + [$database] + ); + + return array_values(array_filter(array_map(function (object $row): ?array { + $table = (string) ($row->TABLE_NAME ?? $row->table_name ?? ''); + $column = (string) ($row->COLUMN_NAME ?? $row->column_name ?? ''); + if ($table === '' || $column === '') { + return null; + } + + return ['table' => $table, 'column' => $column]; + }, $rows))); + } + + /** @return array */ + private function loadSupplierReferenceCounts(): array + { + $counts = []; + foreach ($this->discoverSupplierReferenceColumns() as $reference) { + $table = $reference['table']; + $column = $reference['column']; + if (! Schema::hasTable($table) || ! Schema::hasColumn($table, $column)) { + continue; + } + + $rows = DB::table($table) + ->selectRaw($column . ' as supplier_id, COUNT(*) as aggregate_count') + ->whereNotNull($column) + ->groupBy($column) + ->get(); + + foreach ($rows as $row) { + $supplierId = is_numeric($row->supplier_id ?? null) ? (int) $row->supplier_id : 0; + if ($supplierId <= 0) { + continue; + } + + $counts[$supplierId] = (int) ($counts[$supplierId] ?? 0) + (int) ($row->aggregate_count ?? 0); + } + } + + return $counts; + } + + private function moveSupplierReferences(int $fromSupplierId, int $toSupplierId): int + { + if ($fromSupplierId <= 0 || $toSupplierId <= 0 || $fromSupplierId === $toSupplierId) { + return 0; + } + + $moved = 0; + foreach ($this->referenceColumns as $reference) { + $table = $reference['table']; + $column = $reference['column']; + + if (! Schema::hasTable($table) || ! Schema::hasColumn($table, $column)) { + continue; + } + + if ($table === 'fornitore_stabile_impostazioni' && $column === 'fornitore_id') { + $moved += $this->mergeFornitoreStabileImpostazioni($fromSupplierId, $toSupplierId); + continue; + } + + $moved += (int) DB::table($table) + ->where($column, $fromSupplierId) + ->update([$column => $toSupplierId]); + } + + return $moved; + } + + private function mergeFornitoreStabileImpostazioni(int $fromSupplierId, int $toSupplierId): int + { + if (! Schema::hasTable('fornitore_stabile_impostazioni')) { + return 0; + } + + $moved = 0; + $rows = DB::table('fornitore_stabile_impostazioni') + ->where('fornitore_id', $fromSupplierId) + ->get(); + + foreach ($rows as $row) { + $target = DB::table('fornitore_stabile_impostazioni') + ->where('stabile_id', (int) $row->stabile_id) + ->where('fornitore_id', $toSupplierId) + ->first(); + + if (! $target) { + $moved += (int) DB::table('fornitore_stabile_impostazioni') + ->where('id', (int) $row->id) + ->update(['fornitore_id' => $toSupplierId]); + continue; + } + + if (Schema::hasColumn('fornitore_stabile_impostazioni', 'meta')) { + $sourceMeta = json_decode((string) ($row->meta ?? '[]'), true); + $targetMeta = json_decode((string) ($target->meta ?? '[]'), true); + $mergedMeta = $this->mergeConfigRecursive(is_array($targetMeta) ? $targetMeta : [], is_array($sourceMeta) ? $sourceMeta : []); + + DB::table('fornitore_stabile_impostazioni') + ->where('id', (int) $target->id) + ->update([ + 'meta' => json_encode($mergedMeta, JSON_UNESCAPED_UNICODE), + 'updated_at' => now(), + ]); + } + + DB::table('fornitore_stabile_impostazioni')->where('id', (int) $row->id)->delete(); + $moved++; + } + + return $moved; + } + + private function hasRemainingReferences(int $supplierId): bool + { + foreach ($this->referenceColumns as $reference) { + $table = $reference['table']; + $column = $reference['column']; + if (! Schema::hasTable($table) || ! Schema::hasColumn($table, $column)) { + continue; + } + + if (DB::table($table)->where($column, $supplierId)->exists()) { + return true; + } + } + + return false; + } + + /** @param array $master + * @param array $duplicate + * @return array + */ + private function mergeConfigRecursive(array $master, array $duplicate): array + { + foreach ($duplicate as $key => $value) { + if (! array_key_exists($key, $master)) { + $master[$key] = $value; + continue; + } + + if (is_array($master[$key]) && is_array($value)) { + $master[$key] = $this->mergeConfigRecursive($master[$key], $value); + } + } + + return $master; + } + + private function normalizeNumericIdentity(mixed $value): ?string + { + $digits = preg_replace('/\D+/', '', trim((string) $value)) ?? ''; + if ($digits === '' || preg_match('/^0+$/', $digits) === 1) { + return null; + } + + return strlen($digits) < 11 ? str_pad($digits, 11, '0', STR_PAD_LEFT) : $digits; + } + + private function normalizeAlphaNumericIdentity(mixed $value): ?string + { + $normalized = strtoupper(trim((string) $value)); + $normalized = preg_replace('/\s+/', '', $normalized) ?? ''; + if ($normalized === '' || in_array($normalized, ['0', '00', '000', 'ND', 'N/D', 'NULL'], true)) { + return null; + } + + return $normalized; + } +} diff --git a/app/Console/Commands/GesconSyncOrdinariaPreventivoCommand.php b/app/Console/Commands/GesconSyncOrdinariaPreventivoCommand.php index a752591..d5db6b7 100644 --- a/app/Console/Commands/GesconSyncOrdinariaPreventivoCommand.php +++ b/app/Console/Commands/GesconSyncOrdinariaPreventivoCommand.php @@ -44,7 +44,7 @@ public function handle(): int return self::FAILURE; } - $tableCodes = collect(array_keys($legacyHeaders))->values(); + $tableCodes = collect(array_keys($legacyHeaders))->values(); $legacyDetails = $this->loadLegacyDettagli($stableCode, $year, $tableCodes); $this->assertLegacyVociHaveKnownTables($legacyVoci, $tableCodes); diff --git a/app/Console/Commands/ImportGesconFullPipeline.php b/app/Console/Commands/ImportGesconFullPipeline.php index 934b33c..519727a 100644 --- a/app/Console/Commands/ImportGesconFullPipeline.php +++ b/app/Console/Commands/ImportGesconFullPipeline.php @@ -2828,7 +2828,7 @@ private function stepOperazioni(?int $limit): int $data = [ 'tenant_id' => null, - 'gestione_id' => $this->resolveGestioneId($o->anno ?? null, $o->compet ?? null, $o->gestione ?? null), + 'gestione_id' => $this->resolveGestioneId($o->anno ?? null, $o->compet ?? null, $o->gestione ?? null, $o->n_stra ?? null), 'legacy_id' => $legacy, 'descrizione' => $o->note ?? ($o->natura ?? 'Operazione'), 'data_operazione' => $o->dt_spe ?? now()->toDateString(), @@ -5782,46 +5782,21 @@ private function resolveGestioneContabileIdForStabile(?int $stabileId, ?string $ return $match?->id; } - private function resolveGestioneId($anno, $compet, $tipo): ?int + private function resolveGestioneId($anno, $compet, $tipo, $numeroStraordinaria = null): ?int { if (! Schema::hasTable('gestioni_contabili')) { return null; } - // Normalize tipo from O/R/S to descriptive if needed - $map = ['O' => 'ordinaria', 'R' => 'riscaldamento', 'S' => 'straordinaria']; - $tipo = $map[$tipo ?? 'O'] ?? ($tipo ?: 'ordinaria'); - $anno = $anno ?: date('Y'); - $row = DB::table('gestioni_contabili') - ->where(['anno_gestione' => (int) $anno, 'tipo_gestione' => $tipo]) - ->first(); - if ($row) { - return $row->id; - } + $legacyYear = $anno !== null && $anno !== '' ? (string) $anno : null; + $stabileId = $this->stabileId ?: $this->resolveStabileId(); - // In dry-run, avoid creating missing gestione - if ($this->option('dry-run')) { - return null; - } - - // Create minimal compatible record satisfying not-null constraints - $data = [ - 'tenant_id' => 'T1', - 'anno_gestione' => (int) $anno, - 'tipo_gestione' => $tipo, - 'numero_straordinaria' => null, - 'denominazione' => strtoupper(substr($tipo, 0, 1)) . $anno, - 'data_inizio' => date('Y-01-01', strtotime($anno . '-01-01')), - 'data_fine' => date('Y-12-31', strtotime($anno . '-12-31')), - 'stato' => 'aperta', - 'protocollo_prefix' => strtoupper(substr($tipo, 0, 1)) . $anno, - 'ultimo_protocollo' => 0, - 'created_at' => now(), - 'updated_at' => now(), - ]; - DB::table('gestioni_contabili')->insert($data); - $row = DB::table('gestioni_contabili')->where(['anno_gestione' => (int) $anno, 'tipo_gestione' => $tipo])->first(); - return $row?->id; + return $this->resolveGestioneContabileIdForStabile( + $stabileId, + $legacyYear, + $tipo !== null ? (string) $tipo : null, + is_numeric($numeroStraordinaria) ? (int) $numeroStraordinaria : null, + ); } private function dynamicInsert(string $table, array $data): void diff --git a/app/Console/Commands/NetgesconNodeRefreshCommand.php b/app/Console/Commands/NetgesconNodeRefreshCommand.php index 92395a8..94457f0 100644 --- a/app/Console/Commands/NetgesconNodeRefreshCommand.php +++ b/app/Console/Commands/NetgesconNodeRefreshCommand.php @@ -3,14 +3,21 @@ use Illuminate\Console\Command; use Illuminate\Support\Facades\Artisan; +use Illuminate\Support\Facades\File; +use Illuminate\Support\Facades\Process; class NetgesconNodeRefreshCommand extends Command { protected $signature = 'netgescon:node-refresh {--remote=origin : Remote Git da usare} {--branch=main : Branch da allineare} + {--admin-id= : ID amministratore per eventuale backup Drive} + {--drive : Carica il backup pre-update anche su Google Drive} {--force : Continua anche con working tree sporco lato sync} + {--require-drive : Fallisce se il backup Drive non viene completato} + {--skip-backup : Salta backup pre-update (solo test/debug)} {--skip-migrate : Salta php artisan migrate --force} + {--skip-maintenance : Salta php artisan down/up (solo test/debug)} {--skip-view-cache : Salta rebuild view cache} {--progress-file= : File JSON dove scrivere lo stato avanzamento}'; @@ -18,110 +25,292 @@ class NetgesconNodeRefreshCommand extends Command public function handle(): int { - $remote = trim((string) $this->option('remote')) ?: 'origin'; - $branch = trim((string) $this->option('branch')) ?: 'main'; - $force = (bool) $this->option('force'); - $skipMigrate = (bool) $this->option('skip-migrate'); - $skipViewCache = (bool) $this->option('skip-view-cache'); + $remote = trim((string) $this->option('remote')) ?: 'origin'; + $branch = trim((string) $this->option('branch')) ?: 'main'; + $adminId = (int) $this->option('admin-id'); + $force = (bool) $this->option('force'); + $driveBackup = (bool) $this->option('drive'); + $requireDrive = (bool) $this->option('require-drive'); + $skipBackup = (bool) $this->option('skip-backup'); + $skipMigrate = (bool) $this->option('skip-migrate'); + $skipMaintenance = (bool) $this->option('skip-maintenance'); + $skipViewCache = (bool) $this->option('skip-view-cache'); + $maintenanceOn = false; + $maintenanceError = null; $this->writeProgress(2, 'Avvio refresh nodo da Gitea', 'running'); - $gitParams = [ - '--remote' => $remote, - '--branch' => $branch, - ]; + $fullOutput = []; - if ($force) { - $gitParams['--force'] = true; + if (! $skipBackup) { + $this->writeProgress(8, 'Backup pre-update in corso', 'running'); + $backupArgs = [ + '--differential' => true, + '--tag' => 'node-refresh-' . now()->format('Ymd-His'), + ]; + + if ($driveBackup) { + $backupArgs['--drive'] = true; + + if ($adminId > 0) { + $backupArgs['--admin-id'] = (string) $adminId; + } + + if ($requireDrive) { + $backupArgs['--require-drive'] = true; + } + } + + $backupExit = Artisan::call('netgescon:preupdate-backup', $backupArgs); + $backupOutput = trim((string) Artisan::output()); + $fullOutput[] = 'NODE REFRESH: php artisan netgescon:preupdate-backup'; + $fullOutput[] = $backupOutput; + + if ($backupExit !== 0) { + $this->error('Backup pre-update fallito.'); + $this->line($backupOutput); + $this->writeProgress(100, 'Backup pre-update fallito', 'failed', $backupExit); + + return self::FAILURE; + } } - $this->info("Sync Git da {$remote}/{$branch}..."); - $this->writeProgress(15, 'Sync Git da Gitea in corso', 'running'); - $gitExit = Artisan::call('netgescon:git-sync', $gitParams); - $gitOutput = trim((string) Artisan::output()); + if (! $skipMaintenance) { + $this->writeProgress(16, 'Blocco accessi e maintenance mode', 'running'); + $maintenanceExit = Artisan::call('down', ['--retry' => 60, '--refresh' => 15]); + $maintenanceOutput = trim((string) Artisan::output()); + $fullOutput[] = 'NODE REFRESH: php artisan down --retry=60 --refresh=15'; + $fullOutput[] = $maintenanceOutput; - if ($gitExit !== 0) { - $this->error('Sync Git fallita.'); - $this->line($gitOutput); - $this->writeProgress(100, 'Sync Git fallita', 'failed', $gitExit); + if ($maintenanceExit !== 0) { + $this->error('Impossibile attivare la maintenance mode.'); + $this->line($maintenanceOutput); + $this->writeProgress(100, 'Maintenance mode non attivata', 'failed', $maintenanceExit); + + return self::FAILURE; + } + + $maintenanceOn = true; + } + + try { + $gitParams = [ + '--remote' => $remote, + '--branch' => $branch, + ]; + + if ($force) { + $gitParams['--force'] = true; + } + + $this->info("Sync Git da {$remote}/{$branch}..."); + $this->writeProgress(28, 'Sync Git da Gitea in corso', 'running'); + $gitExit = Artisan::call('netgescon:git-sync', $gitParams); + $gitOutput = trim((string) Artisan::output()); + + if ($gitExit !== 0) { + $this->error('Sync Git fallita.'); + $this->line($gitOutput); + $this->writeProgress(100, 'Sync Git fallita', 'failed', $gitExit); + + return self::FAILURE; + } + + $fullOutput[] = 'NODE REFRESH: netgescon:git-sync'; + $fullOutput[] = $gitOutput; + + $this->writeProgress(46, 'Composer install e autoload', 'running'); + $composerResult = $this->runShellStep([ + 'composer', + 'install', + '--no-interaction', + '--prefer-dist', + '--optimize-autoloader', + '--no-progress', + ]); + $fullOutput[] = 'NODE REFRESH: composer install --no-interaction --prefer-dist --optimize-autoloader --no-progress'; + $fullOutput[] = $composerResult['output']; + + if (! $composerResult['success']) { + $this->error('Composer install fallito.'); + $this->line($composerResult['output']); + $this->writeProgress(100, 'Composer install fallito', 'failed', $composerResult['exit_code']); + + return self::FAILURE; + } + + $this->writeProgress(58, 'Build frontend se disponibile', 'running'); + $frontendBuild = $this->attemptFrontendBuild(); + if ($frontendBuild !== null) { + $fullOutput[] = $frontendBuild['label']; + $fullOutput[] = $frontendBuild['output']; + } + + $this->info('Pulizia cache applicative...'); + $this->writeProgress(68, 'Pulizia cache applicative', 'running'); + $optimizeExit = Artisan::call('optimize:clear'); + $optimizeOutput = trim((string) Artisan::output()); + $fullOutput[] = 'NODE REFRESH: php artisan optimize:clear'; + $fullOutput[] = $optimizeOutput; + + if ($optimizeExit !== 0) { + $this->error('optimize:clear fallito.'); + $this->line($optimizeOutput); + $this->writeProgress(100, 'Pulizia cache fallita', 'failed', $optimizeExit); + + return self::FAILURE; + } + + if (! $skipMigrate) { + $this->info('Esecuzione migrate --force...'); + $this->writeProgress(76, 'Applicazione migration', 'running'); + $migrateExit = Artisan::call('migrate', ['--force' => true]); + $migrateOutput = trim((string) Artisan::output()); + $fullOutput[] = 'NODE REFRESH: php artisan migrate --force'; + $fullOutput[] = $migrateOutput; + + if ($migrateExit !== 0) { + $this->error('Migration fallita.'); + $this->line($migrateOutput); + $this->writeProgress(100, 'Migration fallita', 'failed', $migrateExit); + + return self::FAILURE; + } + } + + if (! $skipViewCache) { + $this->info('Ricostruzione view cache...'); + $this->writeProgress(84, 'Ricostruzione view cache', 'running'); + + $viewClearExit = Artisan::call('view:clear'); + $viewClearOutput = trim((string) Artisan::output()); + $fullOutput[] = 'NODE REFRESH: php artisan view:clear'; + $fullOutput[] = $viewClearOutput; + + if ($viewClearExit !== 0) { + $this->error('view:clear fallito.'); + $this->line($viewClearOutput); + $this->writeProgress(100, 'view:clear fallito', 'failed', $viewClearExit); + + return self::FAILURE; + } + + $viewCacheExit = Artisan::call('view:cache'); + $viewCacheOutput = trim((string) Artisan::output()); + $fullOutput[] = 'NODE REFRESH: php artisan view:cache'; + $fullOutput[] = $viewCacheOutput; + + if ($viewCacheExit !== 0) { + $this->error('view:cache fallita.'); + $this->line($viewCacheOutput); + $this->writeProgress(100, 'view:cache fallita', 'failed', $viewCacheExit); + + return self::FAILURE; + } + } + + $this->writeProgress(91, 'Rigenerazione cache applicative finali', 'running'); + foreach ([ + 'config:cache' => 'NODE REFRESH: php artisan config:cache', + 'event:cache' => 'NODE REFRESH: php artisan event:cache', + 'queue:restart' => 'NODE REFRESH: php artisan queue:restart', + ] as $artisanCommand => $label) { + $exit = Artisan::call($artisanCommand); + $output = trim((string) Artisan::output()); + $fullOutput[] = $label; + $fullOutput[] = $output; + + if ($exit !== 0) { + $this->warn($artisanCommand . ' non completato: ' . ($output !== '' ? $output : 'uscita ' . $exit)); + } + } + + $this->line(implode(PHP_EOL . PHP_EOL, array_filter($fullOutput, static fn(string $chunk): bool => trim($chunk) !== ''))); + $this->writeProgress(98, 'Riapertura nodo e chiusura refresh', 'running'); + } finally { + if ($maintenanceOn) { + $upExit = Artisan::call('up'); + $maintenanceUp = trim((string) Artisan::output()); + $fullOutput[] = 'NODE REFRESH: php artisan up'; + $fullOutput[] = $maintenanceUp; + + if ($upExit !== 0) { + $maintenanceError = $maintenanceUp !== '' ? $maintenanceUp : 'php artisan up fallito'; + } + + $lockPath = storage_path('framework/down'); + if (is_file($lockPath)) { + @unlink($lockPath); + } + } + } + + if ($maintenanceError !== null) { + $this->error('Refresh completato ma il nodo non e stato riaperto automaticamente.'); + $this->line($maintenanceError); + $this->writeProgress(100, 'Refresh completato ma nodo ancora in maintenance', 'failed', 1); return self::FAILURE; } - $fullOutput = [ - 'NODE REFRESH: netgescon:git-sync', - $gitOutput, - ]; - - if (! $skipMigrate) { - $this->info('Esecuzione migrate --force...'); - $this->writeProgress(55, 'Applicazione migration', 'running'); - $migrateExit = Artisan::call('migrate', ['--force' => true]); - $migrateOutput = trim((string) Artisan::output()); - $fullOutput[] = 'NODE REFRESH: php artisan migrate --force'; - $fullOutput[] = $migrateOutput; - - if ($migrateExit !== 0) { - $this->error('Migration fallita.'); - $this->line($migrateOutput); - $this->writeProgress(100, 'Migration fallita', 'failed', $migrateExit); - - return self::FAILURE; - } - } - - $this->info('Pulizia cache applicative...'); - $this->writeProgress(72, 'Pulizia cache applicative', 'running'); - $optimizeExit = Artisan::call('optimize:clear'); - $optimizeOutput = trim((string) Artisan::output()); - $fullOutput[] = 'NODE REFRESH: php artisan optimize:clear'; - $fullOutput[] = $optimizeOutput; - - if ($optimizeExit !== 0) { - $this->error('optimize:clear fallito.'); - $this->line($optimizeOutput); - $this->writeProgress(100, 'Pulizia cache fallita', 'failed', $optimizeExit); - - return self::FAILURE; - } - - if (! $skipViewCache) { - $this->info('Ricostruzione view cache...'); - $this->writeProgress(88, 'Ricostruzione view cache', 'running'); - - $viewClearExit = Artisan::call('view:clear'); - $viewClearOutput = trim((string) Artisan::output()); - $fullOutput[] = 'NODE REFRESH: php artisan view:clear'; - $fullOutput[] = $viewClearOutput; - - if ($viewClearExit !== 0) { - $this->error('view:clear fallito.'); - $this->line($viewClearOutput); - $this->writeProgress(100, 'view:clear fallito', 'failed', $viewClearExit); - - return self::FAILURE; - } - - $viewCacheExit = Artisan::call('view:cache'); - $viewCacheOutput = trim((string) Artisan::output()); - $fullOutput[] = 'NODE REFRESH: php artisan view:cache'; - $fullOutput[] = $viewCacheOutput; - - if ($viewCacheExit !== 0) { - $this->error('view:cache fallita.'); - $this->line($viewCacheOutput); - $this->writeProgress(100, 'view:cache fallita', 'failed', $viewCacheExit); - - return self::FAILURE; - } - } - - $this->line(implode(PHP_EOL . PHP_EOL, array_filter($fullOutput, static fn(string $chunk): bool => trim($chunk) !== ''))); $this->writeProgress(100, 'Refresh nodo completato', 'completed', 0); return self::SUCCESS; } + /** @return array{success:bool,exit_code:int,output:string} */ + private function runShellStep(array $command): array + { + $result = Process::path(base_path()) + ->timeout(3600) + ->run($command); + + return [ + 'success' => $result->successful(), + 'exit_code' => $result->exitCode(), + 'output' => trim((string) ($result->output() !== '' ? $result->output() : $result->errorOutput())), + ]; + } + + /** @return array{label:string,output:string}|null */ + private function attemptFrontendBuild(): ?array + { + if (! is_file(base_path('package.json'))) { + return null; + } + + $nodeCheck = $this->runShellStep(['bash', '-lc', 'command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1']); + if (! $nodeCheck['success']) { + return [ + 'label' => 'NODE REFRESH: frontend build skipped', + 'output' => 'node/npm non disponibili sul nodo: build frontend saltata senza bloccare il deploy.', + ]; + } + + $installCommand = is_file(base_path('package-lock.json')) + ? ['npm', 'ci', '--no-audit', '--no-fund'] + : ['npm', 'install', '--no-audit', '--no-fund']; + + $installResult = $this->runShellStep($installCommand); + if (! $installResult['success']) { + return [ + 'label' => 'NODE REFRESH: npm install/ci', + 'output' => $installResult['output'] !== '' + ? $installResult['output'] + : 'Installazione dipendenze frontend fallita; build saltata.', + ]; + } + + $buildResult = $this->runShellStep(['npm', 'run', 'build']); + + return [ + 'label' => 'NODE REFRESH: npm run build', + 'output' => $buildResult['output'] !== '' + ? $buildResult['output'] + : ($buildResult['success'] ? 'Build frontend completata.' : 'Build frontend fallita.'), + ]; + } + private function writeProgress(int $percent, string $message, string $status, ?int $exitCode = null): void { $path = trim((string) $this->option('progress-file')); diff --git a/app/Console/Commands/SyncAmazonCreatorsCatalogCommand.php b/app/Console/Commands/SyncAmazonCreatorsCatalogCommand.php index a893908..935491b 100644 --- a/app/Console/Commands/SyncAmazonCreatorsCatalogCommand.php +++ b/app/Console/Commands/SyncAmazonCreatorsCatalogCommand.php @@ -116,4 +116,4 @@ private function formatMoney(mixed $amount, string $currency): string ? number_format((float) $amount, 2, ',', '.') . ' ' . strtoupper(trim($currency) !== '' ? $currency : 'EUR') : '-'; } -} \ No newline at end of file +} diff --git a/app/Console/Commands/SyncGesconFattureToContabilitaCommand.php b/app/Console/Commands/SyncGesconFattureToContabilitaCommand.php index 522419c..ef3edac 100644 --- a/app/Console/Commands/SyncGesconFattureToContabilitaCommand.php +++ b/app/Console/Commands/SyncGesconFattureToContabilitaCommand.php @@ -1,6 +1,7 @@ values() ->all(); - $fornitoreMap = []; - if (! empty($codes) && Schema::hasTable('fornitori')) { - $fornitori = DB::table('fornitori') - ->whereIn('old_id', $codes) - ->get(['id', 'old_id']); - - foreach ($fornitori as $fornitore) { - $raw = trim((string) $fornitore->old_id); - $noZero = ltrim($raw, '0'); - - if ($raw !== '') { - $fornitoreMap[$raw] = (int) $fornitore->id; - } - if ($noZero !== '') { - $fornitoreMap[$noZero] = (int) $fornitore->id; - } - } - } + $fornitoreMap = $this->buildFornitoreMapByLegacyCode($codes); $created = 0; $updated = 0; @@ -415,13 +399,15 @@ private function resolveFatturaElettronicaId(int $stabileId, int $fornitoreId, s return null; } + $fornitoreIds = $this->resolveEquivalentFornitoreIds($fornitoreId); + $base = DB::table('fatture_elettroniche') ->where('stabile_id', $stabileId) + ->whereIn('fornitore_id', $fornitoreIds) ->whereDate('data_fattura', $dataDocumento) ->whereRaw('LOWER(TRIM(numero_fattura)) = ?', [$numeroNorm]); $strict = (clone $base) - ->where('fornitore_id', $fornitoreId) ->orderByDesc('id') ->value('id'); @@ -444,4 +430,118 @@ private function resolveFatturaElettronicaId(int $stabileId, int $fornitoreId, s return null; } + + /** @param array $codes + * @return array + */ + private function buildFornitoreMapByLegacyCode(array $codes): array + { + if ($codes === [] || ! Schema::hasTable('fornitori')) { + return []; + } + + $map = []; + $suppliers = Fornitore::query()->get(['id', 'cod_forn', 'old_id', 'operational_config']); + + foreach ($suppliers as $supplier) { + $directCodes = []; + if ((int) ($supplier->cod_forn ?? 0) > 0) { + $directCodes[] = (string) (int) $supplier->cod_forn; + } + if ((int) ($supplier->old_id ?? 0) > 0) { + $directCodes[] = (string) (int) $supplier->old_id; + } + + $operationalConfig = is_array($supplier->operational_config ?? null) ? $supplier->operational_config : []; + $aliasCodes = array_map( + fn($value): string => (string) (int) $value, + array_filter((array) ($operationalConfig['legacy_cod_forn_aliases'] ?? []), fn($value): bool => is_numeric($value) && (int) $value > 0) + ); + $aliasOldIds = array_map( + fn($value): string => (string) (int) $value, + array_filter((array) ($operationalConfig['legacy_old_id_aliases'] ?? []), fn($value): bool => is_numeric($value) && (int) $value > 0) + ); + + foreach (array_merge($directCodes, $aliasCodes, $aliasOldIds) as $code) { + $this->registerFornitoreCodeMap($map, $code, (int) $supplier->id, in_array($code, $directCodes, true)); + } + } + + foreach ($codes as $code) { + $trim = trim((string) $code); + if ($trim === '') { + continue; + } + + $noZero = ltrim($trim, '0'); + if ($noZero !== '' && isset($map[$noZero]) && ! isset($map[$trim])) { + $map[$trim] = $map[$noZero]; + } + if ($noZero !== '' && isset($map[$trim]) && ! isset($map[$noZero])) { + $map[$noZero] = $map[$trim]; + } + } + + return $map; + } + + /** @param array $map */ + private function registerFornitoreCodeMap(array &$map, string $code, int $supplierId, bool $isDirectCode): void + { + $trim = trim($code); + if ($supplierId <= 0 || $trim === '') { + return; + } + + $keys = array_values(array_unique(array_filter([$trim, ltrim($trim, '0')], fn($value): bool => $value !== ''))); + foreach ($keys as $key) { + if (! isset($map[$key]) || $isDirectCode) { + $map[$key] = $supplierId; + } + } + } + + /** @return array */ + private function resolveEquivalentFornitoreIds(int $fornitoreId): array + { + if ($fornitoreId <= 0 || ! Schema::hasTable('fornitori')) { + return [$fornitoreId]; + } + + $supplier = Fornitore::query()->find($fornitoreId, ['id', 'rubrica_id', 'partita_iva', 'codice_fiscale', 'old_id']); + if (! $supplier) { + return [$fornitoreId]; + } + + $query = Fornitore::query()->where('id', $fornitoreId); + + $rubricaId = (int) ($supplier->rubrica_id ?? 0); + if ($rubricaId > 0) { + $query->orWhere('rubrica_id', $rubricaId); + } + + $partitaIva = trim((string) ($supplier->partita_iva ?? '')); + if ($partitaIva !== '') { + $query->orWhere('partita_iva', $partitaIva); + } + + $codiceFiscale = trim((string) ($supplier->codice_fiscale ?? '')); + if ($codiceFiscale !== '') { + $query->orWhere('codice_fiscale', $codiceFiscale); + } + + $oldId = (int) ($supplier->old_id ?? 0); + if ($oldId > 0) { + $query->orWhere('old_id', $oldId); + } + + $ids = $query->pluck('id') + ->map(fn($value) => (int) $value) + ->filter(fn($value) => $value > 0) + ->unique() + ->values() + ->all(); + + return $ids !== [] ? $ids : [$fornitoreId]; + } } diff --git a/app/Console/Commands/TecnoRepairImportLegacyArchiveCommand.php b/app/Console/Commands/TecnoRepairImportLegacyArchiveCommand.php index c480b78..533cd9f 100644 --- a/app/Console/Commands/TecnoRepairImportLegacyArchiveCommand.php +++ b/app/Console/Commands/TecnoRepairImportLegacyArchiveCommand.php @@ -185,8 +185,8 @@ public function handle(TecnoRepairMdbReader $reader, FornitoreProductCatalogServ ->orderBy('id') ->get(); - $existing = $matchingSchede->first(); - $duplicateSchede = $matchingSchede->slice(1); + $existing = $matchingSchede->first(); + $duplicateSchede = $matchingSchede->slice(1); $duplicateSchedaIds = $duplicateSchede ->pluck('id') ->filter(fn($value): bool => is_numeric($value) && (int) $value > 0) @@ -383,7 +383,7 @@ private function consolidateLegacyDuplicates(int $amministratoreId, ?int $fornit } /** @var AssistenzaTecnorepairScheda $canonical */ - $canonical = $matchingSchede->first(); + $canonical = $matchingSchede->first(); $duplicateSchede = $matchingSchede->slice(1); foreach ($duplicateSchede as $duplicateScheda) { diff --git a/app/Filament/Pages/Affitti/GestioneAffitti.php b/app/Filament/Pages/Affitti/GestioneAffitti.php index c09c926..dc0ef7a 100644 --- a/app/Filament/Pages/Affitti/GestioneAffitti.php +++ b/app/Filament/Pages/Affitti/GestioneAffitti.php @@ -614,24 +614,24 @@ private function loadAffitti(): void return [$a->id => $label]; })->all(); $this->affittiList = $rows->map(fn(AffittoImmobile $a) => [ - 'warnings' => $this->buildAffittoWarnings($a), - 'id' => $a->id, - 'codice_appartamento' => $a->codice_appartamento, - 'descrizione_immobile' => $a->descrizione_immobile, - 'nome_inquilino' => $a->nome_inquilino, - 'proprietario_nome' => $a->proprietario_nome, - 'inizio_contratto' => $a->inizio_contratto, + 'warnings' => $this->buildAffittoWarnings($a), + 'id' => $a->id, + 'codice_appartamento' => $a->codice_appartamento, + 'descrizione_immobile' => $a->descrizione_immobile, + 'nome_inquilino' => $a->nome_inquilino, + 'proprietario_nome' => $a->proprietario_nome, + 'inizio_contratto' => $a->inizio_contratto, 'presa_in_carico_operativa_dal' => $a->presa_in_carico_operativa_dal, - 'prossima_registrazione' => $a->prossima_registrazione, - 'tipo_riga' => $a->tipo_riga, - 'stabile_id' => $a->stabile_id, - 'dovuti_totale' => (float) ($a->dovuti_totale ?? 0), - 'pagati_totale' => (float) ($a->pagati_totale ?? 0), - 'saldo_totale' => (float) ($a->dovuti_totale ?? 0) - (float) ($a->pagati_totale ?? 0), - 'obsoleto' => (bool) ($a->obsoleto ?? false), - 'obsoleto_dal' => $a->obsoleto_dal, - 'stabile' => $a->stabile?->codice_univoco ?: ($a->stabile?->codice_stabile ?: $a->codice_stabile_legacy), - 'stabile_nome' => $a->stabile?->denominazione, + 'prossima_registrazione' => $a->prossima_registrazione, + 'tipo_riga' => $a->tipo_riga, + 'stabile_id' => $a->stabile_id, + 'dovuti_totale' => (float) ($a->dovuti_totale ?? 0), + 'pagati_totale' => (float) ($a->pagati_totale ?? 0), + 'saldo_totale' => (float) ($a->dovuti_totale ?? 0) - (float) ($a->pagati_totale ?? 0), + 'obsoleto' => (bool) ($a->obsoleto ?? false), + 'obsoleto_dal' => $a->obsoleto_dal, + 'stabile' => $a->stabile?->codice_univoco ?: ($a->stabile?->codice_stabile ?: $a->codice_stabile_legacy), + 'stabile_nome' => $a->stabile?->denominazione, ])->all(); } diff --git a/app/Filament/Pages/Condomini/ContrattiHub.php b/app/Filament/Pages/Condomini/ContrattiHub.php new file mode 100644 index 0000000..d005fd8 --- /dev/null +++ b/app/Filament/Pages/Condomini/ContrattiHub.php @@ -0,0 +1,470 @@ +hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + } + + public function mount(): void + { + $user = Auth::user(); + if (! $user instanceof User) { + return; + } + + $activeId = StabileContext::resolveActiveStabileId($user); + if (! $activeId) { + return; + } + + $this->stabileAttivo = StabileContext::accessibleStabili($user)->firstWhere('id', $activeId); + } + + protected function getHeaderActions(): array + { + return [ + Action::make('registra_contratto') + ->label('Registra contratto') + ->icon('heroicon-o-plus') + ->form($this->getContrattoFormSchema()) + ->action(function (array $data): void { + $this->createContratto($data); + }), + Action::make('apri_documenti') + ->label('Archivio documenti') + ->icon('heroicon-o-folder-open') + ->url(fn(): string => DocumentiArchivio::getUrl(panel: 'admin-filament')), + ]; + } + + public function getStatsProperty(): array + { + $stabileId = (int) ($this->stabileAttivo?->id ?? 0); + if ($stabileId <= 0) { + return ['totale' => 0, 'attivi' => 0, 'scadenze' => 0, 'documenti' => 0]; + } + + $query = StabileContratto::query()->where('stabile_id', $stabileId); + + return [ + 'totale' => (clone $query)->count(), + 'attivi' => (clone $query)->where('stato', 'attivo')->count(), + 'scadenze' => Scadenza::query() + ->where('stabile_id', $stabileId) + ->whereNotNull('stabile_contratto_id') + ->whereDate('data_scadenza', '>=', now()->toDateString()) + ->whereDate('data_scadenza', '<=', now()->addDays(45)->toDateString()) + ->count(), + 'documenti' => Documento::query() + ->where('stabile_id', $stabileId) + ->where('tipo_documento', 'contratto') + ->count(), + ]; + } + + public function getContrattiRowsProperty(): Collection + { + $stabileId = (int) ($this->stabileAttivo?->id ?? 0); + if ($stabileId <= 0) { + return collect(); + } + + return StabileContratto::query() + ->with(['fornitore:id,ragione_sociale,nome,cognome', 'documentoPrincipale:id,nome,numero_protocollo']) + ->where('stabile_id', $stabileId) + ->orderByRaw("CASE WHEN stato = 'attivo' THEN 0 WHEN stato = 'in_revisione' THEN 1 ELSE 2 END") + ->orderByDesc('decorrenza_dal') + ->orderByDesc('id') + ->get(); + } + + public function getUpcomingScadenzeProperty(): Collection + { + $stabileId = (int) ($this->stabileAttivo?->id ?? 0); + if ($stabileId <= 0) { + return collect(); + } + + return Scadenza::query() + ->with('stabileContratto:id,titolo,tipo_contratto') + ->where('stabile_id', $stabileId) + ->whereNotNull('stabile_contratto_id') + ->whereDate('data_scadenza', '>=', now()->subDays(7)->toDateString()) + ->orderBy('data_scadenza') + ->limit(10) + ->get(); + } + + protected function getContrattoFormSchema(): array + { + return [ + Select::make('stabile_id') + ->label('Stabile') + ->options(fn(): array=> $this->getStabiliOptions()) + ->default(fn(): ?int => $this->stabileAttivo?->id) + ->searchable() + ->required(), + Select::make('fornitore_id') + ->label('Fornitore / manutentore') + ->options(fn(): array=> $this->getFornitoriOptions()) + ->searchable() + ->nullable(), + Select::make('tipo_contratto') + ->label('Tipo contratto') + ->options($this->getTipoContrattoOptions()) + ->required(), + TextInput::make('titolo') + ->label('Titolo') + ->required() + ->maxLength(255), + TextInput::make('servizio_label') + ->label('Servizio / utenza') + ->placeholder('Es. Ascensore scala A, Acqua potabile, Luce comune') + ->maxLength(255), + TextInput::make('codice_contratto') + ->label('Codice contratto') + ->maxLength(80), + TextInput::make('riferimento_esterno') + ->label('Riferimento esterno') + ->placeholder('Es. numero cliente, riferimento pratica, ID POD/PDR principale') + ->maxLength(120), + DatePicker::make('data_stipula') + ->label('Data stipula'), + DatePicker::make('decorrenza_dal') + ->label('Decorrenza dal') + ->required(), + DatePicker::make('decorrenza_al') + ->label('Decorrenza al'), + Select::make('frequenza_scadenze') + ->label('Frequenza scadenze') + ->options($this->getFrequenzaOptions()) + ->default('annuale') + ->required(), + TextInput::make('giorno_scadenza') + ->label('Giorno scadenza') + ->numeric() + ->minValue(1) + ->maxValue(28), + TextInput::make('giorni_preavviso') + ->label('Giorni preavviso') + ->numeric() + ->default(30) + ->minValue(0) + ->maxValue(365), + Toggle::make('rinnovo_automatico') + ->label('Rinnovo automatico') + ->default(false), + TextInput::make('importo_periodico') + ->label('Importo periodico') + ->numeric() + ->step(0.01) + ->minValue(0), + Select::make('documento_principale_id') + ->label('Documento principale') + ->options(fn(callable $get): array=> $this->getDocumentiContrattoOptions((int) ($get('stabile_id') ?: 0))) + ->searchable() + ->nullable(), + Repeater::make('codici_collegati') + ->label('Codici collegati') + ->schema([ + TextInput::make('chiave') + ->label('Chiave') + ->placeholder('Es. POD, PDR, matricola_contatore, codice_cliente, utenza, matricola_ascensore') + ->required(), + TextInput::make('valore') + ->label('Valore') + ->required(), + ]) + ->addActionLabel('Aggiungi codice') + ->defaultItems(0) + ->columns(2), + Textarea::make('note') + ->label('Note') + ->rows(4), + ]; + } + + protected function createContratto(array $data): void + { + $user = Auth::user(); + if (! $user instanceof User) { + Notification::make()->title('Utente non valido')->danger()->send(); + return; + } + + $stabileId = (int) ($data['stabile_id'] ?? 0); + $stabile = StabileContext::accessibleStabili($user)->firstWhere('id', $stabileId); + if (! $stabile instanceof Stabile) { + Notification::make()->title('Stabile non valido')->danger()->send(); + return; + } + + $contract = StabileContratto::query()->create([ + 'stabile_id' => $stabileId, + 'fornitore_id' => isset($data['fornitore_id']) && is_numeric($data['fornitore_id']) ? (int) $data['fornitore_id'] : null, + 'documento_principale_id' => isset($data['documento_principale_id']) && is_numeric($data['documento_principale_id']) ? (int) $data['documento_principale_id'] : null, + 'titolo' => trim((string) ($data['titolo'] ?? '')), + 'tipo_contratto' => trim((string) ($data['tipo_contratto'] ?? 'altro')), + 'categoria_impianto' => trim((string) ($data['tipo_contratto'] ?? 'altro')), + 'servizio_label' => $this->nullableString($data['servizio_label'] ?? null), + 'codice_contratto' => $this->nullableString($data['codice_contratto'] ?? null), + 'riferimento_esterno' => $this->nullableString($data['riferimento_esterno'] ?? null), + 'stato' => 'attivo', + 'data_stipula' => $data['data_stipula'] ?? null, + 'decorrenza_dal' => $data['decorrenza_dal'] ?? null, + 'decorrenza_al' => $data['decorrenza_al'] ?? null, + 'frequenza_scadenze' => $this->nullableString($data['frequenza_scadenze'] ?? null), + 'giorno_scadenza' => isset($data['giorno_scadenza']) && is_numeric($data['giorno_scadenza']) ? (int) $data['giorno_scadenza'] : null, + 'giorni_preavviso' => isset($data['giorni_preavviso']) && is_numeric($data['giorni_preavviso']) ? (int) $data['giorni_preavviso'] : 30, + 'rinnovo_automatico' => (bool) ($data['rinnovo_automatico'] ?? false), + 'importo_periodico' => isset($data['importo_periodico']) && is_numeric($data['importo_periodico']) ? (float) $data['importo_periodico'] : null, + 'valuta' => 'EUR', + 'codici_collegati' => $this->normalizeKeyValueRows($data['codici_collegati'] ?? []), + 'dati_contratto' => [ + 'origine' => 'hub_contratti_stabile', + 'documenti_url' => DocumentiArchivio::getUrl(panel: 'admin-filament'), + ], + 'note' => $this->nullableString($data['note'] ?? null), + 'created_by' => (int) $user->id, + 'updated_by' => (int) $user->id, + ]); + + $this->syncScadenzeContratto($contract); + + Notification::make() + ->title('Contratto registrato') + ->body('Scadenze programmate: ' . $contract->scadenze()->count()) + ->success() + ->send(); + } + + protected function syncScadenzeContratto(StabileContratto $contract): void + { + $contract->scadenze()->delete(); + + foreach ($this->buildScheduleDates($contract) as $date) { + $popupDal = $date->copy()->subDays(max(0, (int) ($contract->giorni_preavviso ?? 0))); + + Scadenza::query()->create([ + 'stabile_id' => (int) $contract->stabile_id, + 'stabile_contratto_id' => (int) $contract->id, + 'descrizione_sintetica' => $contract->titolo, + 'data_scadenza' => $date->toDateString(), + 'natura' => 'contratto_stabile', + 'natura_label' => 'Scadenza contratto', + 'gestione_tipo' => $contract->tipo_contratto, + 'fatto' => 'N', + 'tipo_riga' => trim((string) ($contract->servizio_label ?: $contract->tipo_contratto)), + 'richiede_pop_up' => true, + 'giorni_anticipo' => (int) ($contract->giorni_preavviso ?? 0), + 'popup_dal' => $popupDal->toDateString(), + 'legacy_payload' => [ + 'source' => 'stabile_contratti', + 'codice_contratto' => $contract->codice_contratto, + 'riferimento_esterno' => $contract->riferimento_esterno, + 'codici_collegati' => $contract->codici_collegati ?? [], + ], + ]); + } + } + + /** @return array */ + protected function buildScheduleDates(StabileContratto $contract): array + { + $start = $contract->decorrenza_dal instanceof Carbon + ? $contract->decorrenza_dal->copy() + : ($contract->decorrenza_al instanceof Carbon ? $contract->decorrenza_al->copy() : now()->copy()); + $end = $contract->decorrenza_al instanceof Carbon ? $contract->decorrenza_al->copy() : null; + $frequency = trim((string) ($contract->frequenza_scadenze ?? 'una_tantum')); + + if ($contract->giorno_scadenza) { + $start->day(min(28, (int) $contract->giorno_scadenza)); + } + + if ($frequency === '' || $frequency === 'una_tantum') { + return [$end ?: $start]; + } + + $dates = []; + $cursor = $start->copy(); + $limit = $end + ? $end->copy() + : ($contract->rinnovo_automatico ? $start->copy()->addMonths(24) : $start->copy()->addMonths(12)); + + while ($cursor <= $limit && count($dates) < 60) { + $dates[] = $cursor->copy(); + $cursor = match ($frequency) { + 'mensile' => $cursor->copy()->addMonthNoOverflow(), + 'bimestrale' => $cursor->copy()->addMonthsNoOverflow(2), + 'trimestrale' => $cursor->copy()->addMonthsNoOverflow(3), + 'semestrale' => $cursor->copy()->addMonthsNoOverflow(6), + default => $cursor->copy()->addYearNoOverflow(), + }; + + if ($contract->giorno_scadenza) { + $cursor->day(min(28, (int) $contract->giorno_scadenza)); + } + } + + return $dates; + } + + protected function getTipoContrattoOptions(): array + { + return [ + 'acqua' => 'Acqua', + 'energia_elettrica' => 'Energia elettrica', + 'ascensore' => 'Ascensore', + 'gas' => 'Gas', + 'pulizie' => 'Pulizie', + 'portierato' => 'Portierato', + 'assicurazione' => 'Assicurazione', + 'antincendio' => 'Antincendio', + 'raffrescamento' => 'Raffrescamento', + 'riscaldamento' => 'Riscaldamento', + 'telefonia_dati' => 'Telefonia / dati', + 'altro' => 'Altro servizio', + ]; + } + + protected function getFrequenzaOptions(): array + { + return [ + 'una_tantum' => 'Una tantum', + 'mensile' => 'Mensile', + 'bimestrale' => 'Bimestrale', + 'trimestrale' => 'Trimestrale', + 'semestrale' => 'Semestrale', + 'annuale' => 'Annuale', + ]; + } + + protected function getStabiliOptions(): array + { + $user = Auth::user(); + if (! $user instanceof User) { + return []; + } + + return StabileContext::accessibleStabili($user) + ->mapWithKeys(fn(Stabile $stabile): array=> [(string) $stabile->id => trim((string) ($stabile->denominazione ?: ('Stabile #' . $stabile->id)))]) + ->all(); + } + + protected function getFornitoriOptions(): array + { + return Fornitore::query() + ->orderByRaw("COALESCE(NULLIF(ragione_sociale, ''), CONCAT(COALESCE(nome, ''), ' ', COALESCE(cognome, ''))) asc") + ->limit(1500) + ->get(['id', 'ragione_sociale', 'nome', 'cognome']) + ->mapWithKeys(function (Fornitore $fornitore): array { + $label = trim((string) ($fornitore->ragione_sociale ?: trim(($fornitore->nome ?? '') . ' ' . ($fornitore->cognome ?? '')))); + + return [(string) $fornitore->id => ($label !== '' ? $label : ('Fornitore #' . $fornitore->id))]; + }) + ->all(); + } + + protected function getDocumentiContrattoOptions(int $stabileId): array + { + if ($stabileId <= 0) { + return []; + } + + return Documento::query() + ->where('stabile_id', $stabileId) + ->where('tipo_documento', 'contratto') + ->orderByDesc('data_documento') + ->limit(250) + ->get(['id', 'nome', 'numero_protocollo']) + ->mapWithKeys(fn(Documento $documento): array=> [ + (string) $documento->id => trim((string) (($documento->numero_protocollo ? $documento->numero_protocollo . ' · ' : '') . ($documento->nome ?: ('Documento #' . $documento->id)))), + ]) + ->all(); + } + + /** @param array $rows */ + protected function normalizeKeyValueRows(array $rows): array + { + return collect($rows) + ->map(function ($row): ?array { + $key = trim((string) data_get($row, 'chiave', '')); + $value = trim((string) data_get($row, 'valore', '')); + + return ($key !== '' && $value !== '') ? ['chiave' => $key, 'valore' => $value] : null; + }) + ->filter() + ->values() + ->all(); + } + + protected function nullableString(mixed $value): ?string + { + $value = trim((string) $value); + + return $value !== '' ? $value : null; + } + + public function contractBadgeColor(string $status): string + { + return match ($status) { + 'attivo' => 'emerald', + 'in_revisione' => 'amber', + 'scaduto' => 'rose', + default => 'slate', + }; + } + + public function contractTypeLabel(?string $type): string + { + $key = trim((string) $type); + + return $this->getTipoContrattoOptions()[$key] ?? ($key !== '' ? Str::headline($key) : 'Contratto'); + } +} diff --git a/app/Filament/Pages/Condomini/ServiziStabileArchivio.php b/app/Filament/Pages/Condomini/ServiziStabileArchivio.php index 4040830..c6131b1 100644 --- a/app/Filament/Pages/Condomini/ServiziStabileArchivio.php +++ b/app/Filament/Pages/Condomini/ServiziStabileArchivio.php @@ -195,13 +195,7 @@ public function ensureAcquaAceaContract(): void return; } - $fornitoreAcea = Fornitore::query() - ->whereRaw('LOWER(ragione_sociale) like ?', ['%acea%ato2%']) - ->orderBy('ragione_sociale') - ->first() ?? Fornitore::query() - ->whereRaw('LOWER(ragione_sociale) like ?', ['%acea%']) - ->orderBy('ragione_sociale') - ->first(); + $fornitoreAcea = $this->resolvePreferredAcquaSupplier($stabileId); if (! $fornitoreAcea) { Notification::make()->title('Fornitore Acea Ato2 non trovato in anagrafica fornitori.')->warning()->send(); @@ -1241,11 +1235,7 @@ public function getAcquaContrattoStabileProperty(): array ->orderBy('id') ->first(); - $fornitoreSuggerito = Fornitore::query() - ->whereRaw('LOWER(ragione_sociale) like ?', ['%acea%ato2%']) - ->value('ragione_sociale') ?? Fornitore::query() - ->whereRaw('LOWER(ragione_sociale) like ?', ['%acea%']) - ->value('ragione_sociale'); + $fornitoreSuggerito = $this->resolvePreferredAcquaSupplier($stabileId); return [ 'servizio_id' => $servizio?->id, @@ -1256,7 +1246,7 @@ public function getAcquaContrattoStabileProperty(): array 'codice_cliente' => $servizio?->codice_cliente, 'codice_contratto' => $servizio?->codice_contratto, 'matricola' => $servizio?->contatore_matricola, - 'suggerito_fornitore' => $fornitoreSuggerito ? (string) $fornitoreSuggerito : null, + 'suggerito_fornitore' => $fornitoreSuggerito?->ragione_sociale, ]; } @@ -1429,6 +1419,8 @@ public function getAcquaFatturePerGestioneProperty(): array ->values() ->all(); + $fornitoreIds = $this->resolveEquivalentFornitoreIds($fornitoreIds); + if (Schema::hasTable('fornitori') && Schema::hasColumn('fornitori', 'fe_features')) { $waterEnabledSupplierIds = Fornitore::query() ->where(function (Builder $query) use ($stabileId): Builder { @@ -1452,7 +1444,17 @@ public function getAcquaFatturePerGestioneProperty(): array ->values() ->all(); - $fornitoreIds = array_values(array_unique(array_merge($fornitoreIds, $waterEnabledSupplierIds))); + $fornitoreIds = array_values(array_unique(array_merge( + $fornitoreIds, + $this->resolveEquivalentFornitoreIds($waterEnabledSupplierIds) + ))); + } + + if ($fornitoreIds === []) { + $preferredSupplier = $this->resolvePreferredAcquaSupplier($stabileId); + if ($preferredSupplier) { + $fornitoreIds = $this->resolveEquivalentFornitoreIds([(int) $preferredSupplier->id]); + } } if ($fornitoreIds !== []) { @@ -1557,6 +1559,192 @@ public function getAcquaFatturePerGestioneProperty(): array return $out; } + private function resolvePreferredAcquaSupplier(?int $stabileId = null): ?Fornitore + { + if (! Schema::hasTable('fornitori')) { + return null; + } + + $serviceSupplierId = null; + if ($stabileId) { + $serviceSupplierId = StabileServizio::query() + ->where('stabile_id', $stabileId) + ->where('tipo', 'acqua') + ->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 ?', ['%acea%ato2%']) + ->orWhereRaw('LOWER(ragione_sociale) like ?', ['%acea acqua%']); + }) + ->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->scorePreferredAcquaSupplier($left, $serviceSupplierId, $userAdminId, $stableInvoiceCounts); + $rightScore = $this->scorePreferredAcquaSupplier($right, $serviceSupplierId, $userAdminId, $stableInvoiceCounts); + + if ($leftScore === $rightScore) { + return (int) $left->id <=> (int) $right->id; + } + + return $rightScore <=> $leftScore; + }); + + return $sorted->first(); + } + + /** @param array $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 $stableInvoiceCounts */ + private function scorePreferredAcquaSupplier(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, 'acqua.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> */ public function getAcquaLettureCondominiProperty(): array { diff --git a/app/Filament/Pages/Condomini/StabilePage.php b/app/Filament/Pages/Condomini/StabilePage.php index f7a8c53..f32f20e 100644 --- a/app/Filament/Pages/Condomini/StabilePage.php +++ b/app/Filament/Pages/Condomini/StabilePage.php @@ -6,6 +6,7 @@ use App\Models\CommunicationMessage; use App\Models\Documento; use App\Models\DocumentoCollegato; +use App\Models\DocumentoStabile; use App\Models\InsuranceClaim; use App\Models\InsurancePolicy; use App\Models\RubricaUniversale; @@ -68,6 +69,10 @@ class StabilePage extends Page public ?string $gestioneDataInizio = null; + public ?string $presaInCaricoAmministrazione = null; + + public $verbaleNominaUpload = null; + public ?string $gestioneDataFine = null; /** @var array */ @@ -274,6 +279,97 @@ public function toggleMostraRiscaldamento(): void $this->dispatch('refresh-gestioni-stabile'); } + public function saveAmministrazioneSetup(): void + { + if (! $this->stabile instanceof StabileModel) { + return; + } + + $config = (array) ($this->stabile->configurazione_avanzata ?? []); + $config['amministrazione'] = array_merge( + (array) ($config['amministrazione'] ?? []), + [ + 'presa_in_carico_dal' => $this->presaInCaricoAmministrazione ?: null, + ] + ); + + $this->stabile->configurazione_avanzata = $config; + + if ($this->presaInCaricoAmministrazione) { + $this->stabile->data_nomina = $this->presaInCaricoAmministrazione; + } + + $this->stabile->save(); + + Notification::make() + ->title('Dati amministrazione aggiornati') + ->success() + ->send(); + } + + public function uploadVerbaleNomina(): void + { + if (! $this->stabile instanceof StabileModel) { + return; + } + + if (! is_object($this->verbaleNominaUpload) || ! method_exists($this->verbaleNominaUpload, 'storeAs')) { + Notification::make() + ->title('Seleziona un file da caricare') + ->warning() + ->send(); + return; + } + + $ext = strtolower((string) ($this->verbaleNominaUpload->getClientOriginalExtension() ?: 'pdf')); + $fileName = 'stabile-' . (int) $this->stabile->id . '-verbale-nomina.' . $ext; + $path = $this->verbaleNominaUpload->storeAs('stabili/verbali-nomina', $fileName, 'public'); + + $documento = DocumentoStabile::query()->create([ + 'stabile_id' => (int) $this->stabile->id, + 'nome_file' => $fileName, + 'nome_originale' => (string) $this->verbaleNominaUpload->getClientOriginalName(), + 'percorso_file' => $path, + 'categoria' => 'verbale_nomina', + 'tipo_mime' => (string) ($this->verbaleNominaUpload->getClientMimeType() ?: 'application/octet-stream'), + 'dimensione' => (int) ($this->verbaleNominaUpload->getSize() ?: 0), + 'descrizione' => 'Verbale di nomina amministrazione', + 'pubblico' => false, + 'protetto' => false, + 'versione' => 1, + 'caricato_da' => Auth::id(), + ]); + + $config = (array) ($this->stabile->configurazione_avanzata ?? []); + $config['amministrazione'] = array_merge( + (array) ($config['amministrazione'] ?? []), + ['verbale_nomina_documento_id' => (int) $documento->id] + ); + $this->stabile->configurazione_avanzata = $config; + $this->stabile->save(); + + $this->verbaleNominaUpload = null; + + Notification::make() + ->title('Verbale di nomina caricato') + ->success() + ->send(); + } + + public function getVerbaleNominaDocumento(): ?DocumentoStabile + { + if (! $this->stabile instanceof StabileModel) { + return null; + } + + $documentId = $this->stabile->getNominaVerbaleDocumentoId(); + if (! $documentId) { + return null; + } + + return DocumentoStabile::query()->find($documentId); + } + public function savePeriodiEmissione(): void { if (! $this->stabile) { @@ -1037,6 +1133,9 @@ private function initializeOfficialMailSettings(): void if ($this->officialMailboxes === []) { $this->officialMailboxes = $this->buildDefaultOfficialMailboxes(); } + + $this->presaInCaricoAmministrazione = data_get($config, 'amministrazione.presa_in_carico_dal') + ?: ($this->stabile->data_nomina?->format('Y-m-d')); } /** diff --git a/app/Filament/Pages/Contabilita/CasseBancheMovimenti.php b/app/Filament/Pages/Contabilita/CasseBancheMovimenti.php index a3013fb..d402e87 100644 --- a/app/Filament/Pages/Contabilita/CasseBancheMovimenti.php +++ b/app/Filament/Pages/Contabilita/CasseBancheMovimenti.php @@ -16,16 +16,16 @@ use App\Modules\Contabilita\Models\MovimentoBanca; use App\Modules\Contabilita\Models\PianoConti; use App\Modules\Contabilita\Models\SaldoConto; -use App\Modules\Contabilita\Services\MovimentoBancaRiconciliazioneService; use App\Modules\Contabilita\Services\MovimentoBancaPrimaNotaService; +use App\Modules\Contabilita\Services\MovimentoBancaRiconciliazioneService; use App\Modules\Contabilita\Services\PagamentoFornitorePrimaNotaService; use App\Services\Contabilita\ExcelMovimentiParser; use App\Services\Contabilita\IntesaCsvParser; use App\Services\Contabilita\MovimentiBancaImporter; use App\Services\Contabilita\MpsQifParser; use App\Services\Contabilita\UnicreditWriParser; -use App\Support\ArchivioPaths; use App\Support\AnnoGestioneContext; +use App\Support\ArchivioPaths; use App\Support\GestioneContext; use App\Support\StabileContext; use BackedEnum; @@ -38,7 +38,7 @@ use Filament\Forms\Components\Toggle; use Filament\Notifications\Notification; use Filament\Pages\Page; -use Filament\Tables\Columns\BadgeColumn; +use Filament\Tables\Columns\IconColumn; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Concerns\InteractsWithTable; use Filament\Tables\Contracts\HasTable; @@ -795,16 +795,7 @@ protected function getHeaderActions(): array 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(); + return $this->getVisibleGestioneOptions($stabileId); }) ->default(function (): ?string { $user = Auth::user(); @@ -1277,16 +1268,7 @@ public function table(Table $table): Table 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(); + return $this->getVisibleGestioneOptions($stabileId); }) ->searchable(), ]) @@ -1310,6 +1292,13 @@ public function table(Table $table): Table ->date('d/m/Y') ->sortable(), + IconColumn::make('riconciliazione_stato') + ->label('Sem') + ->alignCenter() + ->icon(fn(MovimentoBanca $record): string => $this->getMovimentoSemaphoreMeta($record)['icon']) + ->color(fn(MovimentoBanca $record): string => $this->getMovimentoSemaphoreMeta($record)['color']) + ->tooltip(fn(MovimentoBanca $record): string => $this->getMovimentoSemaphoreMeta($record)['label']), + TextColumn::make('valuta') ->label('Valuta') ->date('d/m/Y'), @@ -1359,8 +1348,9 @@ public function table(Table $table): Table ]) ->actions([ Action::make('dettaglio_movimento') - ->label('Apri dati') + ->hiddenLabel() ->icon('heroicon-o-eye') + ->tooltip('Apri dati') ->modalWidth('7xl') ->action(function (MovimentoBanca $record): void { $this->detailMovementId = $record->id; @@ -1375,8 +1365,9 @@ public function table(Table $table): Table ]); }), Action::make('conferma_movimento') - ->label('Conferma') + ->hiddenLabel() ->icon('heroicon-o-check-circle') + ->tooltip('Conferma') ->visible(fn(MovimentoBanca $record): bool => (bool) ($record->da_confermare ?? false)) ->action(function (MovimentoBanca $record): void { if (! Schema::hasColumn('contabilita_movimenti_banca', 'da_confermare')) { @@ -1408,16 +1399,7 @@ public function table(Table $table): Table 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(); + return $this->getVisibleGestioneOptions($stabileId); }), DatePicker::make('data') @@ -1531,8 +1513,9 @@ public function table(Table $table): Table }), Action::make('genera_prima_nota') - ->label('Genera prima nota') + ->hiddenLabel() ->icon('heroicon-o-book-open') + ->tooltip('Genera prima nota') ->visible(function (MovimentoBanca $record): bool { if (! Schema::hasColumn('contabilita_movimenti_banca', 'registrazione_id')) { return false; @@ -1557,19 +1540,7 @@ public function table(Table $table): Table 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(); + return $this->getVisibleGestioneOptions($stabileId); }) ->searchable() ->visible(fn(MovimentoBanca $record) => empty($record->gestione_id)) @@ -1633,19 +1604,7 @@ public function table(Table $table): Table 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(); + return $this->getVisibleGestioneOptions($stabileId); }) ->searchable() ->visible(fn(MovimentoBanca $record) => empty($record->gestione_id)) @@ -2074,8 +2033,9 @@ protected function resolveDefaultGestioneId(int $stabileId): ?string return null; } - $anno = AnnoGestioneContext::resolveActiveAnno(); - $tipo = GestioneContext::resolveActiveGestione(); + $anno = AnnoGestioneContext::resolveActiveAnno(); + $tipo = GestioneContext::resolveActiveGestione(); + $stabile = Stabile::query()->find($stabileId); $match = GestioneContabile::query() ->where('stabile_id', $stabileId) @@ -2086,6 +2046,10 @@ protected function resolveDefaultGestioneId(int $stabileId): ?string ->orderByDesc('id') ->first(); + if ($match && ! $this->isGestioneVisibleForStabile($match, $stabile)) { + $match = null; + } + if (! $match) { $match = GestioneContabile::query() ->where('stabile_id', $stabileId) @@ -2093,11 +2057,96 @@ protected function resolveDefaultGestioneId(int $stabileId): ?string ->orderByDesc('gestione_attiva') ->orderByDesc('id') ->first(); + + if ($match && ! $this->isGestioneVisibleForStabile($match, $stabile)) { + $match = GestioneContabile::query() + ->where('stabile_id', $stabileId) + ->where('stato', 'aperta') + ->get(['id', 'denominazione', 'tipo_gestione', 'anno_gestione', 'numero_straordinaria', 'protocollo_prefix', 'stato', 'gestione_attiva']) + ->first(fn(GestioneContabile $gestione): bool => $this->isGestioneVisibleForStabile($gestione, $stabile)); + } } return $match ? (string) $match->id : null; } + /** @return array */ + protected function getVisibleGestioneOptions(int $stabileId): array + { + $stabile = Stabile::query()->find($stabileId); + + return GestioneContabile::query() + ->where('stabile_id', $stabileId) + ->where('stato', 'aperta') + ->orderByDesc('anno_gestione') + ->orderBy('tipo_gestione') + ->orderBy('numero_straordinaria') + ->orderByDesc('id') + ->get(['id', 'denominazione', 'tipo_gestione', 'anno_gestione', 'numero_straordinaria', 'protocollo_prefix', 'stato']) + ->filter(fn(GestioneContabile $gestione): bool => $this->isGestioneVisibleForStabile($gestione, $stabile)) + ->mapWithKeys(fn(GestioneContabile $gestione): array=> [(string) $gestione->id => $this->formatGestioneLabel($gestione, $stabile)]) + ->all(); + } + + protected function isGestioneVisibleForStabile(GestioneContabile $gestione, ?Stabile $stabile): bool + { + if (trim((string) ($gestione->stato ?? '')) !== 'aperta') { + return false; + } + + if (! $stabile instanceof Stabile) { + return true; + } + + if ((string) ($gestione->tipo_gestione ?? '') === 'riscaldamento' && ! $stabile->hasOperationalHeating()) { + return false; + } + + $year = is_numeric($gestione->anno_gestione) ? (int) $gestione->anno_gestione : null; + + return $stabile->isOperationalYearVisible($year); + } + + protected function formatGestioneLabel(GestioneContabile $gestione, ?Stabile $stabile): string + { + $label = trim((string) ($gestione->denominazione ?? '')); + if ($label === '') { + $label = (string) $gestione->anno_gestione . ' - ' . (string) $gestione->tipo_gestione; + } + + if (is_numeric($gestione->numero_straordinaria) && (int) $gestione->numero_straordinaria > 0) { + $label .= ' #' . (int) $gestione->numero_straordinaria; + } + + $prefix = trim((string) ($gestione->protocollo_prefix ?? '')); + if ($prefix !== '') { + $label = $prefix . ' · ' . $label; + } + + if ($stabile instanceof Stabile && $stabile->getTakeoverDate()) { + $year = is_numeric($gestione->anno_gestione) ? (int) $gestione->anno_gestione : null; + if (is_int($year) && $year < (int) $stabile->getTakeoverDate()->year) { + $label .= ' · pre-presa in carico'; + } + } + + return $label; + } + + /** @return array{icon:string,color:string,label:string} */ + protected function getMovimentoSemaphoreMeta(MovimentoBanca $record): array + { + if (! empty($record->registrazione_id) || $this->hasOperationalRiconciliazione($record)) { + return ['icon' => 'heroicon-m-check-circle', 'color' => 'success', 'label' => 'Riconciliato']; + } + + if ((bool) ($record->da_confermare ?? false)) { + return ['icon' => 'heroicon-m-exclamation-triangle', 'color' => 'warning', 'label' => 'Da confermare']; + } + + return ['icon' => 'heroicon-m-minus-circle', 'color' => 'gray', 'label' => 'Da lavorare']; + } + protected function resolveGestioneForYear(int $stabileId, int $year, ?string $preferredType = null): ?GestioneContabile { if (! DB::getSchemaBuilder()->hasTable('gestioni_contabili')) { @@ -2110,7 +2159,7 @@ protected function resolveGestioneForYear(int $stabileId, int $year, ?string $pr if (is_string($preferredType) && trim($preferredType) !== '') { $preferred = trim((string) $preferredType); - $match = (clone $query) + $match = (clone $query) ->where('tipo_gestione', $preferred) ->orderByDesc('gestione_attiva') ->orderByRaw("CASE WHEN stato = 'aperta' THEN 0 ELSE 1 END") @@ -2430,10 +2479,10 @@ public function getRiconciliazioneStats(): array $records = (clone $query)->get(['id', 'registrazione_id', 'match_data']); - $totale = $records->count(); + $totale = $records->count(); $senzaPrimaNota = $records->filter(fn(MovimentoBanca $record): bool => empty($record->registrazione_id) && ! $this->hasOperationalRiconciliazione($record))->count(); - $collegati = $records->filter(fn(MovimentoBanca $record): bool => ! empty($record->registrazione_id) || $this->hasOperationalRiconciliazione($record))->count(); - $daConfermare = Schema::hasColumn('contabilita_movimenti_banca', 'da_confermare') + $collegati = $records->filter(fn(MovimentoBanca $record): bool => ! empty($record->registrazione_id) || $this->hasOperationalRiconciliazione($record))->count(); + $daConfermare = Schema::hasColumn('contabilita_movimenti_banca', 'da_confermare') ? (clone $query)->where('da_confermare', true)->count() : 0; @@ -2554,8 +2603,8 @@ public function getRiconciliazioneCandidates(): array public function riconciliaCanoneAffitto(int $canoneDovutoId): void { - $user = Auth::user(); - $current = $this->getRiconciliazioneCurrent(); + $user = Auth::user(); + $current = $this->getRiconciliazioneCurrent(); $stabileId = $this->getActiveStabileId(); if (! $user instanceof User || ! $current || ! ($current['record'] instanceof MovimentoBanca) || ! $stabileId) { @@ -2589,8 +2638,8 @@ public function riconciliaCanoneAffitto(int $canoneDovutoId): void public function riconciliaPagamentoFornitore(int $fatturaFornitoreId): void { - $user = Auth::user(); - $current = $this->getRiconciliazioneCurrent(); + $user = Auth::user(); + $current = $this->getRiconciliazioneCurrent(); $stabileId = $this->getActiveStabileId(); if (! $user instanceof User || ! $current || ! ($current['record'] instanceof MovimentoBanca) || ! $stabileId) { @@ -2614,19 +2663,19 @@ public function riconciliaPagamentoFornitore(int $fatturaFornitoreId): void $current['record'], (int) $fattura->fornitore_id, [ - 'gestione_id' => (int) (($fattura->gestione_id ?? 0) ?: ($current['record']->gestione_id ?? 0)), + 'gestione_id' => (int) (($fattura->gestione_id ?? 0) ?: ($current['record']->gestione_id ?? 0)), 'fattura_fornitore_id' => (int) $fattura->id, - 'descrizione' => 'Pagamento fattura fornitore ' . trim((string) ($fattura->numero_documento ?: $fattura->id)), + 'descrizione' => 'Pagamento fattura fornitore ' . trim((string) ($fattura->numero_documento ?: $fattura->id)), ] ); - $matchData = is_array($current['record']->match_data ?? null) ? $current['record']->match_data : []; + $matchData = is_array($current['record']->match_data ?? null) ? $current['record']->match_data : []; $matchData['riconciliato_operativo'] = true; - $matchData['riconciliazione_tipo'] = 'pagamento_fattura_fornitore'; - $matchData['riconciliazione_label'] = 'Pagamento fattura fornitore'; - $matchData['riconciliazione_at'] = now()->toDateTimeString(); - $matchData['fattura_fornitore_id'] = (int) $fattura->id; - $matchData['fornitore_id'] = (int) $fattura->fornitore_id; + $matchData['riconciliazione_tipo'] = 'pagamento_fattura_fornitore'; + $matchData['riconciliazione_label'] = 'Pagamento fattura fornitore'; + $matchData['riconciliazione_at'] = now()->toDateTimeString(); + $matchData['fattura_fornitore_id'] = (int) $fattura->id; + $matchData['fornitore_id'] = (int) $fattura->fornitore_id; if (Schema::hasColumn('contabilita_movimenti_banca', 'match_data')) { $current['record']->match_data = $matchData; @@ -2767,15 +2816,15 @@ protected function buildIncassoCandidates(MovimentoBanca $movimento, int $stabil ->get() ->map(function (IncassoPagamento $incasso) use ($movimento, $descrizione, $importo, $dateColumn, $importoColumn): array { $candidateDateRaw = data_get($incasso, $dateColumn); - $candidateDate = $candidateDateRaw instanceof Carbon + $candidateDate = $candidateDateRaw instanceof Carbon ? $candidateDateRaw : ($candidateDateRaw ? Carbon::parse((string) $candidateDateRaw) : null); $candidateImporto = (float) data_get($incasso, $importoColumn, 0); - $candidateLabel = trim( + $candidateLabel = trim( (string) (data_get($incasso, 'causale') - ?: data_get($incasso, 'descrizione') - ?: data_get($incasso, 'modalita_pagamento_label') - ?: 'Incasso') + ?: data_get($incasso, 'descrizione') + ?: data_get($incasso, 'modalita_pagamento_label') + ?: 'Incasso') ); $candidateNote = trim((string) (data_get($incasso, 'note_bancarie') ?: data_get($incasso, 'descrizione') ?: '')); @@ -2812,7 +2861,7 @@ protected function buildAffittoCandidates(MovimentoBanca $movimento, int $stabil return []; } - $importo = abs((float) $movimento->importo); + $importo = abs((float) $movimento->importo); $descrizione = (string) ($movimento->descrizione_estesa_pulita ?? $movimento->descrizione_estesa ?? $movimento->descrizione ?? ''); return AffittoCanoneDovuto::query() @@ -2862,7 +2911,7 @@ protected function buildAffittoCandidates(MovimentoBanca $movimento, int $stabil $managementStart = Carbon::parse($affitto->inizio_contratto)->startOfMonth(); } if (! empty($affitto->presa_in_carico_operativa_dal)) { - $takeoverDate = Carbon::parse($affitto->presa_in_carico_operativa_dal)->startOfMonth(); + $takeoverDate = Carbon::parse($affitto->presa_in_carico_operativa_dal)->startOfMonth(); $managementStart = $managementStart ? $managementStart->max($takeoverDate) : $takeoverDate; } @@ -2875,8 +2924,8 @@ protected function buildAffittoCandidates(MovimentoBanca $movimento, int $stabil return $candidateMonth->gte($managementStart); }) ->map(function (AffittoCanoneDovuto $canone) use ($movimento, $descrizione, $importo): array { - $affitto = $canone->affitto; - $tenantLabel = $this->resolveRubricaLabel($affitto?->rubricaInquilino) ?: trim((string) ($affitto?->nome_inquilino ?? 'Inquilino')); + $affitto = $canone->affitto; + $tenantLabel = $this->resolveRubricaLabel($affitto?->rubricaInquilino) ?: trim((string) ($affitto?->nome_inquilino ?? 'Inquilino')); $immobileLabel = trim((string) ($affitto?->descrizione_immobile ?? $affitto?->indirizzo_immobile ?? 'immobile')); $score = $this->scoreCandidate( @@ -2889,20 +2938,20 @@ protected function buildAffittoCandidates(MovimentoBanca $movimento, int $stabil ); if ((int) ($affitto?->conto_bancario_id ?? 0) > 0 && (int) ($movimento->conto_id ?? 0) === (int) $affitto->conto_bancario_id) { - $score['totale'] = min(100, $score['totale'] + 10); + $score['totale'] = min(100, $score['totale'] + 10); $score['motivo'] .= ' · stesso conto affitto'; } return [ - 'tipo' => 'Canone affitto aperto', - 'titolo' => trim($tenantLabel . ' · ' . $immobileLabel), - 'score' => $score['totale'], - 'importo' => (float) $canone->totale, - 'data' => ($canone->data_scadenza ?? $canone->data_emissione)?->format('d/m/Y') ?? '—', - 'dettaglio' => 'Canone ' . str_pad((string) $canone->mese, 2, '0', STR_PAD_LEFT) . '/' . (string) $canone->anno . ($canone->n_ricevuta ? (' · ricevuta ' . $canone->n_ricevuta) : ''), - 'motivo' => $score['motivo'], - 'azione' => 'riconcilia_affitto', - 'canone_dovuto_id' => (int) $canone->id, + 'tipo' => 'Canone affitto aperto', + 'titolo' => trim($tenantLabel . ' · ' . $immobileLabel), + 'score' => $score['totale'], + 'importo' => (float) $canone->totale, + 'data' => ($canone->data_scadenza ?? $canone->data_emissione)?->format('d/m/Y') ?? '—', + 'dettaglio' => 'Canone ' . str_pad((string) $canone->mese, 2, '0', STR_PAD_LEFT) . '/' . (string) $canone->anno . ($canone->n_ricevuta ? (' · ricevuta ' . $canone->n_ricevuta) : ''), + 'motivo' => $score['motivo'], + 'azione' => 'riconcilia_affitto', + 'canone_dovuto_id' => (int) $canone->id, ]; }) ->sortByDesc('score') @@ -2915,8 +2964,8 @@ protected function buildAffittoCandidates(MovimentoBanca $movimento, int $stabil protected function buildFatturaCandidates(MovimentoBanca $movimento, int $stabileId): array { if (Schema::hasTable('contabilita_fatture_fornitori')) { - $importo = abs((float) $movimento->importo); - $descrizione = (string) ($movimento->descrizione_estesa_pulita ?? $movimento->descrizione_estesa ?? $movimento->descrizione ?? ''); + $importo = abs((float) $movimento->importo); + $descrizione = (string) ($movimento->descrizione_estesa_pulita ?? $movimento->descrizione_estesa ?? $movimento->descrizione ?? ''); $movementHints = is_array($movimento->match_data ?? null) ? $movimento->match_data : []; return FatturaFornitore::query() @@ -2957,32 +3006,32 @@ protected function buildFatturaCandidates(MovimentoBanca $movimento, int $stabil $fornitoreLabel . ' ' . trim((string) ($fattura->numero_documento ?? '')) ); - $fatturaHints = $this->extractFatturaPaymentHints($fattura); + $fatturaHints = $this->extractFatturaPaymentHints($fattura); $movementProvider = strtoupper(trim((string) ($movementHints['payment_provider'] ?? $movementHints['beneficiario'] ?? ''))); - $fatturaProvider = strtoupper(trim((string) ($fatturaHints['provider'] ?? $fornitoreLabel))); + $fatturaProvider = strtoupper(trim((string) ($fatturaHints['provider'] ?? $fornitoreLabel))); if ($movementProvider !== '' && $fatturaProvider !== '' && str_contains($fatturaProvider, $movementProvider)) { - $score['totale'] = min(100, $score['totale'] + 18); + $score['totale'] = min(100, $score['totale'] + 18); $score['motivo'] .= ' · fornitore coerente con il movimento'; } if (($movementHints['cbill'] ?? null) && ($fatturaHints['cbill'] ?? null) && (string) $movementHints['cbill'] === (string) $fatturaHints['cbill']) { - $score['totale'] = min(100, $score['totale'] + 35); + $score['totale'] = min(100, $score['totale'] + 35); $score['motivo'] .= ' · CBILL coincidente'; } if (($movementHints['sia'] ?? null) && ($fatturaHints['sia'] ?? null) && strtoupper((string) $movementHints['sia']) === strtoupper((string) $fatturaHints['sia'])) { - $score['totale'] = min(100, $score['totale'] + 15); + $score['totale'] = min(100, $score['totale'] + 15); $score['motivo'] .= ' · SIA coerente'; } if (($movementHints['suggested_voce_spesa_code'] ?? null) === 'ACQ' && ($fatturaHints['is_water'] ?? false)) { - $score['totale'] = min(100, $score['totale'] + 20); + $score['totale'] = min(100, $score['totale'] + 20); $score['motivo'] .= ' · spesa acqua/ACQ'; } if ((float) ($fattura->ritenuta_importo ?? 0) > 0) { - $score['totale'] = min(100, $score['totale'] + 5); + $score['totale'] = min(100, $score['totale'] + 5); $score['motivo'] .= ' · netto coerente con RA'; } @@ -3001,15 +3050,15 @@ protected function buildFatturaCandidates(MovimentoBanca $movimento, int $stabil } return [ - 'tipo' => 'Fattura fornitore', - 'titolo' => $fornitoreLabel . ' · n. ' . trim((string) ($fattura->numero_documento ?: $fattura->id)), - 'score' => $score['totale'], - 'importo' => $targetImporto, - 'data' => $fattura->data_documento?->format('d/m/Y') ?? '—', - 'dettaglio' => implode(' · ', $detailBits), - 'motivo' => $score['motivo'], - 'azione' => 'riconcilia_pagamento_fornitore', - 'fattura_fornitore_id'=> (int) $fattura->id, + 'tipo' => 'Fattura fornitore', + 'titolo' => $fornitoreLabel . ' · n. ' . trim((string) ($fattura->numero_documento ?: $fattura->id)), + 'score' => $score['totale'], + 'importo' => $targetImporto, + 'data' => $fattura->data_documento?->format('d/m/Y') ?? '—', + 'dettaglio' => implode(' · ', $detailBits), + 'motivo' => $score['motivo'], + 'azione' => 'riconcilia_pagamento_fornitore', + 'fattura_fornitore_id' => (int) $fattura->id, ]; }) ->filter() @@ -3081,15 +3130,15 @@ protected function getOperationalRiconciliazioneLabel(MovimentoBanca $record): ? } $matchData = is_array($record->match_data ?? null) ? $record->match_data : []; - $label = trim((string) ($matchData['riconciliazione_label'] ?? '')); + $label = trim((string) ($matchData['riconciliazione_label'] ?? '')); if ($label !== '') { return $label; } return match ((string) ($matchData['riconciliazione_tipo'] ?? '')) { - 'affitto_canone' => 'Canone affitto chiuso', + 'affitto_canone' => 'Canone affitto chiuso', 'pagamento_fattura_fornitore' => 'Pagamento fattura fornitore', - default => 'Riconciliato operativamente', + default => 'Riconciliato operativamente', }; } @@ -3129,12 +3178,16 @@ protected function extractFatturaPaymentHints(FatturaFornitore $fattura): array $hints['sia'] = trim((string) $payment['sia']); } + if (empty($hints['sia']) && is_string($fattura->fornitore?->sia_effettivo ?? null) && trim((string) $fattura->fornitore->sia_effettivo) !== '') { + $hints['sia'] = trim((string) $fattura->fornitore->sia_effettivo); + } + if (is_string($payload['voce_spesa_code'] ?? null) && trim((string) $payload['voce_spesa_code']) !== '') { $hints['voce_spesa_code'] = strtoupper(trim((string) $payload['voce_spesa_code'])); } - $type = strtoupper(trim((string) ($payload['type'] ?? $payload['tipo'] ?? ''))); - $fe = $fattura->fatturaElettronica; + $type = strtoupper(trim((string) ($payload['type'] ?? $payload['tipo'] ?? ''))); + $fe = $fattura->fatturaElettronica; $isWater = $type === 'ACQUA' || $type === 'WATER' || (($hints['voce_spesa_code'] ?? null) === 'ACQ') @@ -3386,12 +3439,12 @@ public function getGestioniContabiliRilevate(): array $folder = ArchivioPaths::gestioneFolderName($gestione, (int) $gestione->anno_gestione, (string) $gestione->tipo_gestione); return [ - 'id' => (int) $gestione->id, - 'year' => (int) ($gestione->anno_gestione ?? 0), - 'tipo' => (string) ($gestione->tipo_gestione ?? ''), + 'id' => (int) $gestione->id, + 'year' => (int) ($gestione->anno_gestione ?? 0), + 'tipo' => (string) ($gestione->tipo_gestione ?? ''), 'folder' => $folder, - 'label' => trim(($gestione->protocollo_prefix ? $gestione->protocollo_prefix . ' · ' : '') . ($gestione->denominazione ?: $folder)), - 'stato' => (string) ($gestione->stato ?? ''), + 'label' => trim(($gestione->protocollo_prefix ? $gestione->protocollo_prefix . ' · ' : '') . ($gestione->denominazione ?: $folder)), + 'stato' => (string) ($gestione->stato ?? ''), 'active' => (bool) ($gestione->gestione_attiva ?? false), ]; }) @@ -3403,13 +3456,13 @@ public function getGestioniContabiliRilevate(): array public function getArchivioOperativoSummary(): array { $stabile = $this->getActiveStabile(); - $conto = $this->contoId + $conto = $this->contoId ? DatiBancari::query()->where('stabile_id', $this->getActiveStabileId())->whereKey($this->contoId)->first() : null; - $referenceYear = $this->resolveArchivioReferenceYear(); + $referenceYear = $this->resolveArchivioReferenceYear(); $activeGestione = null; - $stabileId = $this->getActiveStabileId(); + $stabileId = $this->getActiveStabileId(); if ($stabileId) { $defaultGestioneId = $this->resolveDefaultGestioneId($stabileId); if ($defaultGestioneId && is_numeric($defaultGestioneId)) { @@ -3425,15 +3478,15 @@ public function getArchivioOperativoSummary(): array : null; return [ - 'reference_year' => $referenceYear, - 'gestione_folder' => ArchivioPaths::gestioneFolderName($activeGestione, $referenceYear, 'ordinaria'), - 'gestione_label' => $activeGestione + 'reference_year' => $referenceYear, + 'gestione_folder' => ArchivioPaths::gestioneFolderName($activeGestione, $referenceYear, 'ordinaria'), + 'gestione_label' => $activeGestione ? trim(((string) ($activeGestione->protocollo_prefix ?? '') !== '' ? $activeGestione->protocollo_prefix . ' · ' : '') . (string) ($activeGestione->denominazione ?? 'Gestione')) : 'Gestione da associare', - 'banca_folder' => $bancaFolder, + 'banca_folder' => $bancaFolder, 'fatture_xml_folder' => 'fatture_xml/' . $referenceYear, 'fatture_pdf_folder' => 'fatture/' . ArchivioPaths::gestioneFolderName($activeGestione, $referenceYear, 'ordinaria'), - 'base_folder' => $stabile ? ArchivioPaths::stabileBase($stabile, $stabile->amministratore) : null, + 'base_folder' => $stabile ? ArchivioPaths::stabileBase($stabile, $stabile->amministratore) : null, ]; } @@ -3442,19 +3495,19 @@ public function getRiconciliazioneBucketRows(): array { return [ [ - 'title' => 'Costi / Entrate', + 'title' => 'Costi / Entrate', 'detail' => 'Lettura economica standard dei movimenti: incassi, spese, pagamenti, bonifici e competenze correnti.', ], [ - 'title' => 'Crediti / Debiti', + 'title' => 'Crediti / Debiti', 'detail' => 'Base già pronta con crediti verso condòmini e debiti verso fornitori, utile per incassi rate, FE passive e note di rettifica.', ], [ - 'title' => 'Fondi / Accantonamenti', + 'title' => 'Fondi / Accantonamenti', 'detail' => 'Predisposti bucket separati per fondo riserva e accantonamenti, così la riconciliazione non confonde disponibilità corrente e somme vincolate.', ], [ - 'title' => 'Rimborsi', + 'title' => 'Rimborsi', 'detail' => 'Separazione tra rimborsi da distribuire e rimborsi da utilizzare, per tenere distinta la quadratura patrimoniale dalla spesa/ricavo puro.', ], ]; @@ -3519,7 +3572,7 @@ protected function archiveImportedBankSourceFile(DatiBancari $conto, string $tem return null; } - $year = (int) now()->format('Y'); + $year = (int) now()->format('Y'); $gestione = null; if ($gestioneId && DB::getSchemaBuilder()->hasTable('gestioni_contabili')) { $gestione = GestioneContabile::query()->find($gestioneId); @@ -3538,15 +3591,15 @@ protected function archiveImportedBankSourceFile(DatiBancari $conto, string $tem : ''; $originalName = basename($sourcePath); - $safeName = preg_replace('/[^A-Za-z0-9._-]+/', '-', $originalName) ?? $originalName; - $targetPath = $targetDir . '/import/' . now()->format('Ymd-His') . '-' . $formatPrefix . $safeName; + $safeName = preg_replace('/[^A-Za-z0-9._-]+/', '-', $originalName) ?? $originalName; + $targetPath = $targetDir . '/import/' . now()->format('Ymd-His') . '-' . $formatPrefix . $safeName; Storage::disk('local')->makeDirectory($targetDir . '/import'); Storage::disk('local')->put($targetPath, Storage::disk('local')->get($sourcePath)); $matchData = [ 'archived_bank_source_path' => $targetPath, - 'gestione_folder' => ArchivioPaths::gestioneFolderName($gestione, $year, 'ordinaria'), + 'gestione_folder' => ArchivioPaths::gestioneFolderName($gestione, $year, 'ordinaria'), ]; return $targetPath; diff --git a/app/Filament/Pages/Contabilita/CasseBancheRiepilogo.php b/app/Filament/Pages/Contabilita/CasseBancheRiepilogo.php index 4f39f13..5d0add1 100644 --- a/app/Filament/Pages/Contabilita/CasseBancheRiepilogo.php +++ b/app/Filament/Pages/Contabilita/CasseBancheRiepilogo.php @@ -1,5 +1,4 @@ label('Conto (IBAN)') ->native(false) ->searchable() - ->options(fn(): array => $this->getIbanOptions()) + ->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()) + ->options(fn(): array=> $this->getGestioniOptions()) ->default(fn(): ?string => $this->resolveDefaultGestioneId()), FileUpload::make('file') @@ -141,9 +140,9 @@ protected function getHeaderActions(): array } try { - $content = Storage::disk('local')->get($path); + $content = Storage::disk('local')->get($path); $importer = new MovimentiBancaImporter(new UnicreditWriParser(), new IntesaCsvParser(), new ExcelMovimentiParser()); - $res = $importer->importUnicreditWri( + $res = $importer->importUnicreditWri( $content, $stabileId, basename($path), @@ -177,7 +176,7 @@ protected function getHeaderActions(): array ->label('Conto (IBAN)') ->native(false) ->searchable() - ->options(fn(): array => $this->getIbanOptions()) + ->options(fn(): array=> $this->getIbanOptions()) ->required(), Textarea::make('note') @@ -246,7 +245,7 @@ protected function getHeaderActions(): array $created = 0; DB::transaction(function () use ($rows, $stabileId, $contoId, $iban, $note, &$created, $user): void { foreach ($rows as $row) { - $date = $row['data_saldo'] ?? null; + $date = $row['data_saldo'] ?? null; $saldo = $row['saldo'] ?? null; if (! $date || ! is_numeric($saldo)) { @@ -255,11 +254,11 @@ protected function getHeaderActions(): array SaldoConto::query()->create([ 'stabile_id' => (int) $stabileId, - 'conto_id' => $contoId, - 'iban' => $iban, + 'conto_id' => $contoId, + 'iban' => $iban, 'data_saldo' => $date, - 'saldo' => (float) $saldo, - 'note' => $note !== '' ? $note : null, + 'saldo' => (float) $saldo, + 'note' => $note !== '' ? $note : null, 'created_by' => (int) ($user->id ?? 0) ?: null, 'updated_by' => (int) ($user->id ?? 0) ?: null, ]); @@ -314,7 +313,7 @@ public function table(Table $table): Table Select::make('gestione_ids') ->label('Gestione') ->multiple() - ->options(fn(): array => $this->getGestioniOptions()) + ->options(fn(): array=> $this->getGestioniOptions()) ->searchable(), ]), ]) @@ -384,10 +383,10 @@ public function getTotaleDisponibilita(): ?float } $periodo = $this->getTableFilterState('periodo'); - $from = is_array($periodo) ? ($periodo['from'] ?? null) : null; - $to = is_array($periodo) ? ($periodo['to'] ?? null) : null; + $from = is_array($periodo) ? ($periodo['from'] ?? null) : null; + $to = is_array($periodo) ? ($periodo['to'] ?? null) : null; - $gestioni = $this->getTableFilterState('gestioni'); + $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)) : []; @@ -425,10 +424,10 @@ protected function resolveSaldoConto(DatiBancari $record): ?float $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; + $from = is_array($periodo) ? ($periodo['from'] ?? null) : null; + $to = is_array($periodo) ? ($periodo['to'] ?? null) : null; - $gestioni = $this->getTableFilterState('gestioni'); + $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)) : []; @@ -451,7 +450,7 @@ protected function resolveSaldoConto(DatiBancari $record): ?float // Base da saldi intermedi: se c'è un saldo <= from (se filtrato) o altrimenti ultimo saldo disponibile. $baseSaldo = null; - $baseDate = null; + $baseDate = null; if (DB::getSchemaBuilder()->hasTable('contabilita_saldi_conti')) { $qSaldo = SaldoConto::query() ->where('stabile_id', $stabileId) @@ -464,7 +463,7 @@ protected function resolveSaldoConto(DatiBancari $record): ?float $row = $qSaldo->orderByDesc('data_saldo')->orderByDesc('id')->first(['data_saldo', 'saldo']); if ($row) { $baseSaldo = (float) $row->saldo; - $baseDate = $row->data_saldo->toDateString(); + $baseDate = $row->data_saldo->toDateString(); } } @@ -539,13 +538,13 @@ protected function resolveLatestSaldoSnapshotLabel(DatiBancari $record): string return 'Nessuno estratto registrato'; } - $parts = []; + $parts = []; $parts[] = $snapshot->tipo_estratto ? ($this->getTipoEstrattoOptions()[$snapshot->tipo_estratto] ?? 'Saldo registrato') : 'Saldo registrato'; if ($snapshot->periodo_da || $snapshot->periodo_a) { - $from = $snapshot->periodo_da?->format('d/m/Y'); - $to = $snapshot->periodo_a?->format('d/m/Y'); + $from = $snapshot->periodo_da?->format('d/m/Y'); + $to = $snapshot->periodo_a?->format('d/m/Y'); $parts[] = trim(($from ?: '...') . ' - ' . ($to ?: '...')); } if ($snapshot->documento instanceof DocumentoStabile) { @@ -558,10 +557,10 @@ protected function resolveLatestSaldoSnapshotLabel(DatiBancari $record): string protected function getTipoEstrattoOptions(): array { return [ - 'estratto_conto' => 'Estratto conto', - 'saldo_banca' => 'Saldo banca', + 'estratto_conto' => 'Estratto conto', + 'saldo_banca' => 'Saldo banca', 'riconciliazione' => 'Riconciliazione', - 'altro' => 'Altro', + 'altro' => 'Altro', ]; } @@ -585,12 +584,12 @@ protected function getContiOptions(): array ->orderBy('id') ->get(['id', 'denominazione_banca', 'iban', 'legacy_cod_cassa', 'numero_conto']) ->mapWithKeys(function (DatiBancari $conto): array { - $parts = []; + $parts = []; $parts[] = trim((string) ($conto->denominazione_banca ?: 'Conto')); $legacyCodCassa = is_string($conto->legacy_cod_cassa) ? trim((string) $conto->legacy_cod_cassa) : ''; - $numeroConto = is_string($conto->numero_conto) ? trim((string) $conto->numero_conto) : ''; - $iban = is_string($conto->iban) ? trim((string) $conto->iban) : ''; + $numeroConto = is_string($conto->numero_conto) ? trim((string) $conto->numero_conto) : ''; + $iban = is_string($conto->iban) ? trim((string) $conto->iban) : ''; if ($legacyCodCassa !== '') { $parts[] = 'cassa ' . strtoupper($legacyCodCassa); diff --git a/app/Filament/Pages/Contabilita/FatturaFornitoreScheda.php b/app/Filament/Pages/Contabilita/FatturaFornitoreScheda.php index 198e8bb..e688e08 100644 --- a/app/Filament/Pages/Contabilita/FatturaFornitoreScheda.php +++ b/app/Filament/Pages/Contabilita/FatturaFornitoreScheda.php @@ -748,35 +748,18 @@ public function getGestioniStraordinarieList(): array return []; } + $stabile = Stabile::query()->find($stabileId); + return GestioneContabile::query() ->where('stabile_id', $stabileId) ->where('tipo_gestione', 'straordinaria') + ->where('stato', 'aperta') ->orderByDesc('anno_gestione') ->orderByDesc('numero_straordinaria') ->orderByDesc('id') ->get(['id', 'anno_gestione', 'tipo_gestione', 'denominazione', 'protocollo_prefix', 'numero_straordinaria', 'stato']) - ->map(function (GestioneContabile $g): array { - $label = trim((string) ($g->denominazione ?? '')); - if ($label === '') { - $label = (string) $g->anno_gestione . ' - ' . (string) $g->tipo_gestione; - } - - if (is_numeric($g->numero_straordinaria) && (int) $g->numero_straordinaria > 0) { - $label .= ' • STR ' . (int) $g->numero_straordinaria; - } - - $prefix = trim((string) ($g->protocollo_prefix ?? '')); - if ($prefix !== '') { - $label = $prefix . ' — ' . $label; - } - - $stato = trim((string) ($g->stato ?? '')); - if ($stato !== '') { - $label .= ' • ' . strtoupper($stato); - } - - return ['id' => (int) $g->id, 'label' => $label]; - }) + ->filter(fn(GestioneContabile $g): bool => $this->isGestioneVisibleForStabile($g, $stabile)) + ->map(fn(GestioneContabile $g): array=> ['id' => (int) $g->id, 'label' => $this->formatGestioneLabel($g, $stabile)]) ->all(); } @@ -1104,47 +1087,10 @@ public function header(Schema $schema): Schema ->schema([ Select::make('gestione_id') ->label('Gestione') - ->options(function (): array { - $user = Auth::user(); - if (! $user instanceof User) { - return []; - } - - $activeStabileId = StabileContext::resolveActiveStabileId($user); - $stabileId = (int) ($this->record?->stabile_id ?: $activeStabileId); - if (! $stabileId) { - return []; - } - - return GestioneContabile::query() - ->where('stabile_id', (int) $stabileId) - ->orderByDesc('anno_gestione') - ->orderByDesc('numero_straordinaria') - ->orderByDesc('id') - ->get(['id', 'anno_gestione', 'tipo_gestione', 'denominazione', 'protocollo_prefix', 'stato', 'numero_straordinaria']) - ->mapWithKeys(function (GestioneContabile $g) { - $label = trim((string) ($g->denominazione ?? '')); - if ($label === '') { - $label = (string) $g->anno_gestione . ' - ' . (string) $g->tipo_gestione; - } - - if ((string) ($g->tipo_gestione ?? '') === 'straordinaria' && is_numeric($g->numero_straordinaria) && (int) $g->numero_straordinaria > 0) { - $label .= ' (N.' . (int) $g->numero_straordinaria . ')'; - } - $prefix = trim((string) ($g->protocollo_prefix ?? '')); - if ($prefix !== '') { - $label = $prefix . ' — ' . $label; - } - if (is_string($g->stato) && $g->stato !== '') { - $label .= ' [' . $g->stato . ']'; - } - return [(string) $g->id => $label]; - }) - ->all(); - }) + ->options(fn(): array=> $this->getVisibleGestioneOptions()) ->searchable() ->required() - ->helperText('Ordinaria / riscaldamento / straordinaria (per anno).'), + ->helperText('Solo gestioni aperte e coerenti con lo stabile attivo.'), Select::make('causale_contabile') ->label('Causale contabile') @@ -2553,6 +2499,11 @@ private function mapFeRitenutaToTributo(?string $rtCode, ?string $causale): stri private function resolveRiscaldamentoGestioneByDate(int $stabileId, string $date): int { + $stabile = Stabile::query()->find($stabileId); + if ($stabile instanceof Stabile && ! $stabile->hasOperationalHeating()) { + return 0; + } + try { $dt = Carbon::parse($date); } catch (\Throwable) { @@ -2561,7 +2512,8 @@ private function resolveRiscaldamentoGestioneByDate(int $stabileId, string $date $q = GestioneContabile::query() ->where('stabile_id', $stabileId) - ->where('tipo_gestione', 'riscaldamento'); + ->where('tipo_gestione', 'riscaldamento') + ->where('stato', 'aperta'); $match = (clone $q) ->whereNotNull('data_inizio') @@ -2595,6 +2547,8 @@ private function resolveDefaultGestioneId(int $stabileId, ?int $year = null): in return 0; } + $stabile = Stabile::query()->find($stabileId); + $baseQuery = GestioneContabile::query() ->where('stabile_id', $stabileId) ->where('stato', 'aperta'); @@ -2603,6 +2557,16 @@ private function resolveDefaultGestioneId(int $stabileId, ?int $year = null): in $baseQuery->where('anno_gestione', $year); } + if ($stabile instanceof Stabile) { + if (! $stabile->hasOperationalHeating()) { + $baseQuery->where('tipo_gestione', '!=', 'riscaldamento'); + } + + if ($stabile->getTakeoverDate()) { + $baseQuery->where('anno_gestione', '>=', (int) $stabile->getTakeoverDate()->year); + } + } + $ordinariaId = (clone $baseQuery) ->where('tipo_gestione', 'ordinaria') ->orderByDesc('anno_gestione') @@ -2621,6 +2585,80 @@ private function resolveDefaultGestioneId(int $stabileId, ?int $year = null): in return is_numeric($fallbackId) ? (int) $fallbackId : 0; } + /** @return array */ + private function getVisibleGestioneOptions(): array + { + $user = Auth::user(); + if (! $user instanceof User) { + return []; + } + + $activeStabileId = StabileContext::resolveActiveStabileId($user); + $stabileId = (int) ($this->record?->stabile_id ?: $activeStabileId); + if ($stabileId <= 0) { + return []; + } + + $stabile = Stabile::query()->find($stabileId); + + return GestioneContabile::query() + ->where('stabile_id', $stabileId) + ->where('stato', 'aperta') + ->orderByDesc('anno_gestione') + ->orderByDesc('numero_straordinaria') + ->orderByDesc('id') + ->get(['id', 'anno_gestione', 'tipo_gestione', 'denominazione', 'protocollo_prefix', 'stato', 'numero_straordinaria']) + ->filter(fn(GestioneContabile $g): bool => $this->isGestioneVisibleForStabile($g, $stabile)) + ->mapWithKeys(fn(GestioneContabile $g) => [(string) $g->id => $this->formatGestioneLabel($g, $stabile)]) + ->all(); + } + + private function isGestioneVisibleForStabile(GestioneContabile $gestione, ?Stabile $stabile): bool + { + if (trim((string) ($gestione->stato ?? '')) !== 'aperta') { + return false; + } + + if ($stabile instanceof Stabile) { + if ((string) ($gestione->tipo_gestione ?? '') === 'riscaldamento' && ! $stabile->hasOperationalHeating()) { + return false; + } + + $year = is_numeric($gestione->anno_gestione) ? (int) $gestione->anno_gestione : null; + if (! $stabile->isOperationalYearVisible($year)) { + return false; + } + } + + return true; + } + + private function formatGestioneLabel(GestioneContabile $gestione, ?Stabile $stabile): string + { + $label = trim((string) ($gestione->denominazione ?? '')); + if ($label === '') { + $label = (string) $gestione->anno_gestione . ' - ' . (string) $gestione->tipo_gestione; + } + + if ((string) ($gestione->tipo_gestione ?? '') === 'straordinaria' && is_numeric($gestione->numero_straordinaria) && (int) $gestione->numero_straordinaria > 0) { + $label .= ' (N.' . (int) $gestione->numero_straordinaria . ')'; + } + + $prefix = trim((string) ($gestione->protocollo_prefix ?? '')); + if ($prefix !== '') { + $label = $prefix . ' — ' . $label; + } + + if ($stabile instanceof Stabile && $stabile->getTakeoverDate()) { + $year = is_numeric($gestione->anno_gestione) ? (int) $gestione->anno_gestione : null; + if (is_int($year) && $year < (int) $stabile->getTakeoverDate()->year) { + $label .= ' • pre-presa in carico'; + } + } + + return $label; + } + public function getLinkedFatturaElettronica(): ?FatturaElettronica { if ($this->linkedFeCache !== null) { diff --git a/app/Filament/Pages/Contabilita/SaldiContiArchivio.php b/app/Filament/Pages/Contabilita/SaldiContiArchivio.php index ff6b630..55c13e1 100644 --- a/app/Filament/Pages/Contabilita/SaldiContiArchivio.php +++ b/app/Filament/Pages/Contabilita/SaldiContiArchivio.php @@ -29,7 +29,7 @@ class SaldiContiArchivio extends Page implements HasTable { use InteractsWithTable; - #[Url(as: 'conto_id')] + #[Url( as : 'conto_id')] public ?int $contoId = null; protected static ?string $navigationLabel = 'Saldi conti'; diff --git a/app/Filament/Pages/Contabilita/SituazioneIniziale.php b/app/Filament/Pages/Contabilita/SituazioneIniziale.php index a66e6d5..3753989 100644 --- a/app/Filament/Pages/Contabilita/SituazioneIniziale.php +++ b/app/Filament/Pages/Contabilita/SituazioneIniziale.php @@ -1,11 +1,9 @@ null, - 'des_voce' => null, - 'descrizione' => null, + 'cod_voc' => null, + 'des_voce' => null, + 'descrizione' => null, 'importo_euro' => null, - 'n_stra' => null, - 'incluso' => 1, + 'n_stra' => null, + 'incluso' => 1, ]; public array $dettTabRows = []; @@ -96,28 +94,28 @@ class SituazioneIniziale extends Page public array $dettTabEdit = []; public array $formDettTabAdd = [ - 'cod_tab' => 'CONG.O', - 'id_cond' => null, + 'cod_tab' => 'CONG.O', + 'id_cond' => null, 'cond_inquil' => null, - 'mm' => null, - 'cons_euro' => null, - 'n_stra' => null, - 'unico' => 0, + 'mm' => null, + 'cons_euro' => null, + 'n_stra' => null, + 'unico' => 0, ]; public array $formVoce = [ - 'gestione' => 'ordinaria', - 'categoria' => 'crediti', - 'n_stra' => null, - 'codice' => null, - 'descrizione' => null, + 'gestione' => 'ordinaria', + 'categoria' => 'crediti', + 'n_stra' => null, + 'codice' => null, + 'descrizione' => null, 'importo_euro' => null, ]; public array $formConguaglio = [ - 'cod_tab' => 'CONG.O', - 'n_stra' => null, - 'id_cond' => null, + 'cod_tab' => 'CONG.O', + 'n_stra' => null, + 'id_cond' => null, 'cons_euro' => null, ]; @@ -213,10 +211,10 @@ public function updatedTab(): void } // Reset stati per evitare UI "incastrata" cambiando tab - $this->editingCreDebId = null; + $this->editingCreDebId = null; $this->editingDettTabId = null; $this->selectedCreDebId = null; - $this->detailTab = 'voce'; + $this->detailTab = 'voce'; $this->reloadData(); } @@ -225,7 +223,7 @@ public function editCreDebRow(int $id): void if ($id <= 0) { return; } - $this->editingCreDebId = $id; + $this->editingCreDebId = $id; $this->selectedCreDebId = $id; } @@ -248,7 +246,7 @@ public function selectCreDebRow(int $id): void public function setDetailTab(string $tab): void { - $tab = strtolower(trim($tab)); + $tab = strtolower(trim($tab)); $this->detailTab = in_array($tab, ['voce', 'fe', 'ra'], true) ? $tab : 'voce'; } @@ -339,7 +337,7 @@ private function loadLocalCondominiLabels(): array } $referenceDate = $this->resolveReferenceDate(); - $units = UnitaImmobiliare::query() + $units = UnitaImmobiliare::query() ->where('stabile_id', (int) $stabileId) ->whereNotNull('legacy_cond_id') ->get(['id', 'legacy_cond_id', 'scala', 'interno', 'denominazione']); @@ -348,7 +346,7 @@ private function loadLocalCondominiLabels(): array return []; } - $unitIds = $units->pluck('id')->map(fn ($id) => (int) $id)->all(); + $unitIds = $units->pluck('id')->map(fn($id) => (int) $id)->all(); $namesByUnit = []; if (Schema::hasTable('persone_unita_relazioni')) { @@ -366,7 +364,7 @@ private function loadLocalCondominiLabels(): array foreach ($relations as $relation) { $unitId = (int) $relation->unita_id; - $role = strtoupper((string) ($relation->ruolo_rate ?: PersonaUnitaRelazione::deriveRuoloRate($relation->tipo_relazione))); + $role = strtoupper((string) ($relation->ruolo_rate ?: PersonaUnitaRelazione::deriveRuoloRate($relation->tipo_relazione))); if (! in_array($role, ['C', 'I'], true)) { continue; } @@ -394,7 +392,7 @@ private function loadLocalCondominiLabels(): array foreach ($legacyNames as $row) { $unitId = (int) ($row->unita_immobiliare_id ?? 0); - $role = strtoupper((string) ($row->ruolo ?? '')); + $role = strtoupper((string) ($row->ruolo ?? '')); if (! in_array($role, ['C', 'I'], true)) { continue; } @@ -415,9 +413,9 @@ private function loadLocalCondominiLabels(): array continue; } - $pieces = []; - $scala = trim((string) ($unit->scala ?? '')); - $interno = trim((string) ($unit->interno ?? '')); + $pieces = []; + $scala = trim((string) ($unit->scala ?? '')); + $interno = trim((string) ($unit->interno ?? '')); $denominazione = trim((string) ($unit->denominazione ?? '')); if ($scala !== '' || $interno !== '') { @@ -429,7 +427,7 @@ private function loadLocalCondominiLabels(): array $pieces[] = $denominazione; } - $owners = array_values($namesByUnit[(int) $unit->id]['C'] ?? []); + $owners = array_values($namesByUnit[(int) $unit->id]['C'] ?? []); $tenants = array_values($namesByUnit[(int) $unit->id]['I'] ?? []); if ($owners !== []) { @@ -504,10 +502,10 @@ private function getActiveStabile(): ?Stabile private function reloadData(): void { - $stabile = $this->getActiveStabile(); + $stabile = $this->getActiveStabile(); $codStabileLegacy = $stabile?->codice_stabile ? (string) $stabile->codice_stabile : null; - $this->legacyYears = $this->loadLegacyYears($codStabileLegacy); + $this->legacyYears = $this->loadLegacyYears($codStabileLegacy); $this->legacyYearLabels = $codStabileLegacy ? $this->loadLegacyYearLabels($codStabileLegacy) : []; // Default/fallback legacyYear: @@ -524,7 +522,7 @@ private function reloadData(): void $this->legacyYear = ! empty($this->legacyYears) ? $this->legacyYears[0] : null; } - $stabileId = $stabile?->id ? (int) $stabile->id : null; + $stabileId = $stabile?->id ? (int) $stabile->id : null; $this->conti = $stabileId ? $this->loadContiOptions($stabileId) : []; if (($this->saldoContoId === null || $this->saldoContoId === '') && $this->conti !== []) { @@ -539,34 +537,34 @@ private function reloadData(): void } if (! $codStabileLegacy || ! $stabileId) { - $this->bilancio = $this->emptyBilancio(); + $this->bilancio = $this->emptyBilancio(); $this->risorseFinanziarie = []; - $this->conguagliExcel = []; + $this->conguagliExcel = []; $this->conguagliExcelCols = []; - $this->detailMeta = []; - $this->detailRows = []; - $this->saldoSnapshots = []; + $this->detailMeta = []; + $this->detailRows = []; + $this->saldoSnapshots = []; return; } $this->risorseFinanziarie = $this->computeRisorseFinanziarie($stabileId, $this->dataBilancio); - $this->bilancio = $this->computeBilancio($stabileId, $codStabileLegacy, $this->legacyYear, $this->dataBilancio, $this->saldoBancaManuale, $this->risorseFinanziarie); + $this->bilancio = $this->computeBilancio($stabileId, $codStabileLegacy, $this->legacyYear, $this->dataBilancio, $this->saldoBancaManuale, $this->risorseFinanziarie); $this->vocSpeOptions = $this->loadVocSpeOptions($codStabileLegacy, $this->legacyYear); if ($this->tab === 'conguagli') { - $this->conguagliExcel = []; - $this->conguagliExcelCols = []; + $this->conguagliExcel = []; + $this->conguagliExcelCols = []; [$this->dettTabRows, $this->dettTabEdit] = $this->loadDettTabCrudRows($codStabileLegacy, $this->legacyYear); } else { - $this->conguagliExcel = []; + $this->conguagliExcel = []; $this->conguagliExcelCols = []; - $this->dettTabRows = []; - $this->dettTabEdit = []; + $this->dettTabRows = []; + $this->dettTabEdit = []; } if (in_array($this->tab, ['crediti', 'debiti'], true)) { - $cd = $this->tab === 'crediti' ? 'C' : 'D'; + $cd = $this->tab === 'crediti' ? 'C' : 'D'; [$this->creDebRows, $this->creDebEdit] = $this->loadCreDebPrecedCrudRows($codStabileLegacy, $this->legacyYear, $cd); // Liste documentali per sub-tab (FE/RA) @@ -575,9 +573,9 @@ private function reloadData(): void $this->creDebRows = []; $this->creDebEdit = []; - $this->feRows = []; + $this->feRows = []; $this->linkedFeIds = []; - $this->raRows = []; + $this->raRows = []; $this->linkedRaIds = []; } @@ -587,7 +585,7 @@ private function reloadData(): void public function saveSaldoSnapshot(): void { - $stabile = $this->getActiveStabile(); + $stabile = $this->getActiveStabile(); $stabileId = $stabile?->id ? (int) $stabile->id : null; if (! $stabileId) { $this->addError('saldoContoId', 'Seleziona prima uno stabile attivo.'); @@ -595,18 +593,18 @@ public function saveSaldoSnapshot(): void } $validated = $this->validate([ - 'saldoContoId' => ['required', 'string'], - 'saldoSnapshotData' => ['required', 'date'], + 'saldoContoId' => ['required', 'string'], + 'saldoSnapshotData' => ['required', 'date'], 'saldoSnapshotPeriodoDa' => ['nullable', 'date'], - 'saldoSnapshotPeriodoA' => ['nullable', 'date'], - 'saldoSnapshotSaldo' => ['required', 'string'], - 'saldoSnapshotTipo' => ['required', 'string', 'in:estratto_conto,saldo_banca,riconciliazione,altro'], - 'saldoSnapshotNote' => ['nullable', 'string', 'max:255'], - 'saldoSnapshotPdf' => ['nullable', 'file', 'mimes:pdf', 'max:20480'], + 'saldoSnapshotPeriodoA' => ['nullable', 'date'], + 'saldoSnapshotSaldo' => ['required', 'string'], + 'saldoSnapshotTipo' => ['required', 'string', 'in:estratto_conto,saldo_banca,riconciliazione,altro'], + 'saldoSnapshotNote' => ['nullable', 'string', 'max:255'], + 'saldoSnapshotPdf' => ['nullable', 'file', 'mimes:pdf', 'max:20480'], ]); $contoId = (int) ($validated['saldoContoId'] ?? 0); - $conto = DatiBancari::query() + $conto = DatiBancari::query() ->where('stabile_id', $stabileId) ->where('id', $contoId) ->first(['id', 'denominazione_banca', 'iban', 'numero_conto', 'legacy_cod_cassa']); @@ -622,25 +620,25 @@ public function saveSaldoSnapshot(): void return; } - $user = Auth::user(); + $user = Auth::user(); $documento = $this->storeSaldoSnapshotDocumento($stabileId, $conto, $this->saldoSnapshotPdf, $validated); SaldoConto::query()->updateOrCreate( [ 'stabile_id' => $stabileId, - 'conto_id' => $contoId, + 'conto_id' => $contoId, 'data_saldo' => $validated['saldoSnapshotData'], ], [ - 'iban' => $this->normalizeIbanValue($conto->iban), - 'periodo_da' => $validated['saldoSnapshotPeriodoDa'] ?? null, - 'periodo_a' => $validated['saldoSnapshotPeriodoA'] ?? null, - 'saldo' => $saldo, - 'tipo_estratto' => $validated['saldoSnapshotTipo'], - 'note' => isset($validated['saldoSnapshotNote']) ? trim((string) $validated['saldoSnapshotNote']) : null, + 'iban' => $this->normalizeIbanValue($conto->iban), + 'periodo_da' => $validated['saldoSnapshotPeriodoDa'] ?? null, + 'periodo_a' => $validated['saldoSnapshotPeriodoA'] ?? null, + 'saldo' => $saldo, + 'tipo_estratto' => $validated['saldoSnapshotTipo'], + 'note' => isset($validated['saldoSnapshotNote']) ? trim((string) $validated['saldoSnapshotNote']) : null, 'documento_stabile_id' => $documento?->id, - 'created_by' => (int) ($user?->id ?? 0) ?: null, - 'updated_by' => (int) ($user?->id ?? 0) ?: null, + 'created_by' => (int) ($user?->id ?? 0) ?: null, + 'updated_by' => (int) ($user?->id ?? 0) ?: null, ] ); @@ -654,7 +652,7 @@ public function deleteSaldoSnapshot(int $snapshotId): void return; } - $stabile = $this->getActiveStabile(); + $stabile = $this->getActiveStabile(); $stabileId = $stabile?->id ? (int) $stabile->id : null; if (! $stabileId) { return; @@ -683,9 +681,9 @@ private function loadSaldoSnapshots(): void { $this->saldoSnapshots = []; - $stabile = $this->getActiveStabile(); + $stabile = $this->getActiveStabile(); $stabileId = $stabile?->id ? (int) $stabile->id : null; - $contoId = is_numeric($this->saldoContoId) ? (int) $this->saldoContoId : 0; + $contoId = is_numeric($this->saldoContoId) ? (int) $this->saldoContoId : 0; if (! $stabileId || $contoId <= 0 || ! DB::getSchemaBuilder()->hasTable('contabilita_saldi_conti')) { return; } @@ -699,14 +697,14 @@ private function loadSaldoSnapshots(): void ->get() ->map(function (SaldoConto $row): array { return [ - 'id' => (int) $row->id, - 'data_saldo' => $row->data_saldo?->format('d/m/Y'), - 'periodo' => $this->formatPeriodoEstrattoLabel($row->periodo_da, $row->periodo_a), - 'tipo' => $this->formatTipoEstrattoLabel($row->tipo_estratto), - 'saldo' => (float) $row->saldo, - 'note' => (string) ($row->note ?? ''), + 'id' => (int) $row->id, + 'data_saldo' => $row->data_saldo?->format('d/m/Y'), + 'periodo' => $this->formatPeriodoEstrattoLabel($row->periodo_da, $row->periodo_a), + 'tipo' => $this->formatTipoEstrattoLabel($row->tipo_estratto), + 'saldo' => (float) $row->saldo, + 'note' => (string) ($row->note ?? ''), 'documento_nome' => $row->documento?->nome_originale, - 'documento_url' => $row->documento?->url_view, + 'documento_url' => $row->documento?->url_view, ]; }) ->all(); @@ -718,13 +716,13 @@ private function resetSaldoSnapshotForm(bool $resetConto = true): void $this->saldoContoId = $this->conti !== [] ? (string) array_key_first($this->conti) : null; } - $this->saldoSnapshotData = $this->dataBilancio; + $this->saldoSnapshotData = $this->dataBilancio; $this->saldoSnapshotPeriodoDa = null; - $this->saldoSnapshotPeriodoA = null; - $this->saldoSnapshotSaldo = null; - $this->saldoSnapshotTipo = 'estratto_conto'; - $this->saldoSnapshotNote = null; - $this->saldoSnapshotPdf = null; + $this->saldoSnapshotPeriodoA = null; + $this->saldoSnapshotSaldo = null; + $this->saldoSnapshotTipo = 'estratto_conto'; + $this->saldoSnapshotNote = null; + $this->saldoSnapshotPdf = null; $this->resetValidation([ 'saldoContoId', 'saldoSnapshotData', @@ -740,18 +738,18 @@ private function resetSaldoSnapshotForm(bool $resetConto = true): void private function formatTipoEstrattoLabel(?string $value): string { return match (trim((string) $value)) { - 'estratto_conto' => 'Estratto conto', - 'saldo_banca' => 'Saldo banca', + 'estratto_conto' => 'Estratto conto', + 'saldo_banca' => 'Saldo banca', 'riconciliazione' => 'Riconciliazione', - 'altro' => 'Altro', - default => 'Saldo registrato', + 'altro' => 'Altro', + default => 'Saldo registrato', }; } private function formatPeriodoEstrattoLabel(mixed $from, mixed $to): string { $fromDate = $from instanceof Carbon ? $from : ($from ? Carbon::parse((string) $from) : null); - $toDate = $to instanceof Carbon ? $to : ($to ? Carbon::parse((string) $to) : null); + $toDate = $to instanceof Carbon ? $to : ($to ? Carbon::parse((string) $to) : null); if ($fromDate && $toDate) { return $fromDate->format('d/m/Y') . ' - ' . $toDate->format('d/m/Y'); @@ -772,28 +770,28 @@ private function normalizeIbanValue(mixed $value): ?string return $iban !== '' ? $iban : null; } - private function storeSaldoSnapshotDocumento(int $stabileId, DatiBancari $conto, TemporaryUploadedFile|string|null $file, array $validated): ?DocumentoStabile + private function storeSaldoSnapshotDocumento(int $stabileId, DatiBancari $conto, TemporaryUploadedFile | string | null $file, array $validated): ?DocumentoStabile { if (! $file instanceof TemporaryUploadedFile) { return null; } $originalName = trim((string) $file->getClientOriginalName()); - $extension = strtolower((string) $file->getClientOriginalExtension()); - $extension = $extension !== '' ? $extension : 'pdf'; - $storedName = 'estratto-conto-' . $stabileId . '-' . (int) $conto->id . '-' . now()->format('YmdHis') . '.' . $extension; - $storedPath = $file->storeAs('documenti/stabili/' . $stabileId . '/bancari', $storedName, 'public'); + $extension = strtolower((string) $file->getClientOriginalExtension()); + $extension = $extension !== '' ? $extension : 'pdf'; + $storedName = 'estratto-conto-' . $stabileId . '-' . (int) $conto->id . '-' . now()->format('YmdHis') . '.' . $extension; + $storedPath = $file->storeAs('documenti/stabili/' . $stabileId . '/bancari', $storedName, 'public'); return DocumentoStabile::query()->create([ - 'stabile_id' => $stabileId, - 'nome_file' => $storedName, + 'stabile_id' => $stabileId, + 'nome_file' => $storedName, 'nome_originale' => $originalName !== '' ? $originalName : $storedName, - 'percorso_file' => $storedPath, - 'categoria' => 'bancari', - 'tipo_mime' => Storage::disk('public')->mimeType($storedPath), - 'dimensione' => Storage::disk('public')->size($storedPath), - 'descrizione' => 'Saldo/estratto ' . ($this->conti[(string) $conto->id] ?? ('Conto #' . $conto->id)) . ' · ' . $this->formatPeriodoEstrattoLabel($validated['saldoSnapshotPeriodoDa'] ?? null, $validated['saldoSnapshotPeriodoA'] ?? null), - 'caricato_da' => Auth::id(), + 'percorso_file' => $storedPath, + 'categoria' => 'bancari', + 'tipo_mime' => Storage::disk('public')->mimeType($storedPath), + 'dimensione' => Storage::disk('public')->size($storedPath), + 'descrizione' => 'Saldo/estratto ' . ($this->conti[(string) $conto->id] ?? ('Conto #' . $conto->id)) . ' · ' . $this->formatPeriodoEstrattoLabel($validated['saldoSnapshotPeriodoDa'] ?? null, $validated['saldoSnapshotPeriodoA'] ?? null), + 'caricato_da' => Auth::id(), ]); } @@ -811,13 +809,13 @@ private function loadLegacyYearLabels(string $codStabileLegacy): array ->where('cod_stabile', $codStabileLegacy); $rows = $q->get(['cartella', 'anno_ordinario', 'anno_riscaldamento']); - $out = []; + $out = []; foreach ($rows as $r) { $cartella = trim((string) ($r->cartella ?? '')); if ($cartella === '') { continue; } - $ord = trim((string) ($r->anno_ordinario ?? '')); + $ord = trim((string) ($r->anno_ordinario ?? '')); $risc = trim((string) ($r->anno_riscaldamento ?? '')); $parts = []; @@ -827,7 +825,7 @@ private function loadLegacyYearLabels(string $codStabileLegacy): array if ($risc !== '' && $risc !== $ord) { $parts[] = 'Risc. ' . $risc; } - $parts[] = 'Dir. ' . $cartella; + $parts[] = 'Dir. ' . $cartella; $out[$cartella] = implode(' · ', $parts); } return $out; @@ -835,9 +833,9 @@ private function loadLegacyYearLabels(string $codStabileLegacy): array private function reloadDocumenti(): void { - $this->feRows = []; + $this->feRows = []; $this->linkedFeIds = []; - $this->raRows = []; + $this->raRows = []; $this->linkedRaIds = []; // Documenti FE/RA: normalmente solo sui DEBITI (fatture passive + ritenute da versare) @@ -845,14 +843,14 @@ private function reloadDocumenti(): void return; } - $stabile = $this->getActiveStabile(); - $stabileId = $stabile?->id ? (int) $stabile->id : null; + $stabile = $this->getActiveStabile(); + $stabileId = $stabile?->id ? (int) $stabile->id : null; $codStabileLegacy = $stabile?->codice_stabile ? (string) $stabile->codice_stabile : null; if (! $stabileId) { return; } - $cd = 'D'; + $cd = 'D'; $this->feRows = $this->loadFeRows($stabileId, $cd); $this->raRows = $this->loadRaRows($stabileId); @@ -870,7 +868,7 @@ private function reloadDocumenti(): void private function loadFeRows(int $stabileId, string $cd): array { $cd = strtoupper(trim($cd)); - $q = FatturaElettronica::query()->where('stabile_id', $stabileId); + $q = FatturaElettronica::query()->where('stabile_id', $stabileId); // Per i debiti ha senso mostrare prima le fatture da pagare. if ($cd === 'D') { @@ -896,13 +894,13 @@ private function loadFeRows(int $stabileId, string $cd): array return $rows->map(function (FatturaElettronica $f) { return [ - 'id' => (int) $f->id, - 'data_fattura' => $f->data_fattura?->format('Y-m-d'), - 'data_scadenza' => $f->data_scadenza?->format('Y-m-d'), - 'numero_fattura' => (string) ($f->numero_fattura ?? ''), + 'id' => (int) $f->id, + 'data_fattura' => $f->data_fattura?->format('Y-m-d'), + 'data_scadenza' => $f->data_scadenza?->format('Y-m-d'), + 'numero_fattura' => (string) ($f->numero_fattura ?? ''), 'fornitore_denominazione' => (string) ($f->fornitore_denominazione ?? ''), - 'totale' => is_numeric($f->totale) ? (float) $f->totale : null, - 'stato' => (string) ($f->stato ?? ''), + 'totale' => is_numeric($f->totale) ? (float) $f->totale : null, + 'stato' => (string) ($f->stato ?? ''), ]; })->all(); } @@ -932,12 +930,12 @@ private function loadRaRows(int $stabileId): array } return [ - 'id' => (int) $r->id, - 'data_competenza' => $r->data_competenza?->format('Y-m-d'), + 'id' => (int) $r->id, + 'data_competenza' => $r->data_competenza?->format('Y-m-d'), 'numero_progressivo' => (int) ($r->numero_progressivo ?? 0), - 'fornitore' => $fornitoreLabel, - 'importo_ritenuta' => is_numeric($r->importo_ritenuta) ? (float) $r->importo_ritenuta : null, - 'stato_versamento' => (string) ($r->stato_versamento ?? ''), + 'fornitore' => $fornitoreLabel, + 'importo_ritenuta' => is_numeric($r->importo_ritenuta) ? (float) $r->importo_ritenuta : null, + 'stato_versamento' => (string) ($r->stato_versamento ?? ''), ]; })->all(); } @@ -957,7 +955,7 @@ private function loadLinkedFeIds(string $codStabileLegacy, string $legacyYear, i ->where('cre_deb_preced_id', $creDebPrecedId) ->where('c_d', strtoupper(trim($cd))) ->pluck('fattura_elettronica_id') - ->map(fn ($v) => (int) $v) + ->map(fn($v) => (int) $v) ->values() ->all(); } @@ -977,7 +975,7 @@ private function loadLinkedRaIds(string $codStabileLegacy, string $legacyYear, i ->where('cre_deb_preced_id', $creDebPrecedId) ->where('c_d', strtoupper(trim($cd))) ->pluck('registro_ritenuta_acconto_id') - ->map(fn ($v) => (int) $v) + ->map(fn($v) => (int) $v) ->values() ->all(); } @@ -992,8 +990,8 @@ public function linkFatturaElettronica(int $fatturaId): void return; } - $stabile = $this->getActiveStabile(); - $stabileId = $stabile?->id ? (int) $stabile->id : null; + $stabile = $this->getActiveStabile(); + $stabileId = $stabile?->id ? (int) $stabile->id : null; $codStabileLegacy = $stabile?->codice_stabile ? (string) $stabile->codice_stabile : null; if (! $codStabileLegacy) { return; @@ -1006,10 +1004,10 @@ public function linkFatturaElettronica(int $fatturaId): void DB::table('contabilita_situazione_iniziale_fe_links')->updateOrInsert( [ - 'cod_stabile_legacy' => $codStabileLegacy, - 'legacy_year' => $this->legacyYear, - 'cre_deb_preced_id' => (int) $this->selectedCreDebId, - 'c_d' => $cd, + 'cod_stabile_legacy' => $codStabileLegacy, + 'legacy_year' => $this->legacyYear, + 'cre_deb_preced_id' => (int) $this->selectedCreDebId, + 'c_d' => $cd, 'fattura_elettronica_id' => $fatturaId, ], [ @@ -1032,7 +1030,7 @@ public function unlinkFatturaElettronica(int $fatturaId): void return; } - $stabile = $this->getActiveStabile(); + $stabile = $this->getActiveStabile(); $codStabileLegacy = $stabile?->codice_stabile ? (string) $stabile->codice_stabile : null; if (! $codStabileLegacy) { return; @@ -1063,8 +1061,8 @@ public function linkRegistroRa(int $registroId): void return; } - $stabile = $this->getActiveStabile(); - $stabileId = $stabile?->id ? (int) $stabile->id : null; + $stabile = $this->getActiveStabile(); + $stabileId = $stabile?->id ? (int) $stabile->id : null; $codStabileLegacy = $stabile?->codice_stabile ? (string) $stabile->codice_stabile : null; if (! $codStabileLegacy) { return; @@ -1077,10 +1075,10 @@ public function linkRegistroRa(int $registroId): void DB::table('contabilita_situazione_iniziale_ra_links')->updateOrInsert( [ - 'cod_stabile_legacy' => $codStabileLegacy, - 'legacy_year' => $this->legacyYear, - 'cre_deb_preced_id' => (int) $this->selectedCreDebId, - 'c_d' => $cd, + 'cod_stabile_legacy' => $codStabileLegacy, + 'legacy_year' => $this->legacyYear, + 'cre_deb_preced_id' => (int) $this->selectedCreDebId, + 'c_d' => $cd, 'registro_ritenuta_acconto_id' => $registroId, ], [ @@ -1103,7 +1101,7 @@ public function unlinkRegistroRa(int $registroId): void return; } - $stabile = $this->getActiveStabile(); + $stabile = $this->getActiveStabile(); $codStabileLegacy = $stabile?->codice_stabile ? (string) $stabile->codice_stabile : null; if (! $codStabileLegacy) { return; @@ -1152,7 +1150,7 @@ private function loadVocSpeOptions(?string $codStabileLegacy, ?string $legacyYea if ($cod === '') { continue; } - $label = trim((string) ($r->descriz ?? '')); + $label = trim((string) ($r->descriz ?? '')); $out[$cod] = $label !== '' ? ($cod . ' — ' . $label) : $cod; } @@ -1191,21 +1189,21 @@ private function loadCreDebPrecedCrudRows(string $codStabileLegacy, ?string $leg $rows = $q->orderBy('cod_voc')->orderBy('id')->limit(5000) ->get(['id', 'c_d', 'cod_voc', 'des_voce', 'descrizione', 'importo_euro', 'n_stra', 'incluso']) - ->map(fn ($r) => [ - 'id' => (int) $r->id, - 'c_d' => (string) ($r->c_d ?? ''), - 'cod_voc' => (string) ($r->cod_voc ?? ''), - 'des_voce' => (string) ($r->des_voce ?? ''), - 'descrizione' => (string) ($r->descrizione ?? ''), + ->map(fn($r) => [ + 'id' => (int) $r->id, + 'c_d' => (string) ($r->c_d ?? ''), + 'cod_voc' => (string) ($r->cod_voc ?? ''), + 'des_voce' => (string) ($r->des_voce ?? ''), + 'descrizione' => (string) ($r->descrizione ?? ''), 'importo_euro' => (float) ($r->importo_euro ?? 0.0), - 'n_stra' => $r->n_stra !== null ? (int) $r->n_stra : null, - 'incluso' => $r->incluso !== null ? (int) $r->incluso : 1, + 'n_stra' => $r->n_stra !== null ? (int) $r->n_stra : null, + 'incluso' => $r->incluso !== null ? (int) $r->incluso : 1, ])->all(); // Lookup descrizione voce di spesa da voc_spe (stesso MDB): cod_voc -> voc_spe.cod $vocMap = []; if (! empty($rows) && Schema::connection('gescon_import')->hasTable('voc_spe')) { - $cods = array_values(array_unique(array_filter(array_map(fn ($r) => trim((string) ($r['cod_voc'] ?? '')), $rows), fn ($v) => $v !== ''))); + $cods = array_values(array_unique(array_filter(array_map(fn($r) => trim((string) ($r['cod_voc'] ?? '')), $rows), fn($v) => $v !== ''))); if (! empty($cods)) { $vq = DB::connection('gescon_import')->table('voc_spe')->where('cod_stabile', $codStabileLegacy); if ($legacyYear && Schema::connection('gescon_import')->hasColumn('voc_spe', 'legacy_year')) { @@ -1221,7 +1219,7 @@ private function loadCreDebPrecedCrudRows(string $codStabileLegacy, ?string $leg continue; } $vocMap[$k] = [ - 'descriz' => trim((string) ($vr->descriz ?? '')), + 'descriz' => trim((string) ($vr->descriz ?? '')), 'consuntivo_euro' => $vr->consuntivo_euro !== null ? (float) $vr->consuntivo_euro : null, 'preventivo_euro' => $vr->preventivo_euro !== null ? (float) $vr->preventivo_euro : null, ]; @@ -1233,11 +1231,11 @@ private function loadCreDebPrecedCrudRows(string $codStabileLegacy, ?string $leg foreach ($rows as &$row) { $cod = trim((string) ($row['cod_voc'] ?? '')); if ($cod !== '' && isset($vocMap[$cod])) { - $row['voc_spe_descriz'] = (string) ($vocMap[$cod]['descriz'] ?? ''); + $row['voc_spe_descriz'] = (string) ($vocMap[$cod]['descriz'] ?? ''); $row['voc_spe_consuntivo_euro'] = $vocMap[$cod]['consuntivo_euro'] ?? null; $row['voc_spe_preventivo_euro'] = $vocMap[$cod]['preventivo_euro'] ?? null; } else { - $row['voc_spe_descriz'] = ''; + $row['voc_spe_descriz'] = ''; $row['voc_spe_consuntivo_euro'] = null; $row['voc_spe_preventivo_euro'] = null; } @@ -1245,7 +1243,7 @@ private function loadCreDebPrecedCrudRows(string $codStabileLegacy, ?string $leg unset($row); } else { foreach ($rows as &$row) { - $row['voc_spe_descriz'] = ''; + $row['voc_spe_descriz'] = ''; $row['voc_spe_consuntivo_euro'] = null; $row['voc_spe_preventivo_euro'] = null; } @@ -1259,12 +1257,12 @@ private function loadCreDebPrecedCrudRows(string $codStabileLegacy, ?string $leg continue; } $edit[$id] = [ - 'cod_voc' => $row['cod_voc'], - 'des_voce' => $row['des_voce'], - 'descrizione' => $row['descrizione'], + 'cod_voc' => $row['cod_voc'], + 'des_voce' => $row['des_voce'], + 'descrizione' => $row['descrizione'], 'importo_euro' => $row['importo_euro'], - 'n_stra' => $row['n_stra'], - 'incluso' => $row['incluso'], + 'n_stra' => $row['n_stra'], + 'incluso' => $row['incluso'], ]; } @@ -1309,16 +1307,16 @@ private function loadDettTabCrudRows(string $codStabileLegacy, ?string $legacyYe ->orderBy('id') ->limit(5000) ->get(['id', 'cod_tab', 'id_cond', 'cond_inquil', 'mm', 'cons_euro', 'n_stra', 'unico']) - ->map(fn ($r) => [ - 'id' => (int) $r->id, - 'cod_tab' => (string) ($r->cod_tab ?? ''), - 'id_cond' => (string) ($r->id_cond ?? ''), - 'cond_label' => (string) ($labels[trim((string) ($r->id_cond ?? ''))] ?? ''), + ->map(fn($r) => [ + 'id' => (int) $r->id, + 'cod_tab' => (string) ($r->cod_tab ?? ''), + 'id_cond' => (string) ($r->id_cond ?? ''), + 'cond_label' => (string) ($labels[trim((string) ($r->id_cond ?? ''))] ?? ''), 'cond_inquil' => (string) ($r->cond_inquil ?? ''), - 'mm' => $r->mm !== null ? (float) $r->mm : null, - 'cons_euro' => $r->cons_euro !== null ? (float) $r->cons_euro : 0.0, - 'n_stra' => $r->n_stra !== null ? (int) $r->n_stra : null, - 'unico' => $r->unico !== null ? (int) $r->unico : 0, + 'mm' => $r->mm !== null ? (float) $r->mm : null, + 'cons_euro' => $r->cons_euro !== null ? (float) $r->cons_euro : 0.0, + 'n_stra' => $r->n_stra !== null ? (int) $r->n_stra : null, + 'unico' => $r->unico !== null ? (int) $r->unico : 0, ])->all(); $edit = []; @@ -1328,13 +1326,13 @@ private function loadDettTabCrudRows(string $codStabileLegacy, ?string $legacyYe continue; } $edit[$id] = [ - 'cod_tab' => $row['cod_tab'], - 'id_cond' => $row['id_cond'], + 'cod_tab' => $row['cod_tab'], + 'id_cond' => $row['id_cond'], 'cond_inquil' => $row['cond_inquil'], - 'mm' => $row['mm'], - 'cons_euro' => $row['cons_euro'], - 'n_stra' => $row['n_stra'], - 'unico' => $row['unico'], + 'mm' => $row['mm'], + 'cons_euro' => $row['cons_euro'], + 'n_stra' => $row['n_stra'], + 'unico' => $row['unico'], ]; } @@ -1362,13 +1360,13 @@ public function saveCreDebRow(int $id): void DB::connection('gescon_import')->table('cre_deb_preced') ->where('id', $id) ->update([ - 'cod_voc' => is_string($edit['cod_voc'] ?? null) ? trim((string) $edit['cod_voc']) : null, - 'des_voce' => is_string($edit['des_voce'] ?? null) ? trim((string) $edit['des_voce']) : null, - 'descrizione' => is_string($edit['descrizione'] ?? null) ? trim((string) $edit['descrizione']) : null, + 'cod_voc' => is_string($edit['cod_voc'] ?? null) ? trim((string) $edit['cod_voc']) : null, + 'des_voce' => is_string($edit['des_voce'] ?? null) ? trim((string) $edit['des_voce']) : null, + 'descrizione' => is_string($edit['descrizione'] ?? null) ? trim((string) $edit['descrizione']) : null, 'importo_euro' => round((float) $importo, 2), - 'n_stra' => is_numeric($edit['n_stra'] ?? null) ? (int) $edit['n_stra'] : null, - 'incluso' => ! empty($edit['incluso']) ? 1 : 0, - 'updated_at' => now(), + 'n_stra' => is_numeric($edit['n_stra'] ?? null) ? (int) $edit['n_stra'] : null, + 'incluso' => ! empty($edit['incluso']) ? 1 : 0, + 'updated_at' => now(), ]); $this->reloadData(); @@ -1391,7 +1389,7 @@ public function addCreDebRow(string $cd): void return; } - $stabile = $this->getActiveStabile(); + $stabile = $this->getActiveStabile(); $codStabileLegacy = $stabile?->codice_stabile ? (string) $stabile->codice_stabile : null; if (! $codStabileLegacy) { return; @@ -1408,17 +1406,17 @@ public function addCreDebRow(string $cd): void } $row = [ - 'cod_stabile' => $codStabileLegacy, - 'legacy_year' => $this->legacyYear, - 'c_d' => $cd, - 'cod_voc' => is_string($this->formCreDebAdd['cod_voc'] ?? null) ? trim((string) $this->formCreDebAdd['cod_voc']) : null, - 'des_voce' => is_string($this->formCreDebAdd['des_voce'] ?? null) ? trim((string) $this->formCreDebAdd['des_voce']) : null, - 'descrizione' => is_string($this->formCreDebAdd['descrizione'] ?? null) ? trim((string) $this->formCreDebAdd['descrizione']) : null, + 'cod_stabile' => $codStabileLegacy, + 'legacy_year' => $this->legacyYear, + 'c_d' => $cd, + 'cod_voc' => is_string($this->formCreDebAdd['cod_voc'] ?? null) ? trim((string) $this->formCreDebAdd['cod_voc']) : null, + 'des_voce' => is_string($this->formCreDebAdd['des_voce'] ?? null) ? trim((string) $this->formCreDebAdd['des_voce']) : null, + 'descrizione' => is_string($this->formCreDebAdd['descrizione'] ?? null) ? trim((string) $this->formCreDebAdd['descrizione']) : null, 'importo_euro' => round((float) $importo, 2), - 'n_stra' => is_numeric($this->formCreDebAdd['n_stra'] ?? null) ? (int) $this->formCreDebAdd['n_stra'] : null, - 'incluso' => ! empty($this->formCreDebAdd['incluso']) ? 1 : 0, - 'created_at' => now(), - 'updated_at' => now(), + 'n_stra' => is_numeric($this->formCreDebAdd['n_stra'] ?? null) ? (int) $this->formCreDebAdd['n_stra'] : null, + 'incluso' => ! empty($this->formCreDebAdd['incluso']) ? 1 : 0, + 'created_at' => now(), + 'updated_at' => now(), ]; $this->editingCreDebId = null; @@ -1427,12 +1425,12 @@ public function addCreDebRow(string $cd): void $this->formCreDebAdd['cod_voc'] = null; - $this->selectedCreDebId = null; - $this->formCreDebAdd['des_voce'] = null; - $this->formCreDebAdd['descrizione'] = null; + $this->selectedCreDebId = null; + $this->formCreDebAdd['des_voce'] = null; + $this->formCreDebAdd['descrizione'] = null; $this->formCreDebAdd['importo_euro'] = null; - $this->formCreDebAdd['n_stra'] = null; - $this->formCreDebAdd['incluso'] = 1; + $this->formCreDebAdd['n_stra'] = null; + $this->formCreDebAdd['incluso'] = 1; $this->reloadData(); } @@ -1458,14 +1456,14 @@ public function saveDettTabRow(int $id): void DB::connection('gescon_import')->table('dett_tab') ->where('id', $id) ->update([ - 'cod_tab' => 'CONG.O', - 'id_cond' => is_string($edit['id_cond'] ?? null) ? trim((string) $edit['id_cond']) : null, + 'cod_tab' => 'CONG.O', + 'id_cond' => is_string($edit['id_cond'] ?? null) ? trim((string) $edit['id_cond']) : null, 'cond_inquil' => is_string($edit['cond_inquil'] ?? null) ? trim((string) $edit['cond_inquil']) : null, - 'mm' => is_numeric($edit['mm'] ?? null) ? (float) $edit['mm'] : null, - 'cons_euro' => round((float) $cons, 2), - 'n_stra' => null, - 'unico' => ! empty($edit['unico']) ? 1 : 0, - 'updated_at' => now(), + 'mm' => is_numeric($edit['mm'] ?? null) ? (float) $edit['mm'] : null, + 'cons_euro' => round((float) $cons, 2), + 'n_stra' => null, + 'unico' => ! empty($edit['unico']) ? 1 : 0, + 'updated_at' => now(), ]); $this->reloadData(); @@ -1488,7 +1486,7 @@ public function addDettTabRow(): void return; } - $stabile = $this->getActiveStabile(); + $stabile = $this->getActiveStabile(); $codStabileLegacy = $stabile?->codice_stabile ? (string) $stabile->codice_stabile : null; if (! $codStabileLegacy) { return; @@ -1506,15 +1504,15 @@ public function addDettTabRow(): void } $row = [ - 'cod_tab' => $codTab, - 'id_cond' => $idCond, + 'cod_tab' => $codTab, + 'id_cond' => $idCond, 'cond_inquil' => is_string($this->formDettTabAdd['cond_inquil'] ?? null) ? trim((string) $this->formDettTabAdd['cond_inquil']) : null, - 'mm' => is_numeric($this->formDettTabAdd['mm'] ?? null) ? (float) $this->formDettTabAdd['mm'] : null, - 'cons_euro' => round((float) $cons, 2), - 'n_stra' => null, - 'unico' => ! empty($this->formDettTabAdd['unico']) ? 1 : 0, - 'created_at' => now(), - 'updated_at' => now(), + 'mm' => is_numeric($this->formDettTabAdd['mm'] ?? null) ? (float) $this->formDettTabAdd['mm'] : null, + 'cons_euro' => round((float) $cons, 2), + 'n_stra' => null, + 'unico' => ! empty($this->formDettTabAdd['unico']) ? 1 : 0, + 'created_at' => now(), + 'updated_at' => now(), ]; $this->editingDettTabId = null; @@ -1528,12 +1526,12 @@ public function addDettTabRow(): void DB::connection('gescon_import')->table('dett_tab')->insert($row); - $this->formDettTabAdd['id_cond'] = null; + $this->formDettTabAdd['id_cond'] = null; $this->formDettTabAdd['cond_inquil'] = null; - $this->formDettTabAdd['mm'] = null; - $this->formDettTabAdd['cons_euro'] = null; - $this->formDettTabAdd['n_stra'] = null; - $this->formDettTabAdd['unico'] = 0; + $this->formDettTabAdd['mm'] = null; + $this->formDettTabAdd['cons_euro'] = null; + $this->formDettTabAdd['n_stra'] = null; + $this->formDettTabAdd['unico'] = 0; $this->reloadData(); } @@ -1555,7 +1553,7 @@ private function loadLegacyYears(?string $codStabileLegacy): array continue; } $ordYear = null; - $ao = trim((string) ($r->anno_ordinario ?? '')); + $ao = trim((string) ($r->anno_ordinario ?? '')); if ($ao !== '' && preg_match('/^(\d{4})$/', $ao, $m)) { $ordYear = (int) $m[1]; } @@ -1576,13 +1574,13 @@ private function loadLegacyYears(?string $codStabileLegacy): array if ($codStabileLegacy && Schema::connection('gescon_import')->hasTable('cre_deb_preced') && Schema::connection('gescon_import')->hasColumn('cre_deb_preced', 'legacy_year')) { $years = array_merge($years, DB::connection('gescon_import')->table('cre_deb_preced') - ->where('cod_stabile', $codStabileLegacy) - ->whereNotNull('legacy_year') - ->distinct() - ->orderBy('legacy_year') - ->pluck('legacy_year') - ->map(fn ($v) => (string) $v) - ->all()); + ->where('cod_stabile', $codStabileLegacy) + ->whereNotNull('legacy_year') + ->distinct() + ->orderBy('legacy_year') + ->pluck('legacy_year') + ->map(fn($v) => (string) $v) + ->all()); } if ($codStabileLegacy && Schema::connection('gescon_import')->hasTable('dett_tab') && Schema::connection('gescon_import')->hasColumn('dett_tab', 'legacy_year')) { @@ -1592,7 +1590,7 @@ private function loadLegacyYears(?string $codStabileLegacy): array if (Schema::connection('gescon_import')->hasColumn('dett_tab', 'cod_stabile')) { $q->where('cod_stabile', $codStabileLegacy); } - $years = array_merge($years, $q->distinct()->orderBy('legacy_year')->pluck('legacy_year')->map(fn ($v) => (string) $v)->all()); + $years = array_merge($years, $q->distinct()->orderBy('legacy_year')->pluck('legacy_year')->map(fn($v) => (string) $v)->all()); } // Include anni presenti nelle straordinarie anche se non ci sono righe in cre_deb_preced/dett_tab. @@ -1618,10 +1616,10 @@ private function loadLegacyYears(?string $codStabileLegacy): array } } - $years = array_merge($years, $q->distinct()->orderBy('legacy_year')->pluck('legacy_year')->map(fn ($v) => (string) $v)->all()); + $years = array_merge($years, $q->distinct()->orderBy('legacy_year')->pluck('legacy_year')->map(fn($v) => (string) $v)->all()); } - $years = array_values(array_unique(array_filter($years, fn ($v) => $v !== ''))); + $years = array_values(array_unique(array_filter($years, fn($v) => $v !== ''))); // Se non abbiamo una mappa "anni", ordina in modo consistente. if (! ($codStabileLegacy && Schema::connection('gescon_import')->hasTable('gestioni_annuali'))) { @@ -1631,21 +1629,20 @@ private function loadLegacyYears(?string $codStabileLegacy): array return $years; } - private function emptyBilancio(): array { return [ - 'ordinaria' => ['cong_pos' => 0.0, 'cong_neg' => 0.0, 'crediti' => 0.0, 'debiti' => 0.0, 'attivita' => 0.0, 'passivita' => 0.0], + 'ordinaria' => ['cong_pos' => 0.0, 'cong_neg' => 0.0, 'crediti' => 0.0, 'debiti' => 0.0, 'attivita' => 0.0, 'passivita' => 0.0], 'riscaldamento' => ['cong_pos' => 0.0, 'cong_neg' => 0.0, 'crediti' => 0.0, 'debiti' => 0.0, 'attivita' => 0.0, 'passivita' => 0.0], 'straordinarie' => [], - 'tot_attivita' => 0.0, + 'tot_attivita' => 0.0, 'tot_passivita' => 0.0, - 'finanziario' => 0.0, - 'banca' => [ + 'finanziario' => 0.0, + 'banca' => [ 'saldo_manuale' => null, - 'saldo_letto' => null, + 'saldo_letto' => null, 'delta_manuale' => null, - 'delta_letto' => null, + 'delta_letto' => null, ], ]; } @@ -1682,12 +1679,12 @@ private function loadContiOptions(int $stabileId): array ->orderBy('id') ->get(['id', 'denominazione_banca', 'iban', 'numero_conto', 'legacy_cod_cassa']) ->mapWithKeys(function (DatiBancari $c): array { - $iban = is_string($c->iban) ? trim((string) $c->iban) : ''; - $numeroConto = is_string($c->numero_conto) ? trim((string) ($c->numero_conto ?? '')) : ''; + $iban = is_string($c->iban) ? trim((string) $c->iban) : ''; + $numeroConto = is_string($c->numero_conto) ? trim((string) ($c->numero_conto ?? '')) : ''; $legacyCodCassa = is_string($c->legacy_cod_cassa) ? strtoupper(trim((string) ($c->legacy_cod_cassa ?? ''))) : ''; - $label = trim((string) ($c->denominazione_banca ?? '')); - $identifier = $iban !== '' ? $iban : ($numeroConto !== '' ? $numeroConto : ($legacyCodCassa !== '' ? ('Cassa ' . $legacyCodCassa) : ('Conto #' . $c->id))); - $label = $label !== '' ? ($label . ' · ' . $identifier) : $identifier; + $label = trim((string) ($c->denominazione_banca ?? '')); + $identifier = $iban !== '' ? $iban : ($numeroConto !== '' ? $numeroConto : ($legacyCodCassa !== '' ? ('Cassa ' . $legacyCodCassa) : ('Conto #' . $c->id))); + $label = $label !== '' ? ($label . ' · ' . $identifier) : $identifier; return [(string) $c->id => $label]; }) ->all(); @@ -1701,31 +1698,31 @@ private function computeBilancio( ?string $saldoBancaManuale, array $risorseFinanziarie, ): array { - $ordinaria = $this->computeBucketOrdinaria($codStabileLegacy, $legacyYear); + $ordinaria = $this->computeBucketOrdinaria($codStabileLegacy, $legacyYear); $riscaldamento = $this->computeBucketRiscaldamento($codStabileLegacy, $legacyYear); // Straordinarie: cross-year. Identità = (legacy_year, id_stra). $straEntries = $this->loadStraEntries($codStabileLegacy, $legacyYear); - $stra = []; + $stra = []; foreach ($straEntries as $e) { $eYear = $e['legacy_year'] ?? null; $eStra = (int) ($e['n_stra'] ?? 0); - $eKey = (string) ($e['key'] ?? (string) $eStra); + $eKey = (string) ($e['key'] ?? (string) $eStra); if ($eStra <= 0) { continue; } - $bucket = $this->computeBucketStraordinaria($codStabileLegacy, $eYear ?? $legacyYear, $eStra); + $bucket = $this->computeBucketStraordinaria($codStabileLegacy, $eYear ?? $legacyYear, $eStra); $bucket['legacy_year'] = $eYear; - $bucket['key'] = $eKey; - $bucket['label'] = $this->formatStraLabel($eYear, $eStra, (string) ($bucket['titolo'] ?? '')); - $stra[$eKey] = $bucket; + $bucket['key'] = $eKey; + $bucket['label'] = $this->formatStraLabel($eYear, $eStra, (string) ($bucket['titolo'] ?? '')); + $stra[$eKey] = $bucket; } // Totali generali (stabile): CREDITI/DEBITI conteggiati UNA SOLA VOLTA, // mentre i conguagli si sommano per gestione (ORD/RISC/STRA). $totCreditiUnici = (float) ($ordinaria['crediti'] ?? 0.0); - $totDebitiUnici = (float) ($ordinaria['debiti'] ?? 0.0); + $totDebitiUnici = (float) ($ordinaria['debiti'] ?? 0.0); $totCongPos = (float) ($ordinaria['cong_pos'] ?? 0.0) + (float) ($riscaldamento['cong_pos'] ?? 0.0); $totCongNeg = (float) ($ordinaria['cong_neg'] ?? 0.0) + (float) ($riscaldamento['cong_neg'] ?? 0.0); @@ -1739,23 +1736,23 @@ private function computeBilancio( $totAtt = round($totAtt, 2); $totPas = round($totPas, 2); - $fin = round($totAtt - $totPas, 2); + $fin = round($totAtt - $totPas, 2); - $saldoLetto = $this->computeSaldoBancaTotaleLetto($risorseFinanziarie); + $saldoLetto = $this->computeSaldoBancaTotaleLetto($risorseFinanziarie); $saldoManualeVal = $this->parseMoney($saldoBancaManuale); return [ - 'ordinaria' => $ordinaria, + 'ordinaria' => $ordinaria, 'riscaldamento' => $riscaldamento, 'straordinarie' => $stra, - 'tot_attivita' => $totAtt, + 'tot_attivita' => $totAtt, 'tot_passivita' => $totPas, - 'finanziario' => $fin, - 'banca' => [ + 'finanziario' => $fin, + 'banca' => [ 'saldo_manuale' => $saldoManualeVal, - 'saldo_letto' => $saldoLetto, + 'saldo_letto' => $saldoLetto, 'delta_manuale' => $saldoManualeVal !== null ? round($saldoManualeVal - $fin, 2) : null, - 'delta_letto' => $saldoLetto !== null ? round($saldoLetto - $fin, 2) : null, + 'delta_letto' => $saldoLetto !== null ? round($saldoLetto - $fin, 2) : null, ], ]; } @@ -1763,20 +1760,20 @@ private function computeBilancio( private function computeBucketOrdinaria(string $codStabileLegacy, ?string $legacyYear): array { $crediti = $this->sumCreDebPreced($codStabileLegacy, $legacyYear, 'C', 'non_stra') - + $this->sumManualVoci('ordinaria', 'crediti', null, $legacyYear); + + $this->sumManualVoci('ordinaria', 'crediti', null, $legacyYear); $debiti = $this->sumCreDebPreced($codStabileLegacy, $legacyYear, 'D', 'non_stra') - + $this->sumManualVoci('ordinaria', 'debiti', null, $legacyYear); + + $this->sumManualVoci('ordinaria', 'debiti', null, $legacyYear); [$congPos, $congNeg] = $this->sumConguagli($codStabileLegacy, $legacyYear, 'CONG.O', 'non_stra'); $att = round($crediti + $congPos, 2); $pas = round($debiti + $congNeg, 2); return [ - 'cong_pos' => $congPos, - 'cong_neg' => $congNeg, - 'crediti' => $crediti, - 'debiti' => $debiti, - 'attivita' => $att, + 'cong_pos' => $congPos, + 'cong_neg' => $congNeg, + 'crediti' => $crediti, + 'debiti' => $debiti, + 'attivita' => $att, 'passivita' => $pas, ]; } @@ -1787,19 +1784,19 @@ private function computeBucketRiscaldamento(string $codStabileLegacy, ?string $l // `cre_deb_preced` (crediti/debiti pregressi) non distingue la gestione (ordinaria vs riscaldamento). // Per evitare duplicazioni percepite in UI, i Crediti/Debiti "pregressi" vengono mostrati SOLO in Ordinaria. // Qui teniamo esclusivamente i conguagli CONG.R (e gli eventuali manuali riscaldamento, se presenti). - $crediti = 0.0; - $debiti = 0.0; + $crediti = 0.0; + $debiti = 0.0; [$congPos, $congNeg] = $this->sumConguagli($codStabileLegacy, $legacyYear, 'CONG.R', 'non_stra'); $att = round($crediti + $congPos, 2); $pas = round($debiti + $congNeg, 2); return [ - 'cong_pos' => $congPos, - 'cong_neg' => $congNeg, - 'crediti' => $crediti, - 'debiti' => $debiti, - 'attivita' => $att, + 'cong_pos' => $congPos, + 'cong_neg' => $congNeg, + 'crediti' => $crediti, + 'debiti' => $debiti, + 'attivita' => $att, 'passivita' => $pas, ]; } @@ -1807,9 +1804,9 @@ private function computeBucketRiscaldamento(string $codStabileLegacy, ?string $l private function computeBucketStraordinaria(string $codStabileLegacy, ?string $legacyYear, int $nStra): array { $crediti = $this->sumCreDebPreced($codStabileLegacy, $legacyYear, 'C', $nStra) - + $this->sumManualVoci('straordinaria', 'crediti', $nStra, $legacyYear); + + $this->sumManualVoci('straordinaria', 'crediti', $nStra, $legacyYear); $debiti = $this->sumCreDebPreced($codStabileLegacy, $legacyYear, 'D', $nStra) - + $this->sumManualVoci('straordinaria', 'debiti', $nStra, $legacyYear); + + $this->sumManualVoci('straordinaria', 'debiti', $nStra, $legacyYear); [$congPos, $congNeg] = $this->sumConguagli($codStabileLegacy, $legacyYear, null, $nStra); $titolo = $this->resolveStraordinariaTitolo($codStabileLegacy, $legacyYear, $nStra); @@ -1818,13 +1815,13 @@ private function computeBucketStraordinaria(string $codStabileLegacy, ?string $l $pas = round($debiti + $congNeg, 2); return [ - 'n_stra' => $nStra, - 'titolo' => $titolo, - 'cong_pos' => $congPos, - 'cong_neg' => $congNeg, - 'crediti' => $crediti, - 'debiti' => $debiti, - 'attivita' => $att, + 'n_stra' => $nStra, + 'titolo' => $titolo, + 'cong_pos' => $congPos, + 'cong_neg' => $congNeg, + 'crediti' => $crediti, + 'debiti' => $debiti, + 'attivita' => $att, 'passivita' => $pas, ]; } @@ -1888,7 +1885,7 @@ private function sumManualVoci(string $gestione, string $categoria, ?int $nStra, return round((float) ($q->sum('importo_euro') ?? 0.0), 2); } - private function sumManualConguagli(?string $legacyYear, ?string $codTab, int|string $straFilter): array + private function sumManualConguagli(?string $legacyYear, ?string $codTab, int | string $straFilter): array { if (! DB::getSchemaBuilder()->hasTable('contabilita_situazione_iniziale_conguagli')) { return [0.0, 0.0]; @@ -1958,8 +1955,8 @@ private function resolveStraordinariaTitolo(string $codStabileLegacy, ?string $l // Preferisci l'anno selezionato se presente (per compatibilità con import per-cartella), // ma fai fallback cross-year così la UI può mostrare SEMPRE tutte le straordinarie. if ($legacyYear && Schema::connection('gescon_import')->hasColumn('straordinarie', 'legacy_year')) { - $q = DB::connection('gescon_import')->table('straordinarie'); - $q = $applyCodStabileFallback($q); + $q = DB::connection('gescon_import')->table('straordinarie'); + $q = $applyCodStabileFallback($q); $row = $q ->where('id_stra', $nStra) ->where('legacy_year', $legacyYear) @@ -1967,8 +1964,8 @@ private function resolveStraordinariaTitolo(string $codStabileLegacy, ?string $l } if (! $row) { - $q = DB::connection('gescon_import')->table('straordinarie'); - $q = $applyCodStabileFallback($q); + $q = DB::connection('gescon_import')->table('straordinarie'); + $q = $applyCodStabileFallback($q); $row = $q ->where('id_stra', $nStra) ->orderByDesc(Schema::connection('gescon_import')->hasColumn('straordinarie', 'legacy_year') ? 'legacy_year' : 'id_stra') @@ -1999,8 +1996,8 @@ private function loadStraEntries(string $codStabileLegacy, ?string $legacyYear): if ($n <= 0) { return; } - $yy = $y !== null && $y !== '' ? (string) $y : null; - $key = $yy ? ($yy . ':' . $n) : (string) $n; + $yy = $y !== null && $y !== '' ? (string) $y : null; + $key = $yy ? ($yy . ':' . $n) : (string) $n; $entries[$key] = ['key' => $key, 'legacy_year' => $yy, 'n_stra' => $n]; }; @@ -2127,7 +2124,7 @@ private function loadStraEntries(string $codStabileLegacy, ?string $legacyYear): return $list; } - private function sumCreDebPreced(string $codStabileLegacy, ?string $legacyYear, string $cd, int|string $straFilter): float + private function sumCreDebPreced(string $codStabileLegacy, ?string $legacyYear, string $cd, int | string $straFilter): float { if (! Schema::connection('gescon_import')->hasTable('cre_deb_preced')) { return 0.0; @@ -2168,7 +2165,7 @@ private function sumCreDebPreced(string $codStabileLegacy, ?string $legacyYear, return round((float) ($q->sum('importo_euro') ?? 0.0), 2); } - private function sumConguagli(string $codStabileLegacy, ?string $legacyYear, ?string $codTab, int|string $straFilter): array + private function sumConguagli(string $codStabileLegacy, ?string $legacyYear, ?string $codTab, int | string $straFilter): array { if (! Schema::connection('gescon_import')->hasTable('dett_tab')) { return [0.0, 0.0]; @@ -2234,22 +2231,22 @@ private function computeRisorseFinanziarie(int $stabileId, string $date): array } $dateC = Carbon::parse($date); - $out = []; + $out = []; foreach ($conti as $conto) { - $iban = is_string($conto->iban) ? trim((string) $conto->iban) : ''; - $numeroConto = is_string($conto->numero_conto) ? trim((string) ($conto->numero_conto ?? '')) : ''; + $iban = is_string($conto->iban) ? trim((string) $conto->iban) : ''; + $numeroConto = is_string($conto->numero_conto) ? trim((string) ($conto->numero_conto ?? '')) : ''; $legacyCodCassa = is_string($conto->legacy_cod_cassa) ? strtoupper(trim((string) ($conto->legacy_cod_cassa ?? ''))) : ''; - $saldo = $this->resolveContoSaldoAllaData($stabileId, $conto, $dateC); + $saldo = $this->resolveContoSaldoAllaData($stabileId, $conto, $dateC); - $label = trim((string) ($conto->denominazione_banca ?? '')); + $label = trim((string) ($conto->denominazione_banca ?? '')); $identifier = $iban !== '' ? $iban : ($numeroConto !== '' ? $numeroConto : ($legacyCodCassa !== '' ? ('Cassa ' . $legacyCodCassa) : ('Conto #' . $conto->id))); - $label = $label !== '' ? ($label . ' · ' . $identifier) : $identifier; + $label = $label !== '' ? ($label . ' · ' . $identifier) : $identifier; $out[] = [ 'conto_id' => (int) $conto->id, - 'iban' => $iban, - 'label' => $label, - 'saldo' => $saldo, + 'iban' => $iban, + 'label' => $label, + 'saldo' => $saldo, ]; } @@ -2273,16 +2270,16 @@ private function resolveContoSaldoAllaData(int $stabileId, DatiBancari $conto, C ? $snapshot->data_saldo->copy()->startOfDay() : Carbon::parse((string) $snapshot->data_saldo)->startOfDay(); $delta = (float) (MovimentoBanca::query() - ->where('stabile_id', $stabileId) - ->where('conto_id', $contoId) - ->whereDate('data', '>', $baseDate->toDateString()) - ->whereDate('data', '<=', $date->toDateString()) - ->sum('importo') ?? 0.0); + ->where('stabile_id', $stabileId) + ->where('conto_id', $contoId) + ->whereDate('data', '>', $baseDate->toDateString()) + ->whereDate('data', '<=', $date->toDateString()) + ->sum('importo') ?? 0.0); return round((float) ($snapshot->saldo ?? 0.0) + $delta, 2); } - $saldoBase = is_numeric($conto->saldo_iniziale) ? (float) $conto->saldo_iniziale : 0.0; + $saldoBase = is_numeric($conto->saldo_iniziale) ? (float) $conto->saldo_iniziale : 0.0; $dataSaldoIniziale = $conto->data_saldo_iniziale instanceof Carbon ? $conto->data_saldo_iniziale->copy()->startOfDay() : ($conto->data_saldo_iniziale ? Carbon::parse((string) $conto->data_saldo_iniziale)->startOfDay() : null); @@ -2351,13 +2348,13 @@ private function resolveSaldoOffsetDaSaldi(int $stabileId, int $contoId, string $saldoIniziale = $this->resolveSaldoInizialeDaDatiBancari($stabileId, $contoId, $iban); $sumToDateSaldo = (float) (MovimentoBanca::query() - ->where('stabile_id', $stabileId) - ->where('conto_id', $contoId) - ->where('iban', $iban) - ->whereDate('data', '<=', $match->data_saldo->toDateString()) - ->sum('importo') ?? 0.0); + ->where('stabile_id', $stabileId) + ->where('conto_id', $contoId) + ->where('iban', $iban) + ->whereDate('data', '<=', $match->data_saldo->toDateString()) + ->sum('importo') ?? 0.0); - $saldoAttesoAllaDataSaldo = is_numeric($match->saldo) ? (float) $match->saldo : 0.0; + $saldoAttesoAllaDataSaldo = is_numeric($match->saldo) ? (float) $match->saldo : 0.0; $saldoCalcolatoAllaDataSaldo = $saldoIniziale + $sumToDateSaldo; return (float) ($saldoAttesoAllaDataSaldo - $saldoCalcolatoAllaDataSaldo); @@ -2408,13 +2405,13 @@ private function reloadDettaglio(string $codStabileLegacy, ?string $legacyYear): $d = $this->dett; if (preg_match('/^(crediti|debiti)_(ordinaria|riscaldamento)$/', $d, $m)) { - $cd = $m[1] === 'crediti' ? 'C' : 'D'; + $cd = $m[1] === 'crediti' ? 'C' : 'D'; $bucket = $m[2]; $straFilter = $bucket === 'ordinaria' ? 'non_stra' : 'none'; $this->detailMeta = [ - 'tipo' => 'cre_deb_preced', + 'tipo' => 'cre_deb_preced', 'titolo' => strtoupper($m[1]) . ' · ' . strtoupper($bucket), ]; $this->detailRows = $this->loadCreDebPrecedRows($codStabileLegacy, $legacyYear, $cd, $straFilter); @@ -2422,11 +2419,11 @@ private function reloadDettaglio(string $codStabileLegacy, ?string $legacyYear): } if (preg_match('/^(crediti|debiti)_stra_(.+)$/', $d, $m)) { - $cd = $m[1] === 'crediti' ? 'C' : 'D'; + $cd = $m[1] === 'crediti' ? 'C' : 'D'; [$detailYear, $nStra] = $this->parseStraKey($m[2]); - $effectiveYear = $detailYear ?? $legacyYear; - $this->detailMeta = [ - 'tipo' => 'cre_deb_preced', + $effectiveYear = $detailYear ?? $legacyYear; + $this->detailMeta = [ + 'tipo' => 'cre_deb_preced', 'titolo' => strtoupper($m[1]) . ' · ' . $this->formatStraLabel($detailYear, (int) $nStra, ''), ]; $this->detailRows = $this->loadCreDebPrecedRows($codStabileLegacy, $effectiveYear, $cd, (int) $nStra); @@ -2434,12 +2431,12 @@ private function reloadDettaglio(string $codStabileLegacy, ?string $legacyYear): } if (preg_match('/^cong_(pos|neg)_(ordinaria|riscaldamento)$/', $d, $m)) { - $sign = $m[1]; + $sign = $m[1]; $bucket = $m[2]; $codTab = $bucket === 'ordinaria' ? 'CONG.O' : 'CONG.R'; $this->detailMeta = [ - 'tipo' => 'dett_tab', + 'tipo' => 'dett_tab', 'titolo' => 'CONGUAGLI ' . ($sign === 'pos' ? '(+)' : '(-)') . ' · ' . strtoupper($bucket) . ' · ' . $codTab, ]; $this->detailRows = $this->loadDettTabRows($codStabileLegacy, $legacyYear, $codTab, 'non_stra', $sign); @@ -2447,11 +2444,11 @@ private function reloadDettaglio(string $codStabileLegacy, ?string $legacyYear): } if (preg_match('/^cong_(pos|neg)_stra_(.+)$/', $d, $m)) { - $sign = $m[1]; + $sign = $m[1]; [$detailYear, $nStra] = $this->parseStraKey($m[2]); - $effectiveYear = $detailYear ?? $legacyYear; - $this->detailMeta = [ - 'tipo' => 'dett_tab', + $effectiveYear = $detailYear ?? $legacyYear; + $this->detailMeta = [ + 'tipo' => 'dett_tab', 'titolo' => 'CONGUAGLI ' . ($sign === 'pos' ? '(+)' : '(-)') . ' · ' . $this->formatStraLabel($detailYear, (int) $nStra, ''), ]; $this->detailRows = $this->loadDettTabRows($codStabileLegacy, $effectiveYear, null, (int) $nStra, $sign); @@ -2459,12 +2456,12 @@ private function reloadDettaglio(string $codStabileLegacy, ?string $legacyYear): } $this->detailMeta = [ - 'tipo' => 'unknown', + 'tipo' => 'unknown', 'titolo' => 'Dettaglio non riconosciuto', ]; } - private function loadCreDebPrecedRows(string $codStabileLegacy, ?string $legacyYear, string $cd, int|string $straFilter): array + private function loadCreDebPrecedRows(string $codStabileLegacy, ?string $legacyYear, string $cd, int | string $straFilter): array { if (! Schema::connection('gescon_import')->hasTable('cre_deb_preced')) { return []; @@ -2508,18 +2505,18 @@ private function loadCreDebPrecedRows(string $codStabileLegacy, ?string $legacyY ->orderBy('cod_voc') ->limit(5000) ->get(['c_d', 'cod_voc', 'des_voce', 'descrizione', 'importo_euro', 'n_stra']) - ->map(fn ($r) => [ - 'c_d' => (string) ($r->c_d ?? ''), - 'cod_voc' => (string) ($r->cod_voc ?? ''), - 'des_voce' => (string) ($r->des_voce ?? ''), - 'descrizione' => (string) ($r->descrizione ?? ''), + ->map(fn($r) => [ + 'c_d' => (string) ($r->c_d ?? ''), + 'cod_voc' => (string) ($r->cod_voc ?? ''), + 'des_voce' => (string) ($r->des_voce ?? ''), + 'descrizione' => (string) ($r->descrizione ?? ''), 'importo_euro' => (float) ($r->importo_euro ?? 0), - 'n_stra' => $r->n_stra !== null ? (int) $r->n_stra : null, + 'n_stra' => $r->n_stra !== null ? (int) $r->n_stra : null, ]) ->all(); } - private function loadDettTabRows(string $codStabileLegacy, ?string $legacyYear, ?string $codTab, int|string $straFilter, string $sign): array + private function loadDettTabRows(string $codStabileLegacy, ?string $legacyYear, ?string $codTab, int | string $straFilter, string $sign): array { if (! Schema::connection('gescon_import')->hasTable('dett_tab')) { return []; @@ -2569,18 +2566,18 @@ private function loadDettTabRows(string $codStabileLegacy, ?string $legacyYear, ->orderBy('id_cond') ->limit(5000) ->get(['cod_tab', 'id_cond', 'cond_inquil', 'mm', 'cons_euro', 'n_stra', 'unico', 'proviene_ors', 'proviene_n_stra', 'proviene_eserc']) - ->map(fn ($r) => [ - 'cod_tab' => (string) ($r->cod_tab ?? ''), - 'id_cond' => (string) ($r->id_cond ?? ''), - 'cond_label' => (string) ($labels[trim((string) ($r->id_cond ?? ''))] ?? ''), - 'cond_inquil' => (string) ($r->cond_inquil ?? ''), - 'mm' => $r->mm !== null ? (float) $r->mm : null, - 'cons_euro' => (float) ($r->cons_euro ?? 0), - 'n_stra' => $r->n_stra !== null ? (int) $r->n_stra : null, - 'unico' => (bool) ($r->unico ?? false), - 'proviene_ors' => (string) ($r->proviene_ors ?? ''), + ->map(fn($r) => [ + 'cod_tab' => (string) ($r->cod_tab ?? ''), + 'id_cond' => (string) ($r->id_cond ?? ''), + 'cond_label' => (string) ($labels[trim((string) ($r->id_cond ?? ''))] ?? ''), + 'cond_inquil' => (string) ($r->cond_inquil ?? ''), + 'mm' => $r->mm !== null ? (float) $r->mm : null, + 'cons_euro' => (float) ($r->cons_euro ?? 0), + 'n_stra' => $r->n_stra !== null ? (int) $r->n_stra : null, + 'unico' => (bool) ($r->unico ?? false), + 'proviene_ors' => (string) ($r->proviene_ors ?? ''), 'proviene_n_stra' => $r->proviene_n_stra !== null ? (int) $r->proviene_n_stra : null, - 'proviene_eserc' => (string) ($r->proviene_eserc ?? ''), + 'proviene_eserc' => (string) ($r->proviene_eserc ?? ''), ]) ->all(); } @@ -2631,12 +2628,12 @@ private function computeConguagliExcel(string $codStabileLegacy, ?string $legacy ->groupBy('id_cond', 'cod_tab', 'n_stra') ->orderByRaw('CAST(id_cond AS UNSIGNED)') ->get() - ->map(fn ($r) => [ - 'id_cond' => (string) ($r->id_cond ?? ''), + ->map(fn($r) => [ + 'id_cond' => (string) ($r->id_cond ?? ''), 'cond_label' => (string) ($labels[trim((string) ($r->id_cond ?? ''))] ?? ''), - 'cod_tab' => (string) ($r->cod_tab ?? ''), - 'n_stra' => $r->n_stra !== null ? (int) $r->n_stra : null, - 'totale' => (float) ($r->totale ?? 0.0), + 'cod_tab' => (string) ($r->cod_tab ?? ''), + 'n_stra' => $r->n_stra !== null ? (int) $r->n_stra : null, + 'totale' => (float) ($r->totale ?? 0.0), ]) ->all(); @@ -2660,18 +2657,18 @@ private function computeConguagliExcel(string $codStabileLegacy, ?string $legacy ]) ->groupBy('id_cond', 'cod_tab', 'n_stra') ->get() - ->map(fn ($r) => [ + ->map(fn($r) => [ 'id_cond' => (string) ($r->id_cond ?? ''), 'cod_tab' => (string) ($r->cod_tab ?? ''), - 'n_stra' => $r->n_stra !== null ? (int) $r->n_stra : null, - 'totale' => (float) ($r->totale ?? 0.0), + 'n_stra' => $r->n_stra !== null ? (int) $r->n_stra : null, + 'totale' => (float) ($r->totale ?? 0.0), ]) ->all(); // Sum into existing keys $acc = []; foreach ($rows as $r) { - $k = $r['id_cond'] . '|' . $r['cod_tab'] . '|' . (string) ($r['n_stra'] ?? ''); + $k = $r['id_cond'] . '|' . $r['cod_tab'] . '|' . (string) ($r['n_stra'] ?? ''); $acc[$k] = $r; } foreach ($manual as $r) { @@ -2688,16 +2685,16 @@ private function computeConguagliExcel(string $codStabileLegacy, ?string $legacy $cols = []; foreach ($rows as $r) { - $key = $r['cod_tab'] . '|' . (string) ($r['n_stra'] ?? ''); + $key = $r['cod_tab'] . '|' . (string) ($r['n_stra'] ?? ''); $cols[$key] = [ - 'key' => $key, - 'label' => $r['cod_tab'] . (($r['n_stra'] ?? null) ? (' #' . $r['n_stra']) : ''), + 'key' => $key, + 'label' => $r['cod_tab'] . (($r['n_stra'] ?? null) ? (' #' . $r['n_stra']) : ''), 'cod_tab' => $r['cod_tab'], - 'n_stra' => $r['n_stra'], + 'n_stra' => $r['n_stra'], ]; } $cols = array_values($cols); - usort($cols, fn ($a, $b) => strcmp((string) $a['label'], (string) $b['label'])); + usort($cols, fn($a, $b) => strcmp((string) $a['label'], (string) $b['label'])); $matrix = []; foreach ($rows as $r) { @@ -2705,13 +2702,12 @@ private function computeConguagliExcel(string $codStabileLegacy, ?string $legacy if (! isset($matrix[$idCond])) { $matrix[$idCond] = ['id_cond' => $idCond, 'cells' => []]; } - $key = $r['cod_tab'] . '|' . (string) ($r['n_stra'] ?? ''); + $key = $r['cod_tab'] . '|' . (string) ($r['n_stra'] ?? ''); $matrix[$idCond]['cells'][$key] = (float) ($r['totale'] ?? 0.0); } - $out = array_values($matrix); - usort($out, fn ($a, $b) => (int) ($a['id_cond'] ?? 0) <=> (int) ($b['id_cond'] ?? 0)); + usort($out, fn($a, $b) => (int) ($a['id_cond'] ?? 0) <=> (int) ($b['id_cond'] ?? 0)); return [$cols, $out]; } @@ -2726,9 +2722,9 @@ public function addVoceManuale(): void return; } - $gestione = is_string($this->formVoce['gestione'] ?? null) ? trim((string) $this->formVoce['gestione']) : ''; + $gestione = is_string($this->formVoce['gestione'] ?? null) ? trim((string) $this->formVoce['gestione']) : ''; $categoria = is_string($this->formVoce['categoria'] ?? null) ? trim((string) $this->formVoce['categoria']) : ''; - $gestione = in_array($gestione, ['ordinaria', 'riscaldamento', 'straordinaria'], true) ? $gestione : 'ordinaria'; + $gestione = in_array($gestione, ['ordinaria', 'riscaldamento', 'straordinaria'], true) ? $gestione : 'ordinaria'; $categoria = in_array($categoria, ['crediti', 'debiti'], true) ? $categoria : 'crediti'; $nStra = $gestione === 'straordinaria' && is_numeric($this->formVoce['n_stra'] ?? null) ? (int) $this->formVoce['n_stra'] : null; @@ -2742,22 +2738,22 @@ public function addVoceManuale(): void } DB::table('contabilita_situazione_iniziale_voci')->insert([ - 'stabile_id' => (int) $stabileId, - 'legacy_year' => $this->legacyYear, - 'gestione' => $gestione, - 'n_stra' => $gestione === 'straordinaria' ? $nStra : null, - 'categoria' => $categoria, - 'codice' => is_string($this->formVoce['codice'] ?? null) ? trim((string) $this->formVoce['codice']) : null, - 'descrizione' => is_string($this->formVoce['descrizione'] ?? null) ? trim((string) $this->formVoce['descrizione']) : null, + 'stabile_id' => (int) $stabileId, + 'legacy_year' => $this->legacyYear, + 'gestione' => $gestione, + 'n_stra' => $gestione === 'straordinaria' ? $nStra : null, + 'categoria' => $categoria, + 'codice' => is_string($this->formVoce['codice'] ?? null) ? trim((string) $this->formVoce['codice']) : null, + 'descrizione' => is_string($this->formVoce['descrizione'] ?? null) ? trim((string) $this->formVoce['descrizione']) : null, 'importo_euro' => $importo, - 'incluso' => 1, - 'origine' => 'manuale', - 'created_at' => now(), - 'updated_at' => now(), + 'incluso' => 1, + 'origine' => 'manuale', + 'created_at' => now(), + 'updated_at' => now(), ]); - $this->formVoce['codice'] = null; - $this->formVoce['descrizione'] = null; + $this->formVoce['codice'] = null; + $this->formVoce['descrizione'] = null; $this->formVoce['importo_euro'] = null; $this->reloadData(); @@ -2784,25 +2780,25 @@ public function addConguaglioManuale(): void } $nStra = is_numeric($this->formConguaglio['n_stra'] ?? null) ? (int) $this->formConguaglio['n_stra'] : null; - $cons = $this->parseMoney(is_string($this->formConguaglio['cons_euro'] ?? null) ? (string) $this->formConguaglio['cons_euro'] : null); + $cons = $this->parseMoney(is_string($this->formConguaglio['cons_euro'] ?? null) ? (string) $this->formConguaglio['cons_euro'] : null); if ($cons === null) { return; } DB::table('contabilita_situazione_iniziale_conguagli')->insert([ - 'stabile_id' => (int) $stabileId, + 'stabile_id' => (int) $stabileId, 'legacy_year' => $this->legacyYear, - 'cod_tab' => $codTab, - 'n_stra' => $nStra && $nStra > 0 ? $nStra : null, - 'id_cond' => $idCond, - 'cons_euro' => $cons, - 'incluso' => 1, - 'origine' => 'manuale', - 'created_at' => now(), - 'updated_at' => now(), + 'cod_tab' => $codTab, + 'n_stra' => $nStra && $nStra > 0 ? $nStra : null, + 'id_cond' => $idCond, + 'cons_euro' => $cons, + 'incluso' => 1, + 'origine' => 'manuale', + 'created_at' => now(), + 'updated_at' => now(), ]); - $this->formConguaglio['id_cond'] = null; + $this->formConguaglio['id_cond'] = null; $this->formConguaglio['cons_euro'] = null; $this->reloadData(); } diff --git a/app/Filament/Pages/Contabilita/VociSpesaArchivio.php b/app/Filament/Pages/Contabilita/VociSpesaArchivio.php index c04d89c..c515651 100644 --- a/app/Filament/Pages/Contabilita/VociSpesaArchivio.php +++ b/app/Filament/Pages/Contabilita/VociSpesaArchivio.php @@ -1,16 +1,15 @@ query('tab', 'ordinaria'); + $tab = (string) request()->query('tab', 'ordinaria'); $this->tipoGestioneTab = in_array($tab, ['ordinaria', 'acqua', 'riscaldamento', 'straordinaria'], true) ? $tab : 'ordinaria'; $this->gestioneContabileId = $this->resolveGestioneContabileId(); - $preset = (string) request()->query('preset', 'manuale'); + $preset = (string) request()->query('preset', 'manuale'); $this->presetRipartizione = in_array($preset, ['manuale', 'confedilizia'], true) ? $preset : 'manuale'; - $focus = request()->query('tabella'); + $focus = request()->query('tabella'); $this->tabellaFocusId = is_numeric($focus) ? (int) $focus : null; if (! $this->tabellaFocusId || $this->tabellaFocusId <= 0) { $this->tabellaFocusId = $this->getFirstTabellaIdForFocus(); @@ -114,7 +113,7 @@ protected function getHeaderActions(): array Select::make('anno_sorgente') ->label('Anno sorgente') ->options(function (): array { - $y = (int) now()->year; + $y = (int) now()->year; $out = []; for ($i = 0; $i < 8; $i++) { $out[(string) ($y - $i)] = (string) ($y - $i); @@ -134,7 +133,7 @@ protected function getHeaderActions(): array ]) ->action(function (array $data): void { $annoSorgente = is_numeric($data['anno_sorgente'] ?? null) ? (int) $data['anno_sorgente'] : 0; - $aggiorna = (bool) ($data['aggiorna_esistenti'] ?? true); + $aggiorna = (bool) ($data['aggiorna_esistenti'] ?? true); $this->importaVociPerAnno($annoSorgente, $aggiorna); }), @@ -183,7 +182,7 @@ private function mapTabellaToAnno(?int $sourceTabellaId, int $stabileId, int $ta return $sourceTabellaId; } - $hasAnno = Schema::hasColumn('tabelle_millesimali', 'anno_gestione'); + $hasAnno = Schema::hasColumn('tabelle_millesimali', 'anno_gestione'); $hasLegacyTipo = Schema::hasColumn('tabelle_millesimali', 'tipo'); $q = TabellaMillesimale::query() @@ -219,7 +218,7 @@ private function importaVociPerAnno(int $annoSorgente, bool $aggiornaEsistenti = return; } - $tipo = $this->normalizeTipoGestione($this->tipoGestioneTab ?: 'ordinaria'); + $tipo = $this->normalizeTipoGestione($this->tipoGestioneTab ?: 'ordinaria'); $annoTarget = AnnoGestioneContext::resolveActiveAnno($user); $gestioneTargetId = $this->gestioneContabileId ?: $this->resolveGestioneContabileId(); @@ -297,7 +296,7 @@ private function importaVociPerAnno(int $annoSorgente, bool $aggiornaEsistenti = } $targetId = (int) $existing[$codice]; - $target = VoceSpesa::query()->whereKey($targetId)->first(); + $target = VoceSpesa::query()->whereKey($targetId)->first(); if (! $target) { $skipped++; continue; @@ -327,7 +326,7 @@ private function importaVociPerAnno(int $annoSorgente, bool $aggiornaEsistenti = // Crea nuova voce per gestione target (preserva codice). $data = $src->toArray(); unset($data['id'], $data['id_voce'], $data['created_at'], $data['updated_at']); - $data['stabile_id'] = $stabileId; + $data['stabile_id'] = $stabileId; $data['gestione_contabile_id'] = $gestioneTargetId; if (Schema::hasColumn('voci_spesa', 'tipo_gestione')) { $data['tipo_gestione'] = $tipo; @@ -363,8 +362,8 @@ private function ricollegaTabellePerAnnoAttivo(): void return; } - $annoTarget = AnnoGestioneContext::resolveActiveAnno($user); - $tipo = $this->normalizeTipoGestione($this->tipoGestioneTab ?: 'ordinaria'); + $annoTarget = AnnoGestioneContext::resolveActiveAnno($user); + $tipo = $this->normalizeTipoGestione($this->tipoGestioneTab ?: 'ordinaria'); $gestioneTargetId = $this->gestioneContabileId ?: $this->resolveGestioneContabileId(); if (! $gestioneTargetId) { Notification::make()->title('Gestione non trovata')->danger()->send(); @@ -441,11 +440,11 @@ public function getTabelleMillesimaliOptions(): array return []; } - $tipoTab = $this->tipoGestioneTab ?: 'ordinaria'; - $tipo = $this->normalizeTipoGestione($tipoTab); + $tipoTab = $this->tipoGestioneTab ?: 'ordinaria'; + $tipo = $this->normalizeTipoGestione($tipoTab); $hasLegacyTipo = Schema::hasColumn('tabelle_millesimali', 'tipo'); - $hasAnno = Schema::hasColumn('tabelle_millesimali', 'anno_gestione'); - $anno = AnnoGestioneContext::resolveActiveAnno($user); + $hasAnno = Schema::hasColumn('tabelle_millesimali', 'anno_gestione'); + $anno = AnnoGestioneContext::resolveActiveAnno($user); $q = TabellaMillesimale::query() ->where('stabile_id', $stabileId) @@ -481,15 +480,15 @@ public function getTabelleMillesimaliOptions(): array public function updatedTabellaFocusId($value): void { - $id = is_numeric($value) ? (int) $value : null; + $id = is_numeric($value) ? (int) $value : null; $this->tabellaFocusId = $id && $id > 0 ? $id : null; } /** @return array{label: string, count: int, totale_prev: float, totale_cons: float, voci: array} */ public function getDettaglioTabellaFocus(): array { - $id = (int) ($this->tabellaFocusId ?? 0); - $opts = $this->getTabelleMillesimaliOptions(); + $id = (int) ($this->tabellaFocusId ?? 0); + $opts = $this->getTabelleMillesimaliOptions(); $label = $id > 0 && array_key_exists($id, $opts) ? (string) $opts[$id] : '—'; if ($id <= 0) { @@ -511,13 +510,13 @@ public function getDettaglioTabellaFocus(): array $voci = $records->map(function ($r): array { return [ - 'codice' => (string) ($r->codice ?? ''), + 'codice' => (string) ($r->codice ?? ''), 'descrizione' => (string) ($r->descrizione ?? ''), - 'prev' => (float) ($r->importo_default ?? 0), - 'cons' => (float) ($r->importo_consuntivo ?? 0), - 'prop' => (float) ($r->percentuale_condomino ?? 0), - 'inq' => (float) ($r->percentuale_inquilino ?? 0), - 'attiva' => (bool) ($r->attiva ?? false), + 'prev' => (float) ($r->importo_default ?? 0), + 'cons' => (float) ($r->importo_consuntivo ?? 0), + 'prop' => (float) ($r->percentuale_condomino ?? 0), + 'inq' => (float) ($r->percentuale_inquilino ?? 0), + 'attiva' => (bool) ($r->attiva ?? false), ]; })->all(); @@ -525,11 +524,11 @@ public function getDettaglioTabellaFocus(): array $totCons = (float) $records->sum(fn($r) => (float) ($r->importo_consuntivo ?? 0)); return [ - 'label' => $label, - 'count' => count($voci), + 'label' => $label, + 'count' => count($voci), 'totale_prev' => $totPrev, 'totale_cons' => $totCons, - 'voci' => $voci, + 'voci' => $voci, ]; } @@ -551,7 +550,7 @@ public function updatedPresetRipartizione(string $value): void public function updatedGestioneContabileId($value): void { - $id = is_numeric($value) ? (int) $value : null; + $id = is_numeric($value) ? (int) $value : null; $this->gestioneContabileId = $id && $id > 0 ? $id : null; $this->resetPage(); } @@ -580,14 +579,14 @@ public function getGestioniOptions(): array ->get(['id', 'anno_gestione', 'tipo_gestione', 'stato']) ->all(); - $options = []; + $options = []; $seenYears = []; foreach ($gestioni as $g) { $anno = (string) ($g->anno_gestione ?? ''); if ($anno === '' || isset($seenYears[$anno])) { continue; } - $seenYears[$anno] = true; + $seenYears[$anno] = true; $options[(int) $g->id] = $anno; } @@ -603,10 +602,10 @@ private function normalizeTipoGestione(string $tipo): string private function getLegacyTipoValues(string $tipo): array { return match ($tipo) { - 'ordinaria' => ['ordinaria', 'O', 'o'], + 'ordinaria' => ['ordinaria', 'O', 'o'], 'riscaldamento' => ['riscaldamento', 'R', 'r'], 'straordinaria' => ['straordinaria', 'S', 's'], - default => [$tipo], + default => [$tipo], }; } @@ -636,8 +635,8 @@ private function applyTipoGestioneFilter(Builder $q, string $tipoTab, bool $hasL private function getTabellaGroupTitle(VoceSpesa $record): string { $tabella = $this->resolveTabellaForAnno($record->tabellaMillesimaleDefault, (int) $record->stabile_id); - $code = trim((string) ($tabella?->codice_tabella ?? '')); - $name = trim((string) ($tabella?->denominazione ?? '')); + $code = trim((string) ($tabella?->codice_tabella ?? '')); + $name = trim((string) ($tabella?->denominazione ?? '')); if ($name === '') { $name = trim((string) ($tabella?->nome_tabella ?? '')); } @@ -724,12 +723,12 @@ private function getNordOptions(): array $values = $q->selectRaw('DISTINCT ' . $expr . ' as nord_val') ->pluck('nord_val') - ->filter(fn ($v) => $v !== null && $v !== '') + ->filter(fn($v) => $v !== null && $v !== '') ->unique() ->sort() ->values(); - return $values->mapWithKeys(fn ($v) => [(string) $v => (string) $v])->all(); + return $values->mapWithKeys(fn($v) => [(string) $v => (string) $v])->all(); } /** @return array */ @@ -740,11 +739,11 @@ private function getFirstTabellaCodesToHide(int $stabileId): array return []; } - $tipoTab = $this->tipoGestioneTab ?: 'ordinaria'; - $tipo = $this->normalizeTipoGestione($tipoTab); + $tipoTab = $this->tipoGestioneTab ?: 'ordinaria'; + $tipo = $this->normalizeTipoGestione($tipoTab); $hasLegacyTipo = Schema::hasColumn('tabelle_millesimali', 'tipo'); - $hasAnno = Schema::hasColumn('tabelle_millesimali', 'anno_gestione'); - $anno = AnnoGestioneContext::resolveActiveAnno($user); + $hasAnno = Schema::hasColumn('tabelle_millesimali', 'anno_gestione'); + $anno = AnnoGestioneContext::resolveActiveAnno($user); $q = TabellaMillesimale::query()->where('stabile_id', $stabileId); @@ -824,7 +823,7 @@ public function applyPresetRipartizione(): void } $categoria = $this->mapVoceCategoriaToConfedilizia((string) $record->categoria); - $default = RipartizioneSpeseInquilini::getDefaultConfedilizia($categoria); + $default = RipartizioneSpeseInquilini::getDefaultConfedilizia($categoria); $record->update([ 'percentuale_condomino' => (float) ($default['proprietario'] ?? 100), @@ -838,23 +837,23 @@ public function applyPresetRipartizione(): void private function mapVoceCategoriaToConfedilizia(string $categoriaVoce): string { return match ($categoriaVoce) { - 'riscaldamento' => 'B', - 'ascensore' => 'C', - 'illuminazione' => 'D', - 'pulizia' => 'E', - 'acqua' => 'G', - 'energia_elettrica' => 'L', - 'gas' => 'B', - 'giardino_verde' => 'I', - 'sicurezza' => 'N', - 'amministrazione' => 'O', - 'assicurazioni' => 'A', - 'tasse_tributi' => 'O', - 'spese_legali' => 'O', - 'manutenzione_ordinaria' => 'A', + 'riscaldamento' => 'B', + 'ascensore' => 'C', + 'illuminazione' => 'D', + 'pulizia' => 'E', + 'acqua' => 'G', + 'energia_elettrica' => 'L', + 'gas' => 'B', + 'giardino_verde' => 'I', + 'sicurezza' => 'N', + 'amministrazione' => 'O', + 'assicurazioni' => 'A', + 'tasse_tributi' => 'O', + 'spese_legali' => 'O', + 'manutenzione_ordinaria' => 'A', 'manutenzione_straordinaria' => 'A', - 'lavori_miglioramento' => 'A', - default => 'A', + 'lavori_miglioramento' => 'A', + default => 'A', }; } @@ -867,13 +866,13 @@ public function setTipoGestioneTab(string $tab): void $selectedYear = null; if ($this->gestioneContabileId && Schema::hasTable('gestioni_contabili')) { - $year = GestioneContabile::query()->whereKey($this->gestioneContabileId)->value('anno_gestione'); + $year = GestioneContabile::query()->whereKey($this->gestioneContabileId)->value('anno_gestione'); $selectedYear = is_numeric($year) ? (int) $year : null; } $this->tipoGestioneTab = $tab; - $user = Auth::user(); + $user = Auth::user(); $stabileId = $user instanceof User ? StabileContext::resolveActiveStabileId($user) : null; if ($stabileId && $selectedYear) { $this->gestioneContabileId = $this->resolveGestioneIdForAnnoTipo( @@ -954,7 +953,7 @@ protected function getTableQuery(): Builder if ($this->hideFirstTwoTabelle) { $hidden = $this->getFirstTabellaCodesToHide($activeStabileId); - if (!empty($hidden)) { + if (! empty($hidden)) { $query->where(function (Builder $q) use ($hidden) { $q->whereNull('tm.codice_tabella')->orWhereNotIn('tm.codice_tabella', $hidden); }); @@ -1044,7 +1043,7 @@ public function getTotaleConsuntivo(): float return 0.0; } - $tipoTab = $this->tipoGestioneTab; + $tipoTab = $this->tipoGestioneTab; $hasLegacyTipo = Schema::hasColumn('tabelle_millesimali', 'tipo'); $q = VoceSpesa::query()->where('voci_spesa.stabile_id', $activeStabileId); @@ -1125,9 +1124,9 @@ public function getTotaliPerTabella(): array return []; } - $tipoTab = $this->tipoGestioneTab; + $tipoTab = $this->tipoGestioneTab; $hasLegacyTipo = Schema::hasColumn('tabelle_millesimali', 'tipo'); - $q = VoceSpesa::query() + $q = VoceSpesa::query() ->leftJoin('tabelle_millesimali as tm', 'tm.id', '=', 'voci_spesa.tabella_millesimale_default_id') ->where('voci_spesa.stabile_id', $activeStabileId) ->when(Schema::hasColumn('voci_spesa', 'gestione_contabile_id') && $this->gestioneContabileId, function (Builder $qq) { @@ -1145,7 +1144,7 @@ public function getTotaliPerTabella(): array ->orderBy('tabella') ->get() ->map(fn($r) => [ - 'tabella' => (string) $r->tabella, + 'tabella' => (string) $r->tabella, 'totale_prev' => (float) $r->totale_prev, 'totale_cons' => (float) $r->totale_cons, ]) @@ -1160,25 +1159,25 @@ public function table(Table $table): Table Group::make('tabellaMillesimaleDefault.codice_tabella') ->label('Tabella') ->titlePrefixedWithLabel(false) - ->getTitleFromRecordUsing(fn (VoceSpesa $record): string => $this->getTabellaGroupTitle($record)), + ->getTitleFromRecordUsing(fn(VoceSpesa $record): string => $this->getTabellaGroupTitle($record)), ]) ->defaultGroup('tabellaMillesimaleDefault.codice_tabella') ->groupingSettingsHidden() ->filters([ SelectFilter::make('tabella') ->label('Tabella') - ->options(fn (): array => $this->getTabelleMillesimaliOptions()) + ->options(fn(): array=> $this->getTabelleMillesimaliOptions()) ->searchable() ->query(function (Builder $query, array $data): Builder { $value = $data['value'] ?? null; - if (!is_numeric($value)) { + if (! is_numeric($value)) { return $query; } return $query->where('voci_spesa.tabella_millesimale_default_id', (int) $value); }), SelectFilter::make('nord') ->label('NORD') - ->options(fn (): array => $this->getNordOptions()) + ->options(fn(): array=> $this->getNordOptions()) ->query(function (Builder $query, array $data): Builder { $value = $data['value'] ?? null; if ($value === null || $value === '') { @@ -1230,8 +1229,8 @@ public function table(Table $table): Table ->hiddenLabel() ->icon('heroicon-o-document-chart-bar') ->tooltip('Mastrino') - ->url(fn (VoceSpesa $record) => VoceSpesaMastrino::getUrl([ - 'record' => (int) $record->id, + ->url(fn(VoceSpesa $record) => VoceSpesaMastrino::getUrl([ + 'record' => (int) $record->id, 'gestione' => $this->gestioneContabileId, ], panel: 'admin-filament')), Action::make('modifica') @@ -1284,18 +1283,18 @@ public function table(Table $table): Table ->fillForm(function (VoceSpesa $record): array { return [ 'tabella_millesimale_default_id' => $record->tabella_millesimale_default_id, - 'attiva' => $record->attiva ? 1 : 0, - 'categoria' => $record->categoria, - 'tipo_gestione' => $record->tipo_gestione, - 'importo_default' => $record->importo_default, - 'importo_consuntivo' => $record->importo_consuntivo, - 'percentuale_condomino' => $record->percentuale_condomino, - 'percentuale_inquilino' => $record->percentuale_inquilino, + 'attiva' => $record->attiva ? 1 : 0, + 'categoria' => $record->categoria, + 'tipo_gestione' => $record->tipo_gestione, + 'importo_default' => $record->importo_default, + 'importo_consuntivo' => $record->importo_consuntivo, + 'percentuale_condomino' => $record->percentuale_condomino, + 'percentuale_inquilino' => $record->percentuale_inquilino, ]; }) ->action(function (array $data, VoceSpesa $record): void { $cond = isset($data['percentuale_condomino']) ? (float) $data['percentuale_condomino'] : null; - $inq = isset($data['percentuale_inquilino']) ? (float) $data['percentuale_inquilino'] : null; + $inq = isset($data['percentuale_inquilino']) ? (float) $data['percentuale_inquilino'] : null; if ($cond !== null && $inq === null) { $inq = max(0.0, 100.0 - $cond); } @@ -1303,13 +1302,13 @@ public function table(Table $table): Table 'tabella_millesimale_default_id' => is_numeric($data['tabella_millesimale_default_id'] ?? null) ? (int) $data['tabella_millesimale_default_id'] : null, - 'attiva' => (bool) ($data['attiva'] ?? true), - 'categoria' => $data['categoria'] ?? $record->categoria, - 'tipo_gestione' => $data['tipo_gestione'] ?? $record->tipo_gestione, - 'importo_default' => $data['importo_default'] ?? $record->importo_default, - 'importo_consuntivo' => $data['importo_consuntivo'] ?? $record->importo_consuntivo, - 'percentuale_condomino' => $cond ?? $record->percentuale_condomino, - 'percentuale_inquilino' => $inq ?? $record->percentuale_inquilino, + 'attiva' => (bool) ($data['attiva'] ?? true), + 'categoria' => $data['categoria'] ?? $record->categoria, + 'tipo_gestione' => $data['tipo_gestione'] ?? $record->tipo_gestione, + 'importo_default' => $data['importo_default'] ?? $record->importo_default, + 'importo_consuntivo' => $data['importo_consuntivo'] ?? $record->importo_consuntivo, + 'percentuale_condomino' => $cond ?? $record->percentuale_condomino, + 'percentuale_inquilino' => $inq ?? $record->percentuale_inquilino, ]); $this->dispatch('$refresh'); }), diff --git a/app/Filament/Pages/Fornitore/ProdottiCatalogo.php b/app/Filament/Pages/Fornitore/ProdottiCatalogo.php index 001447b..815bf8e 100644 --- a/app/Filament/Pages/Fornitore/ProdottiCatalogo.php +++ b/app/Filament/Pages/Fornitore/ProdottiCatalogo.php @@ -69,8 +69,8 @@ public function mount(): void return; } - $this->fornitoreId = (int) $fornitore->id; - $this->fornitoreLabel = $this->getFornitoreLabel($fornitore); + $this->fornitoreId = (int) $fornitore->id; + $this->fornitoreLabel = $this->getFornitoreLabel($fornitore); $this->amazonConfigured = app(AmazonCreatorsApiService::class)->isConfigured(); $this->refreshRows(); } @@ -200,8 +200,8 @@ public function saveAmazonCandidate(string $asin): void Notification::make() ->title(! empty($result['created']) ? 'Prodotto Amazon memorizzato' : 'Prodotto Amazon aggiornato') ->body($product instanceof Product - ? 'Articolo salvato nel catalogo come candidato da verificare: ' . ((string) ($product->internal_code ?? '') !== '' ? $product->internal_code . ' · ' : '') . (string) ($product->name ?? '') - : 'Import completato.') + ? 'Articolo salvato nel catalogo come candidato da verificare: ' . ((string) ($product->internal_code ?? '') !== '' ? $product->internal_code . ' · ' : '') . (string) ($product->name ?? '') + : 'Import completato.') ->success() ->send(); diff --git a/app/Filament/Pages/Gescon/FornitoreScheda.php b/app/Filament/Pages/Gescon/FornitoreScheda.php index bbd6b6f..8a3422e 100644 --- a/app/Filament/Pages/Gescon/FornitoreScheda.php +++ b/app/Filament/Pages/Gescon/FornitoreScheda.php @@ -18,8 +18,8 @@ use App\Modules\Contabilita\Models\FatturaFornitore as ContabilitaFatturaFornitore; use App\Modules\Contabilita\Models\PianoConti; use App\Services\Catalog\FornitoreProductCatalogService; -use App\Services\Catalog\ProductHubViewService; use App\Services\Catalog\ProductAssetIngestionService; +use App\Services\Catalog\ProductHubViewService; use App\Services\Catalog\ProductOfferService; use App\Services\Consumi\AcquaPdfTextParser; use App\Services\Consumi\ConsumiAcquaIngestionService; diff --git a/app/Filament/Pages/Mobile/DocumentiMobile.php b/app/Filament/Pages/Mobile/DocumentiMobile.php new file mode 100644 index 0000000..50b5e36 --- /dev/null +++ b/app/Filament/Pages/Mobile/DocumentiMobile.php @@ -0,0 +1,33 @@ +hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + } +} diff --git a/app/Filament/Pages/Strumenti/DocumentiArchivio.php b/app/Filament/Pages/Strumenti/DocumentiArchivio.php index f04a2fe..7f11913 100644 --- a/app/Filament/Pages/Strumenti/DocumentiArchivio.php +++ b/app/Filament/Pages/Strumenti/DocumentiArchivio.php @@ -744,7 +744,7 @@ public function getGenericLabelRowsProperty(): array ->get() ->filter(fn(DocumentoArchivioFisicoItem $item): bool => (bool) data_get($item->metadati, 'generic_label', false)) ->map(function (DocumentoArchivioFisicoItem $item): array { - $printOptions = $this->buildGenericLabelPrintOptions((int) $item->id); + $printOptions = $this->buildGenericLabelPrintOptions((int) $item->id); $defaultPrintOption = ($item->modello_etichetta ?: '11354') === '99014' ? '99014' : '11354'; return [ @@ -777,7 +777,7 @@ private function buildGenericLabelPrintOptions(int $itemId): array $baseUrl = route('filament.archivio-fisico.etichetta', ['item' => $itemId]); return [ - '11354' => [ + '11354' => [ 'label' => 'DYMO 11354 orizzontale', 'url' => $baseUrl . '?format=11354&autoprint=1&dymo_fix=0&dymo_rot=0&dymo_dx=0&dymo_dy=-1', ], @@ -785,7 +785,7 @@ private function buildGenericLabelPrintOptions(int $itemId): array 'label' => 'DYMO 11354 verticale', 'url' => $baseUrl . '?format=11354&autoprint=1&dymo_fix=1&dymo_rot=90&dymo_dx=0&dymo_dy=0', ], - '99014' => [ + '99014' => [ 'label' => 'DYMO 99014 larga', 'url' => $baseUrl . '?format=99014&autoprint=1&dymo_fix=0&dymo_rot=0&dymo_dx=3.5&dymo_dy=3.5', ], @@ -836,7 +836,7 @@ public function getGenericLabelTemplateOptionsProperty(): array public function getGenericLabelTemplateFieldOptionsProperty(): array { return [ - 'mnemonic_code' => 'Codice mnemonico', + 'mnemonic_code' => 'Codice mnemonico', 'titolo' => 'Titolo etichetta', 'linea_secondaria' => 'Seconda riga', 'tipo_label' => 'Tipologia contenitore', @@ -862,11 +862,33 @@ public function getGenericLabelStableFieldOptionsProperty(): array 'stabile_code' => 'Codice stabile', 'stabile_address' => 'Indirizzo stabile', 'stabile_summary' => 'Stabile compatto', - 'blank_line' => 'Riga bianca scrivibile', 'none' => 'Nessuna riga', ]; } + /** @return array */ + public function getGenericLabelLineModeOptionsProperty(): array + { + return [ + 'source' => 'Campo archivio', + 'text' => 'Testo libero', + 'blank_line' => 'Riga bianca scrivibile', + 'none' => 'Nessuna riga', + ]; + } + + /** @return array */ + public function getGenericLabelFontSizeOptionsProperty(): array + { + return [ + 'xs' => 'Extra piccolo', + 'sm' => 'Piccolo', + 'md' => 'Medio', + 'lg' => 'Grande', + 'xl' => 'Extra grande', + ]; + } + /** @return array */ public function getGenericLabelPreviewProperty(): array { @@ -878,10 +900,10 @@ public function getGenericLabelPreviewProperty(): array $mnemonicCode = (string) ($this->mnemonicCodeHint ?? 'GER00079'); } - $stabileCode = ''; + $stabileCode = ''; $stabileAddress = ''; if ($stabile instanceof Stabile) { - $stabileCode = trim((string) ($stabile->codice_stabile ?? '')); + $stabileCode = trim((string) ($stabile->codice_stabile ?? '')); $stabileAddress = trim((string) ($stabile->indirizzo_completo ?? '')); } @@ -925,14 +947,7 @@ public function getGenericLabelPreviewProperty(): array 'amazon_url' => $amazonUrl, ]; - $stableLineOneSource = (string) ($this->genericLabelForm['stable_line_one_source'] ?? 'stabile_summary'); - $stableLineTwoSource = (string) ($this->genericLabelForm['stable_line_two_source'] ?? 'blank_line'); - $blankLinesCount = max(0, min(4, (int) ($this->genericLabelForm['blank_lines_count'] ?? 2))); - - $stableLines = [ - $this->resolveGenericLabelStableLineValue($stableLineOneSource, $payload), - $this->resolveGenericLabelStableLineValue($stableLineTwoSource, $payload), - ]; + $stableLines = $this->buildGenericLabelLineRows($payload); return [ 'template_name' => $templateName, @@ -948,8 +963,15 @@ public function getGenericLabelPreviewProperty(): array ], 'payload' => $payload, 'mnemonic_code' => $mnemonicCode, + 'font_sizes' => [ + 'mnemonic' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['mnemonic_font_size'] ?? 'xl')), + 'title' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['title_font_size'] ?? 'xl')), + 'subtitle' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['subtitle_font_size'] ?? 'md')), + 'lines' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['lines_font_size'] ?? 'md')), + 'note' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['note_font_size'] ?? 'sm')), + ], 'stable_lines' => $stableLines, - 'blank_lines' => array_fill(0, $blankLinesCount, ''), + 'blank_lines' => [], 'amazon_url' => $amazonUrl, ]; } @@ -1284,18 +1306,24 @@ public function saveGenericArchiveLabel(): void 'linea_secondaria' => $this->normalizeNullableText($this->genericLabelForm['linea_secondaria'] ?? null), 'amazon_url' => $amazonUrl, 'generic_label_template' => [ - 'preset_key' => $this->normalizeNullableText($this->genericLabelForm['preset_key'] ?? null), - 'template_name' => $this->normalizeNullableText($this->genericLabelForm['template_name'] ?? null) ?: 'classic-right', - 'mnemonic_code' => $this->normalizeNullableText($this->genericLabelForm['mnemonic_code'] ?? null) ?: $this->mnemonicCodeHint, - 'title_source' => $this->normalizeNullableText($this->genericLabelForm['title_source'] ?? null) ?: 'titolo', - 'subtitle_source' => $this->normalizeNullableText($this->genericLabelForm['subtitle_source'] ?? null) ?: 'linea_secondaria', - 'meta_source' => $this->normalizeNullableText($this->genericLabelForm['meta_source'] ?? null) ?: 'percorso_compatto', - 'note_source' => $this->normalizeNullableText($this->genericLabelForm['note_source'] ?? null) ?: 'note', + 'preset_key' => $this->normalizeNullableText($this->genericLabelForm['preset_key'] ?? null), + 'template_name' => $this->normalizeNullableText($this->genericLabelForm['template_name'] ?? null) ?: 'classic-right', + 'mnemonic_code' => $this->normalizeNullableText($this->genericLabelForm['mnemonic_code'] ?? null) ?: $this->mnemonicCodeHint, + 'title_source' => $this->normalizeNullableText($this->genericLabelForm['title_source'] ?? null) ?: 'titolo', + 'subtitle_source' => $this->normalizeNullableText($this->genericLabelForm['subtitle_source'] ?? null) ?: 'linea_secondaria', + 'meta_source' => $this->normalizeNullableText($this->genericLabelForm['meta_source'] ?? null) ?: 'percorso_compatto', + 'note_source' => $this->normalizeNullableText($this->genericLabelForm['note_source'] ?? null) ?: 'note', 'stable_line_one_source' => $this->normalizeNullableText($this->genericLabelForm['stable_line_one_source'] ?? null) ?: 'stabile_summary', 'stable_line_two_source' => $this->normalizeNullableText($this->genericLabelForm['stable_line_two_source'] ?? null) ?: 'blank_line', - 'blank_lines_count' => max(0, min(4, (int) ($this->genericLabelForm['blank_lines_count'] ?? 2))), - 'qr_position' => $this->normalizeNullableText($this->genericLabelForm['qr_position'] ?? null) ?: 'right-center', - 'qr_scale' => $this->normalizeNullableText($this->genericLabelForm['qr_scale'] ?? null) ?: 'md', + 'blank_lines_count' => max(0, min(4, (int) ($this->genericLabelForm['blank_lines_count'] ?? 2))), + 'qr_position' => $this->normalizeNullableText($this->genericLabelForm['qr_position'] ?? null) ?: 'right-center', + 'qr_scale' => $this->normalizeNullableText($this->genericLabelForm['qr_scale'] ?? null) ?: 'md', + 'mnemonic_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['mnemonic_font_size'] ?? 'xl')), + 'title_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['title_font_size'] ?? 'xl')), + 'subtitle_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['subtitle_font_size'] ?? 'md')), + 'lines_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['lines_font_size'] ?? 'md')), + 'note_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['note_font_size'] ?? 'sm')), + 'line_rows' => $this->serializeGenericLabelLineRows(), ], ], ]); @@ -1407,19 +1435,42 @@ public function movePhysicalArchiveItem(): void return; } - $source->forceFill([ - 'parent_id' => (int) $target->id, - 'updated_by' => Auth::id(), - ])->save(); + $supportoFisico = $this->normalizeNullableText($this->physicalArchiveMoveForm['supporto_fisico'] ?? null); + $magazzino = $this->normalizeNullableText($this->physicalArchiveMoveForm['magazzino'] ?? null) ?? $target->magazzino ?? $source->magazzino; + $scaffale = $this->normalizeNullableText($this->physicalArchiveMoveForm['scaffale'] ?? null) ?? $target->scaffale ?? $source->scaffale; + $ripiano = $this->normalizeNullableText($this->physicalArchiveMoveForm['ripiano'] ?? null) ?? $target->ripiano ?? $source->ripiano; + $ubicazioneDettaglio = $this->normalizeNullableText($this->physicalArchiveMoveForm['ubicazione_dettaglio'] ?? null) ?? $target->ubicazione_dettaglio ?? $source->ubicazione_dettaglio; + + $payload = [ + 'parent_id' => (int) $target->id, + 'updated_by' => Auth::id(), + 'magazzino' => $magazzino, + 'scaffale' => $scaffale, + 'ripiano' => $ripiano, + 'ubicazione_dettaglio' => $ubicazioneDettaglio, + ]; + + if ($supportoFisico !== null) { + $payload['supporto_fisico'] = $supportoFisico; + } + + $source->forceFill($payload)->save(); + + $ubicazione = collect([$magazzino, $scaffale, $ripiano, $ubicazioneDettaglio])->filter()->implode(' · '); $this->physicalArchiveMoveForm = [ - 'source_code' => '', - 'target_code' => '', + 'source_code' => '', + 'target_code' => '', + 'supporto_fisico' => '', + 'magazzino' => '', + 'scaffale' => '', + 'ripiano' => '', + 'ubicazione_dettaglio' => '', ]; Notification::make() ->title('Movimentazione registrata') - ->body($source->codice_univoco . ' → ' . $target->codice_univoco) + ->body(trim($source->codice_univoco . ' → ' . $target->codice_univoco . ($ubicazione !== '' ? ' · ' . $ubicazione : ''))) ->success() ->send(); } @@ -1854,38 +1905,60 @@ private function initializePhysicalArchiveForms(): void ]; $this->physicalArchiveMoveForm = [ - 'source_code' => '', - 'target_code' => '', + 'source_code' => '', + 'target_code' => '', + 'supporto_fisico' => '', + 'magazzino' => '', + 'scaffale' => '', + 'ripiano' => '', + 'ubicazione_dettaglio' => '', ]; } private function initializeGenericLabelForm(): void { $this->genericLabelForm = [ - 'preset_key' => '', - 'mnemonic_code' => (string) ($this->mnemonicCodeHint ?? 'GER00079'), - 'titolo' => '', - 'linea_secondaria' => '', - 'note' => '', - 'tipo_item' => 'scatola', - 'supporto_fisico' => 'Scatola', - 'parent_id' => null, - 'magazzino' => '', - 'scaffale' => '', - 'ripiano' => '', - 'ubicazione_dettaglio' => '', - 'modello_etichetta' => '11354', - 'amazon_url' => '', - 'template_name' => 'archive-99014', - 'title_source' => 'titolo', - 'subtitle_source' => 'linea_secondaria', - 'meta_source' => 'percorso_compatto', - 'note_source' => 'note', + 'preset_key' => '', + 'mnemonic_code' => (string) ($this->mnemonicCodeHint ?? 'GER00079'), + 'titolo' => '', + 'linea_secondaria' => '', + 'note' => '', + 'tipo_item' => 'scatola', + 'supporto_fisico' => 'Scatola', + 'parent_id' => null, + 'magazzino' => '', + 'scaffale' => '', + 'ripiano' => '', + 'ubicazione_dettaglio' => '', + 'modello_etichetta' => '11354', + 'amazon_url' => '', + 'template_name' => 'archive-99014', + 'title_source' => 'titolo', + 'subtitle_source' => 'linea_secondaria', + 'meta_source' => 'percorso_compatto', + 'note_source' => 'note', 'stable_line_one_source' => 'stabile_summary', 'stable_line_two_source' => 'blank_line', - 'blank_lines_count' => 2, - 'qr_position' => 'bottom-right', - 'qr_scale' => 'lg', + 'blank_lines_count' => 2, + 'qr_position' => 'bottom-right', + 'qr_scale' => 'lg', + 'line_1_mode' => 'source', + 'line_1_source' => 'stabile_summary', + 'line_1_text' => '', + 'line_2_mode' => 'blank_line', + 'line_2_source' => 'none', + 'line_2_text' => '', + 'line_3_mode' => 'blank_line', + 'line_3_source' => 'none', + 'line_3_text' => '', + 'line_4_mode' => 'none', + 'line_4_source' => 'none', + 'line_4_text' => '', + 'mnemonic_font_size' => 'xl', + 'title_font_size' => 'xl', + 'subtitle_font_size' => 'md', + 'lines_font_size' => 'md', + 'note_font_size' => 'sm', ]; $this->genericLabelTemplateLabel = ''; @@ -2045,24 +2118,39 @@ public function applyGenericLabelPreset(string $presetKey): void } $this->genericLabelForm = array_merge($this->genericLabelForm, [ - 'preset_key' => $presetKey, - 'mnemonic_code' => (string) ($preset['mnemonic_code'] ?? ($this->mnemonicCodeHint ?? 'GER00079')), - 'titolo' => (string) ($preset['titolo'] ?? ''), - 'linea_secondaria' => (string) ($preset['linea_secondaria'] ?? ''), - 'tipo_item' => (string) ($preset['tipo_item'] ?? 'scatola'), - 'supporto_fisico' => (string) ($preset['supporto_fisico'] ?? ''), - 'modello_etichetta' => (string) ($preset['modello_etichetta'] ?? '11354'), - 'template_name' => (string) ($preset['template_name'] ?? 'classic-right'), - 'title_source' => (string) data_get($preset, 'sources.title', 'titolo'), - 'subtitle_source' => (string) data_get($preset, 'sources.subtitle', 'linea_secondaria'), - 'meta_source' => (string) data_get($preset, 'sources.meta', 'percorso_compatto'), - 'note_source' => (string) data_get($preset, 'sources.note', 'note'), + 'preset_key' => $presetKey, + 'mnemonic_code' => (string) ($preset['mnemonic_code'] ?? ($this->mnemonicCodeHint ?? 'GER00079')), + 'titolo' => (string) ($preset['titolo'] ?? ''), + 'linea_secondaria' => (string) ($preset['linea_secondaria'] ?? ''), + 'tipo_item' => (string) ($preset['tipo_item'] ?? 'scatola'), + 'supporto_fisico' => (string) ($preset['supporto_fisico'] ?? ''), + 'modello_etichetta' => (string) ($preset['modello_etichetta'] ?? '11354'), + 'template_name' => (string) ($preset['template_name'] ?? 'classic-right'), + 'title_source' => (string) data_get($preset, 'sources.title', 'titolo'), + 'subtitle_source' => (string) data_get($preset, 'sources.subtitle', 'linea_secondaria'), + 'meta_source' => (string) data_get($preset, 'sources.meta', 'percorso_compatto'), + 'note_source' => (string) data_get($preset, 'sources.note', 'note'), 'stable_line_one_source' => (string) ($preset['stable_line_one_source'] ?? 'stabile_summary'), 'stable_line_two_source' => (string) ($preset['stable_line_two_source'] ?? 'blank_line'), - 'blank_lines_count' => max(0, min(4, (int) ($preset['blank_lines_count'] ?? 2))), - 'qr_position' => (string) ($preset['qr_position'] ?? 'right-center'), - 'qr_scale' => (string) ($preset['qr_scale'] ?? 'md'), + 'blank_lines_count' => max(0, min(4, (int) ($preset['blank_lines_count'] ?? 2))), + 'qr_position' => (string) ($preset['qr_position'] ?? 'right-center'), + 'qr_scale' => (string) ($preset['qr_scale'] ?? 'md'), + 'mnemonic_font_size' => $this->normalizeGenericLabelFontSize((string) ($preset['mnemonic_font_size'] ?? 'xl')), + 'title_font_size' => $this->normalizeGenericLabelFontSize((string) ($preset['title_font_size'] ?? 'xl')), + 'subtitle_font_size' => $this->normalizeGenericLabelFontSize((string) ($preset['subtitle_font_size'] ?? 'md')), + 'lines_font_size' => $this->normalizeGenericLabelFontSize((string) ($preset['lines_font_size'] ?? 'md')), + 'note_font_size' => $this->normalizeGenericLabelFontSize((string) ($preset['note_font_size'] ?? 'sm')), ]); + + $lineRows = is_array($preset['line_rows'] ?? null) ? $preset['line_rows'] : []; + + for ($index = 1; $index <= 4; $index++) { + $row = $lineRows[$index - 1] ?? $this->getLegacyGenericLabelLineRow($index, $preset); + + $this->genericLabelForm['line_' . $index . '_mode'] = $this->normalizeGenericLabelLineMode((string) ($row['mode'] ?? 'none')); + $this->genericLabelForm['line_' . $index . '_source'] = (string) ($row['source'] ?? 'none'); + $this->genericLabelForm['line_' . $index . '_text'] = (string) ($row['text'] ?? ''); + } } public function applySelectedGenericLabelPreset(): void @@ -2254,20 +2342,20 @@ private function getBuiltInGenericLabelPresetDefinitions(): array ], ], 'faldone-archivio-99014' => [ - 'label' => 'Faldone archivio 99014', - 'mnemonic_code' => (string) ($this->mnemonicCodeHint ?? 'GER00079'), - 'titolo' => 'FALDONE ARCHIVIO', - 'linea_secondaria' => 'Contenitore operativo con note manuali', - 'tipo_item' => 'faldone_anelli', - 'supporto_fisico' => 'Faldone con anelli', - 'modello_etichetta' => '99014', - 'template_name' => 'archive-99014', + 'label' => 'Faldone archivio 99014', + 'mnemonic_code' => (string) ($this->mnemonicCodeHint ?? 'GER00079'), + 'titolo' => 'FALDONE ARCHIVIO', + 'linea_secondaria' => 'Contenitore operativo con note manuali', + 'tipo_item' => 'faldone_anelli', + 'supporto_fisico' => 'Faldone con anelli', + 'modello_etichetta' => '99014', + 'template_name' => 'archive-99014', 'stable_line_one_source' => 'stabile_summary', 'stable_line_two_source' => 'blank_line', - 'blank_lines_count' => 2, - 'qr_position' => 'bottom-right', - 'qr_scale' => 'lg', - 'sources' => [ + 'blank_lines_count' => 2, + 'qr_position' => 'bottom-right', + 'qr_scale' => 'lg', + 'sources' => [ 'title' => 'titolo', 'subtitle' => 'linea_secondaria', 'meta' => 'stabile_summary', @@ -2316,20 +2404,26 @@ private function makeGenericLabelTemplateKey(string $label, array $stored): stri private function buildGenericLabelTemplatePayload(string $label): array { return [ - 'label' => $label, - 'mnemonic_code' => trim((string) ($this->genericLabelForm['mnemonic_code'] ?? '')), - 'titolo' => trim((string) ($this->genericLabelForm['titolo'] ?? '')), - 'linea_secondaria' => trim((string) ($this->genericLabelForm['linea_secondaria'] ?? '')), - 'tipo_item' => (string) ($this->genericLabelForm['tipo_item'] ?? 'scatola'), - 'supporto_fisico' => trim((string) ($this->genericLabelForm['supporto_fisico'] ?? '')), - 'modello_etichetta' => (string) ($this->genericLabelForm['modello_etichetta'] ?? '11354'), - 'template_name' => (string) ($this->genericLabelForm['template_name'] ?? 'classic-right'), + 'label' => $label, + 'mnemonic_code' => trim((string) ($this->genericLabelForm['mnemonic_code'] ?? '')), + 'titolo' => trim((string) ($this->genericLabelForm['titolo'] ?? '')), + 'linea_secondaria' => trim((string) ($this->genericLabelForm['linea_secondaria'] ?? '')), + 'tipo_item' => (string) ($this->genericLabelForm['tipo_item'] ?? 'scatola'), + 'supporto_fisico' => trim((string) ($this->genericLabelForm['supporto_fisico'] ?? '')), + 'modello_etichetta' => (string) ($this->genericLabelForm['modello_etichetta'] ?? '11354'), + 'template_name' => (string) ($this->genericLabelForm['template_name'] ?? 'classic-right'), 'stable_line_one_source' => (string) ($this->genericLabelForm['stable_line_one_source'] ?? 'stabile_summary'), 'stable_line_two_source' => (string) ($this->genericLabelForm['stable_line_two_source'] ?? 'blank_line'), - 'blank_lines_count' => max(0, min(4, (int) ($this->genericLabelForm['blank_lines_count'] ?? 2))), - 'qr_position' => (string) ($this->genericLabelForm['qr_position'] ?? 'right-center'), - 'qr_scale' => (string) ($this->genericLabelForm['qr_scale'] ?? 'md'), - 'sources' => [ + 'blank_lines_count' => max(0, min(4, (int) ($this->genericLabelForm['blank_lines_count'] ?? 2))), + 'qr_position' => (string) ($this->genericLabelForm['qr_position'] ?? 'right-center'), + 'qr_scale' => (string) ($this->genericLabelForm['qr_scale'] ?? 'md'), + 'mnemonic_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['mnemonic_font_size'] ?? 'xl')), + 'title_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['title_font_size'] ?? 'xl')), + 'subtitle_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['subtitle_font_size'] ?? 'md')), + 'lines_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['lines_font_size'] ?? 'md')), + 'note_font_size' => $this->normalizeGenericLabelFontSize((string) ($this->genericLabelForm['note_font_size'] ?? 'sm')), + 'line_rows' => $this->serializeGenericLabelLineRows(), + 'sources' => [ 'title' => (string) ($this->genericLabelForm['title_source'] ?? 'titolo'), 'subtitle' => (string) ($this->genericLabelForm['subtitle_source'] ?? 'linea_secondaria'), 'meta' => (string) ($this->genericLabelForm['meta_source'] ?? 'percorso_compatto'), @@ -2376,6 +2470,92 @@ private function resolveGenericLabelStableLineValue(string $fieldKey, array $pay return trim((string) ($payload[$fieldKey] ?? '')); } + /** @param array $payload + * @return array + */ + private function buildGenericLabelLineRows(array $payload): array + { + $rows = []; + + for ($index = 1; $index <= 4; $index++) { + $mode = $this->normalizeGenericLabelLineMode((string) ($this->genericLabelForm['line_' . $index . '_mode'] ?? 'none')); + + if ($mode === 'blank_line') { + $rows[] = '__BLANK_LINE__'; + continue; + } + + if ($mode === 'text') { + $rows[] = trim((string) ($this->genericLabelForm['line_' . $index . '_text'] ?? '')); + continue; + } + + if ($mode === 'source') { + $rows[] = $this->resolveGenericLabelStableLineValue((string) ($this->genericLabelForm['line_' . $index . '_source'] ?? 'none'), $payload); + } + } + + return $rows; + } + + /** @return array> */ + private function serializeGenericLabelLineRows(): array + { + $rows = []; + + for ($index = 1; $index <= 4; $index++) { + $rows[] = [ + 'mode' => $this->normalizeGenericLabelLineMode((string) ($this->genericLabelForm['line_' . $index . '_mode'] ?? 'none')), + 'source' => (string) ($this->genericLabelForm['line_' . $index . '_source'] ?? 'none'), + 'text' => trim((string) ($this->genericLabelForm['line_' . $index . '_text'] ?? '')), + ]; + } + + return $rows; + } + + /** @param array $preset + * @return array + */ + private function getLegacyGenericLabelLineRow(int $index, array $preset): array + { + if ($index === 1) { + return [ + 'mode' => 'source', + 'source' => (string) ($preset['stable_line_one_source'] ?? 'stabile_summary'), + 'text' => '', + ]; + } + + if ($index === 2) { + $source = (string) ($preset['stable_line_two_source'] ?? 'blank_line'); + + return [ + 'mode' => $source === 'blank_line' ? 'blank_line' : 'source', + 'source' => $source, + 'text' => '', + ]; + } + + $blankCount = max(0, min(4, (int) ($preset['blank_lines_count'] ?? 0))); + + return [ + 'mode' => $index <= ($blankCount + 2) ? 'blank_line' : 'none', + 'source' => 'none', + 'text' => '', + ]; + } + + private function normalizeGenericLabelLineMode(string $mode): string + { + return in_array($mode, ['source', 'text', 'blank_line', 'none'], true) ? $mode : 'none'; + } + + private function normalizeGenericLabelFontSize(string $size): string + { + return in_array($size, ['xs', 'sm', 'md', 'lg', 'xl'], true) ? $size : 'md'; + } + private function getDefaultStabileId(): ?int { $user = Auth::user(); diff --git a/app/Filament/Pages/Strumenti/PostIt.php b/app/Filament/Pages/Strumenti/PostIt.php index bc43669..63e6373 100644 --- a/app/Filament/Pages/Strumenti/PostIt.php +++ b/app/Filament/Pages/Strumenti/PostIt.php @@ -51,7 +51,7 @@ class PostIt extends Page public ?int $assegnazioneUserId = null; public ?int $assegnazioneFornitoreId = null; - public ?int $selectedSuggerimentoId = null; + public ?int $selectedSuggerimentoId = null; public bool $disableAutoStabileAssociation = false; /** @var \Illuminate\Support\Collection */ @@ -407,26 +407,26 @@ public function getRecentiProperty() ->groupBy(fn(ChiamataPostIt $item): string => $this->buildRecenteGroupKey($item)) ->map(function (Collection $group): array { /** @var ChiamataPostIt $primary */ - $primary = $group->first(); + $primary = $group->first(); $callerLabel = $this->resolveRecenteCallerLabel($group); - $phoneLabel = $this->resolveRecentePhoneLabel($group); + $phoneLabel = $this->resolveRecentePhoneLabel($group); return [ - 'primary' => $primary, - 'items' => $group->values(), - 'count' => $group->count(), - 'caller_label' => $callerLabel, - 'phone' => $phoneLabel, - 'latest_at' => optional($primary->chiamata_il)->format('d/m/Y H:i'), + 'primary' => $primary, + 'items' => $group->values(), + 'count' => $group->count(), + 'caller_label' => $callerLabel, + 'phone' => $phoneLabel, + 'latest_at' => optional($primary->chiamata_il)->format('d/m/Y H:i'), 'contains_selected' => $this->selectedPostItId - ? $group->contains(fn(ChiamataPostIt $item): bool => (int) $item->id === (int) $this->selectedPostItId) + ? $group->contains(fn(ChiamataPostIt $item) : bool => (int) $item->id === (int) $this->selectedPostItId) : false, ]; }) ->values(); if (! $this->selectedPostItId && $grouped->isNotEmpty()) { - $first = $grouped->first(); + $first = $grouped->first(); $this->selectedPostItId = (int) ($this->focusPostItId ?: ($first['primary']->id ?? 0)); $this->loadSelectedPostItAssignment(); } @@ -550,13 +550,13 @@ private function createPostItRecord(): ChiamataPostIt private function applyQueryPrefill(): void { - $source = trim((string) request()->query('source', '')); + $source = trim((string) request()->query('source', '')); $this->disableAutoStabileAssociation = request()->boolean('lock_stabile') || $source === 'topbar_live_call'; - $telefono = trim((string) request()->query('telefono', '')); - $nome = trim((string) request()->query('nome', '')); - $nota = trim((string) request()->query('nota', '')); - $oggetto = trim((string) request()->query('oggetto', '')); + $telefono = trim((string) request()->query('telefono', '')); + $nome = trim((string) request()->query('nome', '')); + $nota = trim((string) request()->query('nota', '')); + $oggetto = trim((string) request()->query('oggetto', '')); $direzione = trim((string) request()->query('direzione', '')); $rubricaId = (int) request()->query('rubrica_id', 0); @@ -564,7 +564,7 @@ private function applyQueryPrefill(): void $this->telefono = $telefono; } if ($nome !== '') { - $this->nomeChiamante = $nome; + $this->nomeChiamante = $nome; $this->ricercaChiamante = $nome; } if ($nota !== '') { @@ -637,25 +637,25 @@ private function createRubricaDraftFromPhone(string $phone, string $name = ''): } $normalizedPhone = PhoneNumber::toE164Italy($phone) ?? $digits; - $adminId = $this->resolveCurrentAmministratoreId(); - $cleanName = trim($name); + $adminId = $this->resolveCurrentAmministratoreId(); + $cleanName = trim($name); $payload = [ - 'amministratore_id' => $adminId > 0 ? $adminId : null, - 'tipo_contatto' => str_contains($cleanName, ' ') ? 'persona_fisica' : 'persona_giuridica', - 'telefono_cellulare' => $normalizedPhone, - 'categoria' => 'altro', - 'stato' => 'attivo', - 'data_inserimento' => now()->toDateString(), + 'amministratore_id' => $adminId > 0 ? $adminId : null, + 'tipo_contatto' => str_contains($cleanName, ' ') ? 'persona_fisica' : 'persona_giuridica', + 'telefono_cellulare' => $normalizedPhone, + 'categoria' => 'altro', + 'stato' => 'attivo', + 'data_inserimento' => now()->toDateString(), 'data_ultima_modifica' => now()->toDateString(), - 'creato_da' => Auth::id(), - 'modificato_da' => Auth::id(), - 'note_segreteria' => 'Bozza creata dal box Post-it in topbar per una chiamata in ingresso.', + 'creato_da' => Auth::id(), + 'modificato_da' => Auth::id(), + 'note_segreteria' => 'Bozza creata dal box Post-it in topbar per una chiamata in ingresso.', ]; if ($cleanName !== '' && str_contains($cleanName, ' ')) { - $parts = preg_split('/\s+/', $cleanName) ?: []; - $payload['nome'] = array_shift($parts) ?: null; + $parts = preg_split('/\s+/', $cleanName) ?: []; + $payload['nome'] = array_shift($parts) ?: null; $payload['cognome'] = $parts !== [] ? implode(' ', $parts) : null; } elseif ($cleanName !== '') { $payload['ragione_sociale'] = $cleanName; diff --git a/app/Filament/Pages/Strumenti/PostItGestione.php b/app/Filament/Pages/Strumenti/PostItGestione.php index c8523b6..d7e94a0 100644 --- a/app/Filament/Pages/Strumenti/PostItGestione.php +++ b/app/Filament/Pages/Strumenti/PostItGestione.php @@ -11,6 +11,7 @@ use App\Models\User; use App\Services\Anagrafiche\RubricaPhoneLookupService; use App\Services\Cti\PbxRoutingService; +use App\Support\StabileContext; use BackedEnum; use Filament\Notifications\Notification; use Filament\Pages\Page; @@ -18,7 +19,6 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Schema; use Illuminate\Support\Str; -use App\Support\StabileContext; use UnitEnum; class PostItGestione extends Page @@ -341,7 +341,7 @@ public function assegnaSelectedAppunto(): void if ($this->appuntoAssegnazioneTipo === 'fornitore') { $ticketPayload['assegnato_a_fornitore_id'] = $this->appuntoAssegnazioneFornitoreId; - $ticketPayload['assegnato_a_user_id'] = null; + $ticketPayload['assegnato_a_user_id'] = null; } else { $ticketPayload['assegnato_a_user_id'] = $this->appuntoAssegnazioneUserId; } @@ -535,9 +535,9 @@ public function getSelectedAppuntoProperty(): ?ChiamataPostIt public function isPostItAssignmentReady(): bool { return $this->isPostItTableReady() - && Schema::hasColumn('chiamate_post_it', 'assegnazione_tipo') - && Schema::hasColumn('chiamate_post_it', 'assegnato_a_user_id') - && Schema::hasColumn('chiamate_post_it', 'assegnato_a_fornitore_id'); + && Schema::hasColumn('chiamate_post_it', 'assegnazione_tipo') + && Schema::hasColumn('chiamate_post_it', 'assegnato_a_user_id') + && Schema::hasColumn('chiamate_post_it', 'assegnato_a_fornitore_id'); } public function getOperatoriOptionsProperty(): array @@ -1098,20 +1098,20 @@ private function loadSelectedAppuntoState(): void { $postIt = $this->selectedAppunto; if (! $postIt) { - $this->selectedAppuntoOggetto = null; - $this->selectedAppuntoNota = null; - $this->appuntoAssegnazioneTipo = 'operatore'; - $this->appuntoAssegnazioneUserId = null; + $this->selectedAppuntoOggetto = null; + $this->selectedAppuntoNota = null; + $this->appuntoAssegnazioneTipo = 'operatore'; + $this->appuntoAssegnazioneUserId = null; $this->appuntoAssegnazioneFornitoreId = null; return; } - $this->selectedAppuntoOggetto = $postIt->oggetto; - $this->selectedAppuntoNota = $postIt->nota; + $this->selectedAppuntoOggetto = $postIt->oggetto; + $this->selectedAppuntoNota = $postIt->nota; $this->appuntoAssegnazioneTipo = in_array((string) ($postIt->assegnazione_tipo ?? ''), ['operatore', 'collaboratore', 'fornitore'], true) ? (string) $postIt->assegnazione_tipo : 'operatore'; - $this->appuntoAssegnazioneUserId = isset($postIt->assegnato_a_user_id) ? (int) $postIt->assegnato_a_user_id : null; + $this->appuntoAssegnazioneUserId = isset($postIt->assegnato_a_user_id) ? (int) $postIt->assegnato_a_user_id : null; $this->appuntoAssegnazioneFornitoreId = isset($postIt->assegnato_a_fornitore_id) ? (int) $postIt->assegnato_a_fornitore_id : null; } diff --git a/app/Filament/Pages/Supporto/Modifiche.php b/app/Filament/Pages/Supporto/Modifiche.php index 6fd5a87..4d8173d 100644 --- a/app/Filament/Pages/Supporto/Modifiche.php +++ b/app/Filament/Pages/Supporto/Modifiche.php @@ -1783,10 +1783,27 @@ private function startNodeRefreshJob(): void '--progress-file=' . $progressPath, ]; + $adminId = $this->resolveAmministratoreId(); + $requireDrive = $this->shouldRequireDrivePreupdateBackup(); + $driveEnabled = $this->shouldAttemptDrivePreupdateBackup(); + if ($this->gitForce) { $args[] = '--force'; } + if ($this->updateSkipBackup) { + $args[] = '--skip-backup'; + } + + if ($driveEnabled && $adminId > 0) { + $args[] = '--drive'; + $args[] = '--admin-id=' . $adminId; + } + + if ($requireDrive) { + $args[] = '--require-drive'; + } + $escaped = implode(' ', array_map(static fn(string $arg): string => escapeshellarg($arg), $args)); $shell = 'nohup ' . $escaped . ' > ' . escapeshellarg($logPath) . ' 2>&1 & echo $!'; $started = Process::path(base_path())->run(['bash', '-lc', $shell]); diff --git a/app/Filament/Pages/Supporto/TicketAcqua.php b/app/Filament/Pages/Supporto/TicketAcqua.php index fbfd9c9..7474623 100644 --- a/app/Filament/Pages/Supporto/TicketAcqua.php +++ b/app/Filament/Pages/Supporto/TicketAcqua.php @@ -686,14 +686,14 @@ private function appendPendingWaterLibraryPhotos(): void private function resetWaterDraftUploadState(): void { - $this->newWaterPhotos = []; - $this->pendingWaterPhotos = []; + $this->newWaterPhotos = []; + $this->pendingWaterPhotos = []; $this->pendingWaterLibraryPhotos = []; - $this->waterPhotoDescriptions = []; - $this->waterUploadCodes = []; - $this->waterClientMetadata = []; - $this->waterDraftProtocol = 'TA-' . now()->format('Ymd-His'); - $this->waterDraftSequence = 1; + $this->waterPhotoDescriptions = []; + $this->waterUploadCodes = []; + $this->waterClientMetadata = []; + $this->waterDraftProtocol = 'TA-' . now()->format('Ymd-His'); + $this->waterDraftSequence = 1; } private function syncWaterDescriptionSlots(): void diff --git a/app/Http/Controllers/Filament/DocumentiPrintController.php b/app/Http/Controllers/Filament/DocumentiPrintController.php index 676fb0d..0daaa88 100644 --- a/app/Http/Controllers/Filament/DocumentiPrintController.php +++ b/app/Http/Controllers/Filament/DocumentiPrintController.php @@ -131,14 +131,9 @@ public function etichetta(Request $request, Documento $documento, DocumentoArchi $faldone = $this->extractFaldoneFromNote((string) ($documento->note ?? '')); } - $anno = $documento->data_documento?->format('Y') ?: now()->format('Y'); - $protocollo = (string) ($documento->numero_protocollo ?: ('DOC-' . $anno . '-' . str_pad((string) $documento->id, 3, '0', STR_PAD_LEFT))); - - $stabileId = (int) ($documento->stabile_id ?: 0); - $codiceUnico = $stabileId > 0 ? ('STB-' . $stabileId . '-' . $protocollo) : $protocollo; - if ($faldone !== '') { - $codiceUnico .= '-F' . Str::upper($faldone); - } + $anno = $documento->data_documento?->format('Y') ?: now()->format('Y'); + $protocollo = (string) ($documento->numero_protocollo ?: ('DOC-' . $anno . '-' . str_pad((string) $documento->id, 3, '0', STR_PAD_LEFT))); + $codiceUnico = $documento->resolveArchiveCode(); $openUrl = route('filament.documenti.download', $documento); diff --git a/app/Livewire/Condomini/StabileDatiBancariTable.php b/app/Livewire/Condomini/StabileDatiBancariTable.php index 489bfbd..0217d1a 100644 --- a/app/Livewire/Condomini/StabileDatiBancariTable.php +++ b/app/Livewire/Condomini/StabileDatiBancariTable.php @@ -195,7 +195,7 @@ public function table(Table $table): Table ->label('Movimenti') ->icon('heroicon-o-receipt-percent') ->url(function (DatiBancari $record): string { - $base = CasseBancheMovimenti::getUrl(panel: 'admin-filament'); + $base = CasseBancheMovimenti::getUrl(panel: 'admin-filament'); $contoId = (int) ($record->id ?? 0); if ($contoId <= 0) { return $base; diff --git a/app/Livewire/Filament/TopbarLiveCall.php b/app/Livewire/Filament/TopbarLiveCall.php index 1a252ab..1eded89 100644 --- a/app/Livewire/Filament/TopbarLiveCall.php +++ b/app/Livewire/Filament/TopbarLiveCall.php @@ -37,13 +37,13 @@ public function createPostIt(): void $this->redirect( PostIt::getUrl([ - 'source' => 'topbar_live_call', - 'telefono' => (string) ($this->liveIncomingCall['phone'] ?? ''), - 'nome' => (string) ($this->liveIncomingCall['rubrica_nome'] ?? ''), - 'rubrica_id' => (int) ($this->liveIncomingCall['rubrica_id'] ?? 0) ?: null, - 'direzione' => 'in_arrivo', - 'oggetto' => 'Chiamata in arrivo da valutare', - 'nota' => (string) ($this->liveIncomingCall['line'] ?? ('Chiamata in ingresso da ' . ($this->liveIncomingCall['phone'] ?? ''))), + 'source' => 'topbar_live_call', + 'telefono' => (string) ($this->liveIncomingCall['phone'] ?? ''), + 'nome' => (string) ($this->liveIncomingCall['rubrica_nome'] ?? ''), + 'rubrica_id' => (int) ($this->liveIncomingCall['rubrica_id'] ?? 0) ?: null, + 'direzione' => 'in_arrivo', + 'oggetto' => 'Chiamata in arrivo da valutare', + 'nota' => (string) ($this->liveIncomingCall['line'] ?? ('Chiamata in ingresso da ' . ($this->liveIncomingCall['phone'] ?? ''))), 'lock_stabile' => 1, ], panel: 'admin-filament'), navigate: true, diff --git a/app/Models/Documento.php b/app/Models/Documento.php index 948c14e..418199b 100755 --- a/app/Models/Documento.php +++ b/app/Models/Documento.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\MorphTo; +use Illuminate\Support\Str; class Documento extends Model { @@ -236,6 +237,53 @@ public function getStatoScadenzaAttribute() return 'valido'; } + /** + * Metodi di utilità + */ + public function resolveArchiveCode(): string + { + $meta = is_array($this->metadati_archivio ?? null) ? $this->metadati_archivio : []; + + foreach ([ + $meta['codice_univoco_documento'] ?? null, + $meta['physical_archive_code'] ?? null, + $this->archive_reference ?? null, + ] as $candidate) { + $value = trim((string) $candidate); + + if ($value !== '') { + return $value; + } + } + + $protocollo = trim((string) ($this->numero_protocollo ?? '')); + if ($protocollo === '') { + $protocollo = 'DOC-' . (int) $this->id; + } + + $stabileId = (int) ($this->stabile_id ?? 0); + $faldone = trim((string) data_get($meta, 'contenitore_numero', data_get($meta, 'faldone', ''))); + $code = $stabileId > 0 ? ('STB-' . $stabileId . '-' . $protocollo) : $protocollo; + + if ($faldone !== '') { + $code .= '-F' . Str::upper($faldone); + } + + return $code; + } + + public function resolveArchiveDisplayReference(): string + { + $code = $this->resolveArchiveCode(); + $reference = trim((string) ($this->archive_reference ?? '')); + + if ($reference === '' || $reference === $code) { + return $code; + } + + return $code . ' · ' . $reference; + } + /** * Metodi di utilità */ diff --git a/app/Models/DocumentoArchivioCartella.php b/app/Models/DocumentoArchivioCartella.php index 02ef94f..f728463 100644 --- a/app/Models/DocumentoArchivioCartella.php +++ b/app/Models/DocumentoArchivioCartella.php @@ -1,5 +1,4 @@ nome ?? '') : implode(' / ', $parts); } -} \ No newline at end of file +} diff --git a/app/Models/DocumentoArchivioFisicoItem.php b/app/Models/DocumentoArchivioFisicoItem.php index 35b9f91..c91b544 100644 --- a/app/Models/DocumentoArchivioFisicoItem.php +++ b/app/Models/DocumentoArchivioFisicoItem.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Facades\URL; +use Illuminate\Support\Str; class DocumentoArchivioFisicoItem extends Model { @@ -49,11 +50,15 @@ class DocumentoArchivioFisicoItem extends Model public const TIPI = [ 'documento' => 'Documento cartaceo', + 'foglio' => 'Foglio / documento singolo', + 'cartellina' => 'Cartellina', 'busta_trasparente' => 'Busta trasparente con anelli da archiviare', 'cartella_tre_lembi' => 'Cartella a tre lembi', 'faldone_anelli' => 'Faldone con anelli', 'faldone_lacci' => 'Faldone con lacci', 'scatola' => 'Scatola', + 'scaffale' => 'Scaffale', + 'posto' => 'Posto / ubicazione finale', ]; public function stabile(): BelongsTo @@ -106,7 +111,7 @@ public function getPercorsoFisicoAttribute(): string public function getQrCodeImageAttribute(): string { - $data = $this->buildPublicQrUrl(); + $data = $this->buildQrPayload(); if ($data !== '' && class_exists(\chillerlan\QRCode\QROptions::class) && class_exists(\chillerlan\QRCode\QRCode::class)) { $options = new \chillerlan\QRCode\QROptions([ @@ -156,10 +161,34 @@ public static function generateCodice(int $stabileId): string public function refreshQrCodeData(): void { $this->forceFill([ - 'qr_code_data' => $this->buildPublicQrUrl(), + 'qr_code_data' => $this->buildQrPayload(), ])->saveQuietly(); } + public function buildQrPayload(): string + { + $parentCode = trim((string) ($this->parent?->codice_univoco ?? 'RADICE')); + $documentCode = $this->documento instanceof Documento + ? trim($this->documento->resolveArchiveCode()) + : ''; + $location = collect([ + trim((string) ($this->magazzino ?? '')), + trim((string) ($this->scaffale ?? '')), + trim((string) ($this->ripiano ?? '')), + trim((string) ($this->ubicazione_dettaglio ?? '')), + ])->filter()->implode('/'); + + return implode('|', array_filter([ + 'NGDOC', + 'AF:' . trim((string) $this->codice_univoco), + 'TIPO:' . Str::upper((string) $this->tipo_item), + 'STB:' . (int) $this->stabile_id, + 'PARENT:' . ($parentCode !== '' ? $parentCode : 'RADICE'), + $documentCode !== '' ? 'DOC:' . $documentCode : null, + $location !== '' ? 'LOC:' . $location : null, + ])); + } + public function buildPublicQrUrl(): string { $relativeSignedPath = URL::temporarySignedRoute( diff --git a/app/Models/Fornitore.php b/app/Models/Fornitore.php index e22d421..5a4b097 100755 --- a/app/Models/Fornitore.php +++ b/app/Models/Fornitore.php @@ -36,6 +36,7 @@ class Fornitore extends Model 'iban', 'intestazione_cc_esatta', 'bic', + 'sia', 'modalita_pagamento_predefinita', 'escludi_righe_fe', 'fe_features', @@ -97,6 +98,18 @@ public function getOperationalConfigSafeAttribute(): array return is_array($this->operational_config ?? null) ? $this->operational_config : []; } + public function getSiaEffettivoAttribute(): ?string + { + $sia = trim((string) ($this->sia ?? '')); + if ($sia !== '') { + return $sia; + } + + $fallback = trim((string) data_get($this->operational_config_safe, 'payment_identifiers.sia', '')); + + return $fallback !== '' ? $fallback : null; + } + protected static function booted(): void { static::creating(function (Fornitore $fornitore) { diff --git a/app/Models/Impostazione.php b/app/Models/Impostazione.php new file mode 100644 index 0000000..89ae06c --- /dev/null +++ b/app/Models/Impostazione.php @@ -0,0 +1,15 @@ +belongsTo(Stabile::class, 'stabile_id'); } + public function stabileContratto() + { + return $this->belongsTo(StabileContratto::class, 'stabile_contratto_id'); + } + public function getTitoloCalendarioAttribute(): string { $parts = array_filter([ @@ -62,4 +67,4 @@ public function getTitoloCalendarioAttribute(): string return implode(' - ', $parts) ?: ('Scadenza #' . (int) $this->id); } -} \ No newline at end of file +} diff --git a/app/Models/Stabile.php b/app/Models/Stabile.php index 2d0849f..19a98c3 100755 --- a/app/Models/Stabile.php +++ b/app/Models/Stabile.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasOne; +use Illuminate\Support\Carbon; class Stabile extends Model { @@ -156,6 +157,69 @@ class Stabile extends Model 'ultima_generazione_unita' => 'datetime', ]; + public function hasOperationalHeating(): bool + { + if ((bool) ($this->riscaldamento_centralizzato ?? false)) { + return true; + } + + if ((bool) ($this->millesimi_riscaldamento_separati ?? false)) { + return true; + } + + $rateRiscaldamento = $this->rate_riscaldamento_mesi ?? []; + if (is_string($rateRiscaldamento)) { + $rateRiscaldamento = json_decode($rateRiscaldamento, true); + } + + if (is_array($rateRiscaldamento) && $rateRiscaldamento !== []) { + return true; + } + + return (bool) data_get($this->getOperationalConfiguration(), 'mostra_riscaldamento', false); + } + + public function getOperationalConfiguration(): array + { + return is_array($this->configurazione_avanzata ?? null) ? $this->configurazione_avanzata : []; + } + + public function getTakeoverDate(): ?Carbon + { + $raw = data_get($this->getOperationalConfiguration(), 'amministrazione.presa_in_carico_dal'); + + if (is_string($raw) && trim($raw) !== '') { + try { + return Carbon::parse($raw); + } catch (\Throwable) { + // ignore invalid stored values + } + } + + return $this->data_nomina instanceof Carbon ? $this->data_nomina : null; + } + + public function isOperationalYearVisible(?int $year): bool + { + if (! is_int($year) || $year <= 0) { + return true; + } + + $takeoverDate = $this->getTakeoverDate(); + if (! $takeoverDate) { + return true; + } + + return $year >= (int) $takeoverDate->year; + } + + public function getNominaVerbaleDocumentoId(): ?int + { + $id = data_get($this->getOperationalConfiguration(), 'amministrazione.verbale_nomina_documento_id'); + + return is_numeric($id) && (int) $id > 0 ? (int) $id : null; + } + /** * Relazione con Amministratore */ @@ -241,6 +305,16 @@ public function gestioniContabili() ->orderBy('numero_straordinaria'); } + /** + * Relazione con Contratti Hub + */ + public function contrattiHub() + { + return $this->hasMany(StabileContratto::class, 'stabile_id', 'id') + ->orderByDesc('decorrenza_dal') + ->orderByDesc('id'); + } + /** * Relazione con PianoRateizzazione */ diff --git a/app/Models/StabileContratto.php b/app/Models/StabileContratto.php new file mode 100644 index 0000000..fbf21e1 --- /dev/null +++ b/app/Models/StabileContratto.php @@ -0,0 +1,75 @@ + 'date', + 'decorrenza_dal' => 'date', + 'decorrenza_al' => 'date', + 'giorno_scadenza' => 'integer', + 'giorni_preavviso' => 'integer', + 'rinnovo_automatico' => 'boolean', + 'importo_periodico' => 'decimal:2', + 'dati_contratto' => 'array', + 'codici_collegati' => 'array', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + public function stabile(): BelongsTo + { + return $this->belongsTo(Stabile::class, 'stabile_id'); + } + + public function fornitore(): BelongsTo + { + return $this->belongsTo(Fornitore::class, 'fornitore_id'); + } + + public function documentoPrincipale(): BelongsTo + { + return $this->belongsTo(Documento::class, 'documento_principale_id'); + } + + public function scadenze(): HasMany + { + return $this->hasMany(Scadenza::class, 'stabile_contratto_id'); + } +} diff --git a/app/Modules/Contabilita/Models/SaldoConto.php b/app/Modules/Contabilita/Models/SaldoConto.php index 62a4949..54aca5c 100644 --- a/app/Modules/Contabilita/Models/SaldoConto.php +++ b/app/Modules/Contabilita/Models/SaldoConto.php @@ -1,5 +1,4 @@ 'date', - 'periodo_da' => 'date', - 'periodo_a' => 'date', - 'saldo' => 'decimal:2', + 'data_saldo' => 'date', + 'periodo_da' => 'date', + 'periodo_a' => 'date', + 'saldo' => 'decimal:2', + 'is_partenza_contabile' => 'boolean', ]; public function conto() diff --git a/app/Modules/Contabilita/Services/ContiFinanziariService.php b/app/Modules/Contabilita/Services/ContiFinanziariService.php new file mode 100644 index 0000000..366d630 --- /dev/null +++ b/app/Modules/Contabilita/Services/ContiFinanziariService.php @@ -0,0 +1,54 @@ +stabile_id ?? 0); + if ($stabileId <= 0) { + throw new RuntimeException('Movimento banca senza stabile_id.'); + } + + $conto = null; + $contoId = (int) ($movimento->conto_id ?? 0); + if ($contoId > 0) { + $conto = DatiBancari::query()->find($contoId); + } + + if (! $conto) { + $iban = is_string($movimento->iban ?? null) ? trim((string) $movimento->iban) : ''; + if ($iban !== '') { + $conto = DatiBancari::query() + ->where('stabile_id', $stabileId) + ->where('iban', $iban) + ->orderBy('id') + ->first(); + } + } + + if (! $conto) { + throw new RuntimeException('Il movimento banca non ha un conto finanziario risolvibile. Seleziona o collega il conto bancario corretto.'); + } + + $codice = '1100B' . str_pad((string) $conto->id, 5, '0', STR_PAD_LEFT); + + $label = trim((string) ($conto->denominazione_banca ?? 'Banca')); + $iban = is_string($conto->iban ?? null) ? trim((string) $conto->iban) : ''; + $numeroConto = is_string($conto->numero_conto ?? null) ? trim((string) $conto->numero_conto) : ''; + + $descrizione = $label !== '' ? $label : 'Conto banca'; + if ($iban !== '') { + $descrizione .= ' · ' . $iban; + } elseif ($numeroConto !== '') { + $descrizione .= ' · n. ' . $numeroConto; + } + + return app(ChiusuraGestioneService::class)->resolveOrCreateConto($codice, $descrizione, 'Attività'); + } +} diff --git a/app/Modules/Contabilita/Services/MovimentoBancaRiconciliazioneService.php b/app/Modules/Contabilita/Services/MovimentoBancaRiconciliazioneService.php new file mode 100644 index 0000000..4a09f8f --- /dev/null +++ b/app/Modules/Contabilita/Services/MovimentoBancaRiconciliazioneService.php @@ -0,0 +1,85 @@ +affitto; + if (! $affitto) { + throw new RuntimeException('Canone affitto senza contratto collegato.'); + } + + $pagato = AffittoCanonePagato::query() + ->where('affitto_id', (int) $canone->affitto_id) + ->where('anno', (int) $canone->anno) + ->where('mese', (int) $canone->mese) + ->first(); + + if (! $pagato) { + $descrizioni = array_filter([ + $canone->descrizione_1, + $canone->descrizione_2, + $canone->descrizione_3, + $canone->descrizione_4, + ], static fn($value): bool => is_string($value) && trim($value) !== ''); + + $pagato = new AffittoCanonePagato([ + 'affitto_id' => (int) $canone->affitto_id, + 'anno' => (int) $canone->anno, + 'mese' => (int) $canone->mese, + 'mese_descrizione' => $canone->mese_descrizione, + 'data_pagamento' => $movimento->data?->toDateString() ?: now()->toDateString(), + 'descrizione' => trim(implode(' · ', $descrizioni)) ?: ('Incasso canone da banca del ' . ($movimento->data?->format('d/m/Y') ?? now()->format('d/m/Y'))), + 'fitto' => $canone->fitto, + 'istat_percentuale' => $canone->istat_percentuale, + 'istat_importo' => $canone->istat_importo, + 'descrizione_1' => $canone->descrizione_1, + 'descrizione_2' => $canone->descrizione_2, + 'importo_1' => $canone->importo_1, + 'importo_2' => $canone->importo_2, + 'bollo' => $canone->bollo, + 'totale' => $canone->totale, + ]); + } else { + $pagato->data_pagamento = $pagato->data_pagamento ?: ($movimento->data?->toDateString() ?: now()->toDateString()); + $pagato->descrizione = is_string($pagato->descrizione ?? null) && trim((string) $pagato->descrizione) !== '' + ? $pagato->descrizione + : 'Incasso canone da banca'; + } + + $pagato->save(); + + $matchData = is_array($movimento->match_data ?? null) ? $movimento->match_data : []; + $matchData['riconciliato_operativo'] = true; + $matchData['riconciliazione_tipo'] = 'affitto_canone'; + $matchData['riconciliazione_label'] = 'Canone affitto chiuso'; + $matchData['riconciliazione_at'] = now()->toDateTimeString(); + $matchData['riconciliato_da'] = (int) $user->id; + $matchData['affitto_canone_dovuto_id'] = (int) $canone->id; + $matchData['affitto_canone_pagato_id'] = (int) $pagato->id; + $matchData['affitto_id'] = (int) $canone->affitto_id; + + if (Schema::hasColumn('contabilita_movimenti_banca', 'rubrica_mittente_id') && (int) ($affitto->rubrica_inquilino_id ?? 0) > 0) { + $movimento->rubrica_mittente_id = (int) $affitto->rubrica_inquilino_id; + } + + if (Schema::hasColumn('contabilita_movimenti_banca', 'da_confermare')) { + $movimento->da_confermare = false; + } + if (Schema::hasColumn('contabilita_movimenti_banca', 'match_data')) { + $movimento->match_data = $matchData; + } + + $movimento->save(); + + return $pagato; + } +} diff --git a/app/Modules/Contabilita/Services/PagamentoFornitorePrimaNotaService.php b/app/Modules/Contabilita/Services/PagamentoFornitorePrimaNotaService.php index efb0dd7..ac7451b 100644 --- a/app/Modules/Contabilita/Services/PagamentoFornitorePrimaNotaService.php +++ b/app/Modules/Contabilita/Services/PagamentoFornitorePrimaNotaService.php @@ -1,13 +1,16 @@ resolveContoFinanziarioDaMovimentoBanca($movimento) + ->id; } $contoFinanziario = PianoConti::query()->find($contoFinanziarioId); @@ -65,7 +70,7 @@ public function generaPagamentoDaMovimento(User $user, MovimentoBanca $movimento throw new RuntimeException('Conto finanziario non valido.'); } - $fornitore = Fornitore::query()->find($fornitoreId); + $fornitore = Fornitore::query()->find($fornitoreId); $fornitoreLabel = $fornitore && is_string($fornitore->ragione_sociale ?? null) ? trim((string) $fornitore->ragione_sociale) : null; @@ -73,6 +78,21 @@ public function generaPagamentoDaMovimento(User $user, MovimentoBanca $movimento ? trim((string) $fornitore->codice_univoco) : null; + $fattura = null; + $fatturaId = (int) (($override['fattura_fornitore_id'] ?? null) ?: 0); + if ($fatturaId > 0 && Schema::hasTable('contabilita_fatture_fornitori')) { + $fattura = FatturaFornitore::query()->find($fatturaId); + if (! $fattura) { + throw new RuntimeException('Fattura fornitore non trovata.'); + } + if ((int) ($fattura->fornitore_id ?? 0) !== $fornitoreId) { + throw new RuntimeException('La fattura selezionata non appartiene al fornitore indicato.'); + } + if ((int) ($fattura->gestione_id ?? 0) > 0 && $gestioneId <= 0) { + $gestioneId = (int) $fattura->gestione_id; + } + } + $contoDebitoFornitore = app(ContiSoggettiService::class)->resolveContoDebitoFornitore( $stabileId, $fornitoreId, @@ -90,20 +110,20 @@ public function generaPagamentoDaMovimento(User $user, MovimentoBanca $movimento ? trim((string) $override['descrizione']) : ''; if ($descr === '') { - $base = is_string($movimento->descrizione) ? trim((string) $movimento->descrizione) : ''; + $base = is_string($movimento->descrizione) ? trim((string) $movimento->descrizione) : ''; $descr = $base !== '' ? $base : 'Pagamento fornitore'; } - return DB::transaction(function () use ($user, $movimento, $stabileId, $gestioneId, $data, $descr, $amount, $contoFinanziarioId, $contoDebitoFornitore) { + return DB::transaction(function () use ($user, $movimento, $stabileId, $gestioneId, $data, $descr, $amount, $contoFinanziarioId, $contoDebitoFornitore, $fornitoreId, $fattura) { $regPayload = [ - 'stabile_id' => $stabileId, - 'gestione_id' => (Schema::hasColumn('contabilita_registrazioni', 'gestione_id') - ? Registrazione::resolveGestioneIdOrDefault($gestioneId, $stabileId, $data) - : null), + 'stabile_id' => $stabileId, + 'gestione_id' => (Schema::hasColumn('contabilita_registrazioni', 'gestione_id') + ? Registrazione::resolveGestioneIdOrDefault($gestioneId, $stabileId, $data) + : null), 'data_registrazione' => $data, - 'descrizione' => $descr, - 'created_by' => (int) $user->id, - 'updated_by' => (int) $user->id, + 'descrizione' => $descr, + 'created_by' => (int) $user->id, + 'updated_by' => (int) $user->id, ]; if (Schema::hasColumn('contabilita_registrazioni', 'user_id')) { @@ -116,26 +136,108 @@ public function generaPagamentoDaMovimento(User $user, MovimentoBanca $movimento // Dare: diminuisce il debito verso fornitore Movimento::query()->create([ 'registrazione_id' => (int) $reg->id, - 'conto_id' => (int) $contoDebitoFornitore->id, - 'tipo' => 'dare', - 'importo' => $amount, + 'conto_id' => (int) $contoDebitoFornitore->id, + 'tipo' => 'dare', + 'importo' => $amount, ]); // Avere: diminuisce la banca/cassa Movimento::query()->create([ 'registrazione_id' => (int) $reg->id, - 'conto_id' => $contoFinanziarioId, - 'tipo' => 'avere', - 'importo' => $amount, + 'conto_id' => $contoFinanziarioId, + 'tipo' => 'avere', + 'importo' => $amount, ]); $reg->assertBilanciata(); $movimento->registrazione_id = (int) $reg->id; - $movimento->gestione_id = $gestioneId; + $movimento->gestione_id = $gestioneId; + if (Schema::hasColumn('contabilita_movimenti_banca', 'fornitore_id')) { + $movimento->fornitore_id = $fornitoreId; + } $movimento->save(); + if ($fattura) { + $fattura->data_pagamento = $fattura->data_pagamento ?: $data; + $fattura->movimento_pagamento_id = (int) $movimento->id; + $fattura->save(); + + $this->syncRegistroRitenutaDaPagamento($fattura, $gestioneId, $data); + } + return $reg; }); } + + private function syncRegistroRitenutaDaPagamento(FatturaFornitore $fattura, int $gestioneId, string $dataPagamento): void + { + if (! Schema::hasTable('registro_ritenute_acconto')) { + return; + } + + $ritenutaImporto = round((float) ($fattura->ritenuta_importo ?? 0), 4); + $ritenutaAliquota = round((float) ($fattura->ritenuta_aliquota ?? 0), 2); + if ($ritenutaImporto <= 0 || $ritenutaAliquota <= 0) { + return; + } + + $dataPagamentoCarbon = Carbon::parse($dataPagamento)->startOfDay(); + $dataCompetenza = $fattura->data_documento?->copy() + ?: $fattura->data_registrazione?->copy() + ?: $dataPagamentoCarbon->copy(); + $scadenzaVersamento = $dataPagamentoCarbon->copy()->addMonthNoOverflow()->day(16); + + $tenantId = ''; + if (Schema::hasTable('gestioni_contabili')) { + $gestione = \App\Models\GestioneContabile::query()->find($gestioneId); + $tenantId = is_string($gestione?->tenant_id ?? null) ? trim((string) $gestione->tenant_id) : ''; + } + if ($tenantId === '') { + $stabile = Stabile::query() + ->with('amministratore:id,codice_amministratore') + ->select(['id', 'amministratore_id']) + ->find((int) $fattura->stabile_id); + $tenantId = is_string($stabile?->amministratore?->codice_amministratore ?? null) + ? trim((string) $stabile->amministratore->codice_amministratore) + : ''; + } + + $registro = RegistroRitenuteAcconto::query() + ->where('gestione_id', $gestioneId) + ->where(function ($query) use ($fattura, $dataCompetenza, $ritenutaImporto) { + $query->where('fattura_id', (int) $fattura->id) + ->orWhere(function ($sub) use ($fattura, $dataCompetenza, $ritenutaImporto) { + $sub->where('fornitore_id', (int) $fattura->fornitore_id) + ->whereDate('data_competenza', $dataCompetenza->toDateString()) + ->where('importo_ritenuta', $ritenutaImporto); + }); + }) + ->orderByDesc('id') + ->first(); + + if (! $registro) { + $registro = new RegistroRitenuteAcconto(); + $registro->gestione_id = $gestioneId; + $registro->numero_progressivo = ((int) (RegistroRitenuteAcconto::query() + ->where('gestione_id', $gestioneId) + ->lockForUpdate() + ->max('numero_progressivo') ?? 0)) + 1; + $registro->stato_versamento = 'da_versare'; + $registro->fornitore_id = (int) $fattura->fornitore_id; + $registro->fattura_id = (int) $fattura->id; + } + + $registro->tenant_id = $tenantId !== '' ? $tenantId : 'stabile-' . (int) $fattura->stabile_id; + $registro->data_competenza = $dataCompetenza->toDateString(); + $registro->data_pagamento_fattura = $dataPagamentoCarbon->toDateString(); + $registro->imponibile = round((float) ($fattura->imponibile ?? 0), 4); + $registro->aliquota_ritenuta = $ritenutaAliquota; + $registro->importo_ritenuta = $ritenutaImporto; + $registro->codice_tributo = trim((string) ($fattura->ritenuta_codice_tributo ?? '')) ?: '1040'; + $registro->tipo_ritenuta = trim((string) ($fattura->ritenuta_previdenziale_codice ?? '')) ?: 'professionale'; + $registro->data_scadenza_versamento = $scadenzaVersamento->toDateString(); + $registro->anno_dichiarazione = (int) $dataCompetenza->format('Y'); + $registro->save(); + } } diff --git a/app/Providers/Filament/AdminFilamentPanelProvider.php b/app/Providers/Filament/AdminFilamentPanelProvider.php index aac0a23..80e8d79 100644 --- a/app/Providers/Filament/AdminFilamentPanelProvider.php +++ b/app/Providers/Filament/AdminFilamentPanelProvider.php @@ -56,41 +56,41 @@ public function panel(Panel $panel): Panel ->group('NetGescon') ->icon('heroicon-o-users') ->url(fn() => RubricaUniversaleArchivio::getUrl(panel: 'admin-filament')) - ->visible(function () { + ->visible(function (): bool { $user = Auth::user(); return $user instanceof User - && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); }), NavigationItem::make('Cellulare / WebApp') ->group('NetGescon') ->icon('heroicon-o-phone') ->url(fn() => route('admin.mobile')) - ->visible(function () { + ->visible(function (): bool { $user = Auth::user(); return $user instanceof User - && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); }), NavigationItem::make('Ticket Mobile') ->group('NetGescon') ->icon('heroicon-o-device-phone-mobile') ->url(fn() => TicketMobile::getUrl(panel: 'admin-filament')) - ->visible(function () { + ->visible(function (): bool { $user = Auth::user(); return $user instanceof User - && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); }), NavigationItem::make('Scheda Stabile') ->group('NetGescon') ->icon('heroicon-o-building-office-2') ->url(fn() => StabilePage::getUrl(panel: 'admin-filament')) - ->visible(function () { + ->visible(function (): bool { $user = Auth::user(); return $user instanceof User - && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); }), NavigationItem::make('Catalogo Fornitore') ->group('Fornitore') @@ -102,8 +102,8 @@ public function panel(Panel $panel): Panel return $user instanceof User && $user->hasRole('fornitore'); }), ]) - ->discoverResources(in: app_path('Filament/Resources'), for : 'App\\Filament\\Resources') - ->discoverPages(in: app_path('Filament/Pages'), for : 'App\\Filament\\Pages') + ->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources') + ->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages') ->pages([ Dashboard::class, SupportoDashboard::class, @@ -111,7 +111,7 @@ public function panel(Panel $panel): Panel PrimaNotaModifica::class, ContoMastrino::class, ]) - ->discoverWidgets(in: app_path('Filament/Widgets'), for : 'App\\Filament\\Widgets') + ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets') ->widgets([ AccountWidget::class, TicketMobileOverview::class, @@ -171,5 +171,4 @@ protected function resolveSupplierCatalogUrl(): string return TicketOperativi::getUrl(panel: 'admin-filament'); } - } diff --git a/app/Services/Catalog/AmazonCreatorsApiService.php b/app/Services/Catalog/AmazonCreatorsApiService.php index 3d171ce..eaa64f4 100644 --- a/app/Services/Catalog/AmazonCreatorsApiService.php +++ b/app/Services/Catalog/AmazonCreatorsApiService.php @@ -11,10 +11,10 @@ class AmazonCreatorsApiService public function isConfigured(): bool { return (bool) config('services.amazon.creators_enabled', false) - && $this->credentialId() !== '' - && $this->credentialSecret() !== '' - && $this->tokenUrl() !== '' - && $this->apiBaseUrl() !== ''; + && $this->credentialId() !== '' + && $this->credentialSecret() !== '' + && $this->tokenUrl() !== '' + && $this->apiBaseUrl() !== ''; } public function getMarketplace(): string @@ -151,4 +151,4 @@ private function getAccessToken(): string return $token; } -} \ No newline at end of file +} diff --git a/app/Services/Catalog/AmazonCreatorsCatalogSyncService.php b/app/Services/Catalog/AmazonCreatorsCatalogSyncService.php index 1561e89..fad39b4 100644 --- a/app/Services/Catalog/AmazonCreatorsCatalogSyncService.php +++ b/app/Services/Catalog/AmazonCreatorsCatalogSyncService.php @@ -6,8 +6,8 @@ use App\Models\ProductIdentifier; use App\Models\ProductMedia; use App\Models\ProductOffer; -use Illuminate\Support\Collection; use Illuminate\Support\Arr; +use Illuminate\Support\Collection; use Illuminate\Support\Str; use RuntimeException; @@ -162,9 +162,9 @@ public function importItemForSupplier(Fornitore $fornitore, array $item, array $ throw new RuntimeException('Il risultato Amazon selezionato non contiene un ASIN valido.'); } - $ean = $this->extractEan($item); - $title = $this->extractTitle($item) ?: 'Prodotto Amazon'; - $brand = $this->extractBrand($item); + $ean = $this->extractEan($item); + $title = $this->extractTitle($item) ?: 'Prodotto Amazon'; + $brand = $this->extractBrand($item); $product = $this->findExistingProduct($asin, $ean); $created = false; @@ -180,8 +180,8 @@ public function importItemForSupplier(Fornitore $fornitore, array $item, array $ 'track_serials' => false, 'is_active' => true, 'meta' => [ - 'source' => 'amazon_creators_api', - 'catalog' => [ + 'source' => 'amazon_creators_api', + 'catalog' => [ 'publication_mode' => 'internal_only', ], 'verification' => [ @@ -189,7 +189,7 @@ public function importItemForSupplier(Fornitore $fornitore, array $item, array $ 'channel' => 'amazon_catalog_search_ui', 'updated_at' => now()->toIso8601String(), ], - 'amazon' => [ + 'amazon' => [ 'asin' => $asin, 'ean' => $ean, 'title' => $title, @@ -221,10 +221,10 @@ public function importItemForSupplier(Fornitore $fornitore, array $item, array $ } $sync = $this->syncProduct($product, [ - 'item' => $item, - 'source' => (string) ($options['source'] ?? 'amazon_catalog_search_ui'), - 'marketplace' => (string) ($options['marketplace'] ?? $this->apiService->getMarketplace()), - 'associate_tag'=> (string) ($options['associate_tag'] ?? ($this->apiService->getAssociateTag() ?? '')), + 'item' => $item, + 'source' => (string) ($options['source'] ?? 'amazon_catalog_search_ui'), + 'marketplace' => (string) ($options['marketplace'] ?? $this->apiService->getMarketplace()), + 'associate_tag' => (string) ($options['associate_tag'] ?? ($this->apiService->getAssociateTag() ?? '')), ]); return [ diff --git a/app/Services/Catalog/ProductHubViewService.php b/app/Services/Catalog/ProductHubViewService.php index 23397fe..66c9f81 100644 --- a/app/Services/Catalog/ProductHubViewService.php +++ b/app/Services/Catalog/ProductHubViewService.php @@ -7,7 +7,6 @@ use App\Models\ProductOffer; use App\Models\TicketIntervento; use App\Modules\Contabilita\Models\FatturaFornitoreRiga; -use Illuminate\Support\Collection; use Illuminate\Support\Facades\Schema; class ProductHubViewService @@ -49,8 +48,8 @@ public function buildRows(Fornitore $fornitore, ?int $stabileId = null, int $lim } $productIds = $products->pluck('id')->map(fn($value): int => (int) $value)->all(); - $usageByProduct = $this->buildUsageSummary($fornitore, $productIds, $stabileId); - $pricingByProduct = $this->buildPurchaseSummary($fornitore, $productIds); + $usageByProduct = $this->buildUsageSummary($fornitore, $productIds, $stabileId); + $pricingByProduct = $this->buildPurchaseSummary($fornitore, $productIds); return $products ->map(function (Product $product) use ($fornitore, $usageByProduct, $pricingByProduct): array { @@ -69,42 +68,42 @@ public function buildRows(Fornitore $fornitore, ?int $stabileId = null, int $lim $purchaseSummary = $pricingByProduct[(int) $product->id] ?? $this->emptyPurchaseSummary(); return [ - 'id' => (int) $product->id, - 'internal_code' => (string) ($product->internal_code ?? ''), - 'name' => (string) ($product->name ?? ''), - 'brand' => (string) ($product->brand ?? ''), - 'model' => (string) ($product->model ?? ''), - 'color_label' => (string) ($product->color_label ?? ''), - 'track_serials' => (bool) $product->track_serials, - 'identifier' => (string) ($identifier->code_value ?? ''), - 'identifiers_count' => (int) ($product->identifiers_count ?? 0), - 'serials_count' => (int) ($product->serials_count ?? 0), - 'media_count' => (int) ($product->media_count ?? 0), - 'source' => (string) (($meta['source'] ?? 'manual') ?: 'manual'), - 'publication_mode' => (string) (($catalogMeta['publication_mode'] ?? '') ?: ''), - 'public_reference' => (string) (($catalogMeta['public_reference'] ?? '') ?: ($product->internal_code ?? '')), - 'private_links' => count(array_filter(is_array($catalogMeta['private_source_links'] ?? null) ? $catalogMeta['private_source_links'] : [], fn($value) => filled($value))), - 'categories' => (string) (($catalogMeta['categories'] ?? '') ?: ''), - 'stock_quantity' => is_numeric($meta['stock']['quantity'] ?? null) ? (int) $meta['stock']['quantity'] : null, - 'price_wholesale' => is_numeric($meta['pricing']['wholesale'] ?? null) ? (float) $meta['pricing']['wholesale'] : null, - 'offers_count' => $offers->count(), - 'own_offer_price' => $internalOffer && filled($internalOffer->price_amount) ? (float) $internalOffer->price_amount : null, - 'best_competitor_price' => $bestExternal && filled($bestExternal->price_amount) ? (float) $bestExternal->price_amount : null, - 'best_competitor_source' => $bestExternal ? (string) ($bestExternal->source_name ?? $bestExternal->source_type ?? '') : '', - 'amazon_referral_url' => $amazonOffer ? (string) ($amazonOffer->referral_url ?? $amazonOffer->external_url ?? '') : '', - 'usage_count' => (int) ($usage['usage_count'] ?? 0), - 'usage_quantity' => (float) ($usage['usage_quantity'] ?? 0), - 'last_used_at' => (string) ($usage['last_used_at'] ?? ''), - 'stable_usage_count' => (int) ($usage['stable_usage_count'] ?? 0), - 'stable_usage_quantity' => (float) ($usage['stable_usage_quantity'] ?? 0), - 'stable_last_used_at' => (string) ($usage['stable_last_used_at'] ?? ''), - 'purchase_lines_count' => (int) ($purchaseSummary['purchase_lines_count'] ?? 0), - 'last_purchase_total' => $purchaseSummary['last_purchase_total'] ?? null, - 'last_purchase_date' => (string) ($purchaseSummary['last_purchase_date'] ?? ''), - 'purchase_history_labels' => (array) ($purchaseSummary['purchase_history_labels'] ?? []), - 'sort_score' => ((int) ($usage['stable_usage_count'] ?? 0) * 1000) - + ((int) round((float) ($usage['stable_usage_quantity'] ?? 0) * 100)) - + ((int) ($usage['usage_count'] ?? 0) * 10), + 'id' => (int) $product->id, + 'internal_code' => (string) ($product->internal_code ?? ''), + 'name' => (string) ($product->name ?? ''), + 'brand' => (string) ($product->brand ?? ''), + 'model' => (string) ($product->model ?? ''), + 'color_label' => (string) ($product->color_label ?? ''), + 'track_serials' => (bool) $product->track_serials, + 'identifier' => (string) ($identifier->code_value ?? ''), + 'identifiers_count' => (int) ($product->identifiers_count ?? 0), + 'serials_count' => (int) ($product->serials_count ?? 0), + 'media_count' => (int) ($product->media_count ?? 0), + 'source' => (string) (($meta['source'] ?? 'manual') ?: 'manual'), + 'publication_mode' => (string) (($catalogMeta['publication_mode'] ?? '') ?: ''), + 'public_reference' => (string) (($catalogMeta['public_reference'] ?? '') ?: ($product->internal_code ?? '')), + 'private_links' => count(array_filter(is_array($catalogMeta['private_source_links'] ?? null) ? $catalogMeta['private_source_links'] : [], fn($value) => filled($value))), + 'categories' => (string) (($catalogMeta['categories'] ?? '') ?: ''), + 'stock_quantity' => is_numeric($meta['stock']['quantity'] ?? null) ? (int) $meta['stock']['quantity'] : null, + 'price_wholesale' => is_numeric($meta['pricing']['wholesale'] ?? null) ? (float) $meta['pricing']['wholesale'] : null, + 'offers_count' => $offers->count(), + 'own_offer_price' => $internalOffer && filled($internalOffer->price_amount) ? (float) $internalOffer->price_amount : null, + 'best_competitor_price' => $bestExternal && filled($bestExternal->price_amount) ? (float) $bestExternal->price_amount : null, + 'best_competitor_source' => $bestExternal ? (string) ($bestExternal->source_name ?? $bestExternal->source_type ?? '') : '', + 'amazon_referral_url' => $amazonOffer ? (string) ($amazonOffer->referral_url ?? $amazonOffer->external_url ?? '') : '', + 'usage_count' => (int) ($usage['usage_count'] ?? 0), + 'usage_quantity' => (float) ($usage['usage_quantity'] ?? 0), + 'last_used_at' => (string) ($usage['last_used_at'] ?? ''), + 'stable_usage_count' => (int) ($usage['stable_usage_count'] ?? 0), + 'stable_usage_quantity' => (float) ($usage['stable_usage_quantity'] ?? 0), + 'stable_last_used_at' => (string) ($usage['stable_last_used_at'] ?? ''), + 'purchase_lines_count' => (int) ($purchaseSummary['purchase_lines_count'] ?? 0), + 'last_purchase_total' => $purchaseSummary['last_purchase_total'] ?? null, + 'last_purchase_date' => (string) ($purchaseSummary['last_purchase_date'] ?? ''), + 'purchase_history_labels' => (array) ($purchaseSummary['purchase_history_labels'] ?? []), + 'sort_score' => ((int) ($usage['stable_usage_count'] ?? 0) * 1000) + + ((int) round((float) ($usage['stable_usage_quantity'] ?? 0) * 100)) + + ((int) ($usage['usage_count'] ?? 0) * 10), ]; }) ->sort(function (array $left, array $right): int { @@ -160,14 +159,14 @@ private function buildUsageSummary(Fornitore $fornitore, array $productIds, ?int $summary[$productId]['usage_count']++; $summary[$productId]['usage_quantity'] += $qty; - $summary[$productId]['last_used_at'] = $summary[$productId]['last_used_at'] !== '' + $summary[$productId]['last_used_at'] = $summary[$productId]['last_used_at'] !== '' ? $summary[$productId]['last_used_at'] : (optional($intervento->updated_at)->format('d/m/Y H:i') ?: ''); if ($stabileId !== null && $stabileId > 0 && $currentStabileId === $stabileId) { $summary[$productId]['stable_usage_count']++; $summary[$productId]['stable_usage_quantity'] += $qty; - $summary[$productId]['stable_last_used_at'] = $summary[$productId]['stable_last_used_at'] !== '' + $summary[$productId]['stable_last_used_at'] = $summary[$productId]['stable_last_used_at'] !== '' ? $summary[$productId]['stable_last_used_at'] : (optional($intervento->updated_at)->format('d/m/Y H:i') ?: ''); } @@ -247,4 +246,4 @@ private function emptyPurchaseSummary(): array 'purchase_history_labels' => [], ]; } -} \ No newline at end of file +} diff --git a/app/Services/Contabilita/MovimentiBancaImporter.php b/app/Services/Contabilita/MovimentiBancaImporter.php index 607a6e2..8e37ff0 100644 --- a/app/Services/Contabilita/MovimentiBancaImporter.php +++ b/app/Services/Contabilita/MovimentiBancaImporter.php @@ -1035,7 +1035,7 @@ private function parseDescrizioneEstesa(string $text): array $providerUpper = strtoupper(trim((string) ($res['payment_provider'] ?? $res['beneficiario'] ?? ''))); if ($providerUpper !== '') { if (str_contains($providerUpper, 'ACEA ATO2') || str_contains($providerUpper, 'ACEA ACQUA')) { - $res['utility_type'] = 'acqua'; + $res['utility_type'] = 'acqua'; $res['suggested_voce_spesa_code'] = 'ACQ'; } elseif (str_contains($providerUpper, 'A2A')) { $res['utility_type'] = 'utenza'; diff --git a/app/Services/Contabilita/PreventivoOrdinarioPreviewService.php b/app/Services/Contabilita/PreventivoOrdinarioPreviewService.php index 4dbe6e8..d63ec7c 100644 --- a/app/Services/Contabilita/PreventivoOrdinarioPreviewService.php +++ b/app/Services/Contabilita/PreventivoOrdinarioPreviewService.php @@ -245,7 +245,7 @@ private function buildTableSummary(Collection $voci, array $tabellaMap): array return $voci ->groupBy(fn(array $row): int => (int) ($row['tabella_millesimale_default_id'] ?? 0)) ->map(function (Collection $rows, int $tabellaId) use ($tabellaMap): array { - $tabella = $tabellaMap[$tabellaId] ?? [ + $tabella = $tabellaMap[$tabellaId] ?? [ 'codice' => '', 'label' => '', 'tipo_calcolo' => null, @@ -274,7 +274,7 @@ private function buildTableSummary(Collection $voci, array $tabellaMap): array 'voci' => $rows->values()->all(), ]; }) - ->sortBy(fn(array $row): string => sprintf('%06d|%s', (int) ($row['nord'] ?? 999999), (string) ($row['codice'] ?? ''))) + ->sortBy(fn(array $row): string => sprintf('%06d|%s', (int) ($row['nord'] ?? 999999), (string) ($row['codice'] ?? ''))) ->mapWithKeys(fn(array $row): array=> [$row['codice'] => $row]) ->all(); } diff --git a/app/Services/Documenti/DocumentoArchivioVisibilityService.php b/app/Services/Documenti/DocumentoArchivioVisibilityService.php index 6899ade..047969b 100644 --- a/app/Services/Documenti/DocumentoArchivioVisibilityService.php +++ b/app/Services/Documenti/DocumentoArchivioVisibilityService.php @@ -1,9 +1,7 @@ getRoleNames()->map(fn($role): string => (string) $role)->values()->all(); - $groupLabels = $this->resolveAudienceGroupsForUser($user); - $userId = (int) $user->id; - $hasRolesColumn = $this->hasVisibilityColumn($table, 'visibility_roles'); - $hasGroupsColumn = $this->hasVisibilityColumn($table, 'visibility_groups'); + $roles = $user->getRoleNames()->map(fn($role): string => (string) $role)->values()->all(); + $groupLabels = $this->resolveAudienceGroupsForUser($user); + $userId = (int) $user->id; + $hasRolesColumn = $this->hasVisibilityColumn($table, 'visibility_roles'); + $hasGroupsColumn = $this->hasVisibilityColumn($table, 'visibility_groups'); $hasAllowedUsersColumn = $this->hasVisibilityColumn($table, 'allowed_user_ids'); $query->where(function (Builder $visibilityQuery) use ($table, $roles, $groupLabels, $userId, $hasRolesColumn, $hasGroupsColumn, $hasAllowedUsersColumn): void { @@ -171,4 +169,4 @@ private function resolveAudienceGroupsForUser(User $user): array return array_values(array_unique($groups)); } -} \ No newline at end of file +} diff --git a/app/Services/FattureElettroniche/FatturaElettronicaImporter.php b/app/Services/FattureElettroniche/FatturaElettronicaImporter.php index 31fd800..b5b9ab6 100644 --- a/app/Services/FattureElettroniche/FatturaElettronicaImporter.php +++ b/app/Services/FattureElettroniche/FatturaElettronicaImporter.php @@ -548,13 +548,13 @@ private function updateContabilitaFornituraDaPdf(FatturaElettronica $fattura): v } $fullPath = $disk->path($resolvedPath); - $text = $this->extractPdfText($fullPath); + $text = $this->extractPdfText($fullPath); if (! is_string($text) || trim($text) === '') { return; } - $gasData = $this->extractGasFornituraDataFromPdfText($text); - $waterData = $this->extractAcquaFornituraDataFromPdfText($text); + $gasData = $this->extractGasFornituraDataFromPdfText($text); + $waterData = $this->extractAcquaFornituraDataFromPdfText($text); $paymentData = $this->extractPaymentReferenceDataFromPdfText($text, $fattura); $data = is_array($waterData) && $waterData !== [] ? $waterData : $gasData; @@ -686,11 +686,11 @@ private function extractAcquaFornituraDataFromPdfText(string $text): ?array return null; } - $codici = is_array($parsed['codici'] ?? null) ? $parsed['codici'] : []; + $codici = is_array($parsed['codici'] ?? null) ? $parsed['codici'] : []; $contatore = is_array($parsed['contatore'] ?? null) ? $parsed['contatore'] : []; - $consumi = is_array($parsed['consumi'] ?? null) ? $parsed['consumi'] : []; - $generale = is_array($parsed['generale'] ?? null) ? $parsed['generale'] : []; - $quadro = is_array($parsed['quadro_dettaglio'] ?? null) ? $parsed['quadro_dettaglio'] : []; + $consumi = is_array($parsed['consumi'] ?? null) ? $parsed['consumi'] : []; + $generale = is_array($parsed['generale'] ?? null) ? $parsed['generale'] : []; + $quadro = is_array($parsed['quadro_dettaglio'] ?? null) ? $parsed['quadro_dettaglio'] : []; $hasIdentifiers = count(array_filter([ $codici['utenza'] ?? null, @@ -705,19 +705,19 @@ private function extractAcquaFornituraDataFromPdfText(string $text): ?array } $payload = [ - 'type' => 'acqua', - 'voce_spesa_code' => 'ACQ', - 'codici' => [ - 'utenza' => $codici['utenza'] ?? null, - 'cliente' => $codici['cliente'] ?? null, + 'type' => 'acqua', + 'voce_spesa_code' => 'ACQ', + 'codici' => [ + 'utenza' => $codici['utenza'] ?? null, + 'cliente' => $codici['cliente'] ?? null, 'contratto' => $codici['contratto'] ?? null, ], - 'contatore' => [ + 'contatore' => [ 'matricola' => $contatore['matricola'] ?? null, ], - 'generale' => $generale, + 'generale' => $generale, 'quadro_dettaglio' => $quadro, - 'consumi' => array_values($consumi), + 'consumi' => array_values($consumi), ]; return array_filter($payload, fn($value) => $value !== null && $value !== ''); @@ -727,8 +727,8 @@ private function extractAcquaFornituraDataFromPdfText(string $text): ?array private function extractPaymentReferenceDataFromPdfText(string $text, FatturaElettronica $fattura): array { $provider = trim((string) ($fattura->fornitore_denominazione ?? '')); - $cbill = null; - $sia = null; + $cbill = null; + $sia = null; if (preg_match('/\bCBILL\b(?:\s*N\.)?\s*\.?\s*([0-9]{9,})/i', $text, $m)) { $cbill = trim((string) ($m[1] ?? '')); @@ -744,8 +744,8 @@ private function extractPaymentReferenceDataFromPdfText(string $text, FatturaEle $payload = [ 'provider' => $provider !== '' ? $provider : null, - 'cbill' => $cbill, - 'sia' => $sia, + 'cbill' => $cbill, + 'sia' => $sia, ]; return array_filter($payload, fn($value) => $value !== null && $value !== ''); @@ -776,18 +776,18 @@ private function enrichWaterInvoiceFromParsedData(FatturaElettronica $fattura, a return; } - $sum = 0.0; + $sum = 0.0; $hasNumeric = false; foreach ($consumi as $consumo) { if (! is_array($consumo) || ! is_numeric($consumo['valore'] ?? null)) { continue; } - $sum += (float) $consumo['valore']; - $hasNumeric = true; + $sum += (float) $consumo['valore']; + $hasNumeric = true; } $reference = null; - $first = is_array($consumi[0] ?? null) ? $consumi[0] : []; + $first = is_array($consumi[0] ?? null) ? $consumi[0] : []; if (is_string($first['dal'] ?? null) && is_string($first['al'] ?? null) && $first['dal'] !== '' && $first['al'] !== '') { $reference = $first['dal'] . ' - ' . $first['al']; } @@ -795,19 +795,19 @@ private function enrichWaterInvoiceFromParsedData(FatturaElettronica $fattura, a $dirty = false; if ($hasNumeric && (! is_numeric($fattura->consumo_valore) || (float) $fattura->consumo_valore <= 0)) { $fattura->consumo_valore = $sum; - $dirty = true; + $dirty = true; } if ((! is_string($fattura->consumo_unita ?? null) || trim((string) $fattura->consumo_unita) === '') && $hasNumeric) { $fattura->consumo_unita = 'mc'; - $dirty = true; + $dirty = true; } if ($reference !== null && (! is_string($fattura->consumo_riferimento ?? null) || trim((string) $fattura->consumo_riferimento) === '')) { $fattura->consumo_riferimento = $reference; - $dirty = true; + $dirty = true; } if (! is_string($fattura->consumo_raw ?? null) || trim((string) $fattura->consumo_raw) === '') { $fattura->consumo_raw = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - $dirty = true; + $dirty = true; } if ($dirty) { diff --git a/app/Services/FattureElettroniche/FatturaElettronicaProtocolloService.php b/app/Services/FattureElettroniche/FatturaElettronicaProtocolloService.php index 3b67c52..2f71fe9 100644 --- a/app/Services/FattureElettroniche/FatturaElettronicaProtocolloService.php +++ b/app/Services/FattureElettroniche/FatturaElettronicaProtocolloService.php @@ -1,5 +1,4 @@ findOrFail((int) $fattura->stabile_id); $dataDoc = $fattura->data_fattura; - $anno = $dataDoc ? (int) $dataDoc->format('Y') : (int) now()->format('Y'); - $ym = $dataDoc ? $dataDoc->format('Y/m') : now()->format('Y/m'); + $anno = $dataDoc ? (int) $dataDoc->format('Y') : (int) now()->format('Y'); + $ym = $dataDoc ? $dataDoc->format('Y/m') : now()->format('Y/m'); - $adminCode = $stabile->amministratore?->codice_amministratore; - $stabileCode = $stabile->codice_stabile; - $adminFolder = $adminCode ?: ('ID-' . (int) $stabile->amministratore_id); + $adminCode = $stabile->amministratore?->codice_amministratore; + $stabileCode = $stabile->codice_stabile; + $adminFolder = $adminCode ?: ('ID-' . (int) $stabile->amministratore_id); $stabileFolder = $stabileCode ?: ('ID-' . (int) $fattura->stabile_id); $stabileBase = ArchivioPaths::stabileBase($stabile, $stabile->amministratore); - $gestione = $this->resolveGestioneForYear($stabile, $anno); - $pdfBase = ArchivioPaths::fatturePdfGestionePath($stabile, $gestione, $anno, 'ordinaria', $stabile->amministratore) + $gestione = $this->resolveGestioneForYear($stabile, $anno); + $pdfBase = ArchivioPaths::fatturePdfGestionePath($stabile, $gestione, $anno, 'ordinaria', $stabile->amministratore) ?: $this->basePath($adminFolder, $stabileFolder, $anno); $xmlBase = ArchivioPaths::fattureXmlYearPath($stabile, $anno, $stabile->amministratore) ?: $this->basePath($adminFolder, $stabileFolder, $anno); @@ -80,7 +79,7 @@ public function ensureDocumentoProtocollato(FatturaElettronica $fattura, ?int $u $found = $this->findFirstFileStartingWith($dir, $fattura->allegato_pdf_hash . '-'); if ($found) { $fattura->allegato_pdf_path = $found; - $pdfPath = $found; + $pdfPath = $found; break; } } @@ -96,11 +95,11 @@ public function ensureDocumentoProtocollato(FatturaElettronica $fattura, ?int $u : ($pdfPathNow !== '' ? basename($pdfPathNow) : ''); $isProtocollato = ($pdfNameNow !== '' && preg_match('/^FT-\d{4}\b/i', $pdfNameNow) === 1); - $supplierToken = $this->sanitizeFilename((string) ($fattura->fornitore_denominazione ?? '')); - $supplierToken = trim((string) $supplierToken); + $supplierToken = $this->sanitizeFilename((string) ($fattura->fornitore_denominazione ?? '')); + $supplierToken = trim((string) $supplierToken); if ($isProtocollato && $supplierToken !== '' && mb_strlen($supplierToken) >= 4) { - $nameUpper = mb_strtoupper($pdfNameNow); + $nameUpper = mb_strtoupper($pdfNameNow); $tokenUpper = mb_strtoupper($supplierToken); if (! str_contains($nameUpper, $tokenUpper)) { $fattura->allegato_pdf_path = null; @@ -135,7 +134,7 @@ public function ensureDocumentoProtocollato(FatturaElettronica $fattura, ?int $u } if (! $protocollo) { if ($protocolCol) { - $next = $this->nextNumeroProtocollo((int) $fattura->stabile_id, $anno); + $next = $this->nextNumeroProtocollo((int) $fattura->stabile_id, $anno); $protocollo = 'FT-' . str_pad((string) $next, 4, '0', STR_PAD_LEFT); } else { // Se la tabella documenti non ha una colonna protocollo, assicuriamo comunque un nome unico per i file. @@ -158,7 +157,7 @@ public function ensureDocumentoProtocollato(FatturaElettronica $fattura, ?int $u break; } if ($protocolCol) { - $next = $this->nextNumeroProtocollo((int) $fattura->stabile_id, $anno); + $next = $this->nextNumeroProtocollo((int) $fattura->stabile_id, $anno); $protocollo = 'FT-' . str_pad((string) $next, 4, '0', STR_PAD_LEFT); } else { $protocollo = 'FT-' . str_pad((string) ((int) $fattura->id), 6, '0', STR_PAD_LEFT); @@ -168,13 +167,13 @@ public function ensureDocumentoProtocollato(FatturaElettronica $fattura, ?int $u } $sanFornitore = $this->sanitizeFilename((string) ($fattura->fornitore_denominazione ?? 'FORNITORE')); - $sanNumero = $this->sanitizeFilename((string) ($fattura->numero_fattura ?? ('ID-' . $fattura->id))); - $dateForName = $dataDoc ? $dataDoc->format('Ymd') : now()->format('Ymd'); + $sanNumero = $this->sanitizeFilename((string) ($fattura->numero_fattura ?? ('ID-' . $fattura->id))); + $dateForName = $dataDoc ? $dataDoc->format('Ymd') : now()->format('Ymd'); // Se manca il PDF (ma abbiamo almeno XML/metadata), generiamo un prospetto PDF archiviabile. $pdfPathNow = is_string($fattura->allegato_pdf_path) ? $fattura->allegato_pdf_path : ''; - $hasPdfNow = is_string($pdfPathNow) && $pdfPathNow !== '' && Storage::disk('local')->exists($pdfPathNow); - $hasXmlNow = (is_string($fattura->xml_content) && trim($fattura->xml_content) !== '') + $hasPdfNow = is_string($pdfPathNow) && $pdfPathNow !== '' && Storage::disk('local')->exists($pdfPathNow); + $hasXmlNow = (is_string($fattura->xml_content) && trim($fattura->xml_content) !== '') || (is_string($fattura->xml_path) && $fattura->xml_path !== '' && Storage::disk('local')->exists($fattura->xml_path)); if (! $hasPdfNow && $hasXmlNow) { $targetName = $protocollo . ' ' . $dateForName . ' ' . $sanFornitore . ' ' . $sanNumero . '.pdf'; @@ -219,19 +218,19 @@ public function ensureDocumentoProtocollato(FatturaElettronica $fattura, ?int $u $fattura->save(); $docData = [ - 'utente_id' => $userId, - 'nome' => $protocollo . ' - ' . $sanFornitore, - 'tipologia' => 'fattura', - 'tipo_documento' => 'fattura', - 'fornitore' => $fattura->fornitore_denominazione, - 'data_documento' => $fattura->data_fattura, - 'data_scadenza' => $fattura->data_scadenza, + 'utente_id' => $userId, + 'nome' => $protocollo . ' - ' . $sanFornitore, + 'tipologia' => 'fattura', + 'tipo_documento' => 'fattura', + 'fornitore' => $fattura->fornitore_denominazione, + 'data_documento' => $fattura->data_fattura, + 'data_scadenza' => $fattura->data_scadenza, 'importo_collegato' => $fattura->totale, 'numero_protocollo' => $protocollo, - 'protocollo' => $protocollo, - 'nome_file' => $fattura->allegato_pdf_nome ?: ($fattura->nome_file_xml ?: null), - 'path_file' => $fattura->allegato_pdf_path ?: ($fattura->xml_path ?: null), - 'percorso_file' => $fattura->allegato_pdf_path ?: ($fattura->xml_path ?: null), + 'protocollo' => $protocollo, + 'nome_file' => $fattura->allegato_pdf_nome ?: ($fattura->nome_file_xml ?: null), + 'path_file' => $fattura->allegato_pdf_path ?: ($fattura->xml_path ?: null), + 'percorso_file' => $fattura->allegato_pdf_path ?: ($fattura->xml_path ?: null), ]; // Compatibilità: alcune installazioni non hanno (più) certi campi su documenti. @@ -257,12 +256,12 @@ public function ensureDocumentoProtocollato(FatturaElettronica $fattura, ?int $u $createData = array_merge($docData, [ 'documentable_type' => FatturaElettronica::class, - 'documentable_id' => $fattura->id, - 'stabile_id' => (int) $fattura->stabile_id, - 'data_upload' => now(), - 'approvato' => false, - 'archiviato' => false, - 'is_demo' => false, + 'documentable_id' => $fattura->id, + 'stabile_id' => (int) $fattura->stabile_id, + 'data_upload' => now(), + 'approvato' => false, + 'archiviato' => false, + 'is_demo' => false, ]); foreach (array_keys($createData) as $col) { @@ -310,7 +309,7 @@ private function resolveGestioneForYear(Stabile $stabile, int $year): ?GestioneC private function getDocumentoProtocolColumn(): ?string { - static $cached = null; + static $cached = null; static $resolved = false; if ($resolved) { @@ -339,22 +338,33 @@ private function getDocumentoProtocolColumn(): ?string private function generateFallbackPdfForFattura(FatturaElettronica $fattura, string $protocollo): string { - $lines = []; + $lines = []; $lines[] = 'NETGESCON - Prospetto fattura (generato automaticamente)'; $lines[] = 'Protocollo: ' . $protocollo; $lines[] = ''; $fornitore = trim((string) ($fattura->fornitore_denominazione ?? '')); - $num = trim((string) ($fattura->numero_fattura ?? '')); - $data = $fattura->data_fattura ? $fattura->data_fattura->format('d/m/Y') : ''; - $tot = is_numeric($fattura->totale ?? null) ? number_format((float) $fattura->totale, 2, ',', '.') : ''; - $piva = trim((string) ($fattura->fornitore_piva ?? '')); - $cf = trim((string) ($fattura->fornitore_cf ?? '')); + $num = trim((string) ($fattura->numero_fattura ?? '')); + $data = $fattura->data_fattura ? $fattura->data_fattura->format('d/m/Y') : ''; + $tot = is_numeric($fattura->totale ?? null) ? number_format((float) $fattura->totale, 2, ',', '.') : ''; + $piva = trim((string) ($fattura->fornitore_piva ?? '')); + $cf = trim((string) ($fattura->fornitore_cf ?? '')); - if ($fornitore !== '') $lines[] = 'Fornitore: ' . $fornitore; - if ($piva !== '' || $cf !== '') $lines[] = 'P.IVA/CF: ' . ($piva !== '' ? $piva : '—') . ' / ' . ($cf !== '' ? $cf : '—'); - if ($num !== '' || $data !== '') $lines[] = 'Fattura: ' . ($num !== '' ? $num : '—') . ' - ' . ($data !== '' ? $data : '—'); - if ($tot !== '') $lines[] = 'Totale: EUR ' . $tot; + if ($fornitore !== '') { + $lines[] = 'Fornitore: ' . $fornitore; + } + + if ($piva !== '' || $cf !== '') { + $lines[] = 'P.IVA/CF: ' . ($piva !== '' ? $piva : '—') . ' / ' . ($cf !== '' ? $cf : '—'); + } + + if ($num !== '' || $data !== '') { + $lines[] = 'Fattura: ' . ($num !== '' ? $num : '—') . ' - ' . ($data !== '' ? $data : '—'); + } + + if ($tot !== '') { + $lines[] = 'Totale: EUR ' . $tot; + } $lines[] = ''; $lines[] = 'Nota: il PDF originale non era disponibile; questo documento serve solo per archiviazione e protocollazione.'; @@ -451,7 +461,7 @@ private function transformFatturaPaXmlToHtmlWithAssosoftwareXsl(string $xml): ?s private function injectProtocolloIntoHtml(string $html, string $protocollo): string { - $safe = htmlspecialchars($protocollo, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + $safe = htmlspecialchars($protocollo, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); $badge = '
Protocollo: ' . $safe . '
'; // Inserisce subito dopo l'apertura del body, se possibile. @@ -527,23 +537,23 @@ private function buildSimplePdf(array $lines): string } $content .= "\nET"; - $objects = []; - $objects[] = "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n"; - $objects[] = "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $objects[] = "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>\nendobj\n"; - $objects[] = "4 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n"; - $objects[] = "5 0 obj\n<< /Length " . strlen($content) . " >>\nstream\n" . $content . "\nendstream\nendobj\n"; + $objects = []; + $objects[] = "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n"; + $objects[] = "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $objects[] = "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>\nendobj\n"; + $objects[] = "4 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n"; + $objects[] = "5 0 obj\n<< /Length " . strlen($content) . " >>\nstream\n" . $content . "\nendstream\nendobj\n"; - $pdf = "%PDF-1.4\n"; + $pdf = "%PDF-1.4\n"; $offsets = [0]; foreach ($objects as $obj) { - $offsets[] = strlen($pdf); - $pdf .= $obj; + $offsets[] = strlen($pdf); + $pdf .= $obj; } - $xrefPos = strlen($pdf); - $pdf .= "xref\n0 " . (count($objects) + 1) . "\n"; - $pdf .= "0000000000 65535 f \n"; + $xrefPos = strlen($pdf); + $pdf .= "xref\n0 " . (count($objects) + 1) . "\n"; + $pdf .= "0000000000 65535 f \n"; for ($i = 1; $i <= count($objects); $i++) { $pdf .= str_pad((string) $offsets[$i], 10, '0', STR_PAD_LEFT) . " 00000 n \n"; } @@ -600,8 +610,8 @@ private function syncDocumentoAndPaths(FatturaElettronica $fattura, Documento $d if ($fatturaPath && (! is_string($docPath) || $docPath === '' || ! Storage::disk('local')->exists($docPath))) { $doc->forceFill([ - 'nome_file' => $fatturaName, - 'path_file' => $fatturaPath, + 'nome_file' => $fatturaName, + 'path_file' => $fatturaPath, 'percorso_file' => $fatturaPath, ])->save(); } @@ -624,10 +634,10 @@ private function recoverMissingPathsFromBase(FatturaElettronica $fattura, string $files = Storage::disk('local')->files($base); - $needleNumero = trim((string) $this->sanitizeFilename((string) ($fattura->numero_fattura ?? ''))); + $needleNumero = trim((string) $this->sanitizeFilename((string) ($fattura->numero_fattura ?? ''))); $needleFornitore = trim((string) $this->sanitizeFilename((string) ($fattura->fornitore_denominazione ?? ''))); - $needlePiva = trim((string) ($fattura->fornitore_piva ?? '')); - $needleCf = trim((string) ($fattura->fornitore_cf ?? '')); + $needlePiva = trim((string) ($fattura->fornitore_piva ?? '')); + $needleCf = trim((string) ($fattura->fornitore_cf ?? '')); // Evita agganci sbagliati: se il numero è vuoto o troppo corto (es. "1", "2"), non proviamo recovery. // In quei casi è più sicuro rigenerare da XML. @@ -646,14 +656,14 @@ private function recoverMissingPathsFromBase(FatturaElettronica $fattura, string foreach ($files as $file) { $name = basename($file); - $nameUpper = mb_strtoupper($name); - $numUpper = mb_strtoupper($needleNumero); - $fornUpper = mb_strtoupper($needleFornitore); - $matchNumero = str_contains($nameUpper, $numUpper); + $nameUpper = mb_strtoupper($name); + $numUpper = mb_strtoupper($needleNumero); + $fornUpper = mb_strtoupper($needleFornitore); + $matchNumero = str_contains($nameUpper, $numUpper); $matchFornitore = str_contains($nameUpper, $fornUpper); - $matchPiva = ($needlePiva !== '' && str_contains($nameUpper, $needlePiva)); - $matchCf = ($needleCf !== '' && str_contains($nameUpper, $needleCf)); - $strongMatch = $matchNumero && ($matchFornitore || $matchPiva || $matchCf); + $matchPiva = ($needlePiva !== '' && str_contains($nameUpper, $needlePiva)); + $matchCf = ($needleCf !== '' && str_contains($nameUpper, $needleCf)); + $strongMatch = $matchNumero && ($matchFornitore || $matchPiva || $matchCf); if (! $pdfOk && str_ends_with(strtolower($name), '.pdf')) { if ($strongMatch) { @@ -671,15 +681,15 @@ private function recoverMissingPathsFromBase(FatturaElettronica $fattura, string if (! $pdfOk && count($pdfCandidates) === 1) { $fattura->allegato_pdf_path = $pdfCandidates[0]['path']; $fattura->allegato_pdf_nome = $pdfCandidates[0]['name']; - $pdfOk = true; - $needsSave = true; + $pdfOk = true; + $needsSave = true; } if (! $xmlOk && count($xmlCandidates) === 1) { - $fattura->xml_path = $xmlCandidates[0]['path']; + $fattura->xml_path = $xmlCandidates[0]['path']; $fattura->nome_file_xml = $xmlCandidates[0]['name']; - $xmlOk = true; - $needsSave = true; + $xmlOk = true; + $needsSave = true; } if ($needsSave) { diff --git a/app/Support/ArchivioPaths.php b/app/Support/ArchivioPaths.php index 5d972f2..9f2b95b 100644 --- a/app/Support/ArchivioPaths.php +++ b/app/Support/ArchivioPaths.php @@ -3,8 +3,8 @@ use App\Models\Amministratore; use App\Models\DatiBancari; -use App\Models\GestioneContabile; use App\Models\Fornitore; +use App\Models\GestioneContabile; use App\Models\Stabile; use App\Models\User; @@ -87,7 +87,7 @@ public static function gestioneFolderName(?GestioneContabile $gestione, ?int $fa return $prefix . $year; } - public static function fattureXmlYearPath(Stabile $stabile, int|string $year, ?Amministratore $admin = null): ?string + public static function fattureXmlYearPath(Stabile $stabile, int | string $year, ?Amministratore $admin = null): ?string { $base = self::stabileBase($stabile, $admin); if (! is_string($base) || $base === '') { @@ -113,11 +113,11 @@ public static function bankAccountFolderName(DatiBancari $conto): string $bankCode = match (true) { str_contains($bankLabel, 'monte') || str_contains($bankLabel, 'paschi') || str_contains($bankLabel, 'mps') => 'MPS', - str_contains($bankLabel, 'intesa') => 'INTESA', - str_contains($bankLabel, 'unicredit') => 'UNICREDIT', - str_contains($bankLabel, 'bper') => 'BPER', - str_contains($bankLabel, 'bcc') => 'BCC', - default => strtoupper(self::sanitizeFolder((string) ($conto->denominazione_banca ?: 'BANCA'))), + str_contains($bankLabel, 'intesa') => 'INTESA', + str_contains($bankLabel, 'unicredit') => 'UNICREDIT', + str_contains($bankLabel, 'bper') => 'BPER', + str_contains($bankLabel, 'bcc') => 'BCC', + default => strtoupper(self::sanitizeFolder((string) ($conto->denominazione_banca ?: 'BANCA'))), }; if ($bankCode === '') { @@ -133,7 +133,7 @@ public static function bankAccountFolderName(DatiBancari $conto): string $identifier = self::sanitizeFolder((string) ($conto->legacy_cod_cassa ?? '')); } if ($identifier === '') { - $iban = strtoupper(preg_replace('/[^A-Z0-9]+/', '', (string) ($conto->iban ?? '')) ?? ''); + $iban = strtoupper(preg_replace('/[^A-Z0-9]+/', '', (string) ($conto->iban ?? '')) ?? ''); $identifier = $iban !== '' ? substr($iban, -8) : ''; } if ($identifier === '') { @@ -143,7 +143,7 @@ public static function bankAccountFolderName(DatiBancari $conto): string return $bankCode . '-' . $typeCode . '-' . $identifier; } - public static function bankYearPath(Stabile $stabile, DatiBancari $conto, int|string $year, ?Amministratore $admin = null): ?string + public static function bankYearPath(Stabile $stabile, DatiBancari $conto, int | string $year, ?Amministratore $admin = null): ?string { $base = self::stabileBase($stabile, $admin); if (! is_string($base) || $base === '') { diff --git a/config/app.php b/config/app.php index 49335df..71382c9 100755 --- a/config/app.php +++ b/config/app.php @@ -1,7 +1,8 @@ env('APP_NAME', 'NetGescon'), + 'name' => env('APP_NAME', 'NetGescon'), /* |-------------------------------------------------------------------------- @@ -19,7 +20,7 @@ |-------------------------------------------------------------------------- */ - 'env' => env('APP_ENV', 'production'), + 'env' => env('APP_ENV', 'production'), /* |-------------------------------------------------------------------------- @@ -27,7 +28,7 @@ |-------------------------------------------------------------------------- */ - 'debug' => (bool) env('APP_DEBUG', false), + 'debug' => (bool) env('APP_DEBUG', false), /* |-------------------------------------------------------------------------- @@ -35,11 +36,11 @@ |-------------------------------------------------------------------------- */ - 'url' => env('APP_URL', 'http://localhost'), + 'url' => env('APP_URL', 'http://localhost'), - 'public_url' => env('APP_PUBLIC_URL', env('APP_URL', 'http://localhost')), + 'public_url' => env('APP_PUBLIC_URL', env('APP_URL', 'http://localhost')), - 'asset_url' => env('ASSET_URL'), + 'asset_url' => env('ASSET_URL'), /* |-------------------------------------------------------------------------- @@ -47,7 +48,7 @@ |-------------------------------------------------------------------------- */ - 'timezone' => 'UTC', + 'timezone' => 'UTC', /* |-------------------------------------------------------------------------- @@ -55,11 +56,11 @@ |-------------------------------------------------------------------------- */ - 'locale' => 'it', + 'locale' => 'it', 'fallback_locale' => 'it', - 'faker_locale' => 'en_US', + 'faker_locale' => 'en_US', /* |-------------------------------------------------------------------------- @@ -67,9 +68,9 @@ |-------------------------------------------------------------------------- */ - 'key' => env('APP_KEY'), + 'key' => env('APP_KEY'), - 'cipher' => 'AES-256-CBC', + 'cipher' => 'AES-256-CBC', /* |-------------------------------------------------------------------------- @@ -77,7 +78,7 @@ |-------------------------------------------------------------------------- */ - 'maintenance' => [ + 'maintenance' => [ 'driver' => 'file', // 'store' => 'redis', ], @@ -88,7 +89,7 @@ |-------------------------------------------------------------------------- */ - 'providers' => ServiceProvider::defaultProviders()->merge([ + 'providers' => ServiceProvider::defaultProviders()->merge([ /* * Application Service Providers... */ @@ -101,14 +102,13 @@ ])->toArray(), - /* |-------------------------------------------------------------------------- | Class Aliases |-------------------------------------------------------------------------- */ - 'aliases' => Facade::defaultAliases()->merge([ + 'aliases' => Facade::defaultAliases()->merge([ // 'Example' => App\Facades\Example::class, ])->toArray(), diff --git a/config/contabilita.php b/config/contabilita.php index d572730..a9f1c49 100644 --- a/config/contabilita.php +++ b/config/contabilita.php @@ -3,49 +3,49 @@ return [ // Conti tecnici usati per chiusure/aperture automatiche. // Il codice del conto è già alfanumerico (string): puoi usare anche valori tipo "CHIUSURA". - 'conti_speciali' => [ - 'chiusura_codice' => env('CONTABILITA_CONTO_CHIUSURA', '9999'), - 'chiusura_descrizione' => env('CONTABILITA_CONTO_CHIUSURA_DESC', 'Conto chiusura esercizio'), - 'chiusura_tipo' => env('CONTABILITA_CONTO_CHIUSURA_TIPO', 'Patrimonio Netto'), + 'conti_speciali' => [ + 'chiusura_codice' => env('CONTABILITA_CONTO_CHIUSURA', '9999'), + 'chiusura_descrizione' => env('CONTABILITA_CONTO_CHIUSURA_DESC', 'Conto chiusura esercizio'), + 'chiusura_tipo' => env('CONTABILITA_CONTO_CHIUSURA_TIPO', 'Patrimonio Netto'), - 'apertura_codice' => env('CONTABILITA_CONTO_APERTURA', '9998'), - 'apertura_descrizione' => env('CONTABILITA_CONTO_APERTURA_DESC', 'Conto apertura esercizio'), - 'apertura_tipo' => env('CONTABILITA_CONTO_APERTURA_TIPO', 'Patrimonio Netto'), + 'apertura_codice' => env('CONTABILITA_CONTO_APERTURA', '9998'), + 'apertura_descrizione' => env('CONTABILITA_CONTO_APERTURA_DESC', 'Conto apertura esercizio'), + 'apertura_tipo' => env('CONTABILITA_CONTO_APERTURA_TIPO', 'Patrimonio Netto'), // Incassi rate: banca/cassa a Dare, crediti condòmini a Avere. - 'crediti_condomini_codice' => env('CONTABILITA_CONTO_CREDITI_CONDOMINI', '1100'), - 'crediti_condomini_descrizione' => env('CONTABILITA_CONTO_CREDITI_CONDOMINI_DESC', 'Crediti verso condomini'), - 'crediti_condomini_tipo' => env('CONTABILITA_CONTO_CREDITI_CONDOMINI_TIPO', 'Attività'), + 'crediti_condomini_codice' => env('CONTABILITA_CONTO_CREDITI_CONDOMINI', '1100'), + 'crediti_condomini_descrizione' => env('CONTABILITA_CONTO_CREDITI_CONDOMINI_DESC', 'Crediti verso condomini'), + 'crediti_condomini_tipo' => env('CONTABILITA_CONTO_CREDITI_CONDOMINI_TIPO', 'Attività'), // Fatture fornitori: costi (righe) + IVA a credito a Dare; debiti fornitori + ritenute a Avere. - 'debiti_fornitori_codice' => env('CONTABILITA_CONTO_DEBITI_FORNITORI', '2000'), - 'debiti_fornitori_descrizione' => env('CONTABILITA_CONTO_DEBITI_FORNITORI_DESC', 'Debiti verso fornitori'), - 'debiti_fornitori_tipo' => env('CONTABILITA_CONTO_DEBITI_FORNITORI_TIPO', 'Passività'), + 'debiti_fornitori_codice' => env('CONTABILITA_CONTO_DEBITI_FORNITORI', '2000'), + 'debiti_fornitori_descrizione' => env('CONTABILITA_CONTO_DEBITI_FORNITORI_DESC', 'Debiti verso fornitori'), + 'debiti_fornitori_tipo' => env('CONTABILITA_CONTO_DEBITI_FORNITORI_TIPO', 'Passività'), - 'iva_credito_codice' => env('CONTABILITA_CONTO_IVA_CREDITO', '1210'), - 'iva_credito_descrizione' => env('CONTABILITA_CONTO_IVA_CREDITO_DESC', 'IVA a credito'), - 'iva_credito_tipo' => env('CONTABILITA_CONTO_IVA_CREDITO_TIPO', 'Attività'), + 'iva_credito_codice' => env('CONTABILITA_CONTO_IVA_CREDITO', '1210'), + 'iva_credito_descrizione' => env('CONTABILITA_CONTO_IVA_CREDITO_DESC', 'IVA a credito'), + 'iva_credito_tipo' => env('CONTABILITA_CONTO_IVA_CREDITO_TIPO', 'Attività'), - 'ritenute_da_versare_codice' => env('CONTABILITA_CONTO_RITENUTE_DA_VERSARE', '2010'), - 'ritenute_da_versare_descrizione' => env('CONTABILITA_CONTO_RITENUTE_DA_VERSARE_DESC', 'Erario c/ritenute da versare'), - 'ritenute_da_versare_tipo' => env('CONTABILITA_CONTO_RITENUTE_DA_VERSARE_TIPO', 'Passività'), + 'ritenute_da_versare_codice' => env('CONTABILITA_CONTO_RITENUTE_DA_VERSARE', '2010'), + 'ritenute_da_versare_descrizione' => env('CONTABILITA_CONTO_RITENUTE_DA_VERSARE_DESC', 'Erario c/ritenute da versare'), + 'ritenute_da_versare_tipo' => env('CONTABILITA_CONTO_RITENUTE_DA_VERSARE_TIPO', 'Passività'), // Bucket patrimoniali e di quadratura da usare nella riconciliazione operativa. - 'fondo_riserva_codice' => env('CONTABILITA_CONTO_FONDO_RISERVA', '3100'), - 'fondo_riserva_descrizione' => env('CONTABILITA_CONTO_FONDO_RISERVA_DESC', 'Fondo di riserva'), - 'fondo_riserva_tipo' => env('CONTABILITA_CONTO_FONDO_RISERVA_TIPO', 'Patrimonio Netto'), + 'fondo_riserva_codice' => env('CONTABILITA_CONTO_FONDO_RISERVA', '3100'), + 'fondo_riserva_descrizione' => env('CONTABILITA_CONTO_FONDO_RISERVA_DESC', 'Fondo di riserva'), + 'fondo_riserva_tipo' => env('CONTABILITA_CONTO_FONDO_RISERVA_TIPO', 'Patrimonio Netto'), - 'accantonamenti_codice' => env('CONTABILITA_CONTO_ACCANTONAMENTI', '3110'), - 'accantonamenti_descrizione' => env('CONTABILITA_CONTO_ACCANTONAMENTI_DESC', 'Accantonamenti da destinare'), - 'accantonamenti_tipo' => env('CONTABILITA_CONTO_ACCANTONAMENTI_TIPO', 'Patrimonio Netto'), + 'accantonamenti_codice' => env('CONTABILITA_CONTO_ACCANTONAMENTI', '3110'), + 'accantonamenti_descrizione' => env('CONTABILITA_CONTO_ACCANTONAMENTI_DESC', 'Accantonamenti da destinare'), + 'accantonamenti_tipo' => env('CONTABILITA_CONTO_ACCANTONAMENTI_TIPO', 'Patrimonio Netto'), - 'rimborsi_da_distribuire_codice' => env('CONTABILITA_CONTO_RIMBORSI_DISTRIBUIRE', '2105'), + 'rimborsi_da_distribuire_codice' => env('CONTABILITA_CONTO_RIMBORSI_DISTRIBUIRE', '2105'), 'rimborsi_da_distribuire_descrizione' => env('CONTABILITA_CONTO_RIMBORSI_DISTRIBUIRE_DESC', 'Rimborsi da distribuire'), - 'rimborsi_da_distribuire_tipo' => env('CONTABILITA_CONTO_RIMBORSI_DISTRIBUIRE_TIPO', 'Passività'), + 'rimborsi_da_distribuire_tipo' => env('CONTABILITA_CONTO_RIMBORSI_DISTRIBUIRE_TIPO', 'Passività'), - 'rimborsi_da_utilizzare_codice' => env('CONTABILITA_CONTO_RIMBORSI_UTILIZZARE', '1115'), - 'rimborsi_da_utilizzare_descrizione' => env('CONTABILITA_CONTO_RIMBORSI_UTILIZZARE_DESC', 'Rimborsi da utilizzare'), - 'rimborsi_da_utilizzare_tipo' => env('CONTABILITA_CONTO_RIMBORSI_UTILIZZARE_TIPO', 'Attività'), + 'rimborsi_da_utilizzare_codice' => env('CONTABILITA_CONTO_RIMBORSI_UTILIZZARE', '1115'), + 'rimborsi_da_utilizzare_descrizione' => env('CONTABILITA_CONTO_RIMBORSI_UTILIZZARE_DESC', 'Rimborsi da utilizzare'), + 'rimborsi_da_utilizzare_tipo' => env('CONTABILITA_CONTO_RIMBORSI_UTILIZZARE_TIPO', 'Attività'), ], 'fatture_elettroniche' => [ diff --git a/config/services.php b/config/services.php index a20f936..be3b959 100755 --- a/config/services.php +++ b/config/services.php @@ -49,25 +49,25 @@ ], 'amazon' => [ - 'referral_tag' => env('AMAZON_REFERRAL_TAG', ''), - 'marketplace' => env('AMAZON_MARKETPLACE', 'www.amazon.it'), - 'creators_enabled' => (bool) env('AMAZON_CREATORS_ENABLED', false), - 'creators_api_base_url' => env('AMAZON_CREATORS_API_BASE_URL', ''), - 'creators_token_url' => env('AMAZON_CREATORS_TOKEN_URL', ''), - 'creators_credential_id' => env('AMAZON_CREATORS_CREDENTIAL_ID', ''), - 'creators_credential_secret' => env('AMAZON_CREATORS_CREDENTIAL_SECRET', ''), + 'referral_tag' => env('AMAZON_REFERRAL_TAG', ''), + 'marketplace' => env('AMAZON_MARKETPLACE', 'www.amazon.it'), + 'creators_enabled' => (bool) env('AMAZON_CREATORS_ENABLED', false), + 'creators_api_base_url' => env('AMAZON_CREATORS_API_BASE_URL', ''), + 'creators_token_url' => env('AMAZON_CREATORS_TOKEN_URL', ''), + 'creators_credential_id' => env('AMAZON_CREATORS_CREDENTIAL_ID', ''), + 'creators_credential_secret' => env('AMAZON_CREATORS_CREDENTIAL_SECRET', ''), 'creators_credential_version' => env('AMAZON_CREATORS_CREDENTIAL_VERSION', 'v3.2'), - 'creators_timeout' => (int) env('AMAZON_CREATORS_TIMEOUT', 20), - 'creators_paths' => [ + 'creators_timeout' => (int) env('AMAZON_CREATORS_TIMEOUT', 20), + 'creators_paths' => [ 'searchItems' => env('AMAZON_CREATORS_SEARCH_PATH', ''), 'getItems' => env('AMAZON_CREATORS_GET_ITEMS_PATH', ''), ], ], 'telegram' => [ - 'offers_bot_token' => env('TELEGRAM_OFFERS_BOT_TOKEN', ''), - 'offers_chat_id' => env('TELEGRAM_OFFERS_CHAT_ID', ''), - 'offers_channel_name' => env('TELEGRAM_OFFERS_CHANNEL_NAME', ''), + 'offers_bot_token' => env('TELEGRAM_OFFERS_BOT_TOKEN', ''), + 'offers_chat_id' => env('TELEGRAM_OFFERS_CHAT_ID', ''), + 'offers_channel_name' => env('TELEGRAM_OFFERS_CHANNEL_NAME', ''), 'offers_webhook_secret' => env('TELEGRAM_OFFERS_WEBHOOK_SECRET', ''), ], diff --git a/database/migrations/2026_04_17_120000_add_statement_metadata_to_contabilita_saldi_conti.php b/database/migrations/2026_04_17_120000_add_statement_metadata_to_contabilita_saldi_conti.php index 09f11ff..3bf9a6e 100644 --- a/database/migrations/2026_04_17_120000_add_statement_metadata_to_contabilita_saldi_conti.php +++ b/database/migrations/2026_04_17_120000_add_statement_metadata_to_contabilita_saldi_conti.php @@ -53,4 +53,4 @@ public function down(): void } }); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2026_04_19_180000_add_is_partenza_contabile_to_contabilita_saldi_conti_table.php b/database/migrations/2026_04_19_180000_add_is_partenza_contabile_to_contabilita_saldi_conti_table.php new file mode 100644 index 0000000..0ce5870 --- /dev/null +++ b/database/migrations/2026_04_19_180000_add_is_partenza_contabile_to_contabilita_saldi_conti_table.php @@ -0,0 +1,30 @@ +boolean('is_partenza_contabile') + ->default(false) + ->after('saldo') + ->index(); + } + }); + } + + public function down(): void + { + Schema::table('contabilita_saldi_conti', function (Blueprint $table): void { + if (Schema::hasColumn('contabilita_saldi_conti', 'is_partenza_contabile')) { + $table->dropIndex(['is_partenza_contabile']); + $table->dropColumn('is_partenza_contabile'); + } + }); + } +}; diff --git a/database/migrations/2026_04_20_180000_add_presa_in_carico_to_affitti_immobili.php b/database/migrations/2026_04_20_180000_add_presa_in_carico_to_affitti_immobili.php new file mode 100644 index 0000000..7f91b2b --- /dev/null +++ b/database/migrations/2026_04_20_180000_add_presa_in_carico_to_affitti_immobili.php @@ -0,0 +1,28 @@ +date('presa_in_carico_operativa_dal') + ->nullable() + ->after('inizio_contratto'); + } + }); + } + + public function down(): void + { + Schema::table('affitti_immobili', function (Blueprint $table) { + if (Schema::hasColumn('affitti_immobili', 'presa_in_carico_operativa_dal')) { + $table->dropColumn('presa_in_carico_operativa_dal'); + } + }); + } +}; diff --git a/database/migrations/2026_04_20_210000_create_documento_archivio_cartelle_table.php b/database/migrations/2026_04_20_210000_create_documento_archivio_cartelle_table.php index e164f13..2607660 100644 --- a/database/migrations/2026_04_20_210000_create_documento_archivio_cartelle_table.php +++ b/database/migrations/2026_04_20_210000_create_documento_archivio_cartelle_table.php @@ -54,4 +54,4 @@ public function down(): void Schema::dropIfExists('documento_archivio_cartelle'); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2026_04_20_220000_add_allowed_user_ids_to_documenti_table.php b/database/migrations/2026_04_20_220000_add_allowed_user_ids_to_documenti_table.php index 3614e18..ef54186 100644 --- a/database/migrations/2026_04_20_220000_add_allowed_user_ids_to_documenti_table.php +++ b/database/migrations/2026_04_20_220000_add_allowed_user_ids_to_documenti_table.php @@ -23,4 +23,4 @@ public function down(): void } }); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2026_04_22_120000_add_sia_to_fornitori_table.php b/database/migrations/2026_04_22_120000_add_sia_to_fornitori_table.php new file mode 100644 index 0000000..32e6e2f --- /dev/null +++ b/database/migrations/2026_04_22_120000_add_sia_to_fornitori_table.php @@ -0,0 +1,30 @@ +string('sia', 32)->nullable()->after('bic')->index(); + }); + } + + public function down(): void + { + if (! Schema::hasTable('fornitori') || ! Schema::hasColumn('fornitori', 'sia')) { + return; + } + + Schema::table('fornitori', function (Blueprint $table): void { + $table->dropColumn('sia'); + }); + } +}; diff --git a/database/migrations/2026_04_22_140000_create_stabile_contratti_table.php b/database/migrations/2026_04_22_140000_create_stabile_contratti_table.php new file mode 100644 index 0000000..00a97e7 --- /dev/null +++ b/database/migrations/2026_04_22_140000_create_stabile_contratti_table.php @@ -0,0 +1,45 @@ +id(); + $table->foreignId('stabile_id')->constrained('stabili')->cascadeOnDelete(); + $table->foreignId('fornitore_id')->nullable()->constrained('fornitori')->nullOnDelete(); + $table->foreignId('documento_principale_id')->nullable()->constrained('documenti')->nullOnDelete(); + $table->string('titolo'); + $table->string('tipo_contratto', 60)->index(); + $table->string('categoria_impianto', 60)->nullable()->index(); + $table->string('servizio_label')->nullable(); + $table->string('codice_contratto', 80)->nullable()->index(); + $table->string('riferimento_esterno', 120)->nullable(); + $table->string('stato', 30)->default('attivo')->index(); + $table->date('data_stipula')->nullable(); + $table->date('decorrenza_dal')->nullable()->index(); + $table->date('decorrenza_al')->nullable()->index(); + $table->string('frequenza_scadenze', 30)->nullable(); + $table->unsignedTinyInteger('giorno_scadenza')->nullable(); + $table->unsignedSmallInteger('giorni_preavviso')->default(30); + $table->boolean('rinnovo_automatico')->default(false); + $table->decimal('importo_periodico', 12, 2)->nullable(); + $table->string('valuta', 8)->default('EUR'); + $table->json('dati_contratto')->nullable(); + $table->json('codici_collegati')->nullable(); + $table->text('note')->nullable(); + $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete(); + $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('stabile_contratti'); + } +}; diff --git a/database/migrations/2026_04_22_140100_add_stabile_contratto_id_to_scadenze_table.php b/database/migrations/2026_04_22_140100_add_stabile_contratto_id_to_scadenze_table.php new file mode 100644 index 0000000..64ec469 --- /dev/null +++ b/database/migrations/2026_04_22_140100_add_stabile_contratto_id_to_scadenze_table.php @@ -0,0 +1,30 @@ +foreignId('stabile_contratto_id')->nullable()->after('stabile_id')->constrained('stabile_contratti')->nullOnDelete(); + }); + } + + public function down(): void + { + if (! Schema::hasTable('scadenze') || ! Schema::hasColumn('scadenze', 'stabile_contratto_id')) { + return; + } + + Schema::table('scadenze', function (Blueprint $table): void { + $table->dropConstrainedForeignId('stabile_contratto_id'); + }); + } +}; diff --git a/docker/docker-compose.node-local.yml b/docker/docker-compose.node-local.yml new file mode 100644 index 0000000..78674be --- /dev/null +++ b/docker/docker-compose.node-local.yml @@ -0,0 +1,81 @@ +services: + app: + image: netgescon/app:${NETGESCON_VERSION:-latest} + build: + context: .. + dockerfile: docker/Dockerfile.prod + container_name: ${COMPOSE_PROJECT_NAME:-netgescon_node}_app + restart: unless-stopped + env_file: + - .env.node-local + environment: + DB_CONNECTION: mysql + DB_HOST: db + DB_PORT: 3306 + REDIS_HOST: redis + REDIS_PORT: 6379 + RUN_MIGRATIONS: ${RUN_MIGRATIONS:-true} + MIGRATIONS_STRICT: ${MIGRATIONS_STRICT:-true} + RUN_FILAMENT_ASSETS: ${RUN_FILAMENT_ASSETS:-false} + FIX_PERMISSIONS: ${FIX_PERMISSIONS:-true} + WAIT_FOR_DB: ${WAIT_FOR_DB:-true} + DISABLE_SCHEMA_LOAD: ${DISABLE_SCHEMA_LOAD:-true} + volumes: + - node_storage:/var/www/storage + - node_cache:/var/www/bootstrap/cache + - node_public:/var/www/public + - ${LEGACY_ARCHIVES_PATH:-/opt/netgescon/legacy-archives}:/mnt/gescon-archives/gescon:ro + depends_on: + - db + - redis + networks: + - netgescon_node_local + + nginx: + image: nginx:alpine + container_name: ${COMPOSE_PROJECT_NAME:-netgescon_node}_nginx + restart: unless-stopped + ports: + - "${NODE_HTTP_PORT:-8080}:80" + volumes: + - node_public:/var/www/public:ro + - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro + depends_on: + - app + networks: + - netgescon_node_local + + db: + image: mysql:8.0 + container_name: ${COMPOSE_PROJECT_NAME:-netgescon_node}_db + restart: unless-stopped + command: --log-bin-trust-function-creators=1 + environment: + MYSQL_DATABASE: ${DB_DATABASE} + MYSQL_USER: ${DB_USERNAME} + MYSQL_PASSWORD: ${DB_PASSWORD} + MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} + volumes: + - node_db:/var/lib/mysql + networks: + - netgescon_node_local + + redis: + image: redis:alpine + container_name: ${COMPOSE_PROJECT_NAME:-netgescon_node}_redis + restart: unless-stopped + volumes: + - node_redis:/data + networks: + - netgescon_node_local + +volumes: + node_db: + node_storage: + node_cache: + node_public: + node_redis: + +networks: + netgescon_node_local: + driver: bridge diff --git a/docker/install-node-local.sh b/docker/install-node-local.sh new file mode 100644 index 0000000..606451f --- /dev/null +++ b/docker/install-node-local.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +set -euo pipefail + +DOCKER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$DOCKER_DIR/.." && pwd)" +ENV_FILE="${ENV_FILE:-$DOCKER_DIR/.env.node-local}" +COMPOSE_FILE="$DOCKER_DIR/docker-compose.node-local.yml" + +resolve_compose_cmd() { + if command -v docker >/dev/null 2>&1; then + if docker compose version >/dev/null 2>&1; then + echo "docker compose" + return + fi + fi + + if command -v docker-compose >/dev/null 2>&1; then + echo "docker-compose" + return + fi + + echo "Errore: non trovo né 'docker compose' né 'docker-compose'." >&2 + exit 127 +} + +upsert_env_var() { + local key="$1" + local value="$2" + + if grep -qE "^${key}=" "$ENV_FILE"; then + sed -i "s#^${key}=.*#${key}=${value}#" "$ENV_FILE" + else + printf '\n%s=%s\n' "$key" "$value" >> "$ENV_FILE" + fi +} + +COMPOSE_CMD="$(resolve_compose_cmd)" + +if [[ ! -f "$ENV_FILE" ]]; then + echo "Env mancante: $ENV_FILE" + echo "Crea prima il file: cp $DOCKER_DIR/.env.node-local.example $ENV_FILE" + exit 1 +fi + +source "$ENV_FILE" + +if [[ -z "${APP_KEY:-}" ]]; then + GENERATED_APP_KEY="base64:$(openssl rand -base64 32 | tr -d '\n')" + upsert_env_var "APP_KEY" "$GENERATED_APP_KEY" + APP_KEY="$GENERATED_APP_KEY" + echo "APP_KEY generata automaticamente in $ENV_FILE" +fi + +if [[ -n "${LEGACY_ARCHIVES_PATH:-}" ]]; then + mkdir -p "$LEGACY_ARCHIVES_PATH" +fi + +mkdir -p "$ROOT_DIR/storage/backups" + +echo "[1/6] Validazione compose" +$COMPOSE_CMD --env-file "$ENV_FILE" -f "$COMPOSE_FILE" config >/tmp/netgescon-node-local.config + +echo "[2/6] Build immagine applicativa" +$COMPOSE_CMD --env-file "$ENV_FILE" -f "$COMPOSE_FILE" build app + +echo "[3/6] Avvio database e redis" +$COMPOSE_CMD --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d db redis + +echo "[4/6] Avvio applicazione e nginx" +$COMPOSE_CMD --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d app nginx + +echo "[5/6] Creazione database import legacy" +$COMPOSE_CMD --env-file "$ENV_FILE" -f "$COMPOSE_FILE" exec -T db sh -lc \ + "mysql -uroot -p\"$DB_ROOT_PASSWORD\" -e \"CREATE DATABASE IF NOT EXISTS gescon_import; GRANT ALL PRIVILEGES ON gescon_import.* TO '$DB_USERNAME'@'%'; FLUSH PRIVILEGES;\"" + +echo "[6/6] Stato finale" +$COMPOSE_CMD --env-file "$ENV_FILE" -f "$COMPOSE_FILE" ps + +echo +echo "Installazione nodo locale completata." +echo "URL applicazione: ${APP_URL:-http://localhost:${NODE_HTTP_PORT:-8080}}" +echo "Backup/restore: usa docker/restore-node-backup.sh dopo aver copiato dump SQL e archivio storage sulla VM." diff --git a/docker/restore-node-backup.sh b/docker/restore-node-backup.sh new file mode 100644 index 0000000..3596b04 --- /dev/null +++ b/docker/restore-node-backup.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +set -euo pipefail + +DOCKER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ENV_FILE="${ENV_FILE:-$DOCKER_DIR/.env.node-local}" +COMPOSE_FILE="$DOCKER_DIR/docker-compose.node-local.yml" +SQL_DUMP="" +STORAGE_ARCHIVE="" + +resolve_compose_cmd() { + if command -v docker >/dev/null 2>&1; then + if docker compose version >/dev/null 2>&1; then + echo "docker compose" + return + fi + fi + + if command -v docker-compose >/dev/null 2>&1; then + echo "docker-compose" + return + fi + + echo "Errore: non trovo né 'docker compose' né 'docker-compose'." >&2 + exit 127 +} + +usage() { + cat <&2 + usage + exit 1 + ;; + esac +done + +if [[ -z "$SQL_DUMP" ]]; then + usage + exit 1 +fi + +if [[ ! -f "$ENV_FILE" ]]; then + echo "Env mancante: $ENV_FILE" + exit 1 +fi + +if [[ ! -f "$SQL_DUMP" ]]; then + echo "Dump SQL non trovato: $SQL_DUMP" + exit 1 +fi + +if [[ -n "$STORAGE_ARCHIVE" && ! -f "$STORAGE_ARCHIVE" ]]; then + echo "Archivio storage non trovato: $STORAGE_ARCHIVE" + exit 1 +fi + +COMPOSE_CMD="$(resolve_compose_cmd)" +source "$ENV_FILE" + +echo "[1/4] Avvio stack" +$COMPOSE_CMD --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d db redis app nginx + +echo "[2/4] Ripristino database principale" +if [[ "$SQL_DUMP" == *.gz ]]; then + gzip -dc "$SQL_DUMP" | $COMPOSE_CMD --env-file "$ENV_FILE" -f "$COMPOSE_FILE" exec -T db sh -lc "mysql -uroot -p\"$DB_ROOT_PASSWORD\" \"$DB_DATABASE\"" +else + cat "$SQL_DUMP" | $COMPOSE_CMD --env-file "$ENV_FILE" -f "$COMPOSE_FILE" exec -T db sh -lc "mysql -uroot -p\"$DB_ROOT_PASSWORD\" \"$DB_DATABASE\"" +fi + +echo "[3/4] Ripristino storage applicativo" +if [[ -n "$STORAGE_ARCHIVE" ]]; then + cat "$STORAGE_ARCHIVE" | $COMPOSE_CMD --env-file "$ENV_FILE" -f "$COMPOSE_FILE" exec -T app sh -lc "tar -xzf - -C /var/www" +else + echo "Nessun archivio storage fornito: salto questo passaggio." +fi + +echo "[4/4] Pulizia cache Laravel" +$COMPOSE_CMD --env-file "$ENV_FILE" -f "$COMPOSE_FILE" exec -T app php artisan optimize:clear || true + +echo "Restore completato. Verifica login, storage documenti e connessioni locali del nodo." diff --git a/docs/03-scripts-automazione/INSTALLAZIONE-UBUNTU2404-AUTOMATICA.md b/docs/03-scripts-automazione/INSTALLAZIONE-UBUNTU2404-AUTOMATICA.md new file mode 100644 index 0000000..f35664c --- /dev/null +++ b/docs/03-scripts-automazione/INSTALLAZIONE-UBUNTU2404-AUTOMATICA.md @@ -0,0 +1,194 @@ +# Installazione Ubuntu 24.04 automatica + +Questa famiglia di script separa tre problemi diversi: + +1. `scripts/install/ubuntu2404-laravel-base.sh` + Prepara una macchina Ubuntu 24.04 nuova per progetti Laravel in generale. + Installa stack di base: PHP, Composer, Node.js, MySQL, Nginx, Redis, Supervisor. + Se non disattivato, installa anche XFCE, XRDP e Visual Studio Code. + +2. `scripts/install/laravel-site-bootstrap.sh` + Pubblica un singolo progetto Laravel su una macchina gia pronta. + Usa chiavi univoche per sito, file Nginx, cron e Supervisor. + Di default si rifiuta di sovrascrivere directory, vhost e database gia esistenti. + +3. `scripts/install/netgescon-node-bootstrap.sh` + Prende una macchina gia pronta con lo stack Laravel e installa un nodo NetGescon pronto per il login iniziale e per la pagina `admin-filament/supporto/aggiornamento-nodo`. + +## Installazione via curl + +Se il nodo che espone l'applicazione e raggiungibile, puoi usare direttamente gli endpoint pubblici in `public/installers`. + +Indice rapido browser: + +- `http://192.168.0.200:8000/installers/` +- `https://staging.netgescon.it/installers/` + +Script base macchina: + +```bash +curl -fsSL http://192.168.0.200:8000/installers/ubuntu2404-laravel-base.php -o /tmp/ubuntu2404-laravel-base.sh +sudo bash /tmp/ubuntu2404-laravel-base.sh + +curl -fsSL https://staging.netgescon.it/installers/ubuntu2404-laravel-base.php -o /tmp/ubuntu2404-laravel-base.sh +sudo bash /tmp/ubuntu2404-laravel-base.sh +``` + +Versione server-only senza XFCE, VS Code e XRDP: + +```bash +curl -fsSL http://192.168.0.200:8000/installers/ubuntu2404-laravel-base.php -o /tmp/ubuntu2404-laravel-base.sh +sudo bash /tmp/ubuntu2404-laravel-base.sh --without-xfce --without-vscode --without-xrdp +``` + +Bootstrap sito Laravel generico: + +```bash +curl -fsSL http://192.168.0.200:8000/installers/laravel-site-bootstrap.php -o /tmp/laravel-site-bootstrap.sh +sudo bash /tmp/laravel-site-bootstrap.sh \ + --site-key demo-app \ + --project-name "Demo App" \ + --repo-url git@your-gitea:studio/demo-app.git \ + --branch main \ + --app-url http://demo.example.local \ + --server-name demo.example.local \ + --db-password 'StrongDbPass123!' +``` + +Bootstrap nodo NetGescon: + +```bash +curl -fsSL http://192.168.0.200:8000/installers/netgescon-node-bootstrap.php -o /tmp/netgescon-node-bootstrap.sh +sudo bash /tmp/netgescon-node-bootstrap.sh \ + --site-key netgescon-prod \ + --repo-url git@your-gitea:studio/netgescon.git \ + --branch main \ + --app-url http://192.168.0.200 \ + --server-name 192.168.0.200 \ + --db-password 'StrongDbPass123!' \ + --admin-email admin@netgescon.local \ + --admin-password 'StrongAdminPass123!' +``` + +La stessa chiamata su staging puo diventare: + +```bash +curl -fsSL https://staging.netgescon.it/installers/netgescon-node-bootstrap.php -o /tmp/netgescon-node-bootstrap.sh +sudo bash /tmp/netgescon-node-bootstrap.sh \ + --site-key netgescon-staging \ + --repo-url git@your-gitea:studio/netgescon.git \ + --branch main \ + --app-url https://staging.netgescon.it \ + --server-name staging.netgescon.it \ + --db-password 'StrongDbPass123!' \ + --admin-email admin@netgescon.local \ + --admin-password 'StrongAdminPass123!' +``` + +## 1. Base machine Laravel + +Esecuzione consigliata: + +```bash +sudo bash scripts/install/ubuntu2404-laravel-base.sh +``` + +Varianti utili: + +```bash +sudo bash scripts/install/ubuntu2404-laravel-base.sh --without-xfce --without-vscode +sudo bash scripts/install/ubuntu2404-laravel-base.sh --projects-dir /srv/laravel-projects +``` + +Cosa fa: + +- aggiorna Ubuntu 24.04 +- mostra una schermata iniziale con stato corrente macchina e componenti gia presenti +- chiede prima di partire quali blocchi installare o saltare +- installa PHP 8.3 e moduli Laravel comuni +- installa Composer e Node.js +- installa MySQL, Nginx, Redis e Supervisor +- apre firewall per SSH, HTTP, HTTPS e XRDP se richiesto +- crea una base di lavoro per progetti Laravel in `/srv/laravel-projects` +- opzionalmente installa XFCE, XRDP e VS Code + +Se vuoi saltare la schermata interattiva e usare lo script in automazione, puoi aggiungere `--non-interactive` al comando. + +## 2. Bootstrap sito Laravel generico + +Esempio completo: + +```bash +sudo bash scripts/install/laravel-site-bootstrap.sh \ + --site-key frontoffice-demo \ + --project-name "Frontoffice Demo" \ + --repo-url git@your-gitea:studio/frontoffice-demo.git \ + --branch main \ + --app-url https://frontoffice-demo.example.it \ + --server-name frontoffice-demo.example.it \ + --db-password 'StrongDbPass123!' +``` + +Cosa fa: + +- clona il repository applicativo in una directory dedicata, di default `/var/www/` +- crea un database e un utente MySQL dedicati al sito +- prepara `.env`, chiave applicativa e cache Laravel +- installa dipendenze Composer e, se presenti, builda gli asset frontend +- crea un file Nginx dedicato e separato dagli altri siti pubblicati +- configura cron scheduler e, se richiesto, un worker Supervisor dedicato +- rifiuta per default di sovrascrivere directory applicative, database o vhost gia esistenti + +Flag utili per riuso controllato: + +- `--allow-existing-app-dir` permette di riusare una worktree git Laravel gia esistente +- `--allow-existing-database` permette di riusare database e utente MySQL gia presenti +- `--replace-nginx-site` permette di sostituire esplicitamente un vhost gia presente +- `--with-queue-worker` abilita un worker Supervisor dedicato al sito + +Questo e il pezzo da usare per avere 2 o 3 siti diversi sulla stessa macchina, per esempio frontoffice, demo e produzione, senza collisioni sui file di servizio. + +## 3. Bootstrap nodo NetGescon + +Esempio completo: + +```bash +sudo bash scripts/install/netgescon-node-bootstrap.sh \ + --site-key netgescon-prod \ + --repo-url git@your-gitea:studio/netgescon.git \ + --branch main \ + --app-url https://netgescon.example.it \ + --server-name netgescon.example.it \ + --db-password 'StrongDbPass123!' \ + --admin-email admin@netgescon.example.it \ + --admin-password 'StrongAdminPass123!' +``` + +Cosa fa: + +- clona il repository applicativo in `/var/www/netgescon` +- clona una seconda copia Git sorgente in `/var/www/netgescon-git-source` +- collega il repository sorgente a `storage/app/support/git-source` +- configura `.env` con `NETGESCON_GIT_SYNC_SOURCE_REPO` +- crea database e utente MySQL +- installa dipendenze Composer e, se presenti, builda gli asset frontend +- esegue migrazioni +- crea un utente iniziale con ruoli `super-admin` e `admin` +- configura Nginx, cron scheduler e Supervisor queue worker +- lascia il nodo pronto per aprire `admin-filament/supporto/aggiornamento-nodo` + +Guard-rail aggiunti: + +- usa `--site-key` per rendere univoci file Nginx, cron e Supervisor +- non sovrascrive piu automaticamente directory, database o vhost gia esistenti +- per riusare una installazione esistente servono flag espliciti come `--allow-existing-app-dir`, `--allow-existing-database` e `--replace-nginx-site` + +## Note operative + +- Gli script sono pensati per Ubuntu 24.04. +- Lo script NetGescon presuppone che la macchina abbia gia PHP, Composer, Node, MySQL e Nginx installati. +- Lo script Laravel generico e quello da riusare per altri progetti non NetGescon sulla stessa macchina. +- Se il repository Gitea usa SSH, la macchina deve avere gia la chiave deploy o la chiave utente autorizzata. +- Se vuoi una macchina solo server senza desktop, usa la base machine con `--without-xfce --without-vscode --without-xrdp`. +- Dopo il bootstrap del nodo conviene configurare subito HTTPS e backup automatici. +- Gli endpoint curl leggono direttamente gli script versionati nel repository, quindi dopo pull/deploy espongono sempre la versione aggiornata senza copiare file a mano. diff --git a/docs/AMAZON-CREATORS-API.md b/docs/AMAZON-CREATORS-API.md index 07e17aa..ac6b5e9 100644 --- a/docs/AMAZON-CREATORS-API.md +++ b/docs/AMAZON-CREATORS-API.md @@ -82,4 +82,4 @@ ## Cosa serve per il canale Telegram offerte - una frequenza di invio, per esempio push immediato o digest orario; - un criterio anti-spam, per esempio non ripubblicare lo stesso ASIN nelle ultime 24 ore. -La prossima slice naturale e un publisher che prende le offerte attive dal catalogo e invia il post al canale con titolo, prezzo, immagine e referral URL. \ No newline at end of file +La prossima slice naturale e un publisher che prende le offerte attive dal catalogo e invia il post al canale con titolo, prezzo, immagine e referral URL. diff --git a/docs/INSTALLAZIONE-NODO-LOCALE-0.9.md b/docs/INSTALLAZIONE-NODO-LOCALE-0.9.md new file mode 100644 index 0000000..4a2bbe3 --- /dev/null +++ b/docs/INSTALLAZIONE-NODO-LOCALE-0.9.md @@ -0,0 +1,177 @@ +# NetGescon 0.9 - Installazione nodo locale da macchina vuota + +Questo documento descrive la prova che ha senso fare adesso: una macchina Ubuntu nuova che installa NetGescon come nodo locale singolo, senza dipendere dai servizi cloud pubblici per funzionare nel quotidiano. + +Obiettivo pratico: + +1. preparare una VM o macchina Ubuntu pulita; +2. installare Docker e lo stack applicativo locale; +3. avviare NetGescon come nodo singolo; +4. ripristinare un backup proveniente dal sito; +5. verificare che il nodo lavori in autonomia con DB, storage e servizi locali. + +## Cosa e incluso nel repository + +Per questa prova sono pronti i seguenti file: + +- `docker/docker-compose.node-local.yml` +- `docker/.env.node-local.example` +- `docker/install-node-local.sh` +- `docker/restore-node-backup.sh` +- `docker/provision-vm.sh` + +Questo stack non usa il multisito `www/app/demo`. Porta su un solo nodo applicativo con: + +- `app` PHP-FPM Laravel +- `nginx` +- `mysql` +- `redis` + +## Cosa serve dalla macchina remota + +Per procedere senza perdere tempo mi servono solo queste informazioni: + +1. IP della macchina oppure hostname raggiungibile in LAN/VPN. +2. Utente SSH e porta SSH. +3. Conferma versione Ubuntu (`22.04` o `24.04` va bene). +4. RAM, CPU e spazio disco disponibili. +5. Path in cui vuoi installare il progetto, ad esempio `/opt/netgescon`. +6. Se il nodo dovra rispondere solo in LAN o anche con DNS dedicato. +7. Se hai gia pronto il backup da ripristinare: + - dump SQL del DB applicativo; + - archivio `storage` del sito; + - eventuale archivio legacy MDB/Access da montare in `LEGACY_ARCHIVES_PATH`. + +## Bootstrap da macchina vuota + +### 1. Copia repository sulla macchina + +Se la macchina vede Gitea: + +```bash +cd /opt +git clone ssh://git@192.168.0.53:2222/michele/netgescon-day0.git netgescon-day0 +cd /opt/netgescon-day0 +``` + +Se non vuoi usare Git direttamente sul nodo, puoi copiare un archivio del repository gia allineato. + +### 2. Provision base Ubuntu + +```bash +cd /opt/netgescon-day0/docker +chmod +x provision-vm.sh install-node-local.sh restore-node-backup.sh +./provision-vm.sh +``` + +Poi fai logout/login per applicare il gruppo Docker. + +### 3. Prepara env locale del nodo + +```bash +cd /opt/netgescon-day0 +cp docker/.env.node-local.example docker/.env.node-local +nano docker/.env.node-local +``` + +Campi da impostare subito: + +- `APP_URL` +- `NODE_HTTP_PORT` +- `DB_PASSWORD` +- `DB_ROOT_PASSWORD` +- `LEGACY_ARCHIVES_PATH` + +`APP_KEY` puo restare vuota: viene generata automaticamente da `install-node-local.sh` al primo avvio. + +### 4. Installa e avvia lo stack + +```bash +cd /opt/netgescon-day0/docker +./install-node-local.sh +``` + +Lo script: + +1. valida il compose; +2. genera `APP_KEY` se manca; +3. builda l immagine applicativa; +4. avvia DB e Redis; +5. avvia app e nginx; +6. crea anche il database `gescon_import` per gli import legacy. + +## Restore backup del sito + +Dopo aver copiato sul nodo il dump del database e l archivio storage: + +```bash +cd /opt/netgescon-day0/docker +./restore-node-backup.sh --sql /percorso/backup.sql.gz --storage /percorso/storage.tar.gz +``` + +Note operative: + +- il dump SQL puo essere `.sql` oppure `.sql.gz`; +- lo storage e opzionale, ma senza storage il nodo partira senza documenti/allegati storici; +- l archivio storage deve essere un `tar.gz` estraibile con radice compatibile con `/var/www/storage`. + +## Verifiche minime dopo restore + +```bash +cd /opt/netgescon-day0 + +docker compose --env-file docker/.env.node-local -f docker/docker-compose.node-local.yml ps +docker compose --env-file docker/.env.node-local -f docker/docker-compose.node-local.yml logs -f --tail=100 +``` + +Controlli applicativi minimi: + +1. login backoffice; +2. apertura dashboard; +3. apertura di almeno una pagina contabile; +4. verifica documenti/allegati storici; +5. verifica accesso al DB applicativo; +6. verifica che il nodo non dipenda da servizi cloud per il funzionamento base. + +## Spiegazione del messaggio visto su staging + +Il pannello `Supporto > Aggiornamento Nodo` mostra due cose diverse: + +1. lo stato Git noto al nodo; +2. l eventuale esecuzione dell update applicativo. + +Per questo: + +- `Ultima sync Git` indica quando il nodo ha registrato l ultima sync Git; +- `Ultimo update` resta `-` se non e mai stato lanciato un update applicativo da quella pagina. + +Dopo il push su Gitea di oggi, il nodo che ho verificato mostra gia: + +- branch `main` +- commit `b3e626b` +- tracking `ahead 0 / behind 0` +- working tree `pulito` + +Quindi il problema non era un guasto Git: era solo assenza di un update applicativo registrato, mentre la parte Git ora e allineata. + +## Limiti attuali da tenere presenti + +Questa prova locale serve per installazione e ripristino operativo. Restano fuori, salvo configurazione dedicata: + +- manifest distribution pubblico; +- licensing per canali; +- backup cloud esterni; +- servizi Google/Gmail/Drive lato studio; +- reverse proxy TLS pubblico multi-dominio. + +## Prossimo passo corretto + +Appena mi dai i dati della macchina remota, facciamo in ordine: + +1. verifica accesso SSH e risorse; +2. provisioning Docker; +3. copia repository o clone da Gitea; +4. creazione `docker/.env.node-local`; +5. avvio stack; +6. restore backup del sito; +7. checklist di collaudo del nodo. diff --git a/docs/INTEGRAZIONE-CONTABILE.md b/docs/INTEGRAZIONE-CONTABILE.md index e69de29..85bc409 100644 --- a/docs/INTEGRAZIONE-CONTABILE.md +++ b/docs/INTEGRAZIONE-CONTABILE.md @@ -0,0 +1,25 @@ +# Integrazione contabile operativa + +## Riconciliazione banca MPS + +La pagina Contabilita > Casse e banche > Movimenti non e piu solo una vista di suggerimenti. +La coda di riconciliazione ora puo chiudere due casi operativi direttamente dal movimento banca: + +- Incasso affitto: il movimento positivo puo essere collegato a un canone aperto del modulo affitti. L'azione crea o completa il record in affitti_canoni_pagati e marca il movimento come riconciliato operativamente. +- Pagamento fornitore: il movimento negativo puo essere collegato a una fattura del modulo contabilita_fatture_fornitori. L'azione genera la prima nota del pagamento, collega il movimento alla fattura e aggiorna o apre la riga nel registro_ritenute_acconto quando la fattura ha ritenuta. + +## Regole applicate + +- I canoni affitto vengono proposti in base a importo, data, inquilino e coerenza con il conto bancario associato all'affitto. +- Le fatture fornitore vengono proposte usando il netto da pagare quando presente, cosi il match resta coerente anche in presenza di ritenuta d'acconto. +- I movimenti riconciliati nel dominio operativo vengono marcati in match_data con un esito esplicito, cosi escono dalla coda anche quando non generano subito una registrazione contabile autonoma. + +## Base per moduli dichiarativi + +Questa slice segue la direzione dei moduli dichiarativi: + +- il modulo banca espone una coda operativa e i suoi candidati; +- il modulo affitti chiude i canoni nel proprio dominio senza duplicare logica nella UI; +- il modulo contabilita fornitore mantiene la responsabilita di prima nota, debito fornitore e registro RA. + +La regola da mantenere nelle prossime slice e semplice: la pagina di regia riconosce il caso e delega la chiusura all'unita funzionale proprietaria del dominio. diff --git a/docs/roadmaps/DOCUMENTI-ARCHIVIO-DAFARE.md b/docs/roadmaps/DOCUMENTI-ARCHIVIO-DAFARE.md new file mode 100644 index 0000000..5f2bd7a --- /dev/null +++ b/docs/roadmaps/DOCUMENTI-ARCHIVIO-DAFARE.md @@ -0,0 +1,116 @@ +# Documenti Archivio DAFARE + +## Obiettivo operativo + +Costruire un flusso unico che parta dal file digitale, generi il codice mnemonico/QR, stampi l'etichetta corretta e accompagni il documento dentro la catena fisica fino alla collocazione finale. + +## Slice da fare subito + +1. Rifinire il builder etichette su `etichette-generiche`. +2. Tenere le impostazioni accanto alla riga o al campo che controllano. +3. Rendere piu esplicita la modifica e la ristampa delle etichette gia create. +4. Portare la gestione di supporto e ubicazione nella tab `movimentazione-codici`. +5. Preparare il flusso `file base -> primo supporto -> contenitori successivi`. + +## Convenzioni archivio da fissare + +### Codice categoria archivio + +- `CONTR-ANNO-###`: contratti di servizio +- `ASSIC-ANNO-###`: polizze assicurative +- `CERT-ANNO-###`: certificati e autorizzazioni +- `PREV-ANNO-###`: preventivi e offerte +- `FATT-ANNO-###`: fatture e documenti fiscali +- `VERB-ANNO-###`: verbali assemblee +- `DOC-ANNO-###`: altri documenti + +### Regole comuni + +- Il prefisso categoria va salvato anche nella voce archivio su database. +- Il valore deve essere in formato Linux-safe: maiuscolo, senza spazi, uniforme. +- Il codice mnemonico deve diventare sia nome logico del file digitale sia fallback umano dell'etichetta stampata. +- Il QR o TAG deve contenere l'identificatore unico universale del nodo archivistico, leggibile da smartphone o lettore fisico. +- Il passaggio consegne tra amministratori deve riusare lo stesso codice mnemonico/QR senza ricodifiche locali. + +### Data nei file + +- Inserire la data nel nome file con formato `YYMMGG`. +- Prevedere una futura impostazione generale dell'amministratore per cambiare il formato globale della data. + +## Flussi da realizzare + +### 1. Gestione file base + +Una Blade dedicata deve permettere di registrare il singolo file o una raccolta di fogli con: + +- titolo +- descrizione +- note +- tag +- categoria come tag strutturato +- numero totale dei fogli o dell'incartamento +- selezione del layout etichetta da usare +- stampa etichetta con QR e fallback testuale nel titolo e sotto il QR + +### 2. Primo contenitore fisico + +Dal file base bisogna poter scegliere il primo supporto: + +- busta ad anelli +- faldone a tre lembi +- raccoglitore +- altro supporto scelto a catalogo + +Il record deve restare collegato al documento digitale per consentire nuove stampe dell'etichetta in qualsiasi momento. + +### 3. Contenitori successivi e movimentazione + +Serve una gestione separata dove il materiale gia preparato venga caricato nei contenitori successivi: + +- cartellina +- busta +- faldone +- scatola +- scaffale +- posto finale + +La tab `movimentazione-codici` deve diventare il centro operativo per: + +- cambiare il contenitore padre +- aggiornare supporto operativo +- aggiornare magazzino, scaffale, ripiano e dettaglio ubicazione +- leggere codici da scanner, smartphone, QR o plugin locale + +## Etichette 11354 e binding DB + +- Per il formato `11354` prevedere un campo di collegamento ai dati DB da riversare automaticamente in stampa. +- Ogni riga deve poter leggere un campo DB oppure testo libero. +- Le impostazioni della singola riga devono stare vicino alla riga stessa. +- La ristampa deve essere disponibile anche se fallisce la lettura automatica del QR. + +## Catalogo supporti e Amazon + +- Ogni supporto fisico scelto dall'utente deve poter avere il pulsante Amazon. +- Il link Amazon deve arrivare dal catalogo prodotti gestito dal super admin. +- L'archivio fisico non deve duplicare il catalogo: deve consumare il dato gia scelto. + +## Plugin locale / bridge dispositivi + +Serve un plugin HTTP locale o agent Windows per passare dati hardware fuori dal browser: + +- TWAIN/WIA scanner +- lettori barcode e QR +- NFC +- seriali +- eventuali periferiche di stampa/lettura + +La webapp deve restare il pannello operativo, mentre il bridge locale si occupa della parte hardware. + +## Backlog successivo + +1. Foto ubicazione finale come nel ticket. +2. Vista modifica/ristampa dalle ultime etichette. +3. Blade separata per file singolo e per raccolta fogli. +4. Conteggio pagine persistente su database. +5. Impostazioni globali amministratore per formato data e convenzioni etichetta. +6. Integrazione completa col bridge locale di comunicazione. diff --git a/public/installers/index.php b/public/installers/index.php new file mode 100644 index 0000000..bfce179 --- /dev/null +++ b/public/installers/index.php @@ -0,0 +1,110 @@ + + + + + + + NetGescon Installers + + + +
+

NetGescon Installers

+

Da qui puoi lanciare gli script di installazione direttamente con curl, usando staging.netgescon.it oppure il nodo locale 192.168.0.200.

+ +
+

1. Macchina base Ubuntu 24.04

+

Installa stack Laravel, MySQL, Nginx, Redis, Supervisor e opzionalmente XFCE, XRDP e VS Code.

+
curl -fsSL /ubuntu2404-laravel-base.php -o /tmp/ubuntu2404-laravel-base.sh
+sudo bash /tmp/ubuntu2404-laravel-base.sh
+

Versione server-only:

+
curl -fsSL /ubuntu2404-laravel-base.php -o /tmp/ubuntu2404-laravel-base.sh
+sudo bash /tmp/ubuntu2404-laravel-base.sh --without-xfce --without-vscode --without-xrdp
+
+ +
+

2. Bootstrap sito Laravel

+

Pubblica un progetto Laravel su una macchina gia pronta senza sovrascrivere directory, database o vhost esistenti se non richiesto esplicitamente.

+
curl -fsSL /laravel-site-bootstrap.php -o /tmp/laravel-site-bootstrap.sh
+sudo bash /tmp/laravel-site-bootstrap.sh \
+    --site-key demo-app \
+    --project-name "Demo App" \
+    --repo-url git@your-gitea:studio/demo-app.git \
+    --branch main \
+    --app-url  \
+    --server-name  \
+    --db-password 'StrongDbPass123!'
+
+ +
+

3. Bootstrap nodo NetGescon

+

Clona il repository applicativo, prepara il git-source locale per la sync da browser e lascia il nodo pronto per admin-filament/supporto/aggiornamento-nodo.

+
curl -fsSL /netgescon-node-bootstrap.php -o /tmp/netgescon-node-bootstrap.sh
+sudo bash /tmp/netgescon-node-bootstrap.sh \
+    --site-key netgescon-prod \
+  --repo-url git@your-gitea:studio/netgescon.git \
+  --branch main \
+  --app-url  \
+  --server-name  \
+  --db-password 'StrongDbPass123!' \
+  --admin-email admin@netgescon.local \
+  --admin-password 'StrongAdminPass123!'
+
+ + +
+ + diff --git a/public/installers/laravel-site-bootstrap.php b/public/installers/laravel-site-bootstrap.php new file mode 100644 index 0000000..5cef8c3 --- /dev/null +++ b/public/installers/laravel-site-bootstrap.php @@ -0,0 +1,19 @@ + +
+
Presa in carico amministrazione:
+
+
+ + + Salva data + +
+
+ Le gestioni operative vengono filtrate dall'anno di presa in carico in avanti. +
+
+
+
+
Verbale di nomina:
+
+ +
+ + Carica verbale + + @if($this->getVerbaleNominaDocumento()) + + Allegato presente + + @else + + Nessun allegato caricato + + @endif +
+
+
diff --git a/resources/views/filament/components/archive-code-launcher.blade.php b/resources/views/filament/components/archive-code-launcher.blade.php new file mode 100644 index 0000000..fa28828 --- /dev/null +++ b/resources/views/filament/components/archive-code-launcher.blade.php @@ -0,0 +1,96 @@ +
+
+
+
{{ $title ?? 'Lettore QR archivio' }}
+
{{ $description ?? 'Scansiona un QR archivio e apri subito il contenuto del nodo selezionato.' }}
+
+
Visualizza contenuto senza aprire il contenitore
+
+ +
+
+ + +
+ + +
+ +
+ + +
+
\ No newline at end of file diff --git a/resources/views/filament/components/legacy-admin-assets.blade.php b/resources/views/filament/components/legacy-admin-assets.blade.php index 0d8896a..699d5ed 100644 --- a/resources/views/filament/components/legacy-admin-assets.blade.php +++ b/resources/views/filament/components/legacy-admin-assets.blade.php @@ -24,4 +24,30 @@ .fi-sidebar-item { margin-block: 0 !important; } + + @media (max-width: 63.998rem), (hover: none) and (pointer: coarse) { + .fi-body.fi-body-has-navigation .fi-sidebar { + border-top: 0 !important; + width: 100% !important; + height: auto !important; + position: static !important; + inset: auto !important; + max-height: none !important; + transform: none !important; + } + + .fi-body.fi-body-has-navigation .fi-topbar, + .fi-body.fi-body-has-navigation .fi-main, + .fi-body.fi-body-has-navigation .fi-main-ctn { + width: 100% !important; + max-width: 100% !important; + margin-left: 0 !important; + padding-left: 0 !important; + } + + .fi-body.fi-body-has-navigation .fi-main-ctn { + padding-top: 0 !important; + overflow-x: visible !important; + } + } diff --git a/resources/views/filament/components/stabile-attivo.blade.php b/resources/views/filament/components/stabile-attivo.blade.php index a4bf8c9..5962146 100644 --- a/resources/views/filament/components/stabile-attivo.blade.php +++ b/resources/views/filament/components/stabile-attivo.blade.php @@ -8,30 +8,9 @@ return false; } - if ((bool) ($stabile->riscaldamento_centralizzato ?? false)) { - return true; - } - - $rateRiscaldamento = $stabile->rate_riscaldamento_mesi ?? []; - if (is_string($rateRiscaldamento)) { - $rateRiscaldamento = json_decode($rateRiscaldamento, true); - } - if (is_array($rateRiscaldamento) && $rateRiscaldamento !== []) { - return true; - } - - $config = $stabile->configurazione_avanzata ?? []; - if (is_string($config)) { - $config = json_decode($config, true); - } - if (is_array($config) && array_key_exists('mostra_riscaldamento', $config) && $config['mostra_riscaldamento']) { - return true; - } - - return \Illuminate\Support\Facades\DB::table('gestioni_contabili') - ->where('stabile_id', $stabile->id) - ->where('tipo_gestione', 'riscaldamento') - ->exists(); + return method_exists($stabile, 'hasOperationalHeating') + ? $stabile->hasOperationalHeating() + : false; }; $stabileLabel = static function (?\App\Models\Stabile $stabile): string { @@ -64,20 +43,5 @@ @endif - @if($stabili->count() > 1) -
- @csrf - -
- @endif +
Selettore stabile temporaneamente disattivato.
diff --git a/resources/views/filament/pages/affitti/gestione-affitti.blade.php b/resources/views/filament/pages/affitti/gestione-affitti.blade.php index 891b650..b57f74b 100644 --- a/resources/views/filament/pages/affitti/gestione-affitti.blade.php +++ b/resources/views/filament/pages/affitti/gestione-affitti.blade.php @@ -421,6 +421,11 @@ +
+ + +
I canoni precedenti restano fuori dal gestionale operativo e dalla riconciliazione bancaria.
+
diff --git a/resources/views/filament/pages/condomini/contratti-hub.blade.php b/resources/views/filament/pages/condomini/contratti-hub.blade.php new file mode 100644 index 0000000..035316f --- /dev/null +++ b/resources/views/filament/pages/condomini/contratti-hub.blade.php @@ -0,0 +1,148 @@ + + @php($stats = $this->stats) + @php($contratti = $this->contrattiRows) + @php($scadenze = $this->upcomingScadenze) + +
+
+
+
+
Stabile
+

HUB Contratti {{ $this->stabileAttivo?->denominazione ? '· ' . $this->stabileAttivo->denominazione : '' }}

+

Qui registriamo il contratto, i riferimenti tecnici e le scadenze operative da inviare anche al calendario. Il collegamento documentale e contabile parte da qui.

+
+
+
Collegamenti
+
Fornitore, manutentore, servizio, utenza, documento e codici tecnici nello stesso record.
+
+
+ +
+
+
Contratti
+
{{ $stats['totale'] ?? 0 }}
+
+
+
Attivi
+
{{ $stats['attivi'] ?? 0 }}
+
+
+
Scadenze 45 gg
+
{{ $stats['scadenze'] ?? 0 }}
+
+
+
Documenti contratto
+
{{ $stats['documenti'] ?? 0 }}
+
+
+
+ +
+ + Registro contratti + MVP: contratto, riferimenti essenziali, documento principale e generazione scadenze. + +
+ @forelse($contratti as $contratto) +
+
+
+
+
{{ $contratto->titolo }}
+ + {{ strtoupper($contratto->stato) }} + + + {{ $this->contractTypeLabel($contratto->tipo_contratto) }} + +
+
+ {{ $contratto->servizio_label ?: 'Servizio non specificato' }} + @if($contratto->fornitore) + · {{ $contratto->fornitore->ragione_sociale ?: trim(($contratto->fornitore->nome ?? '') . ' ' . ($contratto->fornitore->cognome ?? '')) }} + @endif +
+
+
+
Decorrenza: {{ $contratto->decorrenza_dal?->format('d/m/Y') ?: '—' }}
+
Fine: {{ $contratto->decorrenza_al?->format('d/m/Y') ?: ($contratto->rinnovo_automatico ? 'rinnovo automatico' : '—') }}
+
Frequenza: {{ $contratto->frequenza_scadenze ? str_replace('_', ' ', $contratto->frequenza_scadenze) : '—' }}
+
+
+ +
+
+
Codice
+
{{ $contratto->codice_contratto ?: '—' }}
+
+
+
Riferimento
+
{{ $contratto->riferimento_esterno ?: '—' }}
+
+
+
Importo
+
{{ $contratto->importo_periodico !== null ? '€ ' . number_format((float) $contratto->importo_periodico, 2, ',', '.') : '—' }}
+
+
+
Documento
+
{{ $contratto->documentoPrincipale?->numero_protocollo ?: ($contratto->documentoPrincipale?->nome ?: '—') }}
+
+
+ + @if(!empty($contratto->codici_collegati)) +
+ @foreach($contratto->codici_collegati as $code) + + {{ $code['chiave'] ?? 'Dato' }}: {{ $code['valore'] ?? '—' }} + + @endforeach +
+ @endif + + @if($contratto->note) +
{{ $contratto->note }}
+ @endif +
+ @empty +
Nessun contratto registrato per lo stabile attivo.
+ @endforelse +
+
+ +
+ + Scadenze programmate + Le nuove scadenze vengono registrate sulla tabella calendario operativa. + +
+ @forelse($scadenze as $scadenza) +
+
+
+
{{ $scadenza->stabileContratto?->titolo ?: $scadenza->descrizione_sintetica }}
+
{{ $scadenza->stabileContratto?->tipo_contratto ? $this->contractTypeLabel($scadenza->stabileContratto->tipo_contratto) : 'Scadenza contratto' }}
+
+
{{ $scadenza->data_scadenza?->format('d/m/Y') ?: '—' }}
+
+ @if(!empty($scadenza->legacy_payload['codice_contratto'])) +
Codice: {{ $scadenza->legacy_payload['codice_contratto'] }}
+ @endif +
+ @empty +
Nessuna scadenza contrattuale registrata.
+ @endforelse +
+
+ + + Copertura dati +
+
Per acqua, energia e altri impianti usa i codici collegati per registrare POD, PDR, matricole, codice cliente, numero utenza e riferimenti bolletta.
+
Il SIA resta sul fornitore; qui teniamo i dati specifici del singolo contratto dello stabile.
+
Il documento principale può essere un PDF già registrato nell’archivio documentale con categoria contratto.
+
+
+
+
+
+
diff --git a/resources/views/filament/pages/mobile/documenti-mobile.blade.php b/resources/views/filament/pages/mobile/documenti-mobile.blade.php new file mode 100644 index 0000000..d3550ff --- /dev/null +++ b/resources/views/filament/pages/mobile/documenti-mobile.blade.php @@ -0,0 +1,90 @@ + +
+
+
Gestione documentale mobile
+
Pagina dedicata alla lettura QR, al codice mnemonico e all'apertura rapida del contenuto archivistico da telefono o tablet.
+
+ + @include('filament.components.archive-code-launcher', [ + 'title' => 'Lettore documenti mobile', + 'description' => 'Scansiona il QR o inserisci il codice mnemonico per aprire il contenuto della cartella, del faldone o della scatola nel lettore archivio.', + ]) + +
+
+
+
Prova NFC da browser
+
Facciamo test progressivi: sui browser compatibili puoi avviare la lettura del tag NFC direttamente dalla pagina e aprire il lettore archivio con il codice letto.
+
+
+
+ +
+ + Apri lettore completo +
+ +
+
+ +
+
+
Contenuto archivio
+
Apri il lettore archivio per vedere catena contenitori, documenti interni e ubicazione del nodo scansionato.
+ +
+ +
+
Archivio digitale
+
Apri l'archivio digitale per vedere la lista documenti collegata al contenitore o al protocollo trovato.
+ +
+ +
+
Movimentazione
+
Dal lettore puoi poi passare alla movimentazione per spostare il contenitore in altro faldone, scaffale o postazione.
+ +
+
+
+
diff --git a/resources/views/filament/pages/strumenti/documenti-archivio.blade.php b/resources/views/filament/pages/strumenti/documenti-archivio.blade.php index 6eee167..f3400f2 100644 --- a/resources/views/filament/pages/strumenti/documenti-archivio.blade.php +++ b/resources/views/filament/pages/strumenti/documenti-archivio.blade.php @@ -6,6 +6,7 @@ 'scansione-documenti' => 'Scansione documenti', 'etichette-generiche' => 'Etichette generiche', 'movimentazione-codici' => 'Movimentazione per codice', + 'lettore-codici' => 'Lettore codici', 'template-drive' => 'Template Drive', 'archivio-fisico' => 'Archivio fisico', 'permessi' => 'Permessi e cartelle', @@ -72,7 +73,7 @@ @if($this->hubTab === 'protocollo-comunicazioni') Protocollo comunicazioni - Prima tab operativa per l'amministratore: ultimi documenti protocollati, filtri rapidi e vista a box con anteprima PDF o lista compatta. + Prima tab operativa per l'amministratore: ultimi documenti protocollati, filtri rapidi e vista a box universale per PDF, immagini, Office e allegati testuali.
@@ -82,9 +83,9 @@
Documenti non fattura nello stabile attivo.
-
Con PDF
-
{{ $protocolloStats['con_pdf'] ?? 0 }}
-
Apribili in anteprima o stampa.
+
Con allegato
+
{{ $protocolloStats['con_file'] ?? 0 }}
+
Apribili inline o scaricabili secondo il tipo file.
Questo mese
@@ -144,39 +145,47 @@
@if($this->protocolloView === 'cards') -
+
@forelse($this->protocolloComunicazioniRows as $row)
{{ $row['protocollo'] }} {{ $row['categoria'] }} + {{ $row['file_badge'] }} {{ $row['data'] }}
-

{{ $row['titolo'] }}

-

{{ $row['descrizione'] !== '' ? $row['descrizione'] : 'Nessuna descrizione registrata.' }}

+

{{ $row['titolo'] }}

+

{{ $row['descrizione'] !== '' ? $row['descrizione'] : 'Nessuna descrizione registrata.' }}

-
+
Cartella: {{ $row['cartella'] }}
@if($row['fornitore'] !== '')
Ente / referente: {{ $row['fornitore'] }}
@endif - @if($row['archive_reference'] !== '') + @if($row['archive_code'] !== '') +
Codice archivio: {{ $row['archive_code'] }}
+ @endif + @if($row['archive_reference'] !== '' && $row['archive_reference'] !== $row['archive_code'])
Rif. archivio: {{ $row['archive_reference'] }}
@endif
- Apri - Scarica - @if($row['has_pdf']) + @if($row['has_file'] && $row['preview_url']) + Apri + Scarica + @else + Solo metadati + @endif + @if($row['can_print'] && $row['print_url']) Stampa @endif
@empty -
+
Nessun documento protocollato trovato con i filtri correnti.
@endforelse @@ -199,8 +208,11 @@
{{ $row['titolo'] }}
{{ $row['descrizione'] !== '' ? $row['descrizione'] : 'Nessuna descrizione registrata.' }}
- @if($row['archive_reference'] !== '') -
{{ $row['archive_reference'] }}
+ @if($row['archive_code'] !== '') +
{{ $row['archive_code'] }}
+ @endif + @if($row['archive_reference'] !== '' && $row['archive_reference'] !== $row['archive_code']) +
{{ $row['archive_reference'] }}
@endif
@@ -208,8 +220,10 @@
{{ $row['cartella'] }}
- Apri - @if($row['has_pdf']) + @if($row['has_file'] && $row['preview_url']) + Apri + @endif + @if($row['can_print'] && $row['print_url']) Stampa @endif
@@ -437,143 +451,7 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
@elseif($this->hubTab === 'etichette-generiche') - - Etichette generiche per contenitori - Tab dedicata alla stampa rapida di etichette per faldoni, scatole, raccoglitori e buste. Il QR resta obbligatorio e il codice generato diventa il riferimento stabile per annidamento e scansione. - -
-
-
-
-
Nuova etichetta generica
-
Crea il contenitore, stampa subito e poi usalo come nodo della catena QR: fattura, cartella, dock, scatola, magazzino, scaffale.
-
-
QR incluso
-
- -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
- -
-
- -
-
-
Standard operativo
-
-
Il codice univoco viene generato automaticamente e diventa il nodo da scansionare.
-
Il QR usa lo stesso schema pubblico gia host-agnostic del resto dell'archivio fisico.
-
Il link Amazon puo essere salvato come URL o come ASIN, con tag affiliato aggiunto dal super-admin.
-
Questa tab rende visibile la nuova etichetta senza doverla cercare dentro il registro generale.
-
-
- -
-
-
Ultime etichette generiche
-
Stampa immediata 11354 / 99014
-
-
- @forelse($this->genericLabelRows as $row) -
-
-
-
- {{ $row['codice_univoco'] }} - {{ $row['tipo'] }} -
-
{{ $row['titolo'] }}
- @if($row['linea_secondaria'] !== '') -
{{ $row['linea_secondaria'] }}
- @endif -
Percorso: {{ $row['percorso'] }}
- @if($row['ubicazione'] !== '') -
Ubicazione: {{ $row['ubicazione'] }}
- @endif - @if($row['amazon_url'] !== '') - - @endif -
- -
-
- @empty -
Nessuna etichetta generica creata ancora per lo stabile attivo.
- @endforelse -
-
-
-
-
+ @include('filament.pages.strumenti.partials.generic-label-builder') @elseif($this->hubTab === 'movimentazione-codici') Movimentazione per codice @@ -583,7 +461,7 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
Sposta per codice
-
Scansiona o inserisci codice sorgente e codice destinazione. Il contenitore padre viene aggiornato senza dover aprire il registro generale.
+
Scansiona o inserisci codice sorgente e codice destinazione. Qui aggiorni anche supporto e ubicazione operativa senza rientrare nella scheda di censimento.
@@ -594,6 +472,42 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
+
+
+
+
Ubicazione dopo lo spostamento
+
Se lasci i campi vuoti, il sistema eredita la posizione del contenitore destinazione e conserva il supporto attuale.
+
+
Operativo
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
@@ -601,7 +515,7 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
Catena QR prevista
-
QR Fattura -> QR Cartella trasparente -> QR Dock -> QR Scatola -> QR Magazzino -> QR Scaffale. Questa tab separa il movimento dal censimento del contenuto.
+
QR Foglio -> QR Cartellina -> QR Busta ad anelli -> QR Faldone -> QR Faldone con lacci -> QR Scatola -> QR Scaffale -> QR Posto. Questa tab separa il movimento dal censimento del contenuto.
@@ -621,6 +535,229 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
+ @elseif($this->hubTab === 'lettore-codici') + + Lettore codici archivio + Scansiona un QR, passa un tag NFC o inserisci un codice archivio per vedere subito il contenuto di una cartella, di un faldone o di una scatola. + + @php + $selectedLookupItem = $this->archiveLookupSelectedItem; + $selectedLookupChain = $this->archiveLookupSelectedChain; + @endphp + +
+
+
+
+
+
Leggi QR / NFC / codice archivio
+
Il sistema estrae il codice archivio dal payload QR e ti mostra subito padre, contenuto interno e documento digitale collegato.
+
+
QR e NFC ready
+
+ +
+
+ + +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ Usa questa pagina anche per i contenitori di trasferimento amministratore: i protocolli si muovono nel contenitore generale senza cambiare i QR già stampati. +
+
+
+ +
+
+
Risultati archivio
+
{{ count($this->archiveLookupResults) }} risultati
+
+
+ @forelse($this->archiveLookupResults as $row) + + @empty +
Nessun nodo archivio trovato con i filtri correnti.
+ @endforelse +
+
+
+ +
+
+
Contenuto del nodo selezionato
+ @if($selectedLookupItem) +
+
+ {{ $selectedLookupItem->codice_univoco }} + {{ $selectedLookupItem->tipo_label }} + @if($selectedLookupItem->nfc_reference) + NFC {{ $selectedLookupItem->nfc_reference }} + @endif +
+ +
+
{{ $selectedLookupItem->titolo }}
+
{{ $selectedLookupItem->percorso_fisico }}
+
+ +
+ Payload QR: {{ $selectedLookupItem->qr_code_data }} +
+ + @if($selectedLookupChain !== []) +
+
Catena contenitori
+
+ @foreach($selectedLookupChain as $node) + + @endforeach +
+
+ @endif + +
+
+
Ubicazione
+
{{ collect([$selectedLookupItem->magazzino, $selectedLookupItem->scaffale, $selectedLookupItem->ripiano, $selectedLookupItem->ubicazione_dettaglio])->filter()->implode(' · ') ?: 'Non definita' }}
+
+
+
Data archiviazione
+
{{ $selectedLookupItem->data_archiviazione?->format('d-m-Y') ?: '—' }}
+
+
+ + @if($selectedLookupItem->documento) +
+
Documento digitale collegato
+
{{ $selectedLookupItem->documento->nome_file ?: $selectedLookupItem->documento->nome }}
+
{{ $selectedLookupItem->documento->resolveArchiveDisplayReference() }}
+
+ @endif + +
+
Contenuto interno
+
+ @forelse($selectedLookupItem->children as $child) + + @empty +
Questo nodo non contiene ancora altri elementi interni.
+ @endforelse +
+
+
+ @else +
Scansiona un QR, passa un tag NFC o seleziona un risultato a sinistra per vedere il contenuto del contenitore.
+ @endif +
+
+
+
@elseif($this->hubTab === 'template-drive') Template cartelle Drive @@ -756,21 +893,8 @@ class="rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-sm tran
-
- - -
-
- - -
-
- - -
-
- - +
+ Dopo il primo censimento usa la tab Movimentazione per codice per assegnare supporto operativo, magazzino, scaffale, ripiano e dettaglio ubicazione ai passaggi successivi.
diff --git a/resources/views/filament/pages/strumenti/partials/generic-label-builder.blade.php b/resources/views/filament/pages/strumenti/partials/generic-label-builder.blade.php index d866529..5f807f0 100644 --- a/resources/views/filament/pages/strumenti/partials/generic-label-builder.blade.php +++ b/resources/views/filament/pages/strumenti/partials/generic-label-builder.blade.php @@ -18,6 +18,13 @@ 'sky' => 'from-sky-600 to-sky-500', default => 'from-slate-700 to-slate-600', }; + $fontSizeMap = ['xs' => 10, 'sm' => 12, 'md' => 14, 'lg' => 18, 'xl' => 24]; + $fontSizes = is_array($preview['font_sizes'] ?? null) ? $preview['font_sizes'] : []; + $mnemonicFontPx = $fontSizeMap[$fontSizes['mnemonic'] ?? 'xl'] ?? 24; + $titleFontPx = $fontSizeMap[$fontSizes['title'] ?? 'xl'] ?? 24; + $subtitleFontPx = $fontSizeMap[$fontSizes['subtitle'] ?? 'md'] ?? 14; + $linesFontPx = $fontSizeMap[$fontSizes['lines'] ?? 'md'] ?? 14; + $noteFontPx = $fontSizeMap[$fontSizes['note'] ?? 'sm'] ?? 12; @endphp
@@ -67,18 +74,48 @@ @endforeach
-
- - -
Suggerito: {{ $this->mnemonicCodeHint ?? 'GER00079' }}
+
+
+ + +
Suggerito: {{ $this->mnemonicCodeHint ?? 'GER00079' }}
+
+
+ + +
-
- - +
+
+ + +
+
+ + +
-
- - +
+
+ + +
+
+ + +
@@ -111,31 +148,53 @@
-
- - -
-
- - -
-
- - +
+
+
+
Quattro righe configurabili
+
Per ogni riga scegli se leggere un campo, scrivere testo libero, lasciare la riga bianca o disattivarla.
+
+
+ + +
+
+
+ @for($index = 1; $index <= 4; $index++) + @php + $modeKey = 'line_' . $index . '_mode'; + $sourceKey = 'line_' . $index . '_source'; + $textKey = 'line_' . $index . '_text'; + $currentMode = data_get($this->genericLabelForm, $modeKey, 'none'); + @endphp +
+
+ + +
+
+ + +
+
+ + +
+
+ @endfor +
@@ -211,9 +270,19 @@
-
- - +
+
+ + +
+
+ + +
@@ -240,11 +309,11 @@
-
{{ $preview['mnemonic_code'] ?? ($this->mnemonicCodeHint ?? 'GER00079') }}
-
{{ data_get($preview, 'slots.title') !== '' ? data_get($preview, 'slots.title') : 'Titolo etichetta' }}
-
{{ data_get($preview, 'slots.subtitle') !== '' ? data_get($preview, 'slots.subtitle') : 'Seconda riga descrittiva' }}
+
{{ $preview['mnemonic_code'] ?? ($this->mnemonicCodeHint ?? 'GER00079') }}
+
{{ data_get($preview, 'slots.title') !== '' ? data_get($preview, 'slots.title') : 'Titolo etichetta' }}
+
{{ data_get($preview, 'slots.subtitle') !== '' ? data_get($preview, 'slots.subtitle') : 'Seconda riga descrittiva' }}
-
+
@foreach(($preview['stable_lines'] ?? []) as $stableLine) @if($stableLine === '__BLANK_LINE__')
@@ -261,7 +330,7 @@
@if(data_get($preview, 'slots.note') !== '') -
{{ data_get($preview, 'slots.note') }}
+
{{ data_get($preview, 'slots.note') }}
@endif
@@ -278,16 +347,27 @@ @endif
-
{{ $preview['payload']['codice_univoco'] ?? 'AF-QR-PREVIEW' }}
-
{{ data_get($preview, 'slots.title') !== '' ? data_get($preview, 'slots.title') : 'Titolo etichetta' }}
+
{{ $preview['payload']['codice_univoco'] ?? 'AF-QR-PREVIEW' }}
+
{{ data_get($preview, 'slots.title') !== '' ? data_get($preview, 'slots.title') : 'Titolo etichetta' }}
@if(data_get($preview, 'slots.subtitle') !== '') -
{{ data_get($preview, 'slots.subtitle') }}
+
{{ data_get($preview, 'slots.subtitle') }}
@endif @if(data_get($preview, 'slots.meta') !== '') -
{{ data_get($preview, 'slots.meta') }}
+
{{ data_get($preview, 'slots.meta') }}
+ @endif + @if(($preview['stable_lines'] ?? []) !== []) +
+ @foreach(($preview['stable_lines'] ?? []) as $stableLine) + @if($stableLine === '__BLANK_LINE__') +
+ @elseif($stableLine !== '') +
{{ $stableLine }}
+ @endif + @endforeach +
@endif @if(data_get($preview, 'slots.note') !== '') -
{{ data_get($preview, 'slots.note') }}
+
{{ data_get($preview, 'slots.note') }}
@endif
QR payload: {{ $preview['payload']['qr_payload'] ?? 'NGDOC|AF:AF-QR-PREVIEW|TIPO:SCATOLA|STB:0|PARENT:RADICE' }} diff --git a/resources/views/filament/pages/supporto/aggiornamento-launcher.blade.php b/resources/views/filament/pages/supporto/aggiornamento-launcher.blade.php index 2f549f9..2aefda0 100644 --- a/resources/views/filament/pages/supporto/aggiornamento-launcher.blade.php +++ b/resources/views/filament/pages/supporto/aggiornamento-launcher.blade.php @@ -10,7 +10,7 @@
Launcher esterno per update e sync Git
- Usa questa pagina per lanciare aggiornamenti senza restare dentro la pagina completa Modifiche mentre Livewire e i file vengono riallineati. + Usa questa pagina per lanciare aggiornamenti senza restare dentro la pagina completa Modifiche mentre Livewire e i file vengono riallineati. Il flusso principale da Gitea ora blocca nuove richieste, crea il backup pre-update, aggiorna DB e ricostruisce l'app prima di riaprire il nodo.
@@ -31,8 +31,8 @@
-
Sync Git da Gitea
-
Flusso consigliato per staging e nodi interni.
+
Refresh nodo da Gitea
+
Flusso consigliato per staging e nodi interni: backup, finestra protetta, sync Git, dipendenze, migrate e cache rebuild.
@if(! $this->ticketAttachmentMetadataReady)
@@ -59,29 +59,36 @@ Aggiorna nodo completo da Gitea - + Aggiorna staging da Gitea - - Aggiorna avanzamento Git + + Solo sync Git avanzata + + + Aggiorna avanzamento refresh
- @if($this->gitSyncInProgress) -
+
+ Il pulsante Aggiorna staging da Gitea esegue il refresh completo del nodo: backup pre-update, maintenance mode per bloccare nuove operazioni utente, sync da Gitea, composer install, tentativo di build frontend se disponibile, migrate --force, pulizia cache, rebuild Blade e riapertura automatica del sistema. +
+ + @if($this->updateInProgress) +
@else
@endif
- Avanzamento sync Git - {{ (int) $this->gitSyncProgressPercent }}% + Avanzamento refresh nodo + {{ (int) $this->updateProgressPercent }}%
-
+
-
{{ $this->gitSyncProgressMessage }}
- @if(filled($this->lastGitSyncIssue)) -
{{ $this->lastGitSyncIssue }}
+
{{ $this->updateProgressMessage }}
+ @if(filled($this->lastUpdateIssue)) +
{{ $this->lastUpdateIssue }}
@endif
@@ -108,7 +115,7 @@
- Il flusso consigliato del nodo e: push su Gitea, poi "Aggiorna nodo completo da Gitea" da questa pagina. Gli archivi caricati manualmente dentro storage o cartelle private non vengono trasferiti da Git, salvo procedure dedicate di backup/sync dati. + Il flusso consigliato del nodo e: push su Gitea, poi "Aggiorna staging da Gitea" da questa pagina. I dati utente non vengono distribuiti via Git: per questo il launcher esegue prima il backup pre-update e poi una finestra di maintenance per evitare scritture concorrenti durante il riallineamento del nodo.
@if(filled($this->pendingGitPreviewIssue)) diff --git a/resources/views/filament/pages/supporto/ticket-mobile.blade.php b/resources/views/filament/pages/supporto/ticket-mobile.blade.php index 9d3dee0..1e7134e 100644 --- a/resources/views/filament/pages/supporto/ticket-mobile.blade.php +++ b/resources/views/filament/pages/supporto/ticket-mobile.blade.php @@ -1,61 +1,5 @@ - +
- @if($liveIncomingCall) - @php - $rubricaUrl = $this->getLiveRubricaUrl(); - $estrattoUrl = $this->getLiveEstrattoContoUrl(); - @endphp -
-
-
-
Chiamata in arrivo in tempo reale
-
- Numero: {{ $liveIncomingCall['phone'] ?? '-' }} - @if(!empty($liveIncomingCall['target_extension'])) - · Interno: {{ $liveIncomingCall['target_extension'] }} - @endif - @if(!empty($liveIncomingCall['received_at'])) - · Ricevuta: {{ $liveIncomingCall['received_at'] }} - @endif -
- @if(!empty($liveIncomingCall['rubrica_nome'])) -
Contatto riconosciuto: {{ $liveIncomingCall['rubrica_nome'] }}
- @endif - @if(!empty($liveIncomingCall['line'])) -
{{ $this->excerpt((string) $liveIncomingCall['line'], 180) }}
- @endif -
-
- - Crea Post-it (prima valutazione) - - - - Precompila ticket (opzionale) - - - @if($liveCallCanClickToCall) - - Chiama da interno assegnato - - @endif - - @if($rubricaUrl) - - Apri scheda rubrica - - @endif - - @if($estrattoUrl) - - Apri estratto conto - - @endif -
-
-
- @endif -
Il riepilogo operativo Ticket Mobile ora e nella dashboard principale, cosi i link rapidi e lo stato ticket restano visibili appena entri in Filament.
@@ -124,70 +68,6 @@
@endif - @php - $ticketCallContext = $this->ticketCallContext; - @endphp - @if(!empty($ticketCallContext['phone']) || !empty($ticketCallContext['caller_name']) || !empty($ticketCallContext['target_extension'])) -
-
Contesto chiamata
-
- @if(!empty($ticketCallContext['phone'])) -
-
Numero chiamante
-
{{ $ticketCallContext['phone'] }}
-
- @endif - @if(!empty($ticketCallContext['target_extension'])) -
-
Interno o gruppo chiamato
-
{{ $ticketCallContext['target_extension'] }}
-
- @endif - @if(!empty($ticketCallContext['received_at'])) -
-
Ricevuta alle
-
{{ $ticketCallContext['received_at'] }}
-
- @endif - @if(!empty($ticketCallContext['caller_name'])) -
-
Contatto associato
-
{{ $ticketCallContext['caller_name'] }}
-
- @endif - @if(!empty($ticketCallContext['caller_profile'])) -
-
Profilo
-
{{ $ticketCallContext['caller_profile'] }}
-
- @endif - @if(!empty($ticketCallContext['riferimento_stabile'])) -
-
Riferimento stabile
-
{{ $ticketCallContext['riferimento_stabile'] }}
-
- @endif - @if(!empty($ticketCallContext['riferimento_unita'])) -
-
Riferimento unità
-
{{ $ticketCallContext['riferimento_unita'] }}
-
- @endif - @if(!empty($ticketCallContext['stabile_label'])) -
-
Stabile collegato
-
{{ $ticketCallContext['stabile_label'] }}
-
- @endif -
- @if(!empty($ticketCallContext['call_note'])) -
-
Nota operativa importata
-
{{ $ticketCallContext['call_note'] }}
-
- @endif -
- @endif