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
|
glastree-image.tar
|
||||||
.env
|
|
||||||
|
|||||||
@@ -1,91 +1,61 @@
|
|||||||
# 🧠 MEMORY.md — Stato Progetto
|
# 🧠 MEMORY.md — Stato Progetto
|
||||||
|
|
||||||
## Goal
|
## Goal
|
||||||
- Implementare 3 feature in Glastree Laravel 13:
|
- Risolvere login non funzionante dopo fresh install (causa: tinker senza escaping + `2>/dev/null`)
|
||||||
1. **Page-length selector** su pagine lista (Individui, Eventi, Email)
|
- Sostituire tinker per creazione admin con PDO prepared statement + SQL file per schema
|
||||||
2. **CSV import per gruppi** (stesso pattern import individui)
|
|
||||||
3. **ICS import per eventi** (via sabre/vobject)
|
|
||||||
|
|
||||||
## Constraints & Preferences
|
## 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
|
- Zero placeholder, zero TODO, codice funzionante
|
||||||
- Seguire pattern esistenti (individui import, controller conventions, view layouts)
|
- Compatibilità cross-DB MySQL ↔ SQLite (fallback migration per SQLite)
|
||||||
- sabre/vobject già installato in composer.json (verificato funzionante in vendor/)
|
|
||||||
- UI in Italiano come da applicazione esistente
|
|
||||||
|
|
||||||
## 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 |
|
### In Progress
|
||||||
|-----------|-----------|
|
- *(nessuno)*
|
||||||
| 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 |
|
|
||||||
|
|
||||||
## Modifiche Effettuate
|
### Blocked
|
||||||
|
- *(nessuno)*
|
||||||
### 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.
|
|
||||||
|
|
||||||
## Key Decisions
|
## Key Decisions
|
||||||
- **perPage clamping**: soli valori `[10,20,25,50,100]`, fallback a default 20 su valore non valido
|
- **PDO > tinker**: `createAdminUser()` usa PDO prepared statement con `password_hash()`, niente interpolazione PHP in stringhe shell → zero escaping issues
|
||||||
- **CSV gruppi segue pattern individui**: stesso approccio fault-tolerant (skip righe incomplete, log errori, riepilogo finale)
|
- **`install.sql` > migration sequenziali**: unico file SQL per fresh install, più veloce e debuggabile (si può lanciare `mysql < install.sql` a mano)
|
||||||
- **ICS senza espansione RRULE**: eventi ricorrenti importati come evento singolo con data della prima occorrenza
|
- **`var_export()` per tinker fallback**: nei rari casi SQLite, si usa `var_export()` che produce stringhe PHP valide con escaping automatico
|
||||||
- **Dedup via uid_esterno**: lookup prima della creazione, skip se già presente
|
- **`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
|
## Relevant Files
|
||||||
- `app/Http/Controllers/IndividuoController.php` — perPage in index()
|
- `database/install.sql`: NEW — schema MySQL completo + seed data (1059 righe)
|
||||||
- `app/Http/Controllers/EventoController.php` — perPage in index() + importIcs()/importIcsStore()
|
- `install.php`: MODIFIED — `createAdminUser()` + `ensureMysqlUserAndDb()` + composer fix + auto-create MySQL user
|
||||||
- `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
|
|
||||||
|
|||||||
@@ -59,8 +59,7 @@ class EmailController extends Controller
|
|||||||
|
|
||||||
$query->orderBy($sortField === 'sent_at' ? 'received_at' : $sortField, $sortDir);
|
$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(20)->withQueryString();
|
||||||
$messages = $query->paginate($perPage)->withQueryString();
|
|
||||||
$folders = EmailFolder::orderBy('sort_order')->get();
|
$folders = EmailFolder::orderBy('sort_order')->get();
|
||||||
$mailingLists = MailingList::where('attiva', true)->get();
|
$mailingLists = MailingList::where('attiva', true)->get();
|
||||||
$gruppi = Gruppo::orderBy('nome')->get();
|
$gruppi = Gruppo::orderBy('nome')->get();
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ use App\Models\Individuo;
|
|||||||
use App\Models\TipologiaEvento;
|
use App\Models\TipologiaEvento;
|
||||||
use App\Services\IcsExportService;
|
use App\Services\IcsExportService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
use Illuminate\Support\Facades\Storage;
|
|
||||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
|
|
||||||
class EventoController extends Controller
|
class EventoController extends Controller
|
||||||
@@ -45,8 +43,7 @@ class EventoController extends Controller
|
|||||||
$query->orderByDesc('created_at');
|
$query->orderByDesc('created_at');
|
||||||
}
|
}
|
||||||
|
|
||||||
$perPage = in_array((int) $request->perPage, [10, 20, 25, 50, 100]) ? (int) $request->perPage : 20;
|
$eventi = $query->paginate(20);
|
||||||
$eventi = $query->paginate($perPage);
|
|
||||||
$gruppi = Gruppo::orderBy('nome')->get();
|
$gruppi = Gruppo::orderBy('nome')->get();
|
||||||
|
|
||||||
return view('eventi.index', compact('eventi', 'gruppi'));
|
return view('eventi.index', compact('eventi', 'gruppi'));
|
||||||
@@ -480,101 +477,4 @@ class EventoController extends Controller
|
|||||||
|
|
||||||
return $colors['primary'];
|
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\Individuo;
|
||||||
use App\Models\Diocesi;
|
use App\Models\Diocesi;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
|
|
||||||
class GruppoController extends Controller
|
class GruppoController extends Controller
|
||||||
{
|
{
|
||||||
@@ -301,101 +300,4 @@ class GruppoController extends Controller
|
|||||||
|
|
||||||
return redirect()->route('gruppi.index')->with('success', 'Vista eliminata.');
|
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)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$this->authorizeRead('individui');
|
$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(20);
|
||||||
$individui = Individuo::with('contatti')->orderBy('cognome')->orderBy('nome')->paginate($perPage);
|
|
||||||
|
|
||||||
$vista = null;
|
$vista = null;
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ class Evento extends Model
|
|||||||
'is_incontro_gruppo',
|
'is_incontro_gruppo',
|
||||||
'luogo_indirizzo',
|
'luogo_indirizzo',
|
||||||
'luogo_url_maps',
|
'luogo_url_maps',
|
||||||
'uid_esterno',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
|
|||||||
@@ -197,7 +197,6 @@ CREATE TABLE `gruppo_individuo` (
|
|||||||
`gruppo_id` bigint unsigned NOT NULL,
|
`gruppo_id` bigint unsigned NOT NULL,
|
||||||
`individuo_id` bigint unsigned NOT NULL,
|
`individuo_id` bigint unsigned NOT NULL,
|
||||||
`ruolo_ids` json DEFAULT 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,
|
`data_adesione` date DEFAULT NULL,
|
||||||
`created_at` timestamp NULL DEFAULT NULL,
|
`created_at` timestamp NULL DEFAULT NULL,
|
||||||
`updated_at` timestamp NULL DEFAULT NULL,
|
`updated_at` timestamp NULL DEFAULT NULL,
|
||||||
@@ -488,7 +487,7 @@ CREATE TABLE `documenti` (
|
|||||||
`file_path` varchar(255) NOT NULL,
|
`file_path` varchar(255) NOT NULL,
|
||||||
`tipo` varchar(255) DEFAULT NULL,
|
`tipo` varchar(255) DEFAULT NULL,
|
||||||
`tipologia` enum('avatar','galleria','documento','statuto','altro','programma','locandina','email_attachment') DEFAULT 'documento',
|
`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_id` bigint unsigned DEFAULT NULL,
|
||||||
`visibilita_target_type` varchar(255) DEFAULT NULL,
|
`visibilita_target_type` varchar(255) DEFAULT NULL,
|
||||||
`mime_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
|
-- Role presets
|
||||||
INSERT INTO `role_presets` (`name`, `description`, `permissions`, `created_at`, `updated_at`) VALUES
|
INSERT INTO `role_presets` (`name`, `description`, `permissions`, `created_at`, `updated_at`) VALUES
|
||||||
('admin', 'Amministratore con accesso completo a tutti i moduli',
|
('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()),
|
NOW(), NOW()),
|
||||||
('operatore', 'Operatore con accesso completo a individui/gruppi/eventi, sola lettura per documenti/mailing',
|
('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()),
|
NOW(), NOW()),
|
||||||
('lettura', 'Sola lettura su tutti i moduli',
|
('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());
|
NOW(), NOW());
|
||||||
|
|
||||||
-- Email folders (system)
|
-- Email folders (system)
|
||||||
|
|||||||
+13
-50
@@ -131,17 +131,11 @@ function createAdminUser(
|
|||||||
|
|
||||||
$userId = $pdo->lastInsertId();
|
$userId = $pdo->lastInsertId();
|
||||||
|
|
||||||
// Assegna ruolo Spatie 'admin' (lookup dinamico per nome)
|
// Assegna ruolo Spatie 'admin' (role_id = 1)
|
||||||
$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');
|
|
||||||
}
|
|
||||||
$stmt = $pdo->prepare(
|
$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;
|
return true;
|
||||||
} catch (\PDOException $e) {
|
} catch (\PDOException $e) {
|
||||||
@@ -185,10 +179,6 @@ $appName = askRequired('Nome del sito/app (es. Glastree MyChurch)');
|
|||||||
$adminName = askRequired('Nome del primo utente amministratore');
|
$adminName = askRequired('Nome del primo utente amministratore');
|
||||||
$adminEmail = askRequired('Email dell\'amministratore');
|
$adminEmail = askRequired('Email dell\'amministratore');
|
||||||
$adminPass = askRequired('Password per l\'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)');
|
$appUrl = askRequired('URL pubblico del sito (es. https://glastree.esempio.it)');
|
||||||
|
|
||||||
// ── DB params (Apache / Restore) ─────────────────────
|
// ── DB params (Apache / Restore) ─────────────────────
|
||||||
@@ -274,9 +264,7 @@ if ($mode === 'Fresh Install su Apache (LAMP tradizionale)') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// APP_KEY
|
// APP_KEY
|
||||||
if (!run("{$sudo}php artisan key:generate --force", 'Generazione APP_KEY...')) {
|
runCapture("{$sudo}php artisan key:generate --force");
|
||||||
fail('Generazione APP_KEY fallita');
|
|
||||||
}
|
|
||||||
ok('APP_KEY generata');
|
ok('APP_KEY generata');
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
@@ -385,9 +373,7 @@ if ($mode === 'Fresh Install su Apache (LAMP tradizionale)') {
|
|||||||
println(color(' 3/7 — INSTALLAZIONE DIPENDENZE', 'yellow'));
|
println(color(' 3/7 — INSTALLAZIONE DIPENDENZE', 'yellow'));
|
||||||
println(color('══════════════════════════════════════════', 'yellow'));
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
|
||||||
if (!run("COMPOSER_ROOT_VERSION=dev-main composer install --no-dev --optimize-autoloader", 'Installazione dipendenze Composer...')) {
|
run("COMPOSER_ROOT_VERSION=dev-main composer install --no-dev --optimize-autoloader", 'Installazione dipendenze Composer...');
|
||||||
fail('Installazione dipendenze Composer fallita');
|
|
||||||
}
|
|
||||||
ok('Dipendenze PHP installate');
|
ok('Dipendenze PHP installate');
|
||||||
|
|
||||||
// ── Step 4: Package discovery ────────────────────
|
// ── Step 4: Package discovery ────────────────────
|
||||||
@@ -411,7 +397,7 @@ if ($mode === 'Fresh Install su Apache (LAMP tradizionale)') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($dbType === 'mysql') {
|
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...')) {
|
if (!run($importCmd, 'Importazione struttura database e dati base...')) {
|
||||||
fail("Importazione SQL fallita. Verifica il file database/install.sql e le credenziali.");
|
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'));
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
|
||||||
checkCmd('docker');
|
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();
|
||||||
println(color('══════════════════════════════════════════', 'yellow'));
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
@@ -538,13 +518,10 @@ if ($mode === 'Fresh Install via Docker') {
|
|||||||
println(color(' INSTALLAZIONE DOCKER', 'yellow'));
|
println(color(' INSTALLAZIONE DOCKER', 'yellow'));
|
||||||
println(color('══════════════════════════════════════════', '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');
|
fail('Build fallita');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Genera APP_KEY per il container Docker
|
|
||||||
$appKey = 'base64:' . base64_encode(random_bytes(32));
|
|
||||||
|
|
||||||
// Override compose con variabili d'ambiente
|
// Override compose con variabili d'ambiente
|
||||||
$override = [
|
$override = [
|
||||||
'services:',
|
'services:',
|
||||||
@@ -554,12 +531,11 @@ if ($mode === 'Fresh Install via Docker') {
|
|||||||
" - APP_URL={$appUrl}",
|
" - APP_URL={$appUrl}",
|
||||||
' - APP_ENV=production',
|
' - APP_ENV=production',
|
||||||
' - APP_DEBUG=false',
|
' - APP_DEBUG=false',
|
||||||
" - APP_KEY={$appKey}",
|
|
||||||
];
|
];
|
||||||
file_put_contents('docker-compose.override.yml', implode(PHP_EOL, $override) . PHP_EOL);
|
file_put_contents('docker-compose.override.yml', implode(PHP_EOL, $override) . PHP_EOL);
|
||||||
|
|
||||||
if (!run(
|
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...'
|
'Avvio container...'
|
||||||
)) {
|
)) {
|
||||||
@unlink('docker-compose.override.yml');
|
@unlink('docker-compose.override.yml');
|
||||||
@@ -568,7 +544,7 @@ if ($mode === 'Fresh Install via Docker') {
|
|||||||
|
|
||||||
// Attendi container pronto
|
// Attendi container pronto
|
||||||
info('Attendo che il container sia 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;
|
$adminCreated = false;
|
||||||
|
|
||||||
if ($containerId !== '') {
|
if ($containerId !== '') {
|
||||||
@@ -584,11 +560,6 @@ if ($mode === 'Fresh Install via Docker') {
|
|||||||
sleep(2);
|
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
|
// Import schema SQL nel container
|
||||||
$sqlFile = $scriptDir . '/database/install.sql';
|
$sqlFile = $scriptDir . '/database/install.sql';
|
||||||
if (file_exists($sqlFile) && $dockerDb === 'mysql') {
|
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("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}']);
|
\$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();
|
\$uid = \$pdo->lastInsertId();
|
||||||
\$stmt = \$pdo->prepare("SELECT id FROM roles WHERE name = 'admin' LIMIT 1");
|
\$pdo->prepare("INSERT INTO model_has_roles (role_id, model_type, model_id) VALUES (1, ?, ?)")->execute(['App\\\\Models\\\\User', \$uid]);
|
||||||
\$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]);
|
|
||||||
echo "OK:" . \$uid;
|
echo "OK:" . \$uid;
|
||||||
PHP;
|
PHP;
|
||||||
file_put_contents($adminScriptPath, $adminPhp);
|
file_put_contents($adminScriptPath, $adminPhp);
|
||||||
@@ -768,9 +735,7 @@ if ($mode === 'Restore da Backup (Apache)') {
|
|||||||
file_put_contents('.env', $dotenv);
|
file_put_contents('.env', $dotenv);
|
||||||
|
|
||||||
// Generate APP_KEY
|
// Generate APP_KEY
|
||||||
if (!run("{$sudo}php artisan key:generate --force", 'Generazione APP_KEY...')) {
|
runCapture("{$sudo}php artisan key:generate --force");
|
||||||
fail('Generazione APP_KEY fallita');
|
|
||||||
}
|
|
||||||
ok('APP_KEY rigenerata');
|
ok('APP_KEY rigenerata');
|
||||||
|
|
||||||
// Import database
|
// Import database
|
||||||
@@ -778,7 +743,7 @@ if ($mode === 'Restore da Backup (Apache)') {
|
|||||||
$sqlFile = "{$tmpDir}/database.sql";
|
$sqlFile = "{$tmpDir}/database.sql";
|
||||||
if (file_exists($sqlFile)) {
|
if (file_exists($sqlFile)) {
|
||||||
info("Importazione database MySQL: {$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)) {
|
if (run($cmd, null)) {
|
||||||
ok('Database importato');
|
ok('Database importato');
|
||||||
} else {
|
} else {
|
||||||
@@ -800,9 +765,7 @@ if ($mode === 'Restore da Backup (Apache)') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Composer
|
// Composer
|
||||||
if (!run("COMPOSER_ROOT_VERSION=dev-main composer install --no-dev --optimize-autoloader", 'Installazione dipendenze Composer...')) {
|
run("COMPOSER_ROOT_VERSION=dev-main composer install --no-dev --optimize-autoloader", 'Installazione dipendenze Composer...');
|
||||||
fail('Installazione dipendenze Composer fallita');
|
|
||||||
}
|
|
||||||
runCapture("{$sudo}php artisan package:discover --ansi 2>/dev/null");
|
runCapture("{$sudo}php artisan package:discover --ansi 2>/dev/null");
|
||||||
|
|
||||||
// Storage link + cache
|
// Storage link + cache
|
||||||
|
|||||||
@@ -131,29 +131,8 @@ $currentFolder = $folders->firstWhere('type', $folder);
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
<div class="card-footer">
|
||||||
<div>
|
{{ $messages->withQueryString()->links('vendor.pagination.simple-bootstrap-4') }}
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@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>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@if($canWriteEventi)
|
@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">
|
<a href="/eventi/create" class="btn btn-xs btn-success">
|
||||||
<i class="fas fa-plus mr-1"></i> Nuovo Evento
|
<i class="fas fa-plus mr-1"></i> Nuovo Evento
|
||||||
</a>
|
</a>
|
||||||
@@ -250,29 +247,8 @@ $canDeleteEventi = Auth::user()->canDelete('eventi');
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
<div class="card-footer">
|
||||||
<div>
|
{{ $eventi->withQueryString()->links() }}
|
||||||
<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>
|
</div>
|
||||||
</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
|
<i class="fas fa-save mr-1"></i> Salva Vista
|
||||||
</button>
|
</button>
|
||||||
@if($canWriteGruppi)
|
@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">
|
<a href="{{ route('gruppi.create') }}" class="btn btn-success btn-sm">
|
||||||
<i class="fas fa-plus"></i> Nuovo Gruppo
|
<i class="fas fa-plus"></i> Nuovo Gruppo
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -220,29 +220,8 @@ $canDeleteIndividui = Auth::user()->canDelete('individui');
|
|||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@if($individui->count() > 0)
|
@if($individui->count() > 0)
|
||||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
<div class="card-footer">
|
||||||
<div>
|
{{ $individui->links() }}
|
||||||
<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>
|
|
||||||
{{ $individui->links() }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -87,9 +87,6 @@ Route::patch('gruppi/{gruppo}/membri/{individuo}', [GruppoMembroController::clas
|
|||||||
Route::delete('gruppi/{gruppo}/membri/{individuo}', [GruppoMembroController::class, 'destroy'])->middleware('auth');
|
Route::delete('gruppi/{gruppo}/membri/{individuo}', [GruppoMembroController::class, 'destroy'])->middleware('auth');
|
||||||
|
|
||||||
// Gruppi specific routes (MUST be before resource route)
|
// 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/all', [GruppoController::class, 'allGruppi'])->middleware('auth');
|
||||||
Route::get('gruppi/{gruppo}/individui-email', [GruppoController::class, 'individuiEmail'])->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::resource('gruppi', GruppoController::class)->middleware('auth');
|
||||||
Route::get('eventi/calendar', [EventoController::class, 'calendar'])->name('eventi.calendar')->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/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::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/export-ics', [EventoController::class, 'exportSelectedIcs'])->name('eventi.export-selected-ics')->middleware('auth');
|
||||||
Route::post('eventi/mass-elimina', [EventoController::class, 'massElimina'])->middleware('auth');
|
Route::post('eventi/mass-elimina', [EventoController::class, 'massElimina'])->middleware('auth');
|
||||||
|
|||||||
Reference in New Issue
Block a user