glastree_on_gitea

This commit is contained in:
2026-05-26 08:14:29 +02:00
commit 0bed099d05
9556 changed files with 1186307 additions and 0 deletions
@@ -0,0 +1,42 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\ActivityLog;
use App\Models\User;
use Illuminate\Http\Request;
class ActivityLogController extends Controller
{
public function index(Request $request)
{
$query = ActivityLog::with('user');
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);
}
$logs = $query->orderByDesc('created_at')->paginate(50);
$users = User::orderBy('name')->get();
$modules = ActivityLog::select('module')->distinct()->pluck('module');
return view('admin.activity-logs.index', compact('logs', 'users', 'modules'));
}
}
@@ -0,0 +1,181 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\EmailSetting;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt;
class EmailSettingsController extends Controller
{
public function index()
{
$settings = EmailSetting::first() ?? new EmailSetting();
return view('admin.email-settings.index', compact('settings'));
}
public function save(Request $request)
{
$validated = $request->validate([
'imap_host' => 'required|string|max:255',
'imap_port' => 'required|integer|min:1|max:65535',
'imap_encryption' => 'nullable|in:ssl,tls,none',
'imap_username' => 'required|string|max:255',
'imap_password' => 'nullable|string',
'smtp_host' => 'nullable|string|max:255',
'smtp_port' => 'nullable|integer|min:1|max:65535',
'smtp_encryption' => 'nullable|in:ssl,tls,none',
'email_address' => 'required|email',
'email_name' => 'nullable|string|max:255',
'reply_to' => 'nullable|email',
'sync_interval_minutes' => 'nullable|integer|min:1|max:60',
'is_active' => 'boolean',
]);
if (!empty($validated['imap_password'])) {
$validated['imap_password'] = Crypt::encryptString($validated['imap_password']);
} else {
unset($validated['imap_password']);
}
if (!empty($validated['smtp_password'])) {
$validated['smtp_password'] = Crypt::encryptString($validated['smtp_password']);
} else {
unset($validated['smtp_password']);
}
$saved = EmailSetting::updateOrCreate(
['id' => $request->id ?? 1],
$validated
);
// Verifica che i dati siano stati salvati
$verify = EmailSetting::find($saved->id);
if ($verify &&
$verify->imap_host === $validated['imap_host'] &&
$verify->email_address === $validated['email_address']) {
return back()->with('success', 'Impostazioni email salvate e verificate nel database.');
} else {
return back()->with('error', 'Errore nel salvataggio: i dati non sono stati salvati correttamente.');
}
}
public function testConnection(Request $request)
{
$settings = EmailSetting::first();
if (!$settings) {
return response()->json(['success' => false, 'message' => 'Configurazione non trovata.']);
}
// Verifica che i dati siano salvati
if (empty($settings->imap_host) || empty($settings->email_address)) {
return response()->json(['success' => false, 'message' => 'Prima salva le impostazioni.']);
}
try {
$result = $settings->testConnection();
return response()->json($result);
} catch (\Exception $e) {
$msg = $e->getMessage();
if (strpos($msg, 'Undefined property') !== false) {
$msg = 'Impossibile connettersi al server IMAP. Verifica host, porta, encryption e credenziali.';
}
return response()->json(['success' => false, 'message' => 'Errore: ' . $msg]);
}
}
public function testSmtp(Request $request)
{
$request->validate([
'email' => 'required|email',
]);
$settings = EmailSetting::first();
if (!$settings) {
return response()->json(['success' => false, 'message' => 'Configurazione non trovata.']);
}
if (empty($settings->smtp_host)) {
$settings->smtp_host = 'smtp.gmail.com';
$settings->smtp_port = 587;
$settings->smtp_encryption = 'tls';
}
try {
$smtpUsername = $settings->smtp_username;
$smtpPassword = $settings->smtp_password;
if (empty($smtpUsername)) {
$smtpUsername = $settings->email_address;
}
if (empty($smtpPassword)) {
$smtpPassword = $settings->getDecryptedPassword();
} else {
$smtpPassword = Crypt::decryptString($smtpPassword);
}
$encryption = $settings->smtp_encryption ?? 'tls';
$scheme = ($encryption === 'ssl') ? 'smtps' : 'smtp';
$dsn = sprintf(
'%s://%s:%s@%s:%d?encryption=%s',
$scheme,
rawurlencode($smtpUsername),
rawurlencode($smtpPassword),
$settings->smtp_host,
$settings->smtp_port ?? 587,
$encryption
);
$transport = \Symfony\Component\Mailer\Transport::fromDsn($dsn);
$mailer = new \Symfony\Component\Mailer\Mailer($transport);
$fromName = $settings->email_name ?? '';
if (filter_var($fromName, FILTER_VALIDATE_EMAIL)) {
$fromName = 'Glastree';
} else {
$fromName = preg_replace('/[^\p{L}\p{N}\s]/u', '', $fromName);
$fromName = trim($fromName);
if (empty($fromName) || strlen($fromName) > 50) {
$fromName = 'Glastree';
}
}
$fromAddress = \Symfony\Component\Mime\Address::create($settings->email_address, $fromName);
$email = (new \Symfony\Component\Mime\Email())
->from($fromAddress)
->to($request->email)
->subject('Test Configurazione SMTP - Glastree')
->text('Test email da Glastree - Configurazione SMTP funziona!');
$mailer->send($email);
\Illuminate\Support\Facades\Log::info('Test SMTP ok', ['to' => $request->email, 'from' => $settings->email_address]);
return response()->json(['success' => true, 'message' => 'Email di test inviata a ' . $request->email]);
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Test SMTP failed', ['error' => $e->getMessage()]);
return response()->json(['success' => false, 'message' => 'Errore invio: ' . $e->getMessage()]);
}
}
public function syncNow()
{
$settings = EmailSetting::getActive();
if (!$settings) {
return back()->with('error', 'Nessuna configurazione email attiva.');
}
try {
app(\App\Http\Controllers\EmailController::class)->syncEmails();
$settings->update(['last_sync_at' => now()]);
return back()->with('success', 'Sincronizzazione completata.');
} catch (\Exception $e) {
return back()->with('error', 'Errore sync: ' . $e->getMessage());
}
}
}
@@ -0,0 +1,106 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\ActivityLog;
use App\Models\RolePreset;
use App\Models\User;
use Illuminate\Http\Request;
class RuoloController extends Controller
{
public function index()
{
$ruoli = RolePreset::withCount('users')->get();
return view('admin.ruoli.index', compact('ruoli'));
}
public function create()
{
return view('admin.ruoli.create');
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:50|unique:role_presets,name',
'description' => 'nullable|string|max:255',
'permissions' => 'required|array',
'permissions.*' => 'in:0,1,2',
]);
$ruolo = RolePreset::create([
'name' => $validated['name'],
'description' => $validated['description'] ?? null,
'permissions' => $validated['permissions'],
]);
ActivityLog::log('create', 'ruoli', 'Creato ruolo: ' . $ruolo->name, [
'role_id' => $ruolo->id,
'permissions' => $validated['permissions'],
]);
return redirect('/admin/ruoli')->with('success', 'Ruolo creato.');
}
public function edit($id)
{
$ruolo = RolePreset::findOrFail($id);
$usersCount = $ruolo->users()->count();
return view('admin.ruoli.edit', compact('ruolo', 'usersCount'));
}
public function update(Request $request, $id)
{
$ruolo = RolePreset::findOrFail($id);
$validated = $request->validate([
'description' => 'nullable|string|max:255',
'permissions' => 'required|array',
'permissions.*' => 'in:0,1,2',
]);
$old = $ruolo->permissions;
$ruolo->description = $validated['description'];
$ruolo->permissions = $validated['permissions'];
$ruolo->save();
ActivityLog::log('update', 'ruoli', 'Modificato ruolo: ' . $ruolo->name, [
'role_id' => $ruolo->id,
'old_permissions' => $old,
'new_permissions' => $validated['permissions'],
]);
if ($request->input('update_users') === '1') {
foreach ($ruolo->users as $user) {
$user->permissions = $ruolo->permissions;
$user->save();
}
return redirect('/admin/ruoli')->with('success', 'Ruolo aggiornato e permessi propagati a ' . $ruolo->users()->count() . ' utenti.');
}
return redirect('/admin/ruoli')->with('success', 'Ruolo aggiornato.');
}
public function destroy($id)
{
$ruolo = RolePreset::findOrFail($id);
if ($ruolo->users()->count() > 0) {
return redirect('/admin/ruoli')->with('error', 'Impossibile eliminare: ci sono ' . $ruolo->users()->count() . ' utenti con questo ruolo.');
}
$name = $ruolo->name;
ActivityLog::log('delete', 'ruoli', 'Eliminato ruolo: ' . $name, [
'role_id' => $id,
'permissions' => $ruolo->permissions,
]);
$ruolo->delete();
return redirect('/admin/ruoli')->with('success', 'Ruolo eliminato.');
}
}
@@ -0,0 +1,150 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\ActivityLog;
use App\Models\RolePreset;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class UtenteController extends Controller
{
public function index(Request $request)
{
$query = User::query();
if ($request->filled('search')) {
$query->where('name', 'like', '%' . $request->search . '%')
->orWhere('email', 'like', '%' . $request->search . '%');
}
if ($request->filled('status')) {
$query->where('status', $request->status);
}
$utenti = $query->orderBy('name')->paginate(20);
$rolePresets = RolePreset::all();
return view('admin.utenti.index', compact('utenti', 'rolePresets'));
}
public function create()
{
$rolePresets = RolePreset::all();
return view('admin.utenti.create', compact('rolePresets'));
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
'password' => 'required|min:8|confirmed',
'role_preset_id' => 'nullable|exists:role_presets,id',
'is_admin' => 'boolean',
'permissions' => 'nullable|array',
]);
$user = new User();
$user->name = $validated['name'];
$user->email = $validated['email'];
$user->password = Hash::make($validated['password']);
$user->is_admin = $validated['is_admin'] ?? false;
$user->status = User::STATUS_ACTIVE;
if (!empty($validated['role_preset_id'])) {
$preset = RolePreset::find($validated['role_preset_id']);
$user->applyRolePreset($preset);
} elseif (!empty($validated['permissions'])) {
$user->setPermissions($validated['permissions']);
}
$user->save();
ActivityLog::log('create', 'utenti', 'Creato utente: ' . $user->name, [
'user_id' => $user->id,
'email' => $user->email,
]);
return redirect('/admin/utenti')->with('success', 'Utente creato.');
}
public function edit($id)
{
$user = User::findOrFail($id);
$rolePresets = RolePreset::all();
return view('admin.utenti.edit', compact('user', 'rolePresets'));
}
public function update(Request $request, $id)
{
$user = User::findOrFail($id);
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email,' . $id,
'password' => 'nullable|min:8|confirmed',
'role_preset_id' => 'nullable|exists:role_presets,id',
'is_admin' => 'boolean',
'status' => 'required|in:active,suspended,pending',
'permissions' => 'nullable|array',
]);
$old = $user->toArray();
$user->name = $validated['name'];
$user->email = $validated['email'];
$user->is_admin = $validated['is_admin'] ?? false;
$user->status = $validated['status'];
if (!empty($validated['password'])) {
$user->password = Hash::make($validated['password']);
}
if (!empty($validated['role_preset_id'])) {
$preset = RolePreset::find($validated['role_preset_id']);
$user->applyRolePreset($preset);
} elseif (isset($validated['permissions'])) {
$user->setPermissions($validated['permissions']);
}
$user->save();
ActivityLog::log('update', 'utenti', 'Modificato utente: ' . $user->name, [
'user_id' => $user->id,
'old' => $old,
'new' => $user->toArray(),
]);
return redirect('/admin/utenti')->with('success', 'Utente aggiornato.');
}
public function destroy($id)
{
$user = User::findOrFail($id);
if ($user->id === auth()->id()) {
return redirect('/admin/utenti')->with('error', 'Non puoi eliminare te stesso.');
}
if ($user->isSuperAdmin()) {
$adminCount = User::where('is_admin', true)->count();
if ($adminCount <= 1) {
return redirect('/admin/utenti')->with('error', 'Impossibile eliminare l\'ultimo amministratore.');
}
}
$userName = $user->name;
ActivityLog::log('delete', 'utenti', 'Eliminato utente: ' . $userName, [
'user_id' => $id,
'deleted' => $user->toArray(),
]);
$user->delete();
return redirect('/admin/utenti')->with('success', 'Utente eliminato.');
}
}
@@ -0,0 +1,108 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\ActivityLog;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
public function showLoginForm()
{
return view('auth.login');
}
public function login(Request $request)
{
$credentials = $request->validate([
'email' => 'required|email',
'password' => 'required',
]);
$user = User::where('email', $credentials['email'])->first();
if ($user && $user->status === 'suspended') {
throw ValidationException::withMessages([
'email' => ['Questo account è sospeso. Contatta un amministratore.'],
]);
}
if (Auth::attempt($credentials)) {
$request->session()->regenerate();
ActivityLog::logLogin();
return redirect()->intended('/dashboard');
}
throw ValidationException::withMessages([
'email' => ['Le credenziali fornite non sono corrette.'],
]);
}
public function showRegisterForm()
{
return view('auth.register');
}
public function register(Request $request)
{
$data = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|string|min:8|confirmed',
]);
$isFirstUser = User::count() === 0;
$defaultPermissions = [
'individui' => 1,
'gruppi' => 1,
'eventi' => 1,
'documenti' => 1,
'mailing' => 1,
'viste' => 1,
];
if ($isFirstUser) {
$defaultPermissions = [
'individui' => 2,
'gruppi' => 2,
'eventi' => 2,
'documenti' => 2,
'mailing' => 2,
'viste' => 2,
];
}
$user = User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
'is_admin' => $isFirstUser,
'status' => User::STATUS_ACTIVE,
'permissions' => $defaultPermissions,
]);
ActivityLog::log('create', 'utenti', 'Registrato nuovo utente: ' . $user->name, [
'user_id' => $user->id,
]);
Auth::login($user);
return redirect('/dashboard');
}
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}
@@ -0,0 +1,42 @@
<?php
namespace App\Http\Controllers;
use App\Models\Contatto;
use Illuminate\Http\Request;
class ContattoController extends Controller
{
public function update($contatto)
{
$this->authorizeWrite('individui');
$contatto = Contatto::findOrFail($contatto);
$data = request()->validate([
'tipo' => 'required|in:telefono,cellulare,email,fax,web,telegram,whatsapp,altro',
'valore' => 'required|string|max:255',
'etichetta' => 'nullable|string|max:100',
'is_primary' => 'nullable',
]);
$contatto->update([
'tipo' => $data['tipo'],
'valore' => $data['valore'],
'etichetta' => $data['etichetta'] ?? null,
'is_primary' => isset($data['is_primary']),
]);
$redirect = request('_redirect', route('individui.show', $contatto->individuo_id));
return redirect($redirect)->with('success', 'Contatto aggiornato.');
}
public function destroy($contatto)
{
$this->authorizeDelete('individui');
$contatto = Contatto::findOrFail($contatto);
$individuo_id = $contatto->individuo_id;
$contatto->delete();
$redirect = request('_redirect', route('individui.show', $individuo_id));
return redirect($redirect)->with('success', 'Contatto eliminato.');
}
}
+50
View File
@@ -0,0 +1,50 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\Request;
abstract class Controller
{
use AuthorizesRequests;
protected function authorizeWrite(string $module): void
{
$user = auth()->user();
if ($user->isSuperAdmin()) {
return;
}
if (!$user->canManage($module)) {
abort(403, 'Non hai i permessi per modificare ' . $module);
}
}
protected function authorizeRead(string $module): void
{
$user = auth()->user();
if ($user->isSuperAdmin()) {
return;
}
if (!$user->canAccess($module)) {
abort(403, 'Non hai i permessi per visualizzare ' . $module);
}
}
protected function authorizeDelete(string $module): void
{
$user = auth()->user();
if ($user->isSuperAdmin()) {
return;
}
if (!$user->canDelete($module)) {
abort(403, 'Non hai i permessi per eliminare ' . $module);
}
}
}
@@ -0,0 +1,455 @@
<?php
namespace App\Http\Controllers;
use App\Models\Documento;
use App\Models\Individuo;
use App\Models\Gruppo;
use App\Models\Evento;
use App\Models\TipologiaDocumento;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class DocumentoController extends Controller
{
public function index(Request $request)
{
$this->authorizeRead('documenti');
$documenti = Documento::with(['user', 'target'])
->orderByDesc('created_at')
->paginate(20);
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
$gruppi = Gruppo::orderBy('nome')->get();
$eventi = Evento::orderBy('nome_evento')->get();
$mailingLists = \App\Models\MailingList::orderBy('nome')->get();
$tipologie = TipologiaDocumento::attive();
return view('documenti.index', compact('documenti', 'individui', 'gruppi', 'eventi', 'mailingLists', 'tipologie'));
}
public function create()
{
$this->authorizeWrite('documenti');
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
$gruppi = Gruppo::orderBy('nome')->get();
$eventi = Evento::orderBy('nome_evento')->get();
$tipologie = TipologiaDocumento::attive();
return view('documenti.create', compact('individui', 'gruppi', 'eventi', 'tipologie'));
}
public function edit($documento)
{
$this->authorizeWrite('documenti');
$documento = Documento::with('target')->findOrFail($documento);
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
$gruppi = Gruppo::orderBy('nome')->get();
$eventi = Evento::orderBy('nome_evento')->get();
$mailingLists = \App\Models\MailingList::orderBy('nome')->get();
$tipologie = TipologiaDocumento::attive();
return view('documenti.edit', compact('documento', 'individui', 'gruppi', 'eventi', 'mailingLists', 'tipologie'));
}
public function options(Request $request)
{
$tipo = $request->get('tipo');
$options = '';
switch ($tipo) {
case 'individuo':
$items = Individuo::orderBy('cognome')->orderBy('nome')->get();
foreach ($items as $item) {
$options .= '<option value="' . $item->id . '">' . e($item->cognome . ' ' . $item->nome) . '</option>';
}
break;
case 'gruppo':
$items = Gruppo::orderBy('nome')->get();
foreach ($items as $item) {
$options .= '<option value="' . $item->id . '">' . e($item->nome) . '</option>';
}
break;
case 'evento':
$items = Evento::orderBy('nome_evento')->get();
foreach ($items as $item) {
$options .= '<option value="' . $item->id . '">' . e($item->nome_evento) . '</option>';
}
break;
case 'mailing':
$items = \App\Models\MailingList::orderBy('nome')->get();
foreach ($items as $item) {
$options .= '<option value="' . $item->id . '">' . e($item->nome) . '</option>';
}
break;
}
return response($options);
}
public function update(Request $request, $documento)
{
$this->authorizeWrite('documenti');
$documento = Documento::findOrFail($documento);
$tipologieValidi = TipologiaDocumento::opzioni();
$tipologieRule = 'in:' . implode(',', $tipologieValidi);
$data = $request->validate([
'nome_file' => 'required|string|max:255',
'tipologia' => 'required|' . $tipologieRule,
'visibilita' => 'nullable|string',
'visibilita_target_id' => 'nullable|integer',
'visibilita_target_type' => 'nullable|string',
'note' => 'nullable|string',
]);
$data['visibilita'] = $request->contesto_tipo ?: 'pubblico';
if ($request->contesto_tipo) {
$typeMap = [
'individuo' => Individuo::class,
'gruppo' => Gruppo::class,
'evento' => Evento::class,
'mailing' => \App\Models\MailingList::class,
];
$data['visibilita_target_type'] = $typeMap[$request->contesto_tipo] ?? null;
} else {
$data['visibilita'] = 'pubblico';
$data['visibilita_target_id'] = null;
$data['visibilita_target_type'] = null;
}
$documento->update($data);
return redirect('/documenti')->with('success', 'Documento aggiornato.');
}
public function store(Request $request)
{
$this->authorizeWrite('documenti');
$data = $request->validate([
'nome_file' => 'required|string|max:255',
'tipologia' => 'required|in:avatar,galleria,documento,statuto,altro',
'file' => 'required|file|max:10240',
'visibilita' => 'required|in:pubblico,individuo,gruppo',
'visibilita_target_id' => 'nullable|integer',
'visibilita_target_type' => 'nullable|string',
]);
$file = $request->file('file');
$path = $file->store('documenti', 'local');
$visibilitaTargetType = null;
if ($data['visibilita'] === 'individuo' && $data['visibilita_target_id']) {
$visibilitaTargetType = Individuo::class;
} elseif ($data['visibilita'] === 'gruppo' && $data['visibilita_target_id']) {
$visibilitaTargetType = Gruppo::class;
}
Documento::create([
'nome_file' => $data['nome_file'],
'file_path' => $path,
'tipo' => 'upload',
'tipologia' => $data['tipologia'],
'visibilita' => $data['visibilita'],
'visibilita_target_id' => $data['visibilita_target_id'] ?? null,
'visibilita_target_type' => $visibilitaTargetType,
'mime_type' => $file->getMimeType(),
'dimensione' => $file->getSize(),
'user_id' => auth()->id(),
]);
$redirect = $request->_redirect ?? url()->previous();
return redirect($redirect)->with('success', 'Documento caricato.');
}
public function download($documento)
{
$this->authorizeRead('documenti');
$documento = Documento::findOrFail($documento);
if (!Storage::disk('local')->exists($documento->file_path)) {
abort(404);
}
$extension = pathinfo($documento->file_path, PATHINFO_EXTENSION);
$filename = $extension ? $documento->nome_file . '.' . $extension : $documento->nome_file;
return Storage::disk('local')->download(
$documento->file_path,
$filename,
['Content-Type' => $documento->mime_type]
);
}
public function preview($documento)
{
$this->authorizeRead('documenti');
$documento = Documento::findOrFail($documento);
if (!Storage::disk('local')->exists($documento->file_path)) {
abort(404);
}
$fullPath = Storage::disk('local')->path($documento->file_path);
$mimeType = $documento->mime_type;
if (str_starts_with($mimeType, 'image/')) {
return response()->file($fullPath, ['Content-Type' => $mimeType]);
}
return response()->file($fullPath, ['Content-Type' => $mimeType]);
}
public function destroy($documento)
{
$this->authorizeDelete('documenti');
$documento = Documento::findOrFail($documento);
if (Storage::disk('local')->exists($documento->file_path)) {
Storage::disk('local')->delete($documento->file_path);
}
$documento->delete();
$redirect = request('_redirect', url('/'));
return redirect($redirect)->with('success', 'Documento eliminato.');
}
public function checkLinks($documento)
{
$documento = Documento::findOrFail($documento);
$links = [];
if (in_array($documento->visibilita, ['individuo', 'gruppo', 'evento']) && $documento->visibilita_target_id) {
if ($documento->visibilita === 'individuo') {
$target = Individuo::find($documento->visibilita_target_id);
if ($target) {
$links[] = [
'type' => 'individuo',
'name' => $target->cognome . ' ' . $target->nome,
'url' => '/individui/' . $target->id,
];
}
} elseif ($documento->visibilita === 'gruppo') {
$target = Gruppo::find($documento->visibilita_target_id);
if ($target) {
$links[] = [
'type' => 'gruppo',
'name' => $target->nome,
'url' => '/gruppi/' . $target->id,
];
}
} elseif ($documento->visibilita === 'evento') {
$target = Evento::find($documento->visibilita_target_id);
if ($target) {
$links[] = [
'type' => 'evento',
'name' => $target->nome_evento,
'url' => '/eventi/' . $target->id,
];
}
}
}
if ($documento->tipologia === 'avatar') {
$linkedToAvatar = Individuo::whereHas('avatar', function($q) use ($documento) {
$q->where('id', $documento->id);
})->count();
if ($linkedToAvatar > 0) {
$links[] = [
'type' => 'avatar',
'name' => 'Avatar di ' . $linkedToAvatar . ' individuo(i)',
'url' => null,
];
}
}
if ($documento->eventi()->count() > 0) {
foreach ($documento->eventi as $evento) {
$links[] = [
'type' => 'evento',
'name' => 'Evento: ' . $evento->nome_evento,
'url' => '/eventi/' . $evento->id,
];
}
}
return response()->json([
'has_links' => count($links) > 0,
'links' => $links,
]);
}
public function checkMassLinks(Request $request)
{
$idsInput = $request->input('ids', '');
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
if (empty($ids)) {
return response()->json(['linked_count' => 0, 'links' => []]);
}
$documenti = Documento::whereIn('id', $ids)->get();
$allLinks = [];
$linkedCount = 0;
foreach ($documenti as $documento) {
$isLinked = false;
$linkName = '';
$linkUrl = null;
if (in_array($documento->visibilita, ['individuo', 'gruppo', 'evento']) && $documento->visibilita_target_id) {
$isLinked = true;
$linkName = $documento->nome_file . ' → ';
if ($documento->visibilita === 'individuo') {
$target = Individuo::find($documento->visibilita_target_id);
$linkName .= $target ? $target->cognome . ' ' . $target->nome : 'Individuo #' . $documento->visibilita_target_id;
$linkUrl = $target ? '/individui/' . $target->id : null;
} elseif ($documento->visibilita === 'gruppo') {
$target = Gruppo::find($documento->visibilita_target_id);
$linkName .= $target ? $target->nome : 'Gruppo #' . $documento->visibilita_target_id;
$linkUrl = $target ? '/gruppi/' . $target->id : null;
} elseif ($documento->visibilita === 'evento') {
$target = Evento::find($documento->visibilita_target_id);
$linkName .= $target ? $target->nome_evento : 'Evento #' . $documento->visibilita_target_id;
$linkUrl = $target ? '/eventi/' . $target->id : null;
}
}
if ($documento->tipologia === 'avatar') {
$linkedToAvatar = Individuo::whereHas('avatar', function($q) use ($documento) {
$q->where('id', $documento->id);
})->count();
if ($linkedToAvatar > 0) {
$isLinked = true;
$allLinks[] = [
'name' => $documento->nome_file . ' → Avatar di ' . $linkedToAvatar . ' individuo(i)',
'url' => null,
];
$linkedCount++;
}
}
if ($documento->eventi()->count() > 0) {
$eventi = $documento->eventi;
foreach ($eventi as $evento) {
$allLinks[] = [
'name' => $documento->nome_file . ' → Evento: ' . $evento->nome_evento,
'url' => '/eventi/' . $evento->id,
];
$linkedCount++;
}
}
if ($isLinked && $documento->tipologia !== 'avatar' && $linkName) {
$allLinks[] = [
'name' => $linkName,
'url' => $linkUrl,
];
$linkedCount++;
}
}
return response()->json([
'linked_count' => $linkedCount,
'links' => $allLinks,
]);
}
public function massDestroy(Request $request)
{
$this->authorizeDelete('documenti');
$idsInput = $request->input('ids', '');
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
if (empty($ids)) {
return back()->with('error', 'Nessun documento selezionato.');
}
$documenti = Documento::whereIn('id', $ids)->get();
foreach ($documenti as $documento) {
if (Storage::disk('local')->exists($documento->file_path)) {
Storage::disk('local')->delete($documento->file_path);
}
$documento->delete();
}
return back()->with('success', count($ids) . ' documenti eliminati.');
}
public function massUpdate(Request $request)
{
$this->authorizeWrite('documenti');
$idsInput = $request->input('ids', '');
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
if (empty($ids)) {
return back()->with('error', 'Nessun documento selezionato.');
}
$data = $request->validate([
'tipologia' => 'nullable|string',
'visibilita' => 'nullable|string',
]);
$tipologia = $data['tipologia'] ?? null;
$visibilita = $data['visibilita'] ?? null;
if (empty($tipologia) && empty($visibilita)) {
return back()->with('error', 'Specificare almeno un campo da aggiornare.');
}
$updateData = [];
if (!empty($tipologia)) {
$updateData['tipologia'] = $tipologia;
}
if (!empty($visibilita)) {
$updateData['visibilita'] = $visibilita;
$typeMap = [
'individuo' => Individuo::class,
'gruppo' => Gruppo::class,
'evento' => Evento::class,
'mailing' => \App\Models\MailingList::class,
];
if (isset($typeMap[$visibilita])) {
$updateData['visibilita_target_type'] = $typeMap[$visibilita];
}
}
Documento::whereIn('id', $ids)->update($updateData);
return back()->with('success', count($ids) . ' documenti aggiornati.');
}
public function massAssociate(Request $request)
{
$this->authorizeWrite('documenti');
$idsInput = $request->input('ids', '');
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
if (empty($ids)) {
return back()->with('error', 'Nessun documento selezionato.');
}
$data = $request->validate([
'target_type' => 'required|in:individuo,gruppo,evento,mailing',
'target_id' => 'required|integer',
]);
$targetType = match($data['target_type']) {
'individuo' => Individuo::class,
'gruppo' => Gruppo::class,
'evento' => Evento::class,
'mailing' => \App\Models\MailingList::class,
};
Documento::whereIn('id', $ids)->update([
'visibilita' => $data['target_type'],
'visibilita_target_id' => $data['target_id'],
'visibilita_target_type' => $targetType,
]);
return back()->with('success', count($ids) . ' documenti associati.');
}
}
+599
View File
@@ -0,0 +1,599 @@
<?php
namespace App\Http\Controllers;
use App\Models\EmailFolder;
use App\Models\EmailMessage;
use App\Models\EmailAttachment;
use App\Models\EmailSetting;
use App\Models\MailingList;
use App\Models\Gruppo;
use App\Models\Individuo;
use App\Models\Documento;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class EmailController extends Controller
{
public function index(Request $request, $folder = 'inbox')
{
$this->authorizeRead('email');
if ($folder === 'compose' || $folder === 'nuovo') {
return $this->compose($request);
}
$folderModel = EmailFolder::where('type', $folder)->first();
if (!$folderModel) {
$folderModel = EmailFolder::where('type', 'inbox')->first();
if (!$folderModel) {
$this->ensureFoldersExist();
$folderModel = EmailFolder::where('type', 'inbox')->first();
}
}
$query = EmailMessage::where('email_folder_id', $folderModel->id);
if ($request->search) {
$query->where(function ($q) use ($request) {
$q->where('subject', 'like', '%' . $request->search . '%')
->orWhere('from_email', 'like', '%' . $request->search . '%')
->orWhere('to_email', 'like', '%' . $request->search . '%')
->orWhere('body_text', 'like', '%' . $request->search . '%');
});
}
$sortField = $request->sort ?? 'received_at';
$sortDir = $request->direction ?? 'desc';
$allowedSorts = ['subject', 'received_at', 'sent_at', 'from_email', 'to_email'];
if (!in_array($sortField, $allowedSorts)) {
$sortField = 'received_at';
}
if (!in_array($sortDir, ['asc', 'desc'])) {
$sortDir = 'desc';
}
$query->orderBy($sortField === 'sent_at' ? 'received_at' : $sortField, $sortDir);
$messages = $query->paginate(20)->withQueryString();
$folders = EmailFolder::orderBy('sort_order')->get();
$mailingLists = MailingList::where('attiva', true)->get();
$gruppi = Gruppo::orderBy('nome')->get();
$emailSettings = EmailSetting::getActive();
$syncInterval = $emailSettings ? $emailSettings->sync_interval_minutes : 0;
return view('email.index', compact('messages', 'folders', 'folder', 'folderModel', 'mailingLists', 'gruppi', 'syncInterval'));
}
public function show($folder, $id)
{
$this->authorizeRead('email');
$message = EmailMessage::with('attachments')->findOrFail($id);
if (!$message->is_read) {
$message->update(['is_read' => true]);
}
$folders = EmailFolder::orderBy('sort_order')->get();
return view('email.show', compact('message', 'folders', 'folder'));
}
public function compose(Request $request)
{
$this->authorizeWrite('email');
$folders = EmailFolder::orderBy('sort_order')->get();
$mailingLists = MailingList::where('attiva', true)->get();
$gruppi = Gruppo::orderBy('nome')->get();
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
$prefill = [];
if ($request->reply_to) {
$original = EmailMessage::find($request->reply_to);
if ($original) {
$prefill['to'] = $original->from_email;
$prefill['subject'] = 'Re: ' . $original->subject;
$prefill['body'] = "\n\n--- Messaggio originale ---\n" . ($original->body_text ?? $original->body_html);
}
}
if ($request->forward) {
$original = EmailMessage::find($request->forward);
if ($original) {
$prefill['subject'] = 'Fwd: ' . $original->subject;
$prefill['body'] = "\n\n--- Messaggio inoltrato ---\n" . ($original->body_text ?? $original->body_html);
}
}
return view('email.compose', compact('folders', 'mailingLists', 'gruppi', 'individui', 'prefill'));
}
public function send(Request $request)
{
$this->authorizeWrite('email');
$rules = [
'subject' => 'required',
'body' => 'required',
];
if ($request->destinatario_tipo === 'manuale' || !$request->destinatario_tipo) {
$rules['to'] = 'required';
}
$validated = $request->validate($rules);
$settings = EmailSetting::getActive();
if (!$settings) {
return back()->with('error', 'Account email non configurato.');
}
$recipients = $this->resolveRecipients($request);
\Illuminate\Support\Facades\Log::info('Email send attempt', [
'recipients' => $recipients,
'recipients_count' => count($recipients),
'subject' => $validated['subject'],
'destinatario_tipo' => $request->destinatario_tipo,
'to' => $validated['to'] ?? null,
'individui' => $request->individui,
'gruppo_id' => $request->gruppo_id,
'mailing_list_id' => $request->mailing_list_id,
]);
if (empty($recipients)) {
return back()->with('error', 'Nessun destinatario trovato. Seleziona almeno un destinatario dal menu a tendina o inserisci un indirizzo email manuale.')->withInput();
}
$imapSuccess = false;
$imapError = null;
foreach ($recipients as $recipient) {
try {
$this->sendViaImap($settings, $recipient, $validated['subject'], $validated['body'], $request->cc);
$imapSuccess = true;
} catch (\Exception $e) {
$imapError = $e->getMessage();
\Illuminate\Support\Facades\Log::error('IMAP send error', ['error' => $e->getMessage()]);
}
}
try {
$this->storeSentMessage($settings, $recipients, $validated['subject'], $validated['body'], $request->cc);
\Illuminate\Support\Facades\Log::info('Stored sent message');
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Store sent message error', ['error' => $e->getMessage()]);
return back()->with('error', 'Errore nel salvataggio: ' . $e->getMessage())->withInput();
}
if (!$imapSuccess && $imapError) {
return redirect()->route('email.index', 'sent')->with('warning', 'Email salvata in Invio ma non inviata via IMAP: ' . $imapError);
}
return redirect()->route('email.index', 'sent')->with('success', 'Email inviata con successo.');
}
public function storeDraft(Request $request)
{
$this->authorizeWrite('email');
$settings = EmailSetting::getActive();
$folder = EmailFolder::where('type', 'drafts')->first();
$message = EmailMessage::create([
'email_folder_id' => $folder->id,
'subject' => $request->subject ?? '(senza oggetto)',
'body_text' => $request->body,
'to_email' => $request->to,
'is_draft' => true,
'from_email' => $settings->email_address ?? '',
'from_name' => $settings->email_name ?? '',
]);
return response()->json(['success' => true, 'draft_id' => $message->id]);
}
public function bulkAction(Request $request)
{
try {
$this->authorizeWrite('email');
} catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) {
return response()->json([
'success' => false,
'message' => 'Non hai i permessi per questa azione'
], 403);
}
$ids = $request->input('ids', []);
$action = $request->input('action');
if ($action === 'empty_trash') {
$trashFolder = EmailFolder::where('type', 'trash')->first();
if ($trashFolder) {
EmailMessage::where('email_folder_id', $trashFolder->id)->delete();
}
return response()->json(['success' => true]);
}
if (empty($ids)) {
return response()->json(['success' => false, 'message' => 'Nessuna email selezionata']);
}
$messages = EmailMessage::whereIn('id', $ids)->get();
switch ($action) {
case 'mark_read':
$messages->each->update(['is_read' => true]);
break;
case 'mark_unread':
$messages->each->update(['is_read' => false]);
break;
case 'star':
$messages->each->update(['is_starred' => true]);
break;
case 'unstar':
$messages->each->update(['is_starred' => false]);
break;
case 'move_trash':
$trashFolder = EmailFolder::where('type', 'trash')->first();
if ($trashFolder) {
$messages->each->update(['email_folder_id' => $trashFolder->id]);
}
break;
case 'delete_permanent':
$messages->each->delete();
break;
default:
return response()->json(['success' => false, 'message' => 'Azione non valida']);
}
return response()->json(['success' => true]);
}
public function markRead($id)
{
try {
$this->authorizeWrite('email');
} catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) {
return response()->json([
'success' => false,
'message' => 'Non hai i permessi per questa azione'
], 403);
}
$message = EmailMessage::findOrFail($id);
$message->update(['is_read' => true]);
return response()->json(['success' => true]);
}
public function toggleStar($id)
{
try {
$this->authorizeWrite('email');
} catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) {
return response()->json([
'success' => false,
'message' => 'Non hai i permessi per questa azione'
], 403);
}
$message = EmailMessage::findOrFail($id);
$message->update(['is_starred' => !$message->is_starred]);
return response()->json(['success' => true, 'is_starred' => $message->is_starred]);
}
public function destroy($id)
{
$this->authorizeDelete('email');
$message = EmailMessage::findOrFail($id);
$trashFolder = EmailFolder::where('type', 'trash')->first();
if ($trashFolder) {
$message->update(['is_trash' => true, 'email_folder_id' => $trashFolder->id]);
} else {
$message->delete();
}
return back()->with('success', 'Email spostata nel cestino.');
}
public function downloadAttachment($id)
{
$this->authorizeRead('email');
$attachment = EmailAttachment::findOrFail($id);
if (!Storage::exists($attachment->file_path)) {
abort(404);
}
return Storage::download($attachment->file_path, $attachment->filename);
}
public function saveAttachmentToDocumenti(Request $request, $id, $attachmentId)
{
$this->authorizeWrite('email');
$this->authorizeWrite('documenti');
$attachment = EmailAttachment::findOrFail($attachmentId);
$documento = Documento::create([
'nome_file' => $attachment->filename,
'file_path' => $attachment->file_path,
'tipo' => 'upload',
'tipologia' => 'email_attachment',
'mime_type' => $attachment->mime_type,
'dimensione' => $attachment->size,
'visibilita' => null,
'note' => 'Allegato email: ' . ($attachment->message->subject ?? ''),
]);
$attachment->update(['is_saved_to_documenti' => true, 'documenti_id' => $documento->id]);
return back()->with('success', 'Allegato salvato nei documenti.');
}
public function quickSync()
{
try {
$this->syncEmails();
return response()->json(['success' => true, 'message' => 'Sincronizzazione completata']);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => $e->getMessage()]);
}
}
public function syncEmails()
{
$settings = EmailSetting::getActive();
if (!$settings) {
throw new \Exception('Nessuna configurazione email attiva');
}
$this->ensureFoldersExist();
$mailbox = $this->connectImap($settings);
$folderMappings = [
'INBOX' => 'inbox',
'Sent' => 'sent',
'[Gmail]/Sent Mail' => 'sent',
'Sent Mail' => 'sent',
'Drafts' => 'drafts',
'[Gmail]/Drafts' => 'drafts',
'Trash' => 'trash',
'[Gmail]/Trash' => 'trash',
];
foreach (array_keys($folderMappings) as $imapFolderName) {
try {
$folder = $mailbox->folders()->find($imapFolderName);
if (!$folder) continue;
$messages = $folder->messages()
->withHeaders()
->withBody()
->withBodyStructure()
->limit(100)
->get();
foreach ($messages as $imapMessage) {
$this->processImapMessage($imapMessage, $imapFolderName);
}
} catch (\Exception $e) {
continue;
}
}
return true;
}
private function resolveRecipients(Request $request)
{
$recipients = [];
if ($request->destinatario_tipo === 'individui' && $request->individui) {
$individui = Individuo::whereIn('id', $request->individui)->get();
foreach ($individui as $ind) {
$emails = $ind->contatti()->where('tipo', 'email')->pluck('valore')->toArray();
$recipients = array_merge($recipients, $emails);
}
} elseif ($request->destinatario_tipo === 'gruppo' && $request->gruppo_id) {
$gruppo = Gruppo::with('individui.contatti')->find($request->gruppo_id);
foreach ($gruppo->individui as $ind) {
$emails = $ind->contatti()->where('tipo', 'email')->pluck('valore')->toArray();
$recipients = array_merge($recipients, $emails);
}
} elseif ($request->destinatario_tipo === 'mailing_list' && $request->mailing_list_id) {
$list = MailingList::with('contatti.individuo')->find($request->mailing_list_id);
foreach ($list->contatti as $contact) {
if ($contact->opt_in && $contact->individuo) {
$emails = $contact->individuo->contatti()->where('tipo', 'email')->pluck('valore')->toArray();
$recipients = array_merge($recipients, $emails);
}
}
} elseif ($request->to) {
$recipients = is_array($request->to) ? $request->to : [$request->to];
}
return array_filter(array_unique($recipients));
}
private function appendSignature(string $body, ?string $signature, bool $enabled): string
{
if (empty($signature) || !$enabled) {
return $body;
}
return $body . "\n\n---\n" . $signature;
}
private function sendViaImap($settings, $to, $subject, $body, $cc = null)
{
$bodyWithSignature = $this->appendSignature($body, $settings->getSignature(), $settings->signature_enabled ?? false);
\Illuminate\Support\Facades\Log::info('sendViaSMTP start', ['to' => $to, 'subject' => $subject]);
$smtpPassword = $settings->getDecryptedSmtpPassword() ?? $settings->getDecryptedPassword();
$encryption = $settings->smtp_encryption ?? 'tls';
$scheme = ($encryption === 'ssl') ? 'smtps' : 'smtp';
$dsn = sprintf(
'%s://%s:%s@%s:%d?encryption=%s',
$scheme,
rawurlencode($settings->smtp_username ?? $settings->email_address),
rawurlencode($smtpPassword),
$settings->smtp_host ?? 'smtp.gmail.com',
$settings->smtp_port ?? 587,
$encryption
);
\Illuminate\Support\Facades\Log::info('SMTP DSN', ['dsn' => $dsn]);
try {
$transport = \Symfony\Component\Mailer\Transport::fromDsn($dsn);
$mailer = new \Symfony\Component\Mailer\Mailer($transport);
$fromName = $settings->email_name ?? '';
if (filter_var($fromName, FILTER_VALIDATE_EMAIL)) {
$fromName = 'Glastree';
} else {
$fromName = preg_replace('/[^\p{L}\p{N}\s]/u', '', $fromName);
$fromName = trim($fromName);
if (empty($fromName)) {
$fromName = 'Glastree';
}
}
$fromAddress = \Symfony\Component\Mime\Address::create($settings->email_address, $fromName);
$email = (new \Symfony\Component\Mime\Email())
->from($fromAddress)
->to($to)
->subject($subject)
->text($bodyWithSignature);
if ($cc) {
$email->cc($cc);
}
$mailer->send($email);
\Illuminate\Support\Facades\Log::info('Email sent via SMTP successfully');
return true;
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('SMTP send error', ['error' => $e->getMessage()]);
throw new \Exception('Errore invio SMTP: ' . $e->getMessage());
}
}
private function storeSentMessage($settings, $recipients, $subject, $body, $cc = null)
{
$sentFolder = EmailFolder::where('type', 'sent')->first();
$bodyWithSignature = $this->appendSignature($body, $settings->getSignature(), $settings->signature_enabled ?? false);
$message = EmailMessage::create([
'email_folder_id' => $sentFolder->id,
'subject' => $subject,
'body_text' => $bodyWithSignature,
'from_email' => $settings->email_address,
'from_name' => $settings->email_name,
'to_email' => implode(', ', $recipients),
'cc' => $cc,
'is_sent' => true,
'sent_at' => now(),
'is_read' => true,
]);
return $message;
}
private function connectImap($settings)
{
if (!$settings->imap_host || !$settings->imap_username) {
throw new \Exception('Configurazione IMAP non valida');
}
$encryption = match ($settings->imap_encryption) {
'ssl' => 'ssl',
'tls' => 'tls',
default => 'none',
};
return new \DirectoryTree\ImapEngine\Mailbox([
'host' => $settings->imap_host,
'port' => $settings->imap_port ?? 993,
'encryption' => $encryption,
'validate_cert' => false,
'username' => $settings->imap_username,
'password' => $settings->getDecryptedPassword(),
]);
}
private function processImapMessage($imapMessage, $imapFolder)
{
$folderType = match ($imapFolder) {
'INBOX', 'inbox' => 'inbox',
'Sent', 'sent', '[Gmail]/Sent Mail' => 'sent',
'Drafts', 'drafts', '[Gmail]/Drafts' => 'drafts',
'Trash', 'trash', '[Gmail]/Trash' => 'trash',
'Archive', 'archive', '[Gmail]/All Mail' => 'archive',
'Starred', 'starred', '[Gmail]/Starred' => 'starred',
default => 'inbox',
};
$folder = EmailFolder::where('type', $folderType)->first();
if (!$folder) return;
$messageId = $imapMessage->messageId() ?? uniqid('msg_');
$existing = EmailMessage::where('message_id', $messageId)->first();
if ($existing) return;
$from = $imapMessage->from();
$to = $imapMessage->to();
$message = EmailMessage::create([
'email_folder_id' => $folder->id,
'message_id' => $messageId,
'subject' => $imapMessage->subject() ?? '(senza oggetto)',
'from_name' => $from?->name() ?? '',
'from_email' => $from?->email() ?? '',
'to_email' => count($to) > 0 ? $to[0]->email() : '',
'body_text' => $imapMessage->text() ?? '',
'body_html' => $imapMessage->html() ?? '',
'is_read' => $imapMessage->isSeen() ?? false,
'is_starred' => $imapMessage->isFlagged() ?? false,
'is_sent' => $folderType === 'sent',
'received_at' => $imapMessage->date() ?? now(),
'imap_uid' => $imapMessage->uid() ?? null,
]);
$attachments = $imapMessage->attachments();
if ($attachments && count($attachments) > 0) {
foreach ($attachments as $attachment) {
$filename = $attachment->filename() ?? 'attachment';
$contents = $attachment->contents();
$path = 'email-attachments/' . Str::uuid() . '_' . $filename;
Storage::put($path, $contents);
EmailAttachment::create([
'email_message_id' => $message->id,
'filename' => $filename,
'mime_type' => $attachment->contentType() ?? 'application/octet-stream',
'size' => strlen($contents),
'file_path' => $path,
]);
}
}
}
private function ensureFoldersExist()
{
$systemFolders = EmailFolder::getSystemFolders();
foreach ($systemFolders as $folder) {
EmailFolder::updateOrCreate(
['type' => $folder['type']],
$folder
);
}
}
}
+296
View File
@@ -0,0 +1,296 @@
<?php
namespace App\Http\Controllers;
use App\Models\Evento;
use App\Models\Gruppo;
use App\Models\Individuo;
use Illuminate\Http\Request;
class EventoController extends Controller
{
public function index(Request $request)
{
$this->authorizeRead('eventi');
$query = Evento::with(['gruppi', 'responsabili']);
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);
}
$sortBy = $request->get('sort', 'created_at');
$sortDir = $request->get('direction', 'desc');
$allowedSorts = ['nome_evento', 'descrizione_evento', 'tipo_recorrenza', 'data_specifica', 'created_at'];
if ($sortBy === 'gruppi') {
$query->withCount('gruppi')->orderBy('gruppi_count', $sortDir === 'asc' ? 'asc' : 'desc');
} elseif ($sortBy === 'responsabili') {
$query->withCount('responsabili')->orderBy('responsabili_count', $sortDir === 'asc' ? 'asc' : 'desc');
} elseif (in_array($sortBy, $allowedSorts)) {
$query->orderBy($sortBy, $sortDir === 'asc' ? 'asc' : 'desc');
} else {
$query->orderByDesc('created_at');
}
$eventi = $query->paginate(20);
$gruppi = Gruppo::orderBy('nome')->get();
return view('eventi.index', compact('eventi', 'gruppi'));
}
public function create(Request $request)
{
$this->authorizeWrite('eventi');
$gruppi = Gruppo::orderBy('nome')->get();
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
$selectedGruppo = $request->query('gruppo_id') ? Gruppo::find($request->query('gruppo_id')) : null;
return view('eventi.create', compact('gruppi', 'individui', 'selectedGruppo'));
}
public function store(Request $request)
{
$this->authorizeWrite('eventi');
$data = $request->validate([
'nome_evento' => 'required|string|max:255',
'descrizione_evento' => 'nullable|string|max:255',
'tipo_evento' => 'nullable|string|max:100',
'tipo_recorrenza' => 'nullable|in:singolo,settimanale,mensile,annuale,altro',
'giorno_settimana' => 'nullable|integer|min:0|max:6',
'giorno_mese' => 'nullable|integer|min:1|max:31',
'occorrenza_mese' => 'nullable|string|max:10',
'mesi_recorrenza' => 'nullable|string',
'mese_annuale' => 'nullable|integer|min:1|max:12',
'ora_inizio' => 'nullable|date_format:H:i',
'data_specifica' => 'nullable|date',
'durata_minuti' => 'nullable|integer|min:1',
'descrizione' => 'nullable|string',
'note' => 'nullable|string',
'is_incontro_gruppo' => 'nullable|boolean',
'gruppi' => 'nullable|array',
'gruppi.*' => 'exists:gruppi,id',
'responsabili' => 'nullable|array',
'responsabili.*' => 'exists:individui,id',
'luogo_indirizzo' => 'nullable|string|max:500',
'luogo_url_maps' => 'nullable|string',
]);
$evento = Evento::create([
'nome_evento' => $data['nome_evento'],
'descrizione_evento' => $data['descrizione_evento'] ?? null,
'tipo_evento' => $data['tipo_evento'] ?? null,
'tipo_recorrenza' => $data['tipo_recorrenza'] ?? 'singolo',
'giorno_settimana' => $data['giorno_settimana'] ?? null,
'giorno_mese' => $data['giorno_mese'] ?? null,
'occorrenza_mese' => $data['occorrenza_mese'] ?? null,
'mesi_recorrenza' => is_array($request->mesi_recorrenza) ? implode(',', $request->mesi_recorrenza) : ($data['mesi_recorrenza'] ?? null),
'mese_annuale' => $data['mese_annuale'] ?? null,
'ora_inizio' => $data['ora_inizio'] ?? null,
'data_specifica' => $data['data_specifica'] ?? null,
'durata_minuti' => $data['durata_minuti'] ?? null,
'descrizione' => $data['descrizione'] ?? null,
'note' => $data['note'] ?? null,
'is_incontro_gruppo' => $request->boolean('is_incontro_gruppo'),
'luogo_indirizzo' => $data['luogo_indirizzo'] ?? null,
'luogo_url_maps' => $data['luogo_url_maps'] ?? null,
]);
if (!empty($data['gruppi'])) {
$evento->gruppi()->attach($data['gruppi']);
}
if (!empty($data['responsabili'])) {
$evento->responsabili()->attach($data['responsabili']);
}
return redirect('/eventi/' . $evento->id)->with('success', 'Evento creato con successo.');
}
public function show($evento)
{
$this->authorizeRead('eventi');
$evento = Evento::with(['gruppi', 'responsabili.contatti', 'documenti'])->findOrFail($evento);
return view('eventi.show', compact('evento'));
}
public function edit($evento)
{
$this->authorizeWrite('eventi');
$evento = Evento::with(['gruppi', 'responsabili', 'documenti'])->findOrFail($evento);
$gruppi = Gruppo::orderBy('nome')->get();
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
return view('eventi.edit', compact('evento', 'gruppi', 'individui'));
}
public function update(Request $request, $evento)
{
$this->authorizeWrite('eventi');
$evento = Evento::findOrFail($evento);
$data = $request->validate([
'nome_evento' => 'required|string|max:255',
'descrizione_evento' => 'nullable|string|max:255',
'tipo_evento' => 'nullable|string|max:100',
'tipo_recorrenza' => 'nullable|in:singolo,settimanale,mensile,annuale,altro',
'giorno_settimana' => 'nullable|integer|min:0|max:6',
'giorno_mese' => 'nullable|integer|min:1|max:31',
'occorrenza_mese' => 'nullable|string|max:10',
'mesi_recorrenza' => 'nullable|string',
'mese_annuale' => 'nullable|integer|min:1|max:12',
'ora_inizio' => 'nullable|date_format:H:i',
'data_specifica' => 'nullable|date',
'durata_minuti' => 'nullable|integer|min:1',
'descrizione' => 'nullable|string',
'note' => 'nullable|string',
'is_incontro_gruppo' => 'nullable|boolean',
'gruppi' => 'nullable|array',
'gruppi.*' => 'exists:gruppi,id',
'responsabili' => 'nullable|array',
'responsabili.*' => 'exists:individui,id',
'luogo_indirizzo' => 'nullable|string|max:500',
'luogo_url_maps' => 'nullable|string',
]);
$evento->update([
'nome_evento' => $data['nome_evento'],
'descrizione_evento' => $data['descrizione_evento'] ?? null,
'tipo_evento' => $data['tipo_evento'] ?? null,
'tipo_recorrenza' => $data['tipo_recorrenza'] ?? 'singolo',
'giorno_settimana' => $data['giorno_settimana'] ?? null,
'giorno_mese' => $data['giorno_mese'] ?? null,
'occorrenza_mese' => $data['occorrenza_mese'] ?? null,
'mesi_recorrenza' => is_array($request->mesi_recorrenza) ? implode(',', $request->mesi_recorrenza) : ($data['mesi_recorrenza'] ?? null),
'mese_annuale' => $data['mese_annuale'] ?? null,
'ora_inizio' => $data['ora_inizio'] ?? null,
'data_specifica' => $data['data_specifica'] ?? null,
'durata_minuti' => $data['durata_minuti'] ?? null,
'descrizione' => $data['descrizione'] ?? null,
'note' => $data['note'] ?? null,
'is_incontro_gruppo' => $request->boolean('is_incontro_gruppo'),
'luogo_indirizzo' => $data['luogo_indirizzo'] ?? null,
'luogo_url_maps' => $data['luogo_url_maps'] ?? null,
]);
$evento->gruppi()->sync($data['gruppi'] ?? []);
$evento->responsabili()->sync($data['responsabili'] ?? []);
return redirect('/eventi/' . $evento->id)->with('success', 'Evento aggiornato.');
}
public function destroy($evento)
{
$this->authorizeDelete('eventi');
$evento = Evento::findOrFail($evento);
$evento->delete();
return redirect('/eventi')->with('success', 'Evento eliminato.');
}
public function massElimina(Request $request)
{
$this->authorizeDelete('eventi');
$ids = $request->input('ids', []);
if (!is_array($ids) || empty($ids)) {
return redirect('/eventi')->with('error', 'Nessun evento selezionato.');
}
$eventi = Evento::whereIn('id', $ids)->get();
foreach ($eventi as $evento) {
$evento->gruppi()->detach();
$evento->responsabili()->detach();
$evento->delete();
}
return redirect('/eventi')->with('success', count($eventi) . ' eventi eliminati con successo.');
}
public function calendar()
{
$this->authorizeRead('eventi');
return view('eventi.calendar');
}
public function calendarEvents(Request $request)
{
$this->authorizeRead('eventi');
$start = $request->input('start');
$end = $request->input('end');
$baseUrl = $request->getSchemeAndHttpHost();
$query = Evento::with(['gruppi', 'responsabili']);
$eventi = $query->get();
$events = [];
foreach ($eventi as $evento) {
if ($evento->tipo_recorrenza === 'singolo' || $evento->tipo_recorrenza === null) {
if ($evento->data_specifica) {
$events[] = [
'id' => $evento->id,
'title' => $evento->nome_evento,
'start' => $evento->data_specifica->format('Y-m-d') . ($evento->ora_inizio ? 'T' . $evento->ora_inizio->format('H:i') : ''),
'url' => $baseUrl . '/eventi/' . $evento->id,
'backgroundColor' => $this->getEventoColor($evento),
'borderColor' => $this->getEventoColor($evento),
];
}
} elseif ($evento->tipo_recorrenza === 'settimanale' && $evento->giorno_settimana !== null) {
$startDate = \Carbon\Carbon::parse($start);
$endDate = \Carbon\Carbon::parse($end);
$current = $startDate->copy();
while ($current <= $endDate) {
$nextOccurrence = $current->copy()->next($evento->giorno_settimana);
if ($nextOccurrence >= $startDate && $nextOccurrence <= $endDate) {
$events[] = [
'id' => $evento->id . '_' . $nextOccurrence->format('Ymd'),
'title' => $evento->nome_evento,
'start' => $nextOccurrence->format('Y-m-d') . ($evento->ora_inizio ? 'T' . $evento->ora_inizio->format('H:i') : ''),
'url' => $baseUrl . '/eventi/' . $evento->id,
'backgroundColor' => $this->getEventoColor($evento),
'borderColor' => $this->getEventoColor($evento),
];
}
$current->addWeek();
}
}
}
return response()->json($events);
}
private function getEventoColor(Evento $evento): string
{
$colors = [
'primary' => '#007bff',
'secondary' => '#6c757d',
'success' => '#28a745',
'danger' => '#dc3545',
'warning' => '#ffc107',
'info' => '#17a2b8',
'dark' => '#343a40',
];
if ($evento->isIncroGruppo()) {
return $colors['info'];
}
if ($evento->isRicorrente()) {
return $colors['success'];
}
return $colors['primary'];
}
}
@@ -0,0 +1,64 @@
<?php
namespace App\Http\Controllers;
use App\Models\Evento;
use App\Models\Documento;
use Illuminate\Http\Request;
class EventoDocumentoController extends Controller
{
public function store(Request $request, $evento)
{
$this->authorizeWrite('documenti');
$this->authorizeWrite('eventi');
$evento = Evento::findOrFail($evento);
$request->validate([
'nome_file' => 'required|string|max:255',
'tipologia' => 'required|in:documento,programma,locandina,altro',
'file' => 'required|file|max:10240',
]);
$file = $request->file('file');
$path = $file->store('documenti/eventi', 'public');
$documento = Documento::create([
'nome_file' => $request->nome_file,
'tipologia' => $request->tipologia,
'file_path' => $path,
'mime_type' => $file->getMimeType(),
'dimensione' => $file->getSize(),
'visibilita' => 'evento',
'visibilita_target_id' => $evento->id,
'visibilita_target_type' => Evento::class,
]);
$evento->documenti()->attach($documento->id);
return redirect()->back()->with('success', 'Documento caricato con successo.');
}
public function destroy($evento, $documento)
{
$this->authorizeDelete('documenti');
$this->authorizeWrite('eventi');
$evento = Evento::findOrFail($evento);
$documento = Documento::findOrFail($documento);
if ($evento->documenti()->where('documenti.id', $documento->id)->exists()) {
$evento->documenti()->detach($documento->id);
}
if ($documento->visibilita === 'evento' && $documento->visibilita_target_id == $evento->id) {
if ($documento->eventi()->count() === 0 && $documento->gruppi()->count() === 0 && $documento->individui()->count() === 0) {
if ($documento->file_path && \Storage::disk('public')->exists($documento->file_path)) {
\Storage::disk('public')->delete($documento->file_path);
}
$documento->delete();
}
}
return redirect()->back()->with('success', 'Documento rimosso.');
}
}
+405
View File
@@ -0,0 +1,405 @@
<?php
namespace App\Http\Controllers;
use App\Models\Gruppo;
use App\Models\Individuo;
use App\Models\Diocesi;
use Illuminate\Http\Request;
class GruppoController extends Controller
{
public function index()
{
$this->authorizeRead('gruppi');
$viewMode = request('view', 'table');
$user = auth()->user();
$vista = \App\Models\VistaReport::where('user_id', $user->id)
->where('tipo', 'gruppi')
->where('is_default', true)
->first();
$allColumns = ['nome', 'descrizione', 'diocesi', 'livello', 'padre', 'membri', 'responsabili', 'indirizzo', 'citta', 'telefono', 'email'];
$defaultVisible = ['nome', 'livello', 'padre', 'membri', 'responsabili'];
if ($vista && !empty($vista->colonne_visibili)) {
$visibleColumns = $vista->colonne_visibili;
} else {
$visibleColumns = $defaultVisible;
}
$allGruppi = Gruppo::with(['diocesi', 'parent', 'children', 'individui'])
->orderBy('nome')
->get()
->map(function ($gruppo) {
$depth = 0;
$parent = $gruppo->parent;
while ($parent) {
$depth++;
$parent = $parent->parent;
}
$gruppo->depth = $depth;
return $gruppo;
});
if ($viewMode === 'table') {
$gruppi = $this->sortGruppiHierarchically($allGruppi);
} else {
$gruppi = $allGruppi;
}
return view('gruppi.index', compact(
'gruppi',
'allColumns',
'visibleColumns',
'viewMode',
'vista'
));
}
protected function sortGruppiHierarchically($gruppi)
{
$rootGroups = $gruppi->filter(fn($g) => $g->parent_id === null)->sortBy('nome');
$sorted = collect();
foreach ($rootGroups as $root) {
$sorted->push($root);
$this->addChildrenRecursive($root, $gruppi, $sorted);
}
return $sorted;
}
protected function addChildrenRecursive($parent, $allGruppi, &$sorted)
{
$children = $allGruppi->filter(fn($g) => $g->parent_id === $parent->id)->sortBy('nome');
foreach ($children as $child) {
$sorted->push($child);
$this->addChildrenRecursive($child, $allGruppi, $sorted);
}
}
$allGruppi = Gruppo::with(['diocesi', 'parent', 'children', 'individui'])
->orderBy('nome')
->get()
->map(function ($gruppo) {
$depth = 0;
$parent = $gruppo->parent;
while ($parent) {
$depth++;
$parent = $parent->parent;
}
$gruppo->depth = $depth;
return $gruppo;
});
if ($viewMode === 'table') {
$gruppi = $this->sortGruppiHierarchically($allGruppi);
} else {
$gruppi = $allGruppi;
}
return view('gruppi.index', compact(
'gruppi',
'allColumns',
'visibleColumns',
'viewMode',
'vista'
));
}
protected function sortGruppiHierarchically($gruppi)
{
$rootGroups = $gruppi->filter(fn($g) => $g->parent_id === null)->sortBy('nome');
$sorted = collect();
foreach ($rootGroups as $root) {
$sorted->push($root);
$this->addChildrenRecursive($root, $gruppi, $sorted);
}
return $sorted;
}
protected function addChildrenRecursive($parent, $allGruppi, &$sorted)
{
$children = $allGruppi->filter(fn($g) => $g->parent_id === $parent->id)->sortBy('nome');
foreach ($children as $child) {
$sorted->push($child);
$this->addChildrenRecursive($child, $allGruppi, $sorted);
}
}
$allGruppi = Gruppo::with(['diocesi', 'parent', 'children', 'individui'])
->orderBy('nome')
->get()
->map(function ($gruppo) {
$depth = 0;
$parent = $gruppo->parent;
while ($parent) {
$depth++;
$parent = $parent->parent;
}
$gruppo->depth = $depth;
return $gruppo;
});
if ($viewMode === 'table') {
$gruppi = $this->sortGruppiHierarchically($allGruppi);
} else {
$gruppi = $allGruppi;
}
return view('gruppi.index', compact(
'gruppi',
'allColumns',
'visibleColumns',
'viewMode',
'vista'
));
}
protected function sortGruppiHierarchically($gruppi)
{
$rootGroups = $gruppi->filter(fn($g) => $g->parent_id === null)->sortBy('nome');
$sorted = collect();
foreach ($rootGroups as $root) {
$sorted->push($root);
$this->addChildrenRecursive($root, $gruppi, $sorted);
}
return $sorted;
}
protected function addChildrenRecursive($parent, $allGruppi, &$sorted)
{
$children = $allGruppi->filter(fn($g) => $g->parent_id === $parent->id)->sortBy('nome');
foreach ($children as $child) {
$sorted->push($child);
$this->addChildrenRecursive($child, $allGruppi, $sorted);
}
}
public function create(Request $request)
{
$this->authorizeWrite('gruppi');
$diocesi = Diocesi::orderBy('nome')->get();
$allGruppi = Gruppo::orderBy('nome')->get();
$selectedParent = $request->query('parent_id') ? Gruppo::find($request->query('parent_id')) : null;
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
return view('gruppi.create', compact('diocesi', 'allGruppi', 'selectedParent', 'individui'));
}
public function store(Request $request)
{
$this->authorizeWrite('gruppi');
$data = $request->validate([
'nome' => 'required|string|max:255',
'descrizione' => 'nullable|string',
'parent_id' => 'nullable|exists:gruppi,id',
'diocesi_id' => 'nullable|exists:diocesi,id',
'responsabile_ids' => 'nullable|array',
'responsabile_ids.*' => 'exists:individui,id',
'indirizzo_incontro' => 'nullable|string|max:500',
'cap_incontro' => 'nullable|string|max:10',
'città_incontro' => 'nullable|string|max:255',
'sigla_provincia_incontro' => 'nullable|string|max:2',
'mappa_posizione' => 'nullable|string',
]);
if (!empty($data['responsabile_ids'])) {
$data['responsabile_ids'] = json_encode(array_values($data['responsabile_ids']));
}
$gruppo = Gruppo::create($data);
if ($request->has('individui')) {
foreach ($request->individui as $individuoData) {
if (!empty($individuoData['individuo_id'])) {
$gruppo->individui()->attach($individuoData['individuo_id'], [
'ruolo_nel_gruppo' => $individuoData['ruolo_nel_gruppo'] ?? null,
'data_adesione' => $individuoData['data_adesione'] ?? null,
]);
}
}
}
return redirect('/gruppi/' . $gruppo->id)->with('success', 'Gruppo creato con successo.');
}
public function show($gruppo)
{
$this->authorizeRead('gruppi');
$gruppo = Gruppo::with(['parent', 'children', 'diocesi', 'individui.contatti', 'individui.documenti', 'individui' => function ($q) {
$q->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione');
}, 'eventi' => function ($q) {
$q->where('is_incontro_gruppo', true);
}, 'eventi.responsabili.contatti'])->findOrFail($gruppo);
return view('gruppi.show', compact('gruppo'));
}
public function edit($gruppo)
{
$this->authorizeWrite('gruppi');
$gruppo = Gruppo::with(['individui.contatti', 'individui.documenti'])->findOrFail($gruppo);
$diocesi = Diocesi::orderBy('nome')->get();
$gruppi = Gruppo::where('id', '!=', $gruppo->id)->orderBy('nome')->get();
$membri = $gruppo->individui()->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione')->orderBy('cognome')->orderBy('nome')->get();
return view('gruppi.edit', compact('gruppo', 'diocesi', 'gruppi', 'membri'));
}
public function update(Request $request, $gruppo)
{
$this->authorizeWrite('gruppi');
$gruppo = Gruppo::findOrFail($gruppo);
$data = $request->validate([
'nome' => 'required|string|max:255',
'descrizione' => 'nullable|string',
'parent_id' => 'nullable|exists:gruppi,id',
'diocesi_id' => 'nullable|exists:diocesi,id',
'responsabile_ids' => 'nullable|array',
'responsabile_ids.*' => 'exists:individui,id',
'indirizzo_incontro' => 'nullable|string|max:500',
'cap_incontro' => 'nullable|string|max:10',
'città_incontro' => 'nullable|string|max:255',
'sigla_provincia_incontro' => 'nullable|string|max:2',
'mappa_posizione' => 'nullable|string',
]);
if (!empty($data['responsabile_ids'])) {
$data['responsabile_ids'] = json_encode(array_values($data['responsabile_ids']));
} else {
$data['responsabile_ids'] = null;
}
$gruppo->update($data);
if ($request->has('individui')) {
$gruppo->individui()->detach();
foreach ($request->individui as $individuoData) {
if (!empty($individuoData['individuo_id'])) {
$gruppo->individui()->attach($individuoData['individuo_id'], [
'ruolo_ids' => !empty($individuoData['ruolo_ids']) ? json_encode(array_values($individuoData['ruolo_ids'])) : null,
'data_adesione' => $individuoData['data_adesione'] ?? null,
]);
}
}
}
return redirect('/gruppi/' . $gruppo->id)->with('success', 'Gruppo aggiornato.');
}
public function destroy($gruppo)
{
$this->authorizeDelete('gruppi');
$gruppo = Gruppo::with('children', 'individui')->findOrFail($gruppo);
$hasChildren = $gruppo->children()->count() > 0;
$isSuperAdmin = auth()->user()->isSuperAdmin();
if ($hasChildren && !$isSuperAdmin) {
return redirect('/gruppi')->with('error', 'Solo il superamministratore può eliminare un gruppo con sottogruppi.');
}
if ($hasChildren && $isSuperAdmin) {
foreach ($gruppo->children as $child) {
$this->destroyGruppo($child);
}
}
$this->destroyGruppo($gruppo);
return redirect('/gruppi')->with('success', 'Gruppo eliminato.');
}
protected function destroyGruppo(Gruppo $gruppo): void
{
$gruppo->individui()->detach();
$gruppo->eventi()->detach();
foreach ($gruppo->documenti as $documento) {
if ($documento->file_path && file_exists(storage_path('app/public/' . $documento->file_path))) {
unlink(storage_path('app/public/' . $documento->file_path));
}
}
$gruppo->documenti()->delete();
$gruppo->delete();
}
public function allGruppi()
{
$this->authorizeRead('gruppi');
$gruppi = Gruppo::select('id', 'nome')
->orderBy('nome')
->get()
->map(function($g) {
return [
'id' => $g->id,
'nome' => $g->nome,
'full_path' => $g->getFullPathAttribute(),
];
});
return response()->json($gruppi);
}
public function individuiEmail(Gruppo $gruppo)
{
$this->authorizeRead('gruppi');
$individui = $gruppo->individui()->with('contatti')->get();
$result = $individui->map(function($ind) {
$emails = $ind->contatti->where('tipo', 'email')->pluck('valore')->toArray();
return [
'id' => $ind->id,
'codice_id' => $ind->codice_id,
'cognome' => $ind->cognome,
'nome' => $ind->nome,
'emails' => $emails,
];
});
return response()->json($result);
}
public function saveVista(Request $request)
{
$data = $request->validate([
'nome' => 'required|string|max:255',
'colonne_visibili' => 'nullable|array',
'colonne_visibili.*' => 'string',
'is_default' => 'nullable|boolean',
]);
if (!empty($data['is_default'])) {
\App\Models\VistaReport::where('user_id', auth()->id())
->where('tipo', 'gruppi')
->where('is_default', true)
->update(['is_default' => false]);
}
$vista = \App\Models\VistaReport::create([
'user_id' => auth()->id(),
'nome' => $data['nome'],
'tipo' => 'gruppi',
'colonne_visibili' => $data['colonne_visibili'] ?? [],
'is_default' => !empty($data['is_default']),
]);
return response()->json(['success' => true, 'vista_id' => $vista->id]);
}
public function deleteVista($vista)
{
$vista = \App\Models\VistaReport::where('user_id', auth()->id())
->where('id', $vista)
->firstOrFail();
$vista->delete();
return redirect()->route('gruppi.index')->with('success', 'Vista eliminata.');
}
}
@@ -0,0 +1,75 @@
<?php
namespace App\Http\Controllers;
use App\Models\Gruppo;
use Illuminate\Http\Request;
class GruppoIndividuoController extends Controller
{
public function store(Request $request, $individuo)
{
$this->authorizeWrite('individui');
$this->authorizeWrite('gruppi');
$data = $request->validate([
'gruppo_id' => 'required|exists:gruppi,id',
'ruolo_ids' => 'nullable|array',
'ruolo_ids.*' => 'exists:ruoli,id',
'data_adesione' => 'nullable|date',
]);
$individuo = \App\Models\Individuo::findOrFail($individuo);
$gruppo = Gruppo::findOrFail($data['gruppo_id']);
if ($individuo->gruppi->contains($gruppo->id)) {
if ($request->expectsJson()) {
return response()->json(['error' => 'Già associato a questo gruppo.'], 422);
}
return redirect()->back()->with('error', 'Già associato a questo gruppo.');
}
$individuo->gruppi()->attach($gruppo->id, [
'ruolo_ids' => !empty($data['ruolo_ids']) ? json_encode(array_values($data['ruolo_ids'])) : null,
'data_adesione' => $data['data_adesione'] ?? null,
]);
if ($request->expectsJson()) {
return response()->json(['success' => 'Gruppo associato.']);
}
return redirect()->back()->with('success', 'Gruppo associato.');
}
public function update(Request $request, $individuo, $gruppo)
{
$this->authorizeWrite('individui');
$this->authorizeWrite('gruppi');
$data = $request->validate([
'ruolo_ids' => 'nullable|array',
'ruolo_ids.*' => 'exists:ruoli,id',
'data_adesione' => 'nullable|date',
]);
$individuo = \App\Models\Individuo::findOrFail($individuo);
$gruppoModel = Gruppo::findOrFail($gruppo);
$individuo->gruppi()->updateExistingPivot($gruppoModel->id, [
'ruolo_ids' => !empty($data['ruolo_ids']) ? json_encode(array_values($data['ruolo_ids'])) : null,
'data_adesione' => $data['data_adesione'] ?? null,
]);
return redirect()->back()->with('success', 'Associazione aggiornata.');
}
public function destroy($individuo, $gruppo)
{
$this->authorizeDelete('individui');
$this->authorizeWrite('gruppi');
$individuo = \App\Models\Individuo::findOrFail($individuo);
$individuo->gruppi()->detach($gruppo);
if (request()->ajax() || request()->expectsJson()) {
return response()->json(['success' => 'Rimosso dal gruppo.']);
}
return redirect()->back()->with('success', 'Rimosso dal gruppo.');
}
}
@@ -0,0 +1,73 @@
<?php
namespace App\Http\Controllers;
use App\Models\Gruppo;
use App\Models\Individuo;
use App\Models\Ruolo;
use Illuminate\Http\Request;
class GruppoMembroController extends Controller
{
public function store(Request $request, $gruppo)
{
$this->authorizeWrite('gruppi');
$this->authorizeRead('individui');
$gruppo = Gruppo::findOrFail($gruppo);
$data = $request->validate([
'individuo_id' => 'required|exists:individui,id',
'ruolo_ids' => 'nullable|array',
'ruolo_ids.*' => 'exists:ruoli,id',
'data_adesione' => 'nullable|date',
]);
if ($gruppo->individui()->where('individuo_id', $data['individuo_id'])->exists()) {
if ($request->ajax() || $request->expectsJson()) {
return response()->json(['error' => 'Questo individuo è già membro di questo gruppo.'], 422);
}
return redirect()->back()->with('error', 'Questo individuo è già membro di questo gruppo.');
}
$gruppo->individui()->attach($data['individuo_id'], [
'ruolo_ids' => !empty($data['ruolo_ids']) ? json_encode(array_values($data['ruolo_ids'])) : null,
'data_adesione' => $data['data_adesione'] ?? null,
]);
if ($request->ajax() || $request->expectsJson()) {
return response()->json(['success' => 'Membro aggiunto con successo.']);
}
return redirect()->back()->with('success', 'Membro aggiunto con successo.');
}
public function update(Request $request, $gruppo, $individuo)
{
$this->authorizeWrite('gruppi');
$gruppo = Gruppo::findOrFail($gruppo);
$data = $request->validate([
'ruolo_ids' => 'nullable|array',
'ruolo_ids.*' => 'exists:ruoli,id',
'data_adesione' => 'nullable|date',
]);
$gruppo->individui()->updateExistingPivot($individuo, [
'ruolo_ids' => !empty($data['ruolo_ids']) ? json_encode(array_values($data['ruolo_ids'])) : null,
'data_adesione' => $data['data_adesione'] ?? null,
]);
return redirect()->back()->with('success', 'Membro aggiornato con successo.');
}
public function destroy($gruppo, $individuo)
{
$this->authorizeDelete('gruppi');
$gruppo = Gruppo::findOrFail($gruppo);
$gruppo->individui()->detach($individuo);
if (request()->ajax() || request()->expectsJson()) {
return response()->json(['success' => 'Membro rimosso dal gruppo.']);
}
return redirect()->back()->with('success', 'Membro rimosso dal gruppo.');
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace App\Http\Controllers;
use App\Models\Individuo;
use App\Models\Gruppo;
use App\Models\Notifica;
use Illuminate\Support\Facades\Auth;
class HomeController extends Controller
{
public function index()
{
$user = Auth::user();
$stats = [
'individui' => Individuo::count(),
'gruppi' => Gruppo::count(),
'notifiche' => $user->unreadNotificheCount(),
];
$notifiche = $user->notifiche()->take(5)->get();
return view('home.dashboard', compact('stats', 'notifiche'));
}
}
@@ -0,0 +1,243 @@
<?php
namespace App\Http\Controllers;
use App\Models\AppSetting;
use App\Models\Ruolo;
use App\Models\TipologiaDocumento;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class ImpostazioniController extends Controller
{
public function index()
{
$this->authorizeWrite('viste');
$tipologie = TipologiaDocumento::orderBy('ordine')->get();
$ruoli = Ruolo::orderBy('ordine')->get();
$appSettings = AppSetting::first();
return view('impostazioni.index', compact('tipologie', 'ruoli', 'appSettings'));
}
public function saveAppSettings(Request $request)
{
$this->authorizeWrite('viste');
$validated = $request->validate([
'nome_applicazione' => 'nullable|string|max:100',
'nome_organizzazione' => 'nullable|string|max:255',
'footer_text' => 'nullable|string|max:255',
'app_version' => 'nullable|string|max:50',
'dashboard_welcome' => 'nullable|string|max:1000',
'show_version' => 'boolean',
]);
$settings = AppSetting::first() ?? new AppSetting();
$settings->nome_applicazione = $validated['nome_applicazione'] ?? null;
$settings->nome_organizzazione = $validated['nome_organizzazione'] ?? null;
$settings->footer_text = $validated['footer_text'] ?? null;
$settings->app_version = $validated['app_version'] ?? null;
$settings->dashboard_welcome = $validated['dashboard_welcome'] ?? null;
$settings->show_version = $request->boolean('show_version');
$settings->save();
return redirect('/impostazioni#applicazione')->with('success', 'Impostazioni applicazione salvate.');
}
public function tipologieStore(Request $request)
{
$this->authorizeWrite('viste');
$validated = $request->validate([
'nome' => 'required|string|max:50|unique:tipologie_documenti,nome',
'descrizione' => 'nullable|string|max:255',
]);
$maxOrdine = TipologiaDocumento::max('ordine') ?? 0;
TipologiaDocumento::create([
'nome' => $validated['nome'],
'descrizione' => $validated['descrizione'] ?? null,
'ordine' => $maxOrdine + 1,
'attiva' => true,
]);
return redirect('/impostazioni#tipologie')->with('success', 'Tipologia aggiunta.');
}
public function tipologieUpdate(Request $request, $id)
{
$this->authorizeWrite('viste');
$tipologia = TipologiaDocumento::findOrFail($id);
$validated = $request->validate([
'nome' => 'required|string|max:50|unique:tipologie_documenti,nome,' . $id,
'descrizione' => 'nullable|string|max:255',
'attiva' => 'boolean',
]);
$tipologia->update($validated);
return redirect('/impostazioni#tipologie')->with('success', 'Tipologia aggiornata.');
}
public function tipologieDestroy($id)
{
$this->authorizeDelete('viste');
$tipologia = TipologiaDocumento::findOrFail($id);
$count = $tipologia->documenti()->count();
if ($count > 0) {
return redirect('/impostazioni#tipologie')->with('error', 'Impossibile eliminare: ' . $count . ' documenti associati.');
}
$tipologia->delete();
return redirect('/impostazioni#tipologie')->with('success', 'Tipologia eliminata.');
}
public function tipologieReorder(Request $request)
{
$this->authorizeWrite('viste');
$order = $request->input('order', []);
foreach ($order as $index => $id) {
TipologiaDocumento::where('id', $id)->update(['ordine' => $index]);
}
return response()->json(['success' => true]);
}
public function ruoliStore(Request $request)
{
$this->authorizeWrite('viste');
$validated = $request->validate([
'nome' => 'required|string|max:50|unique:ruoli,nome',
'descrizione' => 'nullable|string|max:255',
]);
$maxOrdine = Ruolo::max('ordine') ?? 0;
Ruolo::create([
'nome' => $validated['nome'],
'descrizione' => $validated['descrizione'] ?? null,
'ordine' => $maxOrdine + 1,
'attiva' => true,
]);
return redirect('/impostazioni#ruoli')->with('success', 'Ruolo aggiunto.');
}
public function ruoliUpdate(Request $request, $id)
{
$this->authorizeWrite('viste');
$ruolo = Ruolo::findOrFail($id);
$validated = $request->validate([
'nome' => 'required|string|max:50|unique:ruoli,nome,' . $id,
'descrizione' => 'nullable|string|max:255',
'attiva' => 'boolean',
]);
$ruolo->update($validated);
return redirect('/impostazioni#ruoli')->with('success', 'Ruolo aggiornato.');
}
public function ruoliDestroy($id)
{
$this->authorizeDelete('viste');
$ruolo = Ruolo::findOrFail($id);
$count = \DB::table('gruppo_individuo')
->whereNotNull('ruolo_ids')
->whereRaw("JSON_CONTAINS(ruolo_ids, ?)", [json_encode($id)])
->count();
if ($count > 0) {
return redirect('/impostazioni#ruoli')->with('error', 'Impossibile eliminare: ' . $count . ' membri associati a questo ruolo.');
}
$ruolo->delete();
return redirect('/impostazioni#ruoli')->with('success', 'Ruolo eliminato.');
}
public function ruoliReorder(Request $request)
{
$this->authorizeWrite('viste');
$order = $request->input('order', []);
foreach ($order as $index => $id) {
Ruolo::where('id', $id)->update(['ordine' => $index]);
}
return response()->json(['success' => true]);
}
public function uploadLogo(Request $request)
{
$this->authorizeWrite('viste');
$request->validate([
'logo' => 'nullable|image|mimes:png,jpg,jpeg,gif,svg|max:2048',
'logo_small' => 'nullable|image|mimes:png,jpg,jpeg,gif,svg|max:512',
]);
$settings = AppSetting::first() ?? new AppSetting();
if ($request->hasFile('logo')) {
$path = $request->file('logo')->store('logos', 'public');
$settings->logo_path = $path;
}
if ($request->hasFile('logo_small')) {
$path = $request->file('logo_small')->store('logos', 'public');
$settings->logo_small_path = $path;
}
if ($settings->id) {
$settings->save();
} else {
$settings->save();
}
return redirect('/impostazioni#logo')->with('success', 'Logo caricato con successo.');
}
public function removeLogo(Request $request)
{
$this->authorizeWrite('viste');
$type = $request->input('type');
$settings = AppSetting::first();
if ($settings) {
if ($type === 'logo') {
if ($settings->logo_path) {
Storage::disk('public')->delete($settings->logo_path);
$settings->logo_path = null;
}
} elseif ($type === 'logo_small') {
if ($settings->logo_small_path) {
Storage::disk('public')->delete($settings->logo_small_path);
$settings->logo_small_path = null;
}
}
$settings->save();
}
return response()->json(['success' => true]);
}
}
@@ -0,0 +1,566 @@
<?php
namespace App\Http\Controllers;
use App\Models\Individuo;
use App\Models\Contatto;
use App\Models\Documento;
use App\Models\Comune;
use App\Models\Diocesi;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule;
use Symfony\Component\HttpFoundation\StreamedResponse;
class IndividuoController extends Controller
{
public function index(Request $request)
{
$this->authorizeRead('individui');
$individui = Individuo::with('contatti')->orderBy('cognome')->orderBy('nome')->paginate(20);
$vista = null;
if ($request->has('vista_id')) {
$vista = \App\Models\VistaReport::where('id', $request->vista_id)
->where('user_id', auth()->id())
->first();
}
if (!$vista && auth()->check()) {
$vista = \App\Models\VistaReport::where('user_id', auth()->id())
->where('tipo', 'individui')
->where('is_default', true)
->first();
}
$allColumns = [
['key' => 'codice', 'label' => 'Codice'],
['key' => 'cognome', 'label' => 'Cognome'],
['key' => 'nome', 'label' => 'Nome'],
['key' => 'email', 'label' => 'Email'],
['key' => 'telefono', 'label' => 'Telefono'],
];
$defaultVisible = ['codice', 'cognome', 'nome', 'email', 'telefono'];
$visibleColumns = $vista && !empty($vista->colonne_visibili)
? $vista->colonne_visibili
: $defaultVisible;
return view('individui.index', compact('individui', 'vista', 'allColumns', 'visibleColumns'));
}
public function exportCSV(Request $request)
{
$this->authorizeRead('individui');
$ids = $request->input('ids', []);
$query = Individuo::with('contatti')->orderBy('cognome')->orderBy('nome');
if (is_array($ids) && !empty($ids)) {
$query->whereIn('id', array_map('intval', $ids));
}
$individui = $query->get();
$response = new StreamedResponse(function () use ($individui) {
$handle = fopen('php://output', 'w');
fputcsv($handle, ['Codice', 'Cognome', 'Nome', 'Data Nascita', 'Genere', 'Email', 'Telefono', 'Cellulare', 'Indirizzo', 'CAP', 'Città', 'Provincia', 'Note']);
foreach ($individui as $ind) {
$email = $ind->contatti->where('tipo', 'email')->where('is_primary', true)->first()
?? $ind->contatti->where('tipo', 'email')->first();
$telefono = $ind->contatti->where('tipo', 'telefono')->where('is_primary', true)->first()
?? $ind->contatti->where('tipo', 'telefono')->first();
$cellulare = $ind->contatti->where('tipo', 'cellulare')->where('is_primary', true)->first()
?? $ind->contatti->where('tipo', 'cellulare')->first();
fputcsv($handle, [
$ind->codice_id,
$ind->cognome,
$ind->nome,
$ind->data_nascita?->format('d/m/Y') ?? '',
$ind->genere === 'M' ? 'Maschio' : ($ind->genere === 'F' ? 'Femmina' : ''),
$email?->valore ?? '',
$telefono?->valore ?? '',
$cellulare?->valore ?? '',
$ind->indirizzo,
$ind->cap,
$ind->città,
$ind->sigla_provincia,
$ind->note,
]);
}
fclose($handle);
});
$response->headers->set('Content-Type', 'text/csv; charset=utf-8');
$response->headers->set('Content-Disposition', 'attachment; filename="individui_' . now()->format('Y-m-d') . '.csv"');
$response->headers->set('Cache-Control', 'no-store');
return $response;
}
public function create()
{
$this->authorizeWrite('individui');
$comuni = Comune::orderBy('nome')->get();
$diocesi = Diocesi::orderBy('nome')->get();
return view('individui.create', compact('comuni', 'diocesi'));
}
public function store(Request $request)
{
$this->authorizeWrite('individui');
$data = $request->validate([
'cognome' => 'required|string|max:255',
'nome' => 'required|string|max:255',
'data_nascita' => 'nullable|date',
'indirizzo' => 'nullable|string|max:500',
'cap' => 'nullable|string|max:10',
'città' => 'nullable|string|max:255',
'sigla_provincia' => 'nullable|string|max:2',
'genere' => 'nullable|in:M,F',
'tipo_documento' => 'nullable|in:carta_identita,patente',
'numero_documento' => 'nullable|string|max:50',
'scadenza_documento' => 'nullable|date',
'note' => 'nullable|string',
]);
$individuo = Individuo::create($data);
if ($request->has('contatti')) {
foreach ($request->contatti as $contatto) {
if (!empty($contatto['tipo']) && !empty($contatto['valore'])) {
$individuo->contatti()->create([
'tipo' => $contatto['tipo'],
'valore' => $contatto['valore'],
'etichetta' => $contatto['etichetta'] ?? null,
'is_primary' => $contatto['is_primary'] ?? false,
]);
}
}
}
return redirect()->route('individui.index')->with('success', 'Individuo creato con successo.');
}
public function show($individuo)
{
$this->authorizeRead('individui');
$individuo = Individuo::with(['contatti', 'gruppi' => function ($q) {
$q->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione');
}, 'avatar'])->findOrFail($individuo);
return view('individui.show', compact('individuo'));
}
public function report($individuo)
{
$this->authorizeRead('individui');
$individuo = Individuo::with(['contatti', 'gruppi', 'documenti', 'eventiResponsabili', 'avatar'])->findOrFail($individuo);
return view('individui.report', compact('individuo'));
}
public function reportStampa(Request $request)
{
$this->authorizeRead('individui');
$ids = explode(',', $request->ids ?? '');
$individui = Individuo::with(['contatti', 'gruppi', 'documenti', 'eventiResponsabili', 'avatar'])
->whereIn('id', $ids)
->orderBy('cognome')
->orderBy('nome')
->get();
return view('individui.report-multiplo', compact('individui'));
}
public function edit($individuo)
{
$this->authorizeWrite('individui');
$individuo = Individuo::with(['contatti', 'gruppi', 'avatar'])->findOrFail($individuo);
return view('individui.edit', compact('individuo'));
}
public function update(Request $request, $individuo)
{
$this->authorizeWrite('individui');
$individuo = Individuo::findOrFail($individuo);
\Log::info('=== INDIVIDUO UPDATE START ===', ['id' => $individuo->id]);
\Log::info('Request contatti:', ['contatti' => $request->input('contatti'), 'all_keys' => array_keys($request->all())]);
$data = $request->validate([
'cognome' => 'required|string|max:255',
'nome' => 'required|string|max:255',
'data_nascita' => 'nullable|date',
'indirizzo' => 'nullable|string|max:500',
'cap' => 'nullable|string|max:10',
'città' => 'nullable|string|max:255',
'sigla_provincia' => 'nullable|string|max:2',
'genere' => 'nullable|in:M,F',
'tipo_documento' => 'nullable|in:carta_identita,patente',
'numero_documento' => 'nullable|string|max:50',
'scadenza_documento' => 'nullable|date',
'note' => 'nullable|string',
]);
$individuo->update($data);
\Log::info('Individuo dati salvati', ['id' => $individuo->id]);
$contattiData = $request->input('contatti');
\Log::info('Contatti ricevuti:', ['contatti' => $contattiData, 'tipo' => gettype($contattiData)]);
$individuo->contatti()->delete();
\Log::info('Contatti precedenti eliminati, count ora:', ['count' => $individuo->contatti()->count()]);
if (!empty($contattiData) && is_array($contattiData)) {
$created = 0;
foreach ($contattiData as $contatto) {
if (!empty($contatto['tipo']) && !empty($contatto['valore'])) {
$individuo->contatti()->create([
'tipo' => $contatto['tipo'],
'valore' => $contatto['valore'],
'etichetta' => $contatto['etichetta'] ?? null,
'is_primary' => !empty($contatto['is_primary']),
]);
$created++;
}
}
\Log::info('Contatti creati:', ['count' => $created]);
}
return redirect()->route('individui.show', $individuo)->with('success', 'Individuo aggiornato con successo!');
}
public function destroy($individuo)
{
$this->authorizeDelete('individui');
$individuo = Individuo::findOrFail($individuo);
$individuo->delete();
return redirect()->route('individui.index')->with('success', 'Individuo eliminato.');
}
public function elimina(Request $request, $individuo)
{
$this->authorizeDelete('individui');
$individuo = Individuo::findOrFail($individuo);
$eliminaContatti = filter_var($request->input('elimina_contatti', false), FILTER_VALIDATE_BOOLEAN);
$eliminaDocumenti = filter_var($request->input('elimina_documenti', false), FILTER_VALIDATE_BOOLEAN);
if ($eliminaContatti) {
$individuo->contatti()->delete();
}
if ($eliminaDocumenti) {
foreach ($individuo->documenti as $documento) {
if ($documento->file_path && file_exists(storage_path('app/public/' . $documento->file_path))) {
unlink(storage_path('app/public/' . $documento->file_path));
}
}
$individuo->documenti()->delete();
}
$individuo->delete();
return redirect()->route('individui.index')->with('success', 'Individuo eliminato con successo.');
}
public function massElimina(Request $request)
{
$this->authorizeDelete('individui');
$ids = $request->input('ids', []);
if (!is_array($ids) || empty($ids)) {
return redirect()->route('individui.index')->with('error', 'Nessun individuo selezionato.');
}
$eliminaContatti = filter_var($request->input('elimina_contatti', false), FILTER_VALIDATE_BOOLEAN);
$eliminaDocumenti = filter_var($request->input('elimina_documenti', false), FILTER_VALIDATE_BOOLEAN);
$individui = Individuo::whereIn('id', $ids)->get();
foreach ($individui as $individuo) {
if ($eliminaContatti) {
$individuo->contatti()->delete();
}
if ($eliminaDocumenti) {
foreach ($individuo->documenti as $documento) {
if ($documento->file_path && file_exists(storage_path('app/public/' . $documento->file_path))) {
unlink(storage_path('app/public/' . $documento->file_path));
}
}
$individuo->documenti()->delete();
}
$individuo->delete();
}
return redirect()->route('individui.index')->with('success', count($individui) . ' individui eliminati con successo.');
}
public function elementiCollegati($individuo)
{
$individuo = Individuo::findOrFail($individuo);
return response()->json([
'contatti' => $individuo->contatti()->count(),
'gruppi' => $individuo->gruppi()->count(),
'documenti' => $individuo->documenti()->count(),
'eventi' => $individuo->eventiResponsabili()->count(),
]);
}
public function emailList(Request $request)
{
$this->authorizeRead('individui');
$ids = $request->input('ids', '');
$idArray = array_filter(array_map('intval', explode(',', $ids)));
if (empty($idArray)) {
return response()->json([]);
}
$individui = Individuo::with('contatti')->whereIn('id', $idArray)->get();
$result = [];
foreach ($individui as $ind) {
$emails = $ind->contatti->where('tipo', 'email')->pluck('valore')->toArray();
$result[] = [
'id' => $ind->id,
'codice_id' => $ind->codice_id,
'cognome' => $ind->cognome,
'nome' => $ind->nome,
'email' => count($emails) === 1 ? $emails[0] : null,
'emails' => $emails,
'email_count' => count($emails)
];
}
return response()->json($result);
}
public function allIndividui()
{
$this->authorizeRead('individui');
$individui = Individuo::select('id', 'codice_id', 'cognome', 'nome')
->orderBy('cognome')
->orderBy('nome')
->get();
return response()->json($individui);
}
public function import()
{
$this->authorizeWrite('individui');
return view('individui.import');
}
public function importStore(Request $request)
{
$this->authorizeWrite('individui');
$request->validate([
'file' => 'required|file|mimes:csv,txt',
]);
$file = $request->file('file');
$handle = fopen($file->path(), 'r');
$header = fgetcsv($handle, 1000, ',');
$imported = 0;
$skipped = 0;
$errors = [];
$rowNumber = 1;
while (($row = fgetcsv($handle, 1000, ',')) !== false) {
$rowNumber++;
if (count($row) < count($header)) {
$skipped++;
continue;
}
$data = array_combine($header, $row);
if ($data === false) {
$skipped++;
continue;
}
$cognome = trim($data['cognome'] ?? '');
$nome = trim($data['nome'] ?? '');
if (empty($cognome) || empty($nome)) {
$skipped++;
continue;
}
try {
$individuo = Individuo::create([
'cognome' => $cognome,
'nome' => $nome,
'data_nascita' => !empty(trim($data['data_nascita'] ?? '')) ? trim($data['data_nascita']) : null,
'indirizzo' => !empty(trim($data['indirizzo'] ?? '')) ? trim($data['indirizzo']) : null,
'cap' => !empty(trim($data['cap'] ?? '')) ? trim($data['cap']) : null,
'città' => !empty(trim($data['città'] ?? '')) ? trim($data['città']) : null,
'sigla_provincia' => !empty(trim($data['sigla_provincia'] ?? '')) ? trim($data['sigla_provincia']) : null,
'genere' => !empty(trim($data['genere'] ?? '')) ? strtoupper(trim($data['genere'])) : null,
'tipo_documento' => !empty(trim($data['tipo_documento'] ?? '')) ? trim($data['tipo_documento']) : null,
'numero_documento' => !empty(trim($data['numero_documento'] ?? '')) ? trim($data['numero_documento']) : null,
'scadenza_documento' => !empty(trim($data['scadenza_documento'] ?? '')) ? trim($data['scadenza_documento']) : null,
'note' => !empty(trim($data['note'] ?? '')) ? trim($data['note']) : null,
]);
for ($i = 1; $i <= 2; $i++) {
$tipoKey = 'contatto_' . $i . '_tipo';
$valoreKey = 'contatto_' . $i . '_valore';
$etichettaKey = 'contatto_' . $i . '_etichetta';
$tipo = trim($data[$tipoKey] ?? '');
$valore = trim($data[$valoreKey] ?? '');
if (!empty($tipo) && !empty($valore)) {
$individuo->contatti()->create([
'tipo' => $tipo,
'valore' => $valore,
'etichetta' => !empty(trim($data[$etichettaKey] ?? '')) ? trim($data[$etichettaKey]) : null,
'is_primary' => $i === 1,
]);
}
}
$imported++;
} catch (\Exception $e) {
$errors[] = 'Errore riga ' . $rowNumber . ': ' . $e->getMessage();
}
}
fclose($handle);
$message = 'Importati ' . $imported . ' individui.';
if ($skipped > 0) {
$message .= ' Saltate ' . $skipped . ' righe (vuote o incomplete).';
}
if (count($errors) > 0) {
return back()->with('error', $message . ' Errori: ' . implode('; ', $errors));
}
return redirect()->route('individui.index')->with('success', $message);
}
public function downloadTemplate()
{
$this->authorizeRead('individui');
$headers = ['cognome', 'nome', 'data_nascita', 'indirizzo', 'cap', 'città', 'sigla_provincia', 'genere', 'tipo_documento', 'numero_documento', 'scadenza_documento', 'note', 'contatto_1_tipo', 'contatto_1_valore', 'contatto_1_etichetta', 'contatto_2_tipo', 'contatto_2_valore', 'contatto_2_etichetta'];
$example = [
'Rossi',
'Mario',
'1990-05-15',
'Via Roma 10',
'00100',
'Roma',
'RM',
'M',
'carta_identita',
'AB123456',
'2030-05-15',
'Socio fondatore',
'email',
'mario.rossi@email.com',
'personale',
'cellulare',
'3331234567',
'lavoro',
];
$csv = implode(',', $headers) . "\n" . implode(',', $example);
return response($csv, 200, [
'Content-Type' => 'text/csv',
'Content-Disposition' => 'attachment; filename="template_individui.csv"',
]);
}
public function uploadAvatar(Request $request, $individuo)
{
$this->authorizeWrite('individui');
$individuo = Individuo::findOrFail($individuo);
$request->validate([
'avatar' => 'required|image|mimes:jpeg,jpg,png,gif|max:2048',
]);
$file = $request->file('avatar');
$oldAvatar = $individuo->avatar;
if ($oldAvatar) {
if (Storage::disk('local')->exists($oldAvatar->file_path)) {
Storage::disk('local')->delete($oldAvatar->file_path);
}
$oldAvatar->delete();
}
$path = $file->store('avatars', 'local');
$avatar = Documento::create([
'tenant_id' => $individuo->tenant_id,
'nome_file' => $file->getClientOriginalName(),
'file_path' => $path,
'tipo' => 'upload',
'tipologia' => 'avatar',
'visibilita' => 'individuo',
'visibilita_target_id' => $individuo->id,
'visibilita_target_type' => Individuo::class,
'mime_type' => $file->getMimeType(),
'dimensione' => $file->getSize(),
'user_id' => auth()->id(),
]);
return response()->json([
'success' => true,
'avatar_url' => url('/individui/' . $individuo->id . '/avatar'),
'avatar_id' => $avatar->id,
]);
}
public function getAvatar($individuo)
{
$this->authorizeRead('individui');
$individuo = Individuo::findOrFail($individuo);
$avatar = $individuo->avatar;
if (!$avatar || !Storage::disk('local')->exists($avatar->file_path)) {
abort(404);
}
return response(
Storage::disk('local')->get($avatar->file_path),
200,
['Content-Type' => $avatar->mime_type]
);
}
public function deleteAvatar($individuo)
{
$this->authorizeDelete('individui');
$individuo = Individuo::findOrFail($individuo);
$avatar = $individuo->avatar;
if (!$avatar) {
return response()->json(['success' => false, 'message' => 'Nessun avatar da eliminare']);
}
if (Storage::disk('local')->exists($avatar->file_path)) {
Storage::disk('local')->delete($avatar->file_path);
}
$avatar->delete();
return response()->json(['success' => true]);
}
}
+142
View File
@@ -0,0 +1,142 @@
<?php
namespace App\Http\Controllers;
use App\Models\MailingList;
use App\Models\MailingContact;
use App\Models\Individuo;
use App\Models\Documento;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class MailingController extends Controller
{
public function nuovo(Request $request)
{
$this->authorizeWrite('mailing');
$individui = collect();
$individuiIds = $request->input('individui');
if ($individuiIds) {
$ids = array_map('trim', explode(',', $individuiIds));
$individui = Individuo::with('contatti')->whereIn('id', $ids)->get();
}
$liste = MailingList::all();
$documenti = Documento::orderBy('nome_file')->get();
$documentiSelezionati = collect();
if ($request->has('documenti_selezionati')) {
$docIds = array_filter(array_map('trim', explode(',', $request->input('documenti_selezionati'))));
$documentiSelezionati = Documento::whereIn('id', $docIds)->get();
}
return view('mailing.nuovo', compact('individui', 'liste', 'documenti', 'documentiSelezionati'));
}
public function invia(Request $request)
{
$this->authorizeWrite('mailing');
$data = $request->validate([
'oggetto' => 'required|string|max:255',
'corpo' => 'required',
'lista_id' => 'nullable|exists:mailing_liste,id',
'destinatari_ids' => 'nullable|string',
'documenti_selezionati' => 'nullable|string',
'allegato' => 'nullable|file|max:10240',
]);
$documentiAllegati = [];
if ($request->hasFile('allegato')) {
$file = $request->file('allegato');
$nomeOriginale = $file->getClientOriginalName();
$path = $file->store('documenti/allegati');
$documento = Documento::create([
'nome_file' => $nomeOriginale,
'file_path' => $path,
'tipo' => 'allegato',
'tipologia' => 'allegato_email',
'visibilita' => 'pubblico',
'dimensione' => $file->getSize(),
'mime_type' => $file->getMimeType(),
'user_id' => auth()->id(),
]);
$documentiAllegati[] = $documento->id;
}
if (!empty($data['documenti_selezionati'])) {
$docIds = array_filter(array_map('trim', explode(',', $data['documenti_selezionati'])));
$documentiAllegati = array_merge($documentiAllegati, $docIds);
}
$destinatari = [];
if (!empty($data['destinatari_ids'])) {
$ids = array_filter(array_map('trim', explode(',', $data['destinatari_ids'])));
$destinatari = Individuo::with('contatti')->whereIn('id', $ids)->get();
}
return back()->with('success', 'Messaggio preparato per ' . count($destinatari) . ' destinatari con ' . count($documentiAllegati) . ' allegato(i).');
}
public function invio(Request $request)
{
$this->authorizeWrite('mailing');
$liste = MailingList::where('attiva', true)->orderBy('nome')->get();
$documenti = Documento::orderBy('nome_file')->get();
return view('mailing.invio', compact('liste', 'documenti'));
}
public function invioElabora(Request $request)
{
$this->authorizeWrite('mailing');
$data = $request->validate([
'liste' => 'required|array',
'liste.*' => 'exists:mailing_liste,id',
'oggetto' => 'required|string|max:255',
'corpo' => 'required',
'documenti_selezionati' => 'nullable|array',
'documenti_selezionati.*' => 'exists:documenti,id',
'allegato' => 'nullable|file|max:10240',
]);
$documentiAllegati = [];
if ($request->hasFile('allegato')) {
$file = $request->file('allegato');
$nomeOriginale = $file->getClientOriginalName();
$path = $file->store('documenti/allegati');
$documento = Documento::create([
'nome_file' => $nomeOriginale,
'file_path' => $path,
'tipo' => 'allegato',
'tipologia' => 'allegato_email',
'visibilita' => 'pubblico',
'dimensione' => $file->getSize(),
'mime_type' => $file->getMimeType(),
'user_id' => auth()->id(),
]);
$documentiAllegati[] = $documento->id;
}
if (!empty($data['documenti_selezionati'])) {
$documentiAllegati = array_merge($documentiAllegati, $data['documenti_selezionati']);
}
$contatti = MailingContact::whereIn('mailing_list_id', $data['liste'])
->where('opt_in', true)
->with('individuo.contatti')
->get();
$destinatari = $contatti->map(function($c) {
return $c->individuo->contatti->where('tipo', 'email')->first()?->valore;
})->filter()->unique()->values();
return back()->with('success', 'Messaggio pronto per l\'invio a ' . $destinatari->count() . ' destinatari unici da ' . count($data['liste']) . ' lista(e).');
}
}
@@ -0,0 +1,179 @@
<?php
namespace App\Http\Controllers;
use App\Models\MailingList;
use Illuminate\Http\Request;
class MailingListController extends Controller
{
public function index()
{
$this->authorizeRead('mailing');
$mailingLists = MailingList::with(['user', 'contatti'])
->orderBy('nome')
->get();
return view('mailing-liste.index', compact('mailingLists'));
}
public function create()
{
$this->authorizeWrite('mailing');
return view('mailing-liste.create');
}
public function store(Request $request)
{
$this->authorizeWrite('mailing');
$data = $request->validate([
'nome' => 'required|string|max:255',
'descrizione' => 'nullable|string',
'attiva' => 'boolean',
'contatti_json' => 'nullable|string',
]);
$data['user_id'] = auth()->id();
$data['attiva'] = $data['attiva'] ?? true;
$lista = MailingList::create([
'nome' => $data['nome'],
'descrizione' => $data['descrizione'] ?? null,
'attiva' => $data['attiva'],
'user_id' => $data['user_id'],
]);
if (!empty($data['contatti_json'])) {
$contatti = json_decode($data['contatti_json'], true);
if (is_array($contatti)) {
foreach ($contatti as $contatto) {
if (!empty($contatto['individuo_id']) && !empty($contatto['email'])) {
$lista->contatti()->create([
'individuo_id' => $contatto['individuo_id'],
'opt_in' => true,
'opt_in_data' => now(),
]);
}
}
}
}
return redirect()->route('mailing-liste.index')->with('success', 'Lista creata con ' . $lista->contatti()->count() . ' contatti.');
}
public function show($mailing_liste)
{
$this->authorizeRead('mailing');
$mailingList = MailingList::with(['contatti.individuo.contatti'])->findOrFail($mailing_liste);
return view('mailing-liste.show', compact('mailingList'));
}
public function edit($mailingList)
{
$this->authorizeWrite('mailing');
$mailingList = MailingList::with('contatti.individuo.contatti')->findOrFail($mailingList);
return view('mailing-liste.edit', compact('mailingList'));
}
public function update(Request $request, $mailingList)
{
$this->authorizeWrite('mailing');
$mailingList = MailingList::findOrFail($mailingList);
$data = $request->validate([
'nome' => 'required|string|max:255',
'descrizione' => 'nullable|string',
'attiva' => 'boolean',
'contatti_json' => 'nullable|string',
]);
$data['attiva'] = $data['attiva'] ?? false;
$mailingList->update([
'nome' => $data['nome'],
'descrizione' => $data['descrizione'] ?? null,
'attiva' => $data['attiva'],
]);
if (!empty($data['contatti_json'])) {
$newContatti = json_decode($data['contatti_json'], true);
if (is_array($newContatti)) {
$existingIds = $mailingList->contatti()->pluck('individuo_id')->toArray();
$newIds = array_column($newContatti, 'individuo_id');
$toRemove = array_diff($existingIds, $newIds);
if (!empty($toRemove)) {
$mailingList->contatti()->whereIn('individuo_id', $toRemove)->delete();
}
$toAdd = array_diff($newIds, $existingIds);
foreach ($newContatti as $contatto) {
if (in_array($contatto['individuo_id'], $toAdd) && !empty($contatto['email'])) {
$mailingList->contatti()->create([
'individuo_id' => $contatto['individuo_id'],
'opt_in' => true,
'opt_in_data' => now(),
]);
}
}
}
}
return redirect()->route('mailing-liste.show', $mailingList->id)->with('success', 'Lista aggiornata.');
}
public function destroy($mailingList)
{
$this->authorizeDelete('mailing');
$mailingList = MailingList::findOrFail($mailingList);
$mailingList->delete();
return redirect()->route('mailing-liste.index')->with('success', 'Lista eliminata.');
}
public function massElimina(Request $request)
{
$this->authorizeDelete('mailing');
$ids = $request->input('ids', []);
if (!is_array($ids) || empty($ids)) {
return redirect()->route('mailing-liste.index')->with('error', 'Nessuna mailing list selezionata.');
}
$liste = MailingList::whereIn('id', $ids)->get();
foreach ($liste as $lista) {
$lista->contatti()->delete();
$lista->delete();
}
return redirect()->route('mailing-liste.index')->with('success', count($liste) . ' mailing list eliminate con successo.');
}
public function createFromIndividui(Request $request)
{
$this->authorizeWrite('mailing');
$data = $request->validate([
'nome' => 'required|string|max:255',
'descrizione' => 'nullable|string',
'contatti' => 'required|array',
'contatti.*.individuo_id' => 'required|exists:individui,id',
'contatti.*.email' => 'required|email',
]);
$lista = MailingList::create([
'nome' => $data['nome'],
'descrizione' => $data['descrizione'] ?? null,
'attiva' => true,
'user_id' => auth()->id(),
]);
foreach ($data['contatti'] as $contatto) {
$lista->contatti()->create([
'individuo_id' => $contatto['individuo_id'],
'opt_in' => true,
'opt_in_data' => now(),
]);
}
return response()->json(['success' => true, 'lista_id' => $lista->id]);
}
}
+645
View File
@@ -0,0 +1,645 @@
<?php
namespace App\Http\Controllers;
use App\Models\Individuo;
use App\Models\Gruppo;
use App\Models\Evento;
use App\Models\Documento;
use App\Models\MailingList;
use App\Models\Contatto;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class ReportController extends Controller
{
public function index()
{
if (!auth()->user()->canAccess('report')) {
abort(403);
}
$stats = [
'individui' => Individuo::count(),
'gruppi' => Gruppo::count(),
'eventi' => Evento::count(),
'documenti' => Documento::count(),
'mailing_liste' => MailingList::count(),
'contatti' => Contatto::count(),
];
$prebuiltReports = $this->getPrebuiltReports();
$customReports = \App\Models\ReportCustom::where('user_id', auth()->id())
->orderBy('created_at', 'desc')
->get();
return view('report.index', compact('stats', 'prebuiltReports', 'customReports'));
}
public function run(Request $request)
{
if (!auth()->user()->canAccess('report')) {
abort(403);
}
$reportType = $request->input('report');
$data = match ($reportType) {
'individui_per_genere' => $this->reportIndividuiPerGenere(),
'individui_per_eta' => $this->reportIndividuiPerEta(),
'gruppi_gerarchia' => $this->reportGruppiGerarchia(),
'gruppi_membri' => $this->reportGruppiMembri(),
'eventi_calendario' => $this->reportEventiCalendario(),
'documenti_per_tipo' => $this->reportDocumentiPerTipo(),
'contatti_per_tipo' => $this->reportContattiPerTipo(),
'mailing_list_dettaglio' => $this->reportMailingListDettaglio(),
'individui_senza_contatti' => $this->reportIndividuiSenzaContatti(),
'individui_senza_gruppo' => $this->reportIndividuiSenzaGruppo(),
'eventi_per_gruppo' => $this->reportEventiPerGruppo(),
'documenti_orfani' => $this->reportDocumentiOrfani(),
default => null,
};
if (!$data) {
return redirect()->route('report.index')->with('error', 'Report non trovato.');
}
return view('report.result', compact('reportType', 'data'));
}
public function runCustom($id)
{
if (!auth()->user()->canAccess('report')) {
abort(403);
}
$report = \App\Models\ReportCustom::findOrFail($id);
if ($report->user_id !== auth()->id()) {
abort(403);
}
$data = $this->runCustomReport($report);
return view('report.result', ['reportType' => 'custom', 'data' => $data, 'customReport' => $report]);
}
public function storeCustom(Request $request)
{
if (!auth()->user()->canAccess('report')) {
abort(403);
}
$data = $request->validate([
'nome' => 'required|string|max:255',
'descrizione' => 'nullable|string',
'tipo_report' => 'required|string',
'config' => 'nullable|array',
]);
\App\Models\ReportCustom::create([
'user_id' => auth()->id(),
'nome' => $data['nome'],
'descrizione' => $data['descrizione'] ?? null,
'tipo_report' => $data['tipo_report'],
'config' => $data['config'] ?? [],
]);
return redirect()->route('report.index')->with('success', 'Report personalizzato creato.');
}
public function destroyCustom($id)
{
if (!auth()->user()->canAccess('report')) {
abort(403);
}
$report = \App\Models\ReportCustom::where('user_id', auth()->id())->findOrFail($id);
$report->delete();
return redirect()->route('report.index')->with('success', 'Report personalizzato eliminato.');
}
public function exportCSV(Request $request)
{
if (!auth()->user()->canAccess('report')) {
abort(403);
}
$reportType = $request->input('report');
$data = match ($reportType) {
'individui_per_genere' => $this->reportIndividuiPerGenere(),
'individui_per_eta' => $this->reportIndividuiPerEta(),
'gruppi_gerarchia' => $this->reportGruppiGerarchia(),
'gruppi_membri' => $this->reportGruppiMembri(),
'eventi_calendario' => $this->reportEventiCalendario(),
'documenti_per_tipo' => $this->reportDocumentiPerTipo(),
'contatti_per_tipo' => $this->reportContattiPerTipo(),
'mailing_list_dettaglio' => $this->reportMailingListDettaglio(),
'individui_senza_contatti' => $this->reportIndividuiSenzaContatti(),
'individui_senza_gruppo' => $this->reportIndividuiSenzaGruppo(),
'eventi_per_gruppo' => $this->reportEventiPerGruppo(),
'documenti_orfani' => $this->reportDocumentiOrfani(),
default => null,
};
if (!$data) {
return redirect()->route('report.index')->with('error', 'Report non trovato.');
}
$response = new \Symfony\Component\HttpFoundation\StreamedResponse(function () use ($data) {
$handle = fopen('php://output', 'w');
if (!empty($data['headers']) && !empty($data['rows'])) {
fputcsv($handle, $data['headers']);
foreach ($data['rows'] as $row) {
fputcsv($handle, $row);
}
}
fclose($handle);
});
$response->headers->set('Content-Type', 'text/csv; charset=utf-8');
$response->headers->set('Content-Disposition', 'attachment; filename="report_' . $reportType . '_' . now()->format('Y-m-d') . '.csv"');
return $response;
}
protected function getPrebuiltReports(): array
{
return [
[
'id' => 'individui_per_genere',
'nome' => 'Individui per Genere',
'descrizione' => 'Distribuzione degli individui per genere (Maschio/Femmina)',
'icona' => 'fa-venus-mars',
'colore' => 'info',
],
[
'id' => 'individui_per_eta',
'nome' => 'Individui per Fascia d\'Età',
'descrizione' => 'Distribuzione degli individui per fasce d\'età',
'icona' => 'fa-birthday-cake',
'colore' => 'success',
],
[
'id' => 'gruppi_gerarchia',
'nome' => 'Gerarchia Gruppi',
'descrizione' => 'Struttura ad albero completa di tutti i gruppi con livelli',
'icona' => 'fa-sitemap',
'colore' => 'warning',
],
[
'id' => 'gruppi_membri',
'nome' => 'Gruppi e Membri',
'descrizione' => 'Elenco di tutti i gruppi con il conteggio dei membri',
'icona' => 'fa-users',
'colore' => 'primary',
],
[
'id' => 'eventi_calendario',
'nome' => 'Eventi del Mese',
'descrizione' => 'Tutti gli eventi programmati per il mese corrente',
'icona' => 'fa-calendar-alt',
'colore' => 'danger',
],
[
'id' => 'documenti_per_tipo',
'nome' => 'Documenti per Tipologia',
'descrizione' => 'Distribuzione dei documenti per tipologia',
'icona' => 'fa-file-alt',
'colore' => 'secondary',
],
[
'id' => 'contatti_per_tipo',
'nome' => 'Contatti per Tipo',
'descrizione' => 'Distribuzione dei contatti per tipo (telefono, email, ecc.)',
'icona' => 'fa-address-book',
'colore' => 'teal',
],
[
'id' => 'mailing_list_dettaglio',
'nome' => 'Dettaglio Mailing List',
'descrizione' => 'Elenco completo delle mailing list con i contatti associati',
'icona' => 'fa-envelope',
'colore' => 'purple',
],
[
'id' => 'individui_senza_contatti',
'nome' => 'Individui senza Contatti',
'descrizione' => 'Lista degli individui che non hanno nessun contatto registrato',
'icona' => 'fa-user-slash',
'colore' => 'dark',
],
[
'id' => 'individui_senza_gruppo',
'nome' => 'Individui senza Gruppo',
'descrizione' => 'Lista degli individui non associati a nessun gruppo',
'icona' => 'fa-user-friends',
'colore' => 'orange',
],
[
'id' => 'eventi_per_gruppo',
'nome' => 'Eventi per Gruppo',
'descrizione' => 'Distribuzione degli eventi per gruppo di appartenenza',
'icona' => 'fa-calendar-check',
'colore' => 'pink',
],
[
'id' => 'documenti_orfani',
'nome' => 'Documenti Orfani',
'descrizione' => 'Documenti non collegati a nessun individuo, gruppo o evento',
'icona' => 'fa-file-excel',
'colore' => 'gray',
],
];
}
protected function reportIndividuiPerGenere(): array
{
$rows = Individuo::select('genere', DB::raw('count(*) as totale'))
->groupBy('genere')
->get()
->map(function ($item) {
$item->genere_label = match ($item->genere) {
'M' => 'Maschio',
'F' => 'Femmina',
default => 'Non specificato',
};
return $item;
});
return [
'title' => 'Individui per Genere',
'headers' => ['Genere', 'Totale'],
'rows' => $rows->map(fn($r) => [$r->genere_label, $r->totale])->toArray(),
'summary' => 'Totale individui: ' . Individuo::count(),
'detail_rows' => $rows->map(fn($r) => (array) $r)->toArray(),
];
}
protected function reportIndividuiPerEta(): array
{
$now = now();
$fasce = [
['label' => '0-17 (Minorenni)', 'min' => 0, 'max' => 17],
['label' => '18-25 (Giovani)', 'min' => 18, 'max' => 25],
['label' => '26-40 (Adulti)', 'min' => 26, 'max' => 40],
['label' => '41-60 (Adulti)', 'min' => 41, 'max' => 60],
['label' => '61-75 (Senior)', 'min' => 61, 'max' => 75],
['label' => '75+ (Anziani)', 'min' => 76, 'max' => 200],
];
$rows = [];
$totalWithDate = 0;
foreach ($fasce as $fascia) {
$count = Individuo::whereNotNull('data_nascita')
->whereYear('data_nascita', '>=', $now->year - $fascia['max'])
->whereYear('data_nascita', '<=', $now->year - $fascia['min'])
->count();
$rows[] = ['fascia' => $fascia['label'], 'totale' => $count];
$totalWithDate += $count;
}
$noDate = Individuo::whereNull('data_nascita')->count();
$rows[] = ['fascia' => 'Data non specificata', 'totale' => $noDate];
return [
'title' => 'Individui per Fascia d\'Età',
'headers' => ['Fascia d\'Età', 'Totale'],
'rows' => array_map(fn($r) => [$r['fascia'], $r['totale']], $rows),
'summary' => 'Totale con data: ' . $totalWithDate . ' | Senza data: ' . $noDate,
'detail_rows' => $rows,
];
}
protected function reportGruppiGerarchia(): array
{
$rootGruppi = Gruppo::whereNull('parent_id')->with(['children.diocesi', 'children.children'])->orderBy('nome')->get();
$rows = [];
foreach ($rootGruppi as $gruppo) {
$rows[] = [
'livello' => 0,
'nome' => $gruppo->nome,
'diocesi' => $gruppo->diocesi?->nome ?? '-',
'membri' => $gruppo->individui()->count(),
'padre' => '-',
];
$this->addGruppoChildren($gruppo->children, 1, $rows);
}
return [
'title' => 'Gerarchia Gruppi',
'headers' => ['Livello', 'Nome Gruppo', 'Diocesi', 'Membri', 'Gruppo Padre'],
'rows' => array_map(fn($r) => [$r['livello'], $r['nome'], $r['diocesi'], $r['membri'], $r['padre']], $rows),
'summary' => 'Totale gruppi: ' . Gruppo::count(),
'detail_rows' => $rows,
];
}
protected function addGruppoChildren($children, $level, &$rows): void
{
foreach ($children as $child) {
$parent = $child->parent;
$rows[] = [
'livello' => $level,
'nome' => str_repeat(' ', $level) . $child->nome,
'diocesi' => $child->diocesi?->nome ?? '-',
'membri' => $child->individui()->count(),
'padre' => $parent?->nome ?? '-',
];
$this->addGruppoChildren($child->children, $level + 1, $rows);
}
}
protected function reportGruppiMembri(): array
{
$gruppi = Gruppo::withCount('individui')->orderBy('nome')->get();
$rows = $gruppi->map(function ($g) {
$parent = $g->parent;
return [
'nome' => $g->full_path,
'diocesi' => $g->diocesi?->nome ?? '-',
'membri' => $g->individui_count,
'padre' => $parent?->nome ?? '-',
'responsabili' => $g->getResponsabili()->pluck('cognome')->implode(', ') ?: '-',
];
})->toArray();
return [
'title' => 'Gruppi e Membri',
'headers' => ['Gruppo', 'Diocesi', 'Membri', 'Gruppo Padre', 'Responsabili'],
'rows' => array_map(fn($r) => [$r['nome'], $r['diocesi'], $r['membri'], $r['padre'], $r['responsabili']], $rows),
'summary' => 'Totale gruppi: ' . $gruppi->count() . ' | Totale membri: ' . $gruppi->sum('individui_count'),
'detail_rows' => $rows,
];
}
protected function reportEventiCalendario(): array
{
$now = now();
$eventi = Evento::whereBetween('data_specifica', [$now->startOfMonth(), $now->endOfMonth()])
->with(['gruppi', 'responsabili'])
->orderBy('data_specifica')
->get();
$rows = $eventi->map(function ($e) {
return [
'nome' => $e->nome_evento,
'data' => $e->data_specifica?->format('d/m/Y H:i') ?? '-',
'tipo' => ucfirst($e->tipo_recorrenza ?? 'singolo'),
'gruppi' => $e->gruppi->pluck('nome')->implode(', ') ?: '-',
'responsabili' => $e->responsabili->pluck('nome_completo')->implode(', ') ?: '-',
'luogo' => $e->luogo_indirizzo ?? '-',
];
})->toArray();
return [
'title' => 'Eventi del Mese (' . $now->format('F Y') . ')',
'headers' => ['Evento', 'Data', 'Tipo', 'Gruppi', 'Responsabili', 'Luogo'],
'rows' => array_map(fn($r) => [$r['nome'], $r['data'], $r['tipo'], $r['gruppi'], $r['responsabili'], $r['luogo']], $rows),
'summary' => 'Eventi nel mese: ' . $eventi->count(),
'detail_rows' => $rows,
];
}
protected function reportDocumentiPerTipo(): array
{
$rows = Documento::select('tipologia', DB::raw('count(*) as totale'))
->groupBy('tipologia')
->orderBy('totale', 'desc')
->get();
return [
'title' => 'Documenti per Tipologia',
'headers' => ['Tipologia', 'Totale'],
'rows' => $rows->map(fn($r) => [ucfirst($r->tipologia), $r->totale])->toArray(),
'summary' => 'Totale documenti: ' . Documento::count(),
'detail_rows' => $rows->map(fn($r) => (array) $r)->toArray(),
];
}
protected function reportContattiPerTipo(): array
{
$rows = Contatto::select('tipo', DB::raw('count(*) as totale'))
->groupBy('tipo')
->orderBy('totale', 'desc')
->get();
return [
'title' => 'Contatti per Tipo',
'headers' => ['Tipo', 'Totale'],
'rows' => $rows->map(fn($r) => [ucfirst($r->tipo), $r->totale])->toArray(),
'summary' => 'Totale contatti: ' . Contatto::count(),
'detail_rows' => $rows->map(fn($r) => (array) $r)->toArray(),
];
}
protected function reportMailingListDettaglio(): array
{
$mailingLists = MailingList::with(['contatti.individuo'])->orderBy('nome')->get();
$rows = [];
foreach ($mailingLists as $lista) {
foreach ($lista->contatti as $contatto) {
$rows[] = [
'lista' => $lista->nome,
'stato' => $lista->attiva ? 'Attiva' : 'Disattiva',
'individuo' => $contatto->individuo?->nome_completo ?? 'N/D',
'email' => $contatto->email ?? '-',
];
}
}
return [
'title' => 'Dettaglio Mailing List',
'headers' => ['Lista', 'Stato', 'Individuo', 'Email'],
'rows' => array_map(fn($r) => [$r['lista'], $r['stato'], $r['individuo'], $r['email']], $rows),
'summary' => 'Liste: ' . $mailingLists->count() . ' | Contatti totali: ' . count($rows),
'detail_rows' => $rows,
];
}
protected function reportIndividuiSenzaContatti(): array
{
$individui = Individuo::doesntHave('contatti')
->orderBy('cognome')
->orderBy('nome')
->get();
$rows = $individui->map(function ($ind) {
return [
'codice' => $ind->codice_id,
'cognome' => $ind->cognome,
'nome' => $ind->nome,
'email' => $ind->email ?? '-',
];
})->toArray();
return [
'title' => 'Individui senza Contatti',
'headers' => ['Codice', 'Cognome', 'Nome', 'Email'],
'rows' => array_map(fn($r) => [$r['codice'], $r['cognome'], $r['nome'], $r['email']], $rows),
'summary' => 'Individui senza contatti: ' . $individui->count(),
'detail_rows' => $rows,
];
}
protected function reportIndividuiSenzaGruppo(): array
{
$individui = Individuo::doesntHave('gruppi')
->orderBy('cognome')
->orderBy('nome')
->get();
$rows = $individui->map(function ($ind) {
return [
'codice' => $ind->codice_id,
'cognome' => $ind->cognome,
'nome' => $ind->nome,
'email' => $ind->email ?? '-',
];
})->toArray();
return [
'title' => 'Individui senza Gruppo',
'headers' => ['Codice', 'Cognome', 'Nome', 'Email'],
'rows' => array_map(fn($r) => [$r['codice'], $r['cognome'], $r['nome'], $r['email']], $rows),
'summary' => 'Individui senza gruppo: ' . $individui->count(),
'detail_rows' => $rows,
];
}
protected function reportEventiPerGruppo(): array
{
$gruppi = Gruppo::with(['eventi'])->orderBy('nome')->get();
$rows = [];
foreach ($gruppi as $gruppo) {
if ($gruppo->eventi->count() > 0) {
foreach ($gruppo->eventi as $evento) {
$rows[] = [
'gruppo' => $gruppo->nome,
'evento' => $evento->nome_evento,
'data' => $evento->data_specifica?->format('d/m/Y') ?? '-',
'tipo' => ucfirst($evento->tipo_recorrenza ?? 'singolo'),
];
}
} else {
$rows[] = [
'gruppo' => $gruppo->nome,
'evento' => '-',
'data' => '-',
'tipo' => '-',
];
}
}
return [
'title' => 'Eventi per Gruppo',
'headers' => ['Gruppo', 'Evento', 'Data', 'Tipo'],
'rows' => array_map(fn($r) => [$r['gruppo'], $r['evento'], $r['data'], $r['tipo']], $rows),
'summary' => 'Gruppi: ' . $gruppi->count(),
'detail_rows' => $rows,
];
}
protected function reportDocumentiOrfani(): array
{
$documenti = Documento::whereNull('visibilita_target_id')
->orWhere('visibilita', 'pubblico')
->orderBy('nome_file')
->get();
$rows = $documenti->map(function ($doc) {
return [
'nome' => $doc->nome_file,
'tipologia' => ucfirst($doc->tipologia),
'visibilita' => ucfirst($doc->visibilita),
'data' => $doc->created_at->format('d/m/Y'),
];
})->toArray();
return [
'title' => 'Documenti Orfani',
'headers' => ['Nome', 'Tipologia', 'Visibilità', 'Data'],
'rows' => array_map(fn($r) => [$r['nome'], $r['tipologia'], $r['visibilita'], $r['data']], $rows),
'summary' => 'Documenti orfani: ' . $documenti->count(),
'detail_rows' => $rows,
];
}
protected function runCustomReport(\App\Models\ReportCustom $report): array
{
$tipo = $report->tipo_report;
$config = $report->config ?? [];
$query = match ($tipo) {
'individui' => Individuo::query(),
'gruppi' => Gruppo::query(),
'eventi' => Evento::query(),
'documenti' => Documento::query(),
'contatti' => Contatto::query(),
default => null,
};
if (!$query) {
return [
'title' => $report->nome,
'headers' => ['Errore'],
'rows' => [['Tipo report non valido']],
'summary' => 'Configurazione non valida',
'detail_rows' => [],
];
}
if (!empty($config['search'])) {
$search = $config['search'];
$query->where(function ($q) use ($search) {
foreach ($config['search_columns'] ?? [] as $col) {
$q->orWhere($col, 'like', '%' . $search . '%');
}
});
}
if (!empty($config['sort_by'])) {
$query->orderBy($config['sort_by'], $config['sort_direction'] ?? 'asc');
}
if (!empty($config['limit'])) {
$query->limit((int) $config['limit']);
}
$results = $query->get();
$headers = !empty($config['columns']) ? $config['columns'] : array_keys($results->first()?->toArray() ?? []);
$rows = $results->map(function ($item) use ($headers) {
$row = [];
foreach ($headers as $col) {
$value = $item->{$col} ?? '';
if ($value instanceof \Carbon\Carbon) {
$value = $value->format('d/m/Y');
}
$row[] = $value;
}
return $row;
})->toArray();
return [
'title' => $report->nome,
'headers' => $headers,
'rows' => $rows,
'summary' => 'Risultati: ' . count($rows),
'detail_rows' => $results->map(fn($r) => $r->toArray())->toArray(),
];
}
}
@@ -0,0 +1,133 @@
<?php
namespace App\Http\Controllers;
use App\Models\VistaReport;
use Illuminate\Http\Request;
class VistaReportController extends Controller
{
public function index()
{
$this->authorizeRead('viste');
$viste = VistaReport::where('user_id', auth()->id())
->orderBy('tipo')
->orderBy('nome')
->get();
return view('viste.index', compact('viste'));
}
public function showJson(VistaReport $vistaReport)
{
$this->authorizeRead('viste');
if ($vistaReport->user_id !== auth()->id()) {
abort(403);
}
return response()->json($vistaReport);
}
public function store(Request $request)
{
$this->authorizeWrite('viste');
$data = $request->validate([
'nome' => 'required|string|max:255',
'tipo' => 'required|in:individui,gruppi,documenti,eventi',
'colonne_visibili' => 'nullable|array',
'colonne_ordinamento' => 'nullable|array',
'filtri' => 'nullable|array',
'ricerca' => 'nullable|string',
'is_default' => 'nullable|boolean',
]);
$isDefault = filter_var($data['is_default'] ?? false, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? false;
if ($isDefault) {
VistaReport::where('user_id', auth()->id())
->where('tipo', $data['tipo'])
->update(['is_default' => false]);
}
$vista = VistaReport::create([
'user_id' => auth()->id(),
'nome' => $data['nome'],
'tipo' => $data['tipo'],
'colonne_visibili' => $data['colonne_visibili'] ?? null,
'colonne_ordinamento' => $data['colonne_ordinamento'] ?? null,
'filtri' => $data['filtri'] ?? null,
'ricerca' => $data['ricerca'] ?? null,
'is_default' => $isDefault,
]);
if ($request->expectsJson()) {
return response()->json(['success' => true, 'message' => 'Vista salvata.']);
}
return back()->with('success', 'Vista salvata.');
}
public function update(Request $request, VistaReport $vistaReport)
{
$this->authorizeWrite('viste');
if ($vistaReport->user_id !== auth()->id()) {
abort(403);
}
$data = $request->validate([
'nome' => 'required|string|max:255',
'colonne_visibili' => 'nullable|array',
'colonne_ordinamento' => 'nullable|array',
'filtri' => 'nullable|array',
'ricerca' => 'nullable|string',
'is_default' => 'nullable|boolean',
]);
if (!empty($data['is_default'])) {
VistaReport::where('user_id', auth()->id())
->where('tipo', $vistaReport->tipo)
->where('id', '!=', $vistaReport->id)
->update(['is_default' => false]);
}
$vistaReport->update($data);
return back()->with('success', 'Vista aggiornata.');
}
public function setDefault(Request $request, VistaReport $vistaReport)
{
$this->authorizeWrite('viste');
if ($vistaReport->user_id !== auth()->id()) {
abort(403);
}
VistaReport::where('user_id', auth()->id())
->where('tipo', $vistaReport->tipo)
->update(['is_default' => false]);
$vistaReport->update(['is_default' => true]);
return back()->with('success', 'Vista predefinita impostata.');
}
public static function getDefaultForUser($tipo)
{
return VistaReport::where('user_id', auth()->id())
->where('tipo', $tipo)
->where('is_default', true)
->first();
}
public function destroy(VistaReport $vistaReport)
{
$this->authorizeDelete('viste');
if ($vistaReport->user_id !== auth()->id()) {
abort(403);
}
$vistaReport->delete();
return back()->with('success', 'Vista eliminata.');
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class AdminOnly
{
public function handle(Request $request, Closure $next): Response
{
if (!auth()->check()) {
return redirect('/login');
}
if (!auth()->user()->isSuperAdmin()) {
abort(403, 'Accesso riservato agli amministratori.');
}
return $next($request);
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class CheckPermission
{
public function handle(Request $request, Closure $next, string $module, string $level = 'read'): Response
{
if (!auth()->check()) {
return redirect('/login');
}
$user = auth()->user();
if ($user->isSuperAdmin()) {
return $next($request);
}
$requiredLevel = match ($level) {
'read' => 1,
'write', 'create', 'edit' => 2,
'delete' => 2,
default => 1,
};
if (!$user->hasPermission($module, $requiredLevel)) {
abort(403, 'Permesso insufficiente per questa operazione.');
}
return $next($request);
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class ForceHttps
{
public function handle(Request $request, Closure $next): Response
{
if (env('FORCE_HTTPS', false)) {
$scheme = $request->headers->get('X-Forwarded-Proto', $request->getScheme());
if ($scheme === 'https' && !$request->isSecure()) {
return redirect()->secure($request->getRequestUri());
}
}
return $next($request);
}
}
+83
View File
@@ -0,0 +1,83 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ActivityLog extends Model
{
public $timestamps = false;
protected $fillable = [
'user_id',
'action',
'module',
'description',
'details',
'ip_address',
'created_at',
];
protected $casts = [
'details' => 'array',
'created_at' => 'datetime',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public static function log(
string $action,
string $module,
?string $description = null,
?array $details = null
): self {
return static::create([
'user_id' => auth()->id(),
'action' => $action,
'module' => $module,
'description' => $description,
'details' => $details,
'ip_address' => request()->ip(),
'created_at' => now(),
]);
}
public static function logCreate(Model $model, ?string $description = null): self
{
return static::log(
'create',
$model::class,
$description ?? 'Creato ' . class_basename($model),
['new' => $model->toArray()]
);
}
public static function logUpdate(Model $model, array $old, ?string $description = null): self
{
return static::log(
'update',
$model::class,
$description ?? 'Modificato ' . class_basename($model),
['old' => $old, 'new' => $model->toArray()]
);
}
public static function logDelete(Model $model, ?string $description = null): self
{
return static::log(
'delete',
$model::class,
$description ?? 'Eliminato ' . class_basename($model),
['deleted' => $model->toArray()]
);
}
public static function logLogin(): self
{
return static::log('login', 'auth', 'Login effettuato');
}
}
+125
View File
@@ -0,0 +1,125 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class AppSetting extends Model
{
protected $table = 'app_settings';
protected $fillable = [
'logo',
'logo_small',
'logo_path',
'logo_small_path',
'nome_applicazione',
'nome_organizzazione',
'footer_text',
'app_version',
'dashboard_welcome',
'show_version',
];
public static function getSetting(string $key, $default = null)
{
$setting = self::first();
return $setting?->{$key} ?? $default;
}
public static function getLogo(): ?string
{
return self::getSetting('logo');
}
public static function getLogoSmall(): ?string
{
return self::getSetting('logo_small');
}
public static function getAppName(): string
{
return self::getSetting('nome_applicazione', 'Glastree');
}
public static function getOrgName(): string
{
return self::getSetting('nome_organizzazione', 'Parrocchia');
}
public static function getFooterText(): string
{
$footer = self::getSetting('footer_text');
if ($footer) {
return $footer;
}
$appName = self::getAppName();
$year = date('Y');
return "© {$year} {$appName}";
}
public static function getAppVersion(): ?string
{
return self::getSetting('app_version');
}
public static function getDashboardWelcome(): ?string
{
return self::getSetting('dashboard_welcome');
}
public static function shouldShowVersion(): bool
{
return (bool) self::getSetting('show_version', false);
}
public function getLogoUrlInstance(): ?string
{
if ($this->logo && file_exists(public_path('storage/' . $this->logo))) {
return asset('storage/' . $this->logo);
}
return null;
}
public function getLogoSmallUrlInstance(): ?string
{
if ($this->logo_small && file_exists(public_path('storage/' . $this->logo_small))) {
return asset('storage/' . $this->logo_small);
}
return null;
}
public function getLogoPath(): ?string
{
if ($this->logo_path && file_exists(public_path('storage/' . $this->logo_path))) {
return asset('storage/' . $this->logo_path);
}
if ($this->logo && file_exists(public_path('storage/' . $this->logo))) {
return asset('storage/' . $this->logo);
}
return null;
}
public function getLogoSmallPath(): ?string
{
if ($this->logo_small_path && file_exists(public_path('storage/' . $this->logo_small_path))) {
return asset('storage/' . $this->logo_small_path);
}
if ($this->logo_small && file_exists(public_path('storage/' . $this->logo_small))) {
return asset('storage/' . $this->logo_small);
}
return null;
}
public static function getLogoUrl(): ?string
{
$setting = self::first();
return $setting?->getLogoPath();
}
public static function getLogoSmallUrl(): ?string
{
$setting = self::first();
return $setting?->getLogoSmallPath();
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Comune extends Model
{
protected $table = 'comuni';
protected $fillable = ['nome', 'codice_istat', 'cap', 'sigla_provincia', 'regione', 'latitudine', 'longitudine'];
public static function search(string $query)
{
return static::where('nome', 'like', "%{$query}%")
->orderBy('nome')
->limit(20)
->get();
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Contatto extends Model
{
protected $table = 'contatti';
protected $fillable = ['individuo_id', 'tipo', 'valore', 'etichetta', 'is_primary'];
protected $casts = [
'is_primary' => 'boolean',
];
public function individuo(): BelongsTo
{
return $this->belongsTo(Individuo::class);
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Diocesi extends Model
{
protected $table = 'diocesi';
protected $fillable = ['nome', 'regione'];
public function gruppi(): HasMany
{
return $this->hasMany(Gruppo::class);
}
}
+61
View File
@@ -0,0 +1,61 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Documento extends Model
{
protected $table = 'documenti';
protected $fillable = [
'tenant_id', 'user_id', 'nome_file', 'file_path', 'tipo', 'tipologia',
'visibilita', 'visibilita_target_id', 'visibilita_target_type',
'mime_type', 'dimensione', 'note'
];
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function target(): MorphTo
{
return $this->morphTo('target', 'visibilita_target_type', 'visibilita_target_id');
}
public function eventi(): BelongsToMany
{
return $this->belongsToMany(Evento::class, 'eventi_documenti')->withTimestamps();
}
public function gruppi(): BelongsToMany
{
return $this->belongsToMany(Gruppo::class, 'gruppi_documenti')->withTimestamps();
}
public function individui(): BelongsToMany
{
return $this->belongsToMany(Individuo::class, 'individui_documenti')->withTimestamps();
}
public function isVisibilePerIndividuo(Individuo $individuo): bool
{
if ($this->visibilita === 'pubblico') return true;
if ($this->visibilita === 'individuo' && $this->visibilita_target_id === $individuo->id) return true;
if ($this->visibilita === 'gruppo' && $this->visibilita_target_type === Gruppo::class) {
return $individuo->gruppi()->where('gruppi.id', $this->visibilita_target_id)->exists();
}
return false;
}
}
+80
View File
@@ -0,0 +1,80 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\Storage;
class EmailAttachment extends Model
{
protected $table = 'email_attachments';
protected $fillable = [
'email_message_id', 'filename', 'mime_type', 'size', 'file_path',
'is_saved_to_documenti', 'documenti_id'
];
protected $casts = [
'is_saved_to_documenti' => 'boolean',
];
public function message(): BelongsTo
{
return $this->belongsTo(EmailMessage::class, 'email_message_id');
}
public function documento(): BelongsTo
{
return $this->belongsTo(Documento::class, 'documenti_id');
}
public function getUrlAttribute(): string
{
return route('email.attachment.download', $this->id);
}
public function saveToDocumenti(): ?Documento
{
if ($this->is_saved_to_documenti || !$this->documenti_id) {
return $this->documento;
}
$documento = Documento::create([
'nome_file' => $this->filename,
'file_path' => $this->file_path,
'tipo' => 'upload',
'tipologia' => 'email_attachment',
'mime_type' => $this->mime_type,
'dimensione' => $this->size,
'note' => 'Allegato email: ' . ($this->message->subject ?? ''),
]);
$this->update(['is_saved_to_documenti' => true, 'documenti_id' => $documento->id]);
return $documento;
}
public function getIconAttribute(): string
{
if (str_starts_with($this->mime_type, 'image/')) {
return 'fa-image';
}
if (str_starts_with($this->mime_type, 'video/')) {
return 'fa-video';
}
if (str_starts_with($this->mime_type, 'audio/')) {
return 'fa-music';
}
if (str_contains($this->mime_type, 'pdf')) {
return 'fa-file-pdf';
}
if (str_contains($this->mime_type, 'word')) {
return 'fa-file-word';
}
if (str_contains($this->mime_type, 'excel') || str_contains($this->mime_type, 'spreadsheet')) {
return 'fa-file-excel';
}
return 'fa-file';
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class EmailFolder extends Model
{
protected $table = 'email_folders';
protected $fillable = ['name', 'imap_folder_name', 'type', 'icon', 'color', 'sort_order', 'is_system'];
protected $casts = [
'is_system' => 'boolean',
];
public function messages(): HasMany
{
return $this->hasMany(EmailMessage::class, 'email_folder_id')->orderBy('received_at', 'desc');
}
public function unreadCount(): int
{
return $this->messages()->where('is_read', false)->count();
}
public static function getSystemFolders(): array
{
return [
['name' => 'Inbox', 'imap_folder_name' => 'INBOX', 'type' => 'inbox', 'icon' => 'fa-inbox', 'color' => 'primary', 'sort_order' => 0, 'is_system' => true],
['name' => 'Invio', 'imap_folder_name' => 'Sent', 'type' => 'sent', 'icon' => 'fa-paper-plane', 'color' => 'success', 'sort_order' => 1, 'is_system' => true],
['name' => 'Bozze', 'imap_folder_name' => 'Drafts', 'type' => 'drafts', 'icon' => 'fa-edit', 'color' => 'warning', 'sort_order' => 2, 'is_system' => true],
['name' => 'Cestino', 'imap_folder_name' => 'Trash', 'type' => 'trash', 'icon' => 'fa-trash', 'color' => 'danger', 'sort_order' => 3, 'is_system' => true],
['name' => 'Archivio', 'imap_folder_name' => 'Archive', 'type' => 'archive', 'icon' => 'fa-archive', 'color' => 'secondary', 'sort_order' => 4, 'is_system' => true],
['name' => 'Importanti', 'imap_folder_name' => 'Starred', 'type' => 'starred', 'icon' => 'fa-star', 'color' => 'warning', 'sort_order' => 5, 'is_system' => true],
];
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class EmailMessage extends Model
{
protected $table = 'email_messages';
protected $fillable = [
'email_folder_id', 'message_id', 'subject', 'from_name', 'from_email',
'to_name', 'to_email', 'cc', 'bcc', 'body_text', 'body_html',
'is_read', 'is_starred', 'is_important', 'is_sent', 'is_draft', 'is_trash',
'received_at', 'sent_at', 'imap_uid'
];
protected $casts = [
'is_read' => 'boolean',
'is_starred' => 'boolean',
'is_important' => 'boolean',
'is_sent' => 'boolean',
'is_draft' => 'boolean',
'is_trash' => 'boolean',
'received_at' => 'datetime',
'sent_at' => 'datetime',
];
public function folder(): BelongsTo
{
return $this->belongsTo(EmailFolder::class, 'email_folder_id');
}
public function attachments(): HasMany
{
return $this->hasMany(EmailAttachment::class, 'email_message_id');
}
public function getExcerptAttribute(): string
{
$body = strip_tags($this->body_text ?? $this->body_html ?? '');
return mb_strimwidth($body, 0, 100, '...');
}
public function getBodyForDisplayAttribute(): string
{
if ($this->body_html) {
return $this->body_html;
}
return nl2br(e($this->body_text ?? ''));
}
public function scopeUnread($query)
{
return $query->where('is_read', false);
}
public function scopeInbox($query)
{
return $query->whereHas('folder', fn($q) => $q->where('type', 'inbox'));
}
public function scopeSent($query)
{
return $query->where('is_sent', true);
}
}
+137
View File
@@ -0,0 +1,137 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Crypt;
class EmailSetting extends Model
{
protected $table = 'email_settings';
protected $fillable = [
'imap_host', 'imap_port', 'imap_encryption', 'imap_username', 'imap_password',
'smtp_host', 'smtp_port', 'smtp_encryption', 'smtp_username', 'smtp_password',
'email_address', 'email_name', 'reply_to', 'sync_interval_minutes',
'last_sync_at', 'is_active', 'signature', 'signature_enabled'
];
protected $casts = [
'is_active' => 'boolean',
'signature_enabled' => 'boolean',
'last_sync_at' => 'datetime',
];
public function folders(): HasMany
{
return $this->hasMany(EmailFolder::class)->orderBy('sort_order');
}
public function getImapConfig(): array
{
return [
'host' => $this->imap_host,
'port' => $this->imap_port,
'encryption' => $this->imap_encryption,
'username' => $this->imap_username,
'password' => $this->imap_password,
];
}
public function getDecryptedPassword(): string
{
if (empty($this->imap_password)) {
return '';
}
try {
return Crypt::decryptString($this->imap_password);
} catch (\Exception $e) {
return $this->imap_password;
}
}
public function getDecryptedSmtpPassword(): string
{
if (empty($this->smtp_password)) {
return $this->getDecryptedPassword();
}
try {
return Crypt::decryptString($this->smtp_password);
} catch (\Exception $e) {
return $this->smtp_password;
}
}
public function getSmtpConfig(): array
{
return [
'host' => $this->smtp_host ?? $this->imap_host,
'port' => $this->smtp_port ?? 587,
'encryption' => $this->smtp_encryption ?? 'tls',
'username' => $this->smtp_username ?? $this->imap_username,
'password' => $this->smtp_password ?? $this->imap_password,
];
}
public function getImapClient(): ?\DirectoryTree\ImapEngine\Mailbox
{
if (!$this->imap_host || !$this->imap_username) {
return null;
}
try {
$encryption = match ($this->imap_encryption) {
'ssl' => 'ssl',
'tls' => 'tls',
default => 'none',
};
return new \DirectoryTree\ImapEngine\Mailbox([
'host' => $this->imap_host,
'port' => $this->imap_port ?? 993,
'encryption' => $encryption,
'validate_cert' => false,
'username' => $this->imap_username,
'password' => $this->getDecryptedPassword(),
]);
} catch (\Exception $e) {
return null;
}
}
public function testConnection(): array
{
try {
$mailbox = $this->getImapClient();
if (!$mailbox) {
return ['success' => false, 'message' => 'Configurazione IMAP non valida'];
}
$mailbox->connect();
$mailbox->disconnect();
return [
'success' => true,
'message' => 'Connessione IMAP riuscita! Server: ' . $this->imap_host
];
} catch (\Exception $e) {
$message = $e->getMessage();
if (strpos($message, 'Undefined property') !== false) {
return ['success' => true, 'message' => 'Connessione IMAP riuscita! (Server: ' . $this->imap_host . ')'];
}
return ['success' => false, 'message' => $message];
}
}
public static function getActive()
{
return self::where('is_active', true)->first();
}
public function getSignature(): ?string
{
return $this->signature;
}
}
+267
View File
@@ -0,0 +1,267 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Evento extends Model
{
protected $table = 'eventi';
protected $fillable = [
'tenant_id',
'nome_evento',
'descrizione_evento',
'tipo_evento',
'tipo_recorrenza',
'giorno_settimana',
'giorno_mese',
'occorrenza_mese',
'mesi_recorrenza',
'mese_annuale',
'ora_inizio',
'data_specifica',
'durata_minuti',
'descrizione',
'note',
'is_incontro_gruppo',
'luogo_indirizzo',
'luogo_url_maps',
];
protected $casts = [
'data_specifica' => 'date',
'ora_inizio' => 'datetime:H:i',
'is_incontro_gruppo' => 'boolean',
];
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
public function gruppi(): BelongsToMany
{
return $this->belongsToMany(Gruppo::class, 'eventi_gruppi')->withTimestamps();
}
public function responsabili(): BelongsToMany
{
return $this->belongsToMany(Individuo::class, 'eventi_responsabili')->withTimestamps();
}
public function documenti(): BelongsToMany
{
return $this->belongsToMany(Documento::class, 'eventi_documenti')->withTimestamps();
}
public function isRicorrente(): bool
{
return $this->tipo_recorrenza !== null && $this->tipo_recorrenza !== 'singolo';
}
public function isSingolo(): bool
{
return $this->tipo_recorrenza === 'singolo' || $this->tipo_recorrenza === null;
}
public function isGlobale(): bool
{
return $this->gruppi()->count() === 0;
}
public function isEventoRicorrenteSettimanale(): bool
{
return $this->tipo_recorrenza === 'settimanale';
}
public function isEventoRicorrenteMensile(): bool
{
return $this->tipo_recorrenza === 'mensile';
}
public function isEventoRicorrenteAnnuale(): bool
{
return $this->tipo_recorrenza === 'annuale';
}
public function isEventoAltro(): bool
{
return $this->tipo_recorrenza === 'altro';
}
public function isIncroGruppo(): bool
{
return $this->is_incontro_gruppo === true;
}
public function getGiornoSettimanaLabelAttribute(): string
{
$giorni = [
0 => 'Domenica',
1 => 'Lunedì',
2 => 'Martedì',
3 => 'Mercoledì',
4 => 'Giovedì',
5 => 'Venerdì',
6 => 'Sabato',
];
return $giorni[$this->giorno_settimana] ?? '-';
}
public function getGiornoSettimanaList(): array
{
return [
0 => ['value' => 0, 'label' => 'Domenica'],
1 => ['value' => 1, 'label' => 'Lunedì'],
2 => ['value' => 2, 'label' => 'Martedì'],
3 => ['value' => 3, 'label' => 'Mercoledì'],
4 => ['value' => 4, 'label' => 'Giovedì'],
5 => ['value' => 5, 'label' => 'Venerdì'],
6 => ['value' => 6, 'label' => 'Sabato'],
];
}
public function getOccorrenzeMensiliList(): array
{
$giorni = ['Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato', 'Domenica'];
$list = [];
for ($occ = 1; $occ <= 4; $occ++) {
foreach ($giorni as $idx => $giorno) {
$value = $occ . ',' . $idx;
$label = $occ . '° ' . $giorno;
$list[] = ['value' => $value, 'label' => $label];
}
}
return $list;
}
public function getOccorrenzaMensileLabelAttribute(): string
{
if (!$this->occorrenza_mese) {
return '-';
}
$parts = explode(',', $this->occorrenza_mese);
if (count($parts) !== 2) {
return '-';
}
$occorrenza = (int) $parts[0];
$giorno = (int) $parts[1];
$giorni = ['Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato', 'Domenica'];
return $occorrenza . '° ' . ($giorni[$giorno] ?? '-');
}
public function getMesiList(): array
{
return [
1 => ['value' => 1, 'label' => 'Gennaio'],
2 => ['value' => 2, 'label' => 'Febbraio'],
3 => ['value' => 3, 'label' => 'Marzo'],
4 => ['value' => 4, 'label' => 'Aprile'],
5 => ['value' => 5, 'label' => 'Maggio'],
6 => ['value' => 6, 'label' => 'Giugno'],
7 => ['value' => 7, 'label' => 'Luglio'],
8 => ['value' => 8, 'label' => 'Agosto'],
9 => ['value' => 9, 'label' => 'Settembre'],
10 => ['value' => 10, 'label' => 'Ottobre'],
11 => ['value' => 11, 'label' => 'Novembre'],
12 => ['value' => 12, 'label' => 'Dicembre'],
];
}
public function getMesiRecorrenzaLabelAttribute(): string
{
if (!$this->mesi_recorrenza) {
return '-';
}
$mesiSelezionati = explode(',', $this->mesi_recorrenza);
$mesiLabels = $this->getMesiList();
$labels = array_map(function ($mese) use ($mesiLabels) {
return $mesiLabels[$mese]['label'] ?? $mese;
}, $mesiSelezionati);
return implode(', ', $labels);
}
public function getMeseAnnualeLabelAttribute(): string
{
if (!$this->mese_annuale) {
return '-';
}
$mesiLabels = $this->getMesiList();
return $mesiLabels[$this->mese_annuale]['label'] ?? '-';
}
public function getPeriodicitaLabelAttribute(): string
{
$labels = [
'settimanale' => 'Settimanale',
'mensile' => 'Mensile',
'annuale' => 'Annuale',
'altro' => 'Altro',
'singolo' => 'Singolo',
];
return $labels[$this->tipo_recorrenza] ?? '-';
}
public function getInfoRicorrenzaAttribute(): string
{
if ($this->tipo_recorrenza === 'settimanale') {
return 'Ogni ' . $this->giorno_settimana_label;
}
if ($this->tipo_recorrenza === 'mensile') {
$info = [];
if ($this->occorrenza_mese) {
$info[] = $this->occorrenza_mensile_label;
}
if ($this->mesi_recorrenza) {
$info[] = '(' . $this->mesi_recorrenza_label . ')';
}
return implode(' ', $info) ?: '-';
}
if ($this->tipo_recorrenza === 'annuale') {
if ($this->mese_annuale) {
return $this->mese_annuale_label;
}
}
if ($this->tipo_recorrenza === 'altro') {
return $this->giorno_settimana_label ?? '-';
}
return '-';
}
public function getInfoIncroAttribute(): ?string
{
if (!$this->is_incontro_gruppo) {
return null;
}
$info = [];
if ($this->isRicorrente()) {
$info[] = $this->info_ricorrenza;
} else {
$info[] = $this->data_specifica?->format('d/m/Y') ?? '-';
}
if ($this->ora_inizio) {
$info[] = 'ore ' . $this->ora_inizio->format('H:i');
}
return implode(' ', $info);
}
}
+111
View File
@@ -0,0 +1,111 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Gruppo extends Model
{
protected $table = 'gruppi';
protected $fillable = [
'tenant_id', 'parent_id', 'diocesi_id', 'responsabile_ids',
'nome', 'descrizione', 'indirizzo_incontro', 'cap_incontro',
'città_incontro', 'sigla_provincia_incontro', 'mappa_posizione', 'metadata'
];
protected $casts = [
'metadata' => 'array',
'responsabile_ids' => 'array',
];
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
public function parent(): BelongsTo
{
return $this->belongsTo(Gruppo::class, 'parent_id');
}
public function children(): HasMany
{
return $this->hasMany(Gruppo::class, 'parent_id');
}
public function diocesi(): BelongsTo
{
return $this->belongsTo(Diocesi::class);
}
public function individui(): BelongsToMany
{
return $this->belongsToMany(Individuo::class, 'gruppo_individuo')
->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione')
->withTimestamps();
}
public function getResponsabiliIds(): array
{
if (empty($this->responsabile_ids)) {
return [];
}
if (is_array($this->responsabile_ids)) {
return $this->responsabile_ids;
}
return json_decode($this->responsabile_ids, true) ?? [];
}
public function getResponsabili(): \Illuminate\Database\Eloquent\Collection
{
$ids = $this->getResponsabiliIds();
if (empty($ids)) {
return new \Illuminate\Database\Eloquent\Collection();
}
return Individuo::whereIn('id', $ids)->get();
}
public function eventi(): BelongsToMany
{
return $this->belongsToMany(Evento::class, 'eventi_gruppi')->withTimestamps();
}
public function documenti(): HasMany
{
return $this->hasMany(Documento::class, 'visibilita_target_id')
->where('visibilita_target_type', self::class);
}
public function getAncestors(): array
{
$ancestors = [];
$current = $this->parent;
while ($current) {
$ancestors[] = $current;
$current = $current->parent;
}
return $ancestors;
}
public function getDescendants(): array
{
$descendants = [];
foreach ($this->children as $child) {
$descendants[] = $child;
$descendants = array_merge($descendants, $child->getDescendants());
}
return $descendants;
}
public function getFullPathAttribute(): string
{
$path = [$this->nome];
foreach (array_reverse($this->getAncestors()) as $ancestor) {
$path[] = $ancestor->nome;
}
return implode(' > ', array_reverse($path));
}
}
+142
View File
@@ -0,0 +1,142 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
class Individuo extends Model
{
protected $table = 'individui';
protected $fillable = [
'tenant_id', 'codice_id', 'cognome', 'nome', 'data_nascita',
'indirizzo', 'cap', 'città', 'sigla_provincia', 'genere',
'tipo_documento', 'numero_documento', 'scadenza_documento', 'note', 'avatar_path'
];
protected $casts = [
'data_nascita' => 'date',
'scadenza_documento' => 'date',
];
public function hasDocumentoScaduto(): bool
{
return $this->scadenza_documento && $this->scadenza_documento->isPast();
}
public function hasDocumentoScadeEntroGiorni(int $giorni): bool
{
if (!$this->scadenza_documento || $this->scadenza_documento->isPast()) {
return false;
}
return $this->scadenza_documento->diffInDays(null, true) <= $giorni;
}
public function getGiorniScadenzaDocumentoAttribute(): ?int
{
if (!$this->scadenza_documento) {
return null;
}
return (int) round($this->scadenza_documento->diffInDays(null, true));
}
public static function boot(): void
{
parent::boot();
static::creating(function ($individuo) {
if (empty($individuo->codice_id)) {
$individuo->codice_id = static::generaCodiceId();
}
});
}
public static function generaCodiceId(): string
{
do {
$codice = str_pad(random_int(1, 99999), 5, '0', STR_PAD_LEFT);
} while (static::where('codice_id', $codice)->exists());
return $codice;
}
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
public function contatti(): HasMany
{
return $this->hasMany(Contatto::class)->orderBy('is_primary', 'desc');
}
public function gruppi(): BelongsToMany
{
return $this->belongsToMany(Gruppo::class, 'gruppo_individuo')
->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione')
->withTimestamps();
}
public function getRuoloIdsForGruppo($gruppoId): array
{
$pivot = $this->gruppi()->wherePivot('gruppo_id', $gruppoId)->first()?->pivot;
if ($pivot && $pivot->ruolo_ids) {
return json_decode($pivot->ruolo_ids, true) ?? [];
}
return [];
}
public function getRuoliForGruppo($gruppoId): \Illuminate\Database\Eloquent\Collection
{
$ids = $this->getRuoloIdsForGruppo($gruppoId);
return Ruolo::findByIds($ids);
}
public function getRuoliNamesForGruppo($gruppoId): string
{
return Ruolo::namesByIds($this->getRuoloIdsForGruppo($gruppoId));
}
public function mailingContacts(): HasMany
{
return $this->hasMany(MailingContact::class);
}
public function eventiResponsabili(): BelongsToMany
{
return $this->belongsToMany(Evento::class, 'eventi_responsabili');
}
public function avatar(): HasOne
{
return $this->hasOne(Documento::class, 'visibilita_target_id')
->where('tipologia', 'avatar')
->where('visibilita_target_type', self::class);
}
public function getNomeCompletoAttribute(): string
{
return $this->cognome . ' ' . $this->nome;
}
public function getEmailPrimariaAttribute(): ?string
{
return $this->contatti()->where('tipo', 'email')->where('is_primary', true)->first()?->valore
?? $this->contatti()->where('tipo', 'email')->first()?->valore;
}
public function getTelefonoPrimarioAttribute(): ?string
{
return $this->contatti()->whereIn('tipo', ['telefono', 'cellulare'])->where('is_primary', true)->first()?->valore
?? $this->contatti()->whereIn('tipo', ['telefono', 'cellulare'])->first()?->valore;
}
public function documenti(): HasMany
{
return $this->hasMany(Documento::class, 'visibilita_target_id')
->where('visibilita_target_type', self::class);
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class MailingContact extends Model
{
protected $table = 'mailing_contacts';
protected $fillable = ['mailing_list_id', 'individuo_id', 'opt_in', 'opt_in_data', 'opt_out_data'];
protected $casts = [
'opt_in' => 'boolean',
'opt_in_data' => 'datetime',
'opt_out_data' => 'datetime',
];
public function mailingList(): BelongsTo
{
return $this->belongsTo(MailingList::class);
}
public function individuo(): BelongsTo
{
return $this->belongsTo(Individuo::class);
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class MailingList extends Model
{
protected $table = 'mailing_lists';
protected $fillable = ['tenant_id', 'user_id', 'nome', 'descrizione', 'attiva'];
protected $casts = ['attiva' => 'boolean'];
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function contatti(): HasMany
{
return $this->hasMany(MailingContact::class);
}
public function messaggi(): HasMany
{
return $this->hasMany(MailingMessaggio::class);
}
public function getIndividui()
{
return Individuo::whereHas('mailingContacts', function ($q) {
$q->where('mailing_list_id', $this->id)->where('opt_in', true);
})->get();
}
}
+44
View File
@@ -0,0 +1,44 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class MailingMessaggio extends Model
{
protected $table = 'mailing_messaggi';
protected $fillable = [
'tenant_id', 'mailing_list_id', 'user_id', 'oggetto', 'corpo',
'canale', 'stato', 'inviato_al', 'totale_destinatari', 'invii_ok', 'invii_falliti'
];
protected $casts = [
'inviato_al' => 'datetime',
];
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
public function mailingList(): BelongsTo
{
return $this->belongsTo(MailingList::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function isInviato(): bool
{
return $this->stato === 'inviato';
}
public function canBeSent(): bool
{
return in_array($this->stato, ['bozza', 'fallito']);
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Notifica extends Model
{
protected $table = 'notifiche';
protected $fillable = ['user_id', 'tipo', 'titolo', 'messaggio', 'link', 'is_read', 'read_at'];
protected $casts = [
'is_read' => 'boolean',
'read_at' => 'datetime',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function markAsRead(): void
{
$this->update(['is_read' => true, 'read_at' => now()]);
}
public function scopeUnread($query)
{
return $query->where('is_read', false);
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ReportCustom extends Model
{
protected $table = 'report_custom';
protected $fillable = [
'user_id',
'nome',
'descrizione',
'tipo_report',
'config',
];
protected $casts = [
'config' => 'array',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
+68
View File
@@ -0,0 +1,68 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class RolePreset extends Model
{
protected $fillable = ['name', 'description', 'permissions'];
protected $casts = [
'permissions' => 'array',
];
public function users(): HasMany
{
return $this->hasMany(User::class, 'role_preset_id');
}
public static function defaultPermissions(): array
{
return [
'individui' => 1,
'gruppi' => 1,
'eventi' => 1,
'documenti' => 1,
'mailing' => 1,
'viste' => 1,
];
}
public static function adminPreset(): array
{
return [
'individui' => 2,
'gruppi' => 2,
'eventi' => 2,
'documenti' => 2,
'mailing' => 2,
'viste' => 2,
];
}
public static function operatorePreset(): array
{
return [
'individui' => 2,
'gruppi' => 2,
'eventi' => 2,
'documenti' => 1,
'mailing' => 1,
'viste' => 1,
];
}
public static function letturaPreset(): array
{
return [
'individui' => 1,
'gruppi' => 1,
'eventi' => 1,
'documenti' => 1,
'mailing' => 1,
'viste' => 1,
];
}
}
+55
View File
@@ -0,0 +1,55 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
class Ruolo extends Model
{
protected $table = 'ruoli';
protected $fillable = ['nome', 'descrizione', 'ordine', 'attiva'];
protected $casts = [
'attiva' => 'boolean',
'ordine' => 'integer',
];
public static function attive(): \Illuminate\Database\Eloquent\Collection
{
return static::where('attiva', true)->orderBy('ordine')->get();
}
public static function opzioni(): array
{
return static::attive()->pluck('nome', 'id')->toArray();
}
public static function findByIds(?array $ids): \Illuminate\Database\Eloquent\Collection
{
if (empty($ids)) {
return new \Illuminate\Database\Eloquent\Collection();
}
return static::whereIn('id', $ids)->get();
}
public static function namesByIds(?array $ids): string
{
if (empty($ids)) {
return '';
}
return static::whereIn('id', $ids)
->orderBy('ordine')
->pluck('nome')
->implode(', ');
}
public function getMembriCount(): int
{
return DB::table('gruppo_individuo')
->whereNotNull('ruolo_ids')
->whereRaw("JSON_CONTAINS(ruolo_ids, ?)", [json_encode($this->id)])
->count();
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Tenant extends Model
{
protected $table = 'tenants';
protected $fillable = ['nome', 'slug', 'email', 'telefono', 'note', 'attivo'];
public function individui(): HasMany
{
return $this->hasMany(Individuo::class);
}
public function gruppi(): HasMany
{
return $this->hasMany(Gruppo::class);
}
public function documenti(): HasMany
{
return $this->hasMany(Documento::class);
}
public function mailingLists(): HasMany
{
return $this->hasMany(MailingList::class);
}
public function eventi(): HasMany
{
return $this->hasMany(Evento::class);
}
public function users(): HasMany
{
return $this->hasMany(User::class);
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class TipologiaDocumento extends Model
{
protected $table = 'tipologie_documenti';
protected $fillable = ['nome', 'descrizione', 'ordine', 'attiva'];
protected $casts = [
'attiva' => 'boolean',
'ordine' => 'integer',
];
public function documenti(): HasMany
{
return $this->hasMany(Documento::class, 'tipologia', 'nome');
}
public static function attive(): \Illuminate\Database\Eloquent\Collection
{
return static::where('attiva', true)->orderBy('ordine')->get();
}
public static function opzioni(): array
{
return static::attive()->pluck('nome')->toArray();
}
}
+159
View File
@@ -0,0 +1,159 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
#[Fillable(['name', 'email', 'password', 'tenant_id', 'is_admin', 'permissions', 'role_preset_id', 'status'])]
#[Hidden(['password', 'remember_token'])]
class User extends Authenticatable
{
use HasFactory, Notifiable;
protected $fillable = ['name', 'email', 'password', 'tenant_id', 'is_admin', 'permissions', 'role_preset_id', 'status'];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'is_admin' => 'boolean',
'permissions' => 'array',
'status' => 'string',
];
}
public const STATUS_ACTIVE = 'active';
public const STATUS_SUSPENDED = 'suspended';
public const STATUS_PENDING = 'pending';
public const MODULES = ['individui', 'gruppi', 'eventi', 'documenti', 'mailing', 'viste', 'report'];
public const LEVEL_NONE = 0;
public const LEVEL_READ = 1;
public const LEVEL_FULL = 2;
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
public function rolePreset(): BelongsTo
{
return $this->belongsTo(RolePreset::class);
}
public function notifiche(): HasMany
{
return $this->hasMany(Notifica::class)->orderBy('created_at', 'desc');
}
public function unreadNotifiche(): HasMany
{
return $this->hasMany(Notifica::class)->where('is_read', false)->orderBy('created_at', 'desc');
}
public function unreadNotificheCount(): int
{
return $this->notifiche()->unread()->count();
}
public function activityLogs(): HasMany
{
return $this->hasMany(ActivityLog::class)->orderBy('created_at', 'desc');
}
public function isSuperAdmin(): bool
{
return $this->is_admin === true;
}
public static function firstUserIsAdmin(): bool
{
return self::count() === 1;
}
public function isActive(): bool
{
return $this->status !== self::STATUS_SUSPENDED;
}
public function getPermission(string $module): int
{
if ($this->isSuperAdmin()) {
return self::LEVEL_FULL;
}
return $this->permissions[$module] ?? self::LEVEL_NONE;
}
public function hasPermission(string $module, int $level = self::LEVEL_READ): bool
{
return $this->getPermission($module) >= $level;
}
public function canAccess(string $module): bool
{
return $this->getPermission($module) >= self::LEVEL_READ;
}
public function canManage(string $module): bool
{
return $this->getPermission($module) >= self::LEVEL_FULL;
}
public function canRead(string $module): bool
{
return $this->getPermission($module) >= self::LEVEL_READ;
}
public function canWrite(string $module): bool
{
return $this->getPermission($module) >= self::LEVEL_FULL;
}
public function canDelete(string $module): bool
{
return $this->getPermission($module) >= self::LEVEL_FULL;
}
public function setPermission(string $module, int $level): self
{
$permissions = $this->permissions ?? [];
$permissions[$module] = $level;
$this->permissions = $permissions;
return $this;
}
public function setPermissions(array $permissions): self
{
$this->permissions = $permissions;
return $this;
}
public function getPermissionsSummary(): array
{
$result = [];
foreach (self::MODULES as $module) {
$level = $this->getPermission($module);
$result[$module] = match ($level) {
self::LEVEL_NONE => 'Nessuno',
self::LEVEL_READ => 'Lettura',
self::LEVEL_FULL => 'Completo',
};
}
return $result;
}
public function applyRolePreset(RolePreset $preset): self
{
$this->role_preset_id = $preset->id;
$this->permissions = $preset->permissions;
return $this;
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class VistaReport extends Model
{
protected $table = 'viste_report';
protected $fillable = [
'user_id', 'nome', 'tipo', 'colonne_visibili',
'colonne_ordinamento', 'filtri', 'ricerca'
];
protected $casts = [
'colonne_visibili' => 'array',
'colonne_ordinamento' => 'array',
'filtri' => 'array',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Database\Eloquent\Relations\Relation;
use App\Models\Individuo;
use App\Models\Gruppo;
use App\Models\Evento;
use App\Models\MailingList;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
}
public function boot(): void
{
\Illuminate\Pagination\Paginator::defaultView('vendor.pagination.simple-bootstrap-4');
\Illuminate\Pagination\LengthAwarePaginator::defaultView('vendor.pagination.simple-bootstrap-4');
Relation::morphMap([
'individuo' => Individuo::class,
'gruppo' => Gruppo::class,
'evento' => Evento::class,
'mailing' => MailingList::class,
'associazione' => \App\Models\Associazione::class,
'federazione' => \App\Models\Federazione::class,
]);
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace App\Providers;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Route;
use Spatie\Permission\Models\Permission;
class AuthServiceProvider extends ServiceProvider
{
protected $policies = [
//
];
public function boot(): void
{
$this->registerPolicies();
Gate::before(function ($user, $ability) {
if ($user->isSuperAdmin()) {
return true;
}
return null;
});
foreach (Permission::where('guard_name', 'web')->get() as $permission) {
Gate::define($permission->name, function ($user) use ($permission) {
return $user->hasPermissionTo($permission);
});
}
}
}