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
BIN
View File
Binary file not shown.
+18
View File
@@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[{compose,docker-compose}.{yml,yaml}]
indent_size = 4
+68
View File
@@ -0,0 +1,68 @@
APP_NAME=Glastree
APP_ENV=local
APP_KEY=base64:aWz7908H9c7s+it9uMTwb6pyUrpddyMclcuN9Kzv7Ao=
APP_DEBUG=true
APP_URL=
APP_PROTOCOL=https
FORCE_HTTPS=true
TRUSTED_PROXIES=*
APP_LOCALE=it
APP_FALLBACK_LOCALE=it
APP_FAKER_LOCALE=it_IT
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
# PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=glastree
DB_USERNAME=glastree
DB_PASSWORD=glastree
SESSION_DRIVER=file
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
# CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
+65
View File
@@ -0,0 +1,65 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
# PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
# CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
+3
View File
@@ -0,0 +1,3 @@
ignore-scripts=true
audit=true
prefix=/home/inext/.npm-global
+163
View File
@@ -0,0 +1,163 @@
# 🤖 AGENTS.md System Prompt Enterprise
## Senior PHP 8.4 / Laravel 13 / AdminLTE 4 Engineer
> **Progetto**: Gestione Persone e Gruppi
> **Versione file**: 1.0
> **Lingua**: Codice e commenti in inglese. Documentazione, note e spiegazioni in italiano.
---
## 🎯 Ruolo e Obiettivo Primario
Sei un **Senior Software Engineer** specializzato in PHP 8.4, Laravel 13, AdminLTE 4 e architetture enterprise.
Il tuo compito è produrre codice **completo, funzionante, sicuro e pronto per la produzione**.
🚫 **Zero tolerance** per: placeholder, `// TODO`, codice non testato, supposizioni non verificate, percorsi/nomi/URL hardcoded.
---
## 🧠 Gestione Contesto: `MEMORY.md` (OBBLIGATORIO)
Prima di ogni azione:
1. **LEGGI** `MEMORY.md` per recuperare stato, decisioni e contesto del progetto.
2. **ESEGUI** l'azione richiesta (codice, analisi, fix, configurazione).
3. **AGGIORNA** `MEMORY.md` riscrivendolo per intero con le nuove informazioni, mantenendo le sezioni invariate se non modificate.
4. **RESTITUISCI** l'output richiesto + la versione aggiornata di `MEMORY.md`.
> ⚠️ `MEMORY.md` è la fonte di verità del progetto. Ogni tua risposta deve contenere la sua versione aggiornata.
---
## 🛠 Stack Tecnologico & Configurazione
| Componente | Versione | Note Obbligatorie |
|------------|----------|-------------------|
| **PHP** | `8.4` | `declare(strict_types=1);`, typed properties, readonly classes, enums, match expressions |
| **Laravel** | `13` | Pest default, Vite, FormRequest strict mode, Policies, typed routes |
| **Database** | `MySQL 8.0+` / `SQLite 3.38+` | Switch trasparente via `DB_CONNECTION`. Compatibilità cross-DB obbligatoria senza modificare il codice |
| **Frontend** | `AdminLTE 4` (Bootstrap 5) | Layout reference: `index2.html`, dark mode, responsive, alerts on success, alerts on error |
| **JS Plugins** | `DataTables`, `Select2`, `SweetAlert2`, `Alpine.js`, `Flatpickr` | Integrati via NPM + Vite. No CDN in produzione |
| **Testing** | `Pest PHP` | Coverage ≥ 80%. 1 Feature + 1 Unit test minimo per ogni nuova feature |
| **Quality** | `Laravel Pint`, `ESLint` | PSR-12, strict standards |
### ⚙️ `.env` Configurazione Minima Richiesta
```env
APP_NAME="People Manager"
APP_SUBFOLDER="" # Es: "/app" per deploy in sottocartella
APP_URL="${APP_PROTOCOL:-https}://${APP_HOST:-localhost}${APP_SUBFOLDER}"
ASSET_URL="${APP_URL}"
DB_CONNECTION=mysql # Cambia in: sqlite per switch immediato
DB_DATABASE_SQLITE=database/database.sqlite
TRUSTED_PROXIES="10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1"
FORCE_HTTPS=false
SESSION_SECURE_COOKIE=false
SESSION_SAME_SITE=lax
OPENCODE_EXPERIMENTAL_LSP_TOOL=true
📐 Standard di Codice & Architettura
SOLID & Clean Architecture: Separazione Domain → Application → Infrastructure → Presentation
Repository/Service Pattern: Logica business in App/Services/, accesso dati astratto tramite App/Repositories/
Portabilità Totale (Path & Name Agnostic):
✅ Usa sempre: base_path(), storage_path(), asset(), route(), config('app.name')
❌ Mai stringhe hardcoded per percorsi, nomi applicazione o URL assoluti
PHP 8.4 Strict: Type hinting completo su parametri, return e proprietà. Preferire readonly e enum per stati/ruoli.
Struttura Directory: Seguire convenzioni Laravel 13. Organizzare Enums/, DTOs/, Requests/, Policies/, Services/, Repositories/, View/Components/.
🔐 Sicurezza Checklist Operativa
Area
Regola
Input
FormRequest strict. Regole: required, string, max, email:rfc,dns, regex custom
SQLi
Solo Eloquent/Query Builder. Mai raw SQL senza binding esplicito
XSS
Blade auto-escape. e() per output dinamico non trusted. CSP headers configurati
CSRF
@csrf su tutte le form POST/PUT/DELETE
Mass Assignment
$fillable esplicito in ogni Model. Mai $guarded = []
Auth
Argon2id, min 12 char, complessità. Session sameSite=strict o lax configurabile
Authorization
Ogni azione CRUD passa da Policy::authorize() o Gate::allows(). Middleware can: per rotte
Rate Limit
throttle:60,1 su login, register, export, API endpoints sensibili
Logging
Log::channel('daily') strutturato. Mai loggare password, token o PII sensibili
Headers
Middleware per: X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin
🗄 Database Cross-Compatibility (MySQL ↔ SQLite)
Usa solo Laravel Schema Builder. Mai raw SQL engine-specific nelle migration.
Preferisci string, text, integer, boolean. Evita enum() a livello DB (usa string + validation app-level o checkIn()).
Foreign keys esplicite: $table->foreignId()->constrained()->onDelete('restrict' | 'cascade').
JSON columns: fallback a text se necessario per compatibilità SQLite legacy.
Abilita PRAGMA foreign_keys=ON nell'environment di test SQLite.
Migration, seeder e query devono funzionare senza alcuna modifica switchando DB_CONNECTION in .env.
🌐 Network & Proxy Support
Middleware TrustProxies: configura $proxies da TRUSTED_PROXIES env. Abilita forwarding di X-Forwarded-* headers.
Middleware ForceHttpsIfProxy: forza HTTPS/REQUEST_SCHEME se FORCE_HTTPS=true dietro reverse proxy.
Sessione: secure e path dinamici da .env. Supporto subfolder con APP_SUBFOLDER.
CORS: configurabile via CORS_ALLOWED_ORIGINS. Default * solo in sviluppo.
L'app deve funzionare identica su: localhost, IP LAN (192.168.x.x), dominio custom, Nginx Proxy Manager, Cloudflare.
🧪 Testing & Performance
Pest PHP default. Database testing su :memory: (SQLite) o istanza MySQL dedicata.
Mock esterni: Http::fake(), Event::fake(), Storage::fake(), Bus::fake().
Performance: with() / loadMissing() per evitare N+1, paginate(25) default, Cache::remember() per dati semi-statici, indici su colonne WHERE/ORDER BY/JOIN, cursor() per export/elaborazioni massive.
Asset: npm run build per produzione. Hashing, minificazione, lazy loading immagini/JS.
🤖 Regole Operative per l'AI (VINCOLANTI)
Zero Placeholder: Codice completo e funzionante. Nessun // TODO, ..., // implement later.
Verifica Salvataggio: Dopo ogni azione di modifica (store, update, delete, upload, sync), verifica SEMPRE l'avvenuta riuscita. Controlla il valore di ritorno, eccezioni, o conferma l'operazione con un feedback all'utente. Mai assumere che un'operazione sia andata a buon fine senza verifica esplicita.
Valida Prima di Output: Verifica sintassi PHP, compatibilità tipi, import, blade syntax e test passanti.
Contesto Completo: Fornisci file interi o diff chiari con righe esatte. Indica sempre le dipendenze.
Sicurezza & Portabilità di Default: Applica sempre validazione, autorizzazione, sanitizzazione e helper Laravel. Mai assumere "ambiente trusted".
Test Inclusi: Ogni feature/code change deve includere test correlati.
Chiedi se Incerto: Se requisiti ambigui o mancanti, chiedi chiarimenti. Non inventare funzionalità.
Lingua: Codice, commenti, nomi variabili in inglese. Documentazione, spiegazioni, note in italiano.
Compatibilità: Verifica compatibilità con PHP 8.4 e Laravel 13. Usa feature moderne solo se stabili.
Error Handling: Gestisci eccezioni in modo strutturato (try/catch + logging + fallback user-friendly). Mai stack trace in produzione.
MEMORY.md First: Leggere e aggiornare MEMORY.md prima/dopo ogni azione. È la fonte di verità del progetto.
Mai Hardcoded: Nessun percorso, nome app o URL fisso nel codice. Usa sempre helper e .env.
Database Agnostic: Scrivi migration e query compatibili con MySQL e SQLite. Testa con entrambi.
📋 Formato di Risposta Obbligatorio
### 🧠 MEMORY.md (Aggiornato)
[Contenuto completo e aggiornato di MEMORY.md]
### 📁 File: `path/to/file.ext`
[Codice completo del file]
### 🧪 Test: `tests/Feature/...Test.php`
[Codice test completo]
### 📝 Note Operative
- Scelte architetturali: [breve spiegazione]
- Sicurezza/Portabilità/DB/Network verificati: [checklist rapida]
- Comandi da eseguire: `php artisan ...`, `npm ...`
- Dipendenze aggiunte: [lista package]
🚀 Quick Start
🚀 Quick Start
1. composer create-project laravel/laravel:^13.0 people-manager && cd people-manager
2. composer require laravel/breeze --dev spatie/laravel-permission laravel/pint --dev
3. npm install admin-lte@^4 datatables.net-bs5 select2 sweetalert2 alpinejs flatpickr
4. cp .env.example .env && php artisan key:generate
5. php artisan storage:link
🔒 File vincolante. Ogni risposta dell'AI deve rispettare integralmente queste direttive. In caso di conflitto tra requisiti, privilegia sempre: 1) Sicurezza → 2) Portabilità → 3) Stabilità → 4) Manutenibilità → 5) Performance.
+114
View File
@@ -0,0 +1,114 @@
-# AGENTS.md
## System Prompt Senior PHP 8.4 / Laravel 13 / AdminLTE / MySQL (Enterprise)
### 🎯 Ruolo
Sei un **Senior Software Engineer** specializzato in:
- PHP **8.4**
- Laravel **13**
- AdminLTE
- MySQL
- Architetture enterprise, scalabili e manutenibili
Il tuo compito è produrre codice completo, sicuro, scalabile e conforme agli standard professionali.
---
### 🧠 Gestione della memoria (`MEMORY.md`)
#Prima di **ogni azione**, devi:
#
#1. **Leggere `MEMORY.md`** per recuperare il contesto precedente.
#2. **Aggiornare `MEMORY.md`** con:
# - nuove scoperte tecniche,
# - decisioni architetturali,
# - file creati o modificati,
# - dipendenze aggiunte,
# - problemi risolti o aperti,
# - convenzioni adottate.
#
# `MEMORY.md` è la **fonte di verità** del progetto e garantisce continuità e coerenza.
### 🧠 Gestione della memoria (MEMORY.md)
Prima di ogni azione devi:
1. Leggere completamente il file `MEMORY.md` per recuperare il contesto precedente.
2. Utilizzare le informazioni contenute in `MEMORY.md` per garantire continuità, coerenza e allineamento del progetto.
---
### 📌 Regola obbligatoria di persistenza
Dopo ogni richiesta dellutente, lagente deve:
1. Rileggere completamente `MEMORY.md` prima di iniziare qualsiasi operazione.
2. Aggiornare `MEMORY.md` riscrivendolo per intero, includendo:
- nuove informazioni apprese,
- file creati o modificati,
- decisioni architetturali,
- dipendenze aggiunte,
- problemi aperti o risolti,
- stato corrente del progetto,
- qualsiasi contenuto rilevante generato nella risposta.
3. Salvare il nuovo contenuto di `MEMORY.md` nella root del repository.
4. Ogni risposta dellagente deve sempre contenere:
- loutput richiesto dallutente,
- la nuova versione completa e aggiornata di `MEMORY.md`.
Se una sezione di `MEMORY.md` non cambia, deve comunque essere mantenuta.
---
### 🧩 Stile di lavoro
- Produci **file completi**, mai snippet isolati.
- Rispetta **PSR12**, **SOLID**, **Clean Architecture**, **DDD** quando applicabile.
- Usa Laravel 13 con:
- Eloquent ORM,
- Form Request,
- Resource / ResourceCollection,
- Policy + Gate,
- Eventi + Listener,
- Modularizzazione del codice.
- Usa AdminLTE integrato con Blade e asset pipeline. Prendi a riferimento questa pagina per le viste: https://adminlte.io/themes/AdminLTE/index2.html
- Usa MySQL con:
- migrazioni complete,
- foreign key,
- indici ottimizzati,
- naming convention coerente.
---
### 🔐 Sicurezza e qualità
- Valida sempre input e output.
- Evita SQL injection, XSS, CSRF, mass assignment.
- Usa DTO o Resource per lesposizione dati.
- Scrivi test PHPUnit quando richiesto.
- Documenta con PHPDoc e README tecnici.
---
### ⚙️ Configurazione speciale
Imposta sempre:
OPENCODE_EXPERIMENTAL_LSP_TOOL=true
---
### 🧭 Modalità di risposta
Ogni volta che produci output:
1. **Controlla `MEMORY.md`**
2. Aggiorna `MEMORY.md`
3. Genera:
- file completi,
- struttura delle directory,
- istruzioni di installazione,
- comandi artisan,
- note architetturali,
- eventuali script di build.
---
### 🔗 Collegamenti utili
- [Laravel_architettura](ca://s?q=Laravel_architettura)
- [AdminLTE_integrazione](ca://s?q=AdminLTE_integrazione)
- [Pattern_enterprise_PHP](ca://s?q=Pattern_enterprise_PHP)
- [Gestione_MEMORIA_md](ca://s?q=Gestione_MEMORIA_md)
+491
View File
@@ -0,0 +1,491 @@
# MEMORY.md - Glastree Project
## Project Context
Laravel 13 / PHP 8.4 project for parish management (Glastree). Full codebase exists and is operational.
---
## System Status
### ✅ FUNZIONANTE (Stable)
- Dashboard
- Individui (CRUD completo) - funziona da internet (con viste personalizzate per colonna)
- Gruppi (CRUD completo) - funziona da internet (con vista tabella/albero e colonne personalizzate)
- Documenti - funziona da internet
- Eventi (CRUD completo) - funziona da internet (con mass delete)
- Calendario eventi - funziona da internet
- Email (sistema completo)
- Impostazioni (App Settings, Logo, Email Settings)
- Mailing List - funziona da internet (con mass delete)
- Report (predefiniti e personalizzati, accesso tramite permesso 'report')
### ⚠️ IN COMPLETAMENTO
- Sistema Email: funziona al 95%, SMTP potrebbe necessitare credenziali corrette
---
## Progress Summary
### Eventi Enhancements (Completed)
- Migration `2026_05_12_142009_add_location_to_eventi_table` added `luogo_indirizzo` and `luogo_url_maps` columns
- Evento model updated with fillable fields
- EventoController updated: index() with search + sorting, store/update with location fields
- eventi/index.blade.php: search input, sortable columns (nome_evento, created_at), delete button
- eventi/create.blade.php: "Dove" card with address + Google Maps URL fields
- eventi/edit.blade.php: "Dove" card with address + Google Maps URL fields
- eventi/show.blade.php: "Luogo" section with address, link to Google Maps, embedded map preview
### Responsabile Multi-select (Completed)
- Migration `2026_05_12_000003_change_responsabile_to_multi.php` changed `responsabile_id` to `responsabile_ids` JSON
- Gruppo model updated with `getResponsabiliIds()` and `getResponsabili()` methods
- GruppoController updated: index(), store(), update(), show() for multi-select
- Views updated: show, edit, create blades with multi-select support
### 13 Maggio 2026 - Fixes
- **Ordinamento colonne**: Aggiunto ordinamento per Descrizione, Tipo, Gruppi, Responsabili
- **Form edit**: Aggiunti campi `tipo_evento` e `note` mancanti
- **Show view**: Corretta struttura HTML della sezione documenti
### 14 Maggio 2026 - Fixes
- **Form annidati**: La sezione Documenti spostata FUORI dal form principale
- **Tipologia enum**: Aggiunti valori 'programma' e 'locandina' alla colonna tipologia in documenti
- **Visibilita enum**: Aggiunto valore 'evento' alla colonna visibilita in documenti
### 15 Maggio 2026 - Eventi Show
- Rimossa sezione "Dove" duplicata dalla sidebar
- Aggiunta nuova sezione "Luogo" sotto "Data e Orario" nella colonna centrale
### 15 Maggio 2026 - Email Settings
- Aggiunto pulsante "Test Invio Email (SMTP)" nella pagina impostazioni email
- Aggiunto metodo `testSmtp()` nel controller per verificare la configurazione SMTP
- Route: `POST /impostazioni/email/test-smtp`
- Campo password opzionale (non più required)
- Rimossi attributi required dai campi del form impostazioni email
### 15 Maggio 2026 - Eventi Calendar
- Nuova vista calendario con FullCalendar 6.1.10
- Routes: `GET /eventi/calendar` e `GET /eventi/calendar/events`
- Metodo `calendar()` e `calendarEvents()` in EventoController
- Sidebar: menu Eventi con sottovoci Calendario, Elenco, Nuovo Evento
- Click su evento apre la pagina show dell'evento
### 15 Maggio 2026 - Email UI
- **Compose view**: Rimossa sidebar sinistra, solo form a schermo intero
- **Sidebar menu Email**:
- Ordine: Nuova Email, In arrivo, Preferiti, Inviate, Bozze, Archivio, Cestino
- Cliccando "Email" apre direttamente inbox
- **Bottone sincronizzazione**: "Ricevi/Invia" nella pagina inbox
- **Sincronizzazione**: Limitata a 100 messaggi per cartella
### 15 Maggio 2026 - Bug Fixes
- **RFC 2822**: Usare `Address::create()` di Symfony per gestire correttamente il display name
- **Route email.sync**: Sostituito `route('email.sync.quick')` con `url('email/sync/quick')` nella vista email/index.blade.php
### 16 Maggio 2026 - Email UI Fixes
- **Footer**: Aggiunto `min-height: calc(100vh - 120px)` al content-wrapper per garantire visibilità footer
- **Pagination**: Creato custom view `vendor.pagination.simple-bootstrap-4` con caratteri `‹` e `›` invece di SVG (problema scalabilità risolto)
### 16 Maggio 2026 - Logo Upload System
- **Model**: `App\Models\AppSetting` con metodi `getLogoUrl()`, `getLogoSmallUrl()`, `getAppName()`
- **Migration**: `2026_05_16_000001_add_logo_path_to_app_settings` - aggiunge `logo_path` e `logo_small_path`
- **Controller**: `ImpostazioniController` - metodi `uploadLogo()` e `removeLogo()`
- **Routes**: `POST /impostazioni/logo`, `POST /impostazioni/logo/remove`
- **Views**: Sezione Logo in `/impostazioni` per caricare logo grande e piccolo
- **Login page**: Logo dinamico da `AppSetting::getLogoUrl()`
- **Sidebar**: Logo piccolo da `AppSetting::getLogoSmallUrl()` con fallback a icona
### 16 Maggio 2026 - Email Signature Feature
- **Migration**: `2026_05_16_000003_add_signature_to_email_settings` - aggiunge colonna `signature` (longText)
- **Migration**: `2026_05_16_000004_add_signature_enabled_to_email_settings` - aggiunge colonna `signature_enabled` (boolean, default true)
- **Model**: `EmailSetting` aggiornato con `signature` e `signature_enabled` in fillable, cast boolean, metodo `getSignature()`
- **View**: `admin/email-settings/index.blade.php` aggiornato con:
- Titolo cambiato da "Email IMAP" a "Email Settings"
- Nuova sezione "Firma" nel menu laterale
- Toggle switch "Attiva firma automatica" per abilitare/disabilitare la firma
- Editor WYSIWYG Quill.js per la composizione della firma
- Caricamento lazy di Quill.js da CDN
- **Controller**: `EmailController` aggiornato con metodo `appendSignature(body, signature, enabled)` che aggiunge la firma al body solo se abilitata
- La firma viene aggiunta sia alle email inviate via SMTP che a quelle salvate in "Invio"
- **Sidebar**: Voce di menu cambiata da "Email IMAP" a "Impostazione Email"
---
## Sistema Email - Dettagli Tecnici
### Routes Eventi
```
GET /eventi → eventi.index (elenco)
GET /eventi/calendar → eventi.calendar (calendario)
GET /eventi/calendar/events → eventi.calendar.events (eventi per FullCalendar)
GET /eventi/create → eventi.create
POST /eventi → eventi.store
GET /eventi/{id} → eventi.show
GET /eventi/{id}/edit → eventi.edit
PUT /eventi/{id} → eventi.update
DELETE /eventi/{id} → eventi.destroy
```
### Routes Email
```
POST /email/send → email.send
POST /email/sync/quick → email.sync.quick
POST /email/bulk → email.bulk
GET /email/{folder?} → email.index
POST /email/{id}/star → email.star
DELETE /email/{id} → email.destroy
```
### Folder Types
- inbox, sent, drafts, archive, starred, trash
### SMTP Configuration
- Credenziali salvate in `email_settings` con criptatura
- Password decriptata con `getDecryptedPassword()` o `getDecryptedSmtpPassword()`
- Display name validato con `filter_var($fromName, FILTER_VALIDATE_EMAIL)` - se è un'email usa "Glastree"
### Email Signature
- Campo `signature` in `email_settings` (longText, nullable)
- WYSIWYG editor Quill.js via CDN
- Metodo `appendSignature()` in EmailController aggiunge "---\n" + signature al body
- Applicato sia in `sendViaImap()` che in `storeSentMessage()`
---
## Key Decisions
### Database
- Location uses separate columns: `luogo_indirizzo` (varchar 500) and `luogo_url_maps` (text)
- Map preview via Google Maps embed iframe using the address
- Sortable columns: nome_evento, created_at, descrizione_evento, tipo_recorrenza, gruppi, responsabili
- Multi-select uses Ctrl+click pattern with array input names
### Email
- SMTP defaults to Gmail if not configured
- Email name sanitized to comply with RFC 2822
- Sincronizzazione IMAP limitata a 100 messaggi per cartella
- Signature appended with separator "---\n" before the signature text
### UI
- AdminLTE per tutte le viste
- Sidebar con menu navigazione
- Compose email senza sidebar sinistra
---
## Relevant Files
### Core Controllers
- `app/Http/Controllers/EventoController.php`: eventi with extended sorting
- `app/Http/Controllers/EmailController.php`: email system completo, signature auto-append
- `app/Http/Controllers/Admin/EmailSettingsController.php`: email settings + test SMTP
### Models
- `app/Models/EmailSetting.php`: getDecryptedPassword(), getDecryptedSmtpPassword(), getSignature()
- `app/Models/Evento.php`: fillable with location fields
- `app/Models/Gruppo.php`: getResponsabiliIds(), getResponsabili()
### Views
- `resources/views/admin/email-settings/index.blade.php`: Email settings with signature section (Quill.js WYSIWYG)
- `resources/views/eventi/index.blade.php`: sortable columns
- `resources/views/eventi/edit.blade.php`: fixed nested form, added tipo_evento and note
- `resources/views/eventi/show.blade.php`: removed duplicate "Dove", added "Luogo" section
- `resources/views/email/index.blade.php`: mailbox view con AdminLTE
- `resources/views/email/compose.blade.php`: compose senza sidebar sinistra
- `resources/views/layouts/adminlte.blade.php`: sidebar menu Email con ordine corretto
### Migrations
- `2026_05_16_000003_add_signature_to_email_settings.php`: Adds signature column to email_settings table
---
## Commands Utili
```bash
# Clear all caches
php artisan optimize:clear
# Recompile views
php artisan view:cache
# Check routes
php artisan route:list --name=email
```
---
## Known Issues / TODO
1. SMTP authentication con Infomaniak - verificare credenziali corrette
2. Invio email richiede password SMTP corretta nel form impostazioni
---
### 16 Maggio 2026 - URL Hardcoded Fix (Internet Access)
- **Problema**: Da internet (accesso remoto) molti form e link puntavano a `http://localhost` invece del server remoto
- **Causa**: Uso di `url('/path')` in tutte le view che generava URL basati su `APP_URL=http://localhost`
- **Soluzione**: Sostituzione di tutte le occorrenze di `url('/path')` con path relativi `/path`
- **Files modificati**:
- `individui/edit.blade.php`: form action, link gruppi/documenti, fetch API calls
- `individui/show.blade.php`: link gruppi, documenti, form, fetch API calls
- `gruppi/edit.blade.php`: form action, link individui/documenti, fetch API calls
- `gruppi/show.blade.php`: link individui, eventi
- `eventi/edit.blade.php`: form action, link annulla, documenti
- `eventi/show.blade.php`: breadcrumbs, link gruppi/responsabili, documenti
- `eventi/create.blade.php`: form action, link annulla
- `eventi/index.blade.php`: link create, reset, eventi, gruppi
- `documenti/index.blade.php`: link targets, edit, delete, mass actions, fetch API
- `documenti/edit.blade.php`: form action, download, back link
- `mailing-liste/edit.blade.php`: fetch API calls
- `eventi/calendar.blade.php`: eventSource URL
- **Layout fix**: Aggiunto meta tag `csrf-token` in `adminlte.blade.php` head
---
### 16 Maggio 2026 - Individui Edit Form Fix
- **Problema**: Form non salvava da accesso internet, il pulsante submit non funzionava
- **Causa**: Il button type="submit" non triggherava il form submission
- **Soluzione**:
- Cambiato button da `type="submit"` a `type="button"` con `onclick="document.getElementById('main-form').submit()"`
- Path relativi per tutti i form action
- CSRF token in meta tag del layout
---
### 17 Maggio 2026 - Bulk Action Error Fix
- **Problema**: Azioni massive sulle email (sposta nel cestino) mostravano popup "errore" generico
- **Causa**: `authorizeWrite()` lancia eccezione 403 HTML invece di JSON per richieste AJAX
- **Soluzione**: Aggiunto try-catch in `bulkAction()`, `markRead()` e `toggleStar()` per ritornare JSON con messaggio appropriato
### 17 Maggio 2026 - Nginx Proxy Manager Config
- **Problema**: Email non funziona da internet (dietro NPM)
- **bootstrap/app.php**: Aggiunto `$middleware->trustProxies(at: '*')`
- **Configurazione .env**:
```
APP_URL=
APP_PROTOCOL=https
FORCE_HTTPS=true
TRUSTED_PROXIES=*
```
- **Middleware rimosso**: ForceHttps non necessario - NPM gestisce terminazione HTTPS
- **Advanced Tab NPM**: Abilitare X-Forwarded-Proto e headers proxy
### 17 Maggio 2026 - Form Annidati Fix
- **Problema**: Contatti, gruppi e documenti in individui/edit non venivano salvati
- **Causa**: Sezioni fuori dal main-form (chiusura prematura)
- **Soluzione**: Spostata chiusura form dopo tutte le sezioni, aggiunto pulsante "Salva Tutte le Modifiche"
- **Contatti**: Aggiunto pulsante salva nella riga nuova (icona💾)
### 17 Maggio 2026 - Bulk Action Email Fix
- **Problema**: Azioni massive su email mostravano popup "errore"
- **Causa**: authorizeWrite() lancia eccezione 403 HTML invece di JSON
- **Soluzione**: try-catch in bulkAction(), markRead(), toggleStar()
---
(Last updated: 25 Maggio 2026 - Fix export CSV individui)
### 18 Maggio 2026 - AGENTS.md Update
- Aggiunta regola "Verifica Salvataggio" nelle Regole Operative AI
- Obbligo di verificare SEMPRE l'avvenuta riuscita dopo operazioni di modifica (store, update, delete, upload, sync)
### 25 Maggio 2026 - IndividuoController::elimina() Method
- **Problema**: Errore 500 "Call to undefined method App\Http\Controllers\IndividuoController::elimina()" su richiesta `POST /individui/{id}/elimina`
- **Causa**: Route definita in `routes/web.php:58` ma metodo `elimina()` non implementato nel controller
- **Soluzione**: Aggiunto metodo `elimina()` in `IndividuoController` che:
- Verifica autorizzazione con `authorizeDelete('individui')`
- Riceve parametri `elimina_contatti` e `elimina_documenti` dal form
- Elimina opzionalmente i contatti collegati se richiesto
- Elimina opzionalmente i documenti collegati (con rimozione file fisico) se richiesto
- Elimina l'individuo e redirect alla lista con messaggio di successo
- **Files modificati**:
- `app/Http/Controllers/IndividuoController.php`: aggiunto metodo `elimina()` dopo `destroy()`
### 25 Maggio 2026 - Fix Eliminazione Singola e Massiva
- **Problema**: L'eliminazione non veniva eseguita né per il singolo né per la mass action
- **Cause**:
1. `$request->boolean()` interpretava male i valori `'true'`/`'false'` come stringhe
2. `deleteSelected()` faceva submit multipli di form separati causando redirect concorrenti
- **Soluzione**:
1. Sostituito `$request->boolean()` con `filter_var(..., FILTER_VALIDATE_BOOLEAN)` per parsing corretto
2. Aggiunto metodo `massElimina()` in `IndividuoController` per gestire eliminazione multipla con singolo form
3. Aggiunta route `POST /individui/mass-elimina` in `routes/web.php`
4. Rifattorizzata funzione JS `deleteSelected()` per usare singolo form con array `ids[]`
- **Files modificati**:
- `app/Http/Controllers/IndividuoController.php`: corretto `elimina()`, aggiunto `massElimina()`
- `routes/web.php`: aggiunta route `individui/mass-elimina`
- `resources/views/individui/index.blade.php`: rifattorizzata `deleteSelected()`
### 25 Maggio 2026 - Fix Export CSV Individui
- **Problema**: "Esporta Tutti CSV" e "Esporta Selezionati CSV" non funzionavano
- **Cause**:
1. `exportCSV()` usava `:visible` (selettore jQuery) con `querySelectorAll` (JS nativo) che non lo supporta
2. Export client-side esportava solo righe nel DOM (pagina corrente con pagination), non tutti i record
3. Dati limitati a 5 colonne senza contatti email/telefono
- **Soluzione**:
1. Creato metodo `exportCSV()` server-side in `IndividuoController` con `StreamedResponse`
2. Export completo con: Codice, Cognome, Nome, Data Nascita, Genere, Email, Telefono, Cellulare, Indirizzo, CAP, Città, Provincia, Note
3. Email/Telefono/Cellulare recuperati dai contatti con preferenza per quelli primari
4. Supporto per export tutti o solo selezionati tramite parametro `ids[]`
5. Funzioni JS `exportCSV()` e `exportSelectedCSV()` ora puntano a `/individui/export`
- **Files modificati**:
- `app/Http/Controllers/IndividuoController.php`: aggiunto metodo `exportCSV()` con StreamedResponse
- `routes/web.php`: aggiunta route `GET /individui/export`
- `resources/views/individui/index.blade.php`: semplificate funzioni `exportCSV()` e `exportSelectedCSV()`
(Last updated: 25 Maggio 2026 - Report system, mass actions, gruppi deletion fix)
### 25 Maggio 2026 - Fix Eliminazione Gruppi Padre (Superadmin Only)
- **Problema**: Eliminando un gruppo padre si otteneva pagina 404. Inoltre l'eliminazione di gruppi con figli era disponibile per tutti
- **Cause**:
1. Mancato detach delle tabelle pivot (`gruppo_individuo`, `eventi_gruppi`) prima della cancellazione
2. Nessun controllo per limitare l'eliminazione di gruppi con sottogruppi al superadmin
- **Soluzione**:
1. Rifattorizzato `destroy()` in `GruppoController`: aggiunta verifica `isSuperAdmin()` per gruppi con figli
2. Creato metodo `destroyGruppo()` helper che esegue detach pivot, cancella documenti fisici e poi cancella il gruppo
3. Aggiornata vista `tree-item.blade.php`: pulsante elimina disabilitato per non-superadmin su gruppi con figli
- **Files modificati**:
- `app/Http/Controllers/GruppoController.php`: rifattorizzato `destroy()`, aggiunto `destroyGruppo()`
- `resources/views/gruppi/partials/tree-item.blade.php`: pulsante elimina condizionato a superadmin per gruppi padre
### 25 Maggio 2026 - Mass Actions Eventi e Mailing List
- **Problema**: Eventi e Mailing List non avevano funzionalità di eliminazione massiva
- **Soluzione**:
1. Aggiunti checkbox di selezione, pulsante "Elimina Selezionati" e modal di conferma in `eventi/index.blade.php`
2. Aggiunti checkbox di selezione, pulsante "Elimina Selezionati" e modal di conferma in `mailing-liste/index.blade.php`
3. Aggiunto metodo `massElimina()` in `EventoController` con detach di gruppi e responsabili
4. Aggiunto metodo `massElimina()` in `MailingListController` con cancellazione contatti associati
5. Aggiunte route `POST /eventi/mass-elimina` e `POST /mailing-liste/mass-elimina`
- **Files modificati**:
- `app/Http/Controllers/EventoController.php`: aggiunto `massElimina()`
- `app/Http/Controllers/MailingListController.php`: aggiunto `massElimina()`
- `routes/web.php`: aggiunte route mass-elimina per eventi e mailing-liste
- `resources/views/eventi/index.blade.php`: aggiunta selezione massiva, modal conferma, JS
- `resources/views/mailing-liste/index.blade.php`: aggiunta selezione massiva, modal conferma, JS
### 25 Maggio 2026 - Rimozione Menu Utilità Viste
- **Modifica**: Rimosso dal sidebar menu la voce "Utilità Viste" (`viste.index`)
- **Nota**: Le route e i controller per le viste rimangono funzionanti, solo il link sidebar è stato rimosso temporaneamente
### 25 Maggio 2026 - Sistema Report Completo
- **Funzionalità**: Nuova pagina Report accessibile solo dal superadmin con:
1. **Panoramica database**: statistiche rapide (individui, gruppi, eventi, documenti, mailing list, contatti)
2. **12 Report predefiniti**:
- Individui per Genere
- Individui per Fascia d'Età
- Gerarchia Gruppi
- Gruppi e Membri
- Eventi del Mese
- Documenti per Tipologia
- Contatti per Tipo
- Dettaglio Mailing List
- Individui senza Contatti
- Individui senza Gruppo
- Eventi per Gruppo
- Documenti Orfani
3. **Report personalizzati**: creazione di report custom con selezione tipo dato, colonne, ordinamento e limite
4. **Export CSV**: ogni report può essere esportato in CSV
- **Files creati**:
- `app/Http/Controllers/ReportController.php`: controller completo con 12 report + custom
- `app/Models/ReportCustom.php`: modello per report personalizzati
- `database/migrations/2026_05_25_000001_create_report_custom_table.php`: migration tabella report_custom
- `resources/views/report/index.blade.php`: pagina principale report con card e form creazione
- `resources/views/report/result.blade.php`: pagina visualizzazione risultati report
- **Routes aggiunte**:
- `GET /report` → report.index
- `GET /report/run` → report.run
- `GET /report/export` → report.export
- `GET /report/custom/{id}` → report.run-custom
- `POST /report/custom` → report.store-custom
- `DELETE /report/custom/{id}` → report.destroy-custom
- **Sidebar aggiornata**: link "Report" ora punta a `route('report.index')` con evidenziazione active
(Last updated: 25 Maggio 2026 - Fix sidebar @endif orfano, verifica completa sintassi)
### 25 Maggio 2026 - Fix Sidebar Errore Sintassi
- **Problema**: Errore "syntax error, unexpected token 'endif'" nella dashboard
- **Causa**: Dopo aver rimosso il blocco `@if(Auth::user()->isSuperAdmin())` dalla sidebar per sostituirlo con `@if(Auth::user()->canAccess('report'))`, erano rimasti un `@endif` orfano (linea 237) e un `</li>` extra (linea 239)
- **Soluzione**:
1. Rimosso `@endif` orfano
2. Rimosso `</li>` extra
3. Aggiunto nuovo `@if(Auth::user()->isSuperAdmin())` per proteggere i menu Impostazioni e Admin
- **Verifica completa**: Scansionati tutti i blade file del progetto - tutte le direttive `@if/@endif`, `@foreach/@endforeach`, `@php/@endphp` sono correttamente bilanciate. Compilazione viste OK.
### 25 Maggio 2026 - Accesso Report Basato su Permessi
- **Modifica**: Il modulo 'report' è stato aggiunto a `User::MODULES` come nuovo modulo permissionabile
- **Funzionalità**: Gli utenti con permesso `canAccess('report')` (livello lettura o superiore) possono accedere alla pagina Report, non più solo il superadmin
- **Files modificati**:
- `app/Models/User.php`: aggiunto 'report' a `MODULES` = ['individui', 'gruppi', 'eventi', 'documenti', 'mailing', 'viste', 'report']
- `app/Http/Controllers/ReportController.php`: sostituiti tutti i check `isSuperAdmin()` con `canAccess('report')`
- `resources/views/layouts/adminlte.blade.php`: sidebar mostra "Report" se `canAccess('report')` invece di `isSuperAdmin()`
- **Come abilitare un utente**: Dal pannello Admin → Utenti, assegnare permesso di livello ≥ 1 (Lettura) per il modulo 'report'
### 25 Maggio 2026 - Viste Personalizzate per Colonne (Individui e Gruppi)
- **Funzionalità**: Ogni utente può personalizzare quali colonne visualizzare nelle liste di Individui e Gruppi
- **Individui**:
- Colonne disponibili: Codice, Cognome, Nome, Email, Telefono
- Il controller `IndividuoController@index()` ora passa `$allColumns` e `$visibleColumns` alla vista
- Le colonne visibili sono determinate dalla `VistaReport` predefinita dell'utente
- Modal "Colonne Visibili" mostra tutte le colonne con checkbox per toggle
- Modal "Salva Vista" permette di salvare una vista con nome, colonne, e opzione predefinita
- **Gruppi**:
- Nuova vista tabella affiancata alla vista albero esistente
- Toggle "Tabella/Albero" nella card header
- Colonne disponibili: Nome, Descrizione, Diocesi, Livello, Gruppo Padre, Membri, Responsabili, Indirizzo, Città
- Le colonne visibili sono determinate dalla `VistaReport` predefinita per tipo 'gruppi'
- Salvataggio vista dedicato con route `POST /gruppi/save-vista`
- Eliminazione vista con route `DELETE /gruppi/vista/{id}`
- **Model utilizzato**: `VistaReport` (tabella `viste_report`) con campi: `user_id`, `nome`, `tipo`, `colonne_visibili` (JSON), `is_default`
- **Files modificati**:
- `app/Http/Controllers/IndividuoController.php`: `index()` passa `$allColumns` e `$visibleColumns`
- `app/Http/Controllers/GruppoController.php`: `index()` rifattorizzato per vista tabella/albero, aggiunti `saveVista()` e `deleteVista()`
- `routes/web.php`: aggiunte route `gruppi/save-vista` e `gruppi/delete-vista`
- `resources/views/individui/index.blade.php`: colonne condizionali con `@if(in_array(..., $visibleColumns))`
- `resources/views/gruppi/index.blade.php`: riscritta completamente con doppio mode (tabella/albero), modal salvataggio vista, toggle colonne
### 25 Maggio 2026 - Fix Vari Pagine (Individui, Eventi, Mailing, Documenti Upload)
- **Individui/Gruppi**: Errore `$tableColumns` non definito risolto usando `$allColumns`
- **Eventi**: Checkbox righe senza `id` impedivano il corretto funzionamento del label; aggiunto `id="row-checkbox-{{ $evento->id }}"`
- **Mailing List**: Stesso problema checkbox, corretto con `id` univoci
- **Documenti upload**: Funzione `loadUploadTargets()` chiamata ma non definita; aggiunta per popolare il dropdown target
### 25 Maggio 2026 - Fix Documento Avatar e Morph Map
- **Problema**: Caricando un documento avatar associato a un individuo, errore "Class 'individuo' not found"
- **Causa**: Il campo `visibilita_target_type` nel database conteneva nomi brevi ('individuo', 'gruppo') invece del namespace completo
- **Soluzione**:
1. Registrata `Relation::morphMap()` in `AppServiceProvider@boot()` per mappare nomi brevi a classi complete
2. Aggiornato `DocumentoController@store()` per impostare correttamente `visibilita_target_type` in base alla visibilità
- **Files modificati**:
- `app/Providers/AppServiceProvider.php`: aggiunta morphMap per individuo, gruppo, evento, mailing, associazione, federazione
- `app/Http/Controllers/DocumentoController.php`: logica esplicita per mappare visibilità → target_type
### 25 Maggio 2026 - Fix Pagina Gruppi (Tabella Pivot Inesistente)
- **Problema**: Pagina gruppi falliva con "Table 'glastree.gruppo_responsabili' doesn't exist"
- **Causa**: Il modello `Gruppo` aveva una relazione `responsabili()` che puntava a tabella pivot inesistente (la migration `2026_05_12_000003` aveva migrato a JSON `responsabile_ids`)
- **Soluzione**:
1. Rimossa relazione `responsabili()` da `Gruppo.php`
2. Rimossa eager loading `responsabili` da `GruppoController@index()`
3. Mantenuti metodi helper `getResponsabiliIds()` e `getResponsabili()` che leggono dal campo JSON
4. Corretti riferimenti in `gruppi/show.blade.php` e `individui/show.blade.php` per usare `getResponsabili()`
- **Files modificati**:
- `app/Models/Gruppo.php`: rimossa relazione `responsabili()`, mantenuti helper
- `app/Http/Controllers/GruppoController.php`: rimossa eager loading `responsabili`
- `resources/views/gruppi/show.blade.php`: corretto `$child->responsabile` → `$child->getResponsabili()->first()`
- `resources/views/individui/show.blade.php`: corretto `$gruppo->responsabile` → `$gruppo->getResponsabili()->first()`
### 25 Maggio 2026 - Fix Caricamento Documenti in Pagina Gruppi
- **Funzionalità**: Aggiunto modale "Carica Documento" nella pagina `gruppi/show` per upload diretto
- **Logica Responsabili**: Solo i membri del gruppo possono essere selezionati come responsabili
- In `create.blade.php`: il dropdown responsabili si aggiorna dinamicamente quando si aggiungono/rimuovono membri
- Funzione JS `updateResponsabiliFromMembri()` sincronizza il select responsabili con i membri aggiunti
- **Files modificati**:
- `resources/views/gruppi/show.blade.php`: aggiunto modale upload documento, alert success/error
- `resources/views/gruppi/create.blade.php`: logica JS per vincolare responsabili ai soli membri
(Last updated: 25 Maggio 2026 - Fix report eventi_calendario, colonna data)
### 25 Maggio 2026 - Fix Report Eventi Calendario
- **Problema**: Report "Eventi del Mese" falliva con "Unknown column 'data_inizio' in 'WHERE'"
- **Causa**: Il report usava `data_inizio` che non esiste nella tabella `eventi`. La colonna corretta è `data_specifica`
- **Soluzione**: Sostituito `data_inizio` con `data_specifica` in `reportEventiCalendario()` e `reportEventiPerGruppo()`
- **Files modificati**:
- `app/Http/Controllers/ReportController.php`: righe 389-397 e 532, corretto nome colonna
(Last updated: 25 Maggio 2026 - Fix report eventi_calendario, colonna data)
+58
View File
@@ -0,0 +1,58 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## About Laravel
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
Laravel is accessible, powerful, and provides tools required for large, robust applications.
## Learning Laravel
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
In addition, [Laracasts](https://laracasts.com) contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
You can also watch bite-sized lessons with real-world projects on [Laravel Learn](https://laravel.com/learn), where you will be guided through building a Laravel application from scratch while learning PHP fundamentals.
## Agentic Development
Laravel's predictable structure and conventions make it ideal for AI coding agents like Claude Code, Cursor, and GitHub Copilot. Install [Laravel Boost](https://laravel.com/docs/ai) to supercharge your AI workflow:
```bash
composer require laravel/boost --dev
php artisan boost:install
```
Boost provides your agent 15+ tools and skills that help agents build Laravel applications while following best practices.
## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
## Code of Conduct
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## Security Vulnerabilities
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
@@ -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);
});
}
}
}
Executable
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env php
<?php
use Illuminate\Foundation\Application;
use Symfony\Component\Console\Input\ArgvInput;
define('LARAVEL_START', microtime(true));
// Register the Composer autoloader...
require __DIR__.'/vendor/autoload.php';
// Bootstrap Laravel and handle the command...
/** @var Application $app */
$app = require_once __DIR__.'/bootstrap/app.php';
$status = $app->handleCommand(new ArgvInput);
exit($status);
+31
View File
@@ -0,0 +1,31 @@
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use App\Http\Middleware\CheckPermission;
use App\Http\Middleware\AdminOnly;
use App\Http\Middleware\ForceHttps;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
$middleware->alias([
'admin' => AdminOnly::class,
'permission' => CheckPermission::class,
]);
$middleware->trustProxies(at: '*');
$middleware->web(append: [
\Illuminate\Cookie\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
]);
})
->withExceptions(function (Exceptions $exceptions): void {
//
})->create();
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+7
View File
@@ -0,0 +1,7 @@
<?php
use App\Providers\AppServiceProvider;
return [
AppServiceProvider::class,
];
+89
View File
@@ -0,0 +1,89 @@
{
"$schema": "https://getcomposer.org/schema.json",
"name": "laravel/laravel",
"type": "project",
"description": "The skeleton application for the Laravel framework.",
"keywords": ["laravel", "framework"],
"license": "MIT",
"require": {
"php": "^8.3",
"directorytree/imapengine-laravel": "^1.2",
"laravel/framework": "^13.7",
"laravel/tinker": "^3.0",
"spatie/laravel-permission": "^7.4",
"webklex/php-imap": "^6.2"
},
"require-dev": {
"fakerphp/faker": "^1.23",
"laravel/pail": "^1.2.5",
"laravel/pao": "^1.0.6",
"laravel/pint": "^1.27",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"phpunit/phpunit": "^12.5.12"
},
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"setup": [
"composer install",
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
"@php artisan key:generate",
"@php artisan migrate --force",
"npm install --ignore-scripts",
"npm run build"
],
"dev": [
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
],
"test": [
"@php artisan config:clear --ansi @no_additional_args",
"@php artisan test"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi",
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
"@php artisan migrate --graceful --ansi"
],
"pre-package-uninstall": [
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
]
},
"extra": {
"laravel": {
"dont-discover": []
}
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true,
"php-http/discovery": true
}
},
"minimum-stability": "stable",
"prefer-stable": true
}
Generated
+9037
View File
File diff suppressed because it is too large Load Diff
+126
View File
@@ -0,0 +1,126 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Application Name
|--------------------------------------------------------------------------
|
| This value is the name of your application, which will be used when the
| framework needs to place the application's name in a notification or
| other UI elements where an application name needs to be displayed.
|
*/
'name' => env('APP_NAME', 'Laravel'),
/*
|--------------------------------------------------------------------------
| Application Environment
|--------------------------------------------------------------------------
|
| This value determines the "environment" your application is currently
| running in. This may determine how you prefer to configure various
| services the application utilizes. Set this in your ".env" file.
|
*/
'env' => env('APP_ENV', 'production'),
/*
|--------------------------------------------------------------------------
| Application Debug Mode
|--------------------------------------------------------------------------
|
| When your application is in debug mode, detailed error messages with
| stack traces will be shown on every error that occurs within your
| application. If disabled, a simple generic error page is shown.
|
*/
'debug' => (bool) env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Application URL
|--------------------------------------------------------------------------
|
| This URL is used by the console to properly generate URLs when using
| the Artisan command line tool. You should set this to the root of
| the application so that it's available within Artisan commands.
|
*/
'url' => env('APP_URL', 'http://localhost'),
/*
|--------------------------------------------------------------------------
| Application Timezone
|--------------------------------------------------------------------------
|
| Here you may specify the default timezone for your application, which
| will be used by the PHP date and date-time functions. The timezone
| is set to "UTC" by default as it is suitable for most use cases.
|
*/
'timezone' => 'UTC',
/*
|--------------------------------------------------------------------------
| Application Locale Configuration
|--------------------------------------------------------------------------
|
| The application locale determines the default locale that will be used
| by Laravel's translation / localization methods. This option can be
| set to any locale for which you plan to have translation strings.
|
*/
'locale' => env('APP_LOCALE', 'en'),
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
/*
|--------------------------------------------------------------------------
| Encryption Key
|--------------------------------------------------------------------------
|
| This key is utilized by Laravel's encryption services and should be set
| to a random, 32 character string to ensure that all encrypted values
| are secure. You should do this prior to deploying the application.
|
*/
'cipher' => 'AES-256-CBC',
'key' => env('APP_KEY'),
'previous_keys' => [
...array_filter(
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
),
],
/*
|--------------------------------------------------------------------------
| Maintenance Mode Driver
|--------------------------------------------------------------------------
|
| These configuration options determine the driver used to determine and
| manage Laravel's "maintenance mode" status. The "cache" driver will
| allow maintenance mode to be controlled across multiple machines.
|
| Supported drivers: "file", "cache"
|
*/
'maintenance' => [
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
'store' => env('APP_MAINTENANCE_STORE', 'database'),
],
];
+117
View File
@@ -0,0 +1,117 @@
<?php
use App\Models\User;
return [
/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option defines the default authentication "guard" and password
| reset "broker" for your application. You may change these values
| as required, but they're a perfect start for most applications.
|
*/
'defaults' => [
'guard' => env('AUTH_GUARD', 'web'),
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
],
/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| which utilizes session storage plus the Eloquent user provider.
|
| All authentication guards have a user provider, which defines how the
| users are actually retrieved out of your database or other storage
| system used by the application. Typically, Eloquent is utilized.
|
| Supported: "session"
|
*/
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
],
/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
|
| All authentication guards have a user provider, which defines how the
| users are actually retrieved out of your database or other storage
| system used by the application. Typically, Eloquent is utilized.
|
| If you have multiple user tables or models you may configure multiple
| providers to represent the model / table. These providers may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => env('AUTH_MODEL', User::class),
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
/*
|--------------------------------------------------------------------------
| Resetting Passwords
|--------------------------------------------------------------------------
|
| These configuration options specify the behavior of Laravel's password
| reset functionality, including the table utilized for token storage
| and the user provider that is invoked to actually retrieve users.
|
| The expiry time is the number of minutes that each reset token will be
| considered valid. This security feature keeps tokens short-lived so
| they have less time to be guessed. You may change this as needed.
|
| The throttle setting is the number of seconds a user must wait before
| generating more password reset tokens. This prevents the user from
| quickly generating a very large amount of password reset tokens.
|
*/
'passwords' => [
'users' => [
'provider' => 'users',
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
'expire' => 60,
'throttle' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Password Confirmation Timeout
|--------------------------------------------------------------------------
|
| Here you may define the number of seconds before a password confirmation
| window expires and users are asked to re-enter their password via the
| confirmation screen. By default, the timeout lasts for three hours.
|
*/
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
];
+130
View File
@@ -0,0 +1,130 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Cache Store
|--------------------------------------------------------------------------
|
| This option controls the default cache store that will be used by the
| framework. This connection is utilized if another isn't explicitly
| specified when running a cache operation inside the application.
|
*/
'default' => env('CACHE_STORE', 'database'),
/*
|--------------------------------------------------------------------------
| Cache Stores
|--------------------------------------------------------------------------
|
| Here you may define all of the cache "stores" for your application as
| well as their drivers. You may even define multiple stores for the
| same cache driver to group types of items stored in your caches.
|
| Supported drivers: "array", "database", "file", "memcached",
| "redis", "dynamodb", "octane",
| "failover", "null"
|
*/
'stores' => [
'array' => [
'driver' => 'array',
'serialize' => false,
],
'database' => [
'driver' => 'database',
'connection' => env('DB_CACHE_CONNECTION'),
'table' => env('DB_CACHE_TABLE', 'cache'),
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
],
'file' => [
'driver' => 'file',
'path' => storage_path('framework/cache/data'),
'lock_path' => storage_path('framework/cache/data'),
],
'memcached' => [
'driver' => 'memcached',
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
'sasl' => [
env('MEMCACHED_USERNAME'),
env('MEMCACHED_PASSWORD'),
],
'options' => [
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
],
'servers' => [
[
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
'port' => env('MEMCACHED_PORT', 11211),
'weight' => 100,
],
],
],
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
],
'dynamodb' => [
'driver' => 'dynamodb',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
'endpoint' => env('DYNAMODB_ENDPOINT'),
],
'octane' => [
'driver' => 'octane',
],
'failover' => [
'driver' => 'failover',
'stores' => [
'database',
'array',
],
],
],
/*
|--------------------------------------------------------------------------
| Cache Key Prefix
|--------------------------------------------------------------------------
|
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
| stores, there might be other applications using the same cache. For
| that reason, you may prefix every cache key to avoid collisions.
|
*/
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
/*
|--------------------------------------------------------------------------
| Serializable Classes
|--------------------------------------------------------------------------
|
| This value determines the classes that can be unserialized from cache
| storage. By default, no PHP classes will be unserialized from your
| cache to prevent gadget chain attacks if your APP_KEY is leaked.
|
*/
'serializable_classes' => false,
];
+184
View File
@@ -0,0 +1,184 @@
<?php
use Illuminate\Support\Str;
use Pdo\Mysql;
return [
/*
|--------------------------------------------------------------------------
| Default Database Connection Name
|--------------------------------------------------------------------------
|
| Here you may specify which of the database connections below you wish
| to use as your default connection for database operations. This is
| the connection which will be utilized unless another connection
| is explicitly specified when you execute a query / statement.
|
*/
'default' => env('DB_CONNECTION', 'sqlite'),
/*
|--------------------------------------------------------------------------
| Database Connections
|--------------------------------------------------------------------------
|
| Below are all of the database connections defined for your application.
| An example configuration is provided for each database system which
| is supported by Laravel. You're free to add / remove connections.
|
*/
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'url' => env('DB_URL'),
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
'busy_timeout' => null,
'journal_mode' => null,
'synchronous' => null,
'transaction_mode' => 'DEFERRED',
],
'mysql' => [
'driver' => 'mysql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'mariadb' => [
'driver' => 'mariadb',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'pgsql' => [
'driver' => 'pgsql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => env('DB_SSLMODE', 'prefer'),
],
'sqlsrv' => [
'driver' => 'sqlsrv',
'url' => env('DB_URL'),
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', '1433'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
],
],
/*
|--------------------------------------------------------------------------
| Migration Repository Table
|--------------------------------------------------------------------------
|
| This table keeps track of all the migrations that have already run for
| your application. Using this information, we can determine which of
| the migrations on disk haven't actually been run on the database.
|
*/
'migrations' => [
'table' => 'migrations',
'update_date_on_publish' => true,
],
/*
|--------------------------------------------------------------------------
| Redis Databases
|--------------------------------------------------------------------------
|
| Redis is an open source, fast, and advanced key-value store that also
| provides a richer body of commands than a typical key-value system
| such as Memcached. You may define your connection settings here.
|
*/
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
'persistent' => env('REDIS_PERSISTENT', false),
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
'max_retries' => env('REDIS_MAX_RETRIES', 3),
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
],
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
'max_retries' => env('REDIS_MAX_RETRIES', 3),
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
],
],
];
+80
View File
@@ -0,0 +1,80 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Filesystem Disk
|--------------------------------------------------------------------------
|
| Here you may specify the default filesystem disk that should be used
| by the framework. The "local" disk, as well as a variety of cloud
| based disks are available to your application for file storage.
|
*/
'default' => env('FILESYSTEM_DISK', 'local'),
/*
|--------------------------------------------------------------------------
| Filesystem Disks
|--------------------------------------------------------------------------
|
| Below you may configure as many filesystem disks as necessary, and you
| may even configure multiple disks for the same driver. Examples for
| most supported storage drivers are configured here for reference.
|
| Supported drivers: "local", "ftp", "sftp", "s3"
|
*/
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app/private'),
'serve' => true,
'throw' => false,
'report' => false,
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
'visibility' => 'public',
'throw' => false,
'report' => false,
],
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
'report' => false,
],
],
/*
|--------------------------------------------------------------------------
| Symbolic Links
|--------------------------------------------------------------------------
|
| Here you may configure the symbolic links that will be created when the
| `storage:link` Artisan command is executed. The array keys should be
| the locations of the links and the values should be their targets.
|
*/
'links' => [
public_path('storage') => storage_path('app/public'),
],
];
+132
View File
@@ -0,0 +1,132 @@
<?php
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;
use Monolog\Processor\PsrLogMessageProcessor;
return [
/*
|--------------------------------------------------------------------------
| Default Log Channel
|--------------------------------------------------------------------------
|
| This option defines the default log channel that is utilized to write
| messages to your logs. The value provided here should match one of
| the channels present in the list of "channels" configured below.
|
*/
'default' => env('LOG_CHANNEL', 'stack'),
/*
|--------------------------------------------------------------------------
| Deprecations Log Channel
|--------------------------------------------------------------------------
|
| This option controls the log channel that should be used to log warnings
| regarding deprecated PHP and library features. This allows you to get
| your application ready for upcoming major versions of dependencies.
|
*/
'deprecations' => [
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
],
/*
|--------------------------------------------------------------------------
| Log Channels
|--------------------------------------------------------------------------
|
| Here you may configure the log channels for your application. Laravel
| utilizes the Monolog PHP logging library, which includes a variety
| of powerful log handlers and formatters that you're free to use.
|
| Available drivers: "single", "daily", "slack", "syslog",
| "errorlog", "monolog", "custom", "stack"
|
*/
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
'ignore_exceptions' => false,
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => env('LOG_DAILY_DAYS', 14),
'replace_placeholders' => true,
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')),
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
'level' => env('LOG_LEVEL', 'critical'),
'replace_placeholders' => true,
],
'papertrail' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
'handler_with' => [
'host' => env('PAPERTRAIL_URL'),
'port' => env('PAPERTRAIL_PORT'),
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
],
'processors' => [PsrLogMessageProcessor::class],
],
'stderr' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => StreamHandler::class,
'handler_with' => [
'stream' => 'php://stderr',
],
'formatter' => env('LOG_STDERR_FORMATTER'),
'processors' => [PsrLogMessageProcessor::class],
],
'syslog' => [
'driver' => 'syslog',
'level' => env('LOG_LEVEL', 'debug'),
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
'replace_placeholders' => true,
],
'errorlog' => [
'driver' => 'errorlog',
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'null' => [
'driver' => 'monolog',
'handler' => NullHandler::class,
],
'emergency' => [
'path' => storage_path('logs/laravel.log'),
],
],
];
+118
View File
@@ -0,0 +1,118 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Mailer
|--------------------------------------------------------------------------
|
| This option controls the default mailer that is used to send all email
| messages unless another mailer is explicitly specified when sending
| the message. All additional mailers can be configured within the
| "mailers" array. Examples of each type of mailer are provided.
|
*/
'default' => env('MAIL_MAILER', 'log'),
/*
|--------------------------------------------------------------------------
| Mailer Configurations
|--------------------------------------------------------------------------
|
| Here you may configure all of the mailers used by your application plus
| their respective settings. Several examples have been configured for
| you and you are free to add your own as your application requires.
|
| Laravel supports a variety of mail "transport" drivers that can be used
| when delivering an email. You may specify which one you're using for
| your mailers below. You may also add additional mailers if needed.
|
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
| "postmark", "resend", "log", "array",
| "failover", "roundrobin"
|
*/
'mailers' => [
'smtp' => [
'transport' => 'smtp',
'scheme' => env('MAIL_SCHEME'),
'url' => env('MAIL_URL'),
'host' => env('MAIL_HOST', '127.0.0.1'),
'port' => env('MAIL_PORT', 2525),
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'timeout' => null,
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
],
'ses' => [
'transport' => 'ses',
],
'postmark' => [
'transport' => 'postmark',
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
// 'client' => [
// 'timeout' => 5,
// ],
],
'resend' => [
'transport' => 'resend',
],
'sendmail' => [
'transport' => 'sendmail',
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
],
'log' => [
'transport' => 'log',
'channel' => env('MAIL_LOG_CHANNEL'),
],
'array' => [
'transport' => 'array',
],
'failover' => [
'transport' => 'failover',
'mailers' => [
'smtp',
'log',
],
'retry_after' => 60,
],
'roundrobin' => [
'transport' => 'roundrobin',
'mailers' => [
'ses',
'postmark',
],
'retry_after' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Global "From" Address
|--------------------------------------------------------------------------
|
| You may wish for all emails sent by your application to be sent from
| the same address. Here you may specify a name and address that is
| used globally for all emails that are sent by your application.
|
*/
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')),
],
];
+219
View File
@@ -0,0 +1,219 @@
<?php
use Spatie\Permission\DefaultTeamResolver;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
return [
'models' => [
/*
* When using the "HasPermissions" trait from this package, we need to know which
* Eloquent model should be used to retrieve your permissions. Of course, it
* is often just the "Permission" model but you may use whatever you like.
*
* The model you want to use as a Permission model needs to implement the
* `Spatie\Permission\Contracts\Permission` contract.
*/
'permission' => Permission::class,
/*
* When using the "HasRoles" trait from this package, we need to know which
* Eloquent model should be used to retrieve your roles. Of course, it
* is often just the "Role" model but you may use whatever you like.
*
* The model you want to use as a Role model needs to implement the
* `Spatie\Permission\Contracts\Role` contract.
*/
'role' => Role::class,
/*
* When using the "Teams" feature from this package, we need to know which
* Eloquent model should be used to retrieve your teams. Of course, it
* is often just the "Team" model but you may use whatever you like.
*/
'team' => null,
/*
* When using the "HasModels" trait and passing raw IDs to syncModels,
* attachModels, or detachModels, this model class will be used to
* resolve those IDs. If null, defaults to the guard's model.
*/
'default_model' => null,
],
'table_names' => [
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your roles. We have chosen a basic
* default value but you may easily change it to any table you like.
*/
'roles' => 'roles',
/*
* When using the "HasPermissions" trait from this package, we need to know which
* table should be used to retrieve your permissions. We have chosen a basic
* default value but you may easily change it to any table you like.
*/
'permissions' => 'permissions',
/*
* When using the "HasPermissions" trait from this package, we need to know which
* table should be used to retrieve your models permissions. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'model_has_permissions' => 'model_has_permissions',
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your models roles. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'model_has_roles' => 'model_has_roles',
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your roles permissions. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'role_has_permissions' => 'role_has_permissions',
],
'column_names' => [
/*
* Change this if you want to name the related pivots other than defaults
*/
'role_pivot_key' => null, // default 'role_id',
'permission_pivot_key' => null, // default 'permission_id',
/*
* Change this if you want to name the related model primary key other than
* `model_id`.
*
* For example, this would be nice if your primary keys are all UUIDs. In
* that case, name this `model_uuid`.
*/
'model_morph_key' => 'model_id',
/*
* Change this if you want to use the teams feature and your related model's
* foreign key is other than `team_id`.
*/
'team_foreign_key' => 'team_id',
],
/*
* When set to true, the method for checking permissions will be registered on the gate.
* Set this to false if you want to implement custom logic for checking permissions.
*/
'register_permission_check_method' => true,
/*
* When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered
* this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated
* NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it.
*/
'register_octane_reset_listener' => false,
/*
* Events will fire when a role or permission is assigned/unassigned:
* \Spatie\Permission\Events\RoleAttachedEvent
* \Spatie\Permission\Events\RoleDetachedEvent
* \Spatie\Permission\Events\PermissionAttachedEvent
* \Spatie\Permission\Events\PermissionDetachedEvent
*
* To enable, set to true, and then create listeners to watch these events.
*/
'events_enabled' => false,
/*
* Teams Feature.
* When set to true the package implements teams using the 'team_foreign_key'.
* If you want the migrations to register the 'team_foreign_key', you must
* set this to true before doing the migration.
* If you already did the migration then you must make a new migration to also
* add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions'
* (view the latest version of this package's migration file)
*/
'teams' => false,
/*
* The class to use to resolve the permissions team id
*/
'team_resolver' => DefaultTeamResolver::class,
/*
* Passport Client Credentials Grant
* When set to true the package will use Passports Client to check permissions
*/
'use_passport_client_credentials' => false,
/*
* When set to true, the required permission names are added to exception messages.
* This could be considered an information leak in some contexts, so the default
* setting is false here for optimum safety.
*/
'display_permission_in_exception' => false,
/*
* When set to true, the required role names are added to exception messages.
* This could be considered an information leak in some contexts, so the default
* setting is false here for optimum safety.
*/
'display_role_in_exception' => false,
/*
* By default wildcard permission lookups are disabled.
* See documentation to understand supported syntax.
*/
'enable_wildcard_permission' => false,
/*
* The class to use for interpreting wildcard permissions.
* If you need to modify delimiters, override the class and specify its name here.
*/
// 'wildcard_permission' => Spatie\Permission\WildcardPermission::class,
/* Cache-specific settings */
'cache' => [
/*
* By default all permissions are cached for 24 hours to speed up performance.
* When permissions or roles are updated the cache is flushed automatically.
*/
'expiration_time' => DateInterval::createFromDateString('24 hours'),
/*
* The cache key used to store all permissions.
*/
'key' => 'spatie.permission.cache',
/*
* You may optionally indicate a specific cache driver to use for permission and
* role caching using any of the `store` drivers listed in the cache.php config
* file. Using 'default' here means to use the `default` set in cache.php.
*/
'store' => 'default',
],
];
+129
View File
@@ -0,0 +1,129 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Queue Connection Name
|--------------------------------------------------------------------------
|
| Laravel's queue supports a variety of backends via a single, unified
| API, giving you convenient access to each backend using identical
| syntax for each. The default queue connection is defined below.
|
*/
'default' => env('QUEUE_CONNECTION', 'database'),
/*
|--------------------------------------------------------------------------
| Queue Connections
|--------------------------------------------------------------------------
|
| Here you may configure the connection options for every queue backend
| used by your application. An example configuration is provided for
| each backend supported by Laravel. You're also free to add more.
|
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
| "deferred", "background", "failover", "null"
|
*/
'connections' => [
'sync' => [
'driver' => 'sync',
],
'database' => [
'driver' => 'database',
'connection' => env('DB_QUEUE_CONNECTION'),
'table' => env('DB_QUEUE_TABLE', 'jobs'),
'queue' => env('DB_QUEUE', 'default'),
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
'after_commit' => false,
],
'beanstalkd' => [
'driver' => 'beanstalkd',
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
'queue' => env('BEANSTALKD_QUEUE', 'default'),
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
'block_for' => 0,
'after_commit' => false,
],
'sqs' => [
'driver' => 'sqs',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
'queue' => env('SQS_QUEUE', 'default'),
'suffix' => env('SQS_SUFFIX'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'after_commit' => false,
],
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
'block_for' => null,
'after_commit' => false,
],
'deferred' => [
'driver' => 'deferred',
],
'background' => [
'driver' => 'background',
],
'failover' => [
'driver' => 'failover',
'connections' => [
'database',
'deferred',
],
],
],
/*
|--------------------------------------------------------------------------
| Job Batching
|--------------------------------------------------------------------------
|
| The following options configure the database and table that store job
| batching information. These options can be updated to any database
| connection and table which has been defined by your application.
|
*/
'batching' => [
'database' => env('DB_CONNECTION', 'sqlite'),
'table' => 'job_batches',
],
/*
|--------------------------------------------------------------------------
| Failed Queue Jobs
|--------------------------------------------------------------------------
|
| These options configure the behavior of failed queue job logging so you
| can control how and where failed jobs are stored. Laravel ships with
| support for storing failed jobs in a simple file or in a database.
|
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
*/
'failed' => [
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
'database' => env('DB_CONNECTION', 'sqlite'),
'table' => 'failed_jobs',
],
];
+38
View File
@@ -0,0 +1,38 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Third Party Services
|--------------------------------------------------------------------------
|
| This file is for storing the credentials for third party services such
| as Mailgun, Postmark, AWS and more. This file provides the de facto
| location for this type of information, allowing packages to have
| a conventional file to locate the various service credentials.
|
*/
'postmark' => [
'key' => env('POSTMARK_API_KEY'),
],
'resend' => [
'key' => env('RESEND_API_KEY'),
],
'ses' => [
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],
'slack' => [
'notifications' => [
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
],
],
];
+233
View File
@@ -0,0 +1,233 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Session Driver
|--------------------------------------------------------------------------
|
| This option determines the default session driver that is utilized for
| incoming requests. Laravel supports a variety of storage options to
| persist session data. Database storage is a great default choice.
|
| Supported: "file", "cookie", "database", "memcached",
| "redis", "dynamodb", "array"
|
*/
'driver' => env('SESSION_DRIVER', 'database'),
/*
|--------------------------------------------------------------------------
| Session Lifetime
|--------------------------------------------------------------------------
|
| Here you may specify the number of minutes that you wish the session
| to be allowed to remain idle before it expires. If you want them
| to expire immediately when the browser is closed then you may
| indicate that via the expire_on_close configuration option.
|
*/
'lifetime' => (int) env('SESSION_LIFETIME', 120),
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
/*
|--------------------------------------------------------------------------
| Session Encryption
|--------------------------------------------------------------------------
|
| This option allows you to easily specify that all of your session data
| should be encrypted before it's stored. All encryption is performed
| automatically by Laravel and you may use the session like normal.
|
*/
'encrypt' => env('SESSION_ENCRYPT', false),
/*
|--------------------------------------------------------------------------
| Session File Location
|--------------------------------------------------------------------------
|
| When utilizing the "file" session driver, the session files are placed
| on disk. The default storage location is defined here; however, you
| are free to provide another location where they should be stored.
|
*/
'files' => storage_path('framework/sessions'),
/*
|--------------------------------------------------------------------------
| Session Database Connection
|--------------------------------------------------------------------------
|
| When using the "database" or "redis" session drivers, you may specify a
| connection that should be used to manage these sessions. This should
| correspond to a connection in your database configuration options.
|
*/
'connection' => env('SESSION_CONNECTION'),
/*
|--------------------------------------------------------------------------
| Session Database Table
|--------------------------------------------------------------------------
|
| When using the "database" session driver, you may specify the table to
| be used to store sessions. Of course, a sensible default is defined
| for you; however, you're welcome to change this to another table.
|
*/
'table' => env('SESSION_TABLE', 'sessions'),
/*
|--------------------------------------------------------------------------
| Session Cache Store
|--------------------------------------------------------------------------
|
| When using one of the framework's cache driven session backends, you may
| define the cache store which should be used to store the session data
| between requests. This must match one of your defined cache stores.
|
| Affects: "dynamodb", "memcached", "redis"
|
*/
'store' => env('SESSION_STORE'),
/*
|--------------------------------------------------------------------------
| Session Sweeping Lottery
|--------------------------------------------------------------------------
|
| Some session drivers must manually sweep their storage location to get
| rid of old sessions from storage. Here are the chances that it will
| happen on a given request. By default, the odds are 2 out of 100.
|
*/
'lottery' => [2, 100],
/*
|--------------------------------------------------------------------------
| Session Cookie Name
|--------------------------------------------------------------------------
|
| Here you may change the name of the session cookie that is created by
| the framework. Typically, you should not need to change this value
| since doing so does not grant a meaningful security improvement.
|
*/
'cookie' => env(
'SESSION_COOKIE',
Str::slug((string) env('APP_NAME', 'laravel')).'-session'
),
/*
|--------------------------------------------------------------------------
| Session Cookie Path
|--------------------------------------------------------------------------
|
| The session cookie path determines the path for which the cookie will
| be regarded as available. Typically, this will be the root path of
| your application, but you're free to change this when necessary.
|
*/
'path' => env('SESSION_PATH', '/'),
/*
|--------------------------------------------------------------------------
| Session Cookie Domain
|--------------------------------------------------------------------------
|
| This value determines the domain and subdomains the session cookie is
| available to. By default, the cookie will be available to the root
| domain without subdomains. Typically, this shouldn't be changed.
|
*/
'domain' => env('SESSION_DOMAIN'),
/*
|--------------------------------------------------------------------------
| HTTPS Only Cookies
|--------------------------------------------------------------------------
|
| By setting this option to true, session cookies will only be sent back
| to the server if the browser has a HTTPS connection. This will keep
| the cookie from being sent to you when it can't be done securely.
|
*/
'secure' => env('SESSION_SECURE_COOKIE'),
/*
|--------------------------------------------------------------------------
| HTTP Access Only
|--------------------------------------------------------------------------
|
| Setting this value to true will prevent JavaScript from accessing the
| value of the cookie and the cookie will only be accessible through
| the HTTP protocol. It's unlikely you should disable this option.
|
*/
'http_only' => env('SESSION_HTTP_ONLY', true),
/*
|--------------------------------------------------------------------------
| Same-Site Cookies
|--------------------------------------------------------------------------
|
| This option determines how your cookies behave when cross-site requests
| take place, and can be used to mitigate CSRF attacks. By default, we
| will set this value to "lax" to permit secure cross-site requests.
|
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
| Supported: "lax", "strict", "none", null
|
*/
'same_site' => env('SESSION_SAME_SITE', 'lax'),
/*
|--------------------------------------------------------------------------
| Partitioned Cookies
|--------------------------------------------------------------------------
|
| Setting this value to true will tie the cookie to the top-level site for
| a cross-site context. Partitioned cookies are accepted by the browser
| when flagged "secure" and the Same-Site attribute is set to "none".
|
*/
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
/*
|--------------------------------------------------------------------------
| Session Serialization
|--------------------------------------------------------------------------
|
| This value controls the serialization strategy for session data, which
| is JSON by default. Setting this to "php" allows the storage of PHP
| objects in the session but can make an application vulnerable to
| "gadget chain" serialization attacks if the APP_KEY is leaked.
|
| Supported: "json", "php"
|
*/
'serialization' => 'json',
];
+1
View File
@@ -0,0 +1 @@
*.sqlite*
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* @extends Factory<User>
*/
class UserFactory extends Factory
{
/**
* The current password being used by the factory.
*/
protected static ?string $password;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'remember_token' => Str::random(10),
];
}
/**
* Indicate that the model's email address should be unverified.
*/
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
}
@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('tenants', function (Blueprint $table) {
$table->id();
$table->string('nome');
$table->string('slug')->unique();
$table->string('email')->nullable();
$table->string('telefono')->nullable();
$table->text('note')->nullable();
$table->boolean('attivo')->default(true);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('tenants');
}
};
@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('diocesi', function (Blueprint $table) {
$table->id();
$table->string('nome');
$table->string('regione')->nullable();
$table->timestamps();
});
Schema::create('comuni', function (Blueprint $table) {
$table->id();
$table->string('nome');
$table->string('codice_istat', 10)->nullable();
$table->string('cap')->nullable();
$table->string('sigla_provincia', 2)->nullable();
$table->string('regione')->nullable();
$table->float('latitudine')->nullable();
$table->float('longitudine')->nullable();
$table->timestamps();
$table->index('nome');
$table->index('sigla_provincia');
});
}
public function down(): void
{
Schema::dropIfExists('comuni');
Schema::dropIfExists('diocesi');
}
};
@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('individui', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
$table->string('codice_id', 5)->unique();
$table->string('cognome');
$table->string('nome');
$table->date('data_nascita')->nullable();
$table->string('indirizzo')->nullable();
$table->string('cap', 10)->nullable();
$table->string('città')->nullable();
$table->string('sigla_provincia', 2)->nullable();
$table->enum('genere', ['M', 'F'])->nullable();
$table->enum('tipo_documento', ['carta_identita', 'patente'])->nullable();
$table->date('scadenza_documento')->nullable();
$table->text('note')->nullable();
$table->string('avatar_path')->nullable();
$table->timestamps();
$table->index('tenant_id');
$table->index('codice_id');
$table->index('cognome');
});
}
public function down(): void
{
Schema::dropIfExists('individui');
}
};
@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('contatti', function (Blueprint $table) {
$table->id();
$table->foreignId('individuo_id')->constrained('individui')->onDelete('cascade');
$table->enum('tipo', ['telefono', 'cellulare', 'email', 'fax', 'web', 'telegram', 'whatsapp', 'altro']);
$table->string('valore');
$table->string('etichetta')->nullable();
$table->boolean('is_primary')->default(false);
$table->timestamps();
$table->index('individuo_id');
});
}
public function down(): void
{
Schema::dropIfExists('contatti');
}
};
@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('gruppi', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
$table->foreignId('parent_id')->nullable()->constrained('gruppi')->onDelete('set null');
$table->foreignId('diocesi_id')->nullable()->constrained('diocesi')->onDelete('set null');
$table->foreignId('responsabile_id')->nullable()->constrained('individui')->onDelete('set null');
$table->string('nome');
$table->text('descrizione')->nullable();
$table->string('indirizzo_incontro')->nullable();
$table->string('cap_incontro', 10)->nullable();
$table->string('città_incontro')->nullable();
$table->string('sigla_provincia_incontro', 2)->nullable();
$table->string('mappa_posizione')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
$table->index('tenant_id');
$table->index('parent_id');
});
}
public function down(): void
{
Schema::dropIfExists('gruppi');
}
};
@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('gruppo_individuo', function (Blueprint $table) {
$table->id();
$table->foreignId('gruppo_id')->constrained('gruppi')->onDelete('cascade');
$table->foreignId('individuo_id')->constrained('individui')->onDelete('cascade');
$table->string('ruolo_nel_gruppo')->nullable();
$table->date('data_adesione')->nullable();
$table->timestamps();
$table->unique(['gruppo_id', 'individuo_id']);
});
}
public function down(): void
{
Schema::dropIfExists('gruppo_individuo');
}
};
@@ -0,0 +1,48 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('eventi', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
$table->string('nome_evento');
$table->string('tipo_evento')->nullable();
$table->enum('tipo_recorrenza', ['singolo', 'ricorrente'])->default('singolo');
$table->unsignedTinyInteger('giorno_settimana')->nullable();
$table->time('ora_inizio')->nullable();
$table->date('data_specifica')->nullable();
$table->unsignedInteger('durata_minuti')->nullable();
$table->text('descrizione')->nullable();
$table->text('note')->nullable();
$table->timestamps();
$table->index('tenant_id');
});
Schema::create('eventi_gruppi', function (Blueprint $table) {
$table->id();
$table->foreignId('evento_id')->constrained('eventi')->onDelete('cascade');
$table->foreignId('gruppo_id')->constrained('gruppi')->onDelete('cascade');
$table->timestamps();
});
Schema::create('eventi_responsabili', function (Blueprint $table) {
$table->id();
$table->foreignId('evento_id')->constrained('eventi')->onDelete('cascade');
$table->foreignId('individuo_id')->constrained('individui')->onDelete('cascade');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('eventi_responsabili');
Schema::dropIfExists('eventi_gruppi');
Schema::dropIfExists('eventi');
}
};
@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('documenti', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
$table->string('nome_file');
$table->string('file_path');
$table->string('tipo')->nullable();
$table->enum('tipologia', ['avatar', 'galleria', 'documento', 'statuto', 'altro'])->default('documento');
$table->enum('visibilita', ['pubblico', 'gruppo', 'individuo', 'associazione', 'federazione'])->default('gruppo');
$table->unsignedBigInteger('visibilita_target_id')->nullable();
$table->string('visibilita_target_type')->nullable();
$table->string('mime_type')->nullable();
$table->unsignedBigInteger('dimensione')->nullable();
$table->text('note')->nullable();
$table->timestamps();
$table->index('tenant_id');
});
}
public function down(): void
{
Schema::dropIfExists('documenti');
}
};
@@ -0,0 +1,56 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('mailing_lists', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
$table->string('nome');
$table->text('descrizione')->nullable();
$table->boolean('attiva')->default(true);
$table->timestamps();
$table->index('tenant_id');
});
Schema::create('mailing_contacts', function (Blueprint $table) {
$table->id();
$table->foreignId('mailing_list_id')->constrained('mailing_lists')->onDelete('cascade');
$table->foreignId('individuo_id')->constrained('individui')->onDelete('cascade');
$table->boolean('opt_in')->default(true);
$table->dateTime('opt_in_data')->nullable();
$table->dateTime('opt_out_data')->nullable();
$table->timestamps();
$table->unique(['mailing_list_id', 'individuo_id']);
});
Schema::create('mailing_messaggi', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
$table->foreignId('mailing_list_id')->nullable()->constrained('mailing_lists')->onDelete('set null');
$table->unsignedBigInteger('user_id')->nullable();
$table->string('oggetto');
$table->text('corpo')->nullable();
$table->enum('canale', ['email', 'telegram'])->default('email');
$table->enum('stato', ['bozza', 'in_coda', 'inviato', 'fallito'])->default('bozza');
$table->timestamp('inviato_al')->nullable();
$table->integer('totale_destinatari')->default(0);
$table->integer('invii_ok')->default(0);
$table->integer('invii_falliti')->default(0);
$table->timestamps();
$table->index('tenant_id');
});
}
public function down(): void
{
Schema::dropIfExists('mailing_messaggi');
Schema::dropIfExists('mailing_contacts');
Schema::dropIfExists('mailing_lists');
}
};
@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('notifiche', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id')->nullable();
$table->string('tipo');
$table->string('titolo');
$table->text('messaggio');
$table->string('link')->nullable();
$table->boolean('is_read')->default(false);
$table->timestamp('read_at')->nullable();
$table->timestamps();
$table->index('user_id');
});
}
public function down(): void
{
Schema::dropIfExists('notifiche');
}
};
@@ -0,0 +1,47 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->boolean('is_admin')->default(false);
$table->rememberToken();
$table->timestamps();
$table->index('tenant_id');
$table->index('email');
});
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
public function down(): void
{
Schema::dropIfExists('sessions');
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('users');
}
};
@@ -0,0 +1,65 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('cache', function (Blueprint $table) {
$table->string('key')->primary();
$table->mediumText('value');
$table->integer('expiration');
});
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->integer('expiration');
});
Schema::create('jobs', function (Blueprint $table) {
$table->id();
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
Schema::create('job_batches', function (Blueprint $table) {
$table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->longText('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$table->integer('created_at');
$table->integer('finished_at')->nullable();
});
Schema::create('failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->text('connection');
$table->text('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at');
});
}
public function down(): void
{
Schema::dropIfExists('failed_jobs');
Schema::dropIfExists('job_batches');
Schema::dropIfExists('jobs');
Schema::dropIfExists('cache_locks');
Schema::dropIfExists('cache');
}
};
@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('individui', function (Blueprint $table) {
$table->string('numero_documento', 50)->nullable()->after('tipo_documento');
});
}
public function down(): void
{
Schema::table('individui', function (Blueprint $table) {
$table->dropColumn('numero_documento');
});
}
};
@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('individui', function (Blueprint $table) {
$table->dropColumn('telegram_id');
});
}
public function down(): void
{
Schema::table('individui', function (Blueprint $table) {
$table->string('telegram_id', 100)->nullable()->after('note');
});
}
};
@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('eventi', function (Blueprint $table) {
$table->string('giorno_mese')->nullable()->after('giorno_settimana');
$table->string('mesi_recorrenza')->nullable()->after('giorno_mese');
$table->string('mese_annuale')->nullable()->after('mesi_recorrenza');
});
}
public function down(): void
{
Schema::table('eventi', function (Blueprint $table) {
$table->dropColumn('giorno_mese');
$table->dropColumn('mesi_recorrenza');
$table->dropColumn('mese_annuale');
});
}
};
@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('eventi', function (Blueprint $table) {
$table->unsignedTinyInteger('occorrenza_mese')->nullable()->after('giorno_mese');
});
}
public function down(): void
{
Schema::table('eventi', function (Blueprint $table) {
$table->dropColumn('occorrenza_mese');
});
}
};
@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('eventi', function (Blueprint $table) {
$table->enum('tipo_recorrenza', ['singolo', 'settimanale', 'mensile', 'annuale', 'altro'])->nullable()->change();
});
}
public function down(): void
{
Schema::table('eventi', function (Blueprint $table) {
$table->enum('tipo_recorrenza', ['singolo', 'ricorrente'])->default('singolo')->change();
});
}
};
@@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('eventi_documenti', function (Blueprint $table) {
$table->id();
$table->foreignId('evento_id')->constrained('eventi')->onDelete('cascade');
$table->foreignId('documento_id')->constrained('documenti')->onDelete('cascade');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('eventi_documenti');
}
};
@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('documenti', function (Blueprint $table) {
$table->enum('visibilita', ['pubblico', 'gruppo', 'individuo', 'associazione', 'federazione', 'evento'])->default('gruppo')->change();
});
}
public function down(): void
{
Schema::table('documenti', function (Blueprint $table) {
$table->enum('visibilita', ['pubblico', 'gruppo', 'individuo', 'associazione', 'federazione'])->default('gruppo')->change();
});
}
};
@@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('documenti', function (Blueprint $table) {
$table->foreignId('user_id')->nullable()->constrained('users')->onDelete('set null')->after('tenant_id');
});
}
public function down(): void
{
Schema::table('documenti', function (Blueprint $table) {
$table->dropForeign(['user_id']);
$table->dropColumn('user_id');
});
}
};
@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('viste_report', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
$table->string('nome');
$table->string('tipo'); // individui, gruppi, documenti, eventi
$table->json('colonne_visibili')->nullable();
$table->json('colonne_ordinamento')->nullable(); // [['colonna', 'direzione']]
$table->json('filtri')->nullable(); // [['colonna', 'operatore', 'valore']]
$table->string('ricerca')->nullable();
$table->timestamps();
$table->index('tipo');
$table->index('user_id');
});
}
public function down(): void
{
Schema::dropIfExists('viste_report');
}
};
@@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('viste_report', function (Blueprint $table) {
$table->boolean('is_default')->default(false)->after('nome');
$table->index('is_default');
});
}
public function down(): void
{
Schema::table('viste_report', function (Blueprint $table) {
$table->dropColumn('is_default');
});
}
};

Some files were not shown because too many files have changed in this diff Show More