aggiunta calendario
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
# 🧠 MEMORY.md — Stato Progetto
|
||||
|
||||
## Goal
|
||||
- Correggere tutti i bug critici identificati nell'applicazione Glastree, a partire dal bug segnalato "rimuovendo un individuo da un gruppo, l'individuo perdeva tutti i contatti"
|
||||
- Correggere tutti i bug critici identificati nell'applicazione Glastree
|
||||
- Implementare nuove funzionalità richieste: cancellazione account email, fix firma editor, sincronizzazione calendario con Google Calendar / CalDAV
|
||||
|
||||
## Constraints & Preferences
|
||||
- Laravel 13, PHP 8.4, AdminLTE 4, MySQL/MariaDB
|
||||
@@ -11,23 +12,25 @@
|
||||
## Progress
|
||||
### Done
|
||||
- **Cancellata cache Laravel** (`php artisan optimize:clear`)
|
||||
- **Fix BUG CRITICO 3** – `IndividuoController::update()` (riga 216): avvolto `$individuo->contatti()->delete()` dentro `if ($request->has('contatti'))`. Se il request non contiene il campo `contatti` (es. submit da form annidato), i contatti NON vengono eliminati.
|
||||
- **Fix BUG CRITICO 1** – `individui/edit.blade.php`: convertiti 4 form annidati in pulsanti `type="button"` + `fetch()` AJAX:
|
||||
- Linea 235-240: rimozione gruppo → `deleteGruppoFromEdit()`
|
||||
- Linea 245-267: modifica gruppo inline → `submitEditGruppo()`
|
||||
- Linea 291-319: upload documento → `submitUploadDocumentoIndividuo()`
|
||||
- Linea 350-355: eliminazione documento → `deleteDocumentoFromEdit()`
|
||||
- **Fix BUG CRITICO 2** – `gruppi/edit.blade.php`: convertiti 3 form annidati in pulsanti `type="button"` + `fetch()` AJAX:
|
||||
- Linea 155-180: modifica membro inline → `submitEditMembro()`
|
||||
- Linea 204-231: upload documento → `submitUploadDocumentoGruppo()`
|
||||
- Linea 261-266: eliminazione documento → `deleteDocumentoFromEdit()`
|
||||
- **Fix BUG CRITICO 4** – `gruppi/show.blade.php`: spostati i flash messages (session success/error) DENTRO `@section('content')` invece di essere fuori (non venivano mai visualizzati)
|
||||
- **Fix BUG CRITICO 5** – `eventi/show.blade.php`: rimossi 2 tag `</div>` extra alle linee 214-217 che rompevano la struttura HTML
|
||||
- **Fix BUG CRITICO 6** – `eventi/show.blade.php`: escape JS di `$evento->nome_evento` nella `confirm()` (riga 307) per prevenire stored XSS
|
||||
- **Fix BUG CRITICO 7** – `ImpostazioniController.php`: aggiunto `use App\Models\Documento;` mancante (causava Fatal error in `saveAppSettings()` e `migrateStoragePath()`)
|
||||
- **Fix BUG CRITICO 8** – `ImpostazioniController.php`: sostituito `JSON_CONTAINS()` (non supportato da SQLite) con iterazione PHP su `json_decode()` — compatibile MySQL e SQLite
|
||||
- **Fix BUG CRITICO 9** – `ReportController.php`: corretto `$contatto->email` → `$contatto->valore` (riga 605, colonna `email` non esiste)
|
||||
- **Fix BUG CRITICO 10** – `DocumentoController.php`: prevenuta morph injection in `massUpdate()` sostituendo il fallback `?? $data['visibilita_target_type']` con un `isset()` check sul typeMap + `return back()->with('error')` se il tipo non è valido
|
||||
- **Fix BUG CRITICO 3** – `IndividuoController::update()` (riga 216): avvolto `$individuo->contatti()->delete()` dentro `if ($request->has('contatti'))`.
|
||||
- **Fix BUG CRITICO 1** – `individui/edit.blade.php`: convertiti 4 form annidati in pulsanti `type="button"` + `fetch()` AJAX
|
||||
- **Fix BUG CRITICO 2** – `gruppi/edit.blade.php`: convertiti 3 form annidati in pulsanti `type="button"` + `fetch()` AJAX
|
||||
- **Fix BUG CRITICO 4** – `gruppi/show.blade.php`: flash messages spostati dentro `@section('content')`
|
||||
- **Fix BUG CRITICO 5/6** – `eventi/show.blade.php`: rimossi div extra, XSS escape in confirm()
|
||||
- **Fix BUG CRITICO 7/8** – `ImpostazioniController.php`: use Documento + JSON_CONTAINS → PHP
|
||||
- **Fix BUG CRITICO 9** – `ReportController.php`: `$contatto->email` → `$contatto->valore`
|
||||
- **Fix BUG CRITICO 10** – `DocumentoController.php`: morph injection prevention in massUpdate()
|
||||
- **NUOVA FEATURE – Cancellazione account email**: Aggiunto metodo `destroy()` in `EmailSettingsController`, rotta `DELETE /impostazioni/email`, pulsante "Elimina Configurazione" in entrambe le viste (admin dedicata e unificata)
|
||||
- **FIX – Doppia barra icone firma**: Cambiata la guardia di `createEditor()` e `initSignatureEditor()` da `container.querySelector('.ql-toolbar')` (che non funziona perché Quill crea la toolbar come sibling, non child) a flag `quillEditorInstance` in entrambe le viste
|
||||
- **NUOVA FEATURE – Sincronizzazione calendario**: Implementata sincronizzazione con Google Calendar e CalDAV (Infomaniak/NextCloud):
|
||||
- Migration `create_calendario_connessioni_table` + `add_uid_esterno_to_eventi_table`
|
||||
- Model `CalendarioConnessione` con encrypt/decrypt config
|
||||
- Service `CalDavService` (PROPFIND, REPORT, calendar-query, PUT)
|
||||
- Service `GoogleCalendarSyncService` (Google Calendar API v3)
|
||||
- Controller `CalendarioConnessioneController` (CRUD + test + sync + reorder)
|
||||
- Routes RESTful per calendario-connessioni
|
||||
- UI nella tab "Calendario" delle impostazioni unificate con tabella, modale CRUD, pulsanti test/sync/elimina, Sortable drag&drop
|
||||
- Installati pacchetti: `sabre/vobject`, `google/apiclient`
|
||||
|
||||
### In Progress
|
||||
- *(nessuno)*
|
||||
@@ -36,25 +39,32 @@
|
||||
- *(nessuno)*
|
||||
|
||||
## Key Decisions
|
||||
- Per i form annidati si è scelto l'approccio `type="button"` + `fetch()` AJAX, già utilizzato in `individui/show.blade.php` e `gruppi/edit.blade.php` per funzioni esistenti (`deleteMembro`, `submitGruppoForm`, `uploadAvatar`, etc.)
|
||||
- Per documenti (upload/delete) il controller torna `redirect()`, non JSON. Le funzioni AJAX eseguono `window.location.reload()` dopo il fetch per aggiornare la vista.
|
||||
- `JSON_CONTAINS` sostituito con approccio PHP+json_decode per garantire compatibilità SQLite senza modificare lo schema DB.
|
||||
- `$request->has('contatti')` è la guardia corretta: se il form principale non invia il campo `contatti` (perché il submit proviene da un form annidato), non si cancella nulla.
|
||||
- Per i form annidati si è scelto l'approccio `type="button"` + `fetch()` AJAX
|
||||
- Per la cancellazione account email si usa `EmailSetting::first()->delete()` con verifica esistenza
|
||||
- Per il fix Quill: variabile flag `quillEditorInstance` invece di `container.querySelector('.ql-toolbar')` perché Quill inserisce `.ql-toolbar` come elemento sibling, non child
|
||||
- Per CalDAV: usato Guzzle (già in Laravel) per HTTP + `sabre/vobject` per parsing iCal
|
||||
- Per Google Calendar: usato `google/apiclient` con OAuth 2.0 (refresh token)
|
||||
- La sync può essere bidirezionale, solo import o solo export
|
||||
- I campi sensibili delle connessioni calendario sono criptati con `Crypt`
|
||||
|
||||
## Next Steps
|
||||
- *(nessuno)* - Tutti i bug critici risolti
|
||||
- *(nessuno)*
|
||||
|
||||
## Critical Context
|
||||
- **CAUSA DEL BUG SEGNALATO**: I form annidati in `individui/edit.blade.php` facevano sì che il browser ignorasse i form interni. I pulsanti "Rimuovi" inviavano il form principale → `IndividuoController::update()` → `$individuo->contatti()->delete()` incondizionato.
|
||||
- **Tutti i 10 bug critici sono stati risolti**
|
||||
- I controller di rimozione membro (`GruppoMembroController::destroy()` e `GruppoIndividuoController::destroy()`) usano già `detach()` correttamente — nessun problema lato backend
|
||||
- Le connessioni calendario supportano: Google Calendar, CalDAV (NextCloud, Infomaniak, Synology)
|
||||
- La direzione di sync è configurabile per ogni connessione
|
||||
- Le password/refresh token sono automaticamente criptati all salvataggio via `encryptAndSetConfig()`
|
||||
- Il campo `uid_esterno` sugli eventi traccia la corrispondenza con eventi remoti
|
||||
|
||||
## Relevant Files
|
||||
- `app/Http/Controllers/IndividuoController.php`: Fix BUG 3 – contatti condizionale
|
||||
- `app/Http/Controllers/ImpostazioniController.php`: Fix BUG 7 (use Documento) + BUG 8 (JSON_CONTAINS → PHP)
|
||||
- `app/Http/Controllers/ReportController.php`: Fix BUG 9 (email → valore)
|
||||
- `app/Http/Controllers/DocumentoController.php`: Fix BUG 10 (morph injection prevention)
|
||||
- `resources/views/individui/edit.blade.php`: Fix BUG 1 (4 nested forms → AJAX)
|
||||
- `resources/views/gruppi/edit.blade.php`: Fix BUG 2 (3 nested forms → AJAX)
|
||||
- `resources/views/gruppi/show.blade.php`: Fix BUG 4 (flash inside @section)
|
||||
- `resources/views/eventi/show.blade.php`: Fix BUG 5 (extra divs) + BUG 6 (XSS escape)
|
||||
- `app/Http/Controllers/Admin/EmailSettingsController.php`: +destroy() method
|
||||
- `app/Http/Controllers/CalendarioConnessioneController.php`: NEW – CRUD + test + sync
|
||||
- `app/Models/CalendarioConnessione.php`: NEW – model con encrypt config
|
||||
- `app/Services/CalDavService.php`: NEW – CalDAV sync
|
||||
- `app/Services/GoogleCalendarSyncService.php`: NEW – Google Calendar sync
|
||||
- `app/Http/Controllers/ImpostazioniController.php`: +calendarioConnessioni in index()
|
||||
- `resources/views/admin/email-settings/index.blade.php`: +delete button, fix Quill guard
|
||||
- `resources/views/impostazioni/index.blade.php`: +delete button, fix Quill guard, +calendario tab UI, +calendario modal, +JS funcs
|
||||
- `routes/web.php`: +email destroy route, +calendario-connessioni routes
|
||||
- `database/migrations/2026_06_02_000001_create_calendario_connessioni_table.php`: NEW
|
||||
- `database/migrations/2026_06_02_000002_add_uid_esterno_to_eventi_table.php`: NEW
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -257,6 +257,18 @@ class EmailSettingsController extends Controller
|
||||
return back()->with('success', 'Mittente eliminato.');
|
||||
}
|
||||
|
||||
public function destroy(): RedirectResponse
|
||||
{
|
||||
$this->authorizeDelete('settings');
|
||||
|
||||
$settings = EmailSetting::first();
|
||||
if ($settings) {
|
||||
$settings->delete();
|
||||
}
|
||||
|
||||
return redirect('/impostazioni/email')->with('success', 'Configurazione email eliminata. Tutti i dati sono stati rimossi.');
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\AppSetting;
|
||||
use App\Models\CalendarioConnessione;
|
||||
use App\Models\Documento;
|
||||
use App\Models\EmailSetting;
|
||||
use App\Models\Ruolo;
|
||||
@@ -30,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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
@@ -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">
|
||||
@@ -95,3 +100,58 @@
|
||||
</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>
|
||||
@@ -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');
|
||||
|
||||
@@ -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',
|
||||
|
||||
+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');
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
<?php $__env->startSection('title', 'Dashboard'); ?>
|
||||
<?php $__env->startSection('page_title', 'Dashboard'); ?>
|
||||
|
||||
<?php $__env->startSection('content'); ?>
|
||||
<div class="row">
|
||||
<div class="col-lg-3 col-6">
|
||||
<div class="small-box bg-info">
|
||||
<div class="inner">
|
||||
<h3><?php echo e($stats['individui']); ?></h3>
|
||||
<p>Individui</p>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<i class="fas fa-users"></i>
|
||||
</div>
|
||||
<a href="<?php echo e(route('individui.index')); ?>" class="small-box-footer">Visualizza <i class="fas fa-arrow-circle-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-6">
|
||||
<div class="small-box bg-success">
|
||||
<div class="inner">
|
||||
<h3><?php echo e($stats['gruppi']); ?></h3>
|
||||
<p>Gruppi</p>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<i class="fas fa-folder"></i>
|
||||
</div>
|
||||
<a href="<?php echo e(route('gruppi.index')); ?>" class="small-box-footer">Visualizza <i class="fas fa-arrow-circle-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-6">
|
||||
<div class="small-box bg-primary">
|
||||
<div class="inner">
|
||||
<h3><?php echo e($stats['documenti']); ?></h3>
|
||||
<p>Documenti</p>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<i class="fas fa-file"></i>
|
||||
</div>
|
||||
<a href="/documenti" class="small-box-footer">Visualizza <i class="fas fa-arrow-circle-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-6">
|
||||
<div class="small-box bg-danger">
|
||||
<div class="inner">
|
||||
<h3><?php echo e($stats['eventi']); ?></h3>
|
||||
<p>Eventi</p>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<i class="fas fa-calendar"></i>
|
||||
</div>
|
||||
<a href="/eventi" class="small-box-footer">Visualizza <i class="fas fa-arrow-circle-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-3 col-6">
|
||||
<div class="small-box bg-warning">
|
||||
<div class="inner">
|
||||
<h3><?php echo e($stats['notifiche']); ?></h3>
|
||||
<p>Notifiche</p>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<i class="fas fa-bell"></i>
|
||||
</div>
|
||||
<a href="#" class="small-box-footer">Visualizza <i class="fas fa-arrow-circle-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-6">
|
||||
<div class="small-box bg-secondary">
|
||||
<div class="inner">
|
||||
<h3><?php echo e($stats['email_nuove']); ?></h3>
|
||||
<p>Nuove Email</p>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</div>
|
||||
<a href="/email" class="small-box-footer">Visualizza <i class="fas fa-arrow-circle-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-6">
|
||||
<div class="small-box bg-dark">
|
||||
<div class="inner">
|
||||
<h3><?php echo e($stats['mailing_liste']); ?></h3>
|
||||
<p>Mailing List</p>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<i class="fas fa-list-alt"></i>
|
||||
</div>
|
||||
<a href="/mailing-liste" class="small-box-footer">Visualizza <i class="fas fa-arrow-circle-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<?php if(Auth::user()->canManage('settings')): ?>
|
||||
<div class="col-lg-3 col-6">
|
||||
<div class="small-box bg-navy">
|
||||
<div class="inner">
|
||||
<h3><?php echo e($stats['backups']); ?></h3>
|
||||
<p>Backup</p>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<i class="fas fa-hdd"></i>
|
||||
</div>
|
||||
<a href="<?php echo e(route('admin.backup.index')); ?>" class="small-box-footer">Visualizza <i class="fas fa-arrow-circle-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-bell"></i> Ultime Notifiche</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if($notifiche->count() > 0): ?>
|
||||
<ul class="todo-list" data-widget="todo-list">
|
||||
<?php $__currentLoopData = $notifiche; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $notifica): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<li>
|
||||
<span class="text"><?php echo e($notifica->titolo); ?></span>
|
||||
<small class="badge badge-info"><i class="far fa-clock"></i> <?php echo e($notifica->created_at->diffForHumans()); ?></small>
|
||||
</li>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</ul>
|
||||
<?php else: ?>
|
||||
<p class="text-muted">Nessuna notifica</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-info-circle"></i> Benvenuto</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php
|
||||
$appName = \App\Models\AppSetting::getAppName() ?? 'Glastree';
|
||||
$orgName = \App\Models\AppSetting::getOrgName();
|
||||
$welcome = \App\Models\AppSetting::getDashboardWelcome();
|
||||
?>
|
||||
<?php if($welcome): ?>
|
||||
<p><?php echo nl2br(e($welcome)); ?></p>
|
||||
<?php else: ?>
|
||||
<p>Benvenuto in <strong><?php echo e(e($appName)); ?></strong><?php echo e($orgName ? ', ' . e($orgName) : ''); ?>.</p>
|
||||
<?php endif; ?>
|
||||
<p>Dal menu laterale puoi navigare tra le sezioni:</p>
|
||||
<ul>
|
||||
<li><strong>Individui</strong>: Gestione delle persone registrate</li>
|
||||
<li><strong>Gruppi</strong>: Gestione della struttura organizzativa</li>
|
||||
<li><strong>Documenti</strong>: Archivio documentale</li>
|
||||
<li><strong>Eventi</strong>: Calendario eventi</li>
|
||||
<li><strong>Mailing</strong>: Comunicazioni email</li>
|
||||
</ul>
|
||||
<?php if(Auth::user()->is_admin): ?>
|
||||
<div class="alert alert-success mt-3">
|
||||
<i class="fas fa-crown"></i> <strong>Amministratore</strong>: Hai accesso completo al sistema.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?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/home/dashboard.blade.php ENDPATH**/ ?>
|
||||
+2315
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,301 @@
|
||||
<?php
|
||||
$appLogoSmall = \App\Models\AppSetting::getLogoSmallUrl();
|
||||
$appName = \App\Models\AppSetting::getAppName() ?? 'Glastree';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="<?php echo e(csrf_token()); ?>">
|
||||
<title><?php echo $__env->yieldContent('title', 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">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400;600;700&display=swap">
|
||||
<?php echo $__env->yieldContent('styles'); ?>
|
||||
</head>
|
||||
<body class="hold-transition sidebar-mini layout-fixed">
|
||||
<div class="wrapper">
|
||||
<nav class="main-header navbar navbar-expand navbar-white navbar-light">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-widget="pushmenu" href="#" role="button"><i class="fas fa-bars"></i></a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav ml-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="<?php echo e(route('help')); ?>" title="Guida">
|
||||
<i class="fas fa-question-circle"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link" data-toggle="dropdown" href="#">
|
||||
<i class="far fa-bell"></i>
|
||||
<?php $unread = Auth::user()->unreadNotificheCount() ?? 0 ?>
|
||||
<?php if($unread > 0): ?>
|
||||
<span class="badge badge-warning navbar-badge"><?php echo e($unread); ?></span>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<span class="dropdown-header">Notifiche (<?php echo e($unread); ?>)</span>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a href="#" class="dropdown-item">Visualizza tutte</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link" data-toggle="dropdown" href="#">
|
||||
<i class="fas fa-user"></i> <?php echo e(Auth::user()->name); ?>
|
||||
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a href="<?php echo e(route('profile')); ?>" class="dropdown-item">
|
||||
<i class="fas fa-user-cog mr-2"></i> Profilo
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<form method="POST" action="<?php echo e(route('logout')); ?>">
|
||||
<?php echo csrf_field(); ?>
|
||||
<button type="submit" class="dropdown-item">
|
||||
<i class="fas fa-sign-out-alt mr-2"></i> Esci
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<?php
|
||||
$appLogoSmall = \App\Models\AppSetting::getLogoSmallUrl();
|
||||
$appName = \App\Models\AppSetting::getAppName() ?? 'Glastree';
|
||||
?>
|
||||
<aside class="main-sidebar sidebar-dark-primary elevation-4">
|
||||
<a href="<?php echo e(route('dashboard')); ?>" class="brand-link">
|
||||
<?php if($appLogoSmall): ?>
|
||||
<img src="<?php echo e($appLogoSmall); ?>" alt="Logo" class="brand-image" style="max-height: 35px; max-width: 40px;">
|
||||
<?php else: ?>
|
||||
<i class="fas fa-tree brand-image ml-3 mr-2"></i>
|
||||
<?php endif; ?>
|
||||
<span class="brand-text font-weight-light"><?php echo e($appName); ?></span>
|
||||
</a>
|
||||
|
||||
<div class="sidebar">
|
||||
<nav class="mt-2">
|
||||
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu">
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('dashboard')); ?>" class="nav-link <?php echo e(request()->routeIs('dashboard') ? 'active' : ''); ?>">
|
||||
<i class="nav-icon fas fa-tachometer-alt"></i>
|
||||
<p>Dashboard</p>
|
||||
</a>
|
||||
</li>
|
||||
<?php if(Auth::user()->canAccess('individui')): ?>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('individui.index')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-users"></i>
|
||||
<p>Individui</p>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if(Auth::user()->canAccess('gruppi')): ?>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('gruppi.index')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-folder"></i>
|
||||
<p>Gruppi</p>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if(Auth::user()->canAccess('documenti')): ?>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('documenti.index')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-file"></i>
|
||||
<p>Documenti</p>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if(Auth::user()->canAccess('eventi')): ?>
|
||||
<li class="nav-item has-treeview">
|
||||
<a href="<?php echo e(route('eventi.calendar')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-calendar"></i>
|
||||
<p>Eventi <i class="right fas fa-angle-left" style="font-size: 0.8rem;"></i></p>
|
||||
</a>
|
||||
<ul class="nav nav-treeview">
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('eventi.calendar')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-calendar-alt"></i>
|
||||
<p>Calendario</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('eventi.index')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-list"></i>
|
||||
<p>Elenco</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('eventi.create')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-plus"></i>
|
||||
<p>Nuovo Evento</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if(Auth::user()->canAccess('email') || Auth::user()->canAccess('mailing')): ?>
|
||||
<li class="nav-item has-treeview">
|
||||
<a href="<?php echo e(route('email.index', 'inbox')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-envelope"></i>
|
||||
<p>Email <i class="right fas fa-angle-left" style="font-size: 0.8rem;"></i></p>
|
||||
</a>
|
||||
<ul class="nav nav-treeview">
|
||||
<?php if(Auth::user()->canAccess('email')): ?>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('email.compose')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-plus"></i>
|
||||
<p>Nuova Email</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('email.index', 'inbox')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-inbox"></i>
|
||||
<p>In arrivo</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('email.index', 'starred')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-star"></i>
|
||||
<p>Preferiti</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('email.index', 'sent')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-paper-plane"></i>
|
||||
<p>Inviate</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('email.index', 'drafts')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-edit"></i>
|
||||
<p>Bozze</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('email.index', 'archive')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-archive"></i>
|
||||
<p>Archivio</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('email.index', 'trash')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-trash"></i>
|
||||
<p>Cestino</p>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if(Auth::user()->canAccess('mailing')): ?>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('mailing-liste.index')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-list"></i>
|
||||
<p>Mailing List</p>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if(Auth::user()->canAccess('report')): ?>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('report.index')); ?>" class="nav-link <?php echo e(request()->routeIs('report.*') ? 'active' : ''); ?>">
|
||||
<i class="nav-icon fas fa-chart-bar"></i>
|
||||
<p>Report</p>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if(Auth::user()->canManage('settings')): ?>
|
||||
<li class="nav-item">
|
||||
<a href="/impostazioni" class="nav-link <?php echo e(request()->is('impostazioni*') ? 'active' : ''); ?>">
|
||||
<i class="nav-icon fas fa-cog"></i>
|
||||
<p>Impostazioni</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item has-treeview <?php echo e(request()->is('admin/*') ? 'menu-open' : ''); ?>">
|
||||
<a href="#" class="nav-link <?php echo e(request()->is('admin/*') ? 'active' : ''); ?>">
|
||||
<i class="nav-icon fas fa-users-cog"></i>
|
||||
<p>Admin <i class="right fas fa-angle-left" style="font-size: 0.8rem;"></i></p>
|
||||
</a>
|
||||
<ul class="nav nav-treeview">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/utenti" class="nav-link <?php echo e(request()->routeIs('admin.utenti.*') ? 'active' : ''); ?>">
|
||||
<i class="nav-icon fas fa-user-shield"></i>
|
||||
<p>Utenti</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('admin.ruoli.index')); ?>" class="nav-link <?php echo e(request()->routeIs('admin.ruoli.*') ? 'active' : ''); ?>">
|
||||
<i class="nav-icon fas fa-tag"></i>
|
||||
<p>Ruoli</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/backup" class="nav-link <?php echo e(request()->routeIs('admin.backup.*') ? 'active' : ''); ?>">
|
||||
<i class="nav-icon fas fa-hdd"></i>
|
||||
<p>Backup</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('admin.activity-logs.index')); ?>" class="nav-link <?php echo e(request()->routeIs('admin.activity-logs.*') ? 'active' : ''); ?>">
|
||||
<i class="nav-icon fas fa-history"></i>
|
||||
<p>Log Attività</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('help')); ?>" class="nav-link <?php echo e(request()->routeIs('help') ? 'active' : ''); ?>">
|
||||
<i class="nav-icon fas fa-question-circle"></i>
|
||||
<p>Guida / Aiuto</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="content-wrapper" style="min-height: calc(100vh - 120px);">
|
||||
<div class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-6">
|
||||
<h1 class="m-0"><?php echo $__env->yieldContent('page_title', 'Dashboard'); ?></h1>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<ol class="breadcrumb float-sm-right">
|
||||
<?php echo $__env->yieldContent('breadcrumbs'); ?>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="container-fluid">
|
||||
<?php echo $slot ?? ''; ?>
|
||||
|
||||
<?php echo $__env->yieldContent('content'); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="main-footer">
|
||||
<strong><?php echo e(\App\Models\AppSetting::getFooterText()); ?></strong>
|
||||
<?php if(\App\Models\AppSetting::shouldShowVersion() && \App\Models\AppSetting::getAppVersion()): ?>
|
||||
<span class="ml-2">v<?php echo e(\App\Models\AppSetting::getAppVersion()); ?></span>
|
||||
<?php endif; ?>
|
||||
</footer>
|
||||
</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>
|
||||
<script src="https://cdn.jsdelivr.net/npm/admin-lte@3.2/dist/js/adminlte.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<?php echo $__env->yieldContent('scripts'); ?>
|
||||
</body>
|
||||
</html><?php /**PATH /var/www/html/glastree/resources/views/layouts/adminlte.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)) {
|
||||
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)) {
|
||||
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