From 3471befb1af62975088d115a46aa180e13652a0d Mon Sep 17 00:00:00 2001 From: Inext68 Date: Wed, 27 May 2026 10:45:05 +0200 Subject: [PATCH] sistemazione reports - cambio password - profilo utente --- .phpunit.result.cache | 2 +- MEMORY.md | 29 + app/Helpers/ReportColumnRegistry.php | 200 +++++++ app/Http/Controllers/Auth/AuthController.php | 115 ++-- .../Controllers/Auth/ProfileController.php | 32 ++ app/Http/Controllers/ReportController.php | 510 +++++++++++++++--- app/Models/User.php | 70 ++- .../auth/emails/reset-password.blade.php | 33 ++ .../views/auth/forgot-password.blade.php | 60 +++ resources/views/auth/login.blade.php | 2 +- resources/views/auth/profile.blade.php | 76 +++ resources/views/auth/register.blade.php | 80 --- resources/views/auth/reset-password.blade.php | 63 +++ resources/views/layouts/adminlte.blade.php | 2 +- resources/views/report/index.blade.php | 435 ++++++++++----- resources/views/report/result.blade.php | 8 +- routes/web.php | 11 +- .../00edc551e83193362ca154488ca4729a.php | 8 +- .../28c9abf6c3545b82f2c66c4bde35bcb3.php | 435 ++++++++++----- .../486ad3b47013e2f855528231fc36c018.php | 104 ++++ .../7cfff45be1176bacefa7e2488c0914ed.php | 35 ++ .../7fe4bfde84ae8ad75ccbe8b21689cbfa.php | 85 +++ .../ad3b26036a197d93027e9b56fcb36d0a.php | 75 +++ .../e8f60d5f5b2a4c688f1bc12477b209df.php | 2 +- .../f45eecce65c94b1fc574994507f6259e.php | 122 ----- .../f5ef96019b613b67dddc87baed21eb0c.php | 2 +- 26 files changed, 1964 insertions(+), 632 deletions(-) create mode 100644 app/Helpers/ReportColumnRegistry.php create mode 100644 app/Http/Controllers/Auth/ProfileController.php create mode 100644 resources/views/auth/emails/reset-password.blade.php create mode 100644 resources/views/auth/forgot-password.blade.php create mode 100644 resources/views/auth/profile.blade.php delete mode 100644 resources/views/auth/register.blade.php create mode 100644 resources/views/auth/reset-password.blade.php create mode 100755 storage/framework/views/486ad3b47013e2f855528231fc36c018.php create mode 100755 storage/framework/views/7cfff45be1176bacefa7e2488c0914ed.php create mode 100755 storage/framework/views/7fe4bfde84ae8ad75ccbe8b21689cbfa.php create mode 100755 storage/framework/views/ad3b26036a197d93027e9b56fcb36d0a.php delete mode 100755 storage/framework/views/f45eecce65c94b1fc574994507f6259e.php diff --git a/.phpunit.result.cache b/.phpunit.result.cache index a9f59316..28aad123 100644 --- a/.phpunit.result.cache +++ b/.phpunit.result.cache @@ -1 +1 @@ -{"version":2,"defects":{"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":7,"Tests\\Feature\\CalendarEventsTest::test_generate_settimanale_events_produces_weekly_occurrences":7},"times":{"Tests\\Unit\\ExampleTest::test_that_true_is_true":0.005,"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":0.049,"Tests\\Feature\\CalendarEventsTest::test_find_first_sunday_of_june_2026":0.017,"Tests\\Feature\\CalendarEventsTest::test_find_second_saturday_of_june_2026":0.001,"Tests\\Feature\\CalendarEventsTest::test_find_fifth_sunday_returns_null_when_not_in_month":0.001,"Tests\\Feature\\CalendarEventsTest::test_generate_mensile_events_for_all_months":0.013,"Tests\\Feature\\CalendarEventsTest::test_generate_mensile_events_respects_selected_months":0.001,"Tests\\Feature\\CalendarEventsTest::test_generate_annuale_events_produces_correct_dates":0.001,"Tests\\Feature\\CalendarEventsTest::test_generate_settimanale_events_produces_weekly_occurrences":0.003}} \ No newline at end of file +{"version":2,"defects":{"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":7,"Tests\\Feature\\CalendarEventsTest::test_generate_settimanale_events_produces_weekly_occurrences":7},"times":{"Tests\\Unit\\ExampleTest::test_that_true_is_true":0.005,"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":0.062,"Tests\\Feature\\CalendarEventsTest::test_find_first_sunday_of_june_2026":0.037,"Tests\\Feature\\CalendarEventsTest::test_find_second_saturday_of_june_2026":0.001,"Tests\\Feature\\CalendarEventsTest::test_find_fifth_sunday_returns_null_when_not_in_month":0.001,"Tests\\Feature\\CalendarEventsTest::test_generate_mensile_events_for_all_months":0.023,"Tests\\Feature\\CalendarEventsTest::test_generate_mensile_events_respects_selected_months":0.002,"Tests\\Feature\\CalendarEventsTest::test_generate_annuale_events_produces_correct_dates":0.001,"Tests\\Feature\\CalendarEventsTest::test_generate_settimanale_events_produces_weekly_occurrences":0.006}} \ No newline at end of file diff --git a/MEMORY.md b/MEMORY.md index b833e27c..ecc19e88 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -720,3 +720,32 @@ php artisan route:list --name=email - `resources/views/impostazioni/index.blade.php`: pill Mittenti Aggiuntivi, tab-pane email-mittenti, modale sender modifica/crea, funzioni JS (resetSenderForm, editSender, deleteSender, testSenderSmtp) - `app/Http/Controllers/Admin/EmailSettingsController.php`: senderDestroy() con expectsJson() - `resources/views/layouts/adminlte.blade.php`: rimossa voce Impostazioni Email dal sidebar + +(Last updated: 27 Maggio 2026 - Password reset, profile, report enhancements) + +### 27 Maggio 2026 - Password Reset & Profile + +- **Rimosso registration**: Eliminata view `auth/register.blade.php`, rimossi metodi register da AuthController; rimosso link "Non hai un account? Registrati" dalla login +- **Aggiunto password reset**: + - Routes password.request, password.email, password.reset, password.update in routes/web.php + - Metodi showForgotForm(), sendResetLink(), showResetForm(), reset() in AuthController + - `User` model: implementato CanResetPassword con sendPasswordResetNotification() custom via Symfony Mailer su EmailSetting SMTP + - Views: auth/forgot-password.blade.php, auth/reset-password.blade.php, auth/emails/reset-password.blade.php +- **Login fix**: Checkbox "Ricordami" ora usa `$request->has('remember')` invece di hardcoded false +- **Login update**: Aggiunto link "Password dimenticata?" che punta a `route('password.request')` +- **ProfileController**: Creato con `show()` (view auth.profile) e `updatePassword()` con validazione `current_password` e `Password::min(8)` +- **Layout fix**: Link "Profilo" in adminlte.blade.php ora punta a `route('profile')` invece di `#` +- **Profile view**: Già esistente `auth/profile.blade.php` con form cambio password funzionante + +### 27 Maggio 2026 - Report Enhancements + +- **ReportColumnRegistry** (`app/Helpers/ReportColumnRegistry.php`): Registry centralizzato di colonne disponibili per entity type (individui, gruppi, eventi, documenti, contatti) con supporto per colonne dirette, accessor e relazioni (dot notation tipo `contatti.email`, `gruppi.nome`). Metodo `resolveRelationValue()` per risolvere valori su relazioni nidificate. +- **ReportController fix**: + - `storeCustom()`: colonne salvate correttamente come array in config['columns']; supporto per array da Select2 o comma-separated + - `runCustomReport()`: eager loading automatico per relazioni in colonne dot notation; risoluzione valori via `ReportColumnRegistry::resolveRelationValue()` + - `exportCSV()`: supporto per custom_id con filename da slug del nome report; BOM UTF-8 per Excel + - `resolveReport()`: metodo centralizzato per match report type + tipologia filter + - `run()`: supporto per custom_id per eseguire report personalizzati dalla stessa route +- **6 nuovi report predefiniti** (18 totali): documenti_completo, scadenze_documenti, individui_completo, gruppi_completo, eventi_completo, contatti_completo +- **Tipologia filter**: I report `documenti_completo` e `scadenze_documenti` hanno dropdown inline Select2 per filtro tipologia nella card dell'index +- **Report index view**: Select2 multiselect per colonne, Select2 tipologia filter, sort direction, nuovi bottoni CSV export per custom reports diff --git a/app/Helpers/ReportColumnRegistry.php b/app/Helpers/ReportColumnRegistry.php new file mode 100644 index 00000000..744be113 --- /dev/null +++ b/app/Helpers/ReportColumnRegistry.php @@ -0,0 +1,200 @@ +{$column} ?? ''; + } + + $current = $item; + + for ($i = 0; $i < count($parts); $i++) { + $part = $parts[$i]; + + if ($current === null || $current === '') { + return ''; + } + + if ($current instanceof \Illuminate\Support\Collection) { + $remaining = array_slice($parts, $i); + $filterKey = $remaining[0] ?? null; + + if ($filterKey && $current->isNotEmpty() && $current->first()->getAttribute('tipo') !== null) { + return $current->where('tipo', $filterKey) + ->pluck('valore') + ->filter() + ->implode(', '); + } + + $pluckKey = implode('.', $remaining); + return $current->pluck($pluckKey)->filter()->implode(', '); + } + + if ($current instanceof \Illuminate\Database\Eloquent\Model) { + try { + $relation = $current->{$part}; + if ($relation instanceof \Illuminate\Support\Collection) { + $current = $relation; + } elseif ($relation instanceof \Illuminate\Database\Eloquent\Model) { + $current = $relation; + } else { + $current = $relation; + } + } catch (\Exception) { + try { + $current = $current->getAttribute($part) ?? ''; + } catch (\Exception) { + return ''; + } + } + } elseif (is_array($current)) { + $current = $current[$part] ?? ''; + } elseif (is_object($current)) { + $current = $current->{$part} ?? ''; + } else { + return ''; + } + } + + if ($current instanceof \Carbon\Carbon) { + return $current->format('d/m/Y'); + } + + if ($current instanceof \Illuminate\Support\Collection) { + return $current->implode(', '); + } + + if (is_bool($current)) { + return $current ? 'Si' : 'No'; + } + + return $current ?? ''; + } + + private static function boot(): void + { + self::$columns = [ + 'individui' => [ + 'id' => ['label' => 'ID', 'type' => 'direct'], + 'codice_id' => ['label' => 'Codice', 'type' => 'direct'], + 'cognome' => ['label' => 'Cognome', 'type' => 'direct'], + 'nome' => ['label' => 'Nome', 'type' => 'direct'], + 'nome_completo' => ['label' => 'Nome Completo', 'type' => 'accessor'], + 'data_nascita' => ['label' => 'Data Nascita', 'type' => 'direct'], + 'genere' => ['label' => 'Genere', 'type' => 'direct'], + 'indirizzo' => ['label' => 'Indirizzo', 'type' => 'direct'], + 'cap' => ['label' => 'CAP', 'type' => 'direct'], + 'città' => ['label' => 'Città', 'type' => 'direct'], + 'sigla_provincia' => ['label' => 'Provincia', 'type' => 'direct'], + 'tipo_documento' => ['label' => 'Tipo Documento', 'type' => 'direct'], + 'numero_documento' => ['label' => 'Numero Documento', 'type' => 'direct'], + 'scadenza_documento' => ['label' => 'Scadenza Documento', 'type' => 'direct'], + 'note' => ['label' => 'Note', 'type' => 'direct'], + 'created_at' => ['label' => 'Data Creazione', 'type' => 'direct'], + 'updated_at' => ['label' => 'Ultimo Aggiornamento', 'type' => 'direct'], + 'email_primaria' => ['label' => 'Email Primaria', 'type' => 'accessor'], + 'telefono_primario' => ['label' => 'Telefono Primario', 'type' => 'accessor'], + 'contatti.email' => ['label' => 'Email (da contatti)', 'type' => 'relation'], + 'contatti.telefono' => ['label' => 'Telefono (da contatti)', 'type' => 'relation'], + 'contatti.cellulare' => ['label' => 'Cellulare (da contatti)', 'type' => 'relation'], + 'gruppi.nome' => ['label' => 'Gruppi', 'type' => 'relation'], + ], + + 'gruppi' => [ + 'id' => ['label' => 'ID', 'type' => 'direct'], + 'nome' => ['label' => 'Nome', 'type' => 'direct'], + 'descrizione' => ['label' => 'Descrizione', 'type' => 'direct'], + 'parent_id' => ['label' => 'ID Gruppo Padre', 'type' => 'direct'], + 'diocesi_id' => ['label' => 'ID Diocesi', 'type' => 'direct'], + 'indirizzo_incontro' => ['label' => 'Indirizzo Incontro', 'type' => 'direct'], + 'cap_incontro' => ['label' => 'CAP Incontro', 'type' => 'direct'], + 'città_incontro' => ['label' => 'Città Incontro', 'type' => 'direct'], + 'sigla_provincia_incontro' => ['label' => 'Provincia Incontro', 'type' => 'direct'], + 'mappa_posizione' => ['label' => 'Mappa', 'type' => 'direct'], + 'created_at' => ['label' => 'Data Creazione', 'type' => 'direct'], + 'updated_at' => ['label' => 'Ultimo Aggiornamento', 'type' => 'direct'], + 'full_path' => ['label' => 'Percorso Completo', 'type' => 'accessor'], + 'parent.nome' => ['label' => 'Gruppo Padre', 'type' => 'relation'], + 'diocesi.nome' => ['label' => 'Diocesi', 'type' => 'relation'], + 'individui_count' => ['label' => 'Numero Membri', 'type' => 'relation'], + ], + + 'eventi' => [ + 'id' => ['label' => 'ID', 'type' => 'direct'], + 'nome_evento' => ['label' => 'Nome Evento', 'type' => 'direct'], + 'descrizione_evento' => ['label' => 'Descrizione', 'type' => 'direct'], + 'tipo_evento' => ['label' => 'Tipo Evento', 'type' => 'direct'], + 'tipo_recorrenza' => ['label' => 'Ricorrenza', 'type' => 'direct'], + 'data_specifica' => ['label' => 'Data', 'type' => 'direct'], + 'ora_inizio' => ['label' => 'Ora Inizio', 'type' => 'direct'], + 'durata_minuti' => ['label' => 'Durata (minuti)', 'type' => 'direct'], + 'luogo_indirizzo' => ['label' => 'Luogo', 'type' => 'direct'], + 'luogo_url_maps' => ['label' => 'URL Mappe', 'type' => 'direct'], + 'note' => ['label' => 'Note', 'type' => 'direct'], + 'is_incontro_gruppo' => ['label' => 'Incontro Gruppo', 'type' => 'direct'], + 'created_at' => ['label' => 'Data Creazione', 'type' => 'direct'], + 'periodicita_label' => ['label' => 'Periodicità', 'type' => 'accessor'], + 'info_ricorrenza' => ['label' => 'Info Ricorrenza', 'type' => 'accessor'], + 'gruppi.nome' => ['label' => 'Gruppi', 'type' => 'relation'], + 'responsabili.nome_completo' => ['label' => 'Responsabili', 'type' => 'relation'], + ], + + 'documenti' => [ + 'id' => ['label' => 'ID', 'type' => 'direct'], + 'nome_file' => ['label' => 'Nome File', 'type' => 'direct'], + 'tipologia' => ['label' => 'Tipologia', 'type' => 'direct'], + 'visibilita' => ['label' => 'Visibilità', 'type' => 'direct'], + 'mime_type' => ['label' => 'Tipo MIME', 'type' => 'direct'], + 'dimensione' => ['label' => 'Dimensione (bytes)', 'type' => 'direct'], + 'note' => ['label' => 'Note', 'type' => 'direct'], + 'created_at' => ['label' => 'Data Caricamento', 'type' => 'direct'], + 'user.name' => ['label' => 'Caricato Da', 'type' => 'relation'], + ], + + 'contatti' => [ + 'id' => ['label' => 'ID', 'type' => 'direct'], + 'tipo' => ['label' => 'Tipo', 'type' => 'direct'], + 'valore' => ['label' => 'Valore', 'type' => 'direct'], + 'etichetta' => ['label' => 'Etichetta', 'type' => 'direct'], + 'is_primary' => ['label' => 'Primario', 'type' => 'direct'], + 'created_at' => ['label' => 'Data Creazione', 'type' => 'direct'], + 'individuo.nome_completo' => ['label' => 'Individuo', 'type' => 'relation'], + 'individuo.codice_id' => ['label' => 'Codice Individuo', 'type' => 'relation'], + ], + ]; + } +} diff --git a/app/Http/Controllers/Auth/AuthController.php b/app/Http/Controllers/Auth/AuthController.php index d5c66f42..e358ddd4 100644 --- a/app/Http/Controllers/Auth/AuthController.php +++ b/app/Http/Controllers/Auth/AuthController.php @@ -4,11 +4,14 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use App\Models\ActivityLog; +use App\Models\EmailSetting; use App\Models\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Password; use Illuminate\Validation\ValidationException; +use Symfony\Component\Mime\Address; class AuthController extends Controller { @@ -32,11 +35,9 @@ class AuthController extends Controller ]); } - if (Auth::attempt($credentials)) { + if (Auth::attempt($credentials, $request->has('remember'))) { $request->session()->regenerate(); - ActivityLog::logLogin(); - return redirect()->intended('/dashboard'); } @@ -45,63 +46,6 @@ class AuthController extends Controller ]); } - public function showRegisterForm() - { - return view('auth.register'); - } - - public function register(Request $request) - { - $data = $request->validate([ - 'name' => 'required|string|max:255', - 'email' => 'required|email|unique:users', - 'password' => 'required|string|min:8|confirmed', - ]); - - $isFirstUser = User::count() === 0; - - $defaultPermissions = [ - 'individui' => 1, - 'gruppi' => 1, - 'eventi' => 1, - 'documenti' => 1, - 'mailing' => 1, - 'viste' => 1, - 'report' => 0, - 'settings' => 0, - ]; - - if ($isFirstUser) { - $defaultPermissions = [ - 'individui' => 2, - 'gruppi' => 2, - 'eventi' => 2, - 'documenti' => 2, - 'mailing' => 2, - 'viste' => 2, - 'report' => 0, - 'settings' => 0, - ]; - } - - $user = User::create([ - 'name' => $data['name'], - 'email' => $data['email'], - 'password' => Hash::make($data['password']), - 'is_admin' => $isFirstUser, - 'status' => User::STATUS_ACTIVE, - 'permissions' => $defaultPermissions, - ]); - - ActivityLog::log('create', 'utenti', 'Registrato nuovo utente: ' . $user->name, [ - 'user_id' => $user->id, - ]); - - Auth::login($user); - - return redirect('/dashboard'); - } - public function logout(Request $request) { Auth::logout(); @@ -109,4 +53,53 @@ class AuthController extends Controller $request->session()->regenerateToken(); return redirect('/'); } -} \ No newline at end of file + + public function showForgotForm() + { + return view('auth.forgot-password'); + } + + public function sendResetLink(Request $request) + { + $request->validate(['email' => 'required|email']); + + $user = User::where('email', $request->email)->first(); + if (!$user) { + return back()->withErrors(['email' => 'Nessun account trovato con questa email.']); + } + + $token = Password::broker()->createToken($user); + $user->sendPasswordResetNotification($token); + + return back()->with('success', 'Ti abbiamo inviato un link per il reset della password via email.'); + } + + public function showResetForm(string $token) + { + return view('auth.reset-password', ['token' => $token]); + } + + public function reset(Request $request) + { + $request->validate([ + 'token' => 'required', + 'email' => 'required|email', + 'password' => 'required|string|min:8|confirmed', + ]); + + $status = Password::broker()->reset( + $request->only('email', 'password', 'password_confirmation', 'token'), + function (User $user, string $password) { + $user->forceFill([ + 'password' => Hash::make($password), + ])->save(); + } + ); + + if ($status === Password::PASSWORD_RESET) { + return redirect()->route('login')->with('success', 'Password reimpostata con successo. Accedi con la nuova password.'); + } + + return back()->withErrors(['email' => [__($status)]]); + } +} diff --git a/app/Http/Controllers/Auth/ProfileController.php b/app/Http/Controllers/Auth/ProfileController.php new file mode 100644 index 00000000..e2e0d99b --- /dev/null +++ b/app/Http/Controllers/Auth/ProfileController.php @@ -0,0 +1,32 @@ +validate([ + 'current_password' => ['required', 'current_password'], + 'password' => ['required', 'confirmed', Password::min(8)], + ]); + + $request->user()->update([ + 'password' => Hash::make($request->password), + ]); + + return back()->with('success', 'Password aggiornata con successo.'); + } +} diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index 6d703fee..1ec07e45 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -1,15 +1,21 @@ getPrebuiltReports(); - $customReports = \App\Models\ReportCustom::where('user_id', auth()->id()) + $customReports = ReportCustom::where('user_id', auth()->id()) ->orderBy('created_at', 'desc') ->get(); - return view('report.index', compact('stats', 'prebuiltReports', 'customReports')); + $tipologieDocumento = TipologiaDocumento::attive()->pluck('nome'); + + $columnOptions = []; + foreach (ReportColumnRegistry::allEntityTypes() as $type) { + $columnOptions[$type] = ReportColumnRegistry::getColumns($type); + } + + return view('report.index', compact( + 'stats', + 'prebuiltReports', + 'customReports', + 'tipologieDocumento', + 'columnOptions' + )); } public function run(Request $request) @@ -44,28 +63,24 @@ class ReportController extends Controller } $reportType = $request->input('report'); + $customReportId = $request->input('custom_id'); - $data = match ($reportType) { - 'individui_per_genere' => $this->reportIndividuiPerGenere(), - 'individui_per_eta' => $this->reportIndividuiPerEta(), - 'gruppi_gerarchia' => $this->reportGruppiGerarchia(), - 'gruppi_membri' => $this->reportGruppiMembri(), - 'eventi_calendario' => $this->reportEventiCalendario(), - 'documenti_per_tipo' => $this->reportDocumentiPerTipo(), - 'contatti_per_tipo' => $this->reportContattiPerTipo(), - 'mailing_list_dettaglio' => $this->reportMailingListDettaglio(), - 'individui_senza_contatti' => $this->reportIndividuiSenzaContatti(), - 'individui_senza_gruppo' => $this->reportIndividuiSenzaGruppo(), - 'eventi_per_gruppo' => $this->reportEventiPerGruppo(), - 'documenti_orfani' => $this->reportDocumentiOrfani(), - default => null, - }; + if ($customReportId) { + $report = ReportCustom::findOrFail($customReportId); + if ($report->user_id !== auth()->id()) { + abort(403); + } + $data = $this->runCustomReport($report); + return view('report.result', compact('data', 'reportType', 'report')); + } + + $data = $this->resolveReport($request); if (!$data) { return redirect()->route('report.index')->with('error', 'Report non trovato.'); } - return view('report.result', compact('reportType', 'data')); + return view('report.result', compact('data', 'reportType')); } public function runCustom($id) @@ -74,8 +89,8 @@ class ReportController extends Controller abort(403); } - $report = \App\Models\ReportCustom::findOrFail($id); - + $report = ReportCustom::findOrFail($id); + if ($report->user_id !== auth()->id()) { abort(403); } @@ -95,15 +110,38 @@ class ReportController extends Controller 'nome' => 'required|string|max:255', 'descrizione' => 'nullable|string', 'tipo_report' => 'required|string', - 'config' => 'nullable|array', ]); - \App\Models\ReportCustom::create([ + $config = []; + + if ($request->has('columns')) { + $columns = $request->input('columns'); + $config['columns'] = is_array($columns) ? $columns : array_map('trim', explode(',', $columns)); + } + + if ($request->filled('sort_by')) { + $config['sort_by'] = $request->input('sort_by'); + $config['sort_direction'] = $request->input('sort_direction', 'asc'); + } + + if ($request->filled('limit')) { + $config['limit'] = (int) $request->input('limit'); + } + + if ($request->filled('search')) { + $config['search'] = $request->input('search'); + } + + if ($request->has('search_columns')) { + $config['search_columns'] = $request->input('search_columns'); + } + + ReportCustom::create([ 'user_id' => auth()->id(), 'nome' => $data['nome'], 'descrizione' => $data['descrizione'] ?? null, 'tipo_report' => $data['tipo_report'], - 'config' => $data['config'] ?? [], + 'config' => $config, ]); return redirect()->route('report.index')->with('success', 'Report personalizzato creato.'); @@ -115,7 +153,7 @@ class ReportController extends Controller abort(403); } - $report = \App\Models\ReportCustom::where('user_id', auth()->id())->findOrFail($id); + $report = ReportCustom::where('user_id', auth()->id())->findOrFail($id); $report->delete(); return redirect()->route('report.index')->with('success', 'Report personalizzato eliminato.'); @@ -127,39 +165,24 @@ class ReportController extends Controller abort(403); } - $reportType = $request->input('report'); $customReportId = $request->input('custom_id'); if ($customReportId) { - $report = \App\Models\ReportCustom::findOrFail($customReportId); + $report = ReportCustom::findOrFail($customReportId); if ($report->user_id !== auth()->id()) { abort(403); } $data = $this->runCustomReport($report); - return view('report.print-preview', compact('reportType', 'data', 'report')); + return view('report.print-preview', compact('data', 'report')); } - $data = match ($reportType) { - 'individui_per_genere' => $this->reportIndividuiPerGenere(), - 'individui_per_eta' => $this->reportIndividuiPerEta(), - 'gruppi_gerarchia' => $this->reportGruppiGerarchia(), - 'gruppi_membri' => $this->reportGruppiMembri(), - 'eventi_calendario' => $this->reportEventiCalendario(), - 'documenti_per_tipo' => $this->reportDocumentiPerTipo(), - 'contatti_per_tipo' => $this->reportContattiPerTipo(), - 'mailing_list_dettaglio' => $this->reportMailingListDettaglio(), - 'individui_senza_contatti' => $this->reportIndividuiSenzaContatti(), - 'individui_senza_gruppo' => $this->reportIndividuiSenzaGruppo(), - 'eventi_per_gruppo' => $this->reportEventiPerGruppo(), - 'documenti_orfani' => $this->reportDocumentiOrfani(), - default => null, - }; + $data = $this->resolveReport($request); if (!$data) { return redirect()->route('report.index')->with('error', 'Report non trovato.'); } - return view('report.print-preview', compact('reportType', 'data')); + return view('report.print-preview', compact('data')); } public function exportCSV(Request $request) @@ -168,9 +191,49 @@ class ReportController extends Controller abort(403); } - $reportType = $request->input('report'); + $customReportId = $request->input('custom_id'); - $data = match ($reportType) { + if ($customReportId) { + $report = ReportCustom::findOrFail($customReportId); + if ($report->user_id !== auth()->id()) { + abort(403); + } + $data = $this->runCustomReport($report); + $filename = 'report_' . Str::slug($report->nome) . '_' . now()->format('Y-m-d') . '.csv'; + } else { + $data = $this->resolveReport($request); + if (!$data) { + return redirect()->route('report.index')->with('error', 'Report non trovato.'); + } + $filename = 'report_' . $request->input('report') . '_' . now()->format('Y-m-d') . '.csv'; + } + + $response = new \Symfony\Component\HttpFoundation\StreamedResponse(function () use ($data) { + $handle = fopen('php://output', 'w'); + fprintf($handle, chr(0xEF) . chr(0xBB) . chr(0xBF)); + + if (!empty($data['headers']) && !empty($data['rows'])) { + fputcsv($handle, $data['headers']); + foreach ($data['rows'] as $row) { + fputcsv($handle, $row); + } + } + + fclose($handle); + }); + + $response->headers->set('Content-Type', 'text/csv; charset=utf-8'); + $response->headers->set('Content-Disposition', 'attachment; filename="' . $filename . '"'); + + return $response; + } + + protected function resolveReport(Request $request): ?array + { + $reportType = $request->input('report'); + $tipologiaFilter = $request->input('tipologia_documento'); + + return match ($reportType) { 'individui_per_genere' => $this->reportIndividuiPerGenere(), 'individui_per_eta' => $this->reportIndividuiPerEta(), 'gruppi_gerarchia' => $this->reportGruppiGerarchia(), @@ -183,30 +246,14 @@ class ReportController extends Controller 'individui_senza_gruppo' => $this->reportIndividuiSenzaGruppo(), 'eventi_per_gruppo' => $this->reportEventiPerGruppo(), 'documenti_orfani' => $this->reportDocumentiOrfani(), + 'individui_completo' => $this->reportIndividuiCompleto(), + 'gruppi_completo' => $this->reportGruppiCompleto(), + 'eventi_completo' => $this->reportEventiCompleto(), + 'documenti_completo' => $this->reportDocumentiCompleto($tipologiaFilter), + 'contatti_completo' => $this->reportContattiCompleto(), + 'scadenze_documenti' => $this->reportScadenzeDocumenti($tipologiaFilter), default => null, }; - - if (!$data) { - return redirect()->route('report.index')->with('error', 'Report non trovato.'); - } - - $response = new \Symfony\Component\HttpFoundation\StreamedResponse(function () use ($data) { - $handle = fopen('php://output', 'w'); - - if (!empty($data['headers']) && !empty($data['rows'])) { - fputcsv($handle, $data['headers']); - foreach ($data['rows'] as $row) { - fputcsv($handle, $row); - } - } - - fclose($handle); - }); - - $response->headers->set('Content-Type', 'text/csv; charset=utf-8'); - $response->headers->set('Content-Disposition', 'attachment; filename="report_' . $reportType . '_' . now()->format('Y-m-d') . '.csv"'); - - return $response; } protected function getPrebuiltReports(): array @@ -218,6 +265,7 @@ class ReportController extends Controller 'descrizione' => 'Distribuzione degli individui per genere (Maschio/Femmina)', 'icona' => 'fa-venus-mars', 'colore' => 'info', + 'has_tipologia_filter' => false, ], [ 'id' => 'individui_per_eta', @@ -225,6 +273,7 @@ class ReportController extends Controller 'descrizione' => 'Distribuzione degli individui per fasce d\'età', 'icona' => 'fa-birthday-cake', 'colore' => 'success', + 'has_tipologia_filter' => false, ], [ 'id' => 'gruppi_gerarchia', @@ -232,6 +281,7 @@ class ReportController extends Controller 'descrizione' => 'Struttura ad albero completa di tutti i gruppi con livelli', 'icona' => 'fa-sitemap', 'colore' => 'warning', + 'has_tipologia_filter' => false, ], [ 'id' => 'gruppi_membri', @@ -239,6 +289,7 @@ class ReportController extends Controller 'descrizione' => 'Elenco di tutti i gruppi con il conteggio dei membri', 'icona' => 'fa-users', 'colore' => 'primary', + 'has_tipologia_filter' => false, ], [ 'id' => 'eventi_calendario', @@ -246,6 +297,7 @@ class ReportController extends Controller 'descrizione' => 'Tutti gli eventi programmati per il mese corrente', 'icona' => 'fa-calendar-alt', 'colore' => 'danger', + 'has_tipologia_filter' => false, ], [ 'id' => 'documenti_per_tipo', @@ -253,6 +305,7 @@ class ReportController extends Controller 'descrizione' => 'Distribuzione dei documenti per tipologia', 'icona' => 'fa-file-alt', 'colore' => 'secondary', + 'has_tipologia_filter' => false, ], [ 'id' => 'contatti_per_tipo', @@ -260,6 +313,7 @@ class ReportController extends Controller 'descrizione' => 'Distribuzione dei contatti per tipo (telefono, email, ecc.)', 'icona' => 'fa-address-book', 'colore' => 'teal', + 'has_tipologia_filter' => false, ], [ 'id' => 'mailing_list_dettaglio', @@ -267,6 +321,7 @@ class ReportController extends Controller 'descrizione' => 'Elenco completo delle mailing list con i contatti associati', 'icona' => 'fa-envelope', 'colore' => 'purple', + 'has_tipologia_filter' => false, ], [ 'id' => 'individui_senza_contatti', @@ -274,6 +329,7 @@ class ReportController extends Controller 'descrizione' => 'Lista degli individui che non hanno nessun contatto registrato', 'icona' => 'fa-user-slash', 'colore' => 'dark', + 'has_tipologia_filter' => false, ], [ 'id' => 'individui_senza_gruppo', @@ -281,6 +337,7 @@ class ReportController extends Controller 'descrizione' => 'Lista degli individui non associati a nessun gruppo', 'icona' => 'fa-user-friends', 'colore' => 'orange', + 'has_tipologia_filter' => false, ], [ 'id' => 'eventi_per_gruppo', @@ -288,6 +345,7 @@ class ReportController extends Controller 'descrizione' => 'Distribuzione degli eventi per gruppo di appartenenza', 'icona' => 'fa-calendar-check', 'colore' => 'pink', + 'has_tipologia_filter' => false, ], [ 'id' => 'documenti_orfani', @@ -295,6 +353,55 @@ class ReportController extends Controller 'descrizione' => 'Documenti non collegati a nessun individuo, gruppo o evento', 'icona' => 'fa-file-excel', 'colore' => 'gray', + 'has_tipologia_filter' => false, + ], + [ + 'id' => 'documenti_completo', + 'nome' => 'Documenti (dettaglio completo)', + 'descrizione' => 'Elenco completo documenti con uploader, visibilità e filtro per tipologia', + 'icona' => 'fa-file-alt', + 'colore' => 'indigo', + 'has_tipologia_filter' => true, + ], + [ + 'id' => 'scadenze_documenti', + 'nome' => 'Scadenze Documenti', + 'descrizione' => 'Individui con scadenza documento, filtrabile per tipo documento', + 'icona' => 'fa-hourglass-half', + 'colore' => 'danger', + 'has_tipologia_filter' => true, + ], + [ + 'id' => 'individui_completo', + 'nome' => 'Anagrafica Completa', + 'descrizione' => 'Elenco completo individui con contatti email e gruppi di appartenenza', + 'icona' => 'fa-address-card', + 'colore' => 'info', + 'has_tipologia_filter' => false, + ], + [ + 'id' => 'gruppi_completo', + 'nome' => 'Gruppi (dettaglio completo)', + 'descrizione' => 'Gruppi con conteggio membri, diocesi e sottogruppi', + 'icona' => 'fa-sitemap', + 'colore' => 'warning', + 'has_tipologia_filter' => false, + ], + [ + 'id' => 'eventi_completo', + 'nome' => 'Eventi (dettaglio completo)', + 'descrizione' => 'Tutti gli eventi con gruppi associati, responsabili e luogo', + 'icona' => 'fa-calendar-check', + 'colore' => 'success', + 'has_tipologia_filter' => false, + ], + [ + 'id' => 'contatti_completo', + 'nome' => 'Contatti (dettaglio completo)', + 'descrizione' => 'Elenco completo contatti con individuo associato', + 'icona' => 'fa-address-book', + 'colore' => 'teal', + 'has_tipologia_filter' => false, ], ]; } @@ -342,7 +449,7 @@ class ReportController extends Controller ->whereYear('data_nascita', '>=', $now->year - $fascia['max']) ->whereYear('data_nascita', '<=', $now->year - $fascia['min']) ->count(); - + $rows[] = ['fascia' => $fascia['label'], 'totale' => $count]; $totalWithDate += $count; } @@ -361,10 +468,10 @@ class ReportController extends Controller protected function reportGruppiGerarchia(): array { - $rootGruppi = Gruppo::whereNull('parent_id')->with(['children.diocesi', 'children.children'])->orderBy('nome')->get(); + $rootGruppi = Gruppo::whereNull('parent_id')->with(['children', 'diocesi'])->orderBy('nome')->get(); $rows = []; - + foreach ($rootGruppi as $gruppo) { $rows[] = [ 'livello' => 0, @@ -462,7 +569,7 @@ class ReportController extends Controller return [ 'title' => 'Documenti per Tipologia', 'headers' => ['Tipologia', 'Totale'], - 'rows' => $rows->map(fn($r) => [ucfirst($r->tipologia), $r->totale])->toArray(), + 'rows' => $rows->map(fn($r) => [ucfirst((string) $r->tipologia), $r->totale])->toArray(), 'summary' => 'Totale documenti: ' . Documento::count(), 'detail_rows' => $rows->map(fn($r) => (array) $r)->toArray(), ]; @@ -478,7 +585,7 @@ class ReportController extends Controller return [ 'title' => 'Contatti per Tipo', 'headers' => ['Tipo', 'Totale'], - 'rows' => $rows->map(fn($r) => [ucfirst($r->tipo), $r->totale])->toArray(), + 'rows' => $rows->map(fn($r) => [ucfirst((string) $r->tipo), $r->totale])->toArray(), 'summary' => 'Totale contatti: ' . Contatto::count(), 'detail_rows' => $rows->map(fn($r) => (array) $r)->toArray(), ]; @@ -603,8 +710,8 @@ class ReportController extends Controller $rows = $documenti->map(function ($doc) { return [ 'nome' => $doc->nome_file, - 'tipologia' => ucfirst($doc->tipologia), - 'visibilita' => ucfirst($doc->visibilita), + 'tipologia' => ucfirst($doc->tipologia ?? ''), + 'visibilita' => ucfirst($doc->visibilita ?? ''), 'data' => $doc->created_at->format('d/m/Y'), ]; })->toArray(); @@ -618,7 +725,209 @@ class ReportController extends Controller ]; } - protected function runCustomReport(\App\Models\ReportCustom $report): array + protected function reportDocumentiCompleto(?string $tipologiaFilter = null): array + { + $query = Documento::with('user') + ->orderBy('created_at', 'desc'); + + if ($tipologiaFilter) { + $query->where('tipologia', $tipologiaFilter); + } + + $documenti = $query->get(); + + $rows = $documenti->map(function ($doc) { + return [ + 'nome' => $doc->nome_file, + 'tipologia' => ucfirst($doc->tipologia ?? ''), + 'visibilita' => ucfirst($doc->visibilita ?? ''), + 'dimensione' => $doc->dimensione ? number_format($doc->dimensione / 1024, 1) . ' KB' : '-', + 'uploader' => $doc->user?->name ?? '-', + 'data' => $doc->created_at->format('d/m/Y'), + ]; + })->toArray(); + + $title = $tipologiaFilter + ? 'Documenti (tipologia: ' . ucfirst($tipologiaFilter) . ')' + : 'Documenti (dettaglio completo)'; + + return [ + 'title' => $title, + 'headers' => ['Nome File', 'Tipologia', 'Visibilità', 'Dimensione', 'Caricato Da', 'Data'], + 'rows' => array_map(fn($r) => [$r['nome'], $r['tipologia'], $r['visibilita'], $r['dimensione'], $r['uploader'], $r['data']], $rows), + 'summary' => 'Totale documenti: ' . $documenti->count(), + 'detail_rows' => $rows, + ]; + } + + protected function reportScadenzeDocumenti(?string $tipologiaFilter = null): array + { + $query = Individuo::whereNotNull('scadenza_documento') + ->whereNotNull('tipo_documento') + ->orderBy('scadenza_documento'); + + if ($tipologiaFilter) { + $query->where('tipo_documento', $tipologiaFilter); + } + + $individui = $query->get(); + + $rows = $individui->map(function ($ind) { + $scadenza = $ind->scadenza_documento; + $giorniMancanti = $scadenza ? now()->diffInDays($scadenza, false) : null; + + return [ + 'codice' => $ind->codice_id, + 'cognome' => $ind->cognome, + 'nome' => $ind->nome, + 'tipo_documento' => ucfirst($ind->tipo_documento ?? ''), + 'numero' => $ind->numero_documento ?? '-', + 'scadenza' => $scadenza?->format('d/m/Y') ?? '-', + 'stato' => $giorniMancanti !== null + ? ($giorniMancanti < 0 + ? ('Scaduto da ' . abs($giorniMancanti) . ' gg') + : ($giorniMancanti <= 30 + ? 'Scade tra ' . $giorniMancanti . ' gg' + : 'In regola (' . $giorniMancanti . ' gg)')) + : '-', + ]; + })->toArray(); + + $title = $tipologiaFilter + ? 'Scadenze Documenti (tipo: ' . ucfirst($tipologiaFilter) . ')' + : 'Scadenze Documenti'; + + return [ + 'title' => $title, + 'headers' => ['Codice', 'Cognome', 'Nome', 'Tipo Documento', 'Numero', 'Scadenza', 'Stato'], + 'rows' => array_map(fn($r) => [$r['codice'], $r['cognome'], $r['nome'], $r['tipo_documento'], $r['numero'], $r['scadenza'], $r['stato']], $rows), + 'summary' => 'Individui con documento: ' . $individui->count(), + 'detail_rows' => $rows, + ]; + } + + protected function reportIndividuiCompleto(): array + { + $individui = Individuo::with(['contatti', 'gruppi']) + ->orderBy('cognome') + ->orderBy('nome') + ->get(); + + $rows = $individui->map(function ($ind) { + $email = $ind->contatti->where('tipo', 'email')->first()?->valore + ?? $ind->email + ?? '-'; + $telefono = $ind->contatti->whereIn('tipo', ['telefono', 'cellulare'])->first()?->valore + ?? $ind->telefono + ?? '-'; + $gruppi = $ind->gruppi->pluck('nome')->implode(', ') ?: '-'; + + return [ + 'codice' => $ind->codice_id, + 'cognome' => $ind->cognome, + 'nome' => $ind->nome, + 'data_nascita' => $ind->data_nascita?->format('d/m/Y') ?? '-', + 'genere' => match ($ind->genere) { 'M' => 'M', 'F' => 'F', default => '-' }, + 'email' => $email, + 'telefono' => $telefono, + 'città' => $ind->città ?? '-', + 'gruppi' => $gruppi, + ]; + })->toArray(); + + return [ + 'title' => 'Anagrafica Completa Individui', + 'headers' => ['Codice', 'Cognome', 'Nome', 'Data Nascita', 'Genere', 'Email', 'Telefono', 'Città', 'Gruppi'], + 'rows' => array_map(fn($r) => [$r['codice'], $r['cognome'], $r['nome'], $r['data_nascita'], $r['genere'], $r['email'], $r['telefono'], $r['città'], $r['gruppi']], $rows), + 'summary' => 'Totale individui: ' . $individui->count(), + 'detail_rows' => $rows, + ]; + } + + protected function reportGruppiCompleto(): array + { + $gruppi = Gruppo::withCount('individui') + ->with('diocesi') + ->orderBy('nome') + ->get(); + + $rows = $gruppi->map(function ($g) { + $parent = $g->parent; + return [ + 'nome' => $g->nome, + 'descrizione' => $g->descrizione ?? '-', + 'diocesi' => $g->diocesi?->nome ?? '-', + 'livello' => $g->parent_id ? (count($g->getAncestors()) + 1) : 0, + 'membri' => $g->individui_count, + 'padre' => $parent?->nome ?? '-', + 'città' => $g->città_incontro ?? '-', + ]; + })->toArray(); + + return [ + 'title' => 'Gruppi (dettaglio completo)', + 'headers' => ['Nome', 'Descrizione', 'Diocesi', 'Livello', 'Membri', 'Padre', 'Città'], + 'rows' => array_map(fn($r) => [$r['nome'], $r['descrizione'], $r['diocesi'], $r['livello'], $r['membri'], $r['padre'], $r['città']], $rows), + 'summary' => 'Totale gruppi: ' . $gruppi->count() . ' | Totale membri: ' . $gruppi->sum('individui_count'), + 'detail_rows' => $rows, + ]; + } + + protected function reportEventiCompleto(): array + { + $eventi = Evento::with(['gruppi', 'responsabili']) + ->orderBy('data_specifica', 'desc') + ->get(); + + $rows = $eventi->map(function ($e) { + return [ + 'nome' => $e->nome_evento, + 'data' => $e->data_specifica?->format('d/m/Y') ?? '-', + 'tipo_evento' => $e->tipo_evento ?? '-', + 'ricorrenza' => ucfirst($e->tipo_recorrenza ?? 'singolo'), + 'gruppi' => $e->gruppi->pluck('nome')->implode(', ') ?: '-', + 'responsabili' => $e->responsabili->pluck('nome_completo')->implode(', ') ?: '-', + 'luogo' => $e->luogo_indirizzo ?? '-', + ]; + })->toArray(); + + return [ + 'title' => 'Eventi (dettaglio completo)', + 'headers' => ['Evento', 'Data', 'Tipo Evento', 'Ricorrenza', 'Gruppi', 'Responsabili', 'Luogo'], + 'rows' => array_map(fn($r) => [$r['nome'], $r['data'], $r['tipo_evento'], $r['ricorrenza'], $r['gruppi'], $r['responsabili'], $r['luogo']], $rows), + 'summary' => 'Totale eventi: ' . $eventi->count(), + 'detail_rows' => $rows, + ]; + } + + protected function reportContattiCompleto(): array + { + $contatti = Contatto::with('individuo') + ->orderBy('tipo') + ->orderBy('valore') + ->get(); + + $rows = $contatti->map(function ($c) { + return [ + 'tipo' => ucfirst($c->tipo), + 'valore' => $c->valore, + 'etichetta' => $c->etichetta ?? '-', + 'primario' => $c->is_primary ? 'Si' : 'No', + 'individuo' => $c->individuo?->nome_completo ?? '-', + 'codice' => $c->individuo?->codice_id ?? '-', + ]; + })->toArray(); + + return [ + 'title' => 'Contatti (dettaglio completo)', + 'headers' => ['Tipo', 'Valore', 'Etichetta', 'Primario', 'Individuo', 'Codice'], + 'rows' => array_map(fn($r) => [$r['tipo'], $r['valore'], $r['etichetta'], $r['primario'], $r['individuo'], $r['codice']], $rows), + 'summary' => 'Totale contatti: ' . $contatti->count(), + 'detail_rows' => $rows, + ]; + } + + protected function runCustomReport(ReportCustom $report): array { $tipo = $report->tipo_report; $config = $report->config ?? []; @@ -642,17 +951,33 @@ class ReportController extends Controller ]; } + $columns = $config['columns'] ?? []; + $hasRelations = false; + + foreach ($columns as $col) { + if (str_contains($col, '.')) { + $hasRelations = true; + $relation = explode('.', $col)[0]; + $query->with($relation); + } + } + if (!empty($config['search'])) { $search = $config['search']; - $query->where(function ($q) use ($search) { + $query->where(function ($q) use ($config, $search) { foreach ($config['search_columns'] ?? [] as $col) { - $q->orWhere($col, 'like', '%' . $search . '%'); + if (!str_contains($col, '.')) { + $q->orWhere($col, 'like', '%' . $search . '%'); + } } }); } if (!empty($config['sort_by'])) { - $query->orderBy($config['sort_by'], $config['sort_direction'] ?? 'asc'); + $sortBy = $config['sort_by']; + if (!str_contains($sortBy, '.')) { + $query->orderBy($sortBy, $config['sort_direction'] ?? 'asc'); + } } if (!empty($config['limit'])) { @@ -661,16 +986,29 @@ class ReportController extends Controller $results = $query->get(); - $headers = !empty($config['columns']) ? $config['columns'] : array_keys($results->first()?->toArray() ?? []); + if (empty($columns)) { + $first = $results->first(); + $columns = $first ? array_keys($first->toArray()) : []; + } - $rows = $results->map(function ($item) use ($headers) { + $headers = array_map(function ($col) use ($tipo) { + return ReportColumnRegistry::getColumnLabel($tipo, $col) ?? $col; + }, $columns); + + $rows = $results->map(function ($item) use ($columns, $tipo) { $row = []; - foreach ($headers as $col) { - $value = $item->{$col} ?? ''; - if ($value instanceof \Carbon\Carbon) { - $value = $value->format('d/m/Y'); + foreach ($columns as $col) { + if (str_contains($col, '.')) { + $row[] = ReportColumnRegistry::resolveRelationValue($item, $col); + } else { + $value = $item->{$col} ?? ''; + if ($value instanceof \Carbon\Carbon) { + $value = $value->format('d/m/Y'); + } elseif (is_bool($value)) { + $value = $value ? 'Si' : 'No'; + } + $row[] = $value; } - $row[] = $value; } return $row; })->toArray(); diff --git a/app/Models/User.php b/app/Models/User.php index c6316714..796548bf 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -7,14 +7,22 @@ use Illuminate\Database\Eloquent\Attributes\Hidden; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Auth\Passwords\CanResetPassword; +use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Illuminate\Support\Facades\Crypt; +use Illuminate\Support\Facades\Log; +use Symfony\Component\Mailer\Mailer; +use Symfony\Component\Mailer\Transport; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Email; #[Fillable(['name', 'email', 'password', 'tenant_id', 'is_admin', 'permissions', 'role_preset_id', 'status'])] #[Hidden(['password', 'remember_token'])] -class User extends Authenticatable +class User extends Authenticatable implements CanResetPasswordContract { - use HasFactory, Notifiable; + use HasFactory, Notifiable, CanResetPassword; protected $fillable = ['name', 'email', 'password', 'tenant_id', 'is_admin', 'permissions', 'role_preset_id', 'status']; @@ -156,4 +164,62 @@ class User extends Authenticatable $this->permissions = $preset->permissions; return $this; } + + public function sendPasswordResetNotification($token): void + { + $settings = EmailSetting::getActive(); + if (!$settings || empty($settings->smtp_host)) { + Log::warning('Impossibile inviare reset password: SMTP non configurato'); + return; + } + + try { + $smtpUsername = $settings->smtp_username ?: $settings->email_address; + $smtpPassword = $settings->smtp_password + ? Crypt::decryptString($settings->smtp_password) + : $settings->getDecryptedPassword(); + + $encryption = $settings->smtp_encryption ?? 'tls'; + $scheme = ($encryption === 'ssl') ? 'smtps' : 'smtp'; + + $dsn = sprintf( + '%s://%s:%s@%s:%d?encryption=%s', + $scheme, + rawurlencode($smtpUsername), + rawurlencode($smtpPassword), + $settings->smtp_host, + $settings->smtp_port ?? 587, + $encryption + ); + + $transport = Transport::fromDsn($dsn); + $mailer = new Mailer($transport); + + $resetUrl = url('/password/reset/' . $token); + + $fromName = $settings->email_name ?? config('app.name'); + $fromName = preg_replace('/[^\p{L}\p{N}\s]/u', '', $fromName); + $fromName = trim($fromName) ?: config('app.name'); + + $email = (new Email()) + ->from(new Address($settings->email_address, $fromName)) + ->to($this->email) + ->subject('Reset della password - ' . config('app.name')) + ->html(view('auth.emails.reset-password', [ + 'user' => $this, + 'token' => $token, + 'resetUrl' => $resetUrl, + 'appName' => config('app.name'), + ])->render()); + + $mailer->send($email); + + Log::info('Password reset email sent', ['email' => $this->email]); + } catch (\Exception $e) { + Log::error('Failed to send password reset email', [ + 'email' => $this->email, + 'error' => $e->getMessage(), + ]); + } + } } \ No newline at end of file diff --git a/resources/views/auth/emails/reset-password.blade.php b/resources/views/auth/emails/reset-password.blade.php new file mode 100644 index 00000000..fe6acd34 --- /dev/null +++ b/resources/views/auth/emails/reset-password.blade.php @@ -0,0 +1,33 @@ + + + + + + + +
+
+

