aggiunta calendario

This commit is contained in:
2026-06-02 22:07:16 +02:00
parent aa4e582925
commit c9d66c2a80
31 changed files with 4435 additions and 86 deletions
@@ -14,9 +14,14 @@
<div class="card">
<div class="card-header">
<h3 class="card-title">Attività di Sistema</h3>
<div class="card-tools">
<button type="button" class="btn btn-danger btn-sm" onclick="deleteLogs()">
<i class="fas fa-trash mr-1"></i> Cancella Log
</button>
</div>
</div>
<div class="card-body">
<form method="GET" class="mb-3">
<form method="GET" class="mb-3" id="filter-form">
<div class="row">
<div class="col-md-2">
<select name="user_id" class="form-control">
@@ -94,4 +99,59 @@
</div>
</div>
</div>
@endsection
@section('scripts')
<script>
function deleteLogs() {
var filterForm = document.getElementById('filter-form');
var params = new URLSearchParams(new FormData(filterForm));
var filterDescription = [];
var userFilter = params.get('user_id');
var moduleFilter = params.get('module');
var actionFilter = params.get('action');
var fromFilter = params.get('from');
var toFilter = params.get('to');
if (userFilter) filterDescription.push('utente=' + userFilter);
if (moduleFilter) filterDescription.push('modulo=' + moduleFilter);
if (actionFilter) filterDescription.push('azione=' + actionFilter);
if (fromFilter) filterDescription.push('dal=' + fromFilter);
if (toFilter) filterDescription.push('al=' + toFilter);
var message = filterDescription.length > 0
? 'Eliminare i log filtrati? (' + filterDescription.join(', ') + ')'
: 'Eliminare TUTTI i log?';
if (!confirm(message)) return;
var form = document.createElement('form');
form.method = 'POST';
form.action = '/admin/activity-logs';
form.style.display = 'none';
var csrf = document.createElement('input');
csrf.name = '_token';
csrf.value = '{{ csrf_token() }}';
form.appendChild(csrf);
var method = document.createElement('input');
method.name = '_method';
method.value = 'DELETE';
form.appendChild(method);
filterForm.querySelectorAll('select, input').forEach(function(el) {
if (el.name && el.value) {
var input = document.createElement('input');
input.name = el.name;
input.value = el.value;
form.appendChild(input);
}
});
document.body.appendChild(form);
form.submit();
}
</script>
@endsection
@@ -377,7 +377,7 @@
</div>
</div>
<div class="card-footer">
<div class="card-footer d-flex flex-wrap align-items-center gap-2">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save mr-1"></i> Salva Impostazioni
</button>
@@ -390,6 +390,12 @@
<a href="{{ route('impostazioni.email.sync') }}" class="btn btn-warning ml-2">
<i class="fas fa-sync mr-1"></i> Sincronizza Ora
</a>
<form action="{{ route('impostazioni.email.destroy') }}" method="POST" class="ml-auto" onsubmit="return confirm('Eliminare definitivamente la configurazione email? Tutti i dati di connessione verranno rimossi.')">
@csrf @method('DELETE')
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash mr-1"></i> Elimina Configurazione
</button>
</form>
</div>
</form>
</div>
@@ -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');
+365 -12
View File
@@ -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">&times;</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>
&rarr; API &amp; Services &rarr; Credentials &rarr; 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',