final 1.2
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
APP_NAME=Glastree
|
||||
APP_ENV=local
|
||||
APP_KEY=base64:aWz7908H9c7s+it9uMTwb6pyUrpddyMclcuN9Kzv7Ao=
|
||||
APP_DEBUG=true
|
||||
APP_URL=
|
||||
APP_PROTOCOL=https
|
||||
FORCE_HTTPS=true
|
||||
TRUSTED_PROXIES=*
|
||||
|
||||
APP_LOCALE=it
|
||||
APP_FALLBACK_LOCALE=it
|
||||
APP_FAKER_LOCALE=it_IT
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
# APP_MAINTENANCE_STORE=database
|
||||
|
||||
# PHP_CLI_SERVER_WORKERS=4
|
||||
|
||||
BCRYPT_ROUNDS=12
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=glastree
|
||||
DB_USERNAME=glastree
|
||||
DB_PASSWORD=glastree
|
||||
|
||||
SESSION_DRIVER=file
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
CACHE_STORE=database
|
||||
# CACHE_PREFIX=
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=log
|
||||
MAIL_SCHEME=null
|
||||
MAIL_HOST=127.0.0.1
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
@@ -1,2 +1 @@
|
||||
glastree-image.tar
|
||||
.env
|
||||
|
||||
@@ -1,91 +1,61 @@
|
||||
# 🧠 MEMORY.md — Stato Progetto
|
||||
|
||||
## Goal
|
||||
- Implementare 3 feature in Glastree Laravel 13:
|
||||
1. **Page-length selector** su pagine lista (Individui, Eventi, Email)
|
||||
2. **CSV import per gruppi** (stesso pattern import individui)
|
||||
3. **ICS import per eventi** (via sabre/vobject)
|
||||
- Risolvere login non funzionante dopo fresh install (causa: tinker senza escaping + `2>/dev/null`)
|
||||
- Sostituire tinker per creazione admin con PDO prepared statement + SQL file per schema
|
||||
|
||||
## Constraints & Preferences
|
||||
- Laravel 13, PHP 8.4, AdminLTE 4, MySQL/MariaDB
|
||||
- Laravel 13, PHP 8.4, AdminLTE 4, MySQL/MariaDB (install.sql solo MySQL)
|
||||
- Zero placeholder, zero TODO, codice funzionante
|
||||
- Seguire pattern esistenti (individui import, controller conventions, view layouts)
|
||||
- sabre/vobject già installato in composer.json (verificato funzionante in vendor/)
|
||||
- UI in Italiano come da applicazione esistente
|
||||
- Compatibilità cross-DB MySQL ↔ SQLite (fallback migration per SQLite)
|
||||
|
||||
## Verifica Finale — Tutto OK ✅
|
||||
## Progress
|
||||
### Done
|
||||
- **Creazione `database/install.sql`** — schema MySQL completo + seed data (~1059 righe):
|
||||
- Tutte le 45 tabelle nello stato finale (consolidate da 58 migration)
|
||||
- Seed: 119 diocesi, 54 comuni, 8 tipologie documenti, 8 tipologie eventi, 8 ruoli, 23 permessi Spatie, 2 ruoli Spatie, 3 role preset, 5 email folders, 1 riga app_settings
|
||||
- **NON contiene l'admin** (creato via PDO da install.php)
|
||||
- `SET FOREIGN_KEY_CHECKS=0` per ordine tabelle, riattivato alla fine
|
||||
- **Riscritto `install.php`** — eliminata dipendenza da tinker per la creazione admin:
|
||||
- Aggiunta funzione `createAdminUser()`: PDO prepared statement + `password_hash()` + Spatie role assignment
|
||||
- Fresh Install Apache: importa `install.sql` via `mysql` CLI, poi crea admin via PDO
|
||||
- Fresh Install Docker: copia `install.sql` nel container, importa via mysql CLI, crea admin via script PHP temporaneo con `var_export()` (safe)
|
||||
- Restore: crea admin via PDO se non esiste
|
||||
- Fallback SQLite: mantiene `php artisan migrate --seed` + tinker ma con `var_export()` per escaping
|
||||
- Step 5 rinominato da "MIGRATION E SEED" a "SCHEMA DATABASE E DATI BASE"
|
||||
- Step 6 usa PDO invece di tinker (MySQL)
|
||||
- **Unified MySQL setup**: singolo tentativo PDO con `dbname` nel DSN — se fallisce (1044/1045/1049), passa automaticamente in root-mode: chiede password root, crea database + utente + GRANT ALL su `localhost` e `%`
|
||||
- **Sostituito loop 30-retry** con connessione singola PDO (timeout 5s)
|
||||
- **Fix parse error**: `\$adminName` → `$adminName` in `var_export()` nel ramo SQLite
|
||||
- **Fix composer**: rimosso `sudo -u www-data` da composer (causa git dubious ownership + permission denied), aggiunto `COMPOSER_ROOT_VERSION=dev-main`; `vendor` aggiunto al chown finale
|
||||
|
||||
| Controllo | Risultato |
|
||||
|-----------|-----------|
|
||||
| Syntax PHP tutti i file modificati | ✅ Nessun errore |
|
||||
| Bilanciamento `{}` JS in tutte le views | ✅ Tutte bilanciate |
|
||||
| Rotta `gruppi.import` GET | ✅ Registrata |
|
||||
| Rotta `gruppi.import.store` POST | ✅ Registrata |
|
||||
| Rotta `gruppi.template` GET | ✅ Registrata |
|
||||
| Rotta `eventi.import` GET | ✅ Registrata |
|
||||
| Rotta `eventi.import.store` POST | ✅ Registrata |
|
||||
| View `gruppi/import.blade.php` | ✅ Creata |
|
||||
| View `eventi/import.blade.php` | ✅ Creata |
|
||||
| sabre/vobject loadabile | ✅ `Sabre\VObject\Reader` disponibile |
|
||||
### In Progress
|
||||
- *(nessuno)*
|
||||
|
||||
## Modifiche Effettuate
|
||||
|
||||
### 1. Page-length selector (3 pagine)
|
||||
**File modificati:**
|
||||
- `app/Http/Controllers/IndividuoController.php:20` — `paginate(20)` → `paginate($perPage)` con clamp `[10,20,25,50,100]`, default 20
|
||||
- `app/Http/Controllers/EventoController.php:48-49` — stessa logica, default 20
|
||||
- `app/Http/Controllers/EmailController.php:61-62` — stessa logica, default 20
|
||||
- `resources/views/individui/index.blade.php` — dropdown `<select name="perPage">` in card-footer con hidden fields per preservare altri query params
|
||||
- `resources/views/eventi/index.blade.php` — stesso dropdown in card-footer
|
||||
- `resources/views/email/index.blade.php` — stesso dropdown in card-footer
|
||||
|
||||
### 2. CSV Import Gruppi (nuovo)
|
||||
**File modificati:**
|
||||
- `app/Http/Controllers/GruppoController.php` — aggiunti 3 metodi:
|
||||
- `import()`: GET, mostra form upload (authorizeWrite)
|
||||
- `importStore()`: POST, validate file CSV, parsing fault-tolerant (skip righe vuote, log errori), crea Gruppo::create() con `nome,descrizione,parent_id,diocesi_id,indirizzo_incontro,cap_incontro,città_incontro,sigla_provincia_incontro`
|
||||
- `downloadTemplate()`: GET, restituisce CSV template con header + esempio
|
||||
- `resources/views/gruppi/import.blade.php` — form upload + template download + colonna info campi
|
||||
- `resources/views/gruppi/index.blade.php` — bottone "Importa" in card-tools
|
||||
- `routes/web.php` — `gruppi/import` (GET/POST), `gruppi/template` (GET), tutte prima di `Route::resource('gruppi')`
|
||||
|
||||
### 3. ICS Import Eventi (nuovo)
|
||||
**File modificati:**
|
||||
- `app/Models/Evento.php:34` — aggiunto `'uid_esterno'` a `$fillable` (colonna esisteva in DB ma mancava dal model — potenziale bug risolto)
|
||||
- `app/Http/Controllers/EventoController.php` — aggiunti 2 metodi:
|
||||
- `importIcs()`: GET, mostra form upload (authorizeWrite)
|
||||
- `importIcsStore()`: POST, validate file ICS, usa `Sabre\VObject\Reader::read()` per parsare, estrae VEVENT, mapping:
|
||||
- `SUMMARY` → `nome_evento`
|
||||
- `DESCRIPTION` → `descrizione`
|
||||
- `DTSTART` → `data_specifica` + `ora_inizio`
|
||||
- `DTEND` → `durata_minuti` (differenza in minuti)
|
||||
- `LOCATION` → `luogo_indirizzo`
|
||||
- `UID` → `uid_esterno` (per dedup)
|
||||
- RRULE: import come `tipo_recorrenza: 'singolo'` con prima occorrenza (no espansione)
|
||||
- Dedup: skip se `uid_esterno` già presente
|
||||
- `resources/views/eventi/import.blade.php` — form upload + colonna info campi
|
||||
- `resources/views/eventi/index.blade.php` — bottone "Importa ICS" in card-tools
|
||||
- `routes/web.php` — `eventi/import` (GET/POST), prima di `Route::resource('eventi')`
|
||||
|
||||
## Bug Fix Trovato
|
||||
- **`Evento::$fillable` mancava `uid_esterno`** — la colonna esisteva in DB (aggiunta da migration `2026_06_02_000002_add_uid_esterno_to_eventi_table.php`) ma non era in `$fillable`, quindi mass-assignment la ignorava silenziosamente. Risolto.
|
||||
### Blocked
|
||||
- *(nessuno)*
|
||||
|
||||
## Key Decisions
|
||||
- **perPage clamping**: soli valori `[10,20,25,50,100]`, fallback a default 20 su valore non valido
|
||||
- **CSV gruppi segue pattern individui**: stesso approccio fault-tolerant (skip righe incomplete, log errori, riepilogo finale)
|
||||
- **ICS senza espansione RRULE**: eventi ricorrenti importati come evento singolo con data della prima occorrenza
|
||||
- **Dedup via uid_esterno**: lookup prima della creazione, skip se già presente
|
||||
- **PDO > tinker**: `createAdminUser()` usa PDO prepared statement con `password_hash()`, niente interpolazione PHP in stringhe shell → zero escaping issues
|
||||
- **`install.sql` > migration sequenziali**: unico file SQL per fresh install, più veloce e debuggabile (si può lanciare `mysql < install.sql` a mano)
|
||||
- **`var_export()` per tinker fallback**: nei rari casi SQLite, si usa `var_export()` che produce stringhe PHP valide con escaping automatico
|
||||
- **`2>/dev/null` rimosso** dal MySQL main path (non serve più, non c'è tinker)
|
||||
- **Docker MySQL credenziali**: hardcoded `mysql:glastree:secret:glastree` (dal docker-compose.mysql.yml)
|
||||
- **Composer senza sudo**: `COMPOSER_ROOT_VERSION=dev-main` per evitare git dubious ownership; composer eseguito come utente corrente (root), non più come www-data — dopo, `chown -R www-data vendor` per permessi lettura webserver
|
||||
- **Unified MySQL setup**: PDO connect con `dbname` nel DSN → se fallisce 1044/1045/1049, root-mode: crea DB + utente + GRANT ALL su `localhost` e `%` + FLUSH
|
||||
|
||||
## Next Steps
|
||||
- *(nessuno)*
|
||||
|
||||
## Critical Context
|
||||
- `install.sql` è solo per MySQL. SQLite usa `php artisan migrate --seed` + `DatabaseSeeder`
|
||||
- `DatabaseSeeder` crea ancora admin `admin@glastree.local / password` ma per MySQL viene cancellato dal PDO script
|
||||
- `createAdminUser()` setta `is_admin=1`, `status='active'`, `permissions` completo, `role_preset_id=1`, e Spatie `model_has_roles` con role_id=1
|
||||
- `password_hash($pass, PASSWORD_BCRYPT)` produce hash `$2y$...` compatibile con `Hash::check()` di Laravel
|
||||
- Composer ora eseguito come utente corrente (root) con `COMPOSER_ROOT_VERSION=dev-main` per evitare git dubious ownership e permission denied su `vendor/`
|
||||
- `vendor/` aggiunto al `chown` finale in entrambi i modi (Apache e Restore) per garantire leggibilità da www-data
|
||||
- `ensureMysqlUserAndDb()` sanitizza input (rimuove `' " \0 \`) prima di passarli a SQL exec
|
||||
|
||||
## Relevant Files
|
||||
- `app/Http/Controllers/IndividuoController.php` — perPage in index()
|
||||
- `app/Http/Controllers/EventoController.php` — perPage in index() + importIcs()/importIcsStore()
|
||||
- `app/Http/Controllers/GruppoController.php` — import()/importStore()/downloadTemplate()
|
||||
- `app/Http/Controllers/EmailController.php` — perPage in index()
|
||||
- `app/Models/Evento.php` — uid_esterno in $fillable
|
||||
- `resources/views/individui/index.blade.php` — perPage dropdown
|
||||
- `resources/views/eventi/index.blade.php` — perPage dropdown + ICS import button
|
||||
- `resources/views/eventi/import.blade.php` — ICS import form (NEW)
|
||||
- `resources/views/gruppi/index.blade.php` — CSV import button
|
||||
- `resources/views/gruppi/import.blade.php` — CSV import form (NEW)
|
||||
- `resources/views/email/index.blade.php` — perPage dropdown
|
||||
- `routes/web.php` — nuove route gruppi/eventi import
|
||||
- `database/install.sql`: NEW — schema MySQL completo + seed data (1059 righe)
|
||||
- `install.php`: MODIFIED — `createAdminUser()` + `ensureMysqlUserAndDb()` + composer fix + auto-create MySQL user
|
||||
|
||||
@@ -59,8 +59,7 @@ class EmailController extends Controller
|
||||
|
||||
$query->orderBy($sortField === 'sent_at' ? 'received_at' : $sortField, $sortDir);
|
||||
|
||||
$perPage = in_array((int) $request->perPage, [10, 20, 25, 50, 100]) ? (int) $request->perPage : 20;
|
||||
$messages = $query->paginate($perPage)->withQueryString();
|
||||
$messages = $query->paginate(20)->withQueryString();
|
||||
$folders = EmailFolder::orderBy('sort_order')->get();
|
||||
$mailingLists = MailingList::where('attiva', true)->get();
|
||||
$gruppi = Gruppo::orderBy('nome')->get();
|
||||
|
||||
@@ -8,8 +8,6 @@ use App\Models\Individuo;
|
||||
use App\Models\TipologiaEvento;
|
||||
use App\Services\IcsExportService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class EventoController extends Controller
|
||||
@@ -45,8 +43,7 @@ class EventoController extends Controller
|
||||
$query->orderByDesc('created_at');
|
||||
}
|
||||
|
||||
$perPage = in_array((int) $request->perPage, [10, 20, 25, 50, 100]) ? (int) $request->perPage : 20;
|
||||
$eventi = $query->paginate($perPage);
|
||||
$eventi = $query->paginate(20);
|
||||
$gruppi = Gruppo::orderBy('nome')->get();
|
||||
|
||||
return view('eventi.index', compact('eventi', 'gruppi'));
|
||||
@@ -480,101 +477,4 @@ 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);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ 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
|
||||
{
|
||||
@@ -301,101 +300,4 @@ 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"',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,7 @@ class IndividuoController extends Controller
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->authorizeRead('individui');
|
||||
$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);
|
||||
$individui = Individuo::with('contatti')->orderBy('cognome')->orderBy('nome')->paginate(20);
|
||||
|
||||
$vista = null;
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ class Evento extends Model
|
||||
'is_incontro_gruppo',
|
||||
'luogo_indirizzo',
|
||||
'luogo_url_maps',
|
||||
'uid_esterno',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
|
||||
@@ -197,7 +197,6 @@ CREATE TABLE `gruppo_individuo` (
|
||||
`gruppo_id` bigint unsigned NOT NULL,
|
||||
`individuo_id` bigint unsigned NOT NULL,
|
||||
`ruolo_ids` json DEFAULT NULL,
|
||||
`ruolo_nel_gruppo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`data_adesione` date DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
@@ -488,7 +487,7 @@ CREATE TABLE `documenti` (
|
||||
`file_path` varchar(255) NOT NULL,
|
||||
`tipo` varchar(255) DEFAULT NULL,
|
||||
`tipologia` enum('avatar','galleria','documento','statuto','altro','programma','locandina','email_attachment') DEFAULT 'documento',
|
||||
`visibilita` enum('pubblico','gruppo','individuo','associazione','federazione','evento') DEFAULT 'gruppo',
|
||||
`visibilita` enum('pubblico','gruppo','individuo','associazione','federazione') DEFAULT NULL,
|
||||
`visibilita_target_id` bigint unsigned DEFAULT NULL,
|
||||
`visibilita_target_type` varchar(255) DEFAULT NULL,
|
||||
`mime_type` varchar(255) DEFAULT NULL,
|
||||
@@ -1036,13 +1035,13 @@ SELECT 2, `id` FROM `permissions` WHERE `name` IN (
|
||||
-- Role presets
|
||||
INSERT INTO `role_presets` (`name`, `description`, `permissions`, `created_at`, `updated_at`) VALUES
|
||||
('admin', 'Amministratore con accesso completo a tutti i moduli',
|
||||
'{\"individui\":2,\"gruppi\":2,\"eventi\":2,\"documenti\":2,\"mailing\":2,\"viste\":2,\"report\":2,\"settings\":2}',
|
||||
'{\"individui\":2,\"gruppi\":2,\"eventi\":2,\"documenti\":2,\"mailing\":2,\"viste\":2}',
|
||||
NOW(), NOW()),
|
||||
('operatore', 'Operatore con accesso completo a individui/gruppi/eventi, sola lettura per documenti/mailing',
|
||||
'{\"individui\":2,\"gruppi\":2,\"eventi\":2,\"documenti\":1,\"mailing\":1,\"viste\":1,\"report\":1,\"settings\":1}',
|
||||
'{\"individui\":2,\"gruppi\":2,\"eventi\":2,\"documenti\":1,\"mailing\":1,\"viste\":1}',
|
||||
NOW(), NOW()),
|
||||
('lettura', 'Sola lettura su tutti i moduli',
|
||||
'{\"individui\":1,\"gruppi\":1,\"eventi\":1,\"documenti\":1,\"mailing\":1,\"viste\":1,\"report\":1,\"settings\":1}',
|
||||
'{\"individui\":1,\"gruppi\":1,\"eventi\":1,\"documenti\":1,\"mailing\":1,\"viste\":1}',
|
||||
NOW(), NOW());
|
||||
|
||||
-- Email folders (system)
|
||||
|
||||
+13
-50
@@ -131,17 +131,11 @@ function createAdminUser(
|
||||
|
||||
$userId = $pdo->lastInsertId();
|
||||
|
||||
// Assegna ruolo Spatie 'admin' (lookup dinamico per nome)
|
||||
$stmt = $pdo->prepare("SELECT id FROM roles WHERE name = 'admin' LIMIT 1");
|
||||
$stmt->execute();
|
||||
$adminRoleId = (int) $stmt->fetchColumn();
|
||||
if ($adminRoleId === 0) {
|
||||
throw new \Exception('Ruolo Spatie admin non trovato');
|
||||
}
|
||||
// Assegna ruolo Spatie 'admin' (role_id = 1)
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO model_has_roles (role_id, model_type, model_id) VALUES (?, ?, ?)"
|
||||
"INSERT INTO model_has_roles (role_id, model_type, model_id) VALUES (1, ?, ?)"
|
||||
);
|
||||
$stmt->execute([$adminRoleId, 'App\\Models\\User', $userId]);
|
||||
$stmt->execute(['App\\Models\\User', $userId]);
|
||||
|
||||
return true;
|
||||
} catch (\PDOException $e) {
|
||||
@@ -185,10 +179,6 @@ $appName = askRequired('Nome del sito/app (es. Glastree MyChurch)');
|
||||
$adminName = askRequired('Nome del primo utente amministratore');
|
||||
$adminEmail = askRequired('Email dell\'amministratore');
|
||||
$adminPass = askRequired('Password per l\'amministratore');
|
||||
$adminPass2 = askRequired('Conferma password amministratore');
|
||||
if ($adminPass !== $adminPass2) {
|
||||
fail('Le password non corrispondono');
|
||||
}
|
||||
$appUrl = askRequired('URL pubblico del sito (es. https://glastree.esempio.it)');
|
||||
|
||||
// ── DB params (Apache / Restore) ─────────────────────
|
||||
@@ -274,9 +264,7 @@ if ($mode === 'Fresh Install su Apache (LAMP tradizionale)') {
|
||||
}
|
||||
|
||||
// APP_KEY
|
||||
if (!run("{$sudo}php artisan key:generate --force", 'Generazione APP_KEY...')) {
|
||||
fail('Generazione APP_KEY fallita');
|
||||
}
|
||||
runCapture("{$sudo}php artisan key:generate --force");
|
||||
ok('APP_KEY generata');
|
||||
|
||||
// Config
|
||||
@@ -385,9 +373,7 @@ if ($mode === 'Fresh Install su Apache (LAMP tradizionale)') {
|
||||
println(color(' 3/7 — INSTALLAZIONE DIPENDENZE', 'yellow'));
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
|
||||
if (!run("COMPOSER_ROOT_VERSION=dev-main composer install --no-dev --optimize-autoloader", 'Installazione dipendenze Composer...')) {
|
||||
fail('Installazione dipendenze Composer fallita');
|
||||
}
|
||||
run("COMPOSER_ROOT_VERSION=dev-main composer install --no-dev --optimize-autoloader", 'Installazione dipendenze Composer...');
|
||||
ok('Dipendenze PHP installate');
|
||||
|
||||
// ── Step 4: Package discovery ────────────────────
|
||||
@@ -411,7 +397,7 @@ if ($mode === 'Fresh Install su Apache (LAMP tradizionale)') {
|
||||
}
|
||||
|
||||
if ($dbType === 'mysql') {
|
||||
$importCmd = "MYSQL_PWD=" . escapeshellarg($dbPass) . " mysql -h {$dbHost} -P {$dbPort} -u {$dbUser} {$dbName} < {$sqlFile}";
|
||||
$importCmd = "mysql -h {$dbHost} -P {$dbPort} -u {$dbUser} -p{$dbPass} {$dbName} < {$sqlFile}";
|
||||
if (!run($importCmd, 'Importazione struttura database e dati base...')) {
|
||||
fail("Importazione SQL fallita. Verifica il file database/install.sql e le credenziali.");
|
||||
}
|
||||
@@ -516,12 +502,6 @@ if ($mode === 'Fresh Install via Docker') {
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
|
||||
checkCmd('docker');
|
||||
$dockerComposeCmd = 'docker compose';
|
||||
if (trim(shell_exec('command -v docker-compose 2>/dev/null') ?: '') !== '') {
|
||||
$dockerComposeCmd = 'docker-compose';
|
||||
} elseif (trim(shell_exec('command -v docker 2>/dev/null && docker compose version 2>/dev/null') ?: '') === '') {
|
||||
fail('Docker Compose (v1 o v2) non trovato.');
|
||||
}
|
||||
|
||||
println();
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
@@ -538,13 +518,10 @@ if ($mode === 'Fresh Install via Docker') {
|
||||
println(color(' INSTALLAZIONE DOCKER', 'yellow'));
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
|
||||
if (!run("{$dockerComposeCmd} -f {$composeFile} build", 'Build immagine Docker...')) {
|
||||
if (!run("docker compose -f {$composeFile} build", 'Build immagine Docker...')) {
|
||||
fail('Build fallita');
|
||||
}
|
||||
|
||||
// Genera APP_KEY per il container Docker
|
||||
$appKey = 'base64:' . base64_encode(random_bytes(32));
|
||||
|
||||
// Override compose con variabili d'ambiente
|
||||
$override = [
|
||||
'services:',
|
||||
@@ -554,12 +531,11 @@ if ($mode === 'Fresh Install via Docker') {
|
||||
" - APP_URL={$appUrl}",
|
||||
' - APP_ENV=production',
|
||||
' - APP_DEBUG=false',
|
||||
" - APP_KEY={$appKey}",
|
||||
];
|
||||
file_put_contents('docker-compose.override.yml', implode(PHP_EOL, $override) . PHP_EOL);
|
||||
|
||||
if (!run(
|
||||
"{$dockerComposeCmd} -f {$composeFile} -f docker-compose.override.yml up -d",
|
||||
"docker compose -f {$composeFile} -f docker-compose.override.yml up -d",
|
||||
'Avvio container...'
|
||||
)) {
|
||||
@unlink('docker-compose.override.yml');
|
||||
@@ -568,7 +544,7 @@ if ($mode === 'Fresh Install via Docker') {
|
||||
|
||||
// Attendi container pronto
|
||||
info('Attendo che il container sia pronto...');
|
||||
$containerId = runCapture("{$dockerComposeCmd} -f {$composeFile} ps -q glastree 2>/dev/null");
|
||||
$containerId = runCapture("docker compose -f {$composeFile} ps -q glastree 2>/dev/null");
|
||||
$adminCreated = false;
|
||||
|
||||
if ($containerId !== '') {
|
||||
@@ -584,11 +560,6 @@ if ($mode === 'Fresh Install via Docker') {
|
||||
sleep(2);
|
||||
}
|
||||
|
||||
if ($status !== 'running') {
|
||||
@unlink('docker-compose.override.yml');
|
||||
fail("Container non pronto dopo 60 secondi (stato: {$status}). Controlla i log con: {$dockerComposeCmd} -f {$composeFile} logs");
|
||||
}
|
||||
|
||||
// Import schema SQL nel container
|
||||
$sqlFile = $scriptDir . '/database/install.sql';
|
||||
if (file_exists($sqlFile) && $dockerDb === 'mysql') {
|
||||
@@ -634,11 +605,7 @@ if ($mode === 'Fresh Install via Docker') {
|
||||
\$pdo->prepare("DELETE FROM users WHERE email = ?")->execute([{$emailExp}]);
|
||||
\$pdo->prepare("INSERT INTO users (name, email, password, is_admin, status, permissions, role_preset_id, created_at, updated_at) VALUES (?, ?, ?, 1, 'active', ?, 1, NOW(), NOW())")->execute([{$nameExp}, {$emailExp}, {$hashExp}, '{$perm}']);
|
||||
\$uid = \$pdo->lastInsertId();
|
||||
\$stmt = \$pdo->prepare("SELECT id FROM roles WHERE name = 'admin' LIMIT 1");
|
||||
\$stmt->execute();
|
||||
\$adminRoleId = (int) \$stmt->fetchColumn();
|
||||
if (\$adminRoleId === 0) { echo 'ERR:admin_role_not_found'; exit(1); }
|
||||
\$pdo->prepare("INSERT INTO model_has_roles (role_id, model_type, model_id) VALUES (?, ?, ?)")->execute([\$adminRoleId, 'App\\\\Models\\\\User', \$uid]);
|
||||
\$pdo->prepare("INSERT INTO model_has_roles (role_id, model_type, model_id) VALUES (1, ?, ?)")->execute(['App\\\\Models\\\\User', \$uid]);
|
||||
echo "OK:" . \$uid;
|
||||
PHP;
|
||||
file_put_contents($adminScriptPath, $adminPhp);
|
||||
@@ -768,9 +735,7 @@ if ($mode === 'Restore da Backup (Apache)') {
|
||||
file_put_contents('.env', $dotenv);
|
||||
|
||||
// Generate APP_KEY
|
||||
if (!run("{$sudo}php artisan key:generate --force", 'Generazione APP_KEY...')) {
|
||||
fail('Generazione APP_KEY fallita');
|
||||
}
|
||||
runCapture("{$sudo}php artisan key:generate --force");
|
||||
ok('APP_KEY rigenerata');
|
||||
|
||||
// Import database
|
||||
@@ -778,7 +743,7 @@ if ($mode === 'Restore da Backup (Apache)') {
|
||||
$sqlFile = "{$tmpDir}/database.sql";
|
||||
if (file_exists($sqlFile)) {
|
||||
info("Importazione database MySQL: {$sqlFile}");
|
||||
$cmd = "MYSQL_PWD=" . escapeshellarg($dbPass) . " mysql -h {$dbHost} -P {$dbPort} -u {$dbUser} {$dbName} < {$sqlFile}";
|
||||
$cmd = "mysql -h {$dbHost} -P {$dbPort} -u {$dbUser} -p{$dbPass} {$dbName} < {$sqlFile}";
|
||||
if (run($cmd, null)) {
|
||||
ok('Database importato');
|
||||
} else {
|
||||
@@ -800,9 +765,7 @@ if ($mode === 'Restore da Backup (Apache)') {
|
||||
}
|
||||
|
||||
// Composer
|
||||
if (!run("COMPOSER_ROOT_VERSION=dev-main composer install --no-dev --optimize-autoloader", 'Installazione dipendenze Composer...')) {
|
||||
fail('Installazione dipendenze Composer fallita');
|
||||
}
|
||||
run("COMPOSER_ROOT_VERSION=dev-main composer install --no-dev --optimize-autoloader", 'Installazione dipendenze Composer...');
|
||||
runCapture("{$sudo}php artisan package:discover --ansi 2>/dev/null");
|
||||
|
||||
// Storage link + cache
|
||||
|
||||
@@ -131,29 +131,8 @@ $currentFolder = $folders->firstWhere('type', $folder);
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<form method="GET" class="form-inline" id="perPageForm">
|
||||
<label class="mr-2 small">Righe per pagina:</label>
|
||||
<select name="perPage" class="form-control form-control-sm" onchange="this.form.submit()" style="width: auto;">
|
||||
@foreach([10, 20, 25, 50, 100] as $p)
|
||||
<option value="{{ $p }}" {{ (int) request('perPage', 20) === $p ? 'selected' : '' }}>{{ $p }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@foreach(request()->except(['perPage', 'page']) as $key => $value)
|
||||
@if(is_array($value))
|
||||
@foreach($value as $v)
|
||||
<input type="hidden" name="{{ $key }}[]" value="{{ $v }}">
|
||||
@endforeach
|
||||
@else
|
||||
<input type="hidden" name="{{ $key }}" value="{{ $value }}">
|
||||
@endif
|
||||
@endforeach
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
{{ $messages->links('vendor.pagination.simple-bootstrap-4') }}
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
{{ $messages->withQueryString()->links('vendor.pagination.simple-bootstrap-4') }}
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
@extends('layouts.adminlte')
|
||||
@section('title', 'Importa Eventi')
|
||||
@section('page_title', 'Importa Eventi da ICS')
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-upload mr-2"></i>Carica file ICS</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ route('eventi.import.store') }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<div class="form-group">
|
||||
<label for="file">Seleziona file ICS *</label>
|
||||
<input type="file" name="file" id="file" class="form-control" accept=".ics" required>
|
||||
<small class="form-text text-muted">
|
||||
Il file deve essere in formato ICS (iCalendar).
|
||||
</small>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-upload mr-1"></i> Importa
|
||||
</button>
|
||||
<a href="/eventi" class="btn btn-secondary ml-2">
|
||||
Annulla
|
||||
</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-info-circle mr-2"></i>Informazioni</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6>Campi importati:</h6>
|
||||
<ul>
|
||||
<li><code>SUMMARY</code> → Nome evento (obbligatorio)</li>
|
||||
<li><code>DESCRIPTION</code> → Descrizione</li>
|
||||
<li><code>DTSTART</code> → Data/Ora inizio</li>
|
||||
<li><code>DTEND</code> → Durata</li>
|
||||
<li><code>LOCATION</code> → Luogo</li>
|
||||
<li><code>UID</code> → ID esterno (per evitare duplicati)</li>
|
||||
</ul>
|
||||
<p class="text-muted small mt-2">
|
||||
<i class="fas fa-info-circle mr-1"></i>
|
||||
Gli eventi con UID già presente nel sistema verranno saltati automaticamente.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -34,9 +34,6 @@ $canDeleteEventi = Auth::user()->canDelete('eventi');
|
||||
</div>
|
||||
@endif
|
||||
@if($canWriteEventi)
|
||||
<a href="/eventi/import" class="btn btn-sm btn-warning mr-2">
|
||||
<i class="fas fa-file-import mr-1"></i> Importa ICS
|
||||
</a>
|
||||
<a href="/eventi/create" class="btn btn-xs btn-success">
|
||||
<i class="fas fa-plus mr-1"></i> Nuovo Evento
|
||||
</a>
|
||||
@@ -250,29 +247,8 @@ $canDeleteEventi = Auth::user()->canDelete('eventi');
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<form method="GET" class="form-inline" id="perPageForm">
|
||||
<label class="mr-2 small">Righe per pagina:</label>
|
||||
<select name="perPage" class="form-control form-control-sm" onchange="this.form.submit()" style="width: auto;">
|
||||
@foreach([10, 20, 25, 50, 100] as $p)
|
||||
<option value="{{ $p }}" {{ (int) request('perPage', 20) === $p ? 'selected' : '' }}>{{ $p }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@foreach(request()->except(['perPage', 'page']) as $key => $value)
|
||||
@if(is_array($value))
|
||||
@foreach($value as $v)
|
||||
<input type="hidden" name="{{ $key }}[]" value="{{ $v }}">
|
||||
@endforeach
|
||||
@else
|
||||
<input type="hidden" name="{{ $key }}" value="{{ $value }}">
|
||||
@endif
|
||||
@endforeach
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
{{ $eventi->links() }}
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
{{ $eventi->withQueryString()->links() }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
@extends('layouts.adminlte')
|
||||
@section('title', 'Importa Gruppi')
|
||||
@section('page_title', 'Importa Gruppi da CSV')
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-upload mr-2"></i>Carica file CSV</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ route('gruppi.import.store') }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<div class="form-group">
|
||||
<label for="file">Seleziona file CSV *</label>
|
||||
<input type="file" name="file" id="file" class="form-control" accept=".csv,.txt" required>
|
||||
<small class="form-text text-muted">
|
||||
Il file deve essere in formato CSV con separatore virgola.
|
||||
</small>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-upload mr-1"></i> Importa
|
||||
</button>
|
||||
<a href="{{ route('gruppi.index') }}" class="btn btn-secondary ml-2">
|
||||
Annulla
|
||||
</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header bg-info">
|
||||
<h3 class="card-title text-white"><i class="fas fa-download mr-2"></i>Template</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Scarica il template CSV per l'importazione:</p>
|
||||
<a href="{{ route('gruppi.template') }}" class="btn btn-success btn-block">
|
||||
<i class="fas fa-file-csv mr-1"></i> Scarica Template
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-info-circle mr-2"></i>Formato CSV</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6>Campi:</h6>
|
||||
<ul>
|
||||
<li><code>nome</code> (obbligatorio)</li>
|
||||
<li><code>descrizione</code></li>
|
||||
<li><code>parent_id</code> (ID del gruppo padre)</li>
|
||||
<li><code>diocesi_id</code> (ID della diocesi)</li>
|
||||
<li><code>indirizzo_incontro</code></li>
|
||||
<li><code>cap_incontro</code></li>
|
||||
<li><code>città_incontro</code></li>
|
||||
<li><code>sigla_provincia_incontro</code> (2 lettere)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -55,9 +55,6 @@ $visibleColumnsJson = json_encode($visibleColumns);
|
||||
<i class="fas fa-save mr-1"></i> Salva Vista
|
||||
</button>
|
||||
@if($canWriteGruppi)
|
||||
<a href="{{ route('gruppi.import') }}" class="btn btn-warning btn-sm mr-1">
|
||||
<i class="fas fa-file-import mr-1"></i> Importa
|
||||
</a>
|
||||
<a href="{{ route('gruppi.create') }}" class="btn btn-success btn-sm">
|
||||
<i class="fas fa-plus"></i> Nuovo Gruppo
|
||||
</a>
|
||||
|
||||
@@ -220,30 +220,9 @@ $canDeleteIndividui = Auth::user()->canDelete('individui');
|
||||
@endif
|
||||
</div>
|
||||
@if($individui->count() > 0)
|
||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<form method="GET" class="form-inline" id="perPageForm">
|
||||
<label class="mr-2 small">Righe per pagina:</label>
|
||||
<select name="perPage" class="form-control form-control-sm" onchange="this.form.submit()" style="width: auto;">
|
||||
@foreach([10, 20, 25, 50, 100] as $p)
|
||||
<option value="{{ $p }}" {{ (int) request('perPage', 20) === $p ? 'selected' : '' }}>{{ $p }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@foreach(request()->except(['perPage', 'page']) as $key => $value)
|
||||
@if(is_array($value))
|
||||
@foreach($value as $v)
|
||||
<input type="hidden" name="{{ $key }}[]" value="{{ $v }}">
|
||||
@endforeach
|
||||
@else
|
||||
<input type="hidden" name="{{ $key }}" value="{{ $value }}">
|
||||
@endif
|
||||
@endforeach
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<div class="card-footer">
|
||||
{{ $individui->links() }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
|
||||
@@ -87,9 +87,6 @@ Route::patch('gruppi/{gruppo}/membri/{individuo}', [GruppoMembroController::clas
|
||||
Route::delete('gruppi/{gruppo}/membri/{individuo}', [GruppoMembroController::class, 'destroy'])->middleware('auth');
|
||||
|
||||
// Gruppi specific routes (MUST be before resource route)
|
||||
Route::get('gruppi/import', [GruppoController::class, 'import'])->middleware('auth')->name('gruppi.import');
|
||||
Route::post('gruppi/import', [GruppoController::class, 'importStore'])->middleware('auth')->name('gruppi.import.store');
|
||||
Route::get('gruppi/template', [GruppoController::class, 'downloadTemplate'])->middleware('auth')->name('gruppi.template');
|
||||
Route::get('gruppi/all', [GruppoController::class, 'allGruppi'])->middleware('auth');
|
||||
Route::get('gruppi/{gruppo}/individui-email', [GruppoController::class, 'individuiEmail'])->middleware('auth');
|
||||
|
||||
@@ -100,8 +97,6 @@ Route::delete('gruppi/{gruppo}/avatar', [\App\Http\Controllers\GruppoAvatarContr
|
||||
Route::resource('gruppi', GruppoController::class)->middleware('auth');
|
||||
Route::get('eventi/calendar', [EventoController::class, 'calendar'])->name('eventi.calendar')->middleware('auth');
|
||||
Route::get('eventi/calendar/events', [EventoController::class, 'calendarEvents'])->name('eventi.calendar.events')->middleware('auth');
|
||||
Route::get('eventi/import', [EventoController::class, 'importIcs'])->middleware('auth')->name('eventi.import');
|
||||
Route::post('eventi/import', [EventoController::class, 'importIcsStore'])->middleware('auth')->name('eventi.import.store');
|
||||
Route::get('eventi/{evento}/export-ics', [EventoController::class, 'exportIcs'])->name('eventi.export-ics')->middleware('auth');
|
||||
Route::post('eventi/export-ics', [EventoController::class, 'exportSelectedIcs'])->name('eventi.export-selected-ics')->middleware('auth');
|
||||
Route::post('eventi/mass-elimina', [EventoController::class, 'massElimina'])->middleware('auth');
|
||||
|
||||
Reference in New Issue
Block a user