{{ e($appName) }}

+
+
+

Ciao {{ e($user->name) }},

+

Hai richiesto il reset della password per il tuo account.

+

+ Reimposta Password +

+

Se non hai richiesto tu il reset, ignora questa email.

+

Il link è valido per 60 minuti.

+
+ +
+ + diff --git a/resources/views/auth/forgot-password.blade.php b/resources/views/auth/forgot-password.blade.php new file mode 100644 index 00000000..f8a4da6a --- /dev/null +++ b/resources/views/auth/forgot-password.blade.php @@ -0,0 +1,60 @@ + + + + + + + Reset Password - {{ e($appName) }} + + + + +
+ +
+ +
+
+ + + + diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 0537cdf6..04526f4b 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -69,7 +69,7 @@ $appOrgName = AppSetting::getOrgName() ?? '';

- Non hai un account? Registrati + Password dimenticata?

diff --git a/resources/views/auth/profile.blade.php b/resources/views/auth/profile.blade.php new file mode 100644 index 00000000..cf8d39f0 --- /dev/null +++ b/resources/views/auth/profile.blade.php @@ -0,0 +1,76 @@ +@extends('layouts.adminlte') + +@section('title', 'Il Mio Profilo') +@section('page_title', 'Il Mio Profilo') + +@section('breadcrumbs') + + +@endsection + +@section('content') +
+
+
+
+

