Compare commits
17 Commits
3fb653f4bf
...
2f454ab749
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f454ab749 | |||
| da022bdf4a | |||
| 6b637fc12b | |||
| 72c48d301d | |||
| abb39e62f9 | |||
| f36a9906a2 | |||
| fcbb6fb061 | |||
| 25168debc0 | |||
| 7381d86362 | |||
| 8404cda475 | |||
| 75f6665252 | |||
| 81541a3af5 | |||
| c6f8403d1a | |||
| 6b913f330e | |||
| 1e9eb29428 | |||
| c9d66c2a80 | |||
| aa4e582925 |
@@ -1,48 +1,61 @@
|
||||
# 🧠 MEMORY.md — Stato Progetto
|
||||
|
||||
## Goal
|
||||
- Stabilizzare il modale "Carica Documento" e migliorare la gestione cartelle durante l'upload.
|
||||
- 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
|
||||
- (none)
|
||||
- Laravel 13, PHP 8.4, AdminLTE 4, MySQL/MariaDB (install.sql solo MySQL)
|
||||
- Zero placeholder, zero TODO, codice funzionante
|
||||
- Compatibilità cross-DB MySQL ↔ SQLite (fallback migration per SQLite)
|
||||
|
||||
## Progress
|
||||
### Done
|
||||
- Aggiunto pulsante "Scarica Guida PDF" nella sidebar di `/help` con nuovo metodo `HelpController::downloadPdf()` e route `GET /help/pdf/download`
|
||||
- Aggiunta card "Backup" in dashboard (small-box bg-navy, icona fa-hdd) con conteggio backup, visibile solo a utenti con `canManage('settings')`
|
||||
- Corretto `modal-body` nel modale `#uploadModal` che chiudeva prematuramente (pulsanti Annulla/Carica finivano fuori dal popup)
|
||||
- Aggiunto campo `visibilita` (select Pubblico/Individuo/Gruppo) nel form di upload documento, mancante e richiesto dal controller
|
||||
- Aggiunto selettore cartelle visibile (`<select name="cartella_id" id="uploadCartellaId">`) nel modale di upload (sostituisce il vecchio hidden input)
|
||||
- Aggiunto pulsante "Nuova cartella" sia nel modale upload (`#uploadNewFolderBtn`) che nel modale "Sposta" (`#massMoveNewFolderBtn`)
|
||||
- Sostituito SweetAlert2 per creazione cartella con il modale Bootstrap `#newFolderModal` già esistente (risolve focus trap Bootstrap 4 che bloccava l'input)
|
||||
- Aggiornato `toggleUploadRepo()` per nascondere/mostrare il gruppo cartella in base alla selezione del repository remoto
|
||||
- Aggiunto flag `window._uploadNewFolder` per comportamento differenziato: da upload aggiunge opzione al select senza ricaricare, da tooltip ricarica la pagina
|
||||
- **Fix backdrop grigio persistente dopo creazione cartella**: ora `#uploadModal` viene prima nascosto (quando si clicca "Nuova cartella") e poi ri-mostrato dopo la creazione nel success handler, risolvendo il conflitto di backdrop tra modali annidati Bootstrap 4
|
||||
- **Fix scroll modal upload**: ristrutturato `<form>` per non avvolgere `.modal-body` e `.modal-footer` (ora è dentro `.modal-body`), così `modal-dialog-scrollable` funziona correttamente. Pulsante submit ha `form="uploadForm"` per collegamento cross-DOM
|
||||
- **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
|
||||
|
||||
### In Progress
|
||||
- (none)
|
||||
- *(nessuno)*
|
||||
|
||||
### Blocked
|
||||
- (none)
|
||||
- *(nessuno)*
|
||||
|
||||
## Key Decisions
|
||||
- Replicato il codice di creazione cartella con `SweetAlert2` → sostituito con modale Bootstrap `#newFolderModal` perché Bootstrap 4 impedisce il focus su elementi fuori dal `.modal` (focus trap)
|
||||
- Pulsante "Nuova cartella" nello stesso `<form>` del modal di upload riceve `e.preventDefault()` per sicurezza
|
||||
- Per evitare conflitti di backdrop Bootstrap 4 con modali annidati, si nasconde `#uploadModal` prima di mostrare `#newFolderModal` e lo si ripristina dopo il success
|
||||
- **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
|
||||
- (none)
|
||||
- *(nessuno)*
|
||||
|
||||
## Critical Context
|
||||
- Bootstrap 4.6.2 con focus trap: impedisce focus su elementi fuori dal `.modal` — SweetAlert2 aggiunge dialog al `<body>`, quindi il suo input non riceve focus
|
||||
- `#newFolderModal` già esisteva con submit handler AJAX a `/documenti/cartelle` e ricaricava pagina — ora con `window._uploadNewFolder` evita reload quando chiamato dall'upload
|
||||
- Bug: backdrop grigio non spariva dopo creazione cartella perché Bootstrap 4 gestisce male i modali annidati (due backdrop concorrenti)
|
||||
- `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/HelpController.php`: metodo `downloadPdf()` + route `/help/pdf/download`
|
||||
- `app/Http/Controllers/HomeController.php`: `$stats['backups']` via `BackupService::list()`
|
||||
- `routes/web.php`: route `/help/pdf/download`
|
||||
- `resources/views/help/index.blade.php`: card-footer con pulsante download PDF
|
||||
- `resources/views/home/dashboard.blade.php`: small-box Backup condizionale
|
||||
- `resources/views/documenti/index.blade.php`: intero modale `#uploadModal` riscritto (visibilita, cartella select, nuova cartella via `#newFolderModal`, toggleUploadRepo); fix backdrop persistente nascondendo/rimostrando `#uploadModal`
|
||||
- `database/install.sql`: NEW — schema MySQL completo + seed data (1059 righe)
|
||||
- `install.php`: MODIFIED — `createAdminUser()` + `ensureMysqlUserAndDb()` + composer fix + auto-create MySQL user
|
||||
|
||||
@@ -39,4 +39,35 @@ class ActivityLogController extends Controller
|
||||
|
||||
return view('admin.activity-logs.index', compact('logs', 'users', 'modules'));
|
||||
}
|
||||
|
||||
public function destroy(Request $request)
|
||||
{
|
||||
$query = ActivityLog::query();
|
||||
|
||||
if ($request->filled('user_id')) {
|
||||
$query->where('user_id', $request->user_id);
|
||||
}
|
||||
|
||||
if ($request->filled('module')) {
|
||||
$query->where('module', $request->module);
|
||||
}
|
||||
|
||||
if ($request->filled('action')) {
|
||||
$query->where('action', $request->action);
|
||||
}
|
||||
|
||||
if ($request->filled('from')) {
|
||||
$query->whereDate('created_at', '>=', $request->from);
|
||||
}
|
||||
|
||||
if ($request->filled('to')) {
|
||||
$query->whereDate('created_at', '<=', $request->to);
|
||||
}
|
||||
|
||||
$count = $query->count();
|
||||
$query->delete();
|
||||
|
||||
return redirect()->route('admin.activity-logs.index')
|
||||
->with('success', "Eliminati {$count} log.");
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,14 @@
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\EmailAttachment;
|
||||
use App\Models\EmailFolder;
|
||||
use App\Models\EmailSetting;
|
||||
use App\Models\SenderAccount;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class EmailSettingsController extends Controller
|
||||
{
|
||||
@@ -257,6 +261,30 @@ class EmailSettingsController extends Controller
|
||||
return back()->with('success', 'Mittente eliminato.');
|
||||
}
|
||||
|
||||
public function destroy(): RedirectResponse
|
||||
{
|
||||
$this->authorizeDelete('settings');
|
||||
|
||||
EmailAttachment::chunk(100, function ($attachments) {
|
||||
foreach ($attachments as $attachment) {
|
||||
if ($attachment->file_path && Storage::exists($attachment->file_path)) {
|
||||
Storage::delete($attachment->file_path);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
EmailFolder::whereIn('type', ['inbox', 'sent', 'drafts', 'trash', 'archive', 'starred'])->each(function ($folder) {
|
||||
$folder->delete();
|
||||
});
|
||||
|
||||
$settings = EmailSetting::first();
|
||||
if ($settings) {
|
||||
$settings->delete();
|
||||
}
|
||||
|
||||
return redirect('/impostazioni/email')->with('success', 'Configurazione email eliminata. Messaggi, cartelle e allegati rimossi completamente.');
|
||||
}
|
||||
|
||||
public function senderTestSmtp(Request $request, int $id)
|
||||
{
|
||||
$this->authorizeWrite('settings');
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\CalendarioConnessione;
|
||||
use App\Services\CalDavService;
|
||||
use App\Services\GoogleCalendarSyncService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CalendarioConnessioneController extends Controller
|
||||
{
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeWrite('settings');
|
||||
|
||||
$data = $request->validate([
|
||||
'nome' => 'required|string|max:255',
|
||||
'tipo' => 'required|in:google_calendar,caldav',
|
||||
'sync_direction' => 'nullable|in:import,export,bidirectional',
|
||||
'sync_interval_minutes' => 'nullable|integer|min:5|max:1440',
|
||||
'is_active' => 'boolean',
|
||||
'config' => 'nullable|array',
|
||||
]);
|
||||
|
||||
$maxOrdine = CalendarioConnessione::max('ordine') ?? 0;
|
||||
|
||||
$connessione = new CalendarioConnessione();
|
||||
$connessione->nome = $data['nome'];
|
||||
$connessione->tipo = $data['tipo'];
|
||||
$connessione->sync_direction = $data['sync_direction'] ?? 'bidirectional';
|
||||
$connessione->sync_interval_minutes = $data['sync_interval_minutes'] ?? 60;
|
||||
$connessione->is_active = $request->boolean('is_active', true);
|
||||
$connessione->stato = 'disconnesso';
|
||||
$connessione->ordine = $maxOrdine + 1;
|
||||
$connessione->encryptAndSetConfig($data['config'] ?? []);
|
||||
$connessione->save();
|
||||
|
||||
return redirect('/impostazioni#calendario')->with('success', 'Connessione calendario "' . $connessione->nome . '" creata.');
|
||||
}
|
||||
|
||||
public function update(Request $request, int $id): RedirectResponse
|
||||
{
|
||||
$this->authorizeWrite('settings');
|
||||
|
||||
$connessione = CalendarioConnessione::findOrFail($id);
|
||||
|
||||
$data = $request->validate([
|
||||
'nome' => 'required|string|max:255',
|
||||
'tipo' => 'required|in:google_calendar,caldav',
|
||||
'sync_direction' => 'nullable|in:import,export,bidirectional',
|
||||
'sync_interval_minutes' => 'nullable|integer|min:5|max:1440',
|
||||
'is_active' => 'boolean',
|
||||
'config' => 'nullable|array',
|
||||
]);
|
||||
|
||||
$connessione->nome = $data['nome'];
|
||||
$connessione->tipo = $data['tipo'];
|
||||
$connessione->sync_direction = $data['sync_direction'] ?? 'bidirectional';
|
||||
$connessione->sync_interval_minutes = $data['sync_interval_minutes'] ?? 60;
|
||||
$connessione->is_active = $request->boolean('is_active', true);
|
||||
|
||||
if (!empty($data['config'])) {
|
||||
$connessione->encryptAndSetConfig($data['config']);
|
||||
}
|
||||
|
||||
$connessione->save();
|
||||
|
||||
return redirect('/impostazioni#calendario')->with('success', 'Connessione calendario "' . $connessione->nome . '" aggiornata.');
|
||||
}
|
||||
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
$this->authorizeDelete('settings');
|
||||
$connessione = CalendarioConnessione::findOrFail($id);
|
||||
$connessione->delete();
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Connessione calendario eliminata.']);
|
||||
}
|
||||
|
||||
public function test(int $id): JsonResponse
|
||||
{
|
||||
$this->authorizeWrite('settings');
|
||||
$connessione = CalendarioConnessione::findOrFail($id);
|
||||
|
||||
$result = match ($connessione->tipo) {
|
||||
'caldav' => app(CalDavService::class)->testConnection($connessione),
|
||||
'google_calendar' => app(GoogleCalendarSyncService::class)->testConnection($connessione),
|
||||
default => ['success' => false, 'message' => 'Tipo sconosciuto'],
|
||||
};
|
||||
|
||||
if ($result['success']) {
|
||||
$connessione->update(['stato' => 'connesso', 'last_error_at' => null, 'last_error_message' => null]);
|
||||
} else {
|
||||
$connessione->update(['stato' => 'errore', 'last_error_at' => now(), 'last_error_message' => $result['message'] ?? 'Errore sconosciuto']);
|
||||
}
|
||||
|
||||
return response()->json($result);
|
||||
}
|
||||
|
||||
public function sync(int $id): RedirectResponse
|
||||
{
|
||||
$this->authorizeWrite('settings');
|
||||
$connessione = CalendarioConnessione::findOrFail($id);
|
||||
|
||||
$result = match ($connessione->tipo) {
|
||||
'caldav' => app(CalDavService::class)->syncEvents($connessione),
|
||||
'google_calendar' => app(GoogleCalendarSyncService::class)->syncEvents($connessione),
|
||||
default => ['success' => false, 'imported' => 0, 'exported' => 0, 'errors' => ['Tipo sconosciuto']],
|
||||
};
|
||||
|
||||
$connessione->update(['last_sync_at' => now()]);
|
||||
|
||||
if ($result['success']) {
|
||||
$msg = 'Sincronizzazione completata: ' . $result['imported'] . ' importati, ' . $result['exported'] . ' esportati.';
|
||||
$connessione->update(['stato' => 'connesso']);
|
||||
return redirect('/impostazioni#calendario')->with('success', $msg);
|
||||
}
|
||||
|
||||
$errorMsg = implode('; ', $result['errors']);
|
||||
$connessione->update(['stato' => 'errore', 'last_error_at' => now(), 'last_error_message' => $errorMsg]);
|
||||
|
||||
return redirect('/impostazioni#calendario')->with('error', 'Errore sync: ' . $errorMsg);
|
||||
}
|
||||
|
||||
public function reorder(Request $request): JsonResponse
|
||||
{
|
||||
$this->authorizeWrite('settings');
|
||||
$order = $request->input('order', []);
|
||||
foreach ($order as $index => $id) {
|
||||
CalendarioConnessione::where('id', $id)->update(['ordine' => $index]);
|
||||
}
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
}
|
||||
@@ -661,9 +661,13 @@ class DocumentoController extends Controller
|
||||
'evento' => Evento::class,
|
||||
'mailing' => \App\Models\MailingList::class,
|
||||
];
|
||||
$updateData['visibilita'] = $data['visibilita_target_type'];
|
||||
$targetType = $data['visibilita_target_type'];
|
||||
if (!isset($typeMap[$targetType])) {
|
||||
return back()->with('error', 'Tipo destinazione non valido.');
|
||||
}
|
||||
$updateData['visibilita'] = $targetType;
|
||||
$updateData['visibilita_target_id'] = (int) $data['visibilita_target_id'];
|
||||
$updateData['visibilita_target_type'] = $typeMap[$data['visibilita_target_type']] ?? $data['visibilita_target_type'];
|
||||
$updateData['visibilita_target_type'] = $typeMap[$targetType];
|
||||
}
|
||||
|
||||
Documento::whereIn('id', $ids)->update($updateData);
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\AppSetting;
|
||||
use App\Models\CalendarioConnessione;
|
||||
use App\Models\Documento;
|
||||
use App\Models\EmailSetting;
|
||||
use App\Models\Ruolo;
|
||||
use App\Models\SenderAccount;
|
||||
@@ -29,8 +31,9 @@ class ImpostazioniController extends Controller
|
||||
$senderAccounts = SenderAccount::orderBy('email_address')->get();
|
||||
$repositories = StorageRepository::orderBy('ordine')->get();
|
||||
$googleDriveNewToken = session('google_drive_new_token');
|
||||
$calendarioConnessioni = CalendarioConnessione::orderBy('ordine')->get();
|
||||
|
||||
return view('impostazioni.index', compact('tipologie', 'tipologieEventi', 'ruoli', 'appSettings', 'emailSettings', 'senderAccounts', 'repositories', 'googleDriveNewToken'));
|
||||
return view('impostazioni.index', compact('tipologie', 'tipologieEventi', 'ruoli', 'appSettings', 'emailSettings', 'senderAccounts', 'repositories', 'googleDriveNewToken', 'calendarioConnessioni'));
|
||||
}
|
||||
|
||||
public function saveAppSettings(Request $request)
|
||||
@@ -268,10 +271,17 @@ class ImpostazioniController extends Controller
|
||||
|
||||
$ruolo = Ruolo::findOrFail($id);
|
||||
|
||||
$count = \DB::table('gruppo_individuo')
|
||||
$allRows = \DB::table('gruppo_individuo')
|
||||
->whereNotNull('ruolo_ids')
|
||||
->whereRaw("JSON_CONTAINS(ruolo_ids, ?)", [json_encode($id)])
|
||||
->count();
|
||||
->get(['id', 'ruolo_ids']);
|
||||
|
||||
$count = 0;
|
||||
foreach ($allRows as $row) {
|
||||
$ids = json_decode($row->ruolo_ids, true);
|
||||
if (is_array($ids) && in_array((int) $id, $ids)) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($count > 0) {
|
||||
return redirect('/impostazioni#ruoli')->with('error', 'Impossibile eliminare: ' . $count . ' membri associati a questo ruolo.');
|
||||
|
||||
@@ -210,11 +210,15 @@ public function create()
|
||||
|
||||
\Log::info('Individuo dati salvati', ['id' => $individuo->id]);
|
||||
|
||||
$contattiData = $request->input('contatti');
|
||||
\Log::info('Contatti ricevuti:', ['contatti' => $contattiData, 'tipo' => gettype($contattiData)]);
|
||||
|
||||
$individuo->contatti()->delete();
|
||||
\Log::info('Contatti precedenti eliminati, count ora:', ['count' => $individuo->contatti()->count()]);
|
||||
if ($request->has('contatti')) {
|
||||
$contattiData = $request->input('contatti');
|
||||
\Log::info('Contatti ricevuti:', ['contatti' => $contattiData, 'tipo' => gettype($contattiData)]);
|
||||
|
||||
$individuo->contatti()->delete();
|
||||
\Log::info('Contatti precedenti eliminati, count ora:', ['count' => $individuo->contatti()->count()]);
|
||||
} else {
|
||||
$contattiData = null;
|
||||
}
|
||||
|
||||
if (!empty($contattiData) && is_array($contattiData)) {
|
||||
$created = 0;
|
||||
|
||||
@@ -602,7 +602,7 @@ class ReportController extends Controller
|
||||
'lista' => $lista->nome,
|
||||
'stato' => $lista->attiva ? 'Attiva' : 'Disattiva',
|
||||
'individuo' => $contatto->individuo?->nome_completo ?? 'N/D',
|
||||
'email' => $contatto->email ?? '-',
|
||||
'email' => $contatto->valore ?? '-',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
+15
-18
@@ -1,31 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AppSetting extends Model
|
||||
{
|
||||
protected $table = 'app_settings';
|
||||
|
||||
protected $fillable = [
|
||||
'logo',
|
||||
'logo_small',
|
||||
'logo_path',
|
||||
'logo_small_path',
|
||||
'nome_applicazione',
|
||||
'nome_organizzazione',
|
||||
'footer_text',
|
||||
'app_version',
|
||||
'dashboard_welcome',
|
||||
'show_version',
|
||||
'backup_path',
|
||||
'backup_retention_days',
|
||||
'backup_include_files',
|
||||
'backup_include_env',
|
||||
'backup_auto_enabled',
|
||||
'backup_auto_frequency',
|
||||
'backup_auto_hour',
|
||||
'logo', 'logo_small', 'logo_path', 'logo_small_path',
|
||||
'nome_applicazione', 'nome_organizzazione',
|
||||
'footer_text', 'app_version', 'dashboard_welcome', 'show_version',
|
||||
'backup_path', 'backup_retention_days', 'backup_include_files',
|
||||
'backup_include_env', 'backup_auto_enabled', 'backup_auto_frequency', 'backup_auto_hour',
|
||||
'documenti_storage_disk', 'documenti_storage_path',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
@@ -36,11 +28,16 @@ class AppSetting extends Model
|
||||
'backup_include_env' => 'boolean',
|
||||
'backup_auto_enabled' => 'boolean',
|
||||
'backup_auto_hour' => 'integer',
|
||||
'show_version' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public static function getSetting(string $key, $default = null)
|
||||
public static function getSetting(string $key, mixed $default = null): mixed
|
||||
{
|
||||
if (!Schema::hasTable('app_settings')) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$setting = self::first();
|
||||
return $setting?->{$key} ?? $default;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
||||
class CalendarioConnessione extends Model
|
||||
{
|
||||
protected $table = 'calendario_connessioni';
|
||||
|
||||
protected $fillable = [
|
||||
'nome',
|
||||
'tipo',
|
||||
'config',
|
||||
'stato',
|
||||
'sync_direction',
|
||||
'sync_interval_minutes',
|
||||
'last_sync_at',
|
||||
'last_error_at',
|
||||
'last_error_message',
|
||||
'is_active',
|
||||
'ordine',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'config' => 'array',
|
||||
'is_active' => 'boolean',
|
||||
'sync_interval_minutes' => 'integer',
|
||||
'last_sync_at' => 'datetime',
|
||||
'last_error_at' => 'datetime',
|
||||
'ordine' => 'integer',
|
||||
];
|
||||
|
||||
public function scopeAttivi($query)
|
||||
{
|
||||
return $query->where('is_active', true)->orderBy('ordine');
|
||||
}
|
||||
|
||||
public function getDecryptedConfig(): array
|
||||
{
|
||||
$config = $this->config;
|
||||
foreach (['username', 'password', 'client_secret', 'refresh_token', 'api_key'] as $field) {
|
||||
if (isset($config[$field])) {
|
||||
try {
|
||||
$config[$field] = Crypt::decryptString($config[$field]);
|
||||
} catch (\Exception) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return $config;
|
||||
}
|
||||
|
||||
public function encryptAndSetConfig(array $config): void
|
||||
{
|
||||
foreach (['username', 'password', 'client_secret', 'refresh_token', 'api_key'] as $field) {
|
||||
if (isset($config[$field]) && $config[$field] !== '' && !str_starts_with($config[$field], 'eyJpdiI')) {
|
||||
$config[$field] = Crypt::encryptString($config[$field]);
|
||||
}
|
||||
}
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public static function opzioniTipo(): array
|
||||
{
|
||||
return ['google_calendar', 'caldav'];
|
||||
}
|
||||
|
||||
public static function etichettaTipo(string $tipo): string
|
||||
{
|
||||
return match ($tipo) {
|
||||
'google_calendar' => 'Google Calendar',
|
||||
'caldav' => 'CalDAV (Infomaniak/NextCloud)',
|
||||
default => ucfirst($tipo),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\CalendarioConnessione;
|
||||
use App\Models\Evento;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CalDavService
|
||||
{
|
||||
private const CALDAV_NS = 'urn:ietf:params:xml:ns:caldav';
|
||||
private const DAV_NS = 'DAV:';
|
||||
|
||||
public function testConnection(CalendarioConnessione $connessione): array
|
||||
{
|
||||
$config = $connessione->getDecryptedConfig();
|
||||
$client = $this->buildClient($config);
|
||||
|
||||
try {
|
||||
$response = $client->request('PROPFIND', $config['url'], [
|
||||
'headers' => ['Depth' => '0'],
|
||||
'body' => '<?xml version="1.0" encoding="utf-8"?>
|
||||
<d:propfind xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/">
|
||||
<d:prop>
|
||||
<d:displayname/>
|
||||
<cs:getctag/>
|
||||
</d:prop>
|
||||
</d:propfind>',
|
||||
]);
|
||||
|
||||
if ($response->getStatusCode() < 300) {
|
||||
return ['success' => true, 'message' => 'Connessione CalDAV riuscita.'];
|
||||
}
|
||||
|
||||
return ['success' => false, 'message' => 'Errore HTTP: ' . $response->getStatusCode()];
|
||||
} catch (\Exception $e) {
|
||||
return ['success' => false, 'message' => 'Errore: ' . $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
public function listCalendars(CalendarioConnessione $connessione): array
|
||||
{
|
||||
$config = $connessione->getDecryptedConfig();
|
||||
$client = $this->buildClient($config);
|
||||
|
||||
try {
|
||||
$response = $client->request('PROPFIND', $config['url'], [
|
||||
'headers' => ['Depth' => '1'],
|
||||
'body' => '<?xml version="1.0" encoding="utf-8"?>
|
||||
<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
||||
<d:prop>
|
||||
<d:resourcetype/>
|
||||
<d:displayname/>
|
||||
<c:supported-calendar-component-set/>
|
||||
</d:prop>
|
||||
</d:propfind>',
|
||||
]);
|
||||
|
||||
$body = (string) $response->getBody();
|
||||
return $this->parseCalendarList($body);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('CalDAV listCalendars: ' . $e->getMessage());
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public function syncEvents(CalendarioConnessione $connessione): array
|
||||
{
|
||||
$config = $connessione->getDecryptedConfig();
|
||||
$client = $this->buildClient($config);
|
||||
$calendarUrl = $config['calendar_url'] ?? $config['url'];
|
||||
$imported = 0;
|
||||
$exported = 0;
|
||||
$errors = [];
|
||||
|
||||
$start = Carbon::now()->subYear()->format('Ymd\THis\Z');
|
||||
$end = Carbon::now()->addYear()->format('Ymd\THis\Z');
|
||||
|
||||
$direction = $connessione->sync_direction ?? 'bidirectional';
|
||||
|
||||
try {
|
||||
if ($direction === 'import' || $direction === 'bidirectional') {
|
||||
$result = $this->fetchRemoteEvents($client, $calendarUrl, $start, $end);
|
||||
$imported = $this->importEvents($result, $errors);
|
||||
}
|
||||
|
||||
if ($direction === 'export' || $direction === 'bidirectional') {
|
||||
$localEvents = Evento::where(function ($q) use ($connessione) {
|
||||
$q->whereNotNull('uid_esterno')
|
||||
->orWhereNull('uid_esterno');
|
||||
})->get();
|
||||
$exported = $this->exportEvents($client, $calendarUrl, $localEvents, $connessione, $errors);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = $e->getMessage();
|
||||
Log::error('CalDAV syncEvents: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => empty($errors),
|
||||
'imported' => $imported,
|
||||
'exported' => $exported,
|
||||
'errors' => $errors,
|
||||
];
|
||||
}
|
||||
|
||||
private function fetchRemoteEvents($client, string $url, string $start, string $end): array
|
||||
{
|
||||
$body = '<?xml version="1.0" encoding="utf-8"?>
|
||||
<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
||||
<d:prop>
|
||||
<d:getetag/>
|
||||
<c:calendar-data/>
|
||||
</d:prop>
|
||||
<c:filter>
|
||||
<c:comp-filter name="VCALENDAR">
|
||||
<c:comp-filter name="VEVENT">
|
||||
<c:time-range start="' . $start . '" end="' . $end . '"/>
|
||||
</c:comp-filter>
|
||||
</c:comp-filter>
|
||||
</c:filter>
|
||||
</c:calendar-query>';
|
||||
|
||||
$response = $client->request('REPORT', $url, [
|
||||
'headers' => ['Depth' => '1', 'Content-Type' => 'application/xml; charset=utf-8'],
|
||||
'body' => $body,
|
||||
]);
|
||||
|
||||
return $this->parseCalendarData((string) $response->getBody());
|
||||
}
|
||||
|
||||
private function importEvents(array $remoteEvents, array &$errors): int
|
||||
{
|
||||
$count = 0;
|
||||
foreach ($remoteEvents as $event) {
|
||||
try {
|
||||
$vcalendar = \Sabre\VObject\Reader::read($event['data']);
|
||||
foreach ($vcalendar->VEVENT as $vevent) {
|
||||
$uid = (string) ($vevent->UID ?? Str::uuid());
|
||||
$existing = Evento::where('uid_esterno', $uid)->first();
|
||||
if ($existing) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$summary = (string) ($vevent->SUMMARY ?? 'Evento importato');
|
||||
$description = (string) ($vevent->DESCRIPTION ?? '');
|
||||
$location = (string) ($vevent->LOCATION ?? '');
|
||||
|
||||
$dtstart = $vevent->DTSTART ? $this->parseDateTime($vevent->DTSTART) : null;
|
||||
$dtend = $vevent->DTEND ? $this->parseDateTime($vevent->DTEND) : null;
|
||||
|
||||
$data = [
|
||||
'nome_evento' => $summary,
|
||||
'descrizione_evento' => mb_substr($description, 0, 255),
|
||||
'tipo_recorrenza' => 'singolo',
|
||||
'luogo_indirizzo' => $location,
|
||||
'uid_esterno' => $uid,
|
||||
];
|
||||
|
||||
if ($dtstart) {
|
||||
$data['data_specifica'] = $dtstart->format('Y-m-d');
|
||||
$data['ora_inizio'] = $dtstart->format('H:i');
|
||||
}
|
||||
|
||||
if ($dtend && $dtstart) {
|
||||
$data['durata_minuti'] = (int) $dtstart->diffInMinutes($dtend);
|
||||
}
|
||||
|
||||
$rrule = $vevent->RRULE ? (string) $vevent->RRULE : null;
|
||||
if ($rrule) {
|
||||
$parsed = $this->parseRRule($rrule);
|
||||
$data = array_merge($data, $parsed);
|
||||
}
|
||||
|
||||
Evento::create($data);
|
||||
$count++;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = 'Errore parsing evento: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
private function exportEvents($client, string $url, iterable $localEvents, CalendarioConnessione $connessione, array &$errors): int
|
||||
{
|
||||
$count = 0;
|
||||
$existingUids = $this->getExistingEventUids($client, $url);
|
||||
|
||||
foreach ($localEvents as $evento) {
|
||||
try {
|
||||
if ($evento->uid_esterno && !in_array($evento->uid_esterno, $existingUids)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$uid = $evento->uid_esterno ?? ('local-' . $evento->id . '-' . Str::uuid());
|
||||
$icsData = $this->buildIcsForExport($evento, $uid);
|
||||
|
||||
$eventUrl = rtrim($url, '/') . '/' . $uid . '.ics';
|
||||
|
||||
$response = $client->request('PUT', $eventUrl, [
|
||||
'headers' => ['Content-Type' => 'text/calendar; charset=utf-8'],
|
||||
'body' => $icsData,
|
||||
]);
|
||||
|
||||
if ($response->getStatusCode() < 300) {
|
||||
if (!$evento->uid_esterno) {
|
||||
$evento->updateQuietly(['uid_esterno' => $uid]);
|
||||
}
|
||||
$count++;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = 'Errore export ' . $evento->nome_evento . ': ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
private function getExistingEventUids($client, string $url): array
|
||||
{
|
||||
try {
|
||||
$response = $client->request('REPORT', $url, [
|
||||
'headers' => ['Depth' => '1'],
|
||||
'body' => '<?xml version="1.0" encoding="utf-8"?>
|
||||
<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
||||
<d:prop>
|
||||
<d:getetag/>
|
||||
<d:resourcetype/>
|
||||
</d:prop>
|
||||
<c:filter>
|
||||
<c:comp-filter name="VCALENDAR">
|
||||
<c:comp-filter name="VEVENT"/>
|
||||
</c:comp-filter>
|
||||
</c:filter>
|
||||
</c:calendar-query>',
|
||||
]);
|
||||
|
||||
$body = (string) $response->getBody();
|
||||
$xml = simplexml_load_string($body);
|
||||
$xml->registerXPathNamespace('d', self::DAV_NS);
|
||||
$uids = [];
|
||||
foreach ($xml->xpath('//d:href') as $href) {
|
||||
$path = (string) $href;
|
||||
$name = basename($path);
|
||||
if (str_ends_with($name, '.ics')) {
|
||||
$uids[] = str_replace('.ics', '', $name);
|
||||
}
|
||||
}
|
||||
return $uids;
|
||||
} catch (\Exception) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private function buildIcsForExport(Evento $evento, string $uid): string
|
||||
{
|
||||
$service = app(IcsExportService::class);
|
||||
return $service->generateSingle($evento);
|
||||
}
|
||||
|
||||
private function parseCalendarList(string $xml): array
|
||||
{
|
||||
$calendars = [];
|
||||
try {
|
||||
$dom = new \DOMDocument();
|
||||
$dom->loadXML($xml);
|
||||
$xpath = new \DOMXPath($dom);
|
||||
$xpath->registerNamespace('d', self::DAV_NS);
|
||||
$xpath->registerNamespace('c', self::CALDAV_NS);
|
||||
|
||||
$responses = $xpath->query('//d:multistatus/d:response');
|
||||
foreach ($responses as $response) {
|
||||
$href = $xpath->query('d:href', $response)->item(0)?->textContent ?? '';
|
||||
$displayName = $xpath->query('d:propstat/d:prop/d:displayname', $response)->item(0)?->textContent ?? $href;
|
||||
|
||||
$hasCalendar = false;
|
||||
$resourcetypes = $xpath->query('d:propstat/d:prop/d:resourcetype/*', $response);
|
||||
foreach ($resourcetypes as $type) {
|
||||
if ($type->localName === 'calendar' && $type->namespaceURI === self::CALDAV_NS) {
|
||||
$hasCalendar = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($hasCalendar && $href) {
|
||||
$calendars[] = [
|
||||
'href' => $href,
|
||||
'displayname' => $displayName,
|
||||
];
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('CalDAV parseCalendarList: ' . $e->getMessage());
|
||||
}
|
||||
return $calendars;
|
||||
}
|
||||
|
||||
private function parseCalendarData(string $xml): array
|
||||
{
|
||||
$events = [];
|
||||
try {
|
||||
$dom = new \DOMDocument();
|
||||
$dom->loadXML($xml);
|
||||
$xpath = new \DOMXPath($dom);
|
||||
$xpath->registerNamespace('d', self::DAV_NS);
|
||||
$xpath->registerNamespace('c', self::CALDAV_NS);
|
||||
|
||||
$responses = $xpath->query('//d:multistatus/d:response');
|
||||
foreach ($responses as $response) {
|
||||
$dataNodes = $xpath->query('d:propstat/d:prop/c:calendar-data', $response);
|
||||
foreach ($dataNodes as $node) {
|
||||
$events[] = ['data' => $node->textContent];
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('CalDAV parseCalendarData: ' . $e->getMessage());
|
||||
}
|
||||
return $events;
|
||||
}
|
||||
|
||||
private function parseDateTime($dt): ?Carbon
|
||||
{
|
||||
try {
|
||||
if ($dt instanceof \Sabre\VObject\Property\ICalendar\DateTime) {
|
||||
$dtValue = $dt->getDateTime();
|
||||
return Carbon::instance($dtValue);
|
||||
}
|
||||
} catch (\Exception) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function parseRRule(string $rrule): array
|
||||
{
|
||||
$result = [];
|
||||
try {
|
||||
$parts = explode(';', $rrule);
|
||||
foreach ($parts as $part) {
|
||||
[$key, $value] = explode('=', $part, 2);
|
||||
if ($key === 'FREQ') {
|
||||
$result['tipo_recorrenza'] = match (strtolower($value)) {
|
||||
'weekly' => 'settimanale',
|
||||
'monthly' => 'mensile',
|
||||
'yearly' => 'annuale',
|
||||
default => 'singolo',
|
||||
};
|
||||
} elseif ($key === 'BYDAY' && isset($result['tipo_recorrenza'])) {
|
||||
$dayMap = ['SU' => 0, 'MO' => 1, 'TU' => 2, 'WE' => 3, 'TH' => 4, 'FR' => 5, 'SA' => 6];
|
||||
$byDay = strtoupper($value);
|
||||
if (preg_match('/^(-?\d+)?([A-Z]+)$/', $byDay, $m)) {
|
||||
$result['giorno_settimana'] = $dayMap[$m[2]] ?? null;
|
||||
if (!empty($m[1]) && $result['tipo_recorrenza'] === 'mensile') {
|
||||
$result['occorrenza_mese'] = $m[1] . ',' . ($dayMap[$m[2]] ?? '1');
|
||||
}
|
||||
}
|
||||
} elseif ($key === 'BYMONTHDAY') {
|
||||
$result['giorno_mese'] = (int) $value;
|
||||
} elseif ($key === 'BYMONTH') {
|
||||
if ($result['tipo_recorrenza'] ?? '' === 'annuale') {
|
||||
$result['mese_annuale'] = (int) $value;
|
||||
} else {
|
||||
$result['mesi_recorrenza'] = explode(',', $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception) {
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function buildClient(array $config): \GuzzleHttp\Client
|
||||
{
|
||||
$options = [
|
||||
'verify' => false,
|
||||
'timeout' => 30,
|
||||
'http_errors' => false,
|
||||
];
|
||||
|
||||
if (!empty($config['username']) && !empty($config['password'])) {
|
||||
$options['auth'] = [$config['username'], $config['password'], 'basic'];
|
||||
}
|
||||
|
||||
return new \GuzzleHttp\Client($options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\CalendarioConnessione;
|
||||
use App\Models\Evento;
|
||||
use Carbon\Carbon;
|
||||
use Google\Client as GoogleClient;
|
||||
use Google\Service\Calendar as GoogleCalendar;
|
||||
use Google\Service\Calendar\Event as GoogleEvent;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class GoogleCalendarSyncService
|
||||
{
|
||||
private ?GoogleCalendar $calendarService = null;
|
||||
private ?string $calendarId = null;
|
||||
|
||||
public function testConnection(CalendarioConnessione $connessione): array
|
||||
{
|
||||
try {
|
||||
$client = $this->buildClient($connessione);
|
||||
$service = new GoogleCalendar($client);
|
||||
$calendarList = $service->calendarList->listCalendarList();
|
||||
return ['success' => true, 'message' => 'Connessione Google Calendar riuscita. Trovati ' . count($calendarList->getItems()) . ' calendari.'];
|
||||
} catch (\Exception $e) {
|
||||
return ['success' => false, 'message' => 'Errore: ' . $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
public function listCalendars(CalendarioConnessione $connessione): array
|
||||
{
|
||||
try {
|
||||
$client = $this->buildClient($connessione);
|
||||
$service = new GoogleCalendar($client);
|
||||
$calendarList = $service->calendarList->listCalendarList();
|
||||
$calendars = [];
|
||||
foreach ($calendarList->getItems() as $cal) {
|
||||
$calendars[] = [
|
||||
'id' => $cal->getId(),
|
||||
'summary' => $cal->getSummary(),
|
||||
];
|
||||
}
|
||||
return $calendars;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('GoogleCalendar listCalendars: ' . $e->getMessage());
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public function syncEvents(CalendarioConnessione $connessione): array
|
||||
{
|
||||
$config = $connessione->getDecryptedConfig();
|
||||
$imported = 0;
|
||||
$exported = 0;
|
||||
$errors = [];
|
||||
|
||||
try {
|
||||
$client = $this->buildClient($connessione);
|
||||
$this->calendarService = new GoogleCalendar($client);
|
||||
$this->calendarId = $config['calendar_id'] ?? 'primary';
|
||||
|
||||
$direction = $connessione->sync_direction ?? 'bidirectional';
|
||||
|
||||
if ($direction === 'import' || $direction === 'bidirectional') {
|
||||
$result = $this->importRemoteEvents($connessione);
|
||||
$imported = $result['count'];
|
||||
foreach ($result['errors'] as $err) {
|
||||
$errors[] = $err;
|
||||
}
|
||||
}
|
||||
|
||||
if ($direction === 'export' || $direction === 'bidirectional') {
|
||||
$result = $this->exportLocalEvents($connessione);
|
||||
$exported = $result['count'];
|
||||
foreach ($result['errors'] as $err) {
|
||||
$errors[] = $err;
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = $e->getMessage();
|
||||
Log::error('GoogleCalendar syncEvents: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => empty($errors),
|
||||
'imported' => $imported,
|
||||
'exported' => $exported,
|
||||
'errors' => $errors,
|
||||
];
|
||||
}
|
||||
|
||||
private function importRemoteEvents(CalendarioConnessione $connessione): array
|
||||
{
|
||||
$count = 0;
|
||||
$errors = [];
|
||||
$service = $this->calendarService;
|
||||
|
||||
try {
|
||||
$timeMin = Carbon::now()->subYear()->toRfc3339String();
|
||||
$timeMax = Carbon::now()->addYear()->toRfc3339String();
|
||||
|
||||
$optParams = [
|
||||
'timeMin' => $timeMin,
|
||||
'timeMax' => $timeMax,
|
||||
'singleEvents' => true,
|
||||
'orderBy' => 'startTime',
|
||||
];
|
||||
|
||||
$events = $service->events->listEvents($this->calendarId, $optParams);
|
||||
|
||||
while (true) {
|
||||
foreach ($events->getItems() as $event) {
|
||||
try {
|
||||
$uid = $event->getId();
|
||||
$existing = Evento::where('uid_esterno', $uid)->first();
|
||||
if ($existing) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$summary = $event->getSummary() ?? 'Evento importato';
|
||||
$description = strip_tags($event->getDescription() ?? '');
|
||||
$location = $event->getLocation() ?? '';
|
||||
|
||||
$start = $event->getStart();
|
||||
$end = $event->getEnd();
|
||||
|
||||
$data = [
|
||||
'nome_evento' => mb_substr($summary, 0, 255),
|
||||
'descrizione_evento' => mb_substr($description, 0, 255),
|
||||
'tipo_recorrenza' => 'singolo',
|
||||
'luogo_indirizzo' => $location,
|
||||
'uid_esterno' => $uid,
|
||||
];
|
||||
|
||||
if ($start && $start->getDateTime()) {
|
||||
$dt = Carbon::parse($start->getDateTime());
|
||||
$data['data_specifica'] = $dt->format('Y-m-d');
|
||||
$data['ora_inizio'] = $dt->format('H:i');
|
||||
} elseif ($start && $start->getDate()) {
|
||||
$data['data_specifica'] = $start->getDate();
|
||||
}
|
||||
|
||||
if ($end && $start && $end->getDateTime() && $start->getDateTime()) {
|
||||
$data['durata_minuti'] = (int) Carbon::parse($start->getDateTime())->diffInMinutes(Carbon::parse($end->getDateTime()));
|
||||
}
|
||||
|
||||
if ($event->getRecurrence()) {
|
||||
foreach ($event->getRecurrence() as $rrule) {
|
||||
if (str_starts_with($rrule, 'RRULE:')) {
|
||||
$parsed = $this->parseRRule(substr($rrule, 6));
|
||||
$data = array_merge($data, $parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Evento::create($data);
|
||||
$count++;
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = 'Errore import evento: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$pageToken = $events->getNextPageToken();
|
||||
if ($pageToken) {
|
||||
$optParams['pageToken'] = $pageToken;
|
||||
$events = $service->events->listEvents($this->calendarId, $optParams);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = 'Errore import Google Calendar: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
return ['count' => $count, 'errors' => $errors];
|
||||
}
|
||||
|
||||
private function exportLocalEvents(CalendarioConnessione $connessione): array
|
||||
{
|
||||
$count = 0;
|
||||
$errors = [];
|
||||
$service = $this->calendarService;
|
||||
|
||||
try {
|
||||
$localEvents = Evento::all();
|
||||
$remoteEventMap = $this->getRemoteEventMap($service);
|
||||
|
||||
foreach ($localEvents as $evento) {
|
||||
try {
|
||||
$googleEvent = new GoogleEvent();
|
||||
$googleEvent->setSummary($evento->nome_evento);
|
||||
|
||||
if ($evento->descrizione_evento) {
|
||||
$googleEvent->setDescription($evento->descrizione_evento);
|
||||
}
|
||||
if ($evento->luogo_indirizzo) {
|
||||
$googleEvent->setLocation($evento->luogo_indirizzo);
|
||||
}
|
||||
|
||||
$startData = $this->buildGoogleDateTime($evento, 'start');
|
||||
$endData = $this->buildGoogleDateTime($evento, 'end');
|
||||
$googleEvent->setStart($startData);
|
||||
$googleEvent->setEnd($endData);
|
||||
|
||||
if ($evento->tipo_recorrenza !== 'singolo') {
|
||||
$rrule = $this->buildGoogleRRule($evento);
|
||||
if ($rrule) {
|
||||
$googleEvent->setRecurrence(['RRULE:' . $rrule]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($evento->uid_esterno && isset($remoteEventMap[$evento->uid_esterno])) {
|
||||
$service->events->update($this->calendarId, $evento->uid_esterno, $googleEvent);
|
||||
} else {
|
||||
$created = $service->events->insert($this->calendarId, $googleEvent);
|
||||
if (!$evento->uid_esterno) {
|
||||
$evento->updateQuietly(['uid_esterno' => $created->getId()]);
|
||||
}
|
||||
}
|
||||
$count++;
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = 'Errore export ' . $evento->nome_evento . ': ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = 'Errore export Google Calendar: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
return ['count' => $count, 'errors' => $errors];
|
||||
}
|
||||
|
||||
private function getRemoteEventMap(GoogleCalendar $service): array
|
||||
{
|
||||
$map = [];
|
||||
try {
|
||||
$events = $service->events->listEvents($this->calendarId);
|
||||
foreach ($events->getItems() as $event) {
|
||||
$map[$event->getId()] = true;
|
||||
}
|
||||
} catch (\Exception) {
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
private function buildGoogleDateTime(Evento $evento, string $type): \Google\Service\Calendar\EventDateTime
|
||||
{
|
||||
$dt = new \Google\Service\Calendar\EventDateTime();
|
||||
|
||||
if ($type === 'start') {
|
||||
if ($evento->data_specifica) {
|
||||
$date = $evento->data_specifica instanceof Carbon ? $evento->data_specifica : Carbon::parse($evento->data_specifica);
|
||||
if ($evento->ora_inizio) {
|
||||
$time = $evento->ora_inizio instanceof Carbon ? $evento->ora_inizio : Carbon::parse($evento->ora_inizio);
|
||||
$dt->setDateTime($date->format('Y-m-d') . 'T' . $time->format('H:i:s'));
|
||||
$dt->setTimeZone('Europe/Rome');
|
||||
} else {
|
||||
$dt->setDate($date->format('Y-m-d'));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($evento->data_specifica) {
|
||||
$date = $evento->data_specifica instanceof Carbon ? $evento->data_specifica : Carbon::parse($evento->data_specifica);
|
||||
$durata = (int) ($evento->durata_minuti ?? 60);
|
||||
if ($evento->ora_inizio) {
|
||||
$time = $evento->ora_inizio instanceof Carbon ? $evento->ora_inizio : Carbon::parse($evento->ora_inizio);
|
||||
$endTime = $time->copy()->addMinutes($durata);
|
||||
$dt->setDateTime($date->format('Y-m-d') . 'T' . $endTime->format('H:i:s'));
|
||||
$dt->setTimeZone('Europe/Rome');
|
||||
} else {
|
||||
$dt->setDate($date->copy()->addDay()->format('Y-m-d'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $dt;
|
||||
}
|
||||
|
||||
private function buildGoogleRRule(Evento $evento): ?string
|
||||
{
|
||||
$dayMap = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'];
|
||||
|
||||
return match ($evento->tipo_recorrenza) {
|
||||
'settimanale' => $evento->giorno_settimana !== null
|
||||
? 'FREQ=WEEKLY;BYDAY=' . ($dayMap[(int) $evento->giorno_settimana] ?? 'MO')
|
||||
: null,
|
||||
'mensile' => $evento->occorrenza_mese
|
||||
? 'FREQ=MONTHLY;BYSETPOS=' . $evento->occorrenza_mese
|
||||
: null,
|
||||
'annuale' => $evento->mese_annuale
|
||||
? 'FREQ=YEARLY;BYMONTH=' . $evento->mese_annuale
|
||||
: null,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
private function parseRRule(string $rrule): array
|
||||
{
|
||||
$result = [];
|
||||
$parts = explode(';', $rrule);
|
||||
foreach ($parts as $part) {
|
||||
[$key, $value] = explode('=', $part, 2);
|
||||
if ($key === 'FREQ') {
|
||||
$result['tipo_recorrenza'] = match (strtolower($value)) {
|
||||
'weekly' => 'settimanale',
|
||||
'monthly' => 'mensile',
|
||||
'yearly' => 'annuale',
|
||||
default => 'singolo',
|
||||
};
|
||||
} elseif ($key === 'BYDAY') {
|
||||
$dayMap = ['SU' => 0, 'MO' => 1, 'TU' => 2, 'WE' => 3, 'TH' => 4, 'FR' => 5, 'SA' => 6];
|
||||
if (preg_match('/^(-?\d+)?([A-Z]+)$/', $value, $m)) {
|
||||
$result['giorno_settimana'] = $dayMap[$m[2]] ?? null;
|
||||
if (!empty($m[1])) {
|
||||
$result['occorrenza_mese'] = $m[1] . ',' . ($dayMap[$m[2]] ?? '1');
|
||||
}
|
||||
}
|
||||
} elseif ($key === 'BYMONTHDAY') {
|
||||
$result['giorno_mese'] = (int) $value;
|
||||
} elseif ($key === 'BYMONTH') {
|
||||
$result['mese_annuale'] = (int) $value;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function buildClient(CalendarioConnessione $connessione): GoogleClient
|
||||
{
|
||||
$config = $connessione->getDecryptedConfig();
|
||||
|
||||
$client = new GoogleClient();
|
||||
$client->setApplicationName(config('app.name'));
|
||||
$client->setScopes([GoogleCalendar::CALENDAR]);
|
||||
$client->setAuthConfig([
|
||||
'web' => [
|
||||
'client_id' => $config['client_id'] ?? '',
|
||||
'client_secret' => $config['client_secret'] ?? '',
|
||||
],
|
||||
]);
|
||||
$client->setAccessType('offline');
|
||||
$client->setPrompt('consent');
|
||||
|
||||
if (!empty($config['refresh_token'])) {
|
||||
$client->fetchAccessTokenWithRefreshToken($config['refresh_token']);
|
||||
}
|
||||
|
||||
return $client;
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,11 @@
|
||||
"php": "^8.3",
|
||||
"as247/flysystem-google-drive": "^3.0",
|
||||
"directorytree/imapengine-laravel": "^1.2",
|
||||
"google/apiclient": "^2.19",
|
||||
"laravel/framework": "^13.7",
|
||||
"laravel/tinker": "^3.0",
|
||||
"league/flysystem-webdav": "^3.31",
|
||||
"sabre/vobject": "^4.6",
|
||||
"spatie/laravel-permission": "^7.4",
|
||||
"webklex/php-imap": "^6.2"
|
||||
},
|
||||
|
||||
Generated
+6
-6
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "c6ab297804fe432e1792b166459e6469",
|
||||
"content-hash": "c594ad5c5f9ff96dc8644231135f694e",
|
||||
"packages": [
|
||||
{
|
||||
"name": "as247/cloud-storages",
|
||||
@@ -4271,16 +4271,16 @@
|
||||
},
|
||||
{
|
||||
"name": "sabre/vobject",
|
||||
"version": "4.5.8",
|
||||
"version": "4.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sabre-io/vobject.git",
|
||||
"reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1"
|
||||
"reference": "9432544fc369851fb8202c5d91159b2e669f0c88"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sabre-io/vobject/zipball/d554eb24d64232922e1eab5896cc2f84b3b9ffb1",
|
||||
"reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1",
|
||||
"url": "https://api.github.com/repos/sabre-io/vobject/zipball/9432544fc369851fb8202c5d91159b2e669f0c88",
|
||||
"reference": "9432544fc369851fb8202c5d91159b2e669f0c88",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4371,7 +4371,7 @@
|
||||
"issues": "https://github.com/sabre-io/vobject/issues",
|
||||
"source": "https://github.com/fruux/sabre-vobject"
|
||||
},
|
||||
"time": "2026-01-12T10:45:19+00:00"
|
||||
"time": "2026-05-31T13:04:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sabre/xml",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasTable('app_settings')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::create('app_settings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nome_applicazione')->nullable();
|
||||
$table->string('nome_organizzazione')->nullable();
|
||||
$table->string('logo')->nullable();
|
||||
$table->string('logo_small')->nullable();
|
||||
$table->string('logo_path')->nullable();
|
||||
$table->string('logo_small_path')->nullable();
|
||||
$table->string('footer_text')->nullable();
|
||||
$table->string('app_version')->nullable();
|
||||
$table->string('dashboard_welcome')->nullable();
|
||||
$table->boolean('show_version')->default(false);
|
||||
$table->string('documenti_storage_disk')->default('local');
|
||||
$table->string('documenti_storage_path')->default('documenti');
|
||||
$table->string('backup_path')->default('backups');
|
||||
$table->integer('backup_retention_days')->default(30);
|
||||
$table->boolean('backup_include_files')->default(true);
|
||||
$table->boolean('backup_include_env')->default(true);
|
||||
$table->boolean('backup_auto_enabled')->default(false);
|
||||
$table->string('backup_auto_frequency', 20)->default('daily');
|
||||
$table->integer('backup_auto_hour')->default(3);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('app_settings');
|
||||
}
|
||||
};
|
||||
@@ -8,10 +8,21 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('app_settings', function (Blueprint $table) {
|
||||
$table->string('logo_path')->nullable()->after('logo');
|
||||
$table->string('logo_small_path')->nullable()->after('logo_small');
|
||||
});
|
||||
if (!Schema::hasTable('app_settings')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('app_settings', 'logo_path')) {
|
||||
Schema::table('app_settings', function (Blueprint $table) {
|
||||
$table->string('logo_path')->nullable()->after('logo');
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('app_settings', 'logo_small_path')) {
|
||||
Schema::table('app_settings', function (Blueprint $table) {
|
||||
$table->string('logo_small_path')->nullable()->after('logo_small');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
|
||||
@@ -8,12 +8,24 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('app_settings', function (Blueprint $table) {
|
||||
$table->string('footer_text')->nullable()->after('nome_organizzazione');
|
||||
$table->string('app_version')->nullable()->after('footer_text');
|
||||
$table->string('dashboard_welcome')->nullable()->after('app_version');
|
||||
$table->boolean('show_version')->default(false)->after('dashboard_welcome');
|
||||
});
|
||||
if (!Schema::hasTable('app_settings')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$columns = ['footer_text', 'app_version', 'dashboard_welcome', 'show_version'];
|
||||
|
||||
foreach ($columns as $column) {
|
||||
if (!Schema::hasColumn('app_settings', $column)) {
|
||||
Schema::table('app_settings', function (Blueprint $table) use ($column) {
|
||||
match ($column) {
|
||||
'footer_text' => $table->string('footer_text')->nullable()->after('nome_organizzazione'),
|
||||
'app_version' => $table->string('app_version')->nullable()->after('footer_text'),
|
||||
'dashboard_welcome' => $table->string('dashboard_welcome')->nullable()->after('app_version'),
|
||||
'show_version' => $table->boolean('show_version')->default(false)->after('dashboard_welcome'),
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
|
||||
@@ -8,10 +8,21 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('app_settings', function (Blueprint $table) {
|
||||
$table->string('documenti_storage_disk')->default('local')->after('show_version');
|
||||
$table->string('documenti_storage_path')->default('documenti')->after('documenti_storage_disk');
|
||||
});
|
||||
if (!Schema::hasTable('app_settings')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('app_settings', 'documenti_storage_disk')) {
|
||||
Schema::table('app_settings', function (Blueprint $table) {
|
||||
$table->string('documenti_storage_disk')->default('local')->after('show_version');
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('app_settings', 'documenti_storage_path')) {
|
||||
Schema::table('app_settings', function (Blueprint $table) {
|
||||
$table->string('documenti_storage_path')->default('documenti')->after('documenti_storage_disk');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
|
||||
@@ -10,15 +10,31 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('app_settings', function (Blueprint $table) {
|
||||
$table->string('backup_path')->default('backups')->after('show_version');
|
||||
$table->integer('backup_retention_days')->default(30)->after('backup_path');
|
||||
$table->boolean('backup_include_files')->default(true)->after('backup_retention_days');
|
||||
$table->boolean('backup_include_env')->default(true)->after('backup_include_files');
|
||||
$table->boolean('backup_auto_enabled')->default(false)->after('backup_include_env');
|
||||
$table->string('backup_auto_frequency', 20)->default('daily')->after('backup_auto_enabled');
|
||||
$table->integer('backup_auto_hour')->default(3)->after('backup_auto_frequency');
|
||||
});
|
||||
if (!Schema::hasTable('app_settings')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$columns = [
|
||||
'backup_path' => ['type' => 'string', 'default' => 'backups'],
|
||||
'backup_retention_days' => ['type' => 'integer', 'default' => 30],
|
||||
'backup_include_files' => ['type' => 'boolean', 'default' => true],
|
||||
'backup_include_env' => ['type' => 'boolean', 'default' => true],
|
||||
'backup_auto_enabled' => ['type' => 'boolean', 'default' => false],
|
||||
'backup_auto_frequency' => ['type' => 'string', 'length' => 20, 'default' => 'daily'],
|
||||
'backup_auto_hour' => ['type' => 'integer', 'default' => 3],
|
||||
];
|
||||
|
||||
foreach ($columns as $column => $config) {
|
||||
if (!Schema::hasColumn('app_settings', $column)) {
|
||||
Schema::table('app_settings', function (Blueprint $table) use ($column, $config) {
|
||||
match ($config['type']) {
|
||||
'string' => $table->string($column, $config['length'] ?? 255)->default($config['default'])->after('show_version'),
|
||||
'integer' => $table->integer($column)->default($config['default'])->after('show_version'),
|
||||
'boolean' => $table->boolean($column)->default($config['default'])->after('show_version'),
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('calendario_connessioni', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nome');
|
||||
$table->string('tipo'); // google_calendar, caldav
|
||||
$table->text('config'); // JSON
|
||||
$table->string('stato')->default('disconnesso'); // connesso, disconnesso, errore
|
||||
$table->string('sync_direction')->default('bidirectional'); // import, export, bidirectional
|
||||
$table->integer('sync_interval_minutes')->default(60);
|
||||
$table->timestamp('last_sync_at')->nullable();
|
||||
$table->timestamp('last_error_at')->nullable();
|
||||
$table->text('last_error_message')->nullable();
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->integer('ordine')->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('calendario_connessioni');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('eventi', function (Blueprint $table) {
|
||||
$table->string('uid_esterno', 500)->nullable()->after('luogo_url_maps');
|
||||
$table->index('uid_esterno');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('eventi', function (Blueprint $table) {
|
||||
$table->dropIndex(['uid_esterno']);
|
||||
$table->dropColumn('uid_esterno');
|
||||
});
|
||||
}
|
||||
};
|
||||
Binary file not shown.
+848
@@ -0,0 +1,848 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Glastree — Installer interattivo per amministratori (PHP CLI)
|
||||
*
|
||||
* Uso:
|
||||
* php install.php
|
||||
*
|
||||
* Richiede: PHP 8.3+, Composer, MySQL o SQLite
|
||||
* Opzionale: npm (per asset frontend)
|
||||
*
|
||||
* Modalità:
|
||||
* 1) Fresh Install su Apache (LAMP)
|
||||
* 2) Fresh Install via Docker
|
||||
* 3) Restore da Backup (Apache)
|
||||
*/
|
||||
|
||||
// ── Safety ──────────────────────────────────────────
|
||||
declare(strict_types=1);
|
||||
|
||||
// ── Constants ────────────────────────────────────────
|
||||
const REQUIREMENTS = ['pdo_mysql', 'mbstring', 'xml', 'curl', 'zip', 'gd', 'fileinfo'];
|
||||
const MIN_PHP = '8.3.0';
|
||||
|
||||
// ── Utilities ────────────────────────────────────────
|
||||
function println(string $msg = ''): void { echo $msg . PHP_EOL; }
|
||||
|
||||
function color(string $text, string $code): string {
|
||||
$map = [
|
||||
'red' => '0;31',
|
||||
'green' => '0;32',
|
||||
'yellow' => '1;33',
|
||||
'cyan' => '0;36',
|
||||
'white' => '1;37',
|
||||
];
|
||||
$c = $map[$code] ?? '0';
|
||||
return "\033[{$c}m{$text}\033[0m";
|
||||
}
|
||||
|
||||
function info(string $msg): void { println(color('[INFO]', 'cyan') . ' ' . $msg); }
|
||||
function ok(string $msg): void { println(color('[OK]', 'green') . ' ' . $msg); }
|
||||
function warn(string $msg): void { println(color('[WARN]', 'yellow') . ' ' . $msg); }
|
||||
function error(string $msg): void { println(color('[ERR]', 'red') . ' ' . $msg); }
|
||||
|
||||
function fail(string $msg): never { error($msg); exit(1); }
|
||||
|
||||
function ask(string $prompt, ?string $default = null): string {
|
||||
$defaultStr = $default !== null ? " [{$default}]" : '';
|
||||
echo color('?', 'cyan') . " {$prompt}{$defaultStr}: ";
|
||||
$val = trim(fgets(STDIN) ?: '');
|
||||
return $val !== '' ? $val : ($default ?? '');
|
||||
}
|
||||
|
||||
function askRequired(string $prompt): string {
|
||||
while (true) {
|
||||
$val = ask($prompt);
|
||||
if ($val !== '') break;
|
||||
warn('Valore obbligatorio.');
|
||||
}
|
||||
return $val;
|
||||
}
|
||||
|
||||
function confirm(string $prompt): bool {
|
||||
$resp = strtolower(ask($prompt . ' [S/n]', 'S'));
|
||||
return $resp === 's' || $resp === '';
|
||||
}
|
||||
|
||||
function menu(string $prompt, array $options): string {
|
||||
foreach ($options as $i => $opt) {
|
||||
println(' ' . ($i + 1) . ') ' . $opt);
|
||||
}
|
||||
while (true) {
|
||||
$val = ask($prompt, '1');
|
||||
if (isset($options[(int)$val - 1])) return $options[(int)$val - 1];
|
||||
warn('Scelta non valida.');
|
||||
}
|
||||
}
|
||||
|
||||
function checkCmd(string $cmd): bool {
|
||||
$found = !(trim(shell_exec("command -v $cmd 2>/dev/null") ?: '') === '');
|
||||
if (!$found) warn("Comando '$cmd' non trovato.");
|
||||
return $found;
|
||||
}
|
||||
|
||||
function run(string $cmd, ?string $label = null): bool {
|
||||
if ($label) info($label);
|
||||
println(" \$ {$cmd}");
|
||||
passthru($cmd, $exitCode);
|
||||
if ($exitCode !== 0) warn("Comando terminato con codice {$exitCode}");
|
||||
return $exitCode === 0;
|
||||
}
|
||||
|
||||
function runCapture(string $cmd): string {
|
||||
return trim(shell_exec($cmd) ?: '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea l'utente amministratore via PDO prepared statement.
|
||||
* Nessun escaping PHP necessario (tutto via prepared statement).
|
||||
*/
|
||||
function createAdminUser(
|
||||
string $dbHost, string $dbPort, string $dbName,
|
||||
string $dbUser, string $dbPass,
|
||||
string $adminName, string $adminEmail, string $adminPass
|
||||
): bool {
|
||||
try {
|
||||
$pdo = new PDO(
|
||||
"mysql:host={$dbHost};port={$dbPort};dbname={$dbName}",
|
||||
$dbUser, $dbPass,
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||
);
|
||||
|
||||
$hash = password_hash($adminPass, PASSWORD_BCRYPT);
|
||||
$permissions = json_encode([
|
||||
'individui' => 2, 'gruppi' => 2, 'eventi' => 2,
|
||||
'documenti' => 2, 'mailing' => 2, 'viste' => 2,
|
||||
'report' => 2, 'settings' => 2,
|
||||
]);
|
||||
|
||||
// Elimina eventuale utente con stessa email (seeder)
|
||||
$stmt = $pdo->prepare('DELETE FROM users WHERE email = ?');
|
||||
$stmt->execute([$adminEmail]);
|
||||
|
||||
// Inserisce admin con is_admin=1, status='active', permissions completi
|
||||
$stmt = $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())"
|
||||
);
|
||||
$stmt->execute([$adminName, $adminEmail, $hash, $permissions]);
|
||||
|
||||
$userId = $pdo->lastInsertId();
|
||||
|
||||
// Assegna ruolo Spatie 'admin' (role_id = 1)
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO model_has_roles (role_id, model_type, model_id) VALUES (1, ?, ?)"
|
||||
);
|
||||
$stmt->execute(['App\\Models\\User', $userId]);
|
||||
|
||||
return true;
|
||||
} catch (\PDOException $e) {
|
||||
error("Errore creazione admin: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Banner ───────────────────────────────────────────
|
||||
println(color(' ____ _ _ _', 'white'));
|
||||
println(color(' / ___| | __ _ ___| |_ ___ _ __ ___ | |_ ___', 'white'));
|
||||
println(color('| | _| |/ _` / __| __/ _ \'__/ _ \ | __/ _ \\', 'white'));
|
||||
println(color('| |_| | | (_| \__ \ || __/ | | __/ | || __/', 'white'));
|
||||
println(color(' \____|_|\__,_|___/\__\___|_| \___| \__\___|', 'white'));
|
||||
println(color(' Installer interattivo per sysadmin', 'cyan'));
|
||||
println();
|
||||
|
||||
// ── Preflight ────────────────────────────────────────
|
||||
$scriptDir = dirname(__FILE__);
|
||||
chdir($scriptDir);
|
||||
|
||||
if (!file_exists('.env.example')) {
|
||||
fail("File .env.example non trovato in {$scriptDir}. Sei nella cartella dell'applicazione?");
|
||||
}
|
||||
|
||||
// ── Select mode ──────────────────────────────────────
|
||||
println('Seleziona la modalità di installazione:');
|
||||
$mode = menu('Scegli', [
|
||||
'Fresh Install su Apache (LAMP tradizionale)',
|
||||
'Fresh Install via Docker',
|
||||
'Restore da Backup (Apache)',
|
||||
]);
|
||||
|
||||
// ── Common params ────────────────────────────────────
|
||||
println();
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
println(color(' PARAMETRI GENERALI', 'yellow'));
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
|
||||
$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');
|
||||
$appUrl = askRequired('URL pubblico del sito (es. https://glastree.esempio.it)');
|
||||
|
||||
// ── DB params (Apache / Restore) ─────────────────────
|
||||
$dbType = 'mysql';
|
||||
$dbHost = '127.0.0.1';
|
||||
$dbPort = '3306';
|
||||
$dbName = 'glastree';
|
||||
$dbUser = 'glastree';
|
||||
$dbPass = '';
|
||||
|
||||
if (in_array($mode, ['Fresh Install su Apache (LAMP tradizionale)', 'Restore da Backup (Apache)'])) {
|
||||
println();
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
println(color(' DATABASE', 'yellow'));
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
|
||||
$dbType = menu('Tipo database', ['mysql', 'sqlite']);
|
||||
|
||||
if ($dbType === 'mysql') {
|
||||
$dbHost = askRequired('Host database');
|
||||
$dbPort = ask('Porta database', '3306');
|
||||
$dbName = ask('Nome database (verrà creato se non esiste)', 'glastree');
|
||||
$dbUser = askRequired('Utente database');
|
||||
$dbPass = askRequired('Password database');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Web server user detection ─────────────────────────
|
||||
$wwwUser = 'www-data';
|
||||
$sudo = '';
|
||||
if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
|
||||
$detected = trim(shell_exec('ps aux | grep -E "apache|httpd" | grep -v grep | head -1 | awk \'{print $1}\'') ?: '');
|
||||
if ($detected !== '' && $detected !== 'root') {
|
||||
$wwwUser = $detected;
|
||||
}
|
||||
if (function_exists('posix_getuid') && posix_getuid() === 0) {
|
||||
$sudo = "sudo -u {$wwwUser} ";
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════
|
||||
// MODE 1: FRESH INSTALL APACHE
|
||||
// ══════════════════════════════════════════════════════
|
||||
if ($mode === 'Fresh Install su Apache (LAMP tradizionale)') {
|
||||
println();
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
println(color(' PRE-FLIGHT CHECKS', 'yellow'));
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
|
||||
// PHP version
|
||||
$phpVer = PHP_VERSION;
|
||||
info("PHP versione: {$phpVer}");
|
||||
if (version_compare($phpVer, MIN_PHP, '<')) {
|
||||
fail("PHP >= " . MIN_PHP . " richiesto (trovato {$phpVer})");
|
||||
}
|
||||
ok("PHP {$phpVer}");
|
||||
|
||||
// Extensions
|
||||
foreach (REQUIREMENTS as $ext) {
|
||||
if (!extension_loaded($ext)) {
|
||||
warn("Estensione PHP '{$ext}' non trovata (alcune funzionalità potrebbero non funzionare)");
|
||||
} else {
|
||||
ok("Estensione PHP '{$ext}'");
|
||||
}
|
||||
}
|
||||
|
||||
// Composer
|
||||
checkCmd('composer');
|
||||
checkCmd('node');
|
||||
checkCmd('npm');
|
||||
|
||||
// ── Step 1: Environment ─────────────────────────
|
||||
println();
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
println(color(' 1/7 — CONFIGURAZIONE AMBIENTE', 'yellow'));
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
|
||||
if (!file_exists('.env')) {
|
||||
copy('.env.example', '.env');
|
||||
ok('.env creato da .env.example');
|
||||
} else {
|
||||
info('.env già esistente, lo aggiorno');
|
||||
}
|
||||
|
||||
// APP_KEY
|
||||
runCapture("{$sudo}php artisan key:generate --force");
|
||||
ok('APP_KEY generata');
|
||||
|
||||
// Config
|
||||
$dotenv = file_get_contents('.env');
|
||||
$replacements = [
|
||||
'/^APP_NAME=.*/m' => 'APP_NAME="' . addslashes($appName) . '"',
|
||||
'/^APP_URL=.*/m' => "APP_URL={$appUrl}",
|
||||
'/^APP_ENV=.*/m' => 'APP_ENV=production',
|
||||
'/^APP_DEBUG=.*/m' => 'APP_DEBUG=false',
|
||||
];
|
||||
|
||||
if ($dbType === 'mysql') {
|
||||
$replacements['/^DB_CONNECTION=.*/m'] = 'DB_CONNECTION=mysql';
|
||||
$replacements['/^DB_HOST=.*/m'] = "DB_HOST={$dbHost}";
|
||||
$replacements['/^DB_PORT=.*/m'] = "DB_PORT={$dbPort}";
|
||||
$replacements['/^DB_DATABASE=.*/m'] = "DB_DATABASE={$dbName}";
|
||||
$replacements['/^DB_USERNAME=.*/m'] = "DB_USERNAME={$dbUser}";
|
||||
$replacements['/^DB_PASSWORD=.*/m'] = "DB_PASSWORD={$dbPass}";
|
||||
} else {
|
||||
$replacements['/^DB_CONNECTION=.*/m'] = 'DB_CONNECTION=sqlite';
|
||||
$replacements['/^DB_HOST=.*/m'] = '# DB_HOST=';
|
||||
$replacements['/^DB_PORT=.*/m'] = '# DB_PORT=';
|
||||
$replacements['/^DB_DATABASE=.*/m'] = 'DB_DATABASE=' . $scriptDir . '/database/database.sqlite';
|
||||
$replacements['/^DB_USERNAME=.*/m'] = '# DB_USERNAME=';
|
||||
$replacements['/^DB_PASSWORD=.*/m'] = '# DB_PASSWORD=';
|
||||
}
|
||||
|
||||
foreach ($replacements as $pattern => $replacement) {
|
||||
$dotenv = preg_replace($pattern, $replacement, $dotenv);
|
||||
}
|
||||
file_put_contents('.env', $dotenv);
|
||||
ok('.env configurato');
|
||||
|
||||
// ── Step 2: Database ────────────────────────────
|
||||
println();
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
println(color(' 2/7 — DATABASE', 'yellow'));
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
|
||||
if ($dbType === 'mysql') {
|
||||
info("Connessione a MySQL {$dbHost}:{$dbPort}, database '{$dbName}'...");
|
||||
|
||||
try {
|
||||
new PDO(
|
||||
"mysql:host={$dbHost};port={$dbPort};dbname={$dbName}",
|
||||
$dbUser, $dbPass,
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 5]
|
||||
);
|
||||
ok("Accesso a '{$dbName}' come '{$dbUser}' riuscito");
|
||||
} catch (\PDOException $e) {
|
||||
// Diagnostica l'errore
|
||||
$errMsg = $e->getMessage();
|
||||
$diagnosi = match (true) {
|
||||
(bool)preg_match('/1049|Unknown database/', $errMsg) => "Database '{$dbName}' non esiste",
|
||||
(bool)preg_match('/1045|Access denied for user/', $errMsg) => "Utente '{$dbUser}' o password non validi",
|
||||
(bool)preg_match('/1044/', $errMsg) => "Utente '{$dbUser}' non ha accesso al database '{$dbName}'",
|
||||
default => $errMsg,
|
||||
};
|
||||
warn($diagnosi);
|
||||
|
||||
info("Connessione come root per creare/riparare utente e database...");
|
||||
$rootPdo = null;
|
||||
try {
|
||||
$rootPdo = new PDO(
|
||||
"mysql:host={$dbHost};port={$dbPort}",
|
||||
'root', '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 5]
|
||||
);
|
||||
} catch (\PDOException) {
|
||||
$rootPass = askRequired('Password utente root MySQL (lascia vuoto se senza password)');
|
||||
try {
|
||||
$rootPdo = new PDO(
|
||||
"mysql:host={$dbHost};port={$dbPort}",
|
||||
'root', $rootPass,
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 5]
|
||||
);
|
||||
} catch (\PDOException $r) {
|
||||
fail("Connessione come root fallita: " . $r->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$safeUser = str_replace(["'", '"', '`', "\0", '\\'], '', $dbUser);
|
||||
$safePass = str_replace(["'", '"', '`', "\0", '\\'], '', $dbPass);
|
||||
$safeName = str_replace(["'", '"', '`', "\0", '\\'], '', $dbName);
|
||||
|
||||
$rootPdo->exec("CREATE DATABASE IF NOT EXISTS `{$safeName}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
|
||||
foreach (['localhost', '%'] as $host) {
|
||||
$rootPdo->exec("CREATE USER IF NOT EXISTS '{$safeUser}'@'{$host}' IDENTIFIED BY '{$safePass}'");
|
||||
$rootPdo->exec("GRANT ALL PRIVILEGES ON `{$safeName}`.* TO '{$safeUser}'@'{$host}'");
|
||||
}
|
||||
$rootPdo->exec("FLUSH PRIVILEGES");
|
||||
ok("Database '{$dbName}' e utente '{$dbUser}' configurati con permessi completi");
|
||||
}
|
||||
} else {
|
||||
$sqlitePath = $scriptDir . '/database/database.sqlite';
|
||||
if (!file_exists($sqlitePath)) {
|
||||
touch($sqlitePath);
|
||||
chmod($sqlitePath, 0664);
|
||||
}
|
||||
ok('SQLite pronto: ' . $sqlitePath);
|
||||
}
|
||||
|
||||
// ── Step 3: Composer ────────────────────────────
|
||||
println();
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
println(color(' 3/7 — INSTALLAZIONE DIPENDENZE', 'yellow'));
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
|
||||
run("COMPOSER_ROOT_VERSION=dev-main composer install --no-dev --optimize-autoloader", 'Installazione dipendenze Composer...');
|
||||
ok('Dipendenze PHP installate');
|
||||
|
||||
// ── Step 4: Package discovery ────────────────────
|
||||
println();
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
println(color(' 4/7 — REGISTRAZIONE PACCHETTI', 'yellow'));
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
|
||||
runCapture("{$sudo}php artisan package:discover --ansi 2>/dev/null");
|
||||
ok('Pacchetti registrati');
|
||||
|
||||
// ── Step 5: Schema + Seed ────────────────────────
|
||||
println();
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
println(color(' 5/7 — SCHEMA DATABASE E DATI BASE', 'yellow'));
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
|
||||
$sqlFile = $scriptDir . '/database/install.sql';
|
||||
if (!file_exists($sqlFile)) {
|
||||
fail("File database/install.sql non trovato in {$scriptDir}");
|
||||
}
|
||||
|
||||
if ($dbType === 'mysql') {
|
||||
$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.");
|
||||
}
|
||||
} else {
|
||||
// SQLite fallback: usa le migration (install.sql è solo per MySQL)
|
||||
if (!run("{$sudo}php artisan migrate --seed --force", 'Esecuzione migration e seed...')) {
|
||||
fail('Migration fallita. Verifica la configurazione SQLite.');
|
||||
}
|
||||
}
|
||||
ok('Database popolato con struttura e dati base');
|
||||
|
||||
runCapture("{$sudo}php artisan storage:link --force 2>/dev/null");
|
||||
|
||||
// ── Step 6: Admin user (via PDO — no tinker) ─────
|
||||
println();
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
println(color(' 6/7 — CREAZIONE AMMINISTRATORE', 'yellow'));
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
|
||||
if ($dbType === 'mysql') {
|
||||
if (createAdminUser($dbHost, $dbPort, $dbName, $dbUser, $dbPass, $adminName, $adminEmail, $adminPass)) {
|
||||
ok("Amministratore {$adminEmail} creato con permessi completi");
|
||||
} else {
|
||||
fail("Creazione admin fallita. Verifica i dati inseriti.");
|
||||
}
|
||||
} else {
|
||||
// SQLite: usa seeder (l'admin verrà creato da DatabaseSeeder)
|
||||
// Poi aggiorna la password con quella scelta dall'utente
|
||||
runCapture("{$sudo}php artisan tinker --execute=" . escapeshellarg(
|
||||
"\$u = \App\Models\User::first(); " .
|
||||
"if (\$u) { " .
|
||||
"\$u->name = " . var_export($adminName, true) . "; " .
|
||||
"\$u->email = " . var_export($adminEmail, true) . "; " .
|
||||
"\$u->password = bcrypt(" . var_export($adminPass, true) . "); " .
|
||||
"\$u->is_admin = true; " .
|
||||
"\$u->status = 'active'; " .
|
||||
"\$u->permissions = " . var_export([
|
||||
'individui' => 2, 'gruppi' => 2, 'eventi' => 2,
|
||||
'documenti' => 2, 'mailing' => 2, 'viste' => 2,
|
||||
'report' => 2, 'settings' => 2,
|
||||
], true) . "; " .
|
||||
"\$u->role_preset_id = 1; " .
|
||||
"\$u->save(); " .
|
||||
"echo 'OK'; " .
|
||||
"}"
|
||||
) . " 2>/dev/null");
|
||||
ok("Amministratore {$adminEmail} configurato");
|
||||
}
|
||||
|
||||
// Clear all caches after schema + admin creation
|
||||
runCapture("{$sudo}php artisan optimize:clear 2>/dev/null");
|
||||
ok('Cache ottimizzate');
|
||||
|
||||
// ── Step 7: Asset ────────────────────────────────
|
||||
println();
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
println(color(' 7/7 — ASSET FRONTEND', 'yellow'));
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
|
||||
$hasNpm = checkCmd('npm');
|
||||
if ($hasNpm && confirm('Compilare gli asset frontend con npm?')) {
|
||||
run("{$sudo}npm install", 'Installazione dipendenze npm...');
|
||||
run("{$sudo}npm run build", 'Compilazione asset...');
|
||||
ok('Asset compilati');
|
||||
} else {
|
||||
if (!$hasNpm) warn('npm non disponibile — asset non compilati (AdminLTE via CDN)');
|
||||
else info('Asset non compilati (AdminLTE via CDN)');
|
||||
}
|
||||
|
||||
// ── Permissions ──────────────────────────────────
|
||||
if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
|
||||
info('Impostazione permessi...');
|
||||
runCapture("chown -R {$wwwUser}:{$wwwUser} vendor storage bootstrap/cache 2>/dev/null");
|
||||
chmod("{$scriptDir}/storage", 0775);
|
||||
chmod("{$scriptDir}/bootstrap/cache", 0775);
|
||||
ok("Permessi impostati (utente: {$wwwUser})");
|
||||
}
|
||||
|
||||
// ── Summary ──────────────────────────────────────
|
||||
println();
|
||||
println(color('══════════════════════════════════════════', 'green'));
|
||||
println(color(' INSTALLAZIONE COMPLETATA!', 'green'));
|
||||
println(color('══════════════════════════════════════════', 'green'));
|
||||
println();
|
||||
println(" URL: {$appUrl}");
|
||||
println(" Admin: {$adminEmail}");
|
||||
println(" Cartella: {$scriptDir}");
|
||||
println();
|
||||
warn('Configura Apache con il VirtualHost (vedi Guida → Installazione, Passo 4).');
|
||||
if ($dbType === 'mysql') {
|
||||
warn('Se hai appena creato il database, assicurati che l\'utente abbia privilegi completi.');
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════
|
||||
// MODE 2: DOCKER
|
||||
// ══════════════════════════════════════════════════════
|
||||
if ($mode === 'Fresh Install via Docker') {
|
||||
println();
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
println(color(' PRE-FLIGHT CHECKS (Docker)', 'yellow'));
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
|
||||
checkCmd('docker');
|
||||
|
||||
println();
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
println(color(' CONFIGURAZIONE DOCKER', 'yellow'));
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
|
||||
$dockerDb = menu('Tipo database per Docker', ['sqlite', 'mysql']);
|
||||
$dockerPort = ask('Porta host (es. 8080)', '8080');
|
||||
|
||||
$composeFile = $dockerDb === 'mysql' ? 'docker-compose.mysql.yml' : 'docker-compose.yml';
|
||||
|
||||
println();
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
println(color(' INSTALLAZIONE DOCKER', 'yellow'));
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
|
||||
if (!run("docker compose -f {$composeFile} build", 'Build immagine Docker...')) {
|
||||
fail('Build fallita');
|
||||
}
|
||||
|
||||
// Override compose con variabili d'ambiente
|
||||
$override = [
|
||||
'services:',
|
||||
' glastree:',
|
||||
' environment:',
|
||||
" - APP_NAME={$appName}",
|
||||
" - APP_URL={$appUrl}",
|
||||
' - APP_ENV=production',
|
||||
' - APP_DEBUG=false',
|
||||
];
|
||||
file_put_contents('docker-compose.override.yml', implode(PHP_EOL, $override) . PHP_EOL);
|
||||
|
||||
if (!run(
|
||||
"docker compose -f {$composeFile} -f docker-compose.override.yml up -d",
|
||||
'Avvio container...'
|
||||
)) {
|
||||
@unlink('docker-compose.override.yml');
|
||||
fail('Avvio container fallito');
|
||||
}
|
||||
|
||||
// Attendi container pronto
|
||||
info('Attendo che il container sia pronto...');
|
||||
$containerId = runCapture("docker compose -f {$composeFile} ps -q glastree 2>/dev/null");
|
||||
$adminCreated = false;
|
||||
|
||||
if ($containerId !== '') {
|
||||
for ($i = 0; $i < 30; $i++) {
|
||||
$status = runCapture("docker inspect -f '{{.State.Status}}' {$containerId} 2>/dev/null");
|
||||
if ($status === 'running') {
|
||||
$http = runCapture("docker exec {$containerId} sh -c 'wget -q -O- http://localhost/ 2>/dev/null && echo OK'");
|
||||
if (str_contains($http, 'OK')) {
|
||||
info('Container pronto');
|
||||
break;
|
||||
}
|
||||
}
|
||||
sleep(2);
|
||||
}
|
||||
|
||||
// Import schema SQL nel container
|
||||
$sqlFile = $scriptDir . '/database/install.sql';
|
||||
if (file_exists($sqlFile) && $dockerDb === 'mysql') {
|
||||
info('Importazione schema nel container...');
|
||||
runCapture("docker cp {$sqlFile} {$containerId}:/tmp/install.sql 2>/dev/null");
|
||||
$mysqlHost = 'mysql';
|
||||
$mysqlUser = 'glastree';
|
||||
$mysqlPass = 'secret';
|
||||
$mysqlDb = 'glastree';
|
||||
$importResult = runCapture(
|
||||
"docker exec {$containerId} sh -c 'mysql -h {$mysqlHost} -u {$mysqlUser} -p{$mysqlPass} {$mysqlDb} < /tmp/install.sql' 2>&1"
|
||||
);
|
||||
if ($importResult !== '') {
|
||||
warn("Import SQL: {$importResult}");
|
||||
} else {
|
||||
ok('Schema database importato');
|
||||
}
|
||||
} elseif ($dockerDb === 'sqlite') {
|
||||
run("docker exec {$containerId} php artisan migrate --seed --force", 'Migration e seed...');
|
||||
}
|
||||
|
||||
// Crea admin
|
||||
if ($dockerDb === 'mysql') {
|
||||
// Script PHP temporaneo con var_export (safe)
|
||||
$adminScriptPath = sys_get_temp_dir() . '/glastree-create-admin-' . uniqid() . '.php';
|
||||
$hash = password_hash($adminPass, PASSWORD_BCRYPT);
|
||||
$perm = json_encode([
|
||||
'individui' => 2, 'gruppi' => 2, 'eventi' => 2,
|
||||
'documenti' => 2, 'mailing' => 2, 'viste' => 2,
|
||||
'report' => 2, 'settings' => 2,
|
||||
]);
|
||||
$nameExp = var_export($adminName, true);
|
||||
$emailExp = var_export($adminEmail, true);
|
||||
$hashExp = var_export($hash, true);
|
||||
$userExp = var_export($mysqlUser, true);
|
||||
$passExp = var_export($mysqlPass, true);
|
||||
$dbExp = var_export($mysqlDb, true);
|
||||
$hostExp = var_export($mysqlHost, true);
|
||||
|
||||
$adminPhp = <<<PHP
|
||||
<?php
|
||||
\$pdo = new PDO("mysql:host=" . {$hostExp} . ";dbname=" . {$dbExp}, {$userExp}, {$passExp}, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||
\$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();
|
||||
\$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);
|
||||
runCapture("docker cp {$adminScriptPath} {$containerId}:/tmp/create_admin.php 2>/dev/null");
|
||||
$adminResult = runCapture("docker exec {$containerId} php /tmp/create_admin.php 2>/dev/null");
|
||||
@unlink($adminScriptPath);
|
||||
} else {
|
||||
// SQLite: usa tinker via docker exec
|
||||
$adminResult = runCapture("docker exec {$containerId} php artisan tinker --execute=" . escapeshellarg(
|
||||
"\$u = \App\Models\User::first(); " .
|
||||
"if (\$u) { " .
|
||||
"\$u->name = " . var_export($adminName, true) . "; " .
|
||||
"\$u->email = " . var_export($adminEmail, true) . "; " .
|
||||
"\$u->password = bcrypt(" . var_export($adminPass, true) . "); " .
|
||||
"\$u->is_admin = true; " .
|
||||
"\$u->status = 'active'; " .
|
||||
"\$u->permissions = " . var_export([
|
||||
'individui' => 2, 'gruppi' => 2, 'eventi' => 2,
|
||||
'documenti' => 2, 'mailing' => 2, 'viste' => 2,
|
||||
'report' => 2, 'settings' => 2,
|
||||
], true) . "; " .
|
||||
"\$u->role_preset_id = 1; " .
|
||||
"\$u->save(); " .
|
||||
"echo 'OK'; " .
|
||||
"}"
|
||||
) . " 2>/dev/null");
|
||||
}
|
||||
|
||||
if (str_contains($adminResult ?? '', 'OK')) {
|
||||
ok("Amministratore {$adminEmail} creato nel container");
|
||||
$adminCreated = true;
|
||||
} else {
|
||||
warn("Creazione admin fallita — output: " . (($adminResult ?? '') ?: 'nessun output'));
|
||||
}
|
||||
}
|
||||
|
||||
@unlink('docker-compose.override.yml');
|
||||
|
||||
println();
|
||||
println(color('══════════════════════════════════════════', 'green'));
|
||||
println(color(' INSTALLAZIONE DOCKER COMPLETATA!', 'green'));
|
||||
println(color('══════════════════════════════════════════', 'green'));
|
||||
println();
|
||||
println(" URL: http://localhost:{$dockerPort}");
|
||||
println(" Admin: {$adminEmail}");
|
||||
println(" Container: {$containerId}");
|
||||
println();
|
||||
warn('Per URL pubblici, imposta APP_URL nelle environment del container.');
|
||||
warn('Dati persistenti: volumi Docker (glastree_storage, glastree_db o mysql_data).');
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════
|
||||
// MODE 3: RESTORE DA BACKUP
|
||||
// ══════════════════════════════════════════════════════
|
||||
if ($mode === 'Restore da Backup (Apache)') {
|
||||
println();
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
println(color(' PRE-FLIGHT CHECKS (Restore)', 'yellow'));
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
|
||||
checkCmd('php');
|
||||
checkCmd('composer');
|
||||
checkCmd('unzip');
|
||||
|
||||
if ($dbType === 'mysql') {
|
||||
checkCmd('mysql');
|
||||
}
|
||||
|
||||
$backupZip = askRequired('Percorso del file ZIP di backup');
|
||||
if (!file_exists($backupZip)) {
|
||||
fail("File non trovato: {$backupZip}");
|
||||
}
|
||||
|
||||
println();
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
println(color(' RESTORE DA BACKUP', 'yellow'));
|
||||
println(color('══════════════════════════════════════════', 'yellow'));
|
||||
|
||||
// Extract
|
||||
$tmpDir = sys_get_temp_dir() . '/glastree-restore-' . uniqid();
|
||||
mkdir($tmpDir, 0700, true);
|
||||
info("Estrazione backup in {$tmpDir}...");
|
||||
|
||||
if (!run("unzip -qo {$backupZip} -d {$tmpDir}", null)) {
|
||||
// pulisci tmp dir prima di fallire
|
||||
runCapture("rm -rf {$tmpDir}");
|
||||
fail('Estrazione backup fallita');
|
||||
}
|
||||
|
||||
// Environment
|
||||
if (!file_exists('.env')) {
|
||||
copy('.env.example', '.env');
|
||||
}
|
||||
|
||||
// Restore .env from backup preserving DB credentials
|
||||
if (file_exists("{$tmpDir}/.env")) {
|
||||
$currentEnv = file_get_contents('.env');
|
||||
$backupEnv = file_get_contents("{$tmpDir}/.env");
|
||||
|
||||
// Save current DB_* values
|
||||
$currentDb = [];
|
||||
foreach (['DB_CONNECTION', 'DB_HOST', 'DB_PORT', 'DB_DATABASE', 'DB_USERNAME', 'DB_PASSWORD'] as $key) {
|
||||
if (preg_match("/^{$key}=(.*)$/m", $currentEnv, $m)) {
|
||||
$currentDb[$key] = $m[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Use backup .env as base
|
||||
file_put_contents('.env', $backupEnv);
|
||||
|
||||
// Restore current DB credentials
|
||||
foreach ($currentDb as $key => $val) {
|
||||
$dotenv = file_get_contents('.env');
|
||||
$dotenv = preg_replace("/^{$key}=.*/m", "{$key}={$val}", $dotenv);
|
||||
file_put_contents('.env', $dotenv);
|
||||
}
|
||||
|
||||
ok('.env ripristinato dal backup (credenziali DB preservate)');
|
||||
}
|
||||
|
||||
// Update APP_* config
|
||||
$dotenv = file_get_contents('.env');
|
||||
$dotenv = preg_replace('/^APP_NAME=.*/m', 'APP_NAME="' . addslashes($appName) . '"', $dotenv);
|
||||
$dotenv = preg_replace('/^APP_URL=.*/m', "APP_URL={$appUrl}", $dotenv);
|
||||
$dotenv = preg_replace('/^APP_ENV=.*/m', 'APP_ENV=production', $dotenv);
|
||||
$dotenv = preg_replace('/^APP_DEBUG=.*/m', 'APP_DEBUG=false', $dotenv);
|
||||
file_put_contents('.env', $dotenv);
|
||||
|
||||
// Generate APP_KEY
|
||||
runCapture("{$sudo}php artisan key:generate --force");
|
||||
ok('APP_KEY rigenerata');
|
||||
|
||||
// Import database
|
||||
if ($dbType === 'mysql') {
|
||||
$sqlFile = "{$tmpDir}/database.sql";
|
||||
if (file_exists($sqlFile)) {
|
||||
info("Importazione database MySQL: {$sqlFile}");
|
||||
$cmd = "mysql -h {$dbHost} -P {$dbPort} -u {$dbUser} -p{$dbPass} {$dbName} < {$sqlFile}";
|
||||
if (run($cmd, null)) {
|
||||
ok('Database importato');
|
||||
} else {
|
||||
warn('Import database fallito — importa manualmente');
|
||||
}
|
||||
} else {
|
||||
warn('database.sql non trovato nel backup');
|
||||
}
|
||||
} else {
|
||||
warn('Restore automatico supportato solo con MySQL. Per SQLite ripristina manualmente i file.');
|
||||
}
|
||||
|
||||
// Restore storage
|
||||
$storageBackup = "{$tmpDir}/storage";
|
||||
if (is_dir($storageBackup)) {
|
||||
info('Ripristino file storage...');
|
||||
runCapture("cp -r {$storageBackup}/* {$scriptDir}/storage/ 2>/dev/null");
|
||||
ok('Storage ripristinato');
|
||||
}
|
||||
|
||||
// Composer
|
||||
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
|
||||
runCapture("{$sudo}php artisan storage:link --force 2>/dev/null");
|
||||
runCapture("{$sudo}php artisan optimize:clear");
|
||||
|
||||
// Permissions
|
||||
if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
|
||||
runCapture("chown -R {$wwwUser}:{$wwwUser} vendor storage bootstrap/cache 2>/dev/null");
|
||||
chmod("{$scriptDir}/storage", 0775);
|
||||
chmod("{$scriptDir}/bootstrap/cache", 0775);
|
||||
ok("Permessi impostati (utente: {$wwwUser})");
|
||||
}
|
||||
|
||||
// Create admin if not exists (via PDO)
|
||||
if ($dbType === 'mysql') {
|
||||
$exists = false;
|
||||
try {
|
||||
$pdo = new PDO("mysql:host={$dbHost};port={$dbPort};dbname={$dbName}", $dbUser, $dbPass);
|
||||
$stmt = $pdo->prepare('SELECT COUNT(*) FROM users WHERE email = ?');
|
||||
$stmt->execute([$adminEmail]);
|
||||
$exists = (int) $stmt->fetchColumn() > 0;
|
||||
} catch (\PDOException) {
|
||||
// Ignora — tenteremo la creazione
|
||||
}
|
||||
|
||||
if ($exists) {
|
||||
ok("Amministratore {$adminEmail} già esistente");
|
||||
} elseif (createAdminUser($dbHost, $dbPort, $dbName, $dbUser, $dbPass, $adminName, $adminEmail, $adminPass)) {
|
||||
ok("Amministratore {$adminEmail} creato");
|
||||
} else {
|
||||
warn("Creazione admin fallita — crealo manualmente dal pannello admin");
|
||||
}
|
||||
} else {
|
||||
// SQLite fallback: usa tinker
|
||||
$result = runCapture("{$sudo}php artisan tinker --execute=" . escapeshellarg(
|
||||
"\$e = \App\Models\User::where('email', " . var_export($adminEmail, true) . ")->exists(); " .
|
||||
"if (!\$e) { " .
|
||||
"\$u = new \App\Models\User; " .
|
||||
"\$u->name = " . var_export($adminName, true) . "; " .
|
||||
"\$u->email = " . var_export($adminEmail, true) . "; " .
|
||||
"\$u->password = bcrypt(" . var_export($adminPass, true) . "); " .
|
||||
"\$u->is_admin = true; " .
|
||||
"\$u->status = 'active'; " .
|
||||
"\$u->permissions = " . var_export([
|
||||
'individui' => 2, 'gruppi' => 2, 'eventi' => 2,
|
||||
'documenti' => 2, 'mailing' => 2, 'viste' => 2,
|
||||
'report' => 2, 'settings' => 2,
|
||||
], true) . "; " .
|
||||
"\$u->role_preset_id = 1; " .
|
||||
"\$u->save(); " .
|
||||
"echo 'OK'; " .
|
||||
"} else { echo 'EXISTS'; }"
|
||||
) . " 2>/dev/null");
|
||||
if (str_contains($result, 'OK')) {
|
||||
ok("Amministratore {$adminEmail} creato");
|
||||
} elseif (str_contains($result, 'EXISTS')) {
|
||||
ok("Amministratore {$adminEmail} già esistente");
|
||||
} else {
|
||||
warn("Creazione admin fallita (output: " . ($result ?: 'vuoto') . ")");
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
runCapture("rm -rf {$tmpDir}");
|
||||
|
||||
println();
|
||||
println(color('══════════════════════════════════════════', 'green'));
|
||||
println(color(' RESTORE COMPLETATO!', 'green'));
|
||||
println(color('══════════════════════════════════════════', 'green'));
|
||||
println();
|
||||
println(" URL: {$appUrl}");
|
||||
println(" Admin: {$adminEmail}");
|
||||
println(" Cartella: {$scriptDir}");
|
||||
println();
|
||||
warn('Verifica che APP_KEY non abbia invalidato dati criptati (token OAuth, password IMAP).');
|
||||
warn('Se necessario, riconfigura SMTP e Google Drive dalle Impostazioni.');
|
||||
}
|
||||
|
||||
println();
|
||||
@@ -14,9 +14,14 @@
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Attività di Sistema</h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-danger btn-sm" onclick="deleteLogs()">
|
||||
<i class="fas fa-trash mr-1"></i> Cancella Log
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="GET" class="mb-3">
|
||||
<form method="GET" class="mb-3" id="filter-form">
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<select name="user_id" class="form-control">
|
||||
@@ -94,4 +99,59 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
function deleteLogs() {
|
||||
var filterForm = document.getElementById('filter-form');
|
||||
var params = new URLSearchParams(new FormData(filterForm));
|
||||
var filterDescription = [];
|
||||
|
||||
var userFilter = params.get('user_id');
|
||||
var moduleFilter = params.get('module');
|
||||
var actionFilter = params.get('action');
|
||||
var fromFilter = params.get('from');
|
||||
var toFilter = params.get('to');
|
||||
|
||||
if (userFilter) filterDescription.push('utente=' + userFilter);
|
||||
if (moduleFilter) filterDescription.push('modulo=' + moduleFilter);
|
||||
if (actionFilter) filterDescription.push('azione=' + actionFilter);
|
||||
if (fromFilter) filterDescription.push('dal=' + fromFilter);
|
||||
if (toFilter) filterDescription.push('al=' + toFilter);
|
||||
|
||||
var message = filterDescription.length > 0
|
||||
? 'Eliminare i log filtrati? (' + filterDescription.join(', ') + ')'
|
||||
: 'Eliminare TUTTI i log?';
|
||||
|
||||
if (!confirm(message)) return;
|
||||
|
||||
var form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/admin/activity-logs';
|
||||
form.style.display = 'none';
|
||||
|
||||
var csrf = document.createElement('input');
|
||||
csrf.name = '_token';
|
||||
csrf.value = '{{ csrf_token() }}';
|
||||
form.appendChild(csrf);
|
||||
|
||||
var method = document.createElement('input');
|
||||
method.name = '_method';
|
||||
method.value = 'DELETE';
|
||||
form.appendChild(method);
|
||||
|
||||
filterForm.querySelectorAll('select, input').forEach(function(el) {
|
||||
if (el.name && el.value) {
|
||||
var input = document.createElement('input');
|
||||
input.name = el.name;
|
||||
input.value = el.value;
|
||||
form.appendChild(input);
|
||||
}
|
||||
});
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
@@ -377,7 +377,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<div class="card-footer d-flex flex-wrap align-items-center gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save mr-1"></i> Salva Impostazioni
|
||||
</button>
|
||||
@@ -390,6 +390,12 @@
|
||||
<a href="{{ route('impostazioni.email.sync') }}" class="btn btn-warning ml-2">
|
||||
<i class="fas fa-sync mr-1"></i> Sincronizza Ora
|
||||
</a>
|
||||
<form action="{{ route('impostazioni.email.destroy') }}" method="POST" class="ml-auto" onsubmit="return confirm('Eliminare definitivamente la configurazione email? Tutti i dati di connessione verranno rimossi.')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="fas fa-trash mr-1"></i> Elimina Configurazione
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -521,7 +527,7 @@ function editSender(id) {
|
||||
const sender = senderAccounts.find(s => s.id === id);
|
||||
if (!sender) return;
|
||||
|
||||
document.getElementById('senderForm').action = '{{ route('impostazioni.sender.update', '') }}/' + id;
|
||||
document.getElementById('senderForm').action = '{{ route('impostazioni.sender.update', '__ID__') }}'.replace('__ID__', id);
|
||||
document.getElementById('senderMethod').value = 'PUT';
|
||||
document.getElementById('senderModalTitle').textContent = 'Modifica Mittente - ' + sender.email_address;
|
||||
document.getElementById('senderId').value = sender.id;
|
||||
@@ -551,7 +557,7 @@ function testSenderSmtp(id) {
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
|
||||
$.ajax({
|
||||
url: '{{ route('impostazioni.sender.test', '') }}/' + id,
|
||||
url: '{{ route('impostazioni.sender.test', '__ID__') }}'.replace('__ID__', id),
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
|
||||
data: { email: email },
|
||||
@@ -628,9 +634,11 @@ function testSmtp() {
|
||||
});
|
||||
}
|
||||
|
||||
var quillEditorInstance = null;
|
||||
|
||||
function createEditor() {
|
||||
var container = document.querySelector('#signature-editor');
|
||||
if (!container || container.querySelector('.ql-toolbar')) return;
|
||||
if (!container || quillEditorInstance) return;
|
||||
|
||||
var quill = new Quill('#signature-editor', {
|
||||
theme: 'snow',
|
||||
@@ -679,10 +687,12 @@ function createEditor() {
|
||||
signatureInput.value = quill.root.innerHTML;
|
||||
}
|
||||
});
|
||||
|
||||
quillEditorInstance = quill;
|
||||
}
|
||||
|
||||
function initSignatureEditor() {
|
||||
if (document.querySelector('#signature-editor .ql-toolbar')) return;
|
||||
if (quillEditorInstance) return;
|
||||
|
||||
if (typeof Quill === 'undefined') {
|
||||
var script = document.createElement('script');
|
||||
|
||||
@@ -59,7 +59,7 @@ $appOrgName = AppSetting::getOrgName() ?? '';
|
||||
<div class="row">
|
||||
<div class="col-8">
|
||||
<div class="icheck-primary">
|
||||
<input type="checkbox" id="remember">
|
||||
<input type="checkbox" id="remember" name="remember">
|
||||
<label for="remember">Ricordami</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -211,11 +211,6 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-file mr-2"></i>Documenti</h3>
|
||||
@@ -304,7 +299,7 @@
|
||||
</a>
|
||||
<form action="/eventi/{{ $evento->id }}" method="POST" class="d-inline">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn btn-danger" onclick="return confirm('Confermi l\'eliminazione di {{ $evento->nome_evento }}?')">
|
||||
<button type="submit" class="btn btn-danger" onclick="return confirm('Confermi l\'eliminazione di {{ $evento->nome_evento }}?'.replace(/'/g, '\\\''))">
|
||||
<i class="fas fa-trash mr-1"></i> Elimina
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -152,8 +152,7 @@
|
||||
</tr>
|
||||
<tr id="membro-edit-{{ $membro->id }}" style="display:none;">
|
||||
<td colspan="6">
|
||||
<form action="/gruppi/{{ $gruppo->id }}/membri/{{ $membro->id }}" method="POST" class="mb-0">
|
||||
@csrf @method('PUT')
|
||||
<div class="mb-0">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<input type="text" class="form-control form-control-sm" value="{{ $membro->cognome }} {{ $membro->nome }}" disabled>
|
||||
@@ -162,7 +161,7 @@
|
||||
<input type="text" class="form-control form-control-sm" value="{{ $membro->codice_id }}" disabled>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select name="ruolo_ids[]" class="form-control form-control-sm" multiple size="3">
|
||||
<select id="edit-membro-ruoli-{{ $membro->id }}" class="form-control form-control-sm" multiple size="3">
|
||||
@foreach(\App\Models\Ruolo::attive() as $ruolo)
|
||||
@php $selected = in_array($ruolo->id, $membro->getRuoloIdsForGruppo($gruppo->id)) @endphp
|
||||
<option value="{{ $ruolo->id }}" {{ $selected ? 'selected' : '' }}>{{ $ruolo->nome }}</option>
|
||||
@@ -170,14 +169,14 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="date" name="data_adesione" class="form-control form-control-sm" value="{{ $membro->pivot->data_adesione }}">
|
||||
<input type="date" id="edit-membro-data-{{ $membro->id }}" class="form-control form-control-sm" value="{{ $membro->pivot->data_adesione }}">
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<button type="submit" class="btn btn-xs btn-success"><i class="fas fa-check"></i></button>
|
||||
<button type="button" class="btn btn-xs btn-success" onclick="submitEditMembro({{ $membro->id }})"><i class="fas fa-check"></i></button>
|
||||
<button type="button" class="btn btn-xs btn-secondary" onclick="cancelEditMembro({{ $membro->id }})"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@@ -201,17 +200,13 @@
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div id="documento-add-form" style="display:none;" class="p-3 bg-light border-bottom">
|
||||
<form action="/documenti" method="POST" enctype="multipart/form-data" class="mb-0">
|
||||
@csrf
|
||||
<input type="hidden" name="visibilita" value="gruppo">
|
||||
<input type="hidden" name="visibilita_target_id" value="{{ $gruppo->id }}">
|
||||
<input type="hidden" name="visibilita_target_type" value="App\Models\Gruppo">
|
||||
<div class="mb-0">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<input type="text" name="nome_file" class="form-control form-control-sm" placeholder="Nome documento" required>
|
||||
<input type="text" id="doc-nome-file" class="form-control form-control-sm" placeholder="Nome documento">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select name="tipologia" class="form-control form-control-sm" required>
|
||||
<select id="doc-tipologia" class="form-control form-control-sm">
|
||||
<option value="">Tipologia...</option>
|
||||
<option value="documento">Documento</option>
|
||||
<option value="statuto">Statuto</option>
|
||||
@@ -221,14 +216,14 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<input type="file" name="file" class="form-control form-control-sm" required>
|
||||
<input type="file" id="doc-file" class="form-control form-control-sm">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-xs btn-success"><i class="fas fa-upload mr-1"></i>Carica</button>
|
||||
<button type="button" class="btn btn-xs btn-success" onclick="submitUploadDocumentoGruppo()"><i class="fas fa-upload mr-1"></i>Carica</button>
|
||||
<button type="button" class="btn btn-xs btn-secondary" onclick="hideDocumentoForm()"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($gruppo->documenti && $gruppo->documenti->count() > 0)
|
||||
@@ -258,12 +253,9 @@
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
@endif
|
||||
<form action="/documenti/{{ $documento->id }}" method="POST" class="d-inline">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn btn-xs btn-danger" onclick="return confirm('Eliminare questo documento?')" title="Elimina">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
<button type="button" class="btn btn-xs btn-danger" onclick="deleteDocumentoFromEdit({{ $documento->id }})" title="Elimina">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@@ -473,5 +465,83 @@ function previewDocumento(id, mimeType) {
|
||||
document.getElementById('previewDownloadBtn').href = '/documenti/' + id + '/download';
|
||||
$('#previewModal').modal('show');
|
||||
}
|
||||
|
||||
function submitEditMembro(membroId) {
|
||||
var ruoloSelect = document.getElementById('edit-membro-ruoli-' + membroId);
|
||||
var dataInput = document.getElementById('edit-membro-data-' + membroId);
|
||||
var formData = new URLSearchParams();
|
||||
|
||||
var selectedRuoli = Array.from(ruoloSelect.selectedOptions).map(opt => opt.value).filter(v => v);
|
||||
selectedRuoli.forEach(function(ruoloId) {
|
||||
formData.append('ruolo_ids[]', ruoloId);
|
||||
});
|
||||
formData.append('data_adesione', dataInput ? dataInput.value : '');
|
||||
|
||||
fetch('/gruppi/{{ $gruppo->id }}/membri/' + membroId, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: formData.toString()
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) return response.text().then(text => { throw new Error(text); });
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) window.location.reload();
|
||||
else alert(data.error || 'Errore');
|
||||
})
|
||||
.catch(error => alert('Errore: ' + error.message));
|
||||
}
|
||||
|
||||
function submitUploadDocumentoGruppo() {
|
||||
var nomeFile = document.getElementById('doc-nome-file');
|
||||
var tipologia = document.getElementById('doc-tipologia');
|
||||
var fileInput = document.getElementById('doc-file');
|
||||
|
||||
if (!nomeFile.value || !tipologia.value || !fileInput.files[0]) {
|
||||
alert('Compila tutti i campi');
|
||||
return;
|
||||
}
|
||||
|
||||
var formData = new FormData();
|
||||
formData.append('_token', '{{ csrf_token() }}');
|
||||
formData.append('visibilita', 'gruppo');
|
||||
formData.append('visibilita_target_id', '{{ $gruppo->id }}');
|
||||
formData.append('visibilita_target_type', 'App\\Models\\Gruppo');
|
||||
formData.append('nome_file', nomeFile.value);
|
||||
formData.append('tipologia', tipologia.value);
|
||||
formData.append('file', fileInput.files[0]);
|
||||
|
||||
document.getElementById('documento-add-form').querySelector('.btn-success').disabled = true;
|
||||
|
||||
fetch('/documenti', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: { 'Accept': 'application/json' }
|
||||
})
|
||||
.then(function() { window.location.reload(); })
|
||||
.catch(function() { window.location.reload(); });
|
||||
}
|
||||
|
||||
function deleteDocumentoFromEdit(documentoId) {
|
||||
if (!confirm('Eliminare questo documento?')) return;
|
||||
|
||||
var formData = new FormData();
|
||||
formData.append('_token', '{{ csrf_token() }}');
|
||||
formData.append('_method', 'DELETE');
|
||||
formData.append('_redirect', '{{ url()->current() }}');
|
||||
|
||||
fetch('/documenti/' + documentoId, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: { 'Accept': 'application/json' }
|
||||
})
|
||||
.then(function() { window.location.reload(); })
|
||||
.catch(function() { window.location.reload(); });
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
@@ -12,6 +12,7 @@ $canDeleteGruppi = Auth::user()->canDelete('gruppi');
|
||||
<li class="breadcrumb-item active">{{ $gruppo->nome }}</li>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success alert-dismissible">
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
@@ -24,8 +25,6 @@ $canDeleteGruppi = Auth::user()->canDelete('gruppi');
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="card card-success">
|
||||
|
||||
@@ -287,7 +287,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Backup e Migrazione --}}
|
||||
<div class="tab-pane" id="help-backup">
|
||||
@@ -471,9 +470,8 @@ sudo certbot --apache -d glastree.esempio.it</code></pre>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<h5>PHP Web Installer</h5>
|
||||
<p>L'applicazione include un <strong>wizard di installazione web</strong> nella cartella <code>installer/</code>. Questo wizard gestisce sia installazioni fresh che ripristino da backup.</p>
|
||||
<p>Vedi la sezione <strong>Installazione</strong> per la guida completa.</p>
|
||||
<h5>Comandi Artisan</h5>
|
||||
<p>Per installazione e ripristino usa i comandi Artisan da terminale. Vedi la sezione <strong>Installazione</strong> per la guida completa.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -777,7 +775,7 @@ echo 'Utente creato con successo!';
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5>Panoramica</h5>
|
||||
<p>l'applicazione include un <strong>wizard di installazione web</strong> che guida passo-passo nella configurazione su un nuovo server LAMP. Supporta due modalita:</p>
|
||||
<p>L'installazione avviene tramite l'interattivo <code>install.php</code> che guida passo-passo. Supporta due modalita:</p>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="callout callout-info">
|
||||
@@ -831,19 +829,37 @@ git clone <URL_REPOSITORY> glastree
|
||||
# Opzione B — ZIP (se non hai git)
|
||||
# unzip /percorso/del/glastree.zip -d glastree
|
||||
|
||||
cd glastree
|
||||
composer install --no-dev --optimize-autoloader
|
||||
npm ci && npm run build</code></pre>
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Se non hai accesso a npm, puoi saltare il build. L'interfaccia usera comunque AdminLTE via CDN.
|
||||
cd glastree</code></pre>
|
||||
|
||||
<h6>Passo 3: Eseguire l'installer</h6>
|
||||
<p>Lancerai l'installer interattivo che ti guidera attraverso tutte le fasi:</p>
|
||||
<pre><code>php install.php</code></pre>
|
||||
<p>L'installer ti chiedera:</p>
|
||||
<ol>
|
||||
<li><strong>Modalità</strong>: Fresh Install (Apache/Docker) o Restore da Backup</li>
|
||||
<li><strong>Parametri generali</strong>: nome sito, amministratore (nome, email, password), URL pubblico</li>
|
||||
<li><strong>Database</strong>: MySQL (host, porta, nome, utente, password) o SQLite</li>
|
||||
</ol>
|
||||
<p>L'installer esegue automaticamente:</p>
|
||||
<ul>
|
||||
<li>Generazione <code>.env</code> e <code>APP_KEY</code></li>
|
||||
<li>Creazione del database</li>
|
||||
<li><code>composer install</code> e registrazione pacchetti</li>
|
||||
<li>Migration e seed dei dati di base</li>
|
||||
<li>Creazione dell'utente amministratore</li>
|
||||
<li>Compilazione asset frontend (se npm disponibile)</li>
|
||||
<li>Impostazione permessi cartelle</li>
|
||||
</ul>
|
||||
<div class="callout callout-info">
|
||||
<h6><i class="fas fa-info-circle"></i> Docker</h6>
|
||||
<p>Se scegli la modalita Docker, l'installer builda l'immagine, avvia i container e crea l'admin all'interno del container.</p>
|
||||
</div>
|
||||
<div class="callout callout-success">
|
||||
<h6><i class="fas fa-upload"></i> Restore da Backup</h6>
|
||||
<p>Se scegli Restore, l'installer ti chiedera il percorso del file ZIP di backup, ripristinera il database, i file e la configurazione.</p>
|
||||
</div>
|
||||
|
||||
<h6>Passo 3: Permessi cartelle</h6>
|
||||
<pre><code>sudo chown -R www-data:www-data storage bootstrap/cache
|
||||
sudo chmod -R 775 storage bootstrap/cache</code></pre>
|
||||
|
||||
<h6>Passo 4: Configurare Apache</h6>
|
||||
<h6 class="mt-4">Passo 4: Configurare Apache (solo modalita Fresh Install)</h6>
|
||||
<pre><code>sudo tee /etc/apache2/sites-available/glastree.conf <<APACHE
|
||||
<VirtualHost *:80>
|
||||
ServerName glastree.esempio.it
|
||||
@@ -863,55 +879,29 @@ APACHE
|
||||
sudo a2dissite 000-default.conf
|
||||
sudo a2ensite glastree.conf
|
||||
sudo systemctl reload apache2</code></pre>
|
||||
|
||||
<h6>Passo 5: Avviare il wizard di installazione</h6>
|
||||
<p>Apri il browser e visita l'indirizzo del tuo server:</p>
|
||||
<pre><code>http://glastree.esempio.it/installer/</code></pre>
|
||||
|
||||
<p>Il wizard ti guidera attraverso 6 passi:</p>
|
||||
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead><tr><th>Passo</th><th>Descrizione</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>1. Benvenuto</td><td>Scegli Fresh Install o Restore da Backup</td></tr>
|
||||
<tr><td>2. Requisiti</td><td>Verifica PHP, estensioni e permessi</td></tr>
|
||||
<tr><td>3. Database</td><td>Configura MySQL (crea nuovo o usa esistente)</td></tr>
|
||||
<tr><td>4. Installazione</td><td>Fresh: migration e seed. Backup: upload ZIP</td></tr>
|
||||
<tr><td>5. Amministratore</td><td>Fresh: crea super-admin. Backup: verifica utenti</td></tr>
|
||||
<tr><td>6. Finalizzazione</td><td>Cache, storage link, auto-eliminazione installer</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<strong>Attenzione:</strong> Al termine dell'installazione, la cartella <code>installer/</code> viene <strong>automaticamente eliminata</strong>. Non puoi rieseguire l'installazione se non ricreando manualmente la cartella.
|
||||
<strong>Importante:</strong> Il restore da backup richiede che tu abbia un file ZIP generato dalla pagina <strong>Admin → Backup</strong> del server originale. Estrai manualmente il contenuto seguendo la struttura del progetto.
|
||||
</div>
|
||||
|
||||
<h5 class="mt-4">Modalita Fresh Install</h5>
|
||||
<p>Seleziona questa modalita quando installi l'applicazione per la prima volta su un server.</p>
|
||||
<p>Cosa succede durante l'installazione:</p>
|
||||
<h5 class="mt-4">Cosa succede durante l'installazione</h5>
|
||||
<p>I comandi <code>php artisan migrate --seed</code> eseguono:</p>
|
||||
<ol>
|
||||
<li>Genera <code>APP_KEY</code> per la crittografia</li>
|
||||
<li>Genera <code>APP_KEY</code> per la crittografia (gia eseguito al passo 5)</li>
|
||||
<li>Esegue tutte le migration per creare le tabelle del database</li>
|
||||
<li>Popola i dati di base (diocesi, comuni, ruoli, preset)</li>
|
||||
<li>Crea il collegamento <code>storage → public/storage</code></li>
|
||||
<li>Crea l'utente amministratore con i permessi completi</li>
|
||||
</ol>
|
||||
|
||||
<h5 class="mt-4">Modalita Restore da Backup</h5>
|
||||
<p>Seleziona questa modalita quando vuoi migrare un'installazione esistente su un nuovo server.</p>
|
||||
<p>Cosa serve:</p>
|
||||
<h5 class="mt-4">Restore da Backup</h5>
|
||||
<p>Il backup ZIP generato dalla pagina <strong>Admin → Backup</strong> contiene un dump SQL e i file dei documenti. Per ripristinare:</p>
|
||||
<ul>
|
||||
<li>Un file ZIP generato dalla pagina <strong>Admin → Backup</strong> del server originale</li>
|
||||
<li>Il wizard accetta upload diretto del file o un percorso sul server</li>
|
||||
<li>Estrai il file ZIP in una cartella temporanea</li>
|
||||
<li>Importa il database dal file SQL (<code>mysql -u root -p glastree < backup/database.sql</code>)</li>
|
||||
<li>Copia i file in <code>storage/app/</code> per ripristinare i documenti</li>
|
||||
<li>Copia il file <code>.env</code> dal backup (modifica le credenziali DB per il nuovo server)</li>
|
||||
<li>Esegui <code>php artisan key:generate</code> se la <code>APP_KEY</code> del backup non è utilizzabile</li>
|
||||
</ul>
|
||||
<p>Cosa succede durante il restore:</p>
|
||||
<ol>
|
||||
<li>Estrae il file ZIP in una cartella temporanea</li>
|
||||
<li>Importa <code>database.sql</code> nel database MySQL configurato</li>
|
||||
<li>Copia i file documenti in <code>storage/app/documenti/</code></li>
|
||||
<li>Ripristina il file <code>.env</code> dal backup (preservando le credenziali DB del nuovo server)</li>
|
||||
</ol>
|
||||
|
||||
<h5 class="mt-4">Dopo l'Installazione</h5>
|
||||
<ol>
|
||||
|
||||
@@ -268,7 +268,7 @@
|
||||
<h2 id="installazione">5. Guida all'Installazione</h2>
|
||||
|
||||
<h3>Panoramica</h3>
|
||||
<p>L'applicazione include un <strong>wizard di installazione web</strong> che guida passo-passo nella configurazione su un nuovo server LAMP. Supporta due modalità:</p>
|
||||
<p>L'installazione avviene tramite l'interattivo <code>install.php</code> che guida passo-passo. Supporta due modalità:</p>
|
||||
|
||||
<table>
|
||||
<thead><tr><th>Modalità</th><th>Descrizione</th></tr></thead>
|
||||
@@ -313,16 +313,15 @@ git clone <URL_REPOSITORY> glastree
|
||||
# Opzione B — ZIP (se non hai git)
|
||||
# unzip /percorso/del/glastree.zip -d glastree
|
||||
|
||||
cd glastree
|
||||
composer install --no-dev --optimize-autoloader
|
||||
npm ci && npm run build</pre>
|
||||
<p>Se non hai accesso a npm, puoi saltare il build. L'interfaccia userà comunque AdminLTE via CDN.</p>
|
||||
cd glastree</pre>
|
||||
|
||||
<h4>Passo 3: Permessi cartelle</h4>
|
||||
<pre>sudo chown -R www-data:www-data storage bootstrap/cache
|
||||
sudo chmod -R 775 storage bootstrap/cache</pre>
|
||||
<h4>Passo 3: Eseguire l'installer</h4>
|
||||
<p>Lancia l'installer interattivo che guida attraverso tutte le fasi:</p>
|
||||
<pre>php install.php</pre>
|
||||
<p>L'installer chiede: modalità (Fresh Install Apache/Docker o Restore), nome sito, dati amministratore, URL pubblico, database (MySQL o SQLite).<br>
|
||||
Esegue automaticamente: <code>.env</code> + <code>APP_KEY</code>, composer, migration/seed, admin user, asset (se npm presente), permessi.</p>
|
||||
|
||||
<h4>Passo 4: Configurare Apache</h4>
|
||||
<h4>Passo 4: Configurare Apache (solo Fresh Install)</h4>
|
||||
<pre>sudo tee /etc/apache2/sites-available/glastree.conf <<APACHE
|
||||
<VirtualHost *:80>
|
||||
ServerName glastree.esempio.it
|
||||
@@ -343,51 +342,27 @@ sudo a2dissite 000-default.conf
|
||||
sudo a2ensite glastree.conf
|
||||
sudo systemctl reload apache2</pre>
|
||||
|
||||
<h4>Passo 5: Avviare il wizard di installazione</h4>
|
||||
<p>Apri il browser e visita: <code>http://glastree.esempio.it/installer/</code></p>
|
||||
<p>Configura SSL con Let's Encrypt dopo l'installazione.</p>
|
||||
php artisan tinker --execute="
|
||||
\$u = new \App\Models\User;
|
||||
\$u->name = 'Admin';
|
||||
\$u->email = 'admin@esempio.it';
|
||||
\$u->password = bcrypt('password_sicura');
|
||||
\$u->save();
|
||||
"</pre>
|
||||
|
||||
<p>Il wizard ti guiderà attraverso 6 passi:</p>
|
||||
<table>
|
||||
<thead><tr><th>Passo</th><th>Descrizione</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>1. Benvenuto</td><td>Scegli Fresh Install o Restore da Backup</td></tr>
|
||||
<tr><td>2. Requisiti</td><td>Verifica PHP, estensioni e permessi</td></tr>
|
||||
<tr><td>3. Database</td><td>Configura MySQL (crea nuovo o usa esistente)</td></tr>
|
||||
<tr><td>4. Installazione</td><td>Fresh: migration e seed. Backup: upload ZIP</td></tr>
|
||||
<tr><td>5. Amministratore</td><td>Fresh: crea super-admin. Backup: verifica utenti</td></tr>
|
||||
<tr><td>6. Finalizzazione</td><td>Cache, storage link, auto-eliminazione installer</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong>Restore da Backup:</strong></p>
|
||||
<pre># Importa il database
|
||||
# mysql -u root -p glastree < database.sql
|
||||
|
||||
<div class="callout callout-warning">
|
||||
<strong>Attenzione:</strong> Al termine dell'installazione, la cartella <code>installer/</code> viene <strong>automaticamente eliminata</strong>. Non puoi rieseguire l'installazione se non ricreando manualmente la cartella.
|
||||
</div>
|
||||
# Ripristina i file dei documenti
|
||||
# tar -xzf storage.tar.gz -C storage/
|
||||
|
||||
<h3>Modalità Fresh Install</h3>
|
||||
<p>Seleziona questa modalità quando installi l'applicazione per la prima volta su un server.</p>
|
||||
<p>Cosa succede durante l'installazione:</p>
|
||||
<ol>
|
||||
<li>Genera <code>APP_KEY</code> per la crittografia</li>
|
||||
<li>Esegue tutte le migration per creare le tabelle del database</li>
|
||||
<li>Popola i dati di base (diocesi, comuni, ruoli, preset)</li>
|
||||
<li>Crea il collegamento <code>storage → public/storage</code></li>
|
||||
<li>Crea l'utente amministratore con i permessi completi</li>
|
||||
</ol>
|
||||
# Rigenera APP_KEY
|
||||
php artisan key:generate
|
||||
|
||||
<h3>Modalità Restore da Backup</h3>
|
||||
<p>Seleziona questa modalità quando vuoi migrare un'installazione esistente su un nuovo server.</p>
|
||||
<p>Cosa serve:</p>
|
||||
<ul>
|
||||
<li>Un file ZIP generato dalla pagina <strong>Admin → Backup</strong> del server originale</li>
|
||||
<li>Il wizard accetta upload diretto del file o un percorso sul server</li>
|
||||
</ul>
|
||||
<p>Cosa succede durante il restore:</p>
|
||||
<ol>
|
||||
<li>Estrae il file ZIP in una cartella temporanea</li>
|
||||
<li>Importa <code>database.sql</code> nel database MySQL configurato</li>
|
||||
<li>Copia i file documenti in <code>storage/app/documenti/</code></li>
|
||||
<li>Ripristina il file <code>.env</code> dal backup (preservando le credenziali DB del nuovo server)</li>
|
||||
</ol>
|
||||
# Crea il collegamento storage
|
||||
php artisan storage:link</pre>
|
||||
|
||||
<h3>Dopo l'Installazione</h3>
|
||||
<ol>
|
||||
@@ -611,9 +586,8 @@ sudo certbot --apache -d glastree.esempio.it</pre>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>PHP Web Installer</h3>
|
||||
<p>L'applicazione include un <strong>wizard di installazione web</strong> nella cartella <code>installer/</code>. Questo wizard gestisce sia installazioni fresh che ripristino da backup.</p>
|
||||
<p>Vedi la sezione <strong>Installazione</strong> per la guida completa.</p>
|
||||
<h3>Comandi Artisan</h3>
|
||||
<p>Per installazione e ripristino usa i comandi Artisan da terminale. Vedi la sezione <strong>Installazione</strong> per la guida completa.</p>
|
||||
|
||||
<div class="page-break"></div>
|
||||
|
||||
|
||||
@@ -709,7 +709,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<div class="card-footer d-flex flex-wrap align-items-center gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save mr-1"></i> Salva Impostazioni Email
|
||||
</button>
|
||||
@@ -722,6 +722,12 @@
|
||||
<a href="{{ route('impostazioni.email.sync') }}" class="btn btn-warning ml-2">
|
||||
<i class="fas fa-sync mr-1"></i> Sincronizza Ora
|
||||
</a>
|
||||
<form action="{{ route('impostazioni.email.destroy') }}" method="POST" class="ml-auto" onsubmit="return confirm('Eliminare definitivamente la configurazione email? Tutti i dati di connessione verranno rimossi.')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="fas fa-trash mr-1"></i> Elimina Configurazione
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -730,19 +736,104 @@
|
||||
<div class="tab-pane" id="calendario">
|
||||
<div class="card card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Calendari Remoti</h3>
|
||||
<h3 class="card-title"><i class="fas fa-calendar-alt mr-2"></i> Calendari Remoti</h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-sm btn-success" data-toggle="modal" data-target="#calendarioModal" onclick="resetCalendarioForm()">
|
||||
<i class="fas fa-plus mr-1"></i> Nuova Connessione
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">Configura la sincronizzazione con calendari esterni (Google Calendar, CalDAV, ecc.).</p>
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<strong>Prossimamente:</strong> Potrai collegare calendari Google, Infomaniak o altri provider CalDAV per sincronizzare gli eventi del calendario.
|
||||
<p class="text-muted">Configura la sincronizzazione con calendari esterni (Google Calendar, CalDAV come Infomaniak/NextCloud).</p>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success">{{ session('success') }}</div>
|
||||
@endif
|
||||
@if(session('error'))
|
||||
<div class="alert alert-danger">{{ session('error') }}</div>
|
||||
@endif
|
||||
|
||||
@php
|
||||
$calendarioConnessioni = \App\Models\CalendarioConnessione::orderBy('ordine')->get();
|
||||
@endphp
|
||||
|
||||
@if($calendarioConnessioni->count() > 0)
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40px;">#</th>
|
||||
<th>Nome</th>
|
||||
<th>Tipo</th>
|
||||
<th>Direzione Sync</th>
|
||||
<th>Stato</th>
|
||||
<th>Ultimo Sync</th>
|
||||
<th style="width:200px;">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="calendarioSortable">
|
||||
@foreach($calendarioConnessioni as $cal)
|
||||
<tr data-id="{{ $cal->id }}">
|
||||
<td class="text-center handle" style="cursor:grab;"><i class="fas fa-grip-vertical text-muted"></i></td>
|
||||
<td><strong>{{ $cal->nome }}</strong></td>
|
||||
<td>
|
||||
<span class="badge badge-{{ $cal->tipo === 'google_calendar' ? 'danger' : 'info' }}">
|
||||
<i class="fas {{ $cal->tipo === 'google_calendar' ? 'fa-google' : 'fa-calendar-alt' }} mr-1"></i>
|
||||
{{ \App\Models\CalendarioConnessione::etichettaTipo($cal->tipo) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
@switch($cal->sync_direction)
|
||||
@case('import') <span class="badge badge-primary"><i class="fas fa-download mr-1"></i> Solo Import</span> @break
|
||||
@case('export') <span class="badge badge-warning"><i class="fas fa-upload mr-1"></i> Solo Export</span> @break
|
||||
@default <span class="badge badge-success"><i class="fas fa-exchange-alt mr-1"></i> Bidirezionale</span>
|
||||
@endswitch
|
||||
</td>
|
||||
<td>
|
||||
@if($cal->stato === 'connesso')
|
||||
<span class="badge badge-success"><i class="fas fa-check-circle mr-1"></i> Connesso</span>
|
||||
@elseif($cal->stato === 'errore')
|
||||
<span class="badge badge-danger" title="{{ $cal->last_error_message ?? '' }}"><i class="fas fa-exclamation-circle mr-1"></i> Errore</span>
|
||||
@else
|
||||
<span class="badge badge-secondary"><i class="fas fa-circle mr-1"></i> {{ ucfirst($cal->stato) }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($cal->last_sync_at)
|
||||
<small class="text-muted">{{ $cal->last_sync_at->format('d/m/Y H:i') }}</small>
|
||||
@else
|
||||
<small class="text-muted">Mai</small>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-xs btn-info" onclick="editCalendario({{ $cal->id }})" title="Modifica">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-xs btn-success" onclick="testCalendario({{ $cal->id }}, this)" title="Test Connessione">
|
||||
<i class="fas fa-plug"></i>
|
||||
</button>
|
||||
<form action="{{ route('calendario-connessioni.sync', $cal->id) }}" method="POST" class="d-inline" onsubmit="return confirm('Avviare la sincronizzazione ora?')">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-xs btn-primary" title="Sincronizza Ora">
|
||||
<i class="fas fa-sync"></i>
|
||||
</button>
|
||||
</form>
|
||||
<button type="button" class="btn btn-xs btn-danger" onclick="deleteCalendario({{ $cal->id }}, '{{ addslashes($cal->nome) }}')" title="Elimina">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="text-center p-5 bg-light rounded">
|
||||
<i class="fas fa-calendar-plus fa-4x text-muted mb-3"></i>
|
||||
<p class="text-muted">Nessun calendario remoto configurato.</p>
|
||||
<p class="text-muted small">La funzionalità sarà disponibile in un prossimo aggiornamento.</p>
|
||||
@else
|
||||
<div class="text-center text-muted py-4">
|
||||
<i class="fas fa-calendar-plus fa-3x mb-3"></i>
|
||||
<p class="mb-0">Nessun calendario remoto configurato.</p>
|
||||
<p class="mb-0">Clicca "Nuova Connessione" per collegare Google Calendar o un server CalDAV (Infomaniak, NextCloud).</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1277,6 +1368,144 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ===== MODALE CONNESSIONE CALENDARIO ===== --}}
|
||||
<div class="modal fade" id="calendarioModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<form id="calendarioForm" method="POST" action="{{ route('calendario-connessioni.store') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="_method" id="calendarioMethod" value="POST">
|
||||
<input type="hidden" name="id" id="calendarioId" value="">
|
||||
<div class="modal-header bg-primary">
|
||||
<h5 class="modal-title" id="calendarioModalTitle"><i class="fas fa-calendar-plus mr-2"></i>Nuova Connessione Calendario</h5>
|
||||
<button type="button" class="close text-white" data-dismiss="modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="cal_nome">Nome *</label>
|
||||
<input type="text" name="nome" id="cal_nome" class="form-control" required placeholder="es. Calendario Parrocchia">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="cal_tipo">Tipo *</label>
|
||||
<select name="tipo" id="cal_tipo" class="form-control" required onchange="toggleCalendarioConfigFields()">
|
||||
<option value="caldav">CalDAV (Infomaniak/NextCloud)</option>
|
||||
<option value="google_calendar">Google Calendar</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="cal_sync_direction">Direzione Sync</label>
|
||||
<select name="sync_direction" id="cal_sync_direction" class="form-control">
|
||||
<option value="bidirectional">Bidirezionale</option>
|
||||
<option value="import">Solo Import (da remoto a locale)</option>
|
||||
<option value="export">Solo Export (da locale a remoto)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="cal_sync_interval">Intervallo Sync (minuti)</label>
|
||||
<select name="sync_interval_minutes" id="cal_sync_interval" class="form-control">
|
||||
<option value="5">5 minuti</option>
|
||||
<option value="15">15 minuti</option>
|
||||
<option value="30">30 minuti</option>
|
||||
<option value="60" selected>1 ora</option>
|
||||
<option value="360">6 ore</option>
|
||||
<option value="1440">24 ore</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" id="cal_is_active" name="is_active" value="1" checked>
|
||||
<label class="custom-control-label" for="cal_is_active">Connessione attiva</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<h6>Configurazione</h6>
|
||||
|
||||
{{-- CalDAV Fields --}}
|
||||
<div id="caldavFields">
|
||||
<div class="form-group">
|
||||
<label for="cal_caldav_url">URL Server CalDAV *</label>
|
||||
<input type="url" name="config[url]" id="cal_caldav_url" class="form-control" placeholder="es. https://nextcloud.parrocchia.it/remote.php/dav/calendars/utente/">
|
||||
<small class="text-muted">URL principale del server CalDAV (es. NextCloud, Infomaniak, Synology)</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cal_caldav_calendar_url">URL Calendario (opzionale)</label>
|
||||
<input type="url" name="config[calendar_url]" id="cal_caldav_calendar_url" class="form-control" placeholder="Lascia vuoto per usare l'URL principale">
|
||||
<small class="text-muted">URL specifico del calendario, se diverso dall'URL del server</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="cal_caldav_username">Username</label>
|
||||
<input type="text" name="config[username]" id="cal_caldav_username" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="cal_caldav_password">Password / App Password</label>
|
||||
<input type="password" name="config[password]" id="cal_caldav_password" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Google Calendar Fields --}}
|
||||
<div id="googleCalendarFields" style="display:none;">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="cal_gc_client_id">Client ID *</label>
|
||||
<input type="text" name="config[client_id]" id="cal_gc_client_id" class="form-control" placeholder="Google Client ID">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="cal_gc_client_secret">Client Secret *</label>
|
||||
<input type="password" name="config[client_secret]" id="cal_gc_client_secret" class="form-control" placeholder="Google Client Secret">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cal_gc_refresh_token">Refresh Token</label>
|
||||
<input type="text" name="config[refresh_token]" id="cal_gc_refresh_token" class="form-control" placeholder="Generato dopo l'autorizzazione OAuth">
|
||||
<small class="text-muted">Dopo aver configurato Client ID e Client Secret, salva la connessione e usa il test per verificare.</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cal_gc_calendar_id">Calendar ID</label>
|
||||
<input type="text" name="config[calendar_id]" id="cal_gc_calendar_id" class="form-control" value="primary" placeholder="primary">
|
||||
<small class="text-muted">ID del calendario Google (default: primary per il calendario principale)</small>
|
||||
</div>
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle mr-2"></i>
|
||||
<strong>Google Calendar:</strong> Crea il progetto in <a href="https://console.cloud.google.com/" target="_blank">Google Cloud Console</a>
|
||||
→ API & Services → Credentials → Crea OAuth Client ID (Web Application).
|
||||
Abilita <strong>Google Calendar API</strong> e configura l'URI di redirect.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annulla</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save mr-1"></i> Salva Connessione
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
@@ -1392,6 +1621,11 @@ $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||
if (target === '#tipologie-eventi') {
|
||||
sortableTipologieEventi.refresh();
|
||||
}
|
||||
if (target === '#calendario') {
|
||||
if (typeof sortableCalendario !== 'undefined') {
|
||||
sortableCalendario.refresh();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ===== MITTENTI AGGIUNTIVI =====
|
||||
@@ -1539,9 +1773,11 @@ function testSmtp() {
|
||||
});
|
||||
}
|
||||
|
||||
var quillEditorInstance = null;
|
||||
|
||||
function createEditor() {
|
||||
var container = document.querySelector('#signature-editor');
|
||||
if (!container || container.querySelector('.ql-toolbar')) return;
|
||||
if (!container || quillEditorInstance) return;
|
||||
|
||||
var quill = new Quill('#signature-editor', {
|
||||
theme: 'snow',
|
||||
@@ -1590,10 +1826,12 @@ function createEditor() {
|
||||
signatureInput.value = quill.root.innerHTML;
|
||||
}
|
||||
});
|
||||
|
||||
quillEditorInstance = quill;
|
||||
}
|
||||
|
||||
function initSignatureEditor() {
|
||||
if (document.querySelector('#signature-editor .ql-toolbar')) return;
|
||||
if (quillEditorInstance) return;
|
||||
|
||||
if (typeof Quill === 'undefined') {
|
||||
var script = document.createElement('script');
|
||||
@@ -1771,7 +2009,122 @@ function testRepo(id, btn) {
|
||||
});
|
||||
}
|
||||
|
||||
// ===== CONNESSIONI CALENDARIO =====
|
||||
var calendarioConnessioni = @json($calendarioConnessioni ?? []);
|
||||
|
||||
function resetCalendarioForm() {
|
||||
document.getElementById('calendarioForm').action = '{{ route('calendario-connessioni.store') }}';
|
||||
document.getElementById('calendarioMethod').value = 'POST';
|
||||
document.getElementById('calendarioModalTitle').textContent = 'Nuova Connessione Calendario';
|
||||
document.getElementById('calendarioForm').reset();
|
||||
document.getElementById('calendarioId').value = '';
|
||||
document.getElementById('cal_is_active').checked = true;
|
||||
document.getElementById('cal_caldav_password').placeholder = '';
|
||||
document.getElementById('cal_gc_client_secret').placeholder = '';
|
||||
toggleCalendarioConfigFields();
|
||||
}
|
||||
|
||||
function toggleCalendarioConfigFields() {
|
||||
const tipo = document.getElementById('cal_tipo').value;
|
||||
document.getElementById('caldavFields').style.display = tipo === 'caldav' ? '' : 'none';
|
||||
document.getElementById('googleCalendarFields').style.display = tipo === 'google_calendar' ? '' : 'none';
|
||||
}
|
||||
|
||||
function editCalendario(id) {
|
||||
const cal = calendarioConnessioni.find(c => c.id === id);
|
||||
if (!cal) return;
|
||||
|
||||
document.getElementById('calendarioForm').action = '{{ route('calendario-connessioni.update', '__ID__') }}'.replace('__ID__', id);
|
||||
document.getElementById('calendarioMethod').value = 'PUT';
|
||||
document.getElementById('calendarioModalTitle').textContent = 'Modifica Connessione - ' + cal.nome;
|
||||
document.getElementById('calendarioId').value = cal.id;
|
||||
document.getElementById('cal_nome').value = cal.nome;
|
||||
document.getElementById('cal_tipo').value = cal.tipo;
|
||||
document.getElementById('cal_sync_direction').value = cal.sync_direction || 'bidirectional';
|
||||
document.getElementById('cal_sync_interval').value = cal.sync_interval_minutes || 60;
|
||||
document.getElementById('cal_is_active').checked = cal.is_active;
|
||||
|
||||
const cfg = cal.config || {};
|
||||
document.getElementById('cal_caldav_url').value = cfg.url || '';
|
||||
document.getElementById('cal_caldav_calendar_url').value = cfg.calendar_url || '';
|
||||
document.getElementById('cal_caldav_username').value = cfg.username || '';
|
||||
document.getElementById('cal_caldav_password').value = '';
|
||||
document.getElementById('cal_caldav_password').placeholder = cfg.password ? '......' : '';
|
||||
document.getElementById('cal_gc_client_id').value = cfg.client_id || '';
|
||||
document.getElementById('cal_gc_client_secret').value = '';
|
||||
document.getElementById('cal_gc_client_secret').placeholder = cfg.client_secret ? '......' : '';
|
||||
document.getElementById('cal_gc_refresh_token').value = cfg.refresh_token || '';
|
||||
document.getElementById('cal_gc_calendar_id').value = cfg.calendar_id || 'primary';
|
||||
|
||||
toggleCalendarioConfigFields();
|
||||
$('#calendarioModal').modal('show');
|
||||
}
|
||||
|
||||
function deleteCalendario(id, nome) {
|
||||
if (!confirm('Eliminare la connessione "' + nome + '"?')) return;
|
||||
var url = '{{ route('calendario-connessioni.destroy', '__ID__') }}'.replace('__ID__', id);
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'X-HTTP-Method-Override': 'DELETE'
|
||||
}
|
||||
}).then(function(r) { return r.json(); }).then(function(data) {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('❌ ' + (data.message || 'Errore'));
|
||||
}
|
||||
}).catch(function() {
|
||||
alert('❌ Errore di rete');
|
||||
});
|
||||
}
|
||||
|
||||
function testCalendario(id, btn) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Test...';
|
||||
|
||||
$.ajax({
|
||||
url: '{{ route('calendario-connessioni.test', '__ID__') }}'.replace('__ID__', id),
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
|
||||
timeout: 30000,
|
||||
success: function(response) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-plug"></i>';
|
||||
alert(response.success ? '✅ ' + response.message : '❌ ' + response.message);
|
||||
},
|
||||
error: function(xhr) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-plug"></i>';
|
||||
const msg = xhr.responseJSON?.message || 'Errore di connessione';
|
||||
alert('❌ ' + msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
if (document.getElementById('calendarioSortable')) {
|
||||
var sortableCalendario = Sortable.create(document.getElementById('calendarioSortable'), {
|
||||
handle: '.handle',
|
||||
animation: 150,
|
||||
onEnd: function(evt) {
|
||||
var order = [];
|
||||
$('#calendarioSortable tr').each(function() {
|
||||
order.push($(this).data('id'));
|
||||
});
|
||||
fetch('{{ route('calendario-connessioni.reorder') }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ order: order })
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (document.getElementById('repoSortable')) {
|
||||
var sortableRepo = Sortable.create(document.getElementById('repoSortable'), {
|
||||
handle: '.handle',
|
||||
|
||||
@@ -232,24 +232,20 @@
|
||||
<button type="button" class="btn btn-xs btn-warning" onclick="editGruppoInline({{ $gruppo->id }})" title="Modifica">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<form action="/individui/{{ $individuo->id }}/gruppi/{{ $gruppo->id }}" method="POST" class="mb-0">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn btn-xs btn-danger" onclick="return confirm('Rimuovere da questo gruppo?')" title="Rimuovi">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
<button type="button" class="btn btn-xs btn-danger" onclick="deleteGruppoFromEdit({{ $individuo->id }}, {{ $gruppo->id }})" title="Rimuovi">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="gruppo-edit-{{ $gruppo->id }}" style="display:none;">
|
||||
<td colspan="5">
|
||||
<form action="/individui/{{ $individuo->id }}/gruppi/{{ $gruppo->id }}" method="POST" class="mb-0">
|
||||
@csrf @method('PUT')
|
||||
<div class="mb-0">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<input type="text" class="form-control form-control-sm" value="{{ $gruppo->nome }}" disabled>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select name="ruolo_ids[]" class="form-control form-control-sm" multiple size="3">
|
||||
<select id="edit-ruoli-{{ $gruppo->id }}" class="form-control form-control-sm" multiple size="3">
|
||||
@php $selectedRuoliIds = $individuo->getRuoloIdsForGruppo($gruppo->id) @endphp
|
||||
@foreach(\App\Models\Ruolo::attive() as $ruolo)
|
||||
<option value="{{ $ruolo->id }}" {{ in_array($ruolo->id, $selectedRuoliIds) ? 'selected' : '' }}>{{ $ruolo->nome }}</option>
|
||||
@@ -257,14 +253,14 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<input type="date" name="data_adesione" class="form-control form-control-sm" value="{{ $gruppo->pivot->data_adesione }}">
|
||||
<input type="date" id="edit-data-{{ $gruppo->id }}" class="form-control form-control-sm" value="{{ $gruppo->pivot->data_adesione }}">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-xs btn-success"><i class="fas fa-check mr-1"></i>Salva</button>
|
||||
<button type="button" class="btn btn-xs btn-success" onclick="submitEditGruppo({{ $gruppo->id }})"><i class="fas fa-check mr-1"></i>Salva</button>
|
||||
<button type="button" class="btn btn-xs btn-secondary" onclick="cancelEditGruppo({{ $gruppo->id }})"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@@ -288,18 +284,13 @@
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div id="documento-add-form" style="display:none;" class="p-3 bg-light border-bottom">
|
||||
<form action="/documenti" method="POST" enctype="multipart/form-data" class="mb-0">
|
||||
@csrf
|
||||
<input type="hidden" name="_redirect" value="{{ url()->current() }}">
|
||||
<input type="hidden" name="visibilita" value="individuo">
|
||||
<input type="hidden" name="visibilita_target_id" value="{{ $individuo->id }}">
|
||||
<input type="hidden" name="visibilita_target_type" value="App\Models\Individuo">
|
||||
<div class="mb-0">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<input type="text" name="nome_file" class="form-control form-control-sm" placeholder="Nome documento" required>
|
||||
<input type="text" id="doc-nome-file" class="form-control form-control-sm" placeholder="Nome documento">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select name="tipologia" class="form-control form-control-sm" required>
|
||||
<select id="doc-tipologia" class="form-control form-control-sm">
|
||||
<option value="">Tipologia...</option>
|
||||
<option value="documento">Documento</option>
|
||||
<option value="avatar">Avatar</option>
|
||||
@@ -309,14 +300,14 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<input type="file" name="file" class="form-control form-control-sm" required>
|
||||
<input type="file" id="doc-file" class="form-control form-control-sm">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-xs btn-success"><i class="fas fa-upload mr-1"></i>Carica</button>
|
||||
<button type="button" class="btn btn-xs btn-success" onclick="submitUploadDocumentoIndividuo()"><i class="fas fa-upload mr-1"></i>Carica</button>
|
||||
<button type="button" class="btn btn-xs btn-secondary" onclick="hideDocumentoForm()"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@if($individuo->documenti->count() > 0)
|
||||
<table class="table table-bordered table-hover table-striped mb-0">
|
||||
@@ -347,12 +338,9 @@
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
@endif
|
||||
<form action="/documenti/{{ $documento->id }}" method="POST" class="d-inline">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn btn-xs btn-danger" onclick="return confirm('Eliminare questo documento?')" title="Elimina">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
<button type="button" class="btn btn-xs btn-danger" onclick="deleteDocumentoFromEdit({{ $documento->id }})" title="Elimina">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@@ -714,5 +702,105 @@ function deleteAvatar() {
|
||||
alert('Errore: ' + error);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteGruppoFromEdit(individuoId, gruppoId) {
|
||||
if (!confirm('Rimuovere da questo gruppo?')) return;
|
||||
|
||||
fetch('/individui/' + individuoId + '/gruppi/' + gruppoId, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) return response.text().then(text => { throw new Error(text); });
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) window.location.reload();
|
||||
else alert(data.error || 'Errore');
|
||||
})
|
||||
.catch(error => alert('Errore: ' + error.message));
|
||||
}
|
||||
|
||||
function submitEditGruppo(gruppoId) {
|
||||
var ruoloSelect = document.getElementById('edit-ruoli-' + gruppoId);
|
||||
var dataInput = document.getElementById('edit-data-' + gruppoId);
|
||||
var formData = new URLSearchParams();
|
||||
|
||||
var selectedRuoli = Array.from(ruoloSelect.selectedOptions).map(opt => opt.value).filter(v => v);
|
||||
selectedRuoli.forEach(function(ruoloId) {
|
||||
formData.append('ruolo_ids[]', ruoloId);
|
||||
});
|
||||
formData.append('data_adesione', dataInput ? dataInput.value : '');
|
||||
|
||||
fetch('/individui/{{ $individuo->id }}/gruppi/' + gruppoId, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: formData.toString()
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) return response.text().then(text => { throw new Error(text); });
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) window.location.reload();
|
||||
else alert(data.error || 'Errore');
|
||||
})
|
||||
.catch(error => alert('Errore: ' + error.message));
|
||||
}
|
||||
|
||||
function submitUploadDocumentoIndividuo() {
|
||||
var nomeFile = document.getElementById('doc-nome-file');
|
||||
var tipologia = document.getElementById('doc-tipologia');
|
||||
var fileInput = document.getElementById('doc-file');
|
||||
|
||||
if (!nomeFile.value || !tipologia.value || !fileInput.files[0]) {
|
||||
alert('Compila tutti i campi');
|
||||
return;
|
||||
}
|
||||
|
||||
var formData = new FormData();
|
||||
formData.append('_token', '{{ csrf_token() }}');
|
||||
formData.append('_redirect', '{{ url()->current() }}');
|
||||
formData.append('visibilita', 'individuo');
|
||||
formData.append('visibilita_target_id', '{{ $individuo->id }}');
|
||||
formData.append('visibilita_target_type', 'App\\Models\\Individuo');
|
||||
formData.append('nome_file', nomeFile.value);
|
||||
formData.append('tipologia', tipologia.value);
|
||||
formData.append('file', fileInput.files[0]);
|
||||
|
||||
document.getElementById('documento-add-form').querySelector('.btn-success').disabled = true;
|
||||
|
||||
fetch('/documenti', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: { 'Accept': 'application/json' }
|
||||
})
|
||||
.then(function() { window.location.reload(); })
|
||||
.catch(function() { window.location.reload(); });
|
||||
}
|
||||
|
||||
function deleteDocumentoFromEdit(documentoId) {
|
||||
if (!confirm('Eliminare questo documento?')) return;
|
||||
|
||||
var formData = new FormData();
|
||||
formData.append('_token', '{{ csrf_token() }}');
|
||||
formData.append('_method', 'DELETE');
|
||||
formData.append('_redirect', '{{ url()->current() }}');
|
||||
|
||||
fetch('/documenti/' + documentoId, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: { 'Accept': 'application/json' }
|
||||
})
|
||||
.then(function() { window.location.reload(); })
|
||||
.catch(function() { window.location.reload(); });
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
|
||||
+12
-1
@@ -23,6 +23,7 @@ use App\Http\Controllers\Admin\ActivityLogController;
|
||||
use App\Http\Controllers\Admin\EmailSettingsController;
|
||||
use App\Http\Controllers\EmailController;
|
||||
use App\Http\Controllers\StorageRepositoryController;
|
||||
use App\Http\Controllers\CalendarioConnessioneController;
|
||||
use App\Http\Middleware\AdminOnly;
|
||||
use App\Http\Middleware\CheckPermission;
|
||||
|
||||
@@ -183,13 +184,22 @@ Route::post('/impostazioni/migra-percorso', [ImpostazioniController::class, 'mig
|
||||
Route::get('/impostazioni/email', [EmailSettingsController::class, 'index'])->middleware('auth')->name('impostazioni.email.index');
|
||||
Route::post('/impostazioni/email', [EmailSettingsController::class, 'save'])->middleware('auth')->name('impostazioni.email.save');
|
||||
Route::post('/impostazioni/email/test', [EmailSettingsController::class, 'testConnection'])->middleware('auth')->name('impostazioni.email.test');
|
||||
Route::post('/impostazioni/email/test-smtp', [EmailSettingsController::class, 'testSmtp'])->middleware('auth')->name('impostazioni.email.testSmtp');
|
||||
Route::post('/impostazioni/email/test-smtp', [EmailSettingsController::class, 'testSmtp'])->middleware('auth')->name('impostazioni.email.testSmtp');
|
||||
Route::get('/impostazioni/email/sync', [EmailSettingsController::class, 'syncNow'])->middleware('auth')->name('impostazioni.email.sync');
|
||||
Route::delete('/impostazioni/email', [EmailSettingsController::class, 'destroy'])->middleware('auth')->name('impostazioni.email.destroy');
|
||||
Route::post('/impostazioni/sender', [EmailSettingsController::class, 'senderStore'])->middleware('auth')->name('impostazioni.sender.store');
|
||||
Route::put('/impostazioni/sender/{id}', [EmailSettingsController::class, 'senderUpdate'])->middleware('auth')->name('impostazioni.sender.update');
|
||||
Route::delete('/impostazioni/sender/{id}', [EmailSettingsController::class, 'senderDestroy'])->middleware('auth')->name('impostazioni.sender.destroy');
|
||||
Route::post('/impostazioni/sender/{id}/test', [EmailSettingsController::class, 'senderTestSmtp'])->middleware('auth')->name('impostazioni.sender.test');
|
||||
|
||||
// Calendar Connections (CalDAV / Google Calendar)
|
||||
Route::post('/calendario-connessioni', [App\Http\Controllers\CalendarioConnessioneController::class, 'store'])->middleware('auth')->name('calendario-connessioni.store');
|
||||
Route::put('/calendario-connessioni/{id}', [App\Http\Controllers\CalendarioConnessioneController::class, 'update'])->middleware('auth')->name('calendario-connessioni.update');
|
||||
Route::delete('/calendario-connessioni/{id}', [App\Http\Controllers\CalendarioConnessioneController::class, 'destroy'])->middleware('auth')->name('calendario-connessioni.destroy');
|
||||
Route::post('/calendario-connessioni/{id}/test', [App\Http\Controllers\CalendarioConnessioneController::class, 'test'])->middleware('auth')->name('calendario-connessioni.test');
|
||||
Route::post('/calendario-connessioni/{id}/sync', [App\Http\Controllers\CalendarioConnessioneController::class, 'sync'])->middleware('auth')->name('calendario-connessioni.sync');
|
||||
Route::post('/calendario-connessioni/reorder', [App\Http\Controllers\CalendarioConnessioneController::class, 'reorder'])->middleware('auth')->name('calendario-connessioni.reorder');
|
||||
|
||||
// Storage Repositories (Opzione B)
|
||||
Route::post('/storage-repositories', [StorageRepositoryController::class, 'store'])->middleware('auth')->name('storage-repositories.store');
|
||||
Route::put('/storage-repositories/{storage_repository}', [StorageRepositoryController::class, 'update'])->middleware('auth')->name('storage-repositories.update');
|
||||
@@ -235,6 +245,7 @@ Route::prefix('admin')->middleware(['auth', AdminOnly::class])->group(function (
|
||||
Route::delete('/ruoli/{id}', [RuoloController::class, 'destroy'])->name('admin.ruoli.destroy');
|
||||
|
||||
Route::get('/activity-logs', [ActivityLogController::class, 'index'])->name('admin.activity-logs.index');
|
||||
Route::delete('/activity-logs', [ActivityLogController::class, 'destroy'])->name('admin.activity-logs.destroy');
|
||||
|
||||
// Backup
|
||||
Route::get('/backup', [\App\Http\Controllers\Admin\BackupController::class, 'index'])->name('admin.backup.index');
|
||||
|
||||
@@ -1,720 +0,0 @@
|
||||
<?php $__env->startSection('title', 'Modifica Individuo'); ?>
|
||||
<?php $__env->startSection('page_title', 'Modifica Individuo'); ?>
|
||||
|
||||
<?php $__env->startSection('content'); ?>
|
||||
<form action="/individui/<?php echo e($individuo->id); ?>" method="POST" id="main-form">
|
||||
<?php echo csrf_field(); ?>
|
||||
<?php echo method_field('PUT'); ?>
|
||||
<input type="hidden" name="individuo_id" value="<?php echo e($individuo->id); ?>">
|
||||
<input type="hidden" name="_avatar_token" value="<?php echo e(csrf_token()); ?>">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-user mr-2"></i>Dati Anagrafici</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label>Codice ID</label>
|
||||
<input type="text" class="form-control" value="<?php echo e($individuo->codice_id); ?>" disabled>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Cognome *</label>
|
||||
<input type="text" name="cognome" class="form-control" value="<?php echo e($individuo->cognome); ?>" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Nome *</label>
|
||||
<input type="text" name="nome" class="form-control" value="<?php echo e($individuo->nome); ?>" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Data di nascita</label>
|
||||
<input type="date" name="data_nascita" class="form-control" value="<?php echo e($individuo->data_nascita?->format('Y-m-d')); ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Genere</label>
|
||||
<select name="genere" class="form-control">
|
||||
<option value="">Seleziona...</option>
|
||||
<option value="M" <?php echo e($individuo->genere === 'M' ? 'selected' : ''); ?>>Maschio</option>
|
||||
<option value="F" <?php echo e($individuo->genere === 'F' ? 'selected' : ''); ?>>Femmina</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card card-secondary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-camera mr-2"></i>Avatar</h3>
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<?php if($individuo->avatar): ?>
|
||||
<img src="/individui/<?php echo e($individuo->id); ?>/avatar"
|
||||
alt="Avatar"
|
||||
class="img-thumbnail mb-2"
|
||||
style="max-width: 120px; max-height: 120px; object-fit: cover; border-radius: 50%;">
|
||||
<br>
|
||||
<button type="button" class="btn btn-danger btn-sm" onclick="deleteAvatar()">
|
||||
<i class="fas fa-trash mr-1"></i>Rimuovi
|
||||
</button>
|
||||
<?php else: ?>
|
||||
<div class="bg-secondary d-flex align-items-center justify-content-center mb-3"
|
||||
style="width: 120px; height: 120px; margin: 0 auto; border-radius: 50%;">
|
||||
<i class="fas fa-user fa-4x text-white"></i>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="custom-file mb-2">
|
||||
<input type="file" class="custom-file-input" id="avatar-input" name="avatar" accept="image/jpeg,image/png,image/gif">
|
||||
<label class="custom-file-label" for="avatar-input">Scegli immagine...</label>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="uploadAvatar()">
|
||||
<i class="fas fa-upload mr-1"></i>Carica Avatar
|
||||
</button>
|
||||
<div id="avatar-message" class="mt-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title"><i class="fas fa-map-marker-alt mr-2"></i>Residenza</h3></div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label>Indirizzo</label>
|
||||
<input type="text" name="indirizzo" class="form-control" value="<?php echo e($individuo->indirizzo); ?>">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-4">
|
||||
<label>CAP</label>
|
||||
<input type="text" name="cap" class="form-control" value="<?php echo e($individuo->cap); ?>">
|
||||
</div>
|
||||
<div class="form-group col-md-5">
|
||||
<label>Città</label>
|
||||
<input type="text" name="città" class="form-control" value="<?php echo e($individuo->città); ?>">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label>Provincia</label>
|
||||
<input type="text" name="sigla_provincia" class="form-control" maxlength="2" value="<?php echo e($individuo->sigla_provincia); ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title"><i class="fas fa-id-card mr-2"></i>Documento di identità</h3></div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label>Tipo documento</label>
|
||||
<select name="tipo_documento" class="form-control">
|
||||
<option value="">Seleziona...</option>
|
||||
<option value="carta_identita" <?php echo e($individuo->tipo_documento === 'carta_identita' ? 'selected' : ''); ?>>Carta d'Identità</option>
|
||||
<option value="patente" <?php echo e($individuo->tipo_documento === 'patente' ? 'selected' : ''); ?>>Patente</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Numero documento</label>
|
||||
<input type="text" name="numero_documento" class="form-control" value="<?php echo e($individuo->numero_documento); ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Scadenza documento</label>
|
||||
<input type="date" name="scadenza_documento" class="form-control" value="<?php echo e($individuo->scadenza_documento?->format('Y-m-d')); ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title"><i class="fas fa-sticky-note mr-2"></i>Note</h3></div>
|
||||
<div class="card-body">
|
||||
<div class="form-group mb-0">
|
||||
<textarea name="note" class="form-control" rows="4"><?php echo e($individuo->note); ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-address-book mr-2"></i>Contatti</h3>
|
||||
<button type="button" class="btn btn-xs btn-success float-right" onclick="aggiungiRigaContatto()">
|
||||
<i class="fas fa-plus mr-1"></i> Aggiungi
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-bordered mb-0" id="contatti-table">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th style="width: 20%;">Tipo</th>
|
||||
<th style="width: 30%;">Valore</th>
|
||||
<th style="width: 20%;">Etichetta</th>
|
||||
<th style="width: 10%;">Primario</th>
|
||||
<th style="width: 50px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="contatti-tbody">
|
||||
<?php $__currentLoopData = $individuo->contatti; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $contatto): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<tr>
|
||||
<td>
|
||||
<select name="contatti[<?php echo e($loop->index); ?>][tipo]" class="form-control">
|
||||
<option value="telefono" <?php echo e($contatto->tipo === 'telefono' ? 'selected' : ''); ?>>Telefono</option>
|
||||
<option value="cellulare" <?php echo e($contatto->tipo === 'cellulare' ? 'selected' : ''); ?>>Cellulare</option>
|
||||
<option value="email" <?php echo e($contatto->tipo === 'email' ? 'selected' : ''); ?>>Email</option>
|
||||
<option value="fax" <?php echo e($contatto->tipo === 'fax' ? 'selected' : ''); ?>>Fax</option>
|
||||
<option value="web" <?php echo e($contatto->tipo === 'web' ? 'selected' : ''); ?>>Web</option>
|
||||
<option value="telegram" <?php echo e($contatto->tipo === 'telegram' ? 'selected' : ''); ?>>Telegram</option>
|
||||
<option value="whatsapp" <?php echo e($contatto->tipo === 'whatsapp' ? 'selected' : ''); ?>>WhatsApp</option>
|
||||
<option value="altro" <?php echo e($contatto->tipo === 'altro' ? 'selected' : ''); ?>>Altro</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><input type="text" name="contatti[<?php echo e($loop->index); ?>][valore]" class="form-control" value="<?php echo e($contatto->valore); ?>"></td>
|
||||
<td><input type="text" name="contatti[<?php echo e($loop->index); ?>][etichetta]" class="form-control" value="<?php echo e($contatto->etichetta); ?>"></td>
|
||||
<td class="text-center"><input type="checkbox" name="contatti[<?php echo e($loop->index); ?>][is_primary]" value="1" <?php echo e($contatto->is_primary ? 'checked' : ''); ?>></td>
|
||||
<td><button type="button" class="btn btn-danger btn-xs" onclick="this.closest('tr').remove()"><i class="fas fa-trash"></i></button></td>
|
||||
</tr>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php if($individuo->contatti->isEmpty()): ?>
|
||||
<div id="no-contatti-msg" class="text-center text-muted py-4">
|
||||
<p class="mb-0">Nessun contatto. Clicca su "Aggiungi" per aggiungerne uno.</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-folder mr-2"></i>Gruppi</h3>
|
||||
<button type="button" class="btn btn-xs btn-success float-right" onclick="showGruppoForm()">
|
||||
<i class="fas fa-plus mr-1"></i> Aggiungi
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
|
||||
<?php if($individuo->gruppi->count() > 0): ?>
|
||||
<table class="table table-bordered table-hover table-striped mb-0">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>Nome</th>
|
||||
<th>Diocesi</th>
|
||||
<th style="width: 130px;">Ruolo</th>
|
||||
<th style="width: 110px;">Data Adesione</th>
|
||||
<th style="width: 80px;">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php $__currentLoopData = $individuo->gruppi; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $gruppo): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<tr id="gruppo-row-<?php echo e($gruppo->id); ?>">
|
||||
<td>
|
||||
<i class="fas fa-folder text-warning mr-1"></i>
|
||||
<a href="/gruppi/<?php echo e($gruppo->id); ?>"><?php echo e($gruppo->nome); ?></a>
|
||||
</td>
|
||||
<td><?php echo e($gruppo->diocesi?->nome ?: '-'); ?></td>
|
||||
<td>
|
||||
<?php $ruoliIndividuo = $individuo->getRuoliForGruppo($gruppo->id) ?>
|
||||
<?php if($ruoliIndividuo->count() > 0): ?>
|
||||
<?php $__currentLoopData = $ruoliIndividuo; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $ruolo): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<span class="badge badge-info mr-1"><?php echo e($ruolo->nome); ?></span>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?php echo e($gruppo->pivot->data_adesione ? \Carbon\Carbon::parse($gruppo->pivot->data_adesione)->format('d/m/Y') : '-'); ?></td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-xs btn-warning" onclick="editGruppoInline(<?php echo e($gruppo->id); ?>)" title="Modifica">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<form action="/individui/<?php echo e($individuo->id); ?>/gruppi/<?php echo e($gruppo->id); ?>" method="POST" class="mb-0">
|
||||
<?php echo csrf_field(); ?> <?php echo method_field('DELETE'); ?>
|
||||
<button type="submit" class="btn btn-xs btn-danger" onclick="return confirm('Rimuovere da questo gruppo?')" title="Rimuovi">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="gruppo-edit-<?php echo e($gruppo->id); ?>" style="display:none;">
|
||||
<td colspan="5">
|
||||
<form action="/individui/<?php echo e($individuo->id); ?>/gruppi/<?php echo e($gruppo->id); ?>" method="POST" class="mb-0">
|
||||
<?php echo csrf_field(); ?> <?php echo method_field('PUT'); ?>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<input type="text" class="form-control form-control-sm" value="<?php echo e($gruppo->nome); ?>" disabled>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select name="ruolo_ids[]" class="form-control form-control-sm" multiple size="3">
|
||||
<?php $selectedRuoliIds = $individuo->getRuoloIdsForGruppo($gruppo->id) ?>
|
||||
<?php $__currentLoopData = \App\Models\Ruolo::attive(); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $ruolo): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<option value="<?php echo e($ruolo->id); ?>" <?php echo e(in_array($ruolo->id, $selectedRuoliIds) ? 'selected' : ''); ?>><?php echo e($ruolo->nome); ?></option>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<input type="date" name="data_adesione" class="form-control form-control-sm" value="<?php echo e($gruppo->pivot->data_adesione); ?>">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-xs btn-success"><i class="fas fa-check mr-1"></i>Salva</button>
|
||||
<button type="button" class="btn btn-xs btn-secondary" onclick="cancelEditGruppo(<?php echo e($gruppo->id); ?>)"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php else: ?>
|
||||
<div class="text-center text-muted py-4">
|
||||
<i class="fas fa-users fa-2x mb-2"></i>
|
||||
<p class="mb-0">Nessun gruppo associato</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-file mr-2"></i>Documenti</h3>
|
||||
<button type="button" class="btn btn-xs btn-success float-right" onclick="showDocumentoForm()">
|
||||
<i class="fas fa-plus mr-1"></i> Carica
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div id="documento-add-form" style="display:none;" class="p-3 bg-light border-bottom">
|
||||
<form action="/documenti" method="POST" enctype="multipart/form-data" class="mb-0">
|
||||
<?php echo csrf_field(); ?>
|
||||
<input type="hidden" name="_redirect" value="<?php echo e(url()->current()); ?>">
|
||||
<input type="hidden" name="visibilita" value="individuo">
|
||||
<input type="hidden" name="visibilita_target_id" value="<?php echo e($individuo->id); ?>">
|
||||
<input type="hidden" name="visibilita_target_type" value="App\Models\Individuo">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<input type="text" name="nome_file" class="form-control form-control-sm" placeholder="Nome documento" required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select name="tipologia" class="form-control form-control-sm" required>
|
||||
<option value="">Tipologia...</option>
|
||||
<option value="documento">Documento</option>
|
||||
<option value="avatar">Avatar</option>
|
||||
<option value="galleria">Galleria</option>
|
||||
<option value="statuto">Statuto</option>
|
||||
<option value="altro">Altro</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<input type="file" name="file" class="form-control form-control-sm" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-xs btn-success"><i class="fas fa-upload mr-1"></i>Carica</button>
|
||||
<button type="button" class="btn btn-xs btn-secondary" onclick="hideDocumentoForm()"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<?php if($individuo->documenti->count() > 0): ?>
|
||||
<table class="table table-bordered table-hover table-striped mb-0">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>Nome</th>
|
||||
<th style="width: 120px;">Tipologia</th>
|
||||
<th style="width: 80px;">Dimensione</th>
|
||||
<th style="width: 130px;">Data Upload</th>
|
||||
<th style="width: 80px;">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php $__currentLoopData = $individuo->documenti; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $documento): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="#" onclick="event.preventDefault(); previewDocumento(<?php echo e($documento->id); ?>, '<?php echo e($documento->mime_type); ?>');">
|
||||
<i class="fas fa-file text-secondary mr-1"></i>
|
||||
<?php echo e($documento->nome_file); ?>
|
||||
|
||||
</a>
|
||||
</td>
|
||||
<td><?php echo e(ucfirst(str_replace('_', ' ', $documento->tipologia))); ?></td>
|
||||
<td><?php echo e(number_format($documento->dimensione / 1024, 1)); ?> KB</td>
|
||||
<td><?php echo e($documento->created_at->format('d/m/Y')); ?></td>
|
||||
<td>
|
||||
<?php if($documento->file_path): ?>
|
||||
<button type="button" class="btn btn-xs btn-primary" onclick="previewDocumento(<?php echo e($documento->id); ?>, '<?php echo e($documento->mime_type); ?>')" title="Anteprima">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<form action="/documenti/<?php echo e($documento->id); ?>" method="POST" class="d-inline">
|
||||
<?php echo csrf_field(); ?> <?php echo method_field('DELETE'); ?>
|
||||
<button type="submit" class="btn btn-xs btn-danger" onclick="return confirm('Eliminare questo documento?')" title="Elimina">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php else: ?>
|
||||
<div class="text-center text-muted py-4">
|
||||
<i class="fas fa-file fa-2x mb-2"></i>
|
||||
<p class="mb-0">Nessun documento</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 mb-4">
|
||||
<button type="button" class="btn btn-success btn-lg" id="save-all-btn" onclick="submitMainForm()">
|
||||
<i class="fas fa-save mr-2"></i> Salva Tutte le Modifiche
|
||||
</button>
|
||||
<a href="/individui/<?php echo e($individuo->id); ?>" class="btn btn-secondary btn-lg ml-2">
|
||||
<i class="fas fa-times mr-1"></i> Annulla
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="card mt-3" id="gruppo-add-card" style="display:none; background-color: #fff3cd; border-color: #ffc107;">
|
||||
<div class="card-header bg-warning">
|
||||
<h3 class="card-title"><i class="fas fa-folder-plus mr-2"></i>Aggiungi a Gruppo</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<select id="new-gruppo-id" class="form-control form-control-sm">
|
||||
<option value="">Seleziona gruppo...</option>
|
||||
<?php $__currentLoopData = \App\Models\Gruppo::orderBy('nome')->get(); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $g): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<?php if(!$individuo->gruppi->contains($g->id)): ?>
|
||||
<option value="<?php echo e($g->id); ?>"><?php echo e($g->full_path); ?></option>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select id="new-gruppo-ruolo" class="form-control form-control-sm" multiple size="3">
|
||||
<?php $__currentLoopData = \App\Models\Ruolo::attive(); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $ruolo): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<option value="<?php echo e($ruolo->id); ?>"><?php echo e($ruolo->nome); ?></option>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</select>
|
||||
<small class="text-muted">Ctrl+click per selezionare più ruoli</small>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<input type="date" id="new-gruppo-data" class="form-control form-control-sm">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="submitGruppoForm()">
|
||||
<i class="fas fa-check mr-1"></i> Associa
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm" onclick="hideGruppoForm()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="previewModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document" style="max-width: 90vw;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="fas fa-file mr-2"></i><span id="previewModalTitle">Anteprima</span></h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Chiudi">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body text-center p-0" style="min-height: 400px;">
|
||||
<iframe id="previewFrame" src="" style="width: 100%; height: 70vh; border: none;"></iframe>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a id="previewDownloadBtn" href="#" class="btn btn-primary" download>
|
||||
<i class="fas fa-download mr-1"></i> Scarica
|
||||
</a>
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Chiudi</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php $__env->stopSection(); ?>
|
||||
|
||||
<?php $__env->startSection('scripts'); ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('All form fields:');
|
||||
var form = document.getElementById('main-form');
|
||||
var fields = form.querySelectorAll('input, textarea, select');
|
||||
fields.forEach(function(f) {
|
||||
console.log(f.name + ' [' + f.type + ']: value="' + f.value.substring(0,30) + '" disabled=' + f.disabled);
|
||||
});
|
||||
|
||||
// Debug note field specifically
|
||||
var noteField = form.querySelector('[name="note"]');
|
||||
console.log('Note field found:', !!noteField);
|
||||
if (noteField) {
|
||||
console.log('Note value:', noteField.value);
|
||||
}
|
||||
|
||||
var saveBtn = document.getElementById('save-btn');
|
||||
if (saveBtn) {
|
||||
saveBtn.addEventListener('click', function() {
|
||||
console.log('=== SAVE CLICKED ===');
|
||||
var note = form.querySelector('[name="note"]');
|
||||
console.log('Note at save time:', note ? note.value : 'NOT FOUND');
|
||||
});
|
||||
}
|
||||
|
||||
form.addEventListener('submit', function(e) {
|
||||
console.log('=== FORM SUBMIT ===');
|
||||
var note = form.querySelector('[name="note"]');
|
||||
console.log('Note at submit:', note ? note.value : 'NOT FOUND');
|
||||
|
||||
var contattiFields = form.querySelectorAll('[name^="contatti"]');
|
||||
console.log('Contatti fields count:', contattiFields.length);
|
||||
contattiFields.forEach(function(f, i) {
|
||||
console.log('Contatto field ' + i + ':', f.name, '=', f.value);
|
||||
});
|
||||
|
||||
var formData = new FormData(form);
|
||||
console.log('FormData contatti:', formData.getAll('contatti'));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
function submitMainForm() {
|
||||
var form = document.getElementById('main-form');
|
||||
console.log('=== DEBUG SUBMIT ===');
|
||||
|
||||
var contattiFields = form.querySelectorAll('[name^="contatti"]');
|
||||
console.log('Contatti fields found:', contattiFields.length);
|
||||
contattiFields.forEach(function(f, i) {
|
||||
console.log('Field ' + i + ': name=' + f.name + ', value=' + f.value);
|
||||
});
|
||||
|
||||
form.action = '/individui/<?php echo e($individuo->id); ?>';
|
||||
var methodInput = form.querySelector('input[name="_method"]');
|
||||
if (!methodInput) {
|
||||
var m = document.createElement('input');
|
||||
m.type = 'hidden';
|
||||
m.name = '_method';
|
||||
m.value = 'PUT';
|
||||
form.appendChild(m);
|
||||
}
|
||||
var csrfInput = form.querySelector('input[name="_token"]');
|
||||
if (!csrfInput) {
|
||||
var c = document.createElement('input');
|
||||
c.type = 'hidden';
|
||||
c.name = '_token';
|
||||
c.value = '<?php echo e(csrf_token()); ?>';
|
||||
form.appendChild(c);
|
||||
}
|
||||
|
||||
var formData = new FormData(form);
|
||||
console.log('FormData contatti keys:');
|
||||
for (var pair of formData.entries()) {
|
||||
if (pair[0].startsWith('contatti')) {
|
||||
console.log(' ' + pair[0] + ' = ' + pair[1]);
|
||||
}
|
||||
}
|
||||
|
||||
form.submit();
|
||||
}
|
||||
|
||||
let contattoIndex = <?php echo e($individuo->contatti->count()); ?>;
|
||||
|
||||
function aggiungiRigaContatto() {
|
||||
const tbody = document.getElementById('contatti-tbody');
|
||||
const noMsg = document.getElementById('no-contatti-msg');
|
||||
if (noMsg) noMsg.style.display = 'none';
|
||||
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>
|
||||
<select name="contatti[${contattoIndex}][tipo]" class="form-control">
|
||||
<option value="">Seleziona...</option>
|
||||
<option value="telefono">Telefono</option>
|
||||
<option value="cellulare">Cellulare</option>
|
||||
<option value="email">Email</option>
|
||||
<option value="fax">Fax</option>
|
||||
<option value="web">Web</option>
|
||||
<option value="telegram">Telegram</option>
|
||||
<option value="whatsapp">WhatsApp</option>
|
||||
<option value="altro">Altro</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><input type="text" name="contatti[${contattoIndex}][valore]" class="form-control" placeholder="Valore"></td>
|
||||
<td><input type="text" name="contatti[${contattoIndex}][etichetta]" class="form-control" placeholder="Etichetta"></td>
|
||||
<td class="text-center"><input type="checkbox" name="contatti[${contattoIndex}][is_primary]" value="1"></td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-success btn-xs" title="Salva" onclick="submitMainForm()">
|
||||
<i class="fas fa-save"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger btn-xs" title="Elimina" onclick="this.closest('tr').remove()">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
contattoIndex++;
|
||||
}
|
||||
|
||||
function showGruppoForm() {
|
||||
var card = document.getElementById('gruppo-add-card');
|
||||
if (card) {
|
||||
card.style.display = 'block';
|
||||
card.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}
|
||||
|
||||
function hideGruppoForm() {
|
||||
var card = document.getElementById('gruppo-add-card');
|
||||
if (card) {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function submitGruppoForm() {
|
||||
var gruppoId = document.getElementById('new-gruppo-id').value;
|
||||
var ruoloSelect = document.getElementById('new-gruppo-ruolo');
|
||||
var dataAdesione = document.getElementById('new-gruppo-data').value;
|
||||
|
||||
if (!gruppoId) {
|
||||
alert('Seleziona un gruppo');
|
||||
return;
|
||||
}
|
||||
|
||||
var formData = new URLSearchParams();
|
||||
formData.append('gruppo_id', gruppoId);
|
||||
|
||||
var selectedRuoli = Array.from(ruoloSelect.selectedOptions).map(opt => opt.value).filter(v => v);
|
||||
selectedRuoli.forEach(function(ruoloId) {
|
||||
formData.append('ruolo_ids[]', ruoloId);
|
||||
});
|
||||
|
||||
formData.append('data_adesione', dataAdesione);
|
||||
|
||||
fetch('/individui/<?php echo e($individuo->id); ?>/gruppi', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '<?php echo e(csrf_token()); ?>',
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.text().then(text => { throw new Error(text); });
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
hideGruppoForm();
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(data.error || 'Errore');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Errore: ' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
function editGruppoInline(id) {
|
||||
document.getElementById('gruppo-row-' + id).style.display = 'none';
|
||||
document.getElementById('gruppo-edit-' + id).style.display = 'table-row';
|
||||
}
|
||||
|
||||
function cancelEditGruppo(id) {
|
||||
document.getElementById('gruppo-row-' + id).style.display = 'table-row';
|
||||
document.getElementById('gruppo-edit-' + id).style.display = 'none';
|
||||
}
|
||||
|
||||
function showDocumentoForm() {
|
||||
document.getElementById('documento-add-form').style.display = 'block';
|
||||
}
|
||||
|
||||
function hideDocumentoForm() {
|
||||
document.getElementById('documento-add-form').style.display = 'none';
|
||||
}
|
||||
|
||||
function previewDocumento(id, mimeType) {
|
||||
var previewUrl = '/documenti/' + id + '/preview';
|
||||
var downloadUrl = '/documenti/' + id + '/download';
|
||||
|
||||
document.getElementById('previewFrame').src = previewUrl;
|
||||
document.getElementById('previewDownloadBtn').href = downloadUrl;
|
||||
document.getElementById('previewModalTitle').textContent = 'Anteprima';
|
||||
|
||||
if (mimeType && mimeType.startsWith('image/')) {
|
||||
document.getElementById('previewDownloadBtn').style.display = 'inline-block';
|
||||
} else {
|
||||
document.getElementById('previewDownloadBtn').style.display = 'none';
|
||||
}
|
||||
|
||||
$('#previewModal').modal('show');
|
||||
}
|
||||
|
||||
document.getElementById('avatar-input').addEventListener('change', function(e) {
|
||||
var fileName = e.target.files[0]?.name || 'Scegli file...';
|
||||
e.target.nextElementSibling.textContent = fileName;
|
||||
});
|
||||
|
||||
function uploadAvatar() {
|
||||
var input = document.getElementById('avatar-input');
|
||||
if (!input.files[0]) {
|
||||
document.getElementById('avatar-message').innerHTML = '<span class="text-danger">Seleziona un\'immagine</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
var formData = new FormData();
|
||||
formData.append('avatar', input.files[0]);
|
||||
formData.append('_token', '<?php echo e(csrf_token()); ?>');
|
||||
|
||||
document.getElementById('avatar-message').innerHTML = '<span class="text-info"><i class="fas fa-spinner fa-spin"></i> Caricamento...</span>';
|
||||
|
||||
fetch('/individui/<?php echo e($individuo->id); ?>/avatar', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('avatar-message').innerHTML = '<span class="text-success">Avatar caricato!</span>';
|
||||
setTimeout(function() { window.location.reload(); }, 500);
|
||||
} else {
|
||||
document.getElementById('avatar-message').innerHTML = '<span class="text-danger">' + (data.message || 'Errore') + '</span>';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
document.getElementById('avatar-message').innerHTML = '<span class="text-danger">Errore: ' + error + '</span>';
|
||||
});
|
||||
}
|
||||
|
||||
function deleteAvatar() {
|
||||
if (!confirm('Rimuovere l\'avatar?')) return;
|
||||
|
||||
fetch('/individui/<?php echo e($individuo->id); ?>/avatar', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '<?php echo e(csrf_token()); ?>',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(data.message || 'Errore');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Errore: ' + error);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<?php $__env->stopSection(); ?>
|
||||
|
||||
<?php echo $__env->make('layouts.adminlte', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH /var/www/html/glastree/resources/views/individui/edit.blade.php ENDPATH**/ ?>
|
||||
@@ -0,0 +1,350 @@
|
||||
<?php $__env->startSection('title', 'Email - ' . ucfirst($folder)); ?>
|
||||
<?php $__env->startSection('page_title', 'Email'); ?>
|
||||
|
||||
<?php
|
||||
$currentFolder = $folders->firstWhere('type', $folder);
|
||||
?>
|
||||
|
||||
<?php $__env->startSection('content'); ?>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-envelope mr-2"></i>Email</h3>
|
||||
<div class="card-tools">
|
||||
<div class="btn-group mr-2">
|
||||
<?php $__currentLoopData = $folders; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $f): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<a href="<?php echo e(route('email.index', $f->type)); ?>"
|
||||
class="btn btn-xs <?php echo e($folder === $f->type ? 'btn-primary' : 'btn-default'); ?>">
|
||||
<i class="fas <?php echo e($f->icon); ?>"></i>
|
||||
<?php echo e($f->name); ?>
|
||||
|
||||
<?php if($f->type === 'inbox' && $messages->where('is_read', false)->count() > 0): ?>
|
||||
<span class="badge badge-light"><?php echo e($messages->where('is_read', false)->count()); ?></span>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</div>
|
||||
<a href="<?php echo e(route('email.compose')); ?>" class="btn btn-success btn-xs">
|
||||
<i class="fas fa-plus"></i> Nuova Email
|
||||
</a>
|
||||
<button class="btn btn-primary btn-xs" onclick="syncEmails()" id="syncBtn">
|
||||
<i class="fas fa-sync"></i> Ricevi/Invia
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="card-header pb-0">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-6">
|
||||
<div class="form-inline">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="selectAll" onchange="toggleAll(this)">
|
||||
<label class="custom-control-label" for="selectAll">Seleziona tutto</label>
|
||||
</div>
|
||||
<span class="ml-3 text-muted" id="selectedCount">0 selezionati</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="input-group input-group-sm float-right" style="width: 220px;">
|
||||
<select class="form-control" id="bulkAction">
|
||||
<option value="">Azioni di massa...</option>
|
||||
<?php if($folder !== 'trash'): ?>
|
||||
<option value="mark_read">Marca come letto</option>
|
||||
<option value="mark_unread">Marca come non letto</option>
|
||||
<option value="star">Aggiungi a preferiti</option>
|
||||
<option value="unstar">Rimuovi da preferiti</option>
|
||||
<option value="move_trash">Sposta nel cestino</option>
|
||||
<?php else: ?>
|
||||
<option value="empty_trash">Svuota cestino</option>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-secondary" type="button" onclick="executeBulkAction()">Applica</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 30px;"></th>
|
||||
<th style="width: 30px;"></th>
|
||||
<th style="width: 150px;">
|
||||
<?php $sort = request('sort'); $dir = request('direction'); ?>
|
||||
<?php if($folder === 'sent' || $folder === 'inviate'): ?>
|
||||
<a href="<?php echo e(request()->fullUrlWithQuery(['sort' => 'to_email', 'direction' => $sort === 'to_email' && $dir === 'asc' ? 'desc' : 'asc'])); ?>" class="text-dark text-decoration-none">
|
||||
Destinatario <?php echo $sort === 'to_email' ? ($dir === 'asc' ? '▲' : '▼') : ''; ?>
|
||||
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<a href="<?php echo e(request()->fullUrlWithQuery(['sort' => 'from_email', 'direction' => $sort === 'from_email' && $dir === 'asc' ? 'desc' : 'asc'])); ?>" class="text-dark text-decoration-none">
|
||||
Mittente <?php echo $sort === 'from_email' ? ($dir === 'asc' ? '▲' : '▼') : ''; ?>
|
||||
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</th>
|
||||
<th>
|
||||
<a href="<?php echo e(request()->fullUrlWithQuery(['sort' => 'subject', 'direction' => $sort === 'subject' && $dir === 'asc' ? 'desc' : 'asc'])); ?>" class="text-dark text-decoration-none">
|
||||
Oggetto <?php echo $sort === 'subject' ? ($dir === 'asc' ? '▲' : '▼') : ''; ?>
|
||||
|
||||
</a>
|
||||
</th>
|
||||
<th style="width: 120px;">
|
||||
<a href="<?php echo e(request()->fullUrlWithQuery(['sort' => 'received_at', 'direction' => $sort === 'received_at' && $dir === 'asc' ? 'desc' : 'asc'])); ?>" class="text-dark text-decoration-none">
|
||||
Data <?php echo $sort === 'received_at' ? ($dir === 'asc' ? '▲' : '▼') : ''; ?>
|
||||
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php $__empty_1 = true; $__currentLoopData = $messages; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $message): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?>
|
||||
<tr class="<?php echo e($message->is_read ? '' : 'font-weight-bold'); ?>">
|
||||
<td style="width: 30px;" onclick="event.stopPropagation()">
|
||||
<input type="checkbox" class="email-checkbox" value="<?php echo e($message->id); ?>" onchange="updateCount()">
|
||||
</td>
|
||||
<td style="width: 30px;" onclick="event.stopPropagation(); window.location='<?php echo e(route('email.show', [$folder, $message->id])); ?>'">
|
||||
<?php if($message->is_starred): ?>
|
||||
<i class="fas fa-star text-warning"></i>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td onclick="window.location='<?php echo e(route('email.show', [$folder, $message->id])); ?>'">
|
||||
<?php echo e($message->is_sent ? 'A: ' . $message->to_email : ($message->from_name ?: $message->from_email)); ?>
|
||||
|
||||
</td>
|
||||
<td onclick="window.location='<?php echo e(route('email.show', [$folder, $message->id])); ?>'">
|
||||
<?php echo e($message->subject ?? '(senza oggetto)'); ?>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<?php if($message->received_at): ?>
|
||||
<?php echo e($message->received_at->format('d/m/Y')); ?>
|
||||
|
||||
<?php elseif($message->sent_at): ?>
|
||||
<?php echo e($message->sent_at->format('d/m/Y')); ?>
|
||||
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?>
|
||||
<tr>
|
||||
<td colspan="5" class="text-center text-muted py-4">
|
||||
<i class="fas fa-inbox fa-2x mb-2"></i>
|
||||
<p>Nessuna email</p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<?php echo e($messages->withQueryString()->links('vendor.pagination.simple-bootstrap-4')); ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<?php $__env->stopSection(); ?>
|
||||
|
||||
<?php $__env->startPush('styles'); ?>
|
||||
<style>
|
||||
/* Bootstrap 4 Pagination override */
|
||||
.pagination {
|
||||
display: flex !important;
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
border-radius: 0.25rem;
|
||||
margin: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.page-item {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.page-link {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: 0.5rem 0.75rem;
|
||||
margin-left: -1px;
|
||||
line-height: 1.25;
|
||||
color: #007bff;
|
||||
background-color: #fff;
|
||||
border: 1px solid #dee2e6;
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.page-item.active .page-link {
|
||||
z-index: 3;
|
||||
color: #fff;
|
||||
background-color: #007bff;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.page-item.disabled .page-link {
|
||||
color: #6c757d;
|
||||
pointer-events: none;
|
||||
background-color: #fff;
|
||||
border-color: #dee2e6;
|
||||
}
|
||||
|
||||
.page-link:hover {
|
||||
color: #0056b3;
|
||||
background-color: #e9ecef;
|
||||
border-color: #dee2e6;
|
||||
}
|
||||
|
||||
/* Icons as text characters */
|
||||
.pagination .page-link::before,
|
||||
.pagination .page-link::after {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<?php $__env->stopPush(); ?>
|
||||
|
||||
<?php if($syncInterval > 0 && $folder === 'inbox'): ?>
|
||||
<?php $__env->startPush('scripts'); ?>
|
||||
<script>
|
||||
let syncInterval = <?php echo e($syncInterval * 60 * 1000); ?>;
|
||||
let syncTimeout;
|
||||
|
||||
function autoSync() {
|
||||
fetch('<?php echo e(url('email/sync/quick')); ?>', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '<?php echo e(csrf_token()); ?>',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(() => {
|
||||
console.log('Auto sync completed');
|
||||
}).catch(() => {
|
||||
console.log('Auto sync failed');
|
||||
});
|
||||
}
|
||||
|
||||
function startAutoSync() {
|
||||
if (syncInterval > 0) {
|
||||
syncTimeout = setInterval(autoSync, syncInterval);
|
||||
}
|
||||
}
|
||||
|
||||
function stopAutoSync() {
|
||||
if (syncTimeout) {
|
||||
clearInterval(syncTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
startAutoSync();
|
||||
|
||||
document.addEventListener('visibilitychange', function() {
|
||||
if (document.hidden) {
|
||||
stopAutoSync();
|
||||
} else {
|
||||
startAutoSync();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php $__env->stopPush(); ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php $__env->startSection('scripts'); ?>
|
||||
<script>
|
||||
function toggleAll(checkbox) {
|
||||
const checkboxes = document.querySelectorAll('.email-checkbox');
|
||||
checkboxes.forEach(function(cb) {
|
||||
cb.checked = checkbox.checked;
|
||||
});
|
||||
updateCount();
|
||||
}
|
||||
|
||||
function updateCount() {
|
||||
const checkboxes = document.querySelectorAll('.email-checkbox:checked');
|
||||
document.getElementById('selectedCount').textContent = checkboxes.length + ' selezionati';
|
||||
}
|
||||
|
||||
function executeBulkAction() {
|
||||
const select = document.getElementById('bulkAction');
|
||||
const action = select.value;
|
||||
if (!action) {
|
||||
alert('Seleziona un\'azione');
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'empty_trash') {
|
||||
if (!confirm('Sei sicuro di voler svuotare il cestino? Questa azione non è reversibile.')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const checkboxes = document.querySelectorAll('.email-checkbox:checked');
|
||||
if (action !== 'empty_trash' && checkboxes.length === 0) {
|
||||
alert('Seleziona almeno una email');
|
||||
return;
|
||||
}
|
||||
|
||||
let ids = [];
|
||||
if (action !== 'empty_trash') {
|
||||
ids = Array.from(checkboxes).map(function(cb) {
|
||||
return cb.value;
|
||||
});
|
||||
}
|
||||
|
||||
fetch('<?php echo e(route('email.bulk')); ?>', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '<?php echo e(csrf_token()); ?>',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ ids: ids, action: action })
|
||||
}).then(function(response) {
|
||||
return response.json();
|
||||
}).then(function(data) {
|
||||
if (data.success) {
|
||||
// Reset checkboxes before reload to avoid ghost selection
|
||||
document.querySelectorAll('.email-checkbox').forEach(function(cb) {
|
||||
cb.checked = false;
|
||||
});
|
||||
document.getElementById('selectAll').checked = false;
|
||||
updateCount();
|
||||
window.location.href = window.location.href;
|
||||
} else {
|
||||
alert(data.message || 'Errore');
|
||||
}
|
||||
}).catch(function() {
|
||||
alert('Errore');
|
||||
});
|
||||
}
|
||||
|
||||
function syncEmails() {
|
||||
const btn = document.getElementById('syncBtn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sincronizzazione...';
|
||||
|
||||
fetch('<?php echo e(url('email/sync/quick')); ?>', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '<?php echo e(csrf_token()); ?>',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(function(response) {
|
||||
return response.json();
|
||||
}).then(function(data) {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.message || 'Errore durante la sincronizzazione');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-sync"></i> Ricevi/Invia';
|
||||
}
|
||||
}).catch(function(error) {
|
||||
alert('Errore di connessione');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-sync"></i> Ricevi/Invia';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<?php $__env->stopSection(); ?>
|
||||
<?php echo $__env->make('layouts.adminlte', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH /var/www/html/glastree/resources/views/email/index.blade.php ENDPATH**/ ?>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php $__env->startSection('title', __('Page Expired')); ?>
|
||||
<?php $__env->startSection('code', '419'); ?>
|
||||
<?php $__env->startSection('message', __('Page Expired')); ?>
|
||||
|
||||
<?php echo $__env->make('errors::minimal', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH /var/www/html/glastree/vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/views/419.blade.php ENDPATH**/ ?>
|
||||
@@ -0,0 +1,723 @@
|
||||
<?php $__env->startSection('title', 'Impostazioni Email'); ?>
|
||||
<?php $__env->startSection('page_title', 'Configurazione Email'); ?>
|
||||
|
||||
<?php $__env->startSection('breadcrumbs'); ?>
|
||||
<li class="breadcrumb-item"><a href="<?php echo e(route('dashboard')); ?>">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="/impostazioni">Impostazioni</a></li>
|
||||
<li class="breadcrumb-item active">Email Settings</li>
|
||||
<?php $__env->stopSection(); ?>
|
||||
|
||||
<?php $__env->startSection('content'); ?>
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="card card-secondary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Sezioni</h3>
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a href="#imap" class="list-group-item list-group-item-action active" data-toggle="tab">
|
||||
<i class="nav-icon fas fa-server"></i> Server IMAP
|
||||
</a>
|
||||
<a href="#smtp" class="list-group-item list-group-item-action" data-toggle="tab">
|
||||
<i class="nav-icon fas fa-paper-plane"></i> Server SMTP (Invio)
|
||||
</a>
|
||||
<a href="#account" class="list-group-item list-group-item-action" data-toggle="tab">
|
||||
<i class="nav-icon fas fa-user"></i> Account
|
||||
</a>
|
||||
<a href="#sync" class="list-group-item list-group-item-action" data-toggle="tab">
|
||||
<i class="nav-icon fas fa-sync"></i> Sincronizzazione
|
||||
</a>
|
||||
<a href="#signature" class="list-group-item list-group-item-action" data-toggle="tab">
|
||||
<i class="nav-icon fas fa-signature"></i> Firma
|
||||
</a>
|
||||
<a href="#mittenti" class="list-group-item list-group-item-action" data-toggle="tab">
|
||||
<i class="nav-icon fas fa-user-plus"></i> Mittenti Aggiuntivi
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<form method="POST" action="<?php echo e(route('impostazioni.email.save')); ?>">
|
||||
<?php echo csrf_field(); ?>
|
||||
<input type="hidden" name="id" value="<?php echo e($settings->id ?? 1); ?>">
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="imap">
|
||||
<div class="card card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Configurazione Server IMAP</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if(session('success')): ?>
|
||||
<div class="alert alert-success"><?php echo e(session('success')); ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if(session('error')): ?>
|
||||
<div class="alert alert-danger"><?php echo e(session('error')); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="imap_host">Host IMAP</label>
|
||||
<input type="text" name="imap_host" id="imap_host" class="form-control"
|
||||
value="<?php echo e($settings->imap_host ?? 'imap.gmail.com'); ?>"
|
||||
placeholder="es. imap.gmail.com">
|
||||
<small class="text-muted">Gmail: imap.gmail.com | Outlook: outlook.office365.com</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label for="imap_port">Porta</label>
|
||||
<input type="number" name="imap_port" id="imap_port" class="form-control"
|
||||
value="<?php echo e($settings->imap_port ?? 993); ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label for="imap_encryption">Crittografia</label>
|
||||
<select name="imap_encryption" id="imap_encryption" class="form-control">
|
||||
<option value="ssl" <?php echo e(($settings->imap_encryption ?? 'ssl') == 'ssl' ? 'selected' : ''); ?>>SSL</option>
|
||||
<option value="tls" <?php echo e(($settings->imap_encryption ?? '') == 'tls' ? 'selected' : ''); ?>>TLS</option>
|
||||
<option value="none" <?php echo e(($settings->imap_encryption ?? '') == 'none' ? 'selected' : ''); ?>>Nessuna</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane" id="smtp">
|
||||
<div class="card card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Configurazione Server SMTP (per invio email)</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<strong>Nota:</strong> Per inviare email devi configurare il server SMTP.
|
||||
Per Gmail, usa <code>smtp.gmail.com</code> sulla porta <code>587</code> con TLS
|
||||
e genera una <a href="https://support.google.com/accounts/answer/185833" target="_blank">App Password</a>.
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="smtp_host">Host SMTP</label>
|
||||
<input type="text" name="smtp_host" id="smtp_host" class="form-control"
|
||||
value="<?php echo e($settings->smtp_host ?? 'smtp.gmail.com'); ?>"
|
||||
placeholder="es. smtp.gmail.com">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label for="smtp_port">Porta</label>
|
||||
<input type="number" name="smtp_port" id="smtp_port" class="form-control"
|
||||
value="<?php echo e($settings->smtp_port ?? 587); ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label for="smtp_encryption">Crittografia</label>
|
||||
<select name="smtp_encryption" id="smtp_encryption" class="form-control">
|
||||
<option value="tls" <?php echo e(($settings->smtp_encryption ?? 'tls') == 'tls' ? 'selected' : ''); ?>>TLS</option>
|
||||
<option value="ssl" <?php echo e(($settings->smtp_encryption ?? '') == 'ssl' ? 'selected' : ''); ?>>SSL</option>
|
||||
<option value="none" <?php echo e(($settings->smtp_encryption ?? '') == 'none' ? 'selected' : ''); ?>>Nessuna</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="smtp_username">Username SMTP</label>
|
||||
<input type="text" name="smtp_username" id="smtp_username" class="form-control"
|
||||
value="<?php echo e($settings->smtp_username ?? ''); ?>"
|
||||
placeholder="lascia vuoto per usare l'account IMAP">
|
||||
<small class="text-muted">Generalmente uguale all'account email</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="smtp_password">Password SMTP / App Password</label>
|
||||
<input type="password" name="smtp_password" id="smtp_password" class="form-control"
|
||||
placeholder="<?php echo e($settings->smtp_password ? '••••••••' : ''); ?>">
|
||||
<small class="text-muted">Lascia vuoto per usare la password IMAP</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane" id="account">
|
||||
<div class="card card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Credenziali Account</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="imap_username">Username/Email</label>
|
||||
<input type="text" name="imap_username" id="imap_username" class="form-control"
|
||||
value="<?php echo e($settings->imap_username ?? ''); ?>"
|
||||
placeholder="es. tuo@gmail.com">
|
||||
<small class="text-muted">Per Gmail usa l'indirizzo completo</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="imap_password">Password / App Password</label>
|
||||
<input type="password" name="imap_password" id="imap_password" class="form-control"
|
||||
placeholder="">
|
||||
<small class="text-muted">
|
||||
<a href="https://support.google.com/accounts/answer/185833" target="_blank">
|
||||
Per Gmail: genera una App Password
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="email_address">Email Indirizzo</label>
|
||||
<input type="email" name="email_address" id="email_address" class="form-control"
|
||||
value="<?php echo e($settings->email_address ?? ''); ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="email_name">Nome Visualizzato</label>
|
||||
<input type="text" name="email_name" id="email_name" class="form-control"
|
||||
value="<?php echo e($settings->email_name ?? ''); ?>"
|
||||
placeholder="es. Nome Cognome">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="reply_to">Email Risposta (Reply-To)</label>
|
||||
<input type="email" name="reply_to" id="reply_to" class="form-control"
|
||||
value="<?php echo e($settings->reply_to ?? ''); ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane" id="sync">
|
||||
<div class="card card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Sincronizzazione</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="sync_interval_minutes">Intervallo Sync (minuti)</label>
|
||||
<select name="sync_interval_minutes" id="sync_interval_minutes" class="form-control">
|
||||
<option value="1" <?php echo e(($settings->sync_interval_minutes ?? 5) == 1 ? 'selected' : ''); ?>>1 minuto</option>
|
||||
<option value="5" <?php echo e(($settings->sync_interval_minutes ?? 5) == 5 ? 'selected' : ''); ?>>5 minuti</option>
|
||||
<option value="15" <?php echo e(($settings->sync_interval_minutes ?? 5) == 15 ? 'selected' : ''); ?>>15 minuti</option>
|
||||
<option value="30" <?php echo e(($settings->sync_interval_minutes ?? 5) == 30 ? 'selected' : ''); ?>>30 minuti</option>
|
||||
<option value="60" <?php echo e(($settings->sync_interval_minutes ?? 5) == 60 ? 'selected' : ''); ?>>1 ora</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>Stato Connessione</label>
|
||||
<div class="mt-2">
|
||||
<?php if($settings->last_sync_at): ?>
|
||||
<span class="badge badge-success">
|
||||
<i class="fas fa-check-circle"></i> Ultimo sync: <?php echo e($settings->last_sync_at->format('d/m/Y H:i')); ?>
|
||||
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-secondary">
|
||||
<i class="fas fa-clock"></i> Mai sincronizzato
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" id="is_active"
|
||||
name="is_active" value="1" <?php echo e(($settings->is_active ?? false) ? 'checked' : ''); ?>>
|
||||
<label class="custom-control-label" for="is_active">Account Email Attivo</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane" id="signature">
|
||||
<div class="card card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Firma Email</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" id="signature_enabled"
|
||||
name="signature_enabled" value="1" <?php echo e(($settings->signature_enabled ?? true) ? 'checked' : ''); ?>>
|
||||
<label class="custom-control-label" for="signature_enabled">Attiva firma automatica</label>
|
||||
</div>
|
||||
<small class="text-muted">Quando attivo, la firma verrà aggiunta in fondo a ogni email inviata</small>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="form-group">
|
||||
<label for="signature">Testo Firma (formato HTML)</label>
|
||||
<div id="signature-editor" style="height: 200px;"></div>
|
||||
<input type="hidden" name="signature" id="signature_input" value="<?php echo e($settings->signature ?? ''); ?>">
|
||||
<small class="text-muted">Usa il riquadro sopra per comporre la tua firma. HTML supportato.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane" id="mittenti">
|
||||
<div class="card card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Mittenti Aggiuntivi</h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-sm btn-success" data-toggle="modal" data-target="#senderModal" onclick="resetSenderForm()">
|
||||
<i class="fas fa-plus mr-1"></i> Nuovo Mittente
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if(session('success')): ?>
|
||||
<div class="alert alert-success"><?php echo e(session('success')); ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if(session('error')): ?>
|
||||
<div class="alert alert-danger"><?php echo e(session('error')); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if($senderAccounts->count() > 0): ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>Nome</th>
|
||||
<th>SMTP</th>
|
||||
<th>Reply-To</th>
|
||||
<th>Verify</th>
|
||||
<th>Stato</th>
|
||||
<th style="width: 160px;">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php $__currentLoopData = $senderAccounts; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $sender): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<tr>
|
||||
<td><strong><?php echo e($sender->email_address); ?></strong></td>
|
||||
<td><?php echo e($sender->email_name ?: '-'); ?></td>
|
||||
<td><small><?php echo e($sender->smtp_host ?: '-'); ?>:<?php echo e($sender->smtp_port ?: '-'); ?></small></td>
|
||||
<td><small class="text-muted"><?php echo e($sender->reply_to ?: '-'); ?></small></td>
|
||||
<td><small class="text-muted"><?php echo e($sender->verify_email ?: '-'); ?></small></td>
|
||||
<td>
|
||||
<?php if($sender->is_active): ?>
|
||||
<span class="badge badge-success">Attivo</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-secondary">Disattivo</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-xs btn-info" onclick="editSender(<?php echo e($sender->id); ?>)" title="Modifica">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-xs btn-success" onclick="testSenderSmtp(<?php echo e($sender->id); ?>)" title="Test SMTP">
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
</button>
|
||||
<form action="<?php echo e(route('impostazioni.sender.destroy', $sender->id)); ?>" method="POST" class="d-inline" onsubmit="return confirm('Eliminare il mittente <?php echo e($sender->email_address); ?>?')">
|
||||
<?php echo csrf_field(); ?> <?php echo method_field('DELETE'); ?>
|
||||
<button type="submit" class="btn btn-xs btn-danger" title="Elimina">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="text-center text-muted py-4">
|
||||
<i class="fas fa-user-plus fa-2x mb-2"></i>
|
||||
<p class="mb-0">Nessun mittente aggiuntivo configurato.</p>
|
||||
<p class="mb-0">Usa il pulsante "Nuovo Mittente" per aggiungere un account di invio dedicato (es. Newsletter).</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if($senderAccounts->count() > 0): ?>
|
||||
<div class="card card-secondary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-info-circle mr-2"></i>Come funziona</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="mb-0">
|
||||
<li>I mittenti aggiuntivi sono account <strong>solo invio</strong> (SMTP) senza IMAP.</li>
|
||||
<li>Puoi selezionarli nel <strong>compose email</strong> o negli <strong>invii mailing</strong>.</li>
|
||||
<li>Il campo <strong>Reply-To</strong> imposta l'indirizzo di risposta (es. <code>no-reply@parrocchia.it</code>).</li>
|
||||
<li>Il campo <strong>Verify Email</strong> riceve un report di riepilogo dopo ogni invio massivo con l'esito (ok/falliti).</li>
|
||||
<li>Se Reply-To non è impostato, viene usato Verify Email come fallback.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer d-flex flex-wrap align-items-center gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save mr-1"></i> Salva Impostazioni
|
||||
</button>
|
||||
<button type="button" class="btn btn-info ml-2" onclick="testConnection()">
|
||||
<i class="fas fa-plug mr-1"></i> Test Connessione IMAP
|
||||
</button>
|
||||
<button type="button" class="btn btn-success ml-2" onclick="testSmtp()">
|
||||
<i class="fas fa-paper-plane mr-1"></i> Test Invio Email (SMTP)
|
||||
</button>
|
||||
<a href="<?php echo e(route('impostazioni.email.sync')); ?>" class="btn btn-warning ml-2">
|
||||
<i class="fas fa-sync mr-1"></i> Sincronizza Ora
|
||||
</a>
|
||||
<form action="<?php echo e(route('impostazioni.email.destroy')); ?>" method="POST" class="ml-auto" onsubmit="return confirm('Eliminare definitivamente la configurazione email? Tutti i dati di connessione verranno rimossi.')">
|
||||
<?php echo csrf_field(); ?> <?php echo method_field('DELETE'); ?>
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="fas fa-trash mr-1"></i> Elimina Configurazione
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php $__env->stopSection(); ?>
|
||||
|
||||
<div class="modal fade" id="senderModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<form id="senderForm" method="POST" action="<?php echo e(route('impostazioni.sender.store')); ?>">
|
||||
<?php echo csrf_field(); ?>
|
||||
<input type="hidden" name="_method" id="senderMethod" value="POST">
|
||||
<input type="hidden" name="id" id="senderId" value="">
|
||||
<div class="modal-header bg-primary">
|
||||
<h5 class="modal-title" id="senderModalTitle"><i class="fas fa-user-plus mr-2"></i>Nuovo Mittente</h5>
|
||||
<button type="button" class="close text-white" data-dismiss="modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="sender_email">Email *</label>
|
||||
<input type="email" name="email_address" id="sender_email" class="form-control" required placeholder="newsletter@parrocchia.it">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="sender_name">Nome visualizzato</label>
|
||||
<input type="text" name="email_name" id="sender_name" class="form-control" placeholder="Newsletter Parrocchiale">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<h6>Configurazione SMTP</h6>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="sender_smtp_host">Host SMTP</label>
|
||||
<input type="text" name="smtp_host" id="sender_smtp_host" class="form-control" placeholder="smtp.gmail.com">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label for="sender_smtp_port">Porta</label>
|
||||
<input type="number" name="smtp_port" id="sender_smtp_port" class="form-control" value="587">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label for="sender_smtp_encryption">Crittografia</label>
|
||||
<select name="smtp_encryption" id="sender_smtp_encryption" class="form-control">
|
||||
<option value="tls">TLS</option>
|
||||
<option value="ssl">SSL</option>
|
||||
<option value="none">Nessuna</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="sender_smtp_username">Username SMTP</label>
|
||||
<input type="text" name="smtp_username" id="sender_smtp_username" class="form-control" placeholder="Lascia vuoto per usare l'email">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="sender_smtp_password">Password SMTP</label>
|
||||
<input type="password" name="smtp_password" id="sender_smtp_password" class="form-control" placeholder="......">
|
||||
<small class="text-muted">Lascia vuoto per non cambiare</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="sender_reply_to">Reply-To <small class="text-muted">(no-reply)</small></label>
|
||||
<input type="email" name="reply_to" id="sender_reply_to" class="form-control" placeholder="no-reply@parrocchia.it">
|
||||
<small class="text-muted">Indirizzo di risposta per le email inviate</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="sender_verify_email">Verify Email <small class="text-muted">(report)</small></label>
|
||||
<input type="email" name="verify_email" id="sender_verify_email" class="form-control" placeholder="verifica@parrocchia.it">
|
||||
<small class="text-muted">Riceve report riepilogo dopo invii massivi</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sender_note">Nota interna</label>
|
||||
<textarea name="note" id="sender_note" class="form-control" rows="2" placeholder="Es. Newsletter settimanale"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" id="sender_is_active" name="is_active" value="1" checked>
|
||||
<label class="custom-control-label" for="sender_is_active">Mittente attivo</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annulla</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save mr-1"></i> Salva Mittente
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php $__env->startSection('scripts'); ?>
|
||||
<script>
|
||||
let senderAccounts = <?php echo json_encode($senderAccounts, 15, 512) ?>;
|
||||
|
||||
function resetSenderForm() {
|
||||
document.getElementById('senderForm').action = '<?php echo e(route('impostazioni.sender.store')); ?>';
|
||||
document.getElementById('senderMethod').value = 'POST';
|
||||
document.getElementById('senderModalTitle').textContent = 'Nuovo Mittente';
|
||||
document.getElementById('senderForm').reset();
|
||||
document.getElementById('senderId').value = '';
|
||||
document.getElementById('sender_is_active').checked = true;
|
||||
document.getElementById('sender_smtp_password').placeholder = '';
|
||||
document.getElementById('sender_smtp_password').required = false;
|
||||
}
|
||||
|
||||
function editSender(id) {
|
||||
const sender = senderAccounts.find(s => s.id === id);
|
||||
if (!sender) return;
|
||||
|
||||
document.getElementById('senderForm').action = '<?php echo e(route('impostazioni.sender.update', '__ID__')); ?>'.replace('__ID__', id);
|
||||
document.getElementById('senderMethod').value = 'PUT';
|
||||
document.getElementById('senderModalTitle').textContent = 'Modifica Mittente - ' + sender.email_address;
|
||||
document.getElementById('senderId').value = sender.id;
|
||||
document.getElementById('sender_email').value = sender.email_address;
|
||||
document.getElementById('sender_name').value = sender.email_name || '';
|
||||
document.getElementById('sender_smtp_host').value = sender.smtp_host || '';
|
||||
document.getElementById('sender_smtp_port').value = sender.smtp_port || 587;
|
||||
document.getElementById('sender_smtp_encryption').value = sender.smtp_encryption || 'tls';
|
||||
document.getElementById('sender_smtp_username').value = sender.smtp_username || '';
|
||||
document.getElementById('sender_smtp_password').value = '';
|
||||
document.getElementById('sender_smtp_password').placeholder = '......';
|
||||
document.getElementById('sender_smtp_password').required = false;
|
||||
document.getElementById('sender_reply_to').value = sender.reply_to || '';
|
||||
document.getElementById('sender_verify_email').value = sender.verify_email || '';
|
||||
document.getElementById('sender_note').value = sender.note || '';
|
||||
document.getElementById('sender_is_active').checked = sender.is_active;
|
||||
|
||||
$('#senderModal').modal('show');
|
||||
}
|
||||
|
||||
function testSenderSmtp(id) {
|
||||
const email = prompt('Inserisci l\'indirizzo email a cui inviare il test:');
|
||||
if (!email || !email.includes('@')) return;
|
||||
|
||||
const btn = event.target;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
|
||||
$.ajax({
|
||||
url: '<?php echo e(route('impostazioni.sender.test', '__ID__')); ?>'.replace('__ID__', id),
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-TOKEN': '<?php echo e(csrf_token()); ?>' },
|
||||
data: { email: email },
|
||||
timeout: 30000,
|
||||
success: function(response) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-paper-plane"></i>';
|
||||
alert(response.success ? '✅ ' + response.message : '❌ ' + response.message);
|
||||
},
|
||||
error: function(xhr) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-paper-plane"></i>';
|
||||
const msg = xhr.responseJSON?.message || 'Errore di connessione';
|
||||
alert('❌ ' + msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function testConnection() {
|
||||
$.ajax({
|
||||
url: '<?php echo e(route('impostazioni.email.test')); ?>',
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-TOKEN': '<?php echo e(csrf_token()); ?>' },
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
alert('✅ ' + response.message);
|
||||
} else {
|
||||
alert('❌ ' + response.message);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('❌ Errore nella richiesta');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function testSmtp() {
|
||||
var email = prompt('Inserisci l\'indirizzo email a cui inviare il test:');
|
||||
if (!email) return;
|
||||
|
||||
if (!email.includes('@')) {
|
||||
alert('⚠️ Indirizzo email non valido');
|
||||
return;
|
||||
}
|
||||
|
||||
var btn = event.target;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Invio...';
|
||||
|
||||
$.ajax({
|
||||
url: '<?php echo e(route('impostazioni.email.testSmtp')); ?>',
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-TOKEN': '<?php echo e(csrf_token()); ?>' },
|
||||
data: { email: email },
|
||||
timeout: 30000,
|
||||
success: function(response) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-paper-plane mr-1"></i> Test Invio Email (SMTP)';
|
||||
if (response.success) {
|
||||
alert('✅ ' + response.message);
|
||||
} else {
|
||||
alert('❌ ' + response.message);
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-paper-plane mr-1"></i> Test Invio Email (SMTP)';
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
alert('❌ ' + xhr.responseJSON.message);
|
||||
} else {
|
||||
alert('❌ Errore: ' + status + ' - ' + error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var quillEditorInstance = null;
|
||||
|
||||
function createEditor() {
|
||||
var container = document.querySelector('#signature-editor');
|
||||
if (!container || quillEditorInstance) return;
|
||||
|
||||
var quill = new Quill('#signature-editor', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
['bold', 'italic', 'underline'],
|
||||
[{ 'color': [] }, { 'background': [] }],
|
||||
[{ 'header': 1 }, { 'header': 2 }, { 'header': 3 }],
|
||||
[{ 'align': [] }],
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
['link', 'image'],
|
||||
['clean', 'code']
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
var signatureInput = document.getElementById('signature_input');
|
||||
var isEditingHtml = false;
|
||||
|
||||
quill.getModule('toolbar').addHandler('code', function() {
|
||||
if (!isEditingHtml) {
|
||||
var textarea = document.createElement('textarea');
|
||||
textarea.className = 'ql-editor';
|
||||
textarea.style.cssText = 'font-family: monospace; min-height: 150px; width: 100%;';
|
||||
textarea.value = signatureInput.value;
|
||||
quill.root.innerHTML = '';
|
||||
quill.root.appendChild(textarea);
|
||||
textarea.focus();
|
||||
isEditingHtml = true;
|
||||
} else {
|
||||
var textarea = quill.root.querySelector('textarea');
|
||||
if (textarea) {
|
||||
signatureInput.value = textarea.value;
|
||||
quill.root.innerHTML = textarea.value;
|
||||
}
|
||||
isEditingHtml = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (signatureInput.value) {
|
||||
quill.root.innerHTML = signatureInput.value;
|
||||
}
|
||||
|
||||
quill.on('text-change', function() {
|
||||
if (!isEditingHtml) {
|
||||
signatureInput.value = quill.root.innerHTML;
|
||||
}
|
||||
});
|
||||
|
||||
quillEditorInstance = quill;
|
||||
}
|
||||
|
||||
function initSignatureEditor() {
|
||||
if (quillEditorInstance) return;
|
||||
|
||||
if (typeof Quill === 'undefined') {
|
||||
var script = document.createElement('script');
|
||||
script.src = 'https://cdn.quilljs.com/1.3.7/quill.min.js';
|
||||
script.onload = createEditor;
|
||||
document.head.appendChild(script);
|
||||
|
||||
var link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = 'https://cdn.quilljs.com/1.3.7/quill.snow.css';
|
||||
document.head.appendChild(link);
|
||||
} else {
|
||||
createEditor();
|
||||
}
|
||||
}
|
||||
|
||||
$('a[data-toggle="tab"]').on('shown.bs.tab', function(e) {
|
||||
if ($(e.target).attr('href') === '#signature') {
|
||||
setTimeout(initSignatureEditor, 100);
|
||||
}
|
||||
});
|
||||
|
||||
if ($('#signature').hasClass('active')) {
|
||||
initSignatureEditor();
|
||||
}
|
||||
</script>
|
||||
<?php $__env->stopSection(); ?>
|
||||
<?php echo $__env->make('layouts.adminlte', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH /var/www/html/glastree/resources/views/admin/email-settings/index.blade.php ENDPATH**/ ?>
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php $__env->startSection('title', 'Gestione Ruoli'); ?>
|
||||
<?php $__env->startSection('page_title', 'Gestione Ruoli'); ?>
|
||||
|
||||
<?php $__env->startSection('breadcrumbs'); ?>
|
||||
<li class="breadcrumb-item active">Admin</li>
|
||||
<li class="breadcrumb-item active">Ruoli</li>
|
||||
<?php $__env->stopSection(); ?>
|
||||
|
||||
<?php $__env->startSection('content'); ?>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Ruoli Predefiniti</h3>
|
||||
<a href="/admin/ruoli/create" class="btn btn-success btn-sm float-right">
|
||||
<i class="fas fa-plus"></i> Nuovo Ruolo
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nome</th>
|
||||
<th>Descrizione</th>
|
||||
<th>Utenti</th>
|
||||
<th>Permessi</th>
|
||||
<th style="width: 100px;">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php $__currentLoopData = $ruoli; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $ruolo): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<tr>
|
||||
<td><strong><?php echo e($ruolo->name); ?></strong></td>
|
||||
<td><?php echo e($ruolo->description); ?></td>
|
||||
<td><span class="badge badge-info"><?php echo e($ruolo->users_count); ?></span></td>
|
||||
<td>
|
||||
<?php $__currentLoopData = $ruolo->permissions; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $module => $level): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<span class="badge badge-<?php echo e($level == 2 ? 'success' : ($level == 1 ? 'info' : 'secondary')); ?>">
|
||||
<?php echo e(ucfirst($module)); ?>: <?php echo e($level == 2 ? 'Full' : ($level == 1 ? 'Read' : 'None')); ?>
|
||||
|
||||
</span>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</td>
|
||||
<td>
|
||||
<a href="<?php echo e(url('/admin/ruoli/' . $ruolo->id . '/edit')); ?>" class="btn btn-xs btn-warning">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<?php if($ruolo->users_count == 0): ?>
|
||||
<form method="POST" action="<?php echo e(url('/admin/ruoli/' . $ruolo->id)); ?>" style="display: inline;">
|
||||
<?php echo csrf_field(); ?> <?php echo method_field('DELETE'); ?>
|
||||
<button type="submit" class="btn btn-xs btn-danger" onclick="return confirm('Eliminare questo ruolo?')">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<button type="button" class="btn btn-xs btn-secondary" disabled title="Utenti associati: <?php echo e($ruolo->users_count); ?>">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php $__env->stopSection(); ?>
|
||||
<?php echo $__env->make('admin.layout', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH /var/www/html/glastree/resources/views/admin/ruoli/index.blade.php ENDPATH**/ ?>
|
||||
@@ -1,72 +0,0 @@
|
||||
<?php $attributes ??= new \Illuminate\View\ComponentAttributeBag;
|
||||
|
||||
$__newAttributes = [];
|
||||
$__propNames = \Illuminate\View\ComponentAttributeBag::extractPropNames((['cartelle', 'currentFolderId' => null, 'level' => 0, 'canWrite' => false, 'canDelete' => false]));
|
||||
|
||||
foreach ($attributes->all() as $__key => $__value) {
|
||||
if (in_array($__key, $__propNames)) {
|
||||
$$__key = $$__key ?? $__value;
|
||||
} else {
|
||||
$__newAttributes[$__key] = $__value;
|
||||
}
|
||||
}
|
||||
|
||||
$attributes = new \Illuminate\View\ComponentAttributeBag($__newAttributes);
|
||||
|
||||
unset($__propNames);
|
||||
unset($__newAttributes);
|
||||
|
||||
foreach (array_filter((['cartelle', 'currentFolderId' => null, 'level' => 0, 'canWrite' => false, 'canDelete' => false]), 'is_string', ARRAY_FILTER_USE_KEY) as $__key => $__value) {
|
||||
$$__key = $$__key ?? $__value;
|
||||
}
|
||||
|
||||
$__defined_vars = get_defined_vars();
|
||||
|
||||
foreach ($attributes->all() as $__key => $__value) {
|
||||
if (array_key_exists($__key, $__defined_vars)) unset($$__key);
|
||||
}
|
||||
|
||||
unset($__defined_vars, $__key, $__value); ?>
|
||||
|
||||
<?php $__currentLoopData = $cartelle; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $cartella): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<?php
|
||||
$isActive = $currentFolderId && (int) $currentFolderId === (int) $cartella->id;
|
||||
$hasChildren = $cartella->children->count() > 0;
|
||||
?>
|
||||
<div class="list-group-item d-flex align-items-center py-1 px-2 <?php echo e($isActive ? 'active' : ''); ?>"
|
||||
style="padding-left: <?php echo e(20 + $level * 20); ?>px !important;">
|
||||
<a href="/documenti?folder_id=<?php echo e($cartella->id); ?>"
|
||||
class="d-flex align-items-center text-decoration-none flex-grow-1 <?php echo e($isActive ? 'text-white' : ''); ?>"
|
||||
style="min-width: 0;">
|
||||
<i class="fas fa-folder <?php echo e($isActive ? 'text-white' : 'text-warning'); ?> mr-2"></i>
|
||||
<span class="small text-truncate"><?php echo e($cartella->nome); ?></span>
|
||||
</a>
|
||||
<?php if($canWrite || $canDelete): ?>
|
||||
<div class="folder-actions ml-1 flex-shrink-0">
|
||||
<?php if($canWrite): ?>
|
||||
<button type="button" class="btn btn-xs btn-link p-0 mr-1" onclick='event.stopPropagation(); renameFolder(<?php echo e($cartella->id); ?>, <?php echo e(json_encode($cartella->nome)); ?>)' title="Rinomina">
|
||||
<i class="fas fa-pen <?php echo e($isActive ? 'text-white' : 'text-muted'); ?>"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-xs btn-link p-0 mr-1" onclick='event.stopPropagation(); moveFolder(<?php echo e($cartella->id); ?>, <?php echo e(json_encode($cartella->nome)); ?>)' title="Sposta">
|
||||
<i class="fas fa-folder-open <?php echo e($isActive ? 'text-white' : 'text-info'); ?>"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if($canDelete): ?>
|
||||
<button type="button" class="btn btn-xs btn-link p-0" onclick='event.stopPropagation(); deleteFolder(<?php echo e($cartella->id); ?>, <?php echo e(json_encode($cartella->nome)); ?>)' title="Elimina">
|
||||
<i class="fas fa-times <?php echo e($isActive ? 'text-white' : 'text-danger'); ?>"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if($hasChildren): ?>
|
||||
<?php echo $__env->make('documenti._folder_tree', [
|
||||
'cartelle' => $cartella->children,
|
||||
'currentFolderId' => $currentFolderId,
|
||||
'level' => $level + 1,
|
||||
'canWrite' => $canWrite,
|
||||
'canDelete' => $canDelete,
|
||||
], array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
<?php /**PATH /var/www/html/glastree/resources/views/documenti/_folder_tree.blade.php ENDPATH**/ ?>
|
||||
@@ -285,7 +285,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="tab-pane" id="help-backup">
|
||||
@@ -469,9 +468,8 @@ sudo certbot --apache -d glastree.esempio.it</code></pre>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<h5>PHP Web Installer</h5>
|
||||
<p>L'applicazione include un <strong>wizard di installazione web</strong> nella cartella <code>installer/</code>. Questo wizard gestisce sia installazioni fresh che ripristino da backup.</p>
|
||||
<p>Vedi la sezione <strong>Installazione</strong> per la guida completa.</p>
|
||||
<h5>Comandi Artisan</h5>
|
||||
<p>Per installazione e ripristino usa i comandi Artisan da terminale. Vedi la sezione <strong>Installazione</strong> per la guida completa.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -775,7 +773,7 @@ echo 'Utente creato con successo!';
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5>Panoramica</h5>
|
||||
<p>l'applicazione include un <strong>wizard di installazione web</strong> che guida passo-passo nella configurazione su un nuovo server LAMP. Supporta due modalita:</p>
|
||||
<p>L'installazione avviene tramite l'interattivo <code>install.php</code> che guida passo-passo. Supporta due modalita:</p>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="callout callout-info">
|
||||
@@ -829,19 +827,37 @@ git clone <URL_REPOSITORY> glastree
|
||||
# Opzione B — ZIP (se non hai git)
|
||||
# unzip /percorso/del/glastree.zip -d glastree
|
||||
|
||||
cd glastree
|
||||
composer install --no-dev --optimize-autoloader
|
||||
npm ci && npm run build</code></pre>
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Se non hai accesso a npm, puoi saltare il build. L'interfaccia usera comunque AdminLTE via CDN.
|
||||
cd glastree</code></pre>
|
||||
|
||||
<h6>Passo 3: Eseguire l'installer</h6>
|
||||
<p>Lancerai l'installer interattivo che ti guidera attraverso tutte le fasi:</p>
|
||||
<pre><code>php install.php</code></pre>
|
||||
<p>L'installer ti chiedera:</p>
|
||||
<ol>
|
||||
<li><strong>Modalità</strong>: Fresh Install (Apache/Docker) o Restore da Backup</li>
|
||||
<li><strong>Parametri generali</strong>: nome sito, amministratore (nome, email, password), URL pubblico</li>
|
||||
<li><strong>Database</strong>: MySQL (host, porta, nome, utente, password) o SQLite</li>
|
||||
</ol>
|
||||
<p>L'installer esegue automaticamente:</p>
|
||||
<ul>
|
||||
<li>Generazione <code>.env</code> e <code>APP_KEY</code></li>
|
||||
<li>Creazione del database</li>
|
||||
<li><code>composer install</code> e registrazione pacchetti</li>
|
||||
<li>Migration e seed dei dati di base</li>
|
||||
<li>Creazione dell'utente amministratore</li>
|
||||
<li>Compilazione asset frontend (se npm disponibile)</li>
|
||||
<li>Impostazione permessi cartelle</li>
|
||||
</ul>
|
||||
<div class="callout callout-info">
|
||||
<h6><i class="fas fa-info-circle"></i> Docker</h6>
|
||||
<p>Se scegli la modalita Docker, l'installer builda l'immagine, avvia i container e crea l'admin all'interno del container.</p>
|
||||
</div>
|
||||
<div class="callout callout-success">
|
||||
<h6><i class="fas fa-upload"></i> Restore da Backup</h6>
|
||||
<p>Se scegli Restore, l'installer ti chiedera il percorso del file ZIP di backup, ripristinera il database, i file e la configurazione.</p>
|
||||
</div>
|
||||
|
||||
<h6>Passo 3: Permessi cartelle</h6>
|
||||
<pre><code>sudo chown -R www-data:www-data storage bootstrap/cache
|
||||
sudo chmod -R 775 storage bootstrap/cache</code></pre>
|
||||
|
||||
<h6>Passo 4: Configurare Apache</h6>
|
||||
<h6 class="mt-4">Passo 4: Configurare Apache (solo modalita Fresh Install)</h6>
|
||||
<pre><code>sudo tee /etc/apache2/sites-available/glastree.conf <<APACHE
|
||||
<VirtualHost *:80>
|
||||
ServerName glastree.esempio.it
|
||||
@@ -861,55 +877,29 @@ APACHE
|
||||
sudo a2dissite 000-default.conf
|
||||
sudo a2ensite glastree.conf
|
||||
sudo systemctl reload apache2</code></pre>
|
||||
|
||||
<h6>Passo 5: Avviare il wizard di installazione</h6>
|
||||
<p>Apri il browser e visita l'indirizzo del tuo server:</p>
|
||||
<pre><code>http://glastree.esempio.it/installer/</code></pre>
|
||||
|
||||
<p>Il wizard ti guidera attraverso 6 passi:</p>
|
||||
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead><tr><th>Passo</th><th>Descrizione</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>1. Benvenuto</td><td>Scegli Fresh Install o Restore da Backup</td></tr>
|
||||
<tr><td>2. Requisiti</td><td>Verifica PHP, estensioni e permessi</td></tr>
|
||||
<tr><td>3. Database</td><td>Configura MySQL (crea nuovo o usa esistente)</td></tr>
|
||||
<tr><td>4. Installazione</td><td>Fresh: migration e seed. Backup: upload ZIP</td></tr>
|
||||
<tr><td>5. Amministratore</td><td>Fresh: crea super-admin. Backup: verifica utenti</td></tr>
|
||||
<tr><td>6. Finalizzazione</td><td>Cache, storage link, auto-eliminazione installer</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<strong>Attenzione:</strong> Al termine dell'installazione, la cartella <code>installer/</code> viene <strong>automaticamente eliminata</strong>. Non puoi rieseguire l'installazione se non ricreando manualmente la cartella.
|
||||
<strong>Importante:</strong> Il restore da backup richiede che tu abbia un file ZIP generato dalla pagina <strong>Admin → Backup</strong> del server originale. Estrai manualmente il contenuto seguendo la struttura del progetto.
|
||||
</div>
|
||||
|
||||
<h5 class="mt-4">Modalita Fresh Install</h5>
|
||||
<p>Seleziona questa modalita quando installi l'applicazione per la prima volta su un server.</p>
|
||||
<p>Cosa succede durante l'installazione:</p>
|
||||
<h5 class="mt-4">Cosa succede durante l'installazione</h5>
|
||||
<p>I comandi <code>php artisan migrate --seed</code> eseguono:</p>
|
||||
<ol>
|
||||
<li>Genera <code>APP_KEY</code> per la crittografia</li>
|
||||
<li>Genera <code>APP_KEY</code> per la crittografia (gia eseguito al passo 5)</li>
|
||||
<li>Esegue tutte le migration per creare le tabelle del database</li>
|
||||
<li>Popola i dati di base (diocesi, comuni, ruoli, preset)</li>
|
||||
<li>Crea il collegamento <code>storage → public/storage</code></li>
|
||||
<li>Crea l'utente amministratore con i permessi completi</li>
|
||||
</ol>
|
||||
|
||||
<h5 class="mt-4">Modalita Restore da Backup</h5>
|
||||
<p>Seleziona questa modalita quando vuoi migrare un'installazione esistente su un nuovo server.</p>
|
||||
<p>Cosa serve:</p>
|
||||
<h5 class="mt-4">Restore da Backup</h5>
|
||||
<p>Il backup ZIP generato dalla pagina <strong>Admin → Backup</strong> contiene un dump SQL e i file dei documenti. Per ripristinare:</p>
|
||||
<ul>
|
||||
<li>Un file ZIP generato dalla pagina <strong>Admin → Backup</strong> del server originale</li>
|
||||
<li>Il wizard accetta upload diretto del file o un percorso sul server</li>
|
||||
<li>Estrai il file ZIP in una cartella temporanea</li>
|
||||
<li>Importa il database dal file SQL (<code>mysql -u root -p glastree < backup/database.sql</code>)</li>
|
||||
<li>Copia i file in <code>storage/app/</code> per ripristinare i documenti</li>
|
||||
<li>Copia il file <code>.env</code> dal backup (modifica le credenziali DB per il nuovo server)</li>
|
||||
<li>Esegui <code>php artisan key:generate</code> se la <code>APP_KEY</code> del backup non è utilizzabile</li>
|
||||
</ul>
|
||||
<p>Cosa succede durante il restore:</p>
|
||||
<ol>
|
||||
<li>Estrae il file ZIP in una cartella temporanea</li>
|
||||
<li>Importa <code>database.sql</code> nel database MySQL configurato</li>
|
||||
<li>Copia i file documenti in <code>storage/app/documenti/</code></li>
|
||||
<li>Ripristina il file <code>.env</code> dal backup (preservando le credenziali DB del nuovo server)</li>
|
||||
</ol>
|
||||
|
||||
<h5 class="mt-4">Dopo l'Installazione</h5>
|
||||
<ol>
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php $__env->startSection('title', 'Calendario Eventi'); ?>
|
||||
<?php $__env->startSection('page_title', 'Calendario Eventi'); ?>
|
||||
|
||||
<?php $__env->startSection('content'); ?>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">
|
||||
<i class="fas fa-calendar-alt mr-2"></i>Calendario Eventi
|
||||
</h3>
|
||||
<div class="card-tools">
|
||||
<a href="<?php echo e(route('eventi.index')); ?>" class="btn btn-secondary btn-sm">
|
||||
<i class="fas fa-list mr-1"></i> Elenco Eventi
|
||||
</a>
|
||||
<a href="<?php echo e(route('eventi.create')); ?>" class="btn btn-success btn-sm">
|
||||
<i class="fas fa-plus mr-1"></i> Nuovo Evento
|
||||
</a>
|
||||
<form action="<?php echo e(route('eventi.export-selected-ics')); ?>" method="POST" class="d-inline">
|
||||
<?php echo csrf_field(); ?>
|
||||
<button type="submit" class="btn btn-success btn-sm">
|
||||
<i class="fas fa-calendar-plus mr-1"></i> Esporta ICS
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="calendar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php $__env->stopSection(); ?>
|
||||
|
||||
<?php $__env->startSection('styles'); ?>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.10/index.global.min.css">
|
||||
<?php $__env->stopSection(); ?>
|
||||
|
||||
<?php $__env->startSection('scripts'); ?>
|
||||
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.10/index.global.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var calendarEl = document.getElementById('calendar');
|
||||
|
||||
var calendar = new FullCalendar.Calendar(calendarEl, {
|
||||
initialView: 'dayGridMonth',
|
||||
locale: 'it',
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: 'dayGridMonth,timeGridWeek,listWeek'
|
||||
},
|
||||
buttonText: {
|
||||
today: 'Oggi',
|
||||
month: 'Mese',
|
||||
week: 'Settimana',
|
||||
list: 'Lista'
|
||||
},
|
||||
eventSources: [
|
||||
{
|
||||
url: '/eventi/calendar/events',
|
||||
method: 'GET',
|
||||
extraParams: function() {
|
||||
return {
|
||||
_token: document.querySelector('meta[name="csrf-token"]')?.content || ''
|
||||
};
|
||||
},
|
||||
startParam: 'start',
|
||||
endParam: 'end',
|
||||
success: function(response) {
|
||||
console.log('Eventi caricati:', response.length, response);
|
||||
},
|
||||
failure: function(error) {
|
||||
console.error('Errore:', error);
|
||||
console.log('XHR:', error.xhr);
|
||||
alert('Errore nel caricamento degli eventi. Controlla la console per dettagli.');
|
||||
}
|
||||
}
|
||||
],
|
||||
eventClick: function(info) {
|
||||
info.jsEvent.preventDefault();
|
||||
window.location.href = info.event.url;
|
||||
},
|
||||
eventDisplay: 'block',
|
||||
displayEventTime: true,
|
||||
displayEventEnd: true,
|
||||
nowIndicator: true,
|
||||
height: 'auto',
|
||||
expandRows: true,
|
||||
stickyHeaderDates: true,
|
||||
dayMaxEvents: true,
|
||||
eventTimeFormat: {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
}
|
||||
});
|
||||
|
||||
calendar.render();
|
||||
});
|
||||
</script>
|
||||
<?php $__env->stopSection(); ?>
|
||||
<?php echo $__env->make('layouts.adminlte', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH /var/www/html/glastree/resources/views/eventi/calendar.blade.php ENDPATH**/ ?>
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
use App\Models\AppSetting;
|
||||
$appLogo = AppSetting::getLogoUrl();
|
||||
$appName = AppSetting::getAppName() ?? 'Glastree';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Reset Password - <?php echo e(e($appName)); ?></title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/admin-lte@3.2/dist/css/adminlte.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
</head>
|
||||
<body class="hold-transition login-page">
|
||||
<div class="login-box">
|
||||
<div class="login-logo">
|
||||
<a href="/">
|
||||
<?php if($appLogo): ?>
|
||||
<img src="<?php echo e($appLogo); ?>" alt="Logo" style="max-height: 80px; max-width: 200px;">
|
||||
<?php else: ?>
|
||||
<i class="fas fa-tree text-success"></i> <b><?php echo e(e($appName)); ?></b>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body login-card-body">
|
||||
<p class="login-box-msg">Password dimenticata? Inserisci la tua email e ti invieremo il link per reimpostarla.</p>
|
||||
|
||||
<?php if(session('success')): ?>
|
||||
<div class="alert alert-success"><?php echo e(session('success')); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form action="<?php echo e(route('password.email')); ?>" method="POST">
|
||||
<?php echo csrf_field(); ?>
|
||||
<div class="input-group mb-3">
|
||||
<input type="email" name="email" class="form-control <?php $__errorArgs = ['email'];
|
||||
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
|
||||
if ($__bag->has($__errorArgs[0])) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs[0]); ?> is-invalid <?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>" placeholder="Email" required value="<?php echo e(old('email')); ?>">
|
||||
<div class="input-group-append">
|
||||
<div class="input-group-text"><span class="fas fa-envelope"></span></div>
|
||||
</div>
|
||||
<?php $__errorArgs = ['email'];
|
||||
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
|
||||
if ($__bag->has($__errorArgs[0])) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs[0]); ?>
|
||||
<span class="invalid-feedback"><?php echo e($message); ?></span>
|
||||
<?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<button type="submit" class="btn btn-success btn-block">Invia link reset</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<p class="mb-0 text-center mt-3">
|
||||
<a href="<?php echo e(route('login')); ?>">Torna al login</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
<?php /**PATH /var/www/html/glastree/resources/views/auth/forgot-password.blade.php ENDPATH**/ ?>
|
||||
+2315
File diff suppressed because it is too large
Load Diff
@@ -1,87 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Anteprima non disponibile</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
background-color: #f4f6f9;
|
||||
color: #333;
|
||||
}
|
||||
.fallback-container {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
max-width: 400px;
|
||||
}
|
||||
.fallback-icon {
|
||||
font-size: 64px;
|
||||
color: #6c757d;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.fallback-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.fallback-message {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.fallback-file-info {
|
||||
background: #f8f9fa;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 24px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.fallback-file-info strong {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.download-btn {
|
||||
display: inline-block;
|
||||
padding: 10px 24px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.download-btn:hover {
|
||||
background-color: #0056b3;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="fallback-container">
|
||||
<div class="fallback-icon">📄</div>
|
||||
<div class="fallback-title">Anteprima non disponibile</div>
|
||||
<div class="fallback-message">
|
||||
Questo tipo di file non può essere visualizzato nell'anteprima.
|
||||
Scarica il file per visualizzarlo con l'applicazione appropriata.
|
||||
</div>
|
||||
<div class="fallback-file-info">
|
||||
<strong><?php echo e($documento->nome_file); ?></strong>
|
||||
<?php echo e(strtoupper(pathinfo($documento->file_path, PATHINFO_EXTENSION))); ?> ·
|
||||
<?php echo e(number_format($documento->dimensione / 1024, 1)); ?> KB
|
||||
</div>
|
||||
<a href="/documenti/<?php echo e($documento->id); ?>/download" class="download-btn">
|
||||
⬇️ Scarica File
|
||||
</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<?php /**PATH /var/www/html/glastree/resources/views/documenti/preview-fallback.blade.php ENDPATH**/ ?>
|
||||
@@ -0,0 +1,349 @@
|
||||
<?php $__env->startSection('title', 'Eventi'); ?>
|
||||
<?php $__env->startSection('page_title', 'Eventi'); ?>
|
||||
|
||||
<?php
|
||||
$canWriteEventi = Auth::user()->canManage('eventi');
|
||||
$canDeleteEventi = Auth::user()->canDelete('eventi');
|
||||
?>
|
||||
|
||||
<?php $__env->startSection('content'); ?>
|
||||
|
||||
<?php if(session('success')): ?>
|
||||
<div class="alert alert-success alert-dismissible">
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
<?php echo e(session('success')); ?>
|
||||
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Tutti gli Eventi</h3>
|
||||
<div class="card-tools">
|
||||
<a href="/eventi/calendar" class="btn btn-sm btn-outline-primary mr-2">
|
||||
<i class="fas fa-calendar-alt mr-1"></i> Calendario
|
||||
</a>
|
||||
<button type="button" class="btn btn-sm btn-success mr-2" onclick="exportSelectedIcs()">
|
||||
<i class="fas fa-calendar-plus mr-1"></i> Esporta ICS
|
||||
</button>
|
||||
<?php if($canDeleteEventi): ?>
|
||||
<div class="btn-group mr-2">
|
||||
<button type="button" class="btn btn-sm btn-danger" onclick="deleteSelected()">
|
||||
<i class="fas fa-trash mr-1"></i> Elimina Selezionati
|
||||
</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if($canWriteEventi): ?>
|
||||
<a href="/eventi/create" class="btn btn-xs btn-success">
|
||||
<i class="fas fa-plus mr-1"></i> Nuovo Evento
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="GET" class="mb-3">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<input type="text" name="search" class="form-control" placeholder="Cerca evento..." value="<?php echo e(request('search')); ?>">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select name="gruppo_id" class="form-control" onchange="this.form.submit()">
|
||||
<option value="">Tutti i gruppi</option>
|
||||
<?php $__currentLoopData = $gruppi; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $g): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<option value="<?php echo e($g->id); ?>" <?php echo e(request('gruppo_id') == $g->id ? 'selected' : ''); ?>><?php echo e($g->full_path); ?></option>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select name="tipo" class="form-control" onchange="this.form.submit()">
|
||||
<option value="">Tutte le ricorrenze</option>
|
||||
<option value="singolo" <?php echo e(request('tipo') == 'singolo' ? 'selected' : ''); ?>>Singolo</option>
|
||||
<option value="settimanale" <?php echo e(request('tipo') == 'settimanale' ? 'selected' : ''); ?>>Settimanale</option>
|
||||
<option value="mensile" <?php echo e(request('tipo') == 'mensile' ? 'selected' : ''); ?>>Mensile</option>
|
||||
<option value="annuale" <?php echo e(request('tipo') == 'annuale' ? 'selected' : ''); ?>>Annuale</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-primary"><i class="fas fa-search mr-1"></i> Cerca</button>
|
||||
<a href="/eventi" class="btn btn-secondary"><i class="fas fa-times"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<table class="table table-bordered table-hover">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th style="width: 40px;">
|
||||
<?php if($canDeleteEventi): ?>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="select-all" onchange="toggleSelectAll(this)">
|
||||
<label class="custom-control-label" for="select-all"></label>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</th>
|
||||
<th>
|
||||
<a href="<?php echo e(request()->fullUrlWithQuery(['sort' => 'nome_evento', 'direction' => request('sort') === 'nome_evento' && request('direction') === 'asc' ? 'desc' : 'asc'])); ?>" class="text-dark">
|
||||
Nome Evento
|
||||
<?php if(request('sort') === 'nome_evento'): ?>
|
||||
<i class="fas fa-sort-<?php echo e(request('direction') === 'asc' ? 'up' : 'down'); ?> ml-1"></i>
|
||||
<?php else: ?>
|
||||
<i class="fas fa-sort ml-1 text-muted"></i>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a href="<?php echo e(request()->fullUrlWithQuery(['sort' => 'descrizione_evento', 'direction' => request('sort') === 'descrizione_evento' && request('direction') === 'asc' ? 'desc' : 'asc'])); ?>" class="text-dark">
|
||||
Descrizione
|
||||
<?php if(request('sort') === 'descrizione_evento'): ?>
|
||||
<i class="fas fa-sort-<?php echo e(request('direction') === 'asc' ? 'up' : 'down'); ?> ml-1"></i>
|
||||
<?php else: ?>
|
||||
<i class="fas fa-sort ml-1 text-muted"></i>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a href="<?php echo e(request()->fullUrlWithQuery(['sort' => 'tipo_evento', 'direction' => request('sort') === 'tipo_evento' && request('direction') === 'asc' ? 'desc' : 'asc'])); ?>" class="text-dark">
|
||||
Tipologia
|
||||
<?php if(request('sort') === 'tipo_evento'): ?>
|
||||
<i class="fas fa-sort-<?php echo e(request('direction') === 'asc' ? 'up' : 'down'); ?> ml-1"></i>
|
||||
<?php else: ?>
|
||||
<i class="fas fa-sort ml-1 text-muted"></i>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a href="<?php echo e(request()->fullUrlWithQuery(['sort' => 'tipo_recorrenza', 'direction' => request('sort') === 'tipo_recorrenza' && request('direction') === 'asc' ? 'desc' : 'asc'])); ?>" class="text-dark">
|
||||
Ricorrenza
|
||||
<?php if(request('sort') === 'tipo_recorrenza'): ?>
|
||||
<i class="fas fa-sort-<?php echo e(request('direction') === 'asc' ? 'up' : 'down'); ?> ml-1"></i>
|
||||
<?php else: ?>
|
||||
<i class="fas fa-sort ml-1 text-muted"></i>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a href="<?php echo e(request()->fullUrlWithQuery(['sort' => 'gruppi', 'direction' => request('sort') === 'gruppi' && request('direction') === 'asc' ? 'desc' : 'asc'])); ?>" class="text-dark">
|
||||
Gruppi
|
||||
<?php if(request('sort') === 'gruppi'): ?>
|
||||
<i class="fas fa-sort-<?php echo e(request('direction') === 'asc' ? 'up' : 'down'); ?> ml-1"></i>
|
||||
<?php else: ?>
|
||||
<i class="fas fa-sort ml-1 text-muted"></i>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a href="<?php echo e(request()->fullUrlWithQuery(['sort' => 'responsabili', 'direction' => request('sort') === 'responsabili' && request('direction') === 'asc' ? 'desc' : 'asc'])); ?>" class="text-dark">
|
||||
Responsabili
|
||||
<?php if(request('sort') === 'responsabili'): ?>
|
||||
<i class="fas fa-sort-<?php echo e(request('direction') === 'asc' ? 'up' : 'down'); ?> ml-1"></i>
|
||||
<?php else: ?>
|
||||
<i class="fas fa-sort ml-1 text-muted"></i>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
</th>
|
||||
<th style="width: 120px;">
|
||||
<a href="<?php echo e(request()->fullUrlWithQuery(['sort' => 'created_at', 'direction' => request('sort') === 'created_at' && request('direction') === 'asc' ? 'desc' : 'asc'])); ?>" class="text-dark">
|
||||
Creato
|
||||
<?php if(request('sort') === 'created_at'): ?>
|
||||
<i class="fas fa-sort-<?php echo e(request('direction') === 'asc' ? 'up' : 'down'); ?> ml-1"></i>
|
||||
<?php else: ?>
|
||||
<i class="fas fa-sort ml-1 text-muted"></i>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
</th>
|
||||
<th style="width: 120px;">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php $__empty_1 = true; $__currentLoopData = $eventi; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $evento): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?>
|
||||
<tr data-id="<?php echo e($evento->id); ?>">
|
||||
<td>
|
||||
<?php if($canDeleteEventi): ?>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input row-checkbox" id="row-checkbox-<?php echo e($evento->id); ?>" value="<?php echo e($evento->id); ?>">
|
||||
<label class="custom-control-label" for="row-checkbox-<?php echo e($evento->id); ?>"></label>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/eventi/<?php echo e($evento->id); ?>">
|
||||
<i class="fas fa-calendar mr-1 text-primary"></i>
|
||||
<?php echo e($evento->nome_evento); ?>
|
||||
|
||||
</a>
|
||||
<?php if($evento->is_incontro_gruppo): ?>
|
||||
<span class="badge badge-success ml-1">Incluso</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?php echo e(Str::limit($evento->descrizione_evento, 50) ?: '-'); ?></td>
|
||||
<td>
|
||||
<?php if($evento->tipo_evento): ?>
|
||||
<?php
|
||||
$tipologiaEvento = \App\Models\TipologiaEvento::where('nome', $evento->tipo_evento)->first();
|
||||
?>
|
||||
<?php if($tipologiaEvento): ?>
|
||||
<span class="badge badge-primary"><?php echo e($tipologiaEvento->descrizione ?: $tipologiaEvento->nome); ?></span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-secondary"><?php echo e($evento->tipo_evento); ?></span>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">-</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if($evento->tipo_recorrenza && $evento->tipo_recorrenza !== 'singolo'): ?>
|
||||
<span class="badge badge-info"><?php echo e($evento->periodicita_label); ?></span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-secondary">Singolo</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php $__empty_2 = true; $__currentLoopData = $evento->gruppi->take(2); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $gruppo): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_2 = false; ?>
|
||||
<a href="/gruppi/<?php echo e($gruppo->id); ?>">
|
||||
<span class="badge badge-warning"><?php echo e($gruppo->nome); ?></span>
|
||||
</a>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); if ($__empty_2): ?>
|
||||
<span class="text-muted">-</span>
|
||||
<?php endif; ?>
|
||||
<?php if($evento->gruppi->count() > 2): ?>
|
||||
<small class="text-muted">+<?php echo e($evento->gruppi->count() - 2); ?></small>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php $__empty_2 = true; $__currentLoopData = $evento->responsabili->take(2); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $resp): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_2 = false; ?>
|
||||
<small><?php echo e($resp->nome_completo); ?></small><br>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); if ($__empty_2): ?>
|
||||
<span class="text-muted">-</span>
|
||||
<?php endif; ?>
|
||||
<?php if($evento->responsabili->count() > 2): ?>
|
||||
<small class="text-muted">+<?php echo e($evento->responsabili->count() - 2); ?> altri</small>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><small class="text-muted"><?php echo e($evento->created_at->format('d/m/Y')); ?></small></td>
|
||||
<td>
|
||||
<a href="<?php echo e(url('/eventi/' . $evento->id)); ?>" class="btn btn-xs btn-info" title="Visualizza">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<?php if($canWriteEventi): ?>
|
||||
<a href="<?php echo e(url('/eventi/' . $evento->id . '/edit')); ?>" class="btn btn-xs btn-warning" title="Modifica">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<form action="<?php echo e(url('/eventi/' . $evento->id)); ?>" method="POST" class="d-inline">
|
||||
<?php echo csrf_field(); ?> <?php echo method_field('DELETE'); ?>
|
||||
<button type="submit" class="btn btn-xs btn-danger" onclick="return confirm('Confermi l\'eliminazione?')" title="Elimina">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?>
|
||||
<tr>
|
||||
<td colspan="9" class="text-center text-muted py-4">
|
||||
<i class="fas fa-calendar fa-2x mb-2"></i>
|
||||
<p class="mb-0">Nessun evento trovato</p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<?php echo e($eventi->withQueryString()->links()); ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog">
|
||||
<?php if($canDeleteEventi): ?>
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-danger">
|
||||
<h5 class="modal-title text-white"><i class="fas fa-trash-alt mr-2"></i>Conferma Eliminazione</h5>
|
||||
<button type="button" class="close text-white" data-dismiss="modal">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Stai per eliminare <strong id="delete-count">0</strong> eventi selezionati.</p>
|
||||
<p class="text-muted small">Verranno eliminati anche i collegamenti con gruppi e responsabili.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annulla</button>
|
||||
<button type="button" class="btn btn-danger" onclick="confirmDelete()">Elimina</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php $__env->stopSection(); ?>
|
||||
|
||||
<?php $__env->startSection('scripts'); ?>
|
||||
<script>
|
||||
function toggleSelectAll(source) {
|
||||
document.querySelectorAll('.row-checkbox').forEach(cb => cb.checked = source.checked);
|
||||
}
|
||||
|
||||
function getSelectedIds() {
|
||||
return Array.from(document.querySelectorAll('.row-checkbox:checked')).map(cb => cb.value);
|
||||
}
|
||||
|
||||
function deleteSelected() {
|
||||
const selectedIds = getSelectedIds();
|
||||
if (selectedIds.length === 0) {
|
||||
alert('Seleziona almeno un evento');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('delete-count').textContent = selectedIds.length;
|
||||
|
||||
document.querySelector('#deleteModal .btn-danger').onclick = function() {
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/eventi/mass-elimina';
|
||||
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '<?php echo e(csrf_token()); ?>';
|
||||
|
||||
let html = `<input type="hidden" name="_token" value="${csrfToken}">`;
|
||||
selectedIds.forEach(function(id) {
|
||||
html += `<input type="hidden" name="ids[]" value="${id}">`;
|
||||
});
|
||||
|
||||
form.innerHTML = html;
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
};
|
||||
|
||||
$('#deleteModal').modal('show');
|
||||
}
|
||||
|
||||
function exportSelectedIcs() {
|
||||
const selectedIds = getSelectedIds();
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/eventi/export-ics';
|
||||
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '<?php echo e(csrf_token()); ?>';
|
||||
|
||||
let html = `<input type="hidden" name="_token" value="${csrfToken}">`;
|
||||
|
||||
if (selectedIds.length > 0) {
|
||||
selectedIds.forEach(function(id) {
|
||||
html += `<input type="hidden" name="ids[]" value="${id}">`;
|
||||
});
|
||||
} else {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.forEach(function(value, key) {
|
||||
if (key === 'search') html += `<input type="hidden" name="search" value="${value}">`;
|
||||
if (key === 'gruppo_id') html += `<input type="hidden" name="gruppo_id" value="${value}">`;
|
||||
if (key === 'tipo') html += `<input type="hidden" name="tipo" value="${value}">`;
|
||||
});
|
||||
}
|
||||
|
||||
form.innerHTML = html;
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
</script>
|
||||
<?php $__env->stopSection(); ?>
|
||||
<?php echo $__env->make('layouts.adminlte', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH /var/www/html/glastree/resources/views/eventi/index.blade.php ENDPATH**/ ?>
|
||||
@@ -1,922 +0,0 @@
|
||||
<?php $__env->startSection('title', 'Individui'); ?>
|
||||
<?php $__env->startSection('page_title', 'Elenco Individui'); ?>
|
||||
|
||||
<?php
|
||||
$columnLabels = [
|
||||
'codice' => 'Codice',
|
||||
'cognome' => 'Cognome',
|
||||
'nome' => 'Nome',
|
||||
'email' => 'Email',
|
||||
'telefono' => 'Telefono',
|
||||
];
|
||||
$vistaDefaultJson = $vista ? $vista->toJson() : 'null';
|
||||
$allColumnsJson = json_encode($allColumns);
|
||||
$visibleColumnsJson = json_encode($visibleColumns);
|
||||
$canWriteIndividui = Auth::user()->canManage('individui');
|
||||
$canDeleteIndividui = Auth::user()->canDelete('individui');
|
||||
?>
|
||||
|
||||
<?php $__env->startSection('content'); ?>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-users mr-2"></i>Lista Individui</h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-info btn-sm mr-1" onclick="toggleSearchPanel()">
|
||||
<i class="fas fa-search mr-1"></i> Ricerca/Filtri
|
||||
</button>
|
||||
<div class="btn-group mr-1">
|
||||
<button type="button" class="btn btn-secondary btn-sm dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fas fa-cog mr-1"></i> Azioni
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#" onclick="exportCSV(); return false;">
|
||||
<i class="fas fa-file-csv mr-2"></i>Esporta Tutti CSV
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" onclick="exportSelectedCSV(); return false;">
|
||||
<i class="fas fa-file-csv mr-2"></i>Esporta Selezionati CSV
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="#" onclick="printSelected(); return false;">
|
||||
<i class="fas fa-print mr-2"></i>Stampa Selezionati
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" onclick="emailSelected(); return false;">
|
||||
<i class="fas fa-envelope mr-2"></i>Email Selezionati
|
||||
</a>
|
||||
<?php if($canWriteIndividui): ?>
|
||||
<a class="dropdown-item" href="#" onclick="showCreateMailingListModal(); return false;">
|
||||
<i class="fas fa-list mr-2"></i>Crea Mailing List
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger" href="#" onclick="deleteSelected(); return false;">
|
||||
<i class="fas fa-trash mr-2"></i>Elimina Selezionati
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="showSaveVistaModal()">
|
||||
<i class="fas fa-save mr-1"></i> Salva Vista
|
||||
</button>
|
||||
<?php if($canWriteIndividui): ?>
|
||||
<a href="<?php echo e(route('individui.import')); ?>" class="btn btn-warning btn-sm ml-2">
|
||||
<i class="fas fa-file-import mr-1"></i> Importa
|
||||
</a>
|
||||
<a href="/individui/create" class="btn btn-success btn-sm ml-2">
|
||||
<i class="fas fa-plus mr-1"></i> Nuovo
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="search-panel" class="p-3 bg-light border-bottom" style="display: none;">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="form-group mb-2">
|
||||
<label class="small font-weight-bold">Cerca</label>
|
||||
<input type="text" id="global-search" class="form-control form-control-sm" placeholder="Cerca in tutte le colonne..." oninput="applyGlobalSearch(this.value)">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group mb-2">
|
||||
<label class="small font-weight-bold">Colonna</label>
|
||||
<select id="filter-column" class="form-control form-control-sm">
|
||||
<option value="">Tutte le colonne</option>
|
||||
<?php $__currentLoopData = $allColumns; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $col): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<option value="<?php echo e($col['key']); ?>"><?php echo e($col['label']); ?></option>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="form-group mb-2">
|
||||
<label class="small font-weight-bold">Operatore</label>
|
||||
<select id="filter-operator" class="form-control form-control-sm">
|
||||
<option value="contains">Contiene</option>
|
||||
<option value="equals">Uguale a</option>
|
||||
<option value="starts">Inizia con</option>
|
||||
<option value="ends">Finisce con</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="form-group mb-2">
|
||||
<label class="small font-weight-bold">Valore</label>
|
||||
<input type="text" id="filter-value" class="form-control form-control-sm" placeholder="Valore filtro">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<label class="small"> </label>
|
||||
<div>
|
||||
<button type="button" class="btn btn-xs btn-primary" onclick="applyColumnFilter()">
|
||||
<i class="fas fa-filter"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-xs btn-secondary" onclick="clearFilters()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-info-circle mr-1"></i>
|
||||
Ordina cliccando sulle intestazioni delle colonne
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-0">
|
||||
<?php if($individui->count() > 0): ?>
|
||||
<table class="table table-bordered table-hover table-striped mb-0" id="individui-table">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th style="width: 40px;">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="select-all" onchange="toggleSelectAll(this)">
|
||||
<label class="custom-control-label" for="select-all"></label>
|
||||
</div>
|
||||
</th>
|
||||
<?php if(in_array('codice', $visibleColumns)): ?>
|
||||
<th style="width: 100px;" data-sortable="true" data-column="codice" onclick="sortTable('codice')">Codice <i class="fas fa-sort float-right"></i></th>
|
||||
<?php endif; ?>
|
||||
<?php if(in_array('cognome', $visibleColumns)): ?>
|
||||
<th data-sortable="true" data-column="cognome" onclick="sortTable('cognome')">Cognome <i class="fas fa-sort float-right"></i></th>
|
||||
<?php endif; ?>
|
||||
<?php if(in_array('nome', $visibleColumns)): ?>
|
||||
<th data-sortable="true" data-column="nome" onclick="sortTable('nome')">Nome <i class="fas fa-sort float-right"></i></th>
|
||||
<?php endif; ?>
|
||||
<?php if(in_array('email', $visibleColumns)): ?>
|
||||
<th data-sortable="true" data-column="email" onclick="sortTable('email')">Email <i class="fas fa-sort float-right"></i></th>
|
||||
<?php endif; ?>
|
||||
<?php if(in_array('telefono', $visibleColumns)): ?>
|
||||
<th data-sortable="true" data-column="telefono" onclick="sortTable('telefono')">Telefono <i class="fas fa-sort float-right"></i></th>
|
||||
<?php endif; ?>
|
||||
<th style="width: 120px;">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="table-body">
|
||||
<?php $__currentLoopData = $individui; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $ind): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<tr data-id="<?php echo e($ind->id); ?>">
|
||||
<td>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input row-checkbox" id="select-<?php echo e($ind->id); ?>" value="<?php echo e($ind->id); ?>">
|
||||
<label class="custom-control-label" for="select-<?php echo e($ind->id); ?>"></label>
|
||||
</div>
|
||||
</td>
|
||||
<?php if(in_array('codice', $visibleColumns)): ?>
|
||||
<td><span class="badge badge-secondary"><?php echo e($ind->codice_id); ?></span></td>
|
||||
<?php endif; ?>
|
||||
<?php if(in_array('cognome', $visibleColumns)): ?>
|
||||
<td class="font-weight-bold">
|
||||
<a href="<?php echo e(url('/individui/' . $ind->id . '/edit')); ?>"><?php echo e($ind->cognome); ?></a>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
<?php if(in_array('nome', $visibleColumns)): ?>
|
||||
<td>
|
||||
<a href="<?php echo e(url('/individui/' . $ind->id . '/edit')); ?>"><?php echo e($ind->nome); ?></a>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
<?php if(in_array('email', $visibleColumns)): ?>
|
||||
<td><?php echo e($ind->getEmailPrimariaAttribute() ?: '-'); ?></td>
|
||||
<?php endif; ?>
|
||||
<?php if(in_array('telefono', $visibleColumns)): ?>
|
||||
<td><?php echo e($ind->getTelefonoPrimarioAttribute() ?: '-'); ?></td>
|
||||
<?php endif; ?>
|
||||
<td>
|
||||
<?php if($canWriteIndividui): ?>
|
||||
<a href="<?php echo e(url('/individui/' . $ind->id . '/edit')); ?>" class="btn btn-xs btn-warning" title="Modifica">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if($canDeleteIndividui): ?>
|
||||
<button type="button" class="btn btn-xs btn-danger" onclick="showDeleteModal(<?php echo e($ind->id); ?>, '<?php echo e($ind->cognome); ?> <?php echo e($ind->nome); ?>')" title="Elimina">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<button type="button" class="btn btn-xs btn-secondary" onclick="printIndividuo(<?php echo e($ind->id); ?>)" title="Stampa">
|
||||
<i class="fas fa-print"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-xs btn-primary" onclick="exportIndividuoCSV(<?php echo e($ind->id); ?>)" title="Esporta CSV">
|
||||
<i class="fas fa-download"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-xs btn-info" onclick="sendEmail(<?php echo e($ind->id); ?>)" title="Invia Email">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-xs btn-dark" onclick="generateReport(<?php echo e($ind->id); ?>)" title="Genera Report">
|
||||
<i class="fas fa-chart-bar"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php else: ?>
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="fas fa-users fa-3x mb-3"></i>
|
||||
<p>Nessun individuo presente</p>
|
||||
<?php if($canWriteIndividui): ?>
|
||||
<a href="/individui/create" class="btn btn-success">Crea il primo individuo</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if($individui->count() > 0): ?>
|
||||
<div class="card-footer">
|
||||
<?php echo e($individui->links()); ?>
|
||||
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="saveVistaModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="fas fa-save mr-2"></i>Salva Vista</h5>
|
||||
<button type="button" class="close" data-dismiss="modal">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>Nome Vista *</label>
|
||||
<input type="text" id="vista-nome" class="form-control" placeholder="Es. Elenco soci attivi">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Colonne visibili</label>
|
||||
<div class="row" id="colonne-checkboxes">
|
||||
<?php $__currentLoopData = $allColumns; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $col): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<div class="col-md-6">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input col-visibile" id="col-vis-<?php echo e($col['key']); ?>" value="<?php echo e($col['key']); ?>" <?php echo e(in_array($col['key'], $visibleColumns) ? 'checked' : ''); ?>>
|
||||
<label class="custom-control-label" for="col-vis-<?php echo e($col['key']); ?>"><?php echo e($col['label']); ?></label>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="vista-default" value="true">
|
||||
<label class="custom-control-label" for="vista-default">Imposta come vista predefinita</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annulla</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveVista()">Salva</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="colonneModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="fas fa-columns mr-2"></i>Colonne Visibili</h5>
|
||||
<button type="button" class="close" data-dismiss="modal">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<?php $__currentLoopData = $allColumns; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $col): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<div class="col-md-6">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input col-toggle" id="col-toggle-<?php echo e($col['key']); ?>" value="<?php echo e($col['key']); ?>" <?php echo e(in_array($col['key'], $visibleColumns) ? 'checked' : ''); ?> onchange="toggleColumn('<?php echo e($col['key']); ?>', this.checked)">
|
||||
<label class="custom-control-label" for="col-toggle-<?php echo e($col['key']); ?>"><?php echo e($col['label']); ?></label>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Chiudi</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog">
|
||||
<?php if($canDeleteIndividui): ?>
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-danger">
|
||||
<h5 class="modal-title text-white"><i class="fas fa-trash-alt mr-2"></i>Conferma Eliminazione</h5>
|
||||
<button type="button" class="close text-white" data-dismiss="modal">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Stai per eliminare l'individuo: <strong id="delete-individuo-name"></strong></p>
|
||||
<p class="text-muted">Seleziona gli elementi collegati da eliminare:</p>
|
||||
<div class="ml-3">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="delete-contatti" checked>
|
||||
<label class="custom-control-label" for="delete-contatti">Contatti (<span id="count-contatti">0</span>)</label>
|
||||
</div>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="delete-gruppi">
|
||||
<label class="custom-control-label" for="delete-gruppi">Gruppi (<span id="count-gruppi">0</span>) - solo scollegamento</label>
|
||||
</div>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="delete-documenti" checked>
|
||||
<label class="custom-control-label" for="delete-documenti">Documenti (<span id="count-documenti">0</span>)</label>
|
||||
</div>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="delete-eventi">
|
||||
<label class="custom-control-label" for="delete-eventi">Eventi (<span id="count-eventi">0</span>) - solo scollegamento</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-warning mt-3 mb-0">
|
||||
<small><i class="fas fa-info-circle mr-1"></i>Se non selezionato, l'elemento verrà scollegato ma non eliminato.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annulla</button>
|
||||
<button type="button" class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete()">Elimina</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="createMailingListModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-info">
|
||||
<h5 class="modal-title text-white"><i class="fas fa-list mr-2"></i>Crea Mailing List</h5>
|
||||
<button type="button" class="close text-white" data-dismiss="modal">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>Nome Lista *</label>
|
||||
<input type="text" id="ml-nome" class="form-control" placeholder="Es. Newsletter Maggio 2026">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Descrizione</label>
|
||||
<textarea id="ml-descrizione" class="form-control" rows="2" placeholder="Descrizione opzionale"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Contatti inclusi (<span id="ml-count">0</span>)</label>
|
||||
<div class="table-responsive" style="max-height: 300px; overflow-y: auto;">
|
||||
<table class="table table-bordered table-sm" id="ml-contatti-table">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th style="width: 40px;">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="ml-select-all" onchange="toggleMlSelectAll(this)">
|
||||
<label class="custom-control-label" for="ml-select-all"></label>
|
||||
</div>
|
||||
</th>
|
||||
<th>Codice</th>
|
||||
<th>Cognome</th>
|
||||
<th>Nome</th>
|
||||
<th>Email</th>
|
||||
<th style="width: 50px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ml-contatti-tbody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annulla</button>
|
||||
<button type="button" class="btn btn-success" onclick="createMailingList()">
|
||||
<i class="fas fa-save mr-1"></i> Salva Lista
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php $__env->stopSection(); ?>
|
||||
|
||||
<?php $__env->startSection('scripts'); ?>
|
||||
<script>
|
||||
let currentSort = { column: null, direction: 'asc' };
|
||||
let columnVisibility = <?php echo json_encode(array_column($allColumns, 'key')); ?>;
|
||||
|
||||
function toggleSearchPanel() {
|
||||
const panel = document.getElementById('search-panel');
|
||||
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function applyGlobalSearch(value) {
|
||||
const rows = document.querySelectorAll('#table-body tr');
|
||||
const searchTerm = value.toLowerCase();
|
||||
rows.forEach(row => {
|
||||
const text = row.textContent.toLowerCase();
|
||||
row.style.display = text.includes(searchTerm) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function applyColumnFilter() {
|
||||
const col = document.getElementById('filter-column').value;
|
||||
const op = document.getElementById('filter-operator').value;
|
||||
const val = document.getElementById('filter-value').value.toLowerCase();
|
||||
|
||||
if (!col || !val) return;
|
||||
|
||||
const rows = document.querySelectorAll('#table-body tr');
|
||||
const colIndex = { codice: 1, cognome: 2, nome: 3, email: 4, telefono: 5, azioni: 6 }[col];
|
||||
|
||||
rows.forEach(row => {
|
||||
const cells = row.querySelectorAll('td');
|
||||
const cellText = cells[colIndex]?.textContent.toLowerCase() || '';
|
||||
let matches = false;
|
||||
|
||||
switch(op) {
|
||||
case 'contains': matches = cellText.includes(val); break;
|
||||
case 'equals': matches = cellText === val; break;
|
||||
case 'starts': matches = cellText.startsWith(val); break;
|
||||
case 'ends': matches = cellText.endsWith(val); break;
|
||||
}
|
||||
|
||||
row.style.display = matches ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
document.getElementById('global-search').value = '';
|
||||
document.getElementById('filter-column').value = '';
|
||||
document.getElementById('filter-value').value = '';
|
||||
document.querySelectorAll('#table-body tr').forEach(row => row.style.display = '');
|
||||
}
|
||||
|
||||
function sortTable(column) {
|
||||
if (currentSort.column === column) {
|
||||
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
currentSort = { column: column, direction: 'asc' };
|
||||
}
|
||||
|
||||
const colIndex = { codice: 0, cognome: 1, nome: 2, email: 3, telefono: 4, azioni: 5 }[column];
|
||||
const tbody = document.getElementById('table-body');
|
||||
const rows = Array.from(tbody.querySelectorAll('tr'));
|
||||
|
||||
rows.sort((a, b) => {
|
||||
const aVal = a.querySelectorAll('td')[colIndex]?.textContent.trim() || '';
|
||||
const bVal = b.querySelectorAll('td')[colIndex]?.textContent.trim() || '';
|
||||
const cmp = aVal.localeCompare(bVal, undefined, { numeric: true });
|
||||
return currentSort.direction === 'asc' ? cmp : -cmp;
|
||||
});
|
||||
|
||||
rows.forEach(row => tbody.appendChild(row));
|
||||
|
||||
document.querySelectorAll('th[data-sortable]').forEach(th => {
|
||||
const icon = th.querySelector('i');
|
||||
if (th.dataset.column === column) {
|
||||
icon.className = currentSort.direction === 'asc' ? 'fas fa-sort-up float-right' : 'fas fa-sort-down float-right';
|
||||
} else {
|
||||
icon.className = 'fas fa-sort float-right';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleColumn(column, visible) {
|
||||
const colIndex = { codice: 1, cognome: 2, nome: 3, email: 4, telefono: 5, azioni: 6 }[column];
|
||||
const ths = document.querySelectorAll('#individui-table thead th');
|
||||
const rows = document.querySelectorAll('#table-body tr');
|
||||
|
||||
if (visible) {
|
||||
ths[colIndex].style.display = '';
|
||||
rows.forEach(row => {
|
||||
row.querySelectorAll('td')[colIndex].style.display = '';
|
||||
});
|
||||
} else {
|
||||
ths[colIndex].style.display = 'none';
|
||||
rows.forEach(row => {
|
||||
row.querySelectorAll('td')[colIndex].style.display = 'none';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function saveVista() {
|
||||
let nome = document.getElementById('vista-nome').value.trim();
|
||||
if (!nome) {
|
||||
const now = new Date();
|
||||
nome = 'Vista ' + now.toLocaleDateString('it-IT') + ' ' + now.toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
const colonneVisibili = Array.from(document.querySelectorAll('#colonne-checkboxes input:checked')).map(i => i.value);
|
||||
const isDefault = document.getElementById('vista-default').checked;
|
||||
|
||||
const data = {
|
||||
nome: nome,
|
||||
tipo: 'individui',
|
||||
colonne_visibili: colonneVisibili,
|
||||
colonne_ordinamento: currentSort.column ? [[currentSort.column, currentSort.direction]] : [],
|
||||
ricerca: document.getElementById('global-search').value,
|
||||
is_default: isDefault
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/viste', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || ''
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Vista salvata!');
|
||||
$('#saveVistaModal').modal('hide');
|
||||
} else {
|
||||
alert('Errore salvataggio');
|
||||
}
|
||||
} catch(e) {
|
||||
alert('Errore: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSelectAll(source) {
|
||||
const checkboxes = document.querySelectorAll('.row-checkbox');
|
||||
checkboxes.forEach(cb => {
|
||||
cb.checked = source.checked;
|
||||
});
|
||||
}
|
||||
|
||||
function exportCSV() {
|
||||
window.location.href = '/individui/export';
|
||||
}
|
||||
|
||||
function exportSelectedCSV() {
|
||||
const selectedIds = getSelectedIds();
|
||||
if (selectedIds.length === 0) {
|
||||
alert('Seleziona almeno un individuo');
|
||||
return;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
selectedIds.forEach(function(id) {
|
||||
params.append('ids[]', id);
|
||||
});
|
||||
|
||||
window.location.href = '/individui/export?' + params.toString();
|
||||
}
|
||||
|
||||
let deleteIndividuoId = null;
|
||||
|
||||
async function showDeleteModal(id, nome) {
|
||||
deleteIndividuoId = id;
|
||||
document.getElementById('delete-individuo-name').textContent = nome;
|
||||
|
||||
document.getElementById('count-contatti').textContent = '...';
|
||||
document.getElementById('count-gruppi').textContent = '...';
|
||||
document.getElementById('count-documenti').textContent = '...';
|
||||
document.getElementById('count-eventi').textContent = '...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/individui/${id}/collegati`);
|
||||
const data = await response.json();
|
||||
document.getElementById('count-contatti').textContent = data.contatti;
|
||||
document.getElementById('count-gruppi').textContent = data.gruppi;
|
||||
document.getElementById('count-documenti').textContent = data.documenti;
|
||||
document.getElementById('count-eventi').textContent = data.eventi;
|
||||
} catch(e) {
|
||||
console.error('Errore recupero dati:', e);
|
||||
}
|
||||
|
||||
$('#deleteModal').modal('show');
|
||||
}
|
||||
|
||||
function confirmDelete() {
|
||||
const eliminaContatti = document.getElementById('delete-contatti').checked;
|
||||
const eliminaDocumenti = document.getElementById('delete-documenti').checked;
|
||||
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = `/individui/${deleteIndividuoId}/elimina`;
|
||||
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '<?php echo e(csrf_token()); ?>';
|
||||
form.innerHTML = `
|
||||
<input type="hidden" name="_token" value="${csrfToken}">
|
||||
<input type="hidden" name="elimina_contatti" value="${eliminaContatti}">
|
||||
<input type="hidden" name="elimina_documenti" value="${eliminaDocumenti}">
|
||||
`;
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
|
||||
function printIndividuo(id) {
|
||||
window.open('/individui/' + id + '/report', '_blank');
|
||||
}
|
||||
|
||||
function exportIndividuoCSV(id) {
|
||||
const row = document.querySelector(`tr[data-id="${id}"]`);
|
||||
if (!row) return;
|
||||
|
||||
const cells = Array.from(row.querySelectorAll('td'));
|
||||
const data = {
|
||||
codice: cells[1].textContent.trim(),
|
||||
cognome: cells[2].textContent.trim(),
|
||||
nome: cells[3].textContent.trim(),
|
||||
email: cells[4].textContent.trim(),
|
||||
telefono: cells[5].textContent.trim()
|
||||
};
|
||||
|
||||
const csv = `Codice,Cognome,Nome,Email,Telefono\n"${data.codice}","${data.cognome}","${data.nome}","${data.email}","${data.telefono}"`;
|
||||
const blob = new Blob([csv], { type: 'text/csv' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `individuo_${data.codice}.csv`;
|
||||
a.click();
|
||||
}
|
||||
|
||||
function sendEmail(id) {
|
||||
window.location.href = `/mailing/nuovo?individui=${id}`;
|
||||
}
|
||||
|
||||
function generateReport(id) {
|
||||
window.open('/individui/' + id + '/report', '_blank');
|
||||
}
|
||||
|
||||
function getSelectedIds() {
|
||||
return Array.from(document.querySelectorAll('.row-checkbox:checked')).map(cb => cb.value);
|
||||
}
|
||||
|
||||
function exportSelectedCSV() {
|
||||
const selectedIds = getSelectedIds();
|
||||
if (selectedIds.length === 0) {
|
||||
alert('Seleziona almeno un individuo');
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = [];
|
||||
rows.push('Codice,Cognome,Nome,Email,Telefono');
|
||||
|
||||
document.querySelectorAll('#table-body tr').forEach(row => {
|
||||
const checkbox = row.querySelector('.row-checkbox');
|
||||
if (checkbox && checkbox.checked) {
|
||||
const cells = Array.from(row.querySelectorAll('td'));
|
||||
const codice = cells[1].textContent.trim();
|
||||
const cognome = cells[2].textContent.trim();
|
||||
const nome = cells[3].textContent.trim();
|
||||
const email = cells[4].textContent.trim();
|
||||
const telefono = cells[5].textContent.trim();
|
||||
rows.push(`"${codice}","${cognome}","${nome}","${email}","${telefono}"`);
|
||||
}
|
||||
});
|
||||
|
||||
const blob = new Blob([rows.join('\n')], { type: 'text/csv' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `individui_selezionati_${new Date().toISOString().slice(0,10)}.csv`;
|
||||
a.click();
|
||||
}
|
||||
|
||||
function printSelected() {
|
||||
const selectedIds = getSelectedIds();
|
||||
if (selectedIds.length === 0) {
|
||||
alert('Seleziona almeno un individuo');
|
||||
return;
|
||||
}
|
||||
window.open(`/individui/report/stampa?ids=${selectedIds.join(',')}`, '_blank');
|
||||
}
|
||||
|
||||
function emailSelected() {
|
||||
const selectedIds = getSelectedIds();
|
||||
if (selectedIds.length === 0) {
|
||||
alert('Seleziona almeno un individuo');
|
||||
return;
|
||||
}
|
||||
window.location.href = `/mailing/nuovo?individui=${selectedIds.join(',')}`;
|
||||
}
|
||||
|
||||
function deleteSelected() {
|
||||
const selectedIds = getSelectedIds();
|
||||
if (selectedIds.length === 0) {
|
||||
alert('Seleziona almeno un individuo');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('delete-individuo-name').textContent = selectedIds.length + ' individui selezionati';
|
||||
document.getElementById('count-contatti').textContent = '...';
|
||||
document.getElementById('count-gruppi').textContent = '...';
|
||||
document.getElementById('count-documenti').textContent = '...';
|
||||
document.getElementById('count-eventi').textContent = '...';
|
||||
|
||||
document.getElementById('confirm-delete-btn').onclick = function() {
|
||||
const eliminaContatti = document.getElementById('delete-contatti').checked;
|
||||
const eliminaDocumenti = document.getElementById('delete-documenti').checked;
|
||||
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/individui/mass-elimina';
|
||||
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '<?php echo e(csrf_token()); ?>';
|
||||
|
||||
let html = `<input type="hidden" name="_token" value="${csrfToken}">`;
|
||||
html += `<input type="hidden" name="elimina_contatti" value="${eliminaContatti}">`;
|
||||
html += `<input type="hidden" name="elimina_documenti" value="${eliminaDocumenti}">`;
|
||||
|
||||
selectedIds.forEach(function(id) {
|
||||
html += `<input type="hidden" name="ids[]" value="${id}">`;
|
||||
});
|
||||
|
||||
form.innerHTML = html;
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
};
|
||||
|
||||
$('#deleteModal').modal('show');
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]');
|
||||
if (!csrfToken) {
|
||||
const meta = document.createElement('meta');
|
||||
meta.name = 'csrf-token';
|
||||
meta.content = '<?php echo e(csrf_token()); ?>';
|
||||
document.head.appendChild(meta);
|
||||
}
|
||||
|
||||
const visibleCols = <?php echo $visibleColumnsJson; ?>;
|
||||
const allCols = <?php echo $allColumnsJson; ?>;
|
||||
|
||||
allCols.forEach(function(col) {
|
||||
const visibile = visibleCols.includes(col.key);
|
||||
const saveCheckbox = document.querySelector(`#col-vis-${col.key}`);
|
||||
const toggleCheckbox = document.querySelector(`#col-toggle-${col.key}`);
|
||||
if (saveCheckbox) saveCheckbox.checked = visibile;
|
||||
if (toggleCheckbox) toggleCheckbox.checked = visibile;
|
||||
});
|
||||
|
||||
const vistaDefault = <?php echo $vistaDefaultJson; ?>;
|
||||
if (vistaDefault && vistaDefault.ricerca) {
|
||||
document.getElementById('global-search').value = vistaDefault.ricerca;
|
||||
applyGlobalSearch(vistaDefault.ricerca);
|
||||
}
|
||||
});
|
||||
|
||||
function showCreateMailingListModal() {
|
||||
document.getElementById('ml-nome').value = '';
|
||||
document.getElementById('ml-descrizione').value = '';
|
||||
document.getElementById('ml-contatti-tbody').innerHTML = '';
|
||||
document.getElementById('ml-count').textContent = '0';
|
||||
|
||||
const selectedIds = getSelectedIds();
|
||||
console.log('Selected IDs:', selectedIds);
|
||||
if (selectedIds.length > 0) {
|
||||
const url = '/individui/email-list?ids=' + selectedIds.join(',');
|
||||
console.log('Fetching URL:', url);
|
||||
fetch(url, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
console.log('Response status:', response.status);
|
||||
console.log('Response type:', response.headers.get('content-type'));
|
||||
if (!response.ok) {
|
||||
return response.text().then(text => {
|
||||
console.log('Full error response:', text.substring(0, 500));
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
const json = JSON.parse(text);
|
||||
throw new Error(json.message || 'Errore HTTP ' + response.status);
|
||||
} else {
|
||||
throw new Error('Server returned HTML (status ' + response.status + ') - probabilmente sessione scaduta. Ricarica la pagina.');
|
||||
}
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Data:', data);
|
||||
updateMlTable(data);
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error('Error:', err);
|
||||
alert('Errore caricamento contatti: ' + err.message + '\nURL: ' + url);
|
||||
});
|
||||
} else {
|
||||
alert('Seleziona almeno un individuo dalla tabella');
|
||||
}
|
||||
|
||||
$('#createMailingListModal').modal('show');
|
||||
}
|
||||
|
||||
function updateMlTable(data) {
|
||||
const tbody = document.getElementById('ml-contatti-tbody');
|
||||
const existingIds = new Set();
|
||||
|
||||
document.querySelectorAll('.ml-row-checkbox').forEach(function(cb) {
|
||||
existingIds.add(parseInt(cb.value));
|
||||
});
|
||||
|
||||
data.forEach(function(item) {
|
||||
if (existingIds.has(item.id)) return;
|
||||
|
||||
const tr = document.createElement('tr');
|
||||
const checked = item.email_count === 1 ? 'checked' : '';
|
||||
tr.innerHTML = `
|
||||
<td>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input ml-row-checkbox" id="ml-${item.id}" value="${item.id}" data-email="${item.email || ''}" ${checked}>
|
||||
<label class="custom-control-label" for="ml-${item.id}"></label>
|
||||
</div>
|
||||
</td>
|
||||
<td><span class="badge badge-secondary">${item.codice_id}</span></td>
|
||||
<td>${item.cognome}</td>
|
||||
<td>${item.nome}</td>
|
||||
<td>
|
||||
${item.email_count === 1 ? (item.email || '-') :
|
||||
'<select class="form-control form-control-sm" onchange="updateMlCheckboxEmail(' + item.id + ', this.value)">' +
|
||||
'<option value="">Seleziona email...</option>' +
|
||||
(item.emails || []).map(function(e) { return '<option value="' + e + '">' + e + '</option>'; }).join('') +
|
||||
'</select>'}
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-xs btn-danger" onclick="this.closest('tr').remove(); updateMlCount();">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
updateMlCount();
|
||||
}
|
||||
|
||||
function updateMlCount() {
|
||||
document.getElementById('ml-count').textContent = document.querySelectorAll('.ml-row-checkbox').length;
|
||||
}
|
||||
|
||||
function toggleMlSelectAll(source) {
|
||||
document.querySelectorAll('.ml-row-checkbox').forEach(function(cb) {
|
||||
cb.checked = source.checked;
|
||||
});
|
||||
}
|
||||
|
||||
function updateMlCheckboxEmail(id, email) {
|
||||
const checkbox = document.getElementById('ml-' + id);
|
||||
if (checkbox) {
|
||||
checkbox.dataset.email = email;
|
||||
checkbox.checked = email !== '';
|
||||
}
|
||||
}
|
||||
|
||||
async function createMailingList() {
|
||||
const nome = document.getElementById('ml-nome').value.trim();
|
||||
if (!nome) {
|
||||
alert('Inserisci un nome per la lista');
|
||||
return;
|
||||
}
|
||||
|
||||
const selected = [];
|
||||
document.querySelectorAll('.ml-row-checkbox:checked').forEach(function(cb) {
|
||||
if (cb.dataset.email) {
|
||||
selected.push({
|
||||
individuo_id: parseInt(cb.value),
|
||||
email: cb.dataset.email
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (selected.length === 0) {
|
||||
alert('Seleziona almeno un contatto con email');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
nome: nome,
|
||||
descrizione: document.getElementById('ml-descrizione').value.trim(),
|
||||
contatti: selected
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/mailing-liste/create-from-individui', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || ''
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Mailing list creata con successo!');
|
||||
$('#createMailingListModal').modal('hide');
|
||||
} else {
|
||||
alert('Errore nella creazione della lista');
|
||||
}
|
||||
} catch(e) {
|
||||
alert('Errore: ' + e.message);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<?php $__env->stopSection(); ?>
|
||||
<?php echo $__env->make('layouts.adminlte', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH /var/www/html/glastree/resources/views/individui/index.blade.php ENDPATH**/ ?>
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php $__env->startSection('title', 'Gestione Utenti'); ?>
|
||||
<?php $__env->startSection('page_title', 'Gestione Utenti'); ?>
|
||||
|
||||
<?php $__env->startSection('breadcrumbs'); ?>
|
||||
<li class="breadcrumb-item active">Admin</li>
|
||||
<li class="breadcrumb-item active">Utenti</li>
|
||||
<?php $__env->stopSection(); ?>
|
||||
|
||||
<?php $__env->startSection('content'); ?>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Utenti Registrati</h3>
|
||||
<a href="/admin/utenti/create" class="btn btn-success btn-sm float-right">
|
||||
<i class="fas fa-plus"></i> Nuovo Utente
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="GET" class="mb-3">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<input type="text" name="search" class="form-control" placeholder="Cerca nome o email..." value="<?php echo e(request('search')); ?>">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select name="status" class="form-control">
|
||||
<option value="">Tutti gli stati</option>
|
||||
<option value="active" <?php echo e(request('status') == 'active' ? 'selected' : ''); ?>>Attivo</option>
|
||||
<option value="suspended" <?php echo e(request('status') == 'suspended' ? 'selected' : ''); ?>>Sospeso</option>
|
||||
<option value="pending" <?php echo e(request('status') == 'pending' ? 'selected' : ''); ?>>In attesa</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-primary">Filtra</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nome</th>
|
||||
<th>Email</th>
|
||||
<th>Stato</th>
|
||||
<th>Permessi</th>
|
||||
<th>Creato</th>
|
||||
<th style="width: 150px;">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php $__currentLoopData = $utenti; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $utente): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<tr>
|
||||
<td>
|
||||
<strong><?php echo e($utente->name); ?></strong>
|
||||
<?php if($utente->isSuperAdmin()): ?>
|
||||
<span class="badge badge-danger ml-1">Admin</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?php echo e($utente->email); ?></td>
|
||||
<td>
|
||||
<?php switch($utente->status):
|
||||
case ('active'): ?>
|
||||
<span class="badge badge-success">Attivo</span>
|
||||
<?php break; ?>
|
||||
<?php case ('suspended'): ?>
|
||||
<span class="badge badge-secondary">Sospeso</span>
|
||||
<?php break; ?>
|
||||
<?php case ('pending'): ?>
|
||||
<span class="badge badge-warning">In attesa</span>
|
||||
<?php break; ?>
|
||||
<?php endswitch; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php $summary = $utente->getPermissionsSummary(); ?>
|
||||
<small>
|
||||
<?php $__currentLoopData = $summary; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $module => $level): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<?php if($level !== 'Nessuno'): ?>
|
||||
<span class="text-<?php echo e($level === 'Completo' ? 'success' : 'info'); ?>"><?php echo e(ucfirst($module)); ?>: <?php echo e($level); ?></span><br>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</small>
|
||||
</td>
|
||||
<td><?php echo e($utente->created_at->format('d/m/Y')); ?></td>
|
||||
<td>
|
||||
<a href="<?php echo e(url('/admin/utenti/' . $utente->id . '/edit')); ?>" class="btn btn-xs btn-warning">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<?php if($utente->id !== auth()->id()): ?>
|
||||
<form method="POST" action="<?php echo e(url('/admin/utenti/' . $utente->id)); ?>" style="display: inline;">
|
||||
<?php echo csrf_field(); ?> <?php echo method_field('DELETE'); ?>
|
||||
<button type="submit" class="btn btn-xs btn-danger" onclick="return confirm('Eliminare questo utente?')">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php echo e($utenti->links()); ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php $__env->stopSection(); ?>
|
||||
<?php echo $__env->make('admin.layout', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH /var/www/html/glastree/resources/views/admin/utenti/index.blade.php ENDPATH**/ ?>
|
||||
+11
@@ -6,9 +6,12 @@ $vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'App\\Console\\Commands\\BackupRunCommand' => $baseDir . '/app/Console/Commands/BackupRunCommand.php',
|
||||
'App\\Console\\Commands\\ExportHelpPdf' => $baseDir . '/app/Console/Commands/ExportHelpPdf.php',
|
||||
'App\\Console\\Commands\\SyncUserPermissions' => $baseDir . '/app/Console/Commands/SyncUserPermissions.php',
|
||||
'App\\Helpers\\ReportColumnRegistry' => $baseDir . '/app/Helpers/ReportColumnRegistry.php',
|
||||
'App\\Http\\Controllers\\Admin\\ActivityLogController' => $baseDir . '/app/Http/Controllers/Admin/ActivityLogController.php',
|
||||
'App\\Http\\Controllers\\Admin\\BackupController' => $baseDir . '/app/Http/Controllers/Admin/BackupController.php',
|
||||
'App\\Http\\Controllers\\Admin\\EmailSettingsController' => $baseDir . '/app/Http/Controllers/Admin/EmailSettingsController.php',
|
||||
'App\\Http\\Controllers\\Admin\\RuoloController' => $baseDir . '/app/Http/Controllers/Admin/RuoloController.php',
|
||||
'App\\Http\\Controllers\\Admin\\UtenteController' => $baseDir . '/app/Http/Controllers/Admin/UtenteController.php',
|
||||
@@ -25,12 +28,14 @@ return array(
|
||||
'App\\Http\\Controllers\\GruppoController' => $baseDir . '/app/Http/Controllers/GruppoController.php',
|
||||
'App\\Http\\Controllers\\GruppoIndividuoController' => $baseDir . '/app/Http/Controllers/GruppoIndividuoController.php',
|
||||
'App\\Http\\Controllers\\GruppoMembroController' => $baseDir . '/app/Http/Controllers/GruppoMembroController.php',
|
||||
'App\\Http\\Controllers\\HelpController' => $baseDir . '/app/Http/Controllers/HelpController.php',
|
||||
'App\\Http\\Controllers\\HomeController' => $baseDir . '/app/Http/Controllers/HomeController.php',
|
||||
'App\\Http\\Controllers\\ImpostazioniController' => $baseDir . '/app/Http/Controllers/ImpostazioniController.php',
|
||||
'App\\Http\\Controllers\\IndividuoController' => $baseDir . '/app/Http/Controllers/IndividuoController.php',
|
||||
'App\\Http\\Controllers\\MailingController' => $baseDir . '/app/Http/Controllers/MailingController.php',
|
||||
'App\\Http\\Controllers\\MailingListController' => $baseDir . '/app/Http/Controllers/MailingListController.php',
|
||||
'App\\Http\\Controllers\\ReportController' => $baseDir . '/app/Http/Controllers/ReportController.php',
|
||||
'App\\Http\\Controllers\\StorageRepositoryController' => $baseDir . '/app/Http/Controllers/StorageRepositoryController.php',
|
||||
'App\\Http\\Controllers\\VistaReportController' => $baseDir . '/app/Http/Controllers/VistaReportController.php',
|
||||
'App\\Http\\Middleware\\AdminOnly' => $baseDir . '/app/Http/Middleware/AdminOnly.php',
|
||||
'App\\Http\\Middleware\\CheckPermission' => $baseDir . '/app/Http/Middleware/CheckPermission.php',
|
||||
@@ -57,6 +62,7 @@ return array(
|
||||
'App\\Models\\RolePreset' => $baseDir . '/app/Models/RolePreset.php',
|
||||
'App\\Models\\Ruolo' => $baseDir . '/app/Models/Ruolo.php',
|
||||
'App\\Models\\SenderAccount' => $baseDir . '/app/Models/SenderAccount.php',
|
||||
'App\\Models\\StorageRepository' => $baseDir . '/app/Models/StorageRepository.php',
|
||||
'App\\Models\\Tenant' => $baseDir . '/app/Models/Tenant.php',
|
||||
'App\\Models\\TipologiaDocumento' => $baseDir . '/app/Models/TipologiaDocumento.php',
|
||||
'App\\Models\\TipologiaEvento' => $baseDir . '/app/Models/TipologiaEvento.php',
|
||||
@@ -64,7 +70,9 @@ return array(
|
||||
'App\\Models\\VistaReport' => $baseDir . '/app/Models/VistaReport.php',
|
||||
'App\\Providers\\AppServiceProvider' => $baseDir . '/app/Providers/AppServiceProvider.php',
|
||||
'App\\Providers\\AuthServiceProvider' => $baseDir . '/app/Providers/AuthServiceProvider.php',
|
||||
'App\\Services\\BackupService' => $baseDir . '/app/Services/BackupService.php',
|
||||
'App\\Services\\IcsExportService' => $baseDir . '/app/Services/IcsExportService.php',
|
||||
'App\\Services\\StorageRepositoryService' => $baseDir . '/app/Services/StorageRepositoryService.php',
|
||||
'As247\\CloudStorages\\Cache\\PathCache' => $vendorDir . '/as247/cloud-storages/src/Cache/PathCache.php',
|
||||
'As247\\CloudStorages\\Cache\\Stores\\ArrayStore' => $vendorDir . '/as247/cloud-storages/src/Cache/Stores/ArrayStore.php',
|
||||
'As247\\CloudStorages\\Cache\\Stores\\GoogleDrivePersistentStore' => $vendorDir . '/as247/cloud-storages/src/Cache/Stores/GoogleDrivePersistentStore.php',
|
||||
@@ -289,6 +297,7 @@ return array(
|
||||
'Database\\Seeders\\ComuniSeeder' => $baseDir . '/database/seeders/ComuniSeeder.php',
|
||||
'Database\\Seeders\\DatabaseSeeder' => $baseDir . '/database/seeders/DatabaseSeeder.php',
|
||||
'Database\\Seeders\\DiocesiSeeder' => $baseDir . '/database/seeders/DiocesiSeeder.php',
|
||||
'Database\\Seeders\\DockerBaseDataSeeder' => $baseDir . '/database/seeders/DockerBaseDataSeeder.php',
|
||||
'Database\\Seeders\\RolePresetSeeder' => $baseDir . '/database/seeders/RolePresetSeeder.php',
|
||||
'Database\\Seeders\\RoleSeeder' => $baseDir . '/database/seeders/RoleSeeder.php',
|
||||
'DeepCopy\\DeepCopy' => $vendorDir . '/myclabs/deep-copy/src/DeepCopy/DeepCopy.php',
|
||||
@@ -41213,8 +41222,10 @@ return array(
|
||||
'Termwind\\ValueObjects\\Styles' => $vendorDir . '/nunomaduro/termwind/src/ValueObjects/Styles.php',
|
||||
'Tests\\Feature\\CalendarEventsTest' => $baseDir . '/tests/Feature/CalendarEventsTest.php',
|
||||
'Tests\\Feature\\ExampleTest' => $baseDir . '/tests/Feature/ExampleTest.php',
|
||||
'Tests\\Feature\\StorageRepositoryTest' => $baseDir . '/tests/Feature/StorageRepositoryTest.php',
|
||||
'Tests\\TestCase' => $baseDir . '/tests/TestCase.php',
|
||||
'Tests\\Unit\\ExampleTest' => $baseDir . '/tests/Unit/ExampleTest.php',
|
||||
'Tests\\Unit\\StorageRepositoryServiceTest' => $baseDir . '/tests/Unit/StorageRepositoryServiceTest.php',
|
||||
'TheSeer\\Tokenizer\\Exception' => $vendorDir . '/theseer/tokenizer/src/Exception.php',
|
||||
'TheSeer\\Tokenizer\\NamespaceUri' => $vendorDir . '/theseer/tokenizer/src/NamespaceUri.php',
|
||||
'TheSeer\\Tokenizer\\NamespaceUriException' => $vendorDir . '/theseer/tokenizer/src/NamespaceUriException.php',
|
||||
|
||||
Vendored
+11
@@ -673,9 +673,12 @@ class ComposerStaticInit74199c0a3889e8d1256a25f93805ef56
|
||||
);
|
||||
|
||||
public static $classMap = array (
|
||||
'App\\Console\\Commands\\BackupRunCommand' => __DIR__ . '/../..' . '/app/Console/Commands/BackupRunCommand.php',
|
||||
'App\\Console\\Commands\\ExportHelpPdf' => __DIR__ . '/../..' . '/app/Console/Commands/ExportHelpPdf.php',
|
||||
'App\\Console\\Commands\\SyncUserPermissions' => __DIR__ . '/../..' . '/app/Console/Commands/SyncUserPermissions.php',
|
||||
'App\\Helpers\\ReportColumnRegistry' => __DIR__ . '/../..' . '/app/Helpers/ReportColumnRegistry.php',
|
||||
'App\\Http\\Controllers\\Admin\\ActivityLogController' => __DIR__ . '/../..' . '/app/Http/Controllers/Admin/ActivityLogController.php',
|
||||
'App\\Http\\Controllers\\Admin\\BackupController' => __DIR__ . '/../..' . '/app/Http/Controllers/Admin/BackupController.php',
|
||||
'App\\Http\\Controllers\\Admin\\EmailSettingsController' => __DIR__ . '/../..' . '/app/Http/Controllers/Admin/EmailSettingsController.php',
|
||||
'App\\Http\\Controllers\\Admin\\RuoloController' => __DIR__ . '/../..' . '/app/Http/Controllers/Admin/RuoloController.php',
|
||||
'App\\Http\\Controllers\\Admin\\UtenteController' => __DIR__ . '/../..' . '/app/Http/Controllers/Admin/UtenteController.php',
|
||||
@@ -692,12 +695,14 @@ class ComposerStaticInit74199c0a3889e8d1256a25f93805ef56
|
||||
'App\\Http\\Controllers\\GruppoController' => __DIR__ . '/../..' . '/app/Http/Controllers/GruppoController.php',
|
||||
'App\\Http\\Controllers\\GruppoIndividuoController' => __DIR__ . '/../..' . '/app/Http/Controllers/GruppoIndividuoController.php',
|
||||
'App\\Http\\Controllers\\GruppoMembroController' => __DIR__ . '/../..' . '/app/Http/Controllers/GruppoMembroController.php',
|
||||
'App\\Http\\Controllers\\HelpController' => __DIR__ . '/../..' . '/app/Http/Controllers/HelpController.php',
|
||||
'App\\Http\\Controllers\\HomeController' => __DIR__ . '/../..' . '/app/Http/Controllers/HomeController.php',
|
||||
'App\\Http\\Controllers\\ImpostazioniController' => __DIR__ . '/../..' . '/app/Http/Controllers/ImpostazioniController.php',
|
||||
'App\\Http\\Controllers\\IndividuoController' => __DIR__ . '/../..' . '/app/Http/Controllers/IndividuoController.php',
|
||||
'App\\Http\\Controllers\\MailingController' => __DIR__ . '/../..' . '/app/Http/Controllers/MailingController.php',
|
||||
'App\\Http\\Controllers\\MailingListController' => __DIR__ . '/../..' . '/app/Http/Controllers/MailingListController.php',
|
||||
'App\\Http\\Controllers\\ReportController' => __DIR__ . '/../..' . '/app/Http/Controllers/ReportController.php',
|
||||
'App\\Http\\Controllers\\StorageRepositoryController' => __DIR__ . '/../..' . '/app/Http/Controllers/StorageRepositoryController.php',
|
||||
'App\\Http\\Controllers\\VistaReportController' => __DIR__ . '/../..' . '/app/Http/Controllers/VistaReportController.php',
|
||||
'App\\Http\\Middleware\\AdminOnly' => __DIR__ . '/../..' . '/app/Http/Middleware/AdminOnly.php',
|
||||
'App\\Http\\Middleware\\CheckPermission' => __DIR__ . '/../..' . '/app/Http/Middleware/CheckPermission.php',
|
||||
@@ -724,6 +729,7 @@ class ComposerStaticInit74199c0a3889e8d1256a25f93805ef56
|
||||
'App\\Models\\RolePreset' => __DIR__ . '/../..' . '/app/Models/RolePreset.php',
|
||||
'App\\Models\\Ruolo' => __DIR__ . '/../..' . '/app/Models/Ruolo.php',
|
||||
'App\\Models\\SenderAccount' => __DIR__ . '/../..' . '/app/Models/SenderAccount.php',
|
||||
'App\\Models\\StorageRepository' => __DIR__ . '/../..' . '/app/Models/StorageRepository.php',
|
||||
'App\\Models\\Tenant' => __DIR__ . '/../..' . '/app/Models/Tenant.php',
|
||||
'App\\Models\\TipologiaDocumento' => __DIR__ . '/../..' . '/app/Models/TipologiaDocumento.php',
|
||||
'App\\Models\\TipologiaEvento' => __DIR__ . '/../..' . '/app/Models/TipologiaEvento.php',
|
||||
@@ -731,7 +737,9 @@ class ComposerStaticInit74199c0a3889e8d1256a25f93805ef56
|
||||
'App\\Models\\VistaReport' => __DIR__ . '/../..' . '/app/Models/VistaReport.php',
|
||||
'App\\Providers\\AppServiceProvider' => __DIR__ . '/../..' . '/app/Providers/AppServiceProvider.php',
|
||||
'App\\Providers\\AuthServiceProvider' => __DIR__ . '/../..' . '/app/Providers/AuthServiceProvider.php',
|
||||
'App\\Services\\BackupService' => __DIR__ . '/../..' . '/app/Services/BackupService.php',
|
||||
'App\\Services\\IcsExportService' => __DIR__ . '/../..' . '/app/Services/IcsExportService.php',
|
||||
'App\\Services\\StorageRepositoryService' => __DIR__ . '/../..' . '/app/Services/StorageRepositoryService.php',
|
||||
'As247\\CloudStorages\\Cache\\PathCache' => __DIR__ . '/..' . '/as247/cloud-storages/src/Cache/PathCache.php',
|
||||
'As247\\CloudStorages\\Cache\\Stores\\ArrayStore' => __DIR__ . '/..' . '/as247/cloud-storages/src/Cache/Stores/ArrayStore.php',
|
||||
'As247\\CloudStorages\\Cache\\Stores\\GoogleDrivePersistentStore' => __DIR__ . '/..' . '/as247/cloud-storages/src/Cache/Stores/GoogleDrivePersistentStore.php',
|
||||
@@ -956,6 +964,7 @@ class ComposerStaticInit74199c0a3889e8d1256a25f93805ef56
|
||||
'Database\\Seeders\\ComuniSeeder' => __DIR__ . '/../..' . '/database/seeders/ComuniSeeder.php',
|
||||
'Database\\Seeders\\DatabaseSeeder' => __DIR__ . '/../..' . '/database/seeders/DatabaseSeeder.php',
|
||||
'Database\\Seeders\\DiocesiSeeder' => __DIR__ . '/../..' . '/database/seeders/DiocesiSeeder.php',
|
||||
'Database\\Seeders\\DockerBaseDataSeeder' => __DIR__ . '/../..' . '/database/seeders/DockerBaseDataSeeder.php',
|
||||
'Database\\Seeders\\RolePresetSeeder' => __DIR__ . '/../..' . '/database/seeders/RolePresetSeeder.php',
|
||||
'Database\\Seeders\\RoleSeeder' => __DIR__ . '/../..' . '/database/seeders/RoleSeeder.php',
|
||||
'DeepCopy\\DeepCopy' => __DIR__ . '/..' . '/myclabs/deep-copy/src/DeepCopy/DeepCopy.php',
|
||||
@@ -41880,8 +41889,10 @@ class ComposerStaticInit74199c0a3889e8d1256a25f93805ef56
|
||||
'Termwind\\ValueObjects\\Styles' => __DIR__ . '/..' . '/nunomaduro/termwind/src/ValueObjects/Styles.php',
|
||||
'Tests\\Feature\\CalendarEventsTest' => __DIR__ . '/../..' . '/tests/Feature/CalendarEventsTest.php',
|
||||
'Tests\\Feature\\ExampleTest' => __DIR__ . '/../..' . '/tests/Feature/ExampleTest.php',
|
||||
'Tests\\Feature\\StorageRepositoryTest' => __DIR__ . '/../..' . '/tests/Feature/StorageRepositoryTest.php',
|
||||
'Tests\\TestCase' => __DIR__ . '/../..' . '/tests/TestCase.php',
|
||||
'Tests\\Unit\\ExampleTest' => __DIR__ . '/../..' . '/tests/Unit/ExampleTest.php',
|
||||
'Tests\\Unit\\StorageRepositoryServiceTest' => __DIR__ . '/../..' . '/tests/Unit/StorageRepositoryServiceTest.php',
|
||||
'TheSeer\\Tokenizer\\Exception' => __DIR__ . '/..' . '/theseer/tokenizer/src/Exception.php',
|
||||
'TheSeer\\Tokenizer\\NamespaceUri' => __DIR__ . '/..' . '/theseer/tokenizer/src/NamespaceUri.php',
|
||||
'TheSeer\\Tokenizer\\NamespaceUriException' => __DIR__ . '/..' . '/theseer/tokenizer/src/NamespaceUriException.php',
|
||||
|
||||
Vendored
+6
-6
@@ -5767,17 +5767,17 @@
|
||||
},
|
||||
{
|
||||
"name": "sabre/vobject",
|
||||
"version": "4.5.8",
|
||||
"version_normalized": "4.5.8.0",
|
||||
"version": "4.6.0",
|
||||
"version_normalized": "4.6.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sabre-io/vobject.git",
|
||||
"reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1"
|
||||
"reference": "9432544fc369851fb8202c5d91159b2e669f0c88"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sabre-io/vobject/zipball/d554eb24d64232922e1eab5896cc2f84b3b9ffb1",
|
||||
"reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1",
|
||||
"url": "https://api.github.com/repos/sabre-io/vobject/zipball/9432544fc369851fb8202c5d91159b2e669f0c88",
|
||||
"reference": "9432544fc369851fb8202c5d91159b2e669f0c88",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5794,7 +5794,7 @@
|
||||
"suggest": {
|
||||
"hoa/bench": "If you would like to run the benchmark scripts"
|
||||
},
|
||||
"time": "2026-01-12T10:45:19+00:00",
|
||||
"time": "2026-05-31T13:04:55+00:00",
|
||||
"bin": [
|
||||
"bin/vobject",
|
||||
"bin/generate_vcards"
|
||||
|
||||
Vendored
+5
-5
@@ -3,7 +3,7 @@
|
||||
'name' => 'laravel/laravel',
|
||||
'pretty_version' => 'dev-main',
|
||||
'version' => 'dev-main',
|
||||
'reference' => '3471befb1af62975088d115a46aa180e13652a0d',
|
||||
'reference' => 'aa4e582925ec155fa08dbeb9bf4e3a97b4751df1',
|
||||
'type' => 'project',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
@@ -481,7 +481,7 @@
|
||||
'laravel/laravel' => array(
|
||||
'pretty_version' => 'dev-main',
|
||||
'version' => 'dev-main',
|
||||
'reference' => '3471befb1af62975088d115a46aa180e13652a0d',
|
||||
'reference' => 'aa4e582925ec155fa08dbeb9bf4e3a97b4751df1',
|
||||
'type' => 'project',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
@@ -1010,9 +1010,9 @@
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'sabre/vobject' => array(
|
||||
'pretty_version' => '4.5.8',
|
||||
'version' => '4.5.8.0',
|
||||
'reference' => 'd554eb24d64232922e1eab5896cc2f84b3b9ffb1',
|
||||
'pretty_version' => '4.6.0',
|
||||
'version' => '4.6.0.0',
|
||||
'reference' => '9432544fc369851fb8202c5d91159b2e669f0c88',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../sabre/vobject',
|
||||
'aliases' => array(),
|
||||
|
||||
+28
@@ -430,6 +430,34 @@ class VCard extends VObject\Document
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a property with a specific TYPE value (ADR, TEL, or EMAIL).
|
||||
*
|
||||
* This function will return null if the exact property list does not exist.
|
||||
*
|
||||
* For example to get the property of `TEL;TYPE=HOME,CELL`
|
||||
* you would call `getByTypes('TEL', ['HOME', 'CELL'])`
|
||||
*
|
||||
* @param string[] $types
|
||||
*
|
||||
* @return \ArrayAccess|array|null
|
||||
*/
|
||||
public function getByTypes(string $propertyName, array $types)
|
||||
{
|
||||
$types = array_map('strtolower', $types);
|
||||
foreach ($this->select($propertyName) as $field) {
|
||||
if (isset($field['TYPE'])) {
|
||||
$parts = array_map('strtolower', $field['TYPE']->getParts());
|
||||
|
||||
if (!array_diff($types, $parts) && !array_diff($parts, $types)) {
|
||||
return $field;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method should return a list of default property values.
|
||||
*
|
||||
|
||||
+1
-1
@@ -685,7 +685,7 @@ class Broker
|
||||
// We only need to do that though, if the master event is not declined.
|
||||
if (isset($instances['master']) && 'DECLINED' !== $instances['master']['newstatus']) {
|
||||
foreach ($eventInfo['exdate'] as $exDate) {
|
||||
if (!in_array($exDate, $oldEventInfo['exdate'])) {
|
||||
if (!in_array($exDate, $oldEventInfo['exdate'] ?? [])) {
|
||||
if (isset($instances[$exDate])) {
|
||||
$instances[$exDate]['newstatus'] = 'DECLINED';
|
||||
} else {
|
||||
|
||||
@@ -18,11 +18,18 @@ class FindFromTimezoneMap implements TimezoneFinder
|
||||
'/^\((UTC|GMT)(\+|\-)[\d]{2}\.[\d]{2}\) (.*)/',
|
||||
];
|
||||
|
||||
/**
|
||||
* @throws void
|
||||
*/
|
||||
public function find(string $tzid, bool $failIfUncertain = false): ?DateTimeZone
|
||||
{
|
||||
// Next, we check if the tzid is somewhere in our tzid map.
|
||||
if ($this->hasTzInMap($tzid)) {
|
||||
return new DateTimeZone($this->getTzFromMap($tzid));
|
||||
try {
|
||||
return new DateTimeZone($this->getTzFromMap($tzid));
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Some Microsoft products prefix the offset first, so let's strip that off
|
||||
@@ -34,7 +41,11 @@ class FindFromTimezoneMap implements TimezoneFinder
|
||||
}
|
||||
$tzidAlternate = $matches[3];
|
||||
if ($this->hasTzInMap($tzidAlternate)) {
|
||||
return new DateTimeZone($this->getTzFromMap($tzidAlternate));
|
||||
try {
|
||||
return new DateTimeZone($this->getTzFromMap($tzidAlternate));
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -14,5 +14,5 @@ class Version
|
||||
/**
|
||||
* Full version number.
|
||||
*/
|
||||
public const VERSION = '4.5.8';
|
||||
public const VERSION = '4.6.0';
|
||||
}
|
||||
|
||||
+3
-3
@@ -40,12 +40,12 @@ return [
|
||||
'Kabul' => 'Asia/Kabul',
|
||||
'Ekaterinburg' => 'Asia/Yekaterinburg',
|
||||
'Islamabad, Karachi, Tashkent' => 'Asia/Karachi',
|
||||
'Kolkata, Chennai, Mumbai, New Delhi, India Standard Time' => 'Asia/Calcutta',
|
||||
'Kolkata, Chennai, Mumbai, New Delhi, India Standard Time' => 'Asia/Kolkata',
|
||||
'Kathmandu, Nepal' => 'Asia/Kathmandu',
|
||||
'Almaty, Novosibirsk, North Central Asia' => 'Asia/Almaty',
|
||||
'Astana, Dhaka' => 'Asia/Dhaka',
|
||||
'Sri Jayawardenepura, Sri Lanka' => 'Asia/Colombo',
|
||||
'Rangoon' => 'Asia/Rangoon',
|
||||
'Rangoon' => 'Asia/Yangon',
|
||||
'Bangkok, Hanoi, Jakarta' => 'Asia/Bangkok',
|
||||
'Krasnoyarsk' => 'Asia/Krasnoyarsk',
|
||||
'Beijing, Chongqing, Hong Kong SAR, Urumqi' => 'Asia/Shanghai',
|
||||
@@ -72,7 +72,7 @@ return [
|
||||
'Mid-Atlantic' => 'America/Noronha',
|
||||
'Brasilia' => 'America/Sao_Paulo', // Best guess
|
||||
'Buenos Aires' => 'America/Argentina/Buenos_Aires',
|
||||
'Greenland' => 'America/Godthab',
|
||||
'Greenland' => 'America/Nuuk',
|
||||
'Newfoundland' => 'America/St_Johns',
|
||||
'Atlantic Time (Canada)' => 'America/Halifax',
|
||||
'Caracas, La Paz' => 'America/Caracas',
|
||||
|
||||
+3
-3
@@ -34,7 +34,7 @@ return [
|
||||
'Newfoundland' => 'America/St_Johns',
|
||||
'Argentina' => 'America/Argentina/Buenos_Aires',
|
||||
'E. South America' => 'America/Belem',
|
||||
'Greenland' => 'America/Godthab',
|
||||
'Greenland' => 'America/Nuuk',
|
||||
'Montevideo' => 'America/Montevideo',
|
||||
'SA Eastern' => 'America/Belem',
|
||||
// 'Mid-Atlantic' => 'Etc/GMT-2', // conflict with windows timezones.
|
||||
@@ -71,12 +71,12 @@ return [
|
||||
'Ekaterinburg' => 'Asia/Yekaterinburg',
|
||||
'Pakistan' => 'Asia/Karachi',
|
||||
'West Asia' => 'Asia/Tashkent',
|
||||
'India' => 'Asia/Calcutta',
|
||||
'India' => 'Asia/Kolkata',
|
||||
'Sri Lanka' => 'Asia/Colombo',
|
||||
'Nepal' => 'Asia/Kathmandu',
|
||||
'Central Asia' => 'Asia/Dhaka',
|
||||
'N. Central Asia' => 'Asia/Almaty',
|
||||
'Myanmar' => 'Asia/Rangoon',
|
||||
'Myanmar' => 'Asia/Yangon',
|
||||
'North Asia' => 'Asia/Krasnoyarsk',
|
||||
'SE Asia' => 'Asia/Bangkok',
|
||||
'China' => 'Asia/Shanghai',
|
||||
|
||||
+7
-7
@@ -20,7 +20,7 @@ return [
|
||||
'Arab Standard Time' => 'Asia/Riyadh',
|
||||
'Arabian Standard Time' => 'Asia/Dubai',
|
||||
'Arabic Standard Time' => 'Asia/Baghdad',
|
||||
'Argentina Standard Time' => 'America/Buenos_Aires',
|
||||
'Argentina Standard Time' => 'America/Argentina/Buenos_Aires',
|
||||
'Astrakhan Standard Time' => 'Europe/Astrakhan',
|
||||
'Atlantic Standard Time' => 'America/Halifax',
|
||||
'Aus Central W. Standard Time' => 'Australia/Eucla',
|
||||
@@ -55,16 +55,16 @@ return [
|
||||
'Eastern Standard Time (Mexico)' => 'America/Cancun',
|
||||
'Egypt Standard Time' => 'Africa/Cairo',
|
||||
'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg',
|
||||
'FLE Standard Time' => 'Europe/Kiev',
|
||||
'FLE Standard Time' => 'Europe/Kyiv',
|
||||
'Fiji Standard Time' => 'Pacific/Fiji',
|
||||
'GMT Standard Time' => 'Europe/London',
|
||||
'GTB Standard Time' => 'Europe/Bucharest',
|
||||
'Georgian Standard Time' => 'Asia/Tbilisi',
|
||||
'Greenland Standard Time' => 'America/Godthab',
|
||||
'Greenland Standard Time' => 'America/Nuuk',
|
||||
'Greenwich Standard Time' => 'Atlantic/Reykjavik',
|
||||
'Haiti Standard Time' => 'America/Port-au-Prince',
|
||||
'Hawaiian Standard Time' => 'Pacific/Honolulu',
|
||||
'India Standard Time' => 'Asia/Calcutta',
|
||||
'India Standard Time' => 'Asia/Kolkata',
|
||||
'Iran Standard Time' => 'Asia/Tehran',
|
||||
'Israel Standard Time' => 'Asia/Jerusalem',
|
||||
'Jordan Standard Time' => 'Asia/Amman',
|
||||
@@ -82,10 +82,10 @@ return [
|
||||
'Morocco Standard Time' => 'Africa/Casablanca',
|
||||
'Mountain Standard Time' => 'America/Denver',
|
||||
'Mountain Standard Time (Mexico)' => 'America/Chihuahua',
|
||||
'Myanmar Standard Time' => 'Asia/Rangoon',
|
||||
'Myanmar Standard Time' => 'Asia/Yangon',
|
||||
'N. Central Asia Standard Time' => 'Asia/Novosibirsk',
|
||||
'Namibia Standard Time' => 'Africa/Windhoek',
|
||||
'Nepal Standard Time' => 'Asia/Katmandu',
|
||||
'Nepal Standard Time' => 'Asia/Kathmandu',
|
||||
'New Zealand Standard Time' => 'Pacific/Auckland',
|
||||
'Newfoundland Standard Time' => 'America/St_Johns',
|
||||
'Norfolk Standard Time' => 'Pacific/Norfolk',
|
||||
@@ -127,7 +127,7 @@ return [
|
||||
'Transbaikal Standard Time' => 'Asia/Chita',
|
||||
'Turkey Standard Time' => 'Europe/Istanbul',
|
||||
'Turks And Caicos Standard Time' => 'America/Grand_Turk',
|
||||
'US Eastern Standard Time' => 'America/Indianapolis',
|
||||
'US Eastern Standard Time' => 'America/Indiana/Indianapolis',
|
||||
'US Mountain Standard Time' => 'America/Phoenix',
|
||||
'UTC' => 'Etc/GMT',
|
||||
'UTC+12' => 'Etc/GMT-12',
|
||||
|
||||
Reference in New Issue
Block a user