netgescon-day0/app/Http/Controllers/Admin/CodeQualityController.php

2045 lines
93 KiB
PHP
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
// Use FQN in code to avoid aliasing issues with static analyzers
class CodeQualityController extends Controller
{
/**
* Regole di qualità del codice NetGescon
*/
private array $qualityRules = [
// Richiede il layout NetGescon nelle Blade
'layout_required' => [
'name' => 'Layout NetGescon Standard',
'description' => 'Le view Blade devono estendere admin.layouts.netgescon',
'pattern' => "/@extends\(['\"]admin\\.layouts\\.netgescon['\"]\)/m",
'suggestion' => "@extends('admin.layouts.netgescon')",
'severity' => 'error',
'mode' => 'require',
'fileTypes' => ['blade'],
// Escludi frammenti inclusi (partials/tabs) e componenti Blade
'excludeContains' => [
'resources/views/admin/gescon-import/partials',
'resources/views/admin/gescon-import/tabs',
'resources/views/components/menu',
'resources/views/components/',
'resources/views/admin/documenti/print-list.blade.php'
]
],
// Vietato l'uso del vecchio layout
'layout_forbidden_app' => [
'name' => 'Layout Obsoleto (layouts.app)',
'description' => 'Evita l\'uso del vecchio layout layouts.app',
'pattern' => "/@extends\(['\"]layouts\\.app['\"]\)/m",
'suggestion' => "Sostituisci con @extends('admin.layouts.netgescon')",
'severity' => 'error',
'mode' => 'prohibit',
'fileTypes' => ['blade']
],
'bootstrap_classes' => [
'name' => 'Classi Bootstrap/Tailwind Obsolete',
'description' => 'Rileva usage di classi Bootstrap o Tailwind non allineate allo stile NetGescon',
'pattern' => '/('
. '(?<!netgescon-)\bbtn(?:-outline)?-(?:primary|secondary|success|danger|warning|info)\b' // bootstrap buttons
. '|(?<!netgescon-)\bbtn\b' // generic btn
. '|\bcontainer(-fluid)?\b|\brow\b|\bcol(-[a-z0-9-]+)*\b' // grid
. '|\bd-(?:none|block|inline|inline-block|flex)\b|\bjustify-content-[a-z-]+\b|\balign-items-[a-z-]+\b' // layout
. '|\bform-control\b|\btable\b|\bbadge\b' // components
. '|\btext-(?:primary|secondary|success|danger|warning|info|muted)\b' // text colors
. '|\bbg-(?:primary|secondary|success|danger|warning|info|light|dark)\b' // bg colors
. '|\bfw-(?:bold|semibold|normal|light)\b' // font weight
. '|\bbg-blue-500\b|\bbg-gray-100\b|\btext-gray-900\b' // some Tailwind tones often misused
. ")/m",
'suggestion' => 'Usare classi NetGescon: netgescon-btn, netgescon-card, netgescon-text, ecc.',
'severity' => 'warning',
'mode' => 'prohibit',
'fileTypes' => ['blade'],
'excludeContains' => [
'resources/views/components/menu'
]
],
'netgescon_components' => [
'name' => 'Componenti NetGescon',
'description' => 'Rileva uso dei componenti NetGescon standard (informativo)',
'pattern' => '/(netgescon-btn|netgescon-card|netgescon-title|netgescon-text|netgescon-input)/m',
'suggestion' => 'Componenti NetGescon rilevati',
'severity' => 'info',
'mode' => 'detect',
'fileTypes' => ['blade']
],
'controller_namespace' => [
'name' => 'Namespace Controller',
'description' => 'I controller devono avere un namespace valido',
'pattern' => '/namespace\\s+App\\\\Http\\\\Controllers(\\\\[A-Za-z]+)?;/',
'suggestion' => 'namespace App\\Http\\Controllers\\Admin; (per i controller admin)',
'severity' => 'error',
'mode' => 'require',
'fileTypes' => ['php'],
'pathContains' => ['app/Http/Controllers']
],
'model_relationships' => [
'name' => 'Relazioni Eloquent',
'description' => 'Rileva la definizione delle relazioni Eloquent (informativo)',
'pattern' => '/(belongsTo|hasMany|hasOne|belongsToMany)\s*\(\s*[\'\"][A-Z]/m',
'suggestion' => 'Relazioni Eloquent definite correttamente',
'severity' => 'info',
'mode' => 'detect',
'fileTypes' => ['php']
],
'deprecated_helpers' => [
'name' => 'Helper Deprecati',
'description' => 'Rileva helper Laravel deprecati (array_get, array_set, str_is, starts_with, ends_with)',
'pattern' => '/\\b(array_get|array_set|str_is|starts_with|ends_with)\s*\(/m',
'suggestion' => 'Usare data_get(), Arr::set(), Str::is(), Str::startsWith(), Str::endsWith() o funzioni PHP 8',
'severity' => 'warning',
'mode' => 'prohibit',
'fileTypes' => ['php']
],
'security_issues' => [
'name' => 'Problemi di Sicurezza',
'description' => 'Rileva accesso diretto a superglobali o SQL non parametrizzato',
'pattern' => '/(\$_GET|\$_POST|\$_REQUEST|DB::raw\s*\(\s*[\'\"][^\'\"]*(SELECT|INSERT|UPDATE|DELETE))/im',
'suggestion' => 'Usare Request validation e Query Builder/parametri',
'severity' => 'critical',
'mode' => 'prohibit',
'fileTypes' => ['php']
],
'migration_standards' => [
'name' => 'Standard Migration',
'description' => 'Verifica uso delle API Laravel Migration (informativo)',
'pattern' => '/(Schema::create|Schema::table|Schema::dropIfExists)/m',
'suggestion' => 'Uso corretto delle Migration rilevato',
'severity' => 'info',
'mode' => 'detect',
'fileTypes' => ['php'],
'pathContains' => ['database/migrations']
],
// Debug Blade: bilanciamento @section/@endsection
'blade_sections_balance' => [
'name' => 'Bilanciamento Sezioni Blade',
'description' => 'Ogni @section deve avere un @endsection corrispondente',
'pattern' => '/@section\b|@endsection\b/m',
'suggestion' => 'Aggiungi o rimuovi le direttive per bilanciare le sezioni',
'severity' => 'error',
'mode' => 'detect',
'fileTypes' => ['blade']
],
// Debug Blade: div bilanciati
'unbalanced_divs' => [
'name' => 'Div non bilanciati',
'description' => 'Numero di <div> aperti non corrisponde ai </div> chiusi',
'pattern' => '/<div\b|<\/div>/im',
'balanceTag' => 'div',
'suggestion' => 'Verifica la struttura dei contenitori e chiudi tutti i div',
'severity' => 'error',
'mode' => 'detect',
'fileTypes' => ['blade']
],
'unbalanced_navs' => [
'name' => 'Nav non bilanciati',
'description' => 'Numero di <nav> aperti non corrisponde ai </nav> chiusi',
'pattern' => '/<nav\b|<\/nav>/im',
'balanceTag' => 'nav',
'suggestion' => 'Verifica che <nav> sia chiuso correttamente',
'severity' => 'warning',
'mode' => 'detect',
'fileTypes' => ['blade']
],
'unbalanced_uls' => [
'name' => 'Liste non bilanciate',
'description' => 'Numero di <ul> aperti non corrisponde ai </ul> chiusi',
'pattern' => '/<ul\b|<\/ul>/im',
'balanceTag' => 'ul',
'suggestion' => 'Verifica la chiusura delle liste <ul>',
'severity' => 'warning',
'mode' => 'detect',
'fileTypes' => ['blade']
],
// Altri contenitori comuni: section, header, footer, main, article
'unbalanced_sections' => [
'name' => 'Section non bilanciate',
'description' => 'Numero di <section> aperti non corrisponde ai </section> chiusi',
'pattern' => '/<section\b|<\/section>/im',
'balanceTag' => 'section',
'suggestion' => 'Verifica la chiusura dei tag <section>',
'severity' => 'warning',
'mode' => 'detect',
'fileTypes' => ['blade']
],
'unbalanced_headers' => [
'name' => 'Header non bilanciati',
'description' => 'Numero di <header> aperti non corrisponde ai </header> chiusi',
'pattern' => '/<header\b|<\/header>/im',
'balanceTag' => 'header',
'suggestion' => 'Verifica la chiusura dei tag <header>',
'severity' => 'warning',
'mode' => 'detect',
'fileTypes' => ['blade']
],
'unbalanced_footers' => [
'name' => 'Footer non bilanciati',
'description' => 'Numero di <footer> aperti non corrisponde ai </footer> chiusi',
'pattern' => '/<footer\b|<\/footer>/im',
'balanceTag' => 'footer',
'suggestion' => 'Verifica la chiusura dei tag <footer>',
'severity' => 'warning',
'mode' => 'detect',
'fileTypes' => ['blade']
],
'unbalanced_main' => [
'name' => 'Main non bilanciati',
'description' => 'Numero di <main> aperti non corrisponde ai </main> chiusi',
'pattern' => '/<main\b|<\/main>/im',
'balanceTag' => 'main',
'suggestion' => 'Verifica la chiusura dei tag <main>',
'severity' => 'warning',
'mode' => 'detect',
'fileTypes' => ['blade']
],
'unbalanced_articles' => [
'name' => 'Article non bilanciati',
'description' => 'Numero di <article> aperti non corrisponde ai </article> chiusi',
'pattern' => '/<article\b|<\/article>/im',
'balanceTag' => 'article',
'suggestion' => 'Verifica la chiusura dei tag <article>',
'severity' => 'warning',
'mode' => 'detect',
'fileTypes' => ['blade']
],
// Debug specifico Gescon Import
// (Confluita in gescon_import_cards_required più in basso)
'gescon_import_menu_partial' => [
'name' => 'Sottomenu Gescon Import',
'description' => 'Includi il parziale del menu Gescon Import per una navigazione coerente',
'pattern' => '/gescon-import\.(partials|partial)\.(menu|nav)/m',
'suggestion' => "@include('admin.gescon-import.partials.menu')",
'severity' => 'warning',
'mode' => 'require',
'fileTypes' => ['blade'],
'pathContains' => ['resources/views/admin/gescon-import/index.blade.php'],
'excludeContains' => [
'resources/views/admin/gescon-import/partials/menu.blade.php',
'resources/views/admin/gescon-import/partials'
],
],
'gescon_import_columns_detect' => [
'name' => 'Layout colonne (Gescon Import)',
'description' => 'Suggerisce uso di griglie/colonne per allineamento (grid-cols-*, col-span-*, flex)',
'pattern' => '/(grid-cols-|col-span-|md:grid-cols-|\bflex\b)/m',
'suggestion' => 'Usa grid/flex NetGescon: <div class="grid md:grid-cols-2 gap-6">…</div>',
'severity' => 'info',
'mode' => 'detect',
'fileTypes' => ['blade'],
'pathContains' => ['resources/views/admin/gescon-import']
],
'gescon_import_cards_required' => [
'name' => 'Card NetGescon coerenti',
'description' => 'I blocchi principali devono usare <div class="netgescon-card"> con bordo/ombra coerenti',
'pattern' => '/netgescon-card/m',
'suggestion' => 'Avvolgi i blocchi in <div class="netgescon-card"> … </div> e usa header/body/footer standard',
'severity' => 'warning',
'mode' => 'require',
'fileTypes' => ['blade'],
'pathContains' => ['resources/views/admin/gescon-import'],
'excludeContains' => [
'resources/views/admin/gescon-import/partials',
'resources/views/admin/gescon-import/tabs'
]
],
// Controllo integrità sidebar NetGescon
'sidebar_toggle_integrity' => [
'name' => 'Integrità Toggle Sidebar',
'description' => 'Ogni toggleCategory(\'id\') deve avere #id-menu e #id-chevron nella sidebar',
'pattern' => '/toggleCategory\(\'([a-zA-Z0-9_-]+)\'/m',
'suggestion' => 'Aggiungi <ul id="id-menu"> e <i id="id-chevron"> per ogni categoria',
'severity' => 'error',
'mode' => 'detect',
'fileTypes' => ['blade'],
'pathContains' => ['resources/views/components/menu/sidebar-netgescon.blade.php']
],
// Sidebar: vieta onclick su <a> per il toggle; usare la freccia .ng-chevron-toggle
'sidebar_anchor_toggle_forbidden' => [
'name' => 'Toggle su <a> vietato',
'description' => 'Il toggle dell\'accordion non deve essere sull\'anchor; usare la freccia con classe ng-chevron-toggle',
'pattern' => '/<a[^>]*onclick\s*=\s*["\']toggleCategory\(/im',
'suggestion' => 'Rimuovi onclick dall\'anchor e aggiungi onclick alla freccia con classe ng-chevron-toggle',
'severity' => 'error',
'mode' => 'prohibit',
'fileTypes' => ['blade'],
'pathContains' => ['resources/views/components/menu/sidebar-netgescon.blade.php']
],
// Sidebar: richiede presenza della classe ng-chevron-toggle (freccia cliccabile)
'sidebar_chevron_toggle_required' => [
'name' => 'Freccia Toggle Richiesta',
'description' => 'Le icone chevron devono avere la classe ng-chevron-toggle per il toggle indipendente',
'pattern' => '/ng-chevron-toggle/m',
'suggestion' => 'Aggiungi class="ng-chevron-toggle" all\'icona <i id="*-chevron">',
'severity' => 'warning',
'mode' => 'require',
'fileTypes' => ['blade'],
'pathContains' => ['resources/views/components/menu/sidebar-netgescon.blade.php']
],
// Sidebar: la dashboard GESCON Import deve essere link di primo livello, non nel sottomenu
'sidebar_gescon_dashboard_top' => [
'name' => 'Dashboard Gescon Import al primo livello',
'description' => 'La voce GESCON IMPORT deve puntare direttamente alla dashboard (#dashboard)',
'pattern' => '/href=\\s*["\']\{\{\s*route\(\'admin\\.gescon-import\\.index\'\)\s*\}\}#dashboard["\']/m',
'suggestion' => "Imposta l'href principale a route('admin.gescon-import.index') . '#dashboard'",
'severity' => 'warning',
'mode' => 'require',
'fileTypes' => ['blade'],
'pathContains' => ['resources/views/components/menu/sidebar-netgescon.blade.php']
],
'sidebar_gescon_dashboard_not_submenu' => [
'name' => 'Dashboard Gescon Import non nel sottomenu',
'description' => 'Rimuovere la voce "Dashboard Import" dal sottomenu Gescon',
'pattern' => '/Dashboard\s+Import/m',
'suggestion' => 'Elimina la voce secondaria e mantieni solo il link principale con #dashboard',
'severity' => 'info',
'mode' => 'prohibit',
'fileTypes' => ['blade'],
'pathContains' => ['resources/views/components/menu/sidebar-netgescon.blade.php']
],
// GESCON Import: garantire uso card NetGescon nella dashboard
'gescon_import_dashboard_cards' => [
'name' => 'Dashboard Gescon Import con Card NetGescon',
'description' => 'La dashboard di Gescon Import deve utilizzare i container netgescon-card',
'pattern' => '/netgescon-card/m',
'suggestion' => 'Avvolgi i blocchi in <div class="netgescon-card"> … </div>',
'severity' => 'warning',
'mode' => 'require',
'fileTypes' => ['blade'],
'pathContains' => ['resources/views/admin/gescon-import/tabs/dashboard.blade.php']
],
// Rileva badge Bootstrap nelle viste Gescon Import
'bootstrap_badges' => [
'name' => 'Badge Bootstrap obsoleti',
'description' => 'Evita l\'uso di classi badge Bootstrap (badge bg-*) nelle viste Gescon Import',
'pattern' => '/\bbadge\s+bg-[a-z0-9-]+/im',
'suggestion' => 'Usa netgescon-badge e varianti (netgescon-badge-primary, -success, -warning, ...)',
'severity' => 'warning',
'mode' => 'prohibit',
'fileTypes' => ['blade'],
'pathContains' => ['resources/views/admin/gescon-import']
],
// Verifica inclusion Material Design Icons nel layout admin
'mdi_stylesheet_required' => [
'name' => 'Material Design Icons non incluse',
'description' => 'Il layout admin deve includere il CSS delle Material Design Icons per le icone mdi-*',
'pattern' => '/materialdesignicons\.min\.css/m',
'suggestion' => '<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css">',
'severity' => 'error',
'mode' => 'require',
'fileTypes' => ['blade'],
'pathContains' => ['resources/views/admin/layouts/netgescon.blade.php']
],
// Verifica presenza Font Awesome nel layout admin
'fontawesome_stylesheet_required' => [
'name' => 'Font Awesome non incluso',
'description' => 'Il layout admin deve includere il CSS di Font Awesome per le icone fa-*/fas',
'pattern' => '/cdnjs\.cloudflare\.com\/ajax\/libs\/font-awesome/m',
'suggestion' => '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">',
'severity' => 'warning',
'mode' => 'require',
'fileTypes' => ['blade'],
'pathContains' => ['resources/views/admin/layouts/netgescon.blade.php']
],
// Verifica presenza CSS/JS NetGescon nel layout
'netgescon_css_required' => [
'name' => 'CSS NetGescon mancante',
'description' => 'Il layout admin deve includere css/netgescon-admin.css',
'pattern' => '/asset\(\'css\/netgescon-admin\.css\'\)/m',
'suggestion' => "<link rel=\"stylesheet\" href=\"{{ asset('css/netgescon-admin.css') }}\">",
'severity' => 'error',
'mode' => 'require',
'fileTypes' => ['blade'],
'pathContains' => ['resources/views/admin/layouts/netgescon.blade.php']
],
'netgescon_js_required' => [
'name' => 'JS NetGescon mancante',
'description' => 'Il layout admin deve includere js/netgescon-admin.js',
'pattern' => '/asset\(\'js\/netgescon-admin\.js\'\)/m',
'suggestion' => "<script src=\"{{ asset('js/netgescon-admin.js') }}\"></script>",
'severity' => 'error',
'mode' => 'require',
'fileTypes' => ['blade'],
'pathContains' => ['resources/views/admin/layouts/netgescon.blade.php']
],
// Verifica presenza Vite assets nel layout
'vite_assets_required' => [
'name' => 'Asset Vite mancanti',
'description' => 'Il layout admin deve caricare gli asset con @vite([...])',
'pattern' => '/@vite\s*\(/m',
'suggestion' => "@vite(['resources/css/app.css', 'resources/js/app.js'])",
'severity' => 'warning',
'mode' => 'require',
'fileTypes' => ['blade'],
'pathContains' => ['resources/views/admin/layouts/netgescon.blade.php']
],
// Uso mdi-* senza layout o link diretto
'mdi_usage_requires_layout_or_link' => [
'name' => 'Uso icone MDI senza stile',
'description' => 'Rilevato uso di classi mdi-* senza estendere il layout NetGescon o linkare il CSS MDI',
'pattern' => '/\bmdi(-|\s)/m',
'suggestion' => "Estendi @extends('admin.layouts.netgescon') o includi il CSS MDI nella pagina",
'severity' => 'error',
'mode' => 'detect',
'fileTypes' => ['blade'],
'excludeContains' => [
'resources/views/components/',
'partials/',
'resources/views/admin/gescon-import/tabs'
]
],
// Uso fa-/fas senza layout o link diretto
'fa_usage_requires_layout_or_link' => [
'name' => 'Uso icone Font Awesome senza stile',
'description' => 'Rilevato uso di classi fa*/fas senza estendere il layout NetGescon o linkare il CSS Font Awesome',
'pattern' => '/\bfa[srldb]?-?[a-z0-9-]*/im',
'suggestion' => "Estendi @extends('admin.layouts.netgescon') o includi il CSS di Font Awesome",
'severity' => 'warning',
'mode' => 'detect',
'fileTypes' => ['blade'],
'excludeContains' => [
'resources/views/components/',
'partials/',
'resources/views/admin/gescon-import/tabs'
]
],
// Uso classi NetGescon senza CSS
'netgescon_btn_usage_requires_css' => [
'name' => 'Uso netgescon-btn senza CSS',
'description' => 'Rilevato uso di classi NetGescon (netgescon-*) senza estendere il layout o includere il CSS',
'pattern' => '/netgescon-\w+/m',
'suggestion' => "Estendi @extends('admin.layouts.netgescon') o includi css/netgescon-admin.css",
'severity' => 'error',
'mode' => 'detect',
'fileTypes' => ['blade'],
'excludeContains' => [
'resources/views/components/',
'partials/',
'resources/views/admin/gescon-import/tabs'
]
],
];
// Mapping utility per filtro modulo/menù
private array $modulePaths = [
'gescon-import' => [
'resources/views/admin/gescon-import',
'app/Http/Controllers/Admin/GesconImportController.php',
],
'documenti' => [
'resources/views/admin/documenti',
'app/Http/Controllers/Admin/DocumentiController.php',
],
'stabili' => [
'resources/views/admin/stabili',
'app/Http/Controllers/Admin/StabileController.php',
],
'contabilita-gescon' => [
'resources/views/admin/contabilita-gescon',
'app/Http/Controllers/Admin/ContabilitaGesconController.php',
],
'supporto' => [
'resources/views/components/menu',
'app/Http/Controllers/Admin/CodeQualityController.php',
'routes/web.php',
],
];
private array $menuPaths = [
'documenti' => ['resources/views/admin/documenti'],
'stabili' => ['resources/views/admin/stabili'],
'condomini' => ['resources/views/admin/condomini'],
'amministrazione' => ['resources/views/admin/amministrazione'],
'contabilita' => ['resources/views/admin/contabilita', 'resources/views/admin/contabilita-gescon'],
'gestione-economica' => ['resources/views/admin/gestione-economica'],
'moduli' => ['resources/views/admin/moduli'],
'gescon-import' => ['resources/views/admin/gescon-import'],
'supporto' => ['resources/views/components/menu', 'resources/views/admin/code-quality'],
];
/**
* Dashboard controllo qualità
*/
public function index(\Illuminate\Http\Request $request)
{
$scanPath = $request->get('path', userSetting('code_quality.path', 'app,resources/views'));
$severityFilter = $request->get('severity', userSetting('code_quality.severity', 'all'));
// Normalizza severity per evitare valori non validi
$allowedSeverities = ['all', 'critical', 'error', 'warning', 'info'];
if (!in_array($severityFilter, $allowedSeverities, true)) {
$severityFilter = 'all';
}
$moduleFilter = $request->get('module', userSetting('code_quality.module', 'all'));
$menuFilter = $request->get('menu', userSetting('code_quality.menu', 'all'));
$includeDataChecks = filter_var($request->get('include_data_checks', userSetting('code_quality.include_data_checks', true)), FILTER_VALIDATE_BOOLEAN);
$results = $this->scanCodeQuality($scanPath, $severityFilter, $moduleFilter, $menuFilter, $includeDataChecks);
$totalScanned = $this->countFilesInPaths($scanPath, $moduleFilter, $menuFilter);
return View::make('admin.code-quality.index', [
'results' => $results,
'scanPath' => $scanPath,
'severityFilter' => $severityFilter,
'totalScanned' => $totalScanned,
'moduleFilter' => $moduleFilter,
'menuFilter' => $menuFilter,
'includeDataChecks' => $includeDataChecks,
]);
}
/**
* Scansiona qualità del codice
*/
public function scan(\Illuminate\Http\Request $request)
{
$scanPath = $request->get('path', userSetting('code_quality.path', 'app,resources/views'));
$severityFilter = $request->get('severity', userSetting('code_quality.severity', 'all'));
$allowedSeverities = ['all', 'critical', 'error', 'warning', 'info'];
if (!in_array($severityFilter, $allowedSeverities, true)) {
$severityFilter = 'all';
}
$moduleFilter = $request->get('module', userSetting('code_quality.module', 'all'));
$menuFilter = $request->get('menu', userSetting('code_quality.menu', 'all'));
$includeDataChecks = filter_var($request->get('include_data_checks', userSetting('code_quality.include_data_checks', true)), FILTER_VALIDATE_BOOLEAN);
// Salva preferenze per utente
setUserSetting('code_quality.path', $scanPath);
setUserSetting('code_quality.severity', $severityFilter);
setUserSetting('code_quality.module', $moduleFilter);
setUserSetting('code_quality.menu', $menuFilter);
setUserSetting('code_quality.include_data_checks', $includeDataChecks);
$results = $this->scanCodeQuality($scanPath, $severityFilter, $moduleFilter, $menuFilter, $includeDataChecks);
$totalScanned = $this->countFilesInPaths($scanPath, $moduleFilter, $menuFilter);
return Response::json([
'success' => true,
'results' => $results,
'summary' => $this->generateSummary($results, $totalScanned)
]);
}
/**
* Esporta i risultati in JSON o CSV (rigenera la scansione con i parametri correnti)
*/
public function export(\Illuminate\Http\Request $request)
{
$scanPath = $request->get('path', userSetting('code_quality.path', 'app,resources/views'));
$severityFilter = $request->get('severity', userSetting('code_quality.severity', 'all'));
$moduleFilter = $request->get('module', userSetting('code_quality.module', 'all'));
$menuFilter = $request->get('menu', userSetting('code_quality.menu', 'all'));
$includeDataChecks = filter_var($request->get('include_data_checks', userSetting('code_quality.include_data_checks', true)), FILTER_VALIDATE_BOOLEAN);
$format = strtolower($request->get('format', 'csv'));
$results = $this->scanCodeQuality($scanPath, $severityFilter, $moduleFilter, $menuFilter, $includeDataChecks);
$totalScanned = $this->countFilesInPaths($scanPath, $moduleFilter, $menuFilter);
$summary = $this->generateSummary($results, $totalScanned);
if ($format === 'json') {
$payload = [
'params' => compact('scanPath', 'severityFilter', 'moduleFilter', 'menuFilter'),
'summary' => $summary,
'results' => $results,
];
$json = json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
return Response::make($json, 200, [
'Content-Type' => 'application/json; charset=UTF-8',
'Content-Disposition' => 'attachment; filename="code-quality-report.json"',
]);
}
// CSV flat: file, module, menu, rule, severity, line, suggestion, match
$rows = [
['file', 'module', 'menu', 'rule', 'name', 'severity', 'line', 'description', 'suggestion', 'match']
];
foreach ($results as $fileResult) {
foreach ($fileResult['issues'] as $issue) {
$rows[] = [
$fileResult['file'] ?? '',
$fileResult['module'] ?? '',
$fileResult['menu'] ?? '',
$issue['rule'] ?? '',
$issue['name'] ?? '',
$issue['severity'] ?? '',
$issue['line'] ?? '',
$issue['description'] ?? '',
$issue['suggestion'] ?? '',
$issue['match'] ?? '',
];
}
}
$fh = fopen('php://temp', 'w+');
foreach ($rows as $r) {
// CSV in UTF-8 con separatore ';' per excel-friendly
fputcsv($fh, $r, ';');
}
rewind($fh);
$csv = stream_get_contents($fh);
fclose($fh);
return Response::make($csv, 200, [
'Content-Type' => 'text/csv; charset=UTF-8',
'Content-Disposition' => 'attachment; filename="code-quality-report.csv"',
]);
}
/**
* Esegue la scansione effettiva
*/
private function scanCodeQuality(string $scanPaths, string $severityFilter, string $moduleFilter = 'all', string $menuFilter = 'all', bool $includeDataChecks = true): array
{
$results = [];
$paths = explode(',', $scanPaths);
foreach ($paths as $path) {
$path = trim($path);
$fullPath = App::basePath($path);
if (!is_dir($fullPath) && !is_file($fullPath)) {
continue;
}
$files = $this->getFilesToScan($fullPath);
foreach ($files as $file) {
// Lettura sicura del contenuto con gestione eccezioni
try {
$content = @file_get_contents($file);
} catch (\Throwable $e) {
$relativePath = str_replace(App::basePath('') . '/', '', $file);
$results[] = [
'file' => $relativePath,
'module' => $this->classifyModule($relativePath),
'menu' => $this->classifyMenu($relativePath),
'issues' => [[
'rule' => 'file_unreadable',
'name' => 'File non leggibile',
'description' => 'Impossibile leggere il file: ' . $e->getMessage(),
'severity' => 'critical',
'line' => 1,
'match' => '—',
'suggestion' => 'Verifica i permessi o escludi il percorso dalla scansione',
'context' => []
]]
];
continue;
}
$relativePath = str_replace(App::basePath('') . '/', '', $file);
// Applica filtri modulo/menù
if (!$this->pathMatchesFilters($relativePath, $moduleFilter, $menuFilter)) {
continue;
}
// Analisi con isolamento degli errori per file
try {
$fileResults = $this->analyzeFile($file, $content, $relativePath);
} catch (\Throwable $e) {
$fileResults = [[
'rule' => 'scan_exception',
'name' => 'Errore di scansione file',
'description' => 'Eccezione durante la scansione: ' . $e->getMessage(),
'severity' => 'critical',
'line' => 1,
'match' => '—',
'suggestion' => 'Riduci il file o escludilo dalla scansione',
'context' => []
]];
}
if (!empty($fileResults)) {
$results[] = [
'file' => $relativePath,
'module' => $this->classifyModule($relativePath),
'menu' => $this->classifyMenu($relativePath),
'issues' => $fileResults
];
}
}
}
if ($includeDataChecks) {
$results = array_merge($results, $this->runArchiveConsistencyChecks());
}
// Filtro per severity
if ($severityFilter !== 'all') {
$results = $this->filterBySeverity($results, $severityFilter);
}
return $results;
}
private function runArchiveConsistencyChecks(): array
{
$results = [];
$millesimiIssues = $this->checkMillesimiConsistency();
if (!empty($millesimiIssues)) {
$results[] = [
'file' => 'quality://archives/millesimi',
'module' => 'contabilita-gescon',
'menu' => 'condomini',
'issues' => $millesimiIssues,
];
}
$legacyIssues = $this->checkLegacyIndividualChargesConsistency();
if (!empty($legacyIssues)) {
$results[] = [
'file' => 'quality://archives/legacy-individuali',
'module' => 'gescon-import',
'menu' => 'contabilita',
'issues' => $legacyIssues,
];
}
$legacyIncassiIssues = $this->checkLegacyIncassiConsistency();
if (!empty($legacyIncassiIssues)) {
$results[] = [
'file' => 'quality://archives/legacy-incassi',
'module' => 'gescon-import',
'menu' => 'contabilita',
'issues' => $legacyIncassiIssues,
];
}
$procedureIssues = $this->checkProcedureArchiveConsistency();
if (!empty($procedureIssues)) {
$results[] = [
'file' => 'quality://procedures/archive-mapping',
'module' => 'supporto',
'menu' => 'supporto',
'issues' => $procedureIssues,
];
}
$anagraficaIssues = $this->checkStabileRegistryConsistency();
if (!empty($anagraficaIssues)) {
$results[] = [
'file' => 'quality://archives/stabile-anagrafica',
'module' => 'stabili',
'menu' => 'condomini',
'issues' => $anagraficaIssues,
];
}
return $results;
}
private function checkStabileRegistryConsistency(): array
{
$issues = [];
if (!Schema::hasTable('stabili')) {
return [
$this->makeQualityIssue(
'anagrafica_missing_stabili',
'Archivio stabili mancante',
'La tabella stabili non è disponibile: impossibile verificare allineamento anagrafico.',
'critical',
'Ripristina la tabella stabili e riesegui il controllo qualità.'
),
];
}
if (Schema::hasTable('unita_immobiliari') && Schema::hasColumn('unita_immobiliari', 'stabile_id')) {
$orphanUnita = DB::table('unita_immobiliari as u')
->leftJoin('stabili as s', 's.id', '=', 'u.stabile_id')
->whereNull('s.id')
->count();
if ($orphanUnita > 0) {
$issues[] = $this->makeQualityIssue(
'anagrafica_unita_orfane_stabile',
'Unità immobiliari senza stabile valido',
'Rilevate ' . $orphanUnita . ' unità immobiliari con stabile_id non valido.',
'error',
'Allinea unita_immobiliari.stabile_id con stabili.id.'
);
}
}
if (Schema::hasTable('tabelle_millesimali') && Schema::hasColumn('tabelle_millesimali', 'stabile_id')) {
$orphanTabelle = DB::table('tabelle_millesimali as t')
->leftJoin('stabili as s', 's.id', '=', 't.stabile_id')
->whereNull('s.id')
->count();
if ($orphanTabelle > 0) {
$issues[] = $this->makeQualityIssue(
'anagrafica_tabelle_orfane_stabile',
'Tabelle millesimali senza stabile valido',
'Rilevate ' . $orphanTabelle . ' tabelle millesimali con stabile_id non valido.',
'error',
'Allinea tabelle_millesimali.stabile_id con stabili.id.'
);
}
}
if (
Schema::hasTable('dettaglio_millesimi')
&& Schema::hasTable('tabelle_millesimali')
&& Schema::hasTable('unita_immobiliari')
&& Schema::hasColumn('tabelle_millesimali', 'stabile_id')
&& Schema::hasColumn('unita_immobiliari', 'stabile_id')
) {
$mismatchMillesimi = DB::table('dettaglio_millesimi as d')
->join('tabelle_millesimali as t', 't.id', '=', 'd.tabella_millesimale_id')
->join('unita_immobiliari as u', 'u.id', '=', 'd.unita_immobiliare_id')
->whereRaw('COALESCE(t.stabile_id, 0) <> COALESCE(u.stabile_id, 0)')
->count();
if ($mismatchMillesimi > 0) {
$issues[] = $this->makeQualityIssue(
'anagrafica_millesimi_stabile_mismatch',
'Dettagli millesimali su unità di stabile diverso',
'Rilevate ' . $mismatchMillesimi . ' righe dettaglio_millesimi con stabile tabella diverso dallo stabile unità.',
'critical',
'Usa una sola anagrafica stabile: tabella e unità devono appartenere allo stesso stabile.'
);
}
}
if (
Schema::hasTable('rubrica_ruoli')
&& Schema::hasTable('unita_immobiliari')
&& Schema::hasColumn('rubrica_ruoli', 'stabile_id')
&& Schema::hasColumn('rubrica_ruoli', 'unita_immobiliare_id')
&& Schema::hasColumn('unita_immobiliari', 'stabile_id')
) {
$mismatchRuoli = DB::table('rubrica_ruoli as rr')
->join('unita_immobiliari as u', 'u.id', '=', 'rr.unita_immobiliare_id')
->whereNotNull('rr.stabile_id')
->whereRaw('COALESCE(rr.stabile_id, 0) <> COALESCE(u.stabile_id, 0)')
->count();
if ($mismatchRuoli > 0) {
$issues[] = $this->makeQualityIssue(
'anagrafica_rubrica_stabile_mismatch',
'Ruoli rubrica disallineati con stabile dellunità',
'Rilevate ' . $mismatchRuoli . ' righe rubrica_ruoli con stabile_id diverso da quello di unita_immobiliare_id.',
'error',
'Allinea rubrica_ruoli.stabile_id al valore stabile dellunità collegata.'
);
}
}
if (
Schema::hasTable('persone_unita_relazioni')
&& Schema::hasTable('persone')
&& Schema::hasTable('unita_immobiliari')
) {
$orphanRelPersona = DB::table('persone_unita_relazioni as pur')
->leftJoin('persone as p', 'p.id', '=', 'pur.persona_id')
->whereNull('p.id')
->count();
if ($orphanRelPersona > 0) {
$issues[] = $this->makeQualityIssue(
'anagrafica_relazioni_persona_orfane',
'Relazioni persona-unità senza persona valida',
'Rilevate ' . $orphanRelPersona . ' righe in persone_unita_relazioni senza persona associata.',
'error',
'Bonifica le relazioni orfane su persone_unita_relazioni.persona_id.'
);
}
$orphanRelUnita = DB::table('persone_unita_relazioni as pur')
->leftJoin('unita_immobiliari as u', 'u.id', '=', 'pur.unita_id')
->whereNull('u.id')
->count();
if ($orphanRelUnita > 0) {
$issues[] = $this->makeQualityIssue(
'anagrafica_relazioni_unita_orfane',
'Relazioni persona-unità senza unità valida',
'Rilevate ' . $orphanRelUnita . ' righe in persone_unita_relazioni senza unità associata.',
'error',
'Bonifica le relazioni orfane su persone_unita_relazioni.unita_id.'
);
}
}
if (Schema::hasTable('incassi')) {
if (Schema::hasColumn('incassi', 'gestione_id') && Schema::hasTable('gestioni_contabili') && Schema::hasColumn('gestioni_contabili', 'id')) {
$orphanIncassiGestione = DB::table('incassi as i')
->leftJoin('gestioni_contabili as g', 'g.id', '=', 'i.gestione_id')
->whereNotNull('i.gestione_id')
->whereNull('g.id')
->count();
if ($orphanIncassiGestione > 0) {
$issues[] = $this->makeQualityIssue(
'anagrafica_incassi_gestione_orfana',
'Incassi con gestione non valida',
'Rilevati ' . $orphanIncassiGestione . ' incassi con gestione_id non presente in gestioni_contabili.',
'error',
'Allinea incassi.gestione_id alle gestioni contabili esistenti.'
);
}
}
if (Schema::hasColumn('incassi', 'importo_pagato_euro') && Schema::hasColumn('incassi', 'importo_euro')) {
$scaledRows = DB::table('incassi')
->whereRaw('ABS(COALESCE(importo_euro, 0)) > 0')
->whereRaw('ABS(COALESCE(importo_pagato_euro, 0)) > (ABS(COALESCE(importo_euro, 0)) * 1000)')
->count();
if ($scaledRows > 0) {
$issues[] = $this->makeQualityIssue(
'anagrafica_incassi_scaled_amounts',
'Incassi con importo_pagato_euro in scala legacy diversa',
'Rilevati ' . $scaledRows . ' incassi dove importo_pagato_euro è in scala diversa da importo_euro (es. x10000).',
'warning',
'In visualizzazione e import usa importo_euro/importo come fonte primaria, importo_pagato_euro solo come fallback normalizzato.'
);
}
$signMismatch = DB::table('incassi')
->whereRaw('ABS(COALESCE(importo_euro, 0)) > 0')
->whereRaw('ABS(COALESCE(importo_pagato_euro, 0)) > 0')
->whereRaw('SIGN(COALESCE(importo_euro, 0)) <> SIGN(COALESCE(importo_pagato_euro, 0))')
->count();
if ($signMismatch > 0) {
$issues[] = $this->makeQualityIssue(
'anagrafica_incassi_sign_mismatch',
'Incassi con segno incoerente tra campi importo',
'Rilevati ' . $signMismatch . ' incassi con segno differente tra importo_euro e importo_pagato_euro.',
'error',
'Verifica import legacy e imposta regola unica di segno in fase di caricamento.'
);
}
}
}
if (empty($issues)) {
$issues[] = $this->makeQualityIssue(
'anagrafica_registry_consistent',
'Anagrafica stabile coerente',
'Nessuna anomalia rilevata tra stabile, unità, millesimi, ruoli, relazioni e incassi.',
'info',
'Mantieni una sola anagrafica stabile come fonte unica per tutte le procedure.'
);
}
return $issues;
}
private function checkProcedureArchiveConsistency(): array
{
$issues = [];
$appPath = App::basePath('app');
if (!is_dir($appPath)) {
return $issues;
}
$millesimiTables = [
'dettaglio_millesimi',
'dettagli_tabelle_millesimali',
'dettagli_millesimali',
];
$tableRefs = [
'dettaglio_millesimi' => [],
'dettagli_tabelle_millesimali' => [],
'dettagli_millesimali' => [],
];
$dettPersFiles = [];
$legacyWeakMapping = [];
$filamentStabileSensitiveNoContext = [];
try {
$files = File::allFiles($appPath);
} catch (\Throwable $e) {
return [
$this->makeQualityIssue(
'procedure_scan_failed',
'Scansione procedure non disponibile',
'Impossibile leggere i file applicativi: ' . $e->getMessage(),
'warning',
'Verifica permessi filesystem e riesegui il controllo qualità.'
),
];
}
foreach ($files as $file) {
$path = $file->getPathname();
if (!str_ends_with($path, '.php')) {
continue;
}
$relative = str_replace(App::basePath('') . DIRECTORY_SEPARATOR, '', $path);
$content = @file_get_contents($path);
if (!is_string($content) || $content === '') {
continue;
}
$lc = Str::lower($content);
foreach ($millesimiTables as $tableName) {
if (Str::contains($lc, Str::lower($tableName))) {
$tableRefs[$tableName][] = $relative;
}
}
if (Str::contains($lc, 'dett_pers')) {
$dettPersFiles[] = $relative;
$hasNspe = Str::contains($lc, 'n_spe');
$hasLegacyComposite = Str::contains($lc, 'unico') || Str::contains($lc, 'n_stra') || Str::contains($lc, 'tipo_gestione');
if ($hasNspe && !$hasLegacyComposite) {
$legacyWeakMapping[] = $relative;
}
}
if (Str::startsWith($relative, 'app/Filament/Pages/')) {
$mentionsSensitiveArchive =
Str::contains($lc, 'tabellamillesimale')
|| Str::contains($lc, 'dettagliomillesimi')
|| Str::contains($lc, 'unitaimmobiliare')
|| Str::contains($lc, 'personaunitarelazione')
|| Str::contains($lc, 'incasso')
|| Str::contains($lc, "'stabile_id'")
|| Str::contains($lc, 'stabile_id');
$hasStabileContext = Str::contains($lc, 'stabilecontext::resolveactivestabileid')
|| Str::contains($lc, 'stabilecontext::resolveactive')
|| Str::contains($lc, 'app\\support\\stabilecontext');
if ($mentionsSensitiveArchive && !$hasStabileContext) {
$filamentStabileSensitiveNoContext[] = $relative;
}
}
}
$usedTables = collect($tableRefs)
->filter(fn ($filesForTable) => !empty($filesForTable))
->map(fn ($filesForTable) => array_values(array_unique($filesForTable)))
->all();
if (count($usedTables) > 1) {
$parts = [];
foreach ($usedTables as $table => $filesForTable) {
$parts[] = $table . ' (' . count($filesForTable) . ' file)';
}
$issues[] = $this->makeQualityIssue(
'procedure_millesimi_multi_archive_usage',
'Procedure millesimi su archivi multipli',
'Le procedure applicative usano più tabelle dettaglio millesimi: ' . implode(', ', $parts) . '.',
'warning',
'Definisci archivio canonico e allinea tutte le procedure a quella sorgente.'
);
foreach ($usedTables as $table => $filesForTable) {
$sample = implode(', ', array_slice($filesForTable, 0, 3));
$issues[] = $this->makeQualityIssue(
'procedure_millesimi_table_usage_' . $table,
'Utilizzo tabella ' . $table,
'Riferimenti in ' . count($filesForTable) . ' file. Esempi: ' . $sample,
'info',
'Conferma che questi file debbano puntare alla stessa sorgente dati.'
);
}
}
if (!empty($dettPersFiles) && !empty($legacyWeakMapping)) {
$sample = implode(', ', array_slice(array_values(array_unique($legacyWeakMapping)), 0, 5));
$issues[] = $this->makeQualityIssue(
'procedure_legacy_dett_pers_weak_join',
'Correlazione dett_pers non completa in alcune procedure',
'Alcuni file sembrano correlare dett_pers solo su n_spe senza chiave composita legacy (unico/n_stra/tipo_gestione). Esempi: ' . $sample,
'warning',
'Usa correlazione completa per evitare mismatch tra gestioni/anni.'
);
}
if (!empty($dettPersFiles)) {
$issues[] = $this->makeQualityIssue(
'procedure_legacy_dett_pers_presence',
'Procedure con addebiti individuali legacy',
'Rilevati ' . count(array_unique($dettPersFiles)) . ' file che referenziano dett_pers.',
'info',
'Valuta un service condiviso per centralizzare regole di correlazione e validazione.'
);
}
if (!empty($filamentStabileSensitiveNoContext)) {
$sample = implode(', ', array_slice(array_values(array_unique($filamentStabileSensitiveNoContext)), 0, 5));
$issues[] = $this->makeQualityIssue(
'procedure_filament_stabile_context_missing',
'Pagine Filament sensibili senza contesto stabile esplicito',
'Trovate pagine Filament che toccano archivi anagrafici/millesimi/incassi ma senza uso esplicito di StabileContext. Esempi: ' . $sample,
'warning',
'Uniforma le pagine usando StabileContext per garantire una sola anagrafica stabile coerente.'
);
}
return $issues;
}
private function checkMillesimiConsistency(): array
{
$issues = [];
if (!Schema::hasTable('tabelle_millesimali')) {
$issues[] = $this->makeQualityIssue(
'archive_missing_tabelle_millesimali',
'Tabella tabelle_millesimali mancante',
'La tabella principale delle tabelle millesimali non esiste nel DB attivo.',
'critical',
'Crea/verifica migrazioni tabelle millesimali e riesegui la scansione.'
);
return $issues;
}
$sources = [];
if (Schema::hasTable('dettaglio_millesimi') && Schema::hasColumn('dettaglio_millesimi', 'tabella_millesimale_id')) {
$sources['dettaglio_millesimi'] = [
'table' => 'dettaglio_millesimi',
'fk' => 'tabella_millesimale_id',
];
}
if (Schema::hasTable('dettagli_tabelle_millesimali') && Schema::hasColumn('dettagli_tabelle_millesimali', 'tabella_millesimale_id')) {
$sources['dettagli_tabelle_millesimali'] = [
'table' => 'dettagli_tabelle_millesimali',
'fk' => 'tabella_millesimale_id',
];
}
if (Schema::hasTable('dettagli_millesimali') && Schema::hasColumn('dettagli_millesimali', 'tabella_id')) {
$sources['dettagli_millesimali'] = [
'table' => 'dettagli_millesimali',
'fk' => 'tabella_id',
];
}
if (empty($sources)) {
$issues[] = $this->makeQualityIssue(
'archive_millesimi_no_detail_sources',
'Sorgenti dettaglio millesimi non trovate',
'Nessuna tabella dettaglio millesimi disponibile (dettaglio_millesimi / dettagli_tabelle_millesimali / dettagli_millesimali).',
'critical',
'Uniforma schema e mapping modelli verso una sorgente dettaglio consistente.'
);
return $issues;
}
$totalsBySource = [];
foreach ($sources as $sourceKey => $cfg) {
$totalsBySource[$sourceKey] = DB::table($cfg['table'])
->selectRaw($cfg['fk'] . ' as tabella_id, COUNT(*) as righe, COALESCE(SUM(COALESCE(millesimi,0)),0) as totale')
->groupBy($cfg['fk'])
->get()
->keyBy('tabella_id')
->all();
$orphanCount = DB::table($cfg['table'] . ' as d')
->leftJoin('tabelle_millesimali as t', 't.id', '=', 'd.' . $cfg['fk'])
->whereNull('t.id')
->count();
if ($orphanCount > 0) {
$issues[] = $this->makeQualityIssue(
'archive_orphan_' . $sourceKey,
'Dettagli millesimali orfani in ' . $cfg['table'],
'Rilevati ' . $orphanCount . ' record senza tabella padre in ' . $cfg['table'] . '.',
'error',
'Bonifica record orfani o ripristina tabelle padre mancanti.'
);
}
}
$tables = DB::table('tabelle_millesimali')
->select(['id', 'totale_millesimi', 'codice_tabella', 'nome_tabella'])
->get();
foreach ($tables as $tab) {
$activeSources = [];
foreach ($totalsBySource as $sourceKey => $rowsByTabella) {
$row = $rowsByTabella[$tab->id] ?? null;
if ($row && ((int) ($row->righe ?? 0)) > 0) {
$activeSources[$sourceKey] = [
'righe' => (int) ($row->righe ?? 0),
'totale' => (float) ($row->totale ?? 0),
];
}
}
$label = trim((string) (($tab->codice_tabella ?? '') . ' ' . ($tab->nome_tabella ?? '')));
$label = $label !== '' ? $label : ('id=' . (string) $tab->id);
if (count($activeSources) > 1) {
$sourceText = collect($activeSources)->map(fn ($v, $k) => $k . ': ' . number_format((float) $v['totale'], 3, '.', ''))
->implode(' | ');
$issues[] = $this->makeQualityIssue(
'archive_millesimi_multi_source',
'Tabella millesimale presente in più archivi dettaglio',
'La tabella ' . $label . ' (id ' . $tab->id . ') ha dettagli in più sorgenti: ' . $sourceText . '.',
'warning',
'Definisci una sorgente canonica per evitare mismatch tra procedure.'
);
}
if (empty($activeSources)) {
continue;
}
$priority = ['dettaglio_millesimi', 'dettagli_tabelle_millesimali', 'dettagli_millesimali'];
$primaryKey = null;
foreach ($priority as $pk) {
if (isset($activeSources[$pk])) {
$primaryKey = $pk;
break;
}
}
if ($primaryKey === null) {
$primaryKey = array_key_first($activeSources);
}
$primaryTotal = (float) ($activeSources[$primaryKey]['totale'] ?? 0);
$tabTotal = (float) ($tab->totale_millesimi ?? 0);
if (abs($primaryTotal - $tabTotal) > 0.05) {
$issues[] = $this->makeQualityIssue(
'archive_millesimi_total_mismatch',
'Totale tabella diverso dal dettaglio',
'Tabella ' . $label . ' (id ' . $tab->id . '): totale tabella ' . number_format($tabTotal, 3, '.', '') . ' vs dettaglio (' . $primaryKey . ') ' . number_format($primaryTotal, 3, '.', '') . '.',
'error',
'Allinea il totale con il prospetto dettaglio o rigenera i dettagli.'
);
}
if (abs($primaryTotal - 1000.0) > 0.1) {
$issues[] = $this->makeQualityIssue(
'archive_millesimi_not_1000',
'Tabella millesimale non bilanciata a 1000',
'Tabella ' . $label . ' (id ' . $tab->id . ') totale dettaglio ' . number_format($primaryTotal, 3, '.', '') . ' (atteso 1000).',
'warning',
'Ribilancia la tabella o verifica eccezioni business previste.'
);
}
}
return $issues;
}
private function checkLegacyIndividualChargesConsistency(): array
{
$issues = [];
if (!Schema::connection('gescon_import')->hasTable('dett_pers')) {
$issues[] = $this->makeQualityIssue(
'legacy_missing_dett_pers',
'Tabella legacy dett_pers mancante',
'Nella connessione gescon_import non esiste la tabella dett_pers.',
'critical',
'Verifica connessione gescon_import o import dello schema legacy.'
);
return $issues;
}
$requiredColumns = ['unico', 'n_stra', 'tipo_gestione', 'n_spe', 'id_cond', 'cond_inq', 'importo_euro'];
$missingColumns = [];
foreach ($requiredColumns as $column) {
if (!Schema::connection('gescon_import')->hasColumn('dett_pers', $column)) {
$missingColumns[] = $column;
}
}
if (!empty($missingColumns)) {
$issues[] = $this->makeQualityIssue(
'legacy_dett_pers_missing_columns',
'Colonne chiave mancanti in dett_pers',
'Mancano colonne in dett_pers: ' . implode(', ', $missingColumns) . '.',
'critical',
'Aggiorna lo schema legacy o adatta le procedure di mapping.'
);
return $issues;
}
$ambiguousGroups = DB::connection('gescon_import')
->table('dett_pers')
->selectRaw('unico, tipo_gestione, n_stra, COUNT(DISTINCT n_spe) as nspe_count, COUNT(*) as righe')
->groupBy(['unico', 'tipo_gestione', 'n_stra'])
->havingRaw('COUNT(DISTINCT n_spe) > 1')
->limit(5)
->get();
if ($ambiguousGroups->isNotEmpty()) {
$examples = $ambiguousGroups->map(fn ($r) => 'unico ' . $r->unico . ' / ' . $r->tipo_gestione . ' / n_stra ' . $r->n_stra . ' => n_spe ' . $r->nspe_count)->implode(' ; ');
$issues[] = $this->makeQualityIssue(
'legacy_dett_pers_ambiguous_mapping',
'Mapping dett_pers ambiguo per spese individuali',
'Trovati gruppi con più n_spe per stessa chiave unico/tipo_gestione/n_stra. Esempi: ' . $examples,
'warning',
'Usa la chiave completa e gestisci eventuali versioni duplicate in import.'
);
}
if (Schema::connection('gescon_import')->hasTable('operazioni')) {
$missingByNSpe = DB::connection('gescon_import')
->table('operazioni as o')
->leftJoin('dett_pers as dp', 'dp.n_spe', '=', 'o.n_spe')
->where('o.cod_spe', 'I05')
->whereNull('dp.n_spe')
->count();
if ($missingByNSpe > 0) {
$issues[] = $this->makeQualityIssue(
'legacy_i05_missing_by_nspe',
'Spese I05 senza dettaglio dett_pers su n_spe',
'Rilevate ' . $missingByNSpe . ' operazioni I05 senza righe dett_pers legate per n_spe.',
'warning',
'Usa anche mapping unico/tipo_gestione/n_stra nelle procedure di riparto.'
);
}
}
$emptyAmounts = DB::connection('gescon_import')
->table('dett_pers')
->where(function ($q) {
$q->whereNull('importo_euro')->orWhere('importo_euro', 0);
})
->count();
if ($emptyAmounts > 0) {
$issues[] = $this->makeQualityIssue(
'legacy_dett_pers_empty_amounts',
'Righe dett_pers con importo nullo/zero',
'Rilevate ' . $emptyAmounts . ' righe dett_pers con importo_euro nullo o 0.',
'info',
'Valuta fallback su importo legacy alternativo o esclusione dai riparti.'
);
}
return $issues;
}
private function checkLegacyIncassiConsistency(): array
{
$issues = [];
if (!Schema::connection('gescon_import')->hasTable('incassi')) {
return [
$this->makeQualityIssue(
'legacy_missing_incassi',
'Tabella legacy incassi mancante',
'Nella connessione gescon_import non esiste la tabella incassi.',
'critical',
'Verifica connessione/schema legacy per gli incassi.'
),
];
}
$required = ['dt_empag', 'importo_euro', 'importo_pagato_euro', 'cod_cond'];
$missing = [];
foreach ($required as $column) {
if (!Schema::connection('gescon_import')->hasColumn('incassi', $column)) {
$missing[] = $column;
}
}
if (!empty($missing)) {
$issues[] = $this->makeQualityIssue(
'legacy_incassi_missing_columns',
'Colonne chiave mancanti in incassi legacy',
'Mancano colonne in incassi: ' . implode(', ', $missing) . '.',
'critical',
'Aggiorna lo schema legacy o adatta le procedure di mapping incassi.'
);
return $issues;
}
$scaledRows = DB::connection('gescon_import')
->table('incassi')
->whereRaw('ABS(COALESCE(importo_euro, 0)) > 0')
->whereRaw('ABS(COALESCE(importo_pagato_euro, 0)) > (ABS(COALESCE(importo_euro, 0)) * 1000)')
->count();
if ($scaledRows > 0) {
$issues[] = $this->makeQualityIssue(
'legacy_incassi_scaled_amounts',
'Incassi legacy con importo_pagato_euro in scala differente',
'Rilevati ' . $scaledRows . ' record incassi con importo_pagato_euro in scala maggiore rispetto a importo_euro (tipicamente x10000).',
'warning',
'In UI e importazioni usa importo_euro/importo come fonte principale e normalizza importo_pagato_euro come fallback.'
);
}
$signMismatch = DB::connection('gescon_import')
->table('incassi')
->whereRaw('ABS(COALESCE(importo_euro, 0)) > 0')
->whereRaw('ABS(COALESCE(importo_pagato_euro, 0)) > 0')
->whereRaw('SIGN(COALESCE(importo_euro, 0)) <> SIGN(COALESCE(importo_pagato_euro, 0))')
->count();
if ($signMismatch > 0) {
$issues[] = $this->makeQualityIssue(
'legacy_incassi_sign_mismatch',
'Incassi legacy con segno incoerente tra campi importo',
'Rilevati ' . $signMismatch . ' record con segno differente tra importo_euro e importo_pagato_euro.',
'error',
'Verifica trasformazioni di segno e assicurati di visualizzare un solo campo normalizzato.'
);
}
$missingDate = DB::connection('gescon_import')
->table('incassi')
->whereNull('dt_empag')
->count();
if ($missingDate > 0) {
$issues[] = $this->makeQualityIssue(
'legacy_incassi_missing_dates',
'Incassi legacy senza data pagamento',
'Rilevati ' . $missingDate . ' record senza dt_empag.',
'warning',
'Usa data_incasso come fallback o completa i dati in import.'
);
}
return $issues;
}
private function makeQualityIssue(string $rule, string $name, string $description, string $severity, string $suggestion, int $line = 1, string $match = '—'): array
{
return [
'rule' => $rule,
'name' => $name,
'description' => $description,
'severity' => $severity,
'line' => $line,
'match' => $match,
'suggestion' => $suggestion,
'context' => [],
];
}
/**
* Ottiene lista file da scansionare
*/
private function getFilesToScan(string $path): array
{
$files = [];
if (File::isFile($path)) {
return [$path];
}
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if ($file->isFile()) {
$extension = $file->getExtension();
if (in_array($extension, ['php', 'blade.php', 'js', 'vue', 'ts'])) {
// Escludi alcune cartelle rumorose
$pathname = $file->getPathname();
if (str_contains($pathname, DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR)) continue;
if (str_contains($pathname, DIRECTORY_SEPARATOR . 'node_modules' . DIRECTORY_SEPARATOR)) continue;
if (str_contains($pathname, DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR)) continue;
if (str_contains($pathname, DIRECTORY_SEPARATOR . 'SAFE_BACKUPS' . DIRECTORY_SEPARATOR)) continue;
if (str_contains($pathname, DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR . 'phpmyadmin' . DIRECTORY_SEPARATOR)) continue;
// Salta file troppo grandi (> 1 MB) per evitare consumo eccessivo di memoria/tempo
try {
if (@filesize($pathname) !== false && @filesize($pathname) > 1024 * 1024) continue;
} catch (\Throwable $e) {
}
$files[] = $file->getPathname();
}
}
}
return $files;
}
/**
* Analizza singolo file
*/
private function analyzeFile(string $filePath, string $content, string $relativePath): array
{
$issues = [];
$extension = pathinfo($filePath, PATHINFO_EXTENSION);
$isBladeFile = Str::endsWith($filePath, '.blade.php');
foreach ($this->qualityRules as $ruleKey => $rule) {
// Applica regole specifiche per tipo file e percorso
if (!$this->shouldApplyRule($ruleKey, $rule, $extension, $isBladeFile, $relativePath)) {
continue;
}
// Gestione regole diagnostiche speciali
if ($ruleKey === 'blade_sections_balance') {
// Conta tutte le @section
$sections = preg_match_all('/@section\b/m', $content);
// Sezioni inline del tipo: @section('title', '...') che NON richiedono @endsection
$inlineSections = preg_match_all('/@section\s*\(\s*[^),]+,\s*[^)]+\)/m', $content);
$requiredEnd = max(0, $sections - $inlineSections);
$endsections = preg_match_all('/@endsection\b/m', $content);
if ($requiredEnd !== $endsections) {
$issues[] = [
'rule' => $ruleKey,
'name' => $rule['name'],
'description' => $rule['description'] . " (section: $sections (inline: $inlineSections), endsection richiesti: $requiredEnd, trovati: $endsections)",
'severity' => $rule['severity'],
'line' => 1,
'match' => '—',
'suggestion' => $rule['suggestion'] ?? '',
'context' => $this->getLineContext($content, 1)
];
}
continue;
}
// Gestione regola per file troppo lunghi
if ($ruleKey === 'monster_class') {
$lineCount = substr_count($content, "\n") + 1;
if ($lineCount > ($rule['lineThreshold'] ?? 500)) {
$issues[] = [
'rule' => $ruleKey,
'name' => $rule['name'],
'description' => $rule['description'] . " (Trovate: $lineCount righe)",
'severity' => $rule['severity'],
'line' => 1,
'match' => "File con $lineCount righe",
'suggestion' => $rule['suggestion'] ?? '',
'context' => $this->getLineContext($content, 1)
];
}
continue;
}
// Integrità toggle della sidebar: verifica presenza di #<id>-menu e #<id>-chevron
if ($ruleKey === 'sidebar_toggle_integrity') {
// Applica solo al file sidebar-netgescon
if (!Str::contains($relativePath, 'resources/views/components/menu/sidebar-netgescon.blade.php')) {
continue;
}
// Trova tutte le categorie usate nel toggleCategory('id', ...)
preg_match_all($rule['pattern'], $content, $m, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
if (!empty($m)) {
foreach ($m as $match) {
$cat = is_array($match[1]) ? $match[1][0] : $match[1];
$hasMenu = Str::contains($content, 'id="' . $cat . '-menu"');
$hasChevron = Str::contains($content, 'id="' . $cat . '-chevron"');
if (!$hasMenu || !$hasChevron) {
$offset = is_array($match[0]) ? ($match[0][1] ?? 0) : 0;
$lineNumber = substr_count(substr($content, 0, $offset), "\n") + 1;
$issues[] = [
'rule' => $ruleKey,
'name' => $rule['name'],
'description' => $rule['description'] . " — categoria: $cat (menu: " . ($hasMenu ? 'OK' : 'MANCANTE') . ", chevron: " . ($hasChevron ? 'OK' : 'MANCANTE') . ")",
'severity' => $rule['severity'],
'line' => $lineNumber,
'match' => is_array($match[0]) ? ($match[0][0] ?? "toggleCategory('$cat')") : (string)$match[0],
'suggestion' => $rule['suggestion'] ?? '',
'context' => $this->getLineContext($content, max(1, $lineNumber))
];
}
}
}
continue;
}
if (isset($rule['balanceTag'])) {
$tag = $rule['balanceTag'];
$opens = preg_match_all('/<' . $tag . '\\b/im', $content);
$closes = preg_match_all('/<\\/' . $tag . '>/im', $content);
if ($opens !== $closes) {
$issues[] = [
'rule' => $ruleKey,
'name' => $rule['name'],
'description' => $rule['description'] . " (aperti: $opens, chiusi: $closes)",
'severity' => $rule['severity'],
'line' => 1,
'match' => '—',
'suggestion' => $rule['suggestion'] ?? '',
'context' => $this->getLineContext($content, 1)
];
}
continue;
}
// Regole di co-occorrenza: uso di icone/classi senza layout o link CSS/JS
if (in_array($ruleKey, ['mdi_usage_requires_layout_or_link', 'fa_usage_requires_layout_or_link', 'netgescon_btn_usage_requires_css'], true)) {
preg_match_all($rule['pattern'], $content, $matches, PREG_OFFSET_CAPTURE);
if (!empty($matches[0])) {
$extendsNetgescon = (bool)preg_match("/@extends\(['\"]admin\\.layouts\\.netgescon['\"]\)/m", $content);
$hasMdiLink = (bool)preg_match('/materialdesignicons\\.min\\.css/m', $content);
$hasFaLink = (bool)preg_match('/cdnjs\\.cloudflare\\.com\\/ajax\\/libs\\/font-awesome/m', $content);
$hasNgCss = (bool)preg_match("/asset\('css\\/netgescon-admin\\.css'\)/m", $content);
foreach ($matches[0] as $match) {
$lineNumber = substr_count(substr($content, 0, $match[1]), "\n") + 1;
$missing = [];
if ($ruleKey === 'mdi_usage_requires_layout_or_link') {
if (!$extendsNetgescon && !$hasMdiLink) {
$missing[] = 'Layout NetGescon o CSS MDI';
}
} elseif ($ruleKey === 'fa_usage_requires_layout_or_link') {
if (!$extendsNetgescon && !$hasFaLink) {
$missing[] = 'Layout NetGescon o CSS Font Awesome';
}
} else { // netgescon_btn_usage_requires_css
if (!$extendsNetgescon && !$hasNgCss) {
$missing[] = 'Layout NetGescon o CSS NetGescon';
}
}
if (!empty($missing)) {
$issues[] = [
'rule' => $ruleKey,
'name' => $rule['name'],
'description' => $rule['description'] . ' — mancante: ' . implode(', ', $missing),
'severity' => $rule['severity'],
'line' => $lineNumber,
'match' => trim($match[0]),
'suggestion' => $rule['suggestion'] ?? '',
'context' => $this->getLineContext($content, $lineNumber)
];
}
}
}
continue;
}
preg_match_all($rule['pattern'], $content, $matches, PREG_OFFSET_CAPTURE);
$mode = $rule['mode'] ?? 'detect';
// Aggregazione per regole informative rumorose: una sola issue per file con conteggi
if (
$mode === 'detect' && ($rule['severity'] ?? 'info') === 'info'
&& in_array($ruleKey, ['netgescon_components', 'gescon_import_columns_detect'], true)
) {
$count = isset($matches[0]) ? count($matches[0]) : 0;
if ($count > 0) {
// Prepara un riassunto specifico per regola
$summaryMatch = '';
$lineNumber = 1;
if ($ruleKey === 'netgescon_components') {
$tokens = [
'netgescon-btn',
'netgescon-card',
'netgescon-title',
'netgescon-text',
'netgescon-input'
];
$parts = [];
foreach ($tokens as $t) {
$c = preg_match_all('/' . preg_quote($t, '/') . '/m', $content);
if ($c > 0) $parts[] = $t . ':' . $c;
}
$summaryMatch = 'componenti: ' . ($parts ? implode(', ', $parts) : ('totali:' . $count));
} else {
// gescon_import_columns_detect
$tokens = [
'grid-cols-' => 'grid-cols',
'md:grid-cols-' => 'md:grid-cols',
'col-span-' => 'col-span',
'\\bflex\\b' => 'flex'
];
$parts = [];
foreach ($tokens as $pattern => $label) {
$c = preg_match_all('/' . $pattern . '/m', $content);
if ($c > 0) $parts[] = $label . ':' . $c;
}
$summaryMatch = 'layout: ' . ($parts ? implode(', ', $parts) : ('totali:' . $count));
}
$issues[] = [
'rule' => $ruleKey,
'name' => $rule['name'],
'description' => $rule['description'],
'severity' => $rule['severity'],
'line' => $lineNumber,
'match' => $summaryMatch,
'suggestion' => $rule['suggestion'] ?? '',
'context' => $this->getLineContext($content, $lineNumber)
];
}
continue;
}
if ($mode === 'require') {
// Se non viene trovato il pattern, segna un problema
if (empty($matches[0])) {
$issues[] = [
'rule' => $ruleKey,
'name' => $rule['name'],
'description' => $rule['description'],
'severity' => $rule['severity'],
'line' => 1,
'match' => '—',
'suggestion' => $rule['suggestion'] ?? '',
'context' => $this->getLineContext($content, 1)
];
}
} else {
// Per 'prohibit' e 'detect' creiamo una issue per ogni match
if (!empty($matches[0])) {
foreach ($matches[0] as $match) {
$lineNumber = substr_count(substr($content, 0, $match[1]), "\n") + 1;
$issues[] = [
'rule' => $ruleKey,
'name' => $rule['name'],
'description' => $rule['description'],
'severity' => $rule['severity'],
'line' => $lineNumber,
'match' => trim($match[0]),
'suggestion' => $rule['suggestion'] ?? '',
'context' => $this->getLineContext($content, $lineNumber)
];
}
}
}
}
return $issues;
}
/**
* Verifica se applicare la regola al file
*/
private function shouldApplyRule(string $ruleKey, array $rule, string $extension, bool $isBladeFile, string $relativePath): bool
{
// Filtri per tipo file
$fileTypes = $rule['fileTypes'] ?? ['php', 'blade'];
$isPhp = $extension === 'php' && !$isBladeFile;
$fileTypeOk = (
($isBladeFile && in_array('blade', $fileTypes)) ||
($isPhp && in_array('php', $fileTypes))
);
if (!$fileTypeOk) {
return false;
}
// Filtri per percorso
if (!empty($rule['pathContains']) && is_array($rule['pathContains'])) {
$ok = false;
foreach ($rule['pathContains'] as $needle) {
if (Str::contains($relativePath, $needle)) {
$ok = true;
break;
}
}
if (!$ok) return false;
}
// Esclusioni per percorso
if (!empty($rule['excludeContains']) && is_array($rule['excludeContains'])) {
foreach ($rule['excludeContains'] as $needle) {
if (Str::contains($relativePath, $needle)) {
return false;
}
}
}
return true;
}
/**
* Ottiene contesto della riga
*/
private function getLineContext(string $content, int $lineNumber): array
{
$lines = explode("\n", $content);
$start = max(0, $lineNumber - 3);
$end = min(count($lines) - 1, $lineNumber + 2);
$context = [];
for ($i = $start; $i <= $end; $i++) {
$context[] = [
'number' => $i + 1,
'content' => $lines[$i] ?? '',
'highlight' => ($i + 1) === $lineNumber
];
}
return $context;
}
/**
* Filtra per severity
*/
private function filterBySeverity(array $results, string $severity): array
{
foreach ($results as &$fileResult) {
$fileResult['issues'] = array_filter($fileResult['issues'], function ($issue) use ($severity) {
return $issue['severity'] === $severity;
});
}
return array_filter($results, function ($fileResult) {
return !empty($fileResult['issues']);
});
}
/**
* Genera summary dei risultati
*/
private function generateSummary(array $results, ?int $totalScannedFiles = null): array
{
$summary = [
// Numero di file scansionati (se fornito), altrimenti file con problemi
'total_files' => $totalScannedFiles ?? count($results),
'total_scanned_files' => $totalScannedFiles ?? count($results),
'total_issues' => 0,
'by_severity' => [
'critical' => 0,
'error' => 0,
'warning' => 0,
'info' => 0
],
'by_rule' => [],
'by_module' => [],
'by_menu' => []
];
foreach ($results as $fileResult) {
foreach ($fileResult['issues'] as $issue) {
$summary['total_issues']++;
$summary['by_severity'][$issue['severity']]++;
$ruleKey = $issue['rule'];
if (!isset($summary['by_rule'][$ruleKey])) {
$summary['by_rule'][$ruleKey] = [
'name' => $issue['name'],
'count' => 0
];
}
$summary['by_rule'][$ruleKey]['count']++;
$module = $fileResult['module'] ?? 'unknown';
$menu = $fileResult['menu'] ?? 'unknown';
if (!isset($summary['by_module'][$module])) $summary['by_module'][$module] = 0;
if (!isset($summary['by_menu'][$menu])) $summary['by_menu'][$menu] = 0;
$summary['by_module'][$module]++;
$summary['by_menu'][$menu]++;
}
}
return $summary;
}
/**
* Applica fix automatici
*/
public function autofix(Request $request)
{
$filePath = $request->get('file');
$ruleKey = $request->get('rule');
$preview = filter_var($request->get('preview', false), FILTER_VALIDATE_BOOLEAN);
if (!$filePath || !$ruleKey) {
return \Illuminate\Support\Facades\Response::json(['success' => false, 'message' => 'Parametri mancanti']);
}
$fullPath = \Illuminate\Support\Facades\App::basePath($filePath);
if (!File::exists($fullPath)) {
return \Illuminate\Support\Facades\Response::json(['success' => false, 'message' => 'File non trovato']);
}
$content = File::get($fullPath);
$fixedContent = $this->applyAutoFix($content, $ruleKey);
if ($fixedContent !== $content) {
// Se è richiesto solo l'anteprima o il file non è scrivibile, restituisci il contenuto proposto
if ($preview || !is_writable($fullPath)) {
return \Illuminate\Support\Facades\Response::json([
'success' => false,
'message' => !is_writable($fullPath)
? 'File non scrivibile: esegui la modifica manualmente o aggiorna i permessi'
: 'Anteprima fix (nessuna scrittura eseguita)',
'canWrite' => is_writable($fullPath),
'proposed' => $fixedContent,
'permissionHints' => !is_writable($fullPath) ? [
'Rendi scrivibile il file per l\'utente del web server (www-data) o per il tuo utente.',
'Valuta di cambiare proprietario/gruppo del progetto o applicare ACL (setfacl) per la modifica.',
'In alternativa, copia il contenuto proposto manualmente nel file e salva.'
] : [],
]);
}
// Scrittura consentita
File::put($fullPath, $fixedContent);
return \Illuminate\Support\Facades\Response::json(['success' => true, 'message' => 'Fix applicato con successo']);
}
return \Illuminate\Support\Facades\Response::json(['success' => false, 'message' => 'Nessun fix automatico disponibile']);
}
/**
* Applica fix automatico specifico
*/
private function applyAutoFix(string $content, string $ruleKey): string
{
switch ($ruleKey) {
case 'layout_required':
// Prependi l'extends NetGescon se manca e se sembra una view completa (non partial)
if (!preg_match("/@extends\(['\"]admin\\.layouts\\.netgescon['\"]\)/m", $content)) {
// Inserisci prima della prima direttiva @section o all'inizio del file
$lines = preg_split('/\r?\n/', $content);
$insertion = "@extends('admin.layouts.netgescon')\n";
$inserted = false;
foreach ($lines as $idx => $line) {
if (preg_match('/^\s*@section\b/', $line)) {
array_splice($lines, $idx, 0, [$insertion]);
$inserted = true;
break;
}
}
if (!$inserted) array_unshift($lines, rtrim($insertion));
$content = implode("\n", $lines);
}
break;
case 'layout_forbidden_app':
$content = preg_replace(
"/@extends\(['\"]layouts\.app['\"]\)/",
"@extends('admin.layouts.netgescon')",
$content
);
break;
case 'bootstrap_classes':
// Bottoni
$content = str_replace(
['btn-primary', 'btn-secondary', 'btn-success', 'btn-danger', 'btn-warning'],
['netgescon-btn netgescon-btn-primary', 'netgescon-btn netgescon-btn-secondary', 'netgescon-btn netgescon-btn-success', 'netgescon-btn netgescon-btn-danger', 'netgescon-btn netgescon-btn-warning'],
$content
);
$content = str_replace(
['btn-outline-primary', 'btn-outline-secondary', 'btn-outline-success', 'btn-outline-danger', 'btn-outline-warning'],
['netgescon-btn netgescon-btn-outline-primary', 'netgescon-btn netgescon-btn-outline-secondary', 'netgescon-btn netgescon-btn-outline-success', 'netgescon-btn netgescon-btn-outline-danger', 'netgescon-btn netgescon-btn-outline-warning'],
$content
);
// Layout di base
$content = str_replace(['container-fluid'], ['px-6'], $content);
$content = preg_replace('/\brow\b/', 'grid md:grid-cols-12 gap-4', $content);
// Colonne Bootstrap comuni in griglia 12 -> col-span
$content = preg_replace('/\bcol-12\b/', 'col-span-12', $content);
$content = preg_replace('/\bcol-6\b/', 'col-span-12 md:col-span-6', $content);
$content = preg_replace('/\bcol-4\b/', 'col-span-12 md:col-span-4', $content);
$content = preg_replace('/\bcol-3\b/', 'col-span-12 md:col-span-3', $content);
// Tabelle e form
$content = preg_replace('/\btable\b/', 'table', $content);
$content = preg_replace('/\bform-control\b/', 'netgescon-input', $content);
// Utility display/flex
$content = preg_replace('/\bd-flex\b/', 'flex', $content);
$content = preg_replace('/\binline-d-flex\b/', 'inline-flex', $content);
// Justify/align mapping
$content = preg_replace('/\bjustify-content-between\b/', 'justify-between', $content);
$content = preg_replace('/\bjustify-content-end\b/', 'justify-end', $content);
$content = preg_replace('/\bjustify-content-start\b/', 'justify-start', $content);
$content = preg_replace('/\balign-items-center\b/', 'items-center', $content);
$content = preg_replace('/\balign-items-start\b/', 'items-start', $content);
$content = preg_replace('/\balign-items-end\b/', 'items-end', $content);
// Tipografia e colori comuni
$content = preg_replace('/\bfw-bold\b/', 'font-bold', $content);
$content = preg_replace('/\btext-muted\b/', 'text-gray-500', $content);
$content = preg_replace('/\btext-dark\b/', 'text-gray-900', $content);
$content = preg_replace('/\bbg-light\b/', 'bg-gray-100', $content);
// Badge Bootstrap -> NetGescon
$content = preg_replace('/\bbadge\s+bg-primary\b/', 'netgescon-badge netgescon-badge-primary', $content);
$content = preg_replace('/\bbadge\s+bg-secondary\b/', 'netgescon-badge netgescon-badge-secondary', $content);
$content = preg_replace('/\bbadge\s+bg-success\b/', 'netgescon-badge netgescon-badge-success', $content);
$content = preg_replace('/\bbadge\s+bg-danger\b/', 'netgescon-badge netgescon-badge-danger', $content);
$content = preg_replace('/\bbadge\s+bg-warning\b/', 'netgescon-badge netgescon-badge-warning', $content);
$content = preg_replace('/\bbadge\s+bg-info\b/', 'netgescon-badge netgescon-badge-info', $content);
break;
case 'deprecated_helpers':
$replacements = [
'array_get(' => 'data_get(',
'array_set(' => 'data_set(',
'str_is(' => 'Str::is(',
'starts_with(' => 'Str::startsWith(',
'ends_with(' => 'Str::endsWith(',
];
foreach ($replacements as $old => $new) {
$content = str_replace($old, $new, $content);
}
break;
}
return $content;
}
/**
* Conta i file scansionabili nei percorsi indicati
*/
private function countFilesInPaths(string $scanPaths, string $moduleFilter = 'all', string $menuFilter = 'all'): int
{
$count = 0;
$paths = explode(',', $scanPaths);
foreach ($paths as $path) {
$path = trim($path);
$fullPath = \Illuminate\Support\Facades\App::basePath($path);
if (!File::exists($fullPath)) continue;
$files = $this->getFilesToScan($fullPath);
if ($moduleFilter !== 'all' || $menuFilter !== 'all') {
$files = array_filter($files, function ($f) use ($moduleFilter, $menuFilter) {
$rel = str_replace(\Illuminate\Support\Facades\App::basePath('') . '/', '', $f);
return $this->pathMatchesFilters($rel, $moduleFilter, $menuFilter);
});
}
$count += count($files);
}
return $count;
}
private function pathMatchesFilters(string $relativePath, string $moduleFilter, string $menuFilter): bool
{
$moduleOk = true;
$menuOk = true;
if ($moduleFilter !== 'all' && isset($this->modulePaths[$moduleFilter])) {
$moduleOk = false;
foreach ($this->modulePaths[$moduleFilter] as $p) {
if (Str::contains($relativePath, $p)) {
$moduleOk = true;
break;
}
}
}
if ($menuFilter !== 'all' && isset($this->menuPaths[$menuFilter])) {
$menuOk = false;
foreach ($this->menuPaths[$menuFilter] as $p) {
if (Str::contains($relativePath, $p)) {
$menuOk = true;
break;
}
}
}
return $moduleOk && $menuOk;
}
private function classifyModule(string $relativePath): string
{
foreach ($this->modulePaths as $key => $paths) {
foreach ($paths as $p) {
if (Str::contains($relativePath, $p)) return $key;
}
}
return 'core';
}
private function classifyMenu(string $relativePath): string
{
foreach ($this->menuPaths as $key => $paths) {
foreach ($paths as $p) {
if (Str::contains($relativePath, $p)) return $key;
}
}
return 'generic';
}
}