Informazioni Account

+
+
+
+
Nome
+
{{ e(Auth::user()->name) }}
+
Email
+
{{ e(Auth::user()->email) }}
+
Membro dal
+
{{ Auth::user()->created_at->format('d/m/Y') }}
+
+
+
+
+ +
+
+
+

Cambia Password

+
+
+ @csrf + @method('PUT') +
+ @if(session('success')) +
{{ session('success') }}
+ @endif + @if(session('error')) +
{{ session('error') }}
+ @endif + +
+ + + @error('current_password') + {{ $message }} + @enderror +
+
+ + + Minimo 8 caratteri + @error('password') + {{ $message }} + @enderror +
+
+ + +
+
+ +
+
+
+
+@endsection diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php deleted file mode 100644 index 31047d2d..00000000 --- a/resources/views/auth/register.blade.php +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - Registrati - {{ e($appName) }} - - - - -
- -
-
- -
- @csrf -
- -
-
- -
-
- @error('name') - {{ $message }} - @enderror -
-
- -
-
- -
-
- @error('email') - {{ $message }} - @enderror -
-
- -
-
- -
-
- @error('password') - {{ $message }} - @enderror -
-
- -
-
- -
-
-
-
-
- -
-
-
-

- Hai già un account? Accedi -

-
-
-
- - - - \ No newline at end of file diff --git a/resources/views/auth/reset-password.blade.php b/resources/views/auth/reset-password.blade.php new file mode 100644 index 00000000..30d7d322 --- /dev/null +++ b/resources/views/auth/reset-password.blade.php @@ -0,0 +1,63 @@ + + + + + + + Reimposta Password - {{ e($appName) }} + + + + +
+ +
+ +
+
+ + + + diff --git a/resources/views/layouts/adminlte.blade.php b/resources/views/layouts/adminlte.blade.php index 6c2c4e90..1a826247 100644 --- a/resources/views/layouts/adminlte.blade.php +++ b/resources/views/layouts/adminlte.blade.php @@ -42,7 +42,7 @@ $appName = \App\Models\AppSetting::getAppName() ?? 'Glastree'; {{ Auth::user()->name }} @endif -
-
-
-
-

