export calendar - modifica impostazioni
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class SyncUserPermissions extends Command
|
||||
{
|
||||
protected $signature = 'users:sync-permissions';
|
||||
protected $description = 'Sync all users permissions with MODULES, adding missing keys with default 0';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$count = 0;
|
||||
|
||||
foreach (User::all() as $user) {
|
||||
$permissions = $user->permissions ?? [];
|
||||
$changed = false;
|
||||
|
||||
foreach (User::MODULES as $module) {
|
||||
if (!array_key_exists($module, $permissions)) {
|
||||
$permissions[$module] = User::LEVEL_NONE;
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($changed) {
|
||||
$user->permissions = $permissions;
|
||||
$user->save();
|
||||
$this->info("Synced permissions for user ID {$user->id} ({$user->email})");
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->info("Done. {$count} users updated.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -11,12 +11,14 @@ class EmailSettingsController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$this->authorizeWrite('settings');
|
||||
$settings = EmailSetting::first() ?? new EmailSetting();
|
||||
return view('admin.email-settings.index', compact('settings'));
|
||||
}
|
||||
|
||||
public function save(Request $request)
|
||||
{
|
||||
$this->authorizeWrite('settings');
|
||||
$validated = $request->validate([
|
||||
'imap_host' => 'required|string|max:255',
|
||||
'imap_port' => 'required|integer|min:1|max:65535',
|
||||
@@ -62,6 +64,7 @@ class EmailSettingsController extends Controller
|
||||
|
||||
public function testConnection(Request $request)
|
||||
{
|
||||
$this->authorizeWrite('settings');
|
||||
$settings = EmailSetting::first();
|
||||
|
||||
if (!$settings) {
|
||||
@@ -87,6 +90,7 @@ class EmailSettingsController extends Controller
|
||||
|
||||
public function testSmtp(Request $request)
|
||||
{
|
||||
$this->authorizeWrite('settings');
|
||||
$request->validate([
|
||||
'email' => 'required|email',
|
||||
]);
|
||||
@@ -164,6 +168,7 @@ class EmailSettingsController extends Controller
|
||||
|
||||
public function syncNow()
|
||||
{
|
||||
$this->authorizeWrite('settings');
|
||||
$settings = EmailSetting::getActive();
|
||||
|
||||
if (!$settings) {
|
||||
|
||||
@@ -67,6 +67,8 @@ class AuthController extends Controller
|
||||
'documenti' => 1,
|
||||
'mailing' => 1,
|
||||
'viste' => 1,
|
||||
'report' => 0,
|
||||
'settings' => 0,
|
||||
];
|
||||
|
||||
if ($isFirstUser) {
|
||||
@@ -75,9 +77,11 @@ class AuthController extends Controller
|
||||
'gruppi' => 2,
|
||||
'eventi' => 2,
|
||||
'documenti' => 2,
|
||||
'mailing' => 2,
|
||||
'viste' => 2,
|
||||
];
|
||||
'mailing' => 2,
|
||||
'viste' => 2,
|
||||
'report' => 0,
|
||||
'settings' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$user = User::create([
|
||||
|
||||
@@ -6,7 +6,9 @@ use App\Models\Evento;
|
||||
use App\Models\Gruppo;
|
||||
use App\Models\Individuo;
|
||||
use App\Models\TipologiaEvento;
|
||||
use App\Services\IcsExportService;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class EventoController extends Controller
|
||||
{
|
||||
@@ -219,6 +221,55 @@ class EventoController extends Controller
|
||||
return redirect('/eventi')->with('success', count($eventi) . ' eventi eliminati con successo.');
|
||||
}
|
||||
|
||||
public function exportIcs(int $id, IcsExportService $ics): StreamedResponse
|
||||
{
|
||||
$this->authorizeRead('eventi');
|
||||
$evento = Evento::findOrFail($id);
|
||||
$content = $ics->generateSingle($evento);
|
||||
|
||||
return response()->streamDownload(function () use ($content) {
|
||||
echo $content;
|
||||
}, $ics->getFilename($evento), [
|
||||
'Content-Type' => 'text/calendar; charset=utf-8',
|
||||
'Content-Length' => strlen($content),
|
||||
]);
|
||||
}
|
||||
|
||||
public function exportSelectedIcs(Request $request, IcsExportService $ics): StreamedResponse
|
||||
{
|
||||
$this->authorizeRead('eventi');
|
||||
|
||||
$ids = $request->input('ids', []);
|
||||
if (!empty($ids) && is_array($ids)) {
|
||||
$eventi = Evento::whereIn('id', $ids)->get();
|
||||
} else {
|
||||
$query = Evento::query();
|
||||
if ($request->filled('search')) {
|
||||
$query->where('nome_evento', 'like', '%' . $request->search . '%');
|
||||
}
|
||||
if ($request->filled('gruppo_id')) {
|
||||
$query->whereHas('gruppi', fn($q) => $q->where('gruppi.id', $request->gruppo_id));
|
||||
}
|
||||
if ($request->filled('tipo')) {
|
||||
$query->where('tipo_recorrenza', $request->tipo);
|
||||
}
|
||||
$eventi = $query->get();
|
||||
}
|
||||
|
||||
if ($eventi->isEmpty()) {
|
||||
return redirect('/eventi')->with('error', 'Nessun evento da esportare.');
|
||||
}
|
||||
|
||||
$content = $ics->generateCollection($eventi);
|
||||
|
||||
return response()->streamDownload(function () use ($content) {
|
||||
echo $content;
|
||||
}, $ics->getCollectionFilename(), [
|
||||
'Content-Type' => 'text/calendar; charset=utf-8',
|
||||
'Content-Length' => strlen($content),
|
||||
]);
|
||||
}
|
||||
|
||||
public function calendar()
|
||||
{
|
||||
$this->authorizeRead('eventi');
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\AppSetting;
|
||||
use App\Models\EmailSetting;
|
||||
use App\Models\Ruolo;
|
||||
use App\Models\TipologiaDocumento;
|
||||
use App\Models\TipologiaEvento;
|
||||
@@ -13,19 +14,20 @@ class ImpostazioniController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$this->authorizeWrite('viste');
|
||||
$this->authorizeWrite('settings');
|
||||
|
||||
$tipologie = TipologiaDocumento::orderBy('ordine')->get();
|
||||
$tipologieEventi = TipologiaEvento::orderBy('ordine')->get();
|
||||
$ruoli = Ruolo::orderBy('ordine')->get();
|
||||
$appSettings = AppSetting::first();
|
||||
$emailSettings = EmailSetting::first() ?? new EmailSetting();
|
||||
|
||||
return view('impostazioni.index', compact('tipologie', 'tipologieEventi', 'ruoli', 'appSettings'));
|
||||
return view('impostazioni.index', compact('tipologie', 'tipologieEventi', 'ruoli', 'appSettings', 'emailSettings'));
|
||||
}
|
||||
|
||||
public function saveAppSettings(Request $request)
|
||||
{
|
||||
$this->authorizeWrite('viste');
|
||||
$this->authorizeWrite('settings');
|
||||
|
||||
$validated = $request->validate([
|
||||
'nome_applicazione' => 'nullable|string|max:100',
|
||||
@@ -52,7 +54,7 @@ class ImpostazioniController extends Controller
|
||||
|
||||
public function tipologieStore(Request $request)
|
||||
{
|
||||
$this->authorizeWrite('viste');
|
||||
$this->authorizeWrite('settings');
|
||||
|
||||
$validated = $request->validate([
|
||||
'nome' => 'required|string|max:50|unique:tipologie_documenti,nome',
|
||||
@@ -73,7 +75,7 @@ class ImpostazioniController extends Controller
|
||||
|
||||
public function tipologieUpdate(Request $request, $id)
|
||||
{
|
||||
$this->authorizeWrite('viste');
|
||||
$this->authorizeWrite('settings');
|
||||
|
||||
$tipologia = TipologiaDocumento::findOrFail($id);
|
||||
|
||||
@@ -90,7 +92,7 @@ class ImpostazioniController extends Controller
|
||||
|
||||
public function tipologieDestroy($id)
|
||||
{
|
||||
$this->authorizeDelete('viste');
|
||||
$this->authorizeDelete('settings');
|
||||
$tipologia = TipologiaDocumento::findOrFail($id);
|
||||
|
||||
$count = $tipologia->documenti()->count();
|
||||
@@ -105,7 +107,7 @@ class ImpostazioniController extends Controller
|
||||
|
||||
public function tipologieReorder(Request $request)
|
||||
{
|
||||
$this->authorizeWrite('viste');
|
||||
$this->authorizeWrite('settings');
|
||||
|
||||
$order = $request->input('order', []);
|
||||
|
||||
@@ -118,7 +120,7 @@ class ImpostazioniController extends Controller
|
||||
|
||||
public function ruoliStore(Request $request)
|
||||
{
|
||||
$this->authorizeWrite('viste');
|
||||
$this->authorizeWrite('settings');
|
||||
|
||||
$validated = $request->validate([
|
||||
'nome' => 'required|string|max:50|unique:ruoli,nome',
|
||||
@@ -139,7 +141,7 @@ class ImpostazioniController extends Controller
|
||||
|
||||
public function ruoliUpdate(Request $request, $id)
|
||||
{
|
||||
$this->authorizeWrite('viste');
|
||||
$this->authorizeWrite('settings');
|
||||
|
||||
$ruolo = Ruolo::findOrFail($id);
|
||||
|
||||
@@ -156,7 +158,7 @@ class ImpostazioniController extends Controller
|
||||
|
||||
public function ruoliDestroy($id)
|
||||
{
|
||||
$this->authorizeDelete('viste');
|
||||
$this->authorizeDelete('settings');
|
||||
|
||||
$ruolo = Ruolo::findOrFail($id);
|
||||
|
||||
@@ -176,7 +178,7 @@ class ImpostazioniController extends Controller
|
||||
|
||||
public function ruoliReorder(Request $request)
|
||||
{
|
||||
$this->authorizeWrite('viste');
|
||||
$this->authorizeWrite('settings');
|
||||
|
||||
$order = $request->input('order', []);
|
||||
|
||||
@@ -189,7 +191,7 @@ class ImpostazioniController extends Controller
|
||||
|
||||
public function tipologieEventiStore(Request $request)
|
||||
{
|
||||
$this->authorizeWrite('viste');
|
||||
$this->authorizeWrite('settings');
|
||||
|
||||
$validated = $request->validate([
|
||||
'nome' => 'required|string|max:50|unique:tipologie_eventi,nome',
|
||||
@@ -210,7 +212,7 @@ class ImpostazioniController extends Controller
|
||||
|
||||
public function tipologieEventiUpdate(Request $request, $id)
|
||||
{
|
||||
$this->authorizeWrite('viste');
|
||||
$this->authorizeWrite('settings');
|
||||
|
||||
$tipologia = TipologiaEvento::findOrFail($id);
|
||||
|
||||
@@ -227,7 +229,7 @@ class ImpostazioniController extends Controller
|
||||
|
||||
public function tipologieEventiDestroy($id)
|
||||
{
|
||||
$this->authorizeDelete('viste');
|
||||
$this->authorizeDelete('settings');
|
||||
$tipologia = TipologiaEvento::findOrFail($id);
|
||||
|
||||
$count = $tipologia->eventi()->count();
|
||||
@@ -242,7 +244,7 @@ class ImpostazioniController extends Controller
|
||||
|
||||
public function tipologieEventiReorder(Request $request)
|
||||
{
|
||||
$this->authorizeWrite('viste');
|
||||
$this->authorizeWrite('settings');
|
||||
|
||||
$order = $request->input('order', []);
|
||||
|
||||
@@ -255,7 +257,7 @@ class ImpostazioniController extends Controller
|
||||
|
||||
public function uploadLogo(Request $request)
|
||||
{
|
||||
$this->authorizeWrite('viste');
|
||||
$this->authorizeWrite('settings');
|
||||
|
||||
$request->validate([
|
||||
'logo' => 'nullable|image|mimes:png,jpg,jpeg,gif,svg|max:2048',
|
||||
@@ -285,7 +287,7 @@ class ImpostazioniController extends Controller
|
||||
|
||||
public function removeLogo(Request $request)
|
||||
{
|
||||
$this->authorizeWrite('viste');
|
||||
$this->authorizeWrite('settings');
|
||||
|
||||
$type = $request->input('type');
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ class AdminOnly
|
||||
return redirect('/login');
|
||||
}
|
||||
|
||||
if (!auth()->user()->isSuperAdmin()) {
|
||||
if (!auth()->user()->isSuperAdmin() && !auth()->user()->canManage('settings')) {
|
||||
abort(403, 'Accesso riservato agli amministratori.');
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ class User extends Authenticatable
|
||||
public const STATUS_SUSPENDED = 'suspended';
|
||||
public const STATUS_PENDING = 'pending';
|
||||
|
||||
public const MODULES = ['individui', 'gruppi', 'eventi', 'documenti', 'mailing', 'viste', 'report'];
|
||||
public const MODULES = ['individui', 'gruppi', 'eventi', 'documenti', 'mailing', 'viste', 'report', 'settings'];
|
||||
public const LEVEL_NONE = 0;
|
||||
public const LEVEL_READ = 1;
|
||||
public const LEVEL_FULL = 2;
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Evento;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class IcsExportService
|
||||
{
|
||||
private const DATETIME_FORMAT = 'Ymd\THis';
|
||||
private const DATE_FORMAT = 'Ymd';
|
||||
private const TIMEZONE = 'Europe/Rome';
|
||||
|
||||
private const DAY_MAP = [
|
||||
0 => 'SU',
|
||||
1 => 'MO',
|
||||
2 => 'TU',
|
||||
3 => 'WE',
|
||||
4 => 'TH',
|
||||
5 => 'FR',
|
||||
6 => 'SA',
|
||||
];
|
||||
|
||||
public function generateSingle(Evento $evento): string
|
||||
{
|
||||
return $this->generateCollection(collect([$evento]));
|
||||
}
|
||||
|
||||
public function generateCollection(iterable $eventi): string
|
||||
{
|
||||
$lines = [
|
||||
'BEGIN:VCALENDAR',
|
||||
'VERSION:2.0',
|
||||
'PRODID:-//Glastree//Eventi//IT',
|
||||
'CALSCALE:GREGORIAN',
|
||||
'METHOD:PUBLISH',
|
||||
'X-WR-CALNAME:' . config('app.name') . ' - Eventi',
|
||||
];
|
||||
|
||||
foreach ($eventi as $evento) {
|
||||
array_push($lines, ...$this->buildVEvent($evento));
|
||||
}
|
||||
|
||||
$lines[] = 'END:VCALENDAR';
|
||||
|
||||
return $this->fold(implode("\r\n", $lines));
|
||||
}
|
||||
|
||||
private function buildVEvent(Evento $evento): array
|
||||
{
|
||||
$uid = 'evento-' . $evento->id . '@' . preg_replace('/[^a-z0-9]/', '', strtolower(config('app.name')));
|
||||
$lines = [
|
||||
'BEGIN:VEVENT',
|
||||
'UID:' . $uid,
|
||||
'DTSTAMP:' . now()->format(self::DATETIME_FORMAT) . 'Z',
|
||||
];
|
||||
|
||||
if ($evento->isSingolo() && $evento->data_specifica) {
|
||||
$date = $evento->data_specifica instanceof Carbon
|
||||
? $evento->data_specifica
|
||||
: Carbon::parse($evento->data_specifica);
|
||||
|
||||
if ($evento->ora_inizio) {
|
||||
$start = $date->copy()->setTimeFrom($evento->ora_inizio instanceof Carbon ? $evento->ora_inizio : Carbon::parse($evento->ora_inizio));
|
||||
$durata = $evento->durata_minuti ?? 60;
|
||||
$end = $start->copy()->addMinutes((int) $durata);
|
||||
$lines[] = 'DTSTART;TZID=' . self::TIMEZONE . ':' . $start->format(self::DATETIME_FORMAT);
|
||||
$lines[] = 'DTEND;TZID=' . self::TIMEZONE . ':' . $end->format(self::DATETIME_FORMAT);
|
||||
} else {
|
||||
$lines[] = 'DTSTART;VALUE=DATE:' . $date->format(self::DATE_FORMAT);
|
||||
$lines[] = 'DTEND;VALUE=DATE:' . $date->copy()->addDay()->format(self::DATE_FORMAT);
|
||||
}
|
||||
} else {
|
||||
$rrule = $this->buildRRule($evento);
|
||||
if ($rrule) {
|
||||
$lines[] = $rrule;
|
||||
}
|
||||
|
||||
if ($evento->ora_inizio) {
|
||||
$time = $evento->ora_inizio instanceof Carbon ? $evento->ora_inizio : Carbon::parse($evento->ora_inizio);
|
||||
$lines[] = 'DTSTART;TZID=' . self::TIMEZONE . ':' . $time->format('His');
|
||||
$durata = $evento->durata_minuti ?? 60;
|
||||
$endTime = $time->copy()->addMinutes((int) $durata);
|
||||
$lines[] = 'DTEND;TZID=' . self::TIMEZONE . ':' . $endTime->format('His');
|
||||
}
|
||||
}
|
||||
|
||||
$lines[] = 'SUMMARY:' . $this->escapeText($evento->nome_evento);
|
||||
|
||||
if ($evento->descrizione_evento || $evento->descrizione) {
|
||||
$desc = $evento->descrizione_evento
|
||||
? ($evento->descrizione ? $evento->descrizione_evento . '\n\n' . $evento->descrizione : $evento->descrizione_evento)
|
||||
: $evento->descrizione;
|
||||
$lines[] = 'DESCRIPTION:' . $this->escapeText($desc);
|
||||
}
|
||||
|
||||
if ($evento->luogo_indirizzo) {
|
||||
$lines[] = 'LOCATION:' . $this->escapeText($evento->luogo_indirizzo);
|
||||
}
|
||||
|
||||
$lines[] = 'END:VEVENT';
|
||||
|
||||
return $lines;
|
||||
}
|
||||
|
||||
private function buildRRule(Evento $evento): ?string
|
||||
{
|
||||
return match ($evento->tipo_recorrenza) {
|
||||
'settimanale' => $this->rruleSettimanale($evento),
|
||||
'mensile' => $this->rruleMensile($evento),
|
||||
'annuale' => $this->rruleAnnuale($evento),
|
||||
'altro' => $this->rruleAltro($evento),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
private function rruleSettimanale(Evento $evento): ?string
|
||||
{
|
||||
if ($evento->giorno_settimana === null) {
|
||||
return null;
|
||||
}
|
||||
return 'RRULE:FREQ=WEEKLY;BYDAY=' . (self::DAY_MAP[(int) $evento->giorno_settimana] ?? 'MO');
|
||||
}
|
||||
|
||||
private function rruleMensile(Evento $evento): ?string
|
||||
{
|
||||
if (!$evento->occorrenza_mese) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parts = explode(',', $evento->occorrenza_mese);
|
||||
if (count($parts) !== 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$occorrenza = (int) $parts[0];
|
||||
$giorno = (int) $parts[1];
|
||||
$byDay = self::DAY_MAP[$giorno] ?? 'MO';
|
||||
|
||||
$rrule = 'RRULE:FREQ=MONTHLY;BYSETPOS=' . $occorrenza . ';BYDAY=' . $byDay;
|
||||
|
||||
if ($evento->mesi_recorrenza) {
|
||||
$mesi = explode(',', $evento->mesi_recorrenza);
|
||||
$rrule .= ';BYMONTH=' . implode(',', $mesi);
|
||||
}
|
||||
|
||||
return $rrule;
|
||||
}
|
||||
|
||||
private function rruleAnnuale(Evento $evento): ?string
|
||||
{
|
||||
if (!$evento->mese_annuale) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$rrule = 'RRULE:FREQ=YEARLY;BYMONTH=' . $evento->mese_annuale;
|
||||
|
||||
$giorno = (int) ($evento->giorno_mese ?? 1);
|
||||
$rrule .= ';BYMONTHDAY=' . $giorno;
|
||||
|
||||
return $rrule;
|
||||
}
|
||||
|
||||
private function rruleAltro(Evento $evento): ?string
|
||||
{
|
||||
return $this->rruleSettimanale($evento);
|
||||
}
|
||||
|
||||
private function escapeText(?string $text): string
|
||||
{
|
||||
if ($text === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$text = str_replace(
|
||||
['\\', ';', ',', "\n"],
|
||||
['\\\\', '\\;', '\\,', '\\n'],
|
||||
$text
|
||||
);
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
private function fold(string $ics): string
|
||||
{
|
||||
$lines = explode("\r\n", $ics);
|
||||
$folded = [];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
while (mb_strlen($line) > 75) {
|
||||
$folded[] = mb_substr($line, 0, 75);
|
||||
$line = ' ' . mb_substr($line, 75);
|
||||
}
|
||||
$folded[] = $line;
|
||||
}
|
||||
|
||||
return implode("\r\n", $folded);
|
||||
}
|
||||
|
||||
public function getFilename(Evento $evento): string
|
||||
{
|
||||
$name = str_replace(['/', '\\', ' '], '_', $evento->nome_evento);
|
||||
return $name . '.ics';
|
||||
}
|
||||
|
||||
public function getCollectionFilename(): string
|
||||
{
|
||||
return 'eventi_' . now()->format('Ymd_His') . '.ics';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user