diff --git a/app/Http/Controllers/EmailController.php b/app/Http/Controllers/EmailController.php index 04198e9a..5624ff22 100644 --- a/app/Http/Controllers/EmailController.php +++ b/app/Http/Controllers/EmailController.php @@ -59,7 +59,8 @@ class EmailController extends Controller $query->orderBy($sortField === 'sent_at' ? 'received_at' : $sortField, $sortDir); - $messages = $query->paginate(20)->withQueryString(); + $perPage = in_array((int) $request->perPage, [10, 20, 25, 50, 100]) ? (int) $request->perPage : 20; + $messages = $query->paginate($perPage)->withQueryString(); $folders = EmailFolder::orderBy('sort_order')->get(); $mailingLists = MailingList::where('attiva', true)->get(); $gruppi = Gruppo::orderBy('nome')->get(); diff --git a/app/Http/Controllers/EventoController.php b/app/Http/Controllers/EventoController.php index 38fe7e2f..e70f4adb 100644 --- a/app/Http/Controllers/EventoController.php +++ b/app/Http/Controllers/EventoController.php @@ -8,6 +8,7 @@ use App\Models\Individuo; use App\Models\TipologiaEvento; use App\Services\IcsExportService; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Log; use Symfony\Component\HttpFoundation\StreamedResponse; class EventoController extends Controller @@ -43,7 +44,8 @@ class EventoController extends Controller $query->orderByDesc('created_at'); } - $eventi = $query->paginate(20); + $perPage = in_array((int) $request->perPage, [10, 20, 25, 50, 100]) ? (int) $request->perPage : 20; + $eventi = $query->paginate($perPage); $gruppi = Gruppo::orderBy('nome')->get(); return view('eventi.index', compact('eventi', 'gruppi')); @@ -477,4 +479,101 @@ class EventoController extends Controller return $colors['primary']; } + + public function importIcs() + { + $this->authorizeWrite('eventi'); + return view('eventi.import'); + } + + public function importIcsStore(Request $request) + { + $this->authorizeWrite('eventi'); + $request->validate([ + 'file' => 'required|file|mimes:ics,text/calendar', + ]); + + $file = $request->file('file'); + $content = file_get_contents($file->path()); + + try { + $vcalendar = \Sabre\VObject\Reader::read($content); + } catch (\Exception $e) { + Log::warning('Errore lettura file ICS: ' . $e->getMessage()); + return back()->with('error', 'Il file ICS non è valido: ' . $e->getMessage()); + } + + $imported = 0; + $skipped = 0; + $errors = []; + + foreach ($vcalendar->VEVENT as $vevent) { + try { + $uid = (string) ($vevent->UID ?? ''); + if (!empty($uid)) { + $existing = Evento::where('uid_esterno', $uid)->first(); + if ($existing) { + $skipped++; + continue; + } + } + + $nomeEvento = (string) ($vevent->SUMMARY ?? ''); + if (empty($nomeEvento)) { + $skipped++; + continue; + } + + $dtStart = $vevent->DTSTART ?? null; + $dtEnd = $vevent->DTEND ?? null; + + $dataSpecifica = null; + $oraInizio = null; + $durataMinuti = null; + + if ($dtStart) { + $dtStartDateTime = new \DateTime((string) $dtStart); + $dataSpecifica = $dtStartDateTime->format('Y-m-d'); + $oraInizio = $dtStartDateTime->format('H:i'); + + if ($dtEnd) { + $dtEndDateTime = new \DateTime((string) $dtEnd); + $durataMinuti = (int) ($dtEndDateTime->getTimestamp() - $dtStartDateTime->getTimestamp()) / 60; + if ($durataMinuti < 1) { + $durataMinuti = null; + } + } + } + + $descrizione = (string) ($vevent->DESCRIPTION ?? ''); + $luogo = (string) ($vevent->LOCATION ?? ''); + + Evento::create([ + 'nome_evento' => $nomeEvento, + 'descrizione' => !empty($descrizione) ? $descrizione : null, + 'tipo_recorrenza' => 'singolo', + 'data_specifica' => $dataSpecifica, + 'ora_inizio' => $oraInizio, + 'durata_minuti' => $durataMinuti, + 'luogo_indirizzo' => !empty($luogo) ? $luogo : null, + 'uid_esterno' => !empty($uid) ? $uid : null, + ]); + + $imported++; + } catch (\Exception $e) { + Log::warning('Errore import evento ICS: ' . $e->getMessage()); + $errors[] = 'Errore: ' . $e->getMessage(); + } + } + + $message = 'Importati ' . $imported . ' eventi.'; + if ($skipped > 0) { + $message .= ' Saltati ' . $skipped . ' eventi (già presenti o senza nome).'; + } + if (count($errors) > 0) { + return back()->with('error', $message . ' Errori: ' . implode('; ', $errors)); + } + + return redirect('/eventi')->with('success', $message); + } } \ No newline at end of file diff --git a/app/Http/Controllers/GruppoController.php b/app/Http/Controllers/GruppoController.php index bd4c2303..3c969448 100644 --- a/app/Http/Controllers/GruppoController.php +++ b/app/Http/Controllers/GruppoController.php @@ -8,6 +8,7 @@ use App\Models\Gruppo; use App\Models\Individuo; use App\Models\Diocesi; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Log; class GruppoController extends Controller { @@ -300,4 +301,101 @@ class GruppoController extends Controller return redirect()->route('gruppi.index')->with('success', 'Vista eliminata.'); } + + public function import() + { + $this->authorizeWrite('gruppi'); + return view('gruppi.import'); + } + + public function importStore(Request $request) + { + $this->authorizeWrite('gruppi'); + $request->validate([ + 'file' => 'required|file|mimes:csv,txt', + ]); + + $file = $request->file('file'); + $handle = fopen($file->path(), 'r'); + $header = fgetcsv($handle, 1000, ','); + + if ($header === false) { + fclose($handle); + return back()->with('error', 'Il file CSV non è valido.'); + } + + $header = array_map('trim', $header); + $imported = 0; + $skipped = 0; + $errors = []; + $rowNumber = 1; + + while (($row = fgetcsv($handle, 1000, ',')) !== false) { + $rowNumber++; + + if (count($row) < count($header)) { + $skipped++; + continue; + } + + $data = array_combine($header, $row); + + if ($data === false) { + $skipped++; + continue; + } + + $data = array_map('trim', $data); + $nome = $data['nome'] ?? ''; + + if (empty($nome)) { + $skipped++; + continue; + } + + try { + Gruppo::create([ + 'nome' => $nome, + 'descrizione' => $data['descrizione'] ?? null, + 'parent_id' => !empty($data['parent_id']) ? (int) $data['parent_id'] : null, + 'diocesi_id' => !empty($data['diocesi_id']) ? (int) $data['diocesi_id'] : null, + 'indirizzo_incontro' => $data['indirizzo_incontro'] ?? null, + 'cap_incontro' => $data['cap_incontro'] ?? null, + 'città_incontro' => $data['città_incontro'] ?? null, + 'sigla_provincia_incontro' => $data['sigla_provincia_incontro'] ?? null, + ]); + + $imported++; + } catch (\Exception $e) { + Log::warning('Errore import gruppo riga ' . $rowNumber . ': ' . $e->getMessage()); + $errors[] = 'Errore riga ' . $rowNumber . ': ' . $e->getMessage(); + } + } + + fclose($handle); + + $message = 'Importati ' . $imported . ' gruppi.'; + if ($skipped > 0) { + $message .= ' Saltate ' . $skipped . ' righe (vuote o incomplete).'; + } + if (count($errors) > 0) { + return back()->with('error', $message . ' Errori: ' . implode('; ', $errors)); + } + + return redirect()->route('gruppi.index')->with('success', $message); + } + + public function downloadTemplate() + { + $this->authorizeRead('gruppi'); + $headers = ['nome', 'descrizione', 'parent_id', 'diocesi_id', 'indirizzo_incontro', 'cap_incontro', 'città_incontro', 'sigla_provincia_incontro']; + $example = ['Gruppo Giovani', 'Giovani dai 18 ai 30 anni', '', '', 'Via Roma 10', '00100', 'Roma', 'RM']; + + $csv = implode(',', $headers) . "\n" . implode(',', $example); + + return response($csv, 200, [ + 'Content-Type' => 'text/csv', + 'Content-Disposition' => 'attachment; filename="template_gruppi.csv"', + ]); + } } diff --git a/app/Http/Controllers/IndividuoController.php b/app/Http/Controllers/IndividuoController.php index d35471ec..b0021589 100644 --- a/app/Http/Controllers/IndividuoController.php +++ b/app/Http/Controllers/IndividuoController.php @@ -17,7 +17,8 @@ class IndividuoController extends Controller public function index(Request $request) { $this->authorizeRead('individui'); - $individui = Individuo::with('contatti')->orderBy('cognome')->orderBy('nome')->paginate(20); + $perPage = in_array((int) $request->perPage, [10, 20, 25, 50, 100]) ? (int) $request->perPage : 20; + $individui = Individuo::with('contatti')->orderBy('cognome')->orderBy('nome')->paginate($perPage); $vista = null; diff --git a/app/Models/Evento.php b/app/Models/Evento.php index 1719b103..21e64340 100644 --- a/app/Models/Evento.php +++ b/app/Models/Evento.php @@ -30,6 +30,7 @@ class Evento extends Model 'is_incontro_gruppo', 'luogo_indirizzo', 'luogo_url_maps', + 'uid_esterno', ]; protected $casts = [ diff --git a/resources/views/email/index.blade.php b/resources/views/email/index.blade.php index f33483ab..ca2395b9 100644 --- a/resources/views/email/index.blade.php +++ b/resources/views/email/index.blade.php @@ -131,8 +131,29 @@ $currentFolder = $folders->firstWhere('type', $folder); -
@endsection diff --git a/resources/views/eventi/import.blade.php b/resources/views/eventi/import.blade.php new file mode 100644 index 00000000..f50306dd --- /dev/null +++ b/resources/views/eventi/import.blade.php @@ -0,0 +1,55 @@ +@extends('layouts.adminlte') +@section('title', 'Importa Eventi') +@section('page_title', 'Importa Eventi da ICS') + +@section('content') +SUMMARY → Nome evento (obbligatorio)DESCRIPTION → DescrizioneDTSTART → Data/Ora inizioDTEND → DurataLOCATION → LuogoUID → ID esterno (per evitare duplicati)+ + Gli eventi con UID già presente nel sistema verranno saltati automaticamente. +
+Scarica il template CSV per l'importazione:
+ + Scarica Template + +nome (obbligatorio)descrizioneparent_id (ID del gruppo padre)diocesi_id (ID della diocesi)indirizzo_incontrocap_incontrocittà_incontrosigla_provincia_incontro (2 lettere)