Panoramica Database

-
+
+ {{-- Sezione 1: Panoramica Database (sempre aperta) --}} +
+
+

+ +

+
+
@@ -84,29 +121,63 @@
-
-
-
-
-
-

Report Predefiniti

-
+ {{-- Sezione 2: Report Predefiniti --}} +
+
+

+ +

+
+
@foreach($prebuiltReports as $report)
-
-
- + @php + $cardColor = $report['colore'] === 'indigo' ? 'card-indigo' : 'card-' . $report['colore']; + $btnColor = $report['colore'] === 'indigo' ? 'primary' : $report['colore']; + @endphp +
+
+
{{ $report['nome'] }}
-

{{ $report['descrizione'] }}

- - Genera - - - CSV - +

{{ $report['descrizione'] }}

+ + @if($report['has_tipologia_filter'] ?? false) +
+
+ +
+ +
+ + + CSV + +
+
+ @else + + @endif
@@ -115,109 +186,123 @@
-
-
-
-
-
-

Report Personalizzati

-
-
- @if($customReports->count() > 0) - - - - - - - - - - - @foreach($customReports as $report) - - - - - - - @endforeach - -
NomeTipoCreato ilAzioni
- - {{ $report->nome }} - {{ ucfirst($report->tipo_report) }}{{ $report->created_at->format('d/m/Y H:i') }} - - - -
- @csrf @method('DELETE') - + + +
+
+
+
+ @if($customReports->count() > 0) + + + + + + + + + + + @foreach($customReports as $report) + + + + + + + @endforeach + +
NomeTipoCreato ilAzioni
+ + {{ $report->nome }} + {{ ucfirst($report->tipo_report) }}{{ $report->created_at->format('d/m/Y H:i') }} + + + + + + + + @csrf @method('DELETE') + + +
+ @else +
+ +

