sistemazione reports - cambio password - profilo utente

This commit is contained in:
2026-05-27 10:45:05 +02:00
parent fefc3bba2a
commit 3471befb1a
26 changed files with 1964 additions and 632 deletions
+200
View File
@@ -0,0 +1,200 @@
<?php
declare(strict_types=1);
namespace App\Helpers;
class ReportColumnRegistry
{
private static array $columns = [];
public static function getColumns(string $entityType): array
{
if (empty(self::$columns)) {
self::boot();
}
return self::$columns[$entityType] ?? [];
}
public static function allEntityTypes(): array
{
if (empty(self::$columns)) {
self::boot();
}
return array_keys(self::$columns);
}
public static function getColumnLabel(string $entityType, string $column): ?string
{
$cols = self::getColumns($entityType);
return $cols[$column]['label'] ?? null;
}
public static function resolveRelationValue(object $item, string $column): mixed
{
$parts = explode('.', $column);
if (count($parts) === 1) {
return $item->{$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'],
],
];
}
}
+54 -61
View File
@@ -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('/');
}
}
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)]]);
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
class ProfileController extends Controller
{
public function show()
{
return view('auth.profile');
}
public function updatePassword(Request $request)
{
$request->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.');
}
}
+424 -86
View File
@@ -1,15 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\Individuo;
use App\Helpers\ReportColumnRegistry;
use App\Models\Gruppo;
use App\Models\Individuo;
use App\Models\Evento;
use App\Models\Documento;
use App\Models\MailingList;
use App\Models\Contatto;
use App\Models\TipologiaDocumento;
use App\Models\ReportCustom;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class ReportController extends Controller
{
@@ -30,11 +36,24 @@ class ReportController extends Controller
$prebuiltReports = $this->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();
+68 -2
View File
@@ -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(),
]);
}
}
}