Nessun report personalizzato creato.

+
+ @endif +
+ +
+
+
+

Crea Report Personalizzato

+
+
+
+ @csrf +
+ + +
+
+ + +
+
+ + +
+
+ + + Seleziona le colonne da includere nel report +
+
+ + +
+
+ + +
+
+ + +
+
-
- @else -
- -

Nessun report personalizzato creato.

+
+
+
- @endif -
-
-
- -
-
-
-

Crea Report Personalizzato

-
-
-
- @csrf -
- - -
-
- - -
-
- - -
-
- - - Lascia vuoto per tutte le colonne -
-
- - -
-
- - -
- -
@@ -225,25 +310,105 @@ @endsection @section('scripts') + @endsection diff --git a/resources/views/report/result.blade.php b/resources/views/report/result.blade.php index 2231e23c..b6a87f72 100644 --- a/resources/views/report/result.blade.php +++ b/resources/views/report/result.blade.php @@ -11,10 +11,14 @@ Torna ai Report - + @php + $printParams = isset($report) ? ['custom_id' => $report->id] : ['report' => $reportType ?? '']; + $exportParams = isset($report) ? ['custom_id' => $report->id] : ['report' => $reportType ?? '']; + @endphp + Stampa Report - + Esporta CSV
diff --git a/routes/web.php b/routes/web.php index 57a7097f..f48e69db 100644 --- a/routes/web.php +++ b/routes/web.php @@ -36,12 +36,19 @@ Route::get('/', function () { Route::middleware('guest')->group(function () { Route::get('/login', [AuthController::class, 'showLoginForm'])->name('login'); Route::post('/login', [AuthController::class, 'login']); - Route::get('/register', [AuthController::class, 'showRegisterForm'])->name('register'); - Route::post('/register', [AuthController::class, 'register']); + Route::get('/password/reset', [AuthController::class, 'showForgotForm'])->name('password.request'); + Route::post('/password/email', [AuthController::class, 'sendResetLink'])->name('password.email'); + Route::get('/password/reset/{token}', [AuthController::class, 'showResetForm'])->name('password.reset'); + Route::post('/password/reset', [AuthController::class, 'reset'])->name('password.update'); }); Route::post('/logout', [AuthController::class, 'logout'])->name('logout')->middleware('auth'); +Route::middleware('auth')->group(function () { + Route::get('/profilo', [\App\Http\Controllers\Auth\ProfileController::class, 'show'])->name('profile'); + Route::put('/profilo/password', [\App\Http\Controllers\Auth\ProfileController::class, 'updatePassword'])->name('profile.password'); +}); + // Dashboard Route::get('/dashboard', [HomeController::class, 'index'])->name('dashboard')->middleware('auth'); diff --git a/storage/framework/views/00edc551e83193362ca154488ca4729a.php b/storage/framework/views/00edc551e83193362ca154488ca4729a.php index 0cc2b384..1ab8446e 100755 --- a/storage/framework/views/00edc551e83193362ca154488ca4729a.php +++ b/storage/framework/views/00edc551e83193362ca154488ca4729a.php @@ -10,10 +10,14 @@ Torna ai Report - + $report->id] : ['report' => $reportType ?? '']; + $exportParams = isset($report) ? ['custom_id' => $report->id] : ['report' => $reportType ?? '']; + ?> + Stampa Report - + Esporta CSV
diff --git a/storage/framework/views/28c9abf6c3545b82f2c66c4bde35bcb3.php b/storage/framework/views/28c9abf6c3545b82f2c66c4bde35bcb3.php index bc9f5f79..5e2ca1ad 100755 --- a/storage/framework/views/28c9abf6c3545b82f2c66c4bde35bcb3.php +++ b/storage/framework/views/28c9abf6c3545b82f2c66c4bde35bcb3.php @@ -1,6 +1,37 @@ startSection('title', 'Report'); ?> startSection('page_title', 'Report e Statistiche'); ?> +startSection('styles'); ?> + + + +stopSection(); ?> + startSection('content'); ?> @@ -19,12 +50,18 @@
-
-
-
-
-

Panoramica Database

-
+
+ +
+
+

+ +

+
+
@@ -85,29 +122,63 @@
-
-
-
-
-
-

Report Predefiniti

-
+ +
+
+

+ +

+
+
addLoop($__currentLoopData); foreach($__currentLoopData as $report): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
-
-
- + +
+
+
-

- - Genera - - - CSV - +

+ + +
+
+ +
+ +
+ + + CSV + +
+
+ + +
@@ -116,110 +187,124 @@
-
-
-
-
-
-

Report Personalizzati

-
-
- count() > 0): ?> - - - - - - - - - - - addLoop($__currentLoopData); foreach($__currentLoopData as $report): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> - - - - popLoop(); $loop = $__env->getLastLoop(); ?> - -
NomeTipoCreato ilAzioni
- - nome); ?> + +
+
+

+ +

+
+
+
+
+
+ count() > 0): ?> + + + + + + + + + + + addLoop($__currentLoopData); foreach($__currentLoopData as $report): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> + + - - - + + + + + popLoop(); $loop = $__env->getLastLoop(); ?> + +
NomeTipoCreato ilAzioni
+ + nome); ?> - tipo_report)); ?>created_at->format('d/m/Y H:i')); ?> - - - -
- -
tipo_report)); ?>created_at->format('d/m/Y H:i')); ?> + + + + + + + + + + +
+ +
+ +

Nessun report personalizzato creato.

+
+ +
+ +
+
+
+

Crea Report Personalizzato

+
+
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + + Seleziona le colonne da includere nel report +
+
+ + +
+
+ + +
+
+ + +
+
-
- -
- -

Nessun report personalizzato creato.

+
+
+
- -
-
-
- -
-
-
-

Crea Report Personalizzato

-
-
-
- -
- - -
-
- - -
-
- - -
-
- - - Lascia vuoto per tutte le colonne -
-
- - -
-
- - -
- -
@@ -227,25 +312,105 @@ stopSection(); ?> startSection('scripts'); ?> + stopSection(); ?> diff --git a/storage/framework/views/486ad3b47013e2f855528231fc36c018.php b/storage/framework/views/486ad3b47013e2f855528231fc36c018.php new file mode 100755 index 00000000..178e4f4f --- /dev/null +++ b/storage/framework/views/486ad3b47013e2f855528231fc36c018.php @@ -0,0 +1,104 @@ +startSection('title', 'Il Mio Profilo'); ?> +startSection('page_title', 'Il Mio Profilo'); ?> + +startSection('breadcrumbs'); ?> + + +stopSection(); ?> + +startSection('content'); ?> +
+
+
+
+

Informazioni Account

+
+
+
+
Nome
+
name)); ?>
+
Email
+
email)); ?>
+
Membro dal
+
created_at->format('d/m/Y')); ?>
+
+
+
+
+ +
+
+
+

Cambia Password

+
+
+ + +
+ +
+ + +
+ + +
+ + + getBag($__errorArgs[1] ?? 'default'); +if ($__bag->has($__errorArgs[0])) : +if (isset($message)) { $__messageOriginal = $message; } +$message = $__bag->first($__errorArgs[0]); ?> + + +
+
+ + + Minimo 8 caratteri + getBag($__errorArgs[1] ?? 'default'); +if ($__bag->has($__errorArgs[0])) : +if (isset($message)) { $__messageOriginal = $message; } +$message = $__bag->first($__errorArgs[0]); ?> + + +
+
+ + +
+
+ +
+
+
+
+stopSection(); ?> + +make('layouts.adminlte', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/7cfff45be1176bacefa7e2488c0914ed.php b/storage/framework/views/7cfff45be1176bacefa7e2488c0914ed.php new file mode 100755 index 00000000..85b2f586 --- /dev/null +++ b/storage/framework/views/7cfff45be1176bacefa7e2488c0914ed.php @@ -0,0 +1,35 @@ + + + + + + + +
+
+

+
+
+

Ciao name)); ?>,

+

Hai richiesto il reset della password per il tuo account.

+

+ Reimposta Password +

+

Se non hai richiesto tu il reset, ignora questa email.

+

Il link è valido per 60 minuti.

+
+ +
+ + + \ No newline at end of file diff --git a/storage/framework/views/7fe4bfde84ae8ad75ccbe8b21689cbfa.php b/storage/framework/views/7fe4bfde84ae8ad75ccbe8b21689cbfa.php new file mode 100755 index 00000000..5c5f476e --- /dev/null +++ b/storage/framework/views/7fe4bfde84ae8ad75ccbe8b21689cbfa.php @@ -0,0 +1,85 @@ + + + + + + + Reimposta Password - <?php echo e(e($appName)); ?> + + + + + + + + + + \ No newline at end of file diff --git a/storage/framework/views/ad3b26036a197d93027e9b56fcb36d0a.php b/storage/framework/views/ad3b26036a197d93027e9b56fcb36d0a.php new file mode 100755 index 00000000..d06d54cd --- /dev/null +++ b/storage/framework/views/ad3b26036a197d93027e9b56fcb36d0a.php @@ -0,0 +1,75 @@ + + + + + + + Reset Password - <?php echo e(e($appName)); ?> + + + + + + + + + + \ No newline at end of file diff --git a/storage/framework/views/e8f60d5f5b2a4c688f1bc12477b209df.php b/storage/framework/views/e8f60d5f5b2a4c688f1bc12477b209df.php index 2d0ad738..4a5ca4aa 100755 --- a/storage/framework/views/e8f60d5f5b2a4c688f1bc12477b209df.php +++ b/storage/framework/views/e8f60d5f5b2a4c688f1bc12477b209df.php @@ -97,7 +97,7 @@ unset($__errorArgs, $__bag); ?>

- Non hai un account? Registrati + Password dimenticata?

diff --git a/storage/framework/views/f45eecce65c94b1fc574994507f6259e.php b/storage/framework/views/f45eecce65c94b1fc574994507f6259e.php deleted file mode 100755 index f147b871..00000000 --- a/storage/framework/views/f45eecce65c94b1fc574994507f6259e.php +++ /dev/null @@ -1,122 +0,0 @@ - - - - - - - Registrati - <?php echo e(e($appName)); ?> - - - - -
- -
-
- -
- -
- -
-
- -
-
- getBag($__errorArgs[1] ?? 'default'); -if ($__bag->has($__errorArgs[0])) : -if (isset($message)) { $__messageOriginal = $message; } -$message = $__bag->first($__errorArgs[0]); ?> - - -
-
- -
-
- -
-
- getBag($__errorArgs[1] ?? 'default'); -if ($__bag->has($__errorArgs[0])) : -if (isset($message)) { $__messageOriginal = $message; } -$message = $__bag->first($__errorArgs[0]); ?> - - -
-
- -
-
- -
-
- getBag($__errorArgs[1] ?? 'default'); -if ($__bag->has($__errorArgs[0])) : -if (isset($message)) { $__messageOriginal = $message; } -$message = $__bag->first($__errorArgs[0]); ?> - - -
-
- -
-
- -
-
-
-
-
- -
-
-
-

- Hai già un account? Accedi -

-
-
-
- - - - \ No newline at end of file diff --git a/storage/framework/views/f5ef96019b613b67dddc87baed21eb0c.php b/storage/framework/views/f5ef96019b613b67dddc87baed21eb0c.php index ab6affbf..8857acd4 100755 --- a/storage/framework/views/f5ef96019b613b67dddc87baed21eb0c.php +++ b/storage/framework/views/f5ef96019b613b67dddc87baed21eb0c.php @@ -43,7 +43,7 @@ $appName = \App\Models\AppSetting::getAppName() ?? 'Glastree';