Compare commits
10 Commits
c333bbce4e
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 557bf3fcdf | |||
| f2ea5e160b | |||
| def8d22544 | |||
| c5127da750 | |||
| baea181159 | |||
| d2084b9d07 | |||
| 3b2ad1a0d5 | |||
| 92ba47e99f | |||
| 9d649180a6 | |||
| 6909e21b67 |
@@ -2,13 +2,17 @@ APP_NAME=Glastree
|
||||
APP_ENV=local
|
||||
APP_KEY=base64:aWz7908H9c7s+it9uMTwb6pyUrpddyMclcuN9Kzv7Ao=
|
||||
APP_DEBUG=true
|
||||
APP_URL=https://glastree.soon.it
|
||||
APP_PROTOCOL=https
|
||||
FORCE_HTTPS=true
|
||||
TRUSTED_PROXIES=*
|
||||
APP_URL=http://glastree.soon.it
|
||||
APP_PROTOCOL=http
|
||||
FORCE_HTTPS=false
|
||||
TRUSTED_PROXIES="10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1"
|
||||
|
||||
#GOOGLE_CLIENT_ID=980774223097-o5h7a6kepvg69te34fof2otn7ibi9uha.apps.googleusercontent.com
|
||||
#GOOGLE_CLIENT_SECRET=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
|
||||
|
||||
GOOGLE_CLIENT_ID=416164569291-mss5favrg1qdmbk343plaj1898g3200f.apps.googleusercontent.com
|
||||
GOOGLE_CLIENT_SECRET=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
|
||||
|
||||
GOOGLE_CLIENT_ID=980774223097-o5h7a6kepvg69te34fof2otn7ibi9uha.apps.googleusercontent.com
|
||||
GOOGLE_CLIENT_SECRET=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
|
||||
|
||||
|
||||
APP_LOCALE=it
|
||||
@@ -34,9 +38,11 @@ DB_DATABASE=glastree
|
||||
DB_USERNAME=glastree
|
||||
DB_PASSWORD=glastree
|
||||
|
||||
SESSION_DRIVER=file
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_SECURE_COOKIE=false
|
||||
SESSION_SAME_SITE=lax
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
|
||||
+9
-3
@@ -1,8 +1,12 @@
|
||||
APP_NAME="Glastree"
|
||||
APP_ENV=local
|
||||
APP_ENV=production
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_DEBUG=false
|
||||
APP_URL=http://localhost
|
||||
APP_PROTOCOL=https
|
||||
APP_SUBFOLDER=
|
||||
FORCE_HTTPS=false
|
||||
TRUSTED_PROXIES="10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1"
|
||||
|
||||
APP_LOCALE=it
|
||||
APP_FALLBACK_LOCALE=en
|
||||
@@ -24,9 +28,11 @@ DB_DATABASE=glastree
|
||||
DB_USERNAME=glastree
|
||||
DB_PASSWORD=
|
||||
|
||||
SESSION_DRIVER=file
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_SECURE_COOKIE=false
|
||||
SESSION_SAME_SITE=lax
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"version":2,"defects":{"Tests\\Feature\\DocumentoLinkTest::test_admin_can_create_link_document":7,"Tests\\Feature\\DocumentoLinkTest::test_create_link_requires_url":8,"Tests\\Feature\\DocumentoLinkTest::test_create_link_validates_url_format":8,"Tests\\Feature\\DocumentoLinkTest::test_create_link_does_not_require_file":7,"Tests\\Feature\\DocumentoLinkTest::test_create_link_sets_tipo_to_link":7,"Tests\\Feature\\DocumentoLinkTest::test_create_link_with_visibility_individuo":7,"Tests\\Feature\\DocumentoLinkTest::test_create_link_with_visibility_gruppo":7,"Tests\\Feature\\DocumentoLinkTest::test_download_link_redirects_to_url":8,"Tests\\Feature\\DocumentoLinkTest::test_preview_link_returns_fallback_view":8,"Tests\\Feature\\DocumentoLinkTest::test_update_link_url":8,"Tests\\Feature\\DocumentoLinkTest::test_update_link_preserves_url_when_not_sent":8,"Tests\\Feature\\DocumentoLinkTest::test_update_link_validates_url_format":8,"Tests\\Feature\\DocumentoLinkTest::test_destroy_link_does_not_call_storage":8,"Tests\\Feature\\DocumentoLinkTest::test_mass_download_skips_links":7},"times":{"Tests\\Unit\\DocumentoLinkTest::test_is_link_returns_true_when_tipo_is_link":0.001,"Tests\\Unit\\DocumentoLinkTest::test_is_link_returns_false_when_tipo_is_upload":0.001,"Tests\\Unit\\DocumentoLinkTest::test_is_link_returns_false_when_tipo_is_allegato":0.001,"Tests\\Unit\\DocumentoLinkTest::test_is_link_returns_false_when_tipo_is_null":0.002,"Tests\\Unit\\DocumentoLinkTest::test_url_is_fillable":0.001,"Tests\\Unit\\DocumentoLinkTest::test_link_doc_has_no_file_fields":0.001,"Tests\\Feature\\DocumentoLinkTest::test_admin_can_create_link_document":0.064,"Tests\\Feature\\DocumentoLinkTest::test_create_link_requires_url":0.008,"Tests\\Feature\\DocumentoLinkTest::test_create_link_validates_url_format":0.007,"Tests\\Feature\\DocumentoLinkTest::test_create_link_does_not_require_file":0.006,"Tests\\Feature\\DocumentoLinkTest::test_create_link_sets_tipo_to_link":0.014,"Tests\\Feature\\DocumentoLinkTest::test_create_link_with_visibility_individuo":0.008,"Tests\\Feature\\DocumentoLinkTest::test_create_link_with_visibility_gruppo":0.008,"Tests\\Feature\\DocumentoLinkTest::test_download_link_redirects_to_url":0.005,"Tests\\Feature\\DocumentoLinkTest::test_preview_link_returns_fallback_view":0.009,"Tests\\Feature\\DocumentoLinkTest::test_update_link_url":0.008,"Tests\\Feature\\DocumentoLinkTest::test_update_link_preserves_url_when_not_sent":0.006,"Tests\\Feature\\DocumentoLinkTest::test_update_link_validates_url_format":0.006,"Tests\\Feature\\DocumentoLinkTest::test_destroy_link_does_not_call_storage":0.004,"Tests\\Feature\\DocumentoLinkTest::test_mass_download_skips_links":0.004}}
|
||||
@@ -3,6 +3,59 @@
|
||||
## Obiettivo
|
||||
App gestionale Laravel 13 con AdminLTE 4 per gestione Persone e Gruppi.
|
||||
|
||||
## Riassunto Generale
|
||||
|
||||
| Area | Stato | Ultima modifica |
|
||||
|------|-------|-----------------|
|
||||
| **Core** (Individui, Gruppi, Eventi, Documenti, Mailing) | ✅ Completo | CRUD, relazioni, filtri, paginazione, ACL, export |
|
||||
| **Tag System** | ✅ Completo | morphToMany su 5 entità, filtri OR/AND, report, ricerca unificata |
|
||||
| **Viste Colonne** | ✅ Completo | Per-User column visibility/width/order, resize/drag-reorder, print |
|
||||
| **Email (IMAP/SMTP)** | ✅ Completo | Sync messaggi, firma, allegati, mittenti multipli, XOAUTH2 OAuth |
|
||||
| **Google OAuth 2.0** | ✅ Implementato | Unified (Email/Drive/Calendar), UI impostazioni, XOAUTH2 SMTP+IMAP |
|
||||
| **Repository Remoti** | ✅ Completo | WebDAV, Google Drive, OAuth |
|
||||
| **Calendario** | ✅ Completo | Google Calendar, CalDAV, sync bidirezionale, pulsante sync, fix salvataggio campi sensibili |
|
||||
| **Report** | ✅ Completo | Custom report con colonne/filtri salvabili, tag filter |
|
||||
| **Help / Documentazione** | ✅ Completo | Tab Google Drive + Calendar in help page, PDF export |
|
||||
| **Distribuzione** | ✅ Completo | build-dist.sh, fix path/cache/permessi/symlink |
|
||||
|
||||
### Cronologia modifiche principali
|
||||
- **2026-06-07**: Fix colonne mancanti (eventi, gruppo_individuo)
|
||||
- **2026-06-08**: build-dist.sh fix, logo remote fix, 18 migration guardie, help Calendar, sync pulsante, form annidato fix, checkbox hidden fallback, DiagnoseEmail command
|
||||
- **2026-06-09**: Tag completato (MailingList, Report, Ricerca), @stack scripts fix, storeCustom tag_filter fix, MailingList edit JS fix
|
||||
- **2026-06-10**: Per-User Column Views, ColumnManager JS, editVista data-* fix, 405 AJAX fix
|
||||
- **2026-06-17**: Google OAuth 2.0 unificato (revert Socialite, unified OAuth per Email/Drive/Calendar, XOAUTH2 SMTP+IMAP, UI impostazioni)
|
||||
- **2026-06-17**: Fix CalendarioConnessione — `encryptAndSetConfig()` perdeva campi sensibili in edit, `is_active` checkbox senza hidden fallback, query duplicata nella view
|
||||
- **2026-06-18**: Fix import CSV — `declare(strict_types=1)` mancante in IndividuoController, header check assente, Log::warning() su errori; fix freeze server (set_time_limit, session_write_close, DB::transaction, fgetcsv length illimitato) in GruppoController e IndividuoController
|
||||
- **2026-06-23**: Email compose: firma spostata dopo il corpo; Mailing list: aggiunto `sender_account_id` (mittente predefinito salvato in creazione, usato all'invio come fallback)
|
||||
- **2026-06-23**: Fix formati nome: responsabili gruppi e individui email ora mostrano "Nome Cognome"; mailing list edit: aggiunto pulsante "Rimuovi selezionati"
|
||||
- **2026-06-22**: Aggiunto supporto Email Mittente Ufficiale (from_email/from_name) per invio con indirizzo diverso dalle credenziali SMTP/IMAP
|
||||
- Migration: `from_email`, `from_name` columns su `email_settings`
|
||||
- `EmailSetting`: metodi `getEffectiveFromAddress()`, `getEffectiveFromName()`, `getEffectiveReplyTo()`
|
||||
- Tutti i metodi di invio (`sendViaImap`, `sendViaSystem`, `sendPasswordResetNotification`, `storeDraft`, `storeSentMessage`, `testSmtp`) ora usano `getEffectiveFromAddress()`/`getEffectiveFromName()` per il mittente
|
||||
- Reply-To automatico impostato su `from_email` quando disponibile
|
||||
- UI: nuovi campi "Email Mittente Ufficiale" e "Nome Mittente Ufficiale" in entrambe le pagine impostazioni email
|
||||
- **2026-06-22**: Backup system enhancements:
|
||||
- `BackupService::run()` ora accetta `$options` (include_files, include_env) per override temporanei senza persistere su DB
|
||||
- `BackupRunCommand --no-files/--no-env` non salva più permanentemente la configurazione (era un bug)
|
||||
- `manifest.json` ora include `included_components` array (database, env, files)
|
||||
- Vista backup: badge .env (grigio) e Files (giallo) nella colonna "Contenuto" basati sul manifest
|
||||
- `run()` restituisce `steps` array; mostrato in flash message (controller) e output (command)
|
||||
- **2026-06-18**: Diagnose script (`diagnose.php`), .env.example aggiornato (SESSION_SECURE_COOKIE, FORCE_HTTPS, TRUSTED_PROXIES), build-dist.sh aggiornato con passo diagnose
|
||||
- **2026-06-23**: Usabilità viste colonne: se non si clicca ✏️ su una vista, la vista attiva corrente viene auto-assunta come target di modifica. Se il nome viene cambiato, crea nuova vista invece di aggiornare (rilevamento rename via `currentVistaOriginalNome`)
|
||||
- **2026-06-23**: Aggiunto campo "Anno fondazione" (anno_fondazione) a Gruppo — migration, model (+fillable/+casts), controller (validazione store/update, import CSV, template CSV), create/edit/show/index views, import view field list
|
||||
- **2026-06-23**: Implementato "Documento come URL esterno" (link a risorsa esterna) — piena integrazione in email, mailing, preview, download, UI
|
||||
- Migration: `url` text nullable after `note`
|
||||
- Model: `url` in `$fillable` + metodo `isLink(): bool` (controlla `tipo === 'link'`)
|
||||
- DocumentoController: store() validazione condizionale file/URL via radio `tipo_caricamento`; update() campo URL condizionale; download() redirect away per link; preview() mostra fallback con pulsante "Apri link esterno"; massDownload() skip link-docs con counter warning; destroy() skip rimozione file
|
||||
- EmailController: processAttachments/resolveAttachmentPaths skip URL-docs dall'allegato fisico, accumulano links array; `appendLinksToBody()` inserisce link come testo nel corpo email prima della firma
|
||||
- MailingController: resolveMailingAttachmentPaths skip URL-docs, accumula links; `appendMailingLinksToBody()` stesso pattern
|
||||
- Upload modal (index.blade.php): toggle radio File/URL con campi condizionali (JS `toggleTipoCaricamento()`)
|
||||
- Grid/list view: icona link (`fa-link`) invece di mime icon per URL-docs; pulsante "Apri link esterno" invece di preview/download; move button hidden per link
|
||||
- Edit view (edit.blade.php): se link, mostra campo URL editabile + info URL nel sidebar invece di file_path/mime/dimensione
|
||||
- Preview fallback (preview-fallback.blade.php): supporto URL-docs con icona link + pulsante "Apri link esterno"
|
||||
- Email/mailing document selection table: badge "URL" nella riga documento
|
||||
- Route `documenti/{id}/download` per URL-docs: redirect HTTP 302 all'URL esterno
|
||||
|
||||
## Funzionalità Implementate
|
||||
|
||||
### Page-length selector
|
||||
@@ -291,8 +344,7 @@ if (!empty($contatto['individuo_id'])) { create }
|
||||
```
|
||||
|
||||
## Prossimi Passi
|
||||
- [DONE] ... (existing items)
|
||||
- Verificare end-to-end su remote: tutte le nuove mass action, mailing list con contatti senza email, ricerca multi-tag
|
||||
- [DONE] Verificare end-to-end su remote: tutte le nuove mass action, mailing list con contatti senza email, ricerca multi-tag
|
||||
- [DONE] Fix performance: spostata query `VistaReport::where(...)->get()` da `@php` nel partial `table-settings.blade.php` a tutti i 5 controller (prima veniva eseguita 1 query per ogni pagina load per ogni entity, anche senza mai aprire la modale)
|
||||
- [DONE] Rimosse vecchie modal legacy (`#saveVistaModal`, `#colonneModal`, `#vistaListModal`) da `individui` e `gruppi` — duplicate rispetto al nuovo modal unificato
|
||||
- [DONE] Rimosse vecchie funzioni JS (`saveVista()`, `toggleColumn()`, `showSaveVistaModal()`) da `individui` e `gruppi`
|
||||
@@ -300,9 +352,10 @@ if (!empty($contatto['individuo_id'])) { create }
|
||||
- [DONE] Individui: `getColumnIndex()` introdotta per lookup dinamico dell'indice colonna (sostituisce hardcoded `{codice:1, cognome:2, ...}` in `sortTable()` e `applyColumnFilter()`)
|
||||
- [DONE] I controller per eventi, documenti, mailing-liste hanno già `$tableColumns` con flag `visible` — stesso meccanismo di gruppi. Nessuna modifica necessaria.
|
||||
- [DONE] Fix: pulsante edit vista usa `data-*` attributi invece di `onclick` con `addslashes(json_encode(...))` — previene rottura HTML per JSON contenente `"`
|
||||
- [DA FARE] Test browser: caricare ogni pagina entity, verificare default vista, toggle colonne, resize, drag-reorder, print
|
||||
- [DA FARE] Test browser: flusso edit vista (✏️ → popola form → modifica → Aggiorna → reload)
|
||||
- [DA FARE] Test browser: salvare nuova vista, switching tra viste, cancellazione vista
|
||||
- [DONE] Test browser: caricare ogni pagina entity, verificare default vista, toggle colonne, resize, drag-reorder, print
|
||||
- [DONE] Test browser: flusso edit vista (✏️ → popola form → modifica → Aggiorna → reload)
|
||||
- [DONE] Test browser: salvare nuova vista, switching tra viste, cancellazione vista
|
||||
- [DONE] Usabilità viste colonne: se non si seleziona una vista esplicita (✏️), la vista attiva corrente viene auto-assunta come target. Se il nome viene cambiato, crea nuova vista invece di aggiornare (rename detection)
|
||||
|
||||
## 2026-06-10 — Per-User Column Views (colonne visibili, larghezze, ordine)
|
||||
|
||||
@@ -380,3 +433,741 @@ if (!empty($contatto['individuo_id'])) { create }
|
||||
**Fix**:
|
||||
1. `VistaReportController@update` (linea 101-103): aggiunto `if ($request->expectsJson()) { return response()->json([...]); }` — così il fetch riceve JSON 200, non un redirect 302.
|
||||
2. Fetch headers in `table-settings.blade.php`: aggiunto `'Accept': 'application/json'` — necessario perché `expectsJson()` controlla l'header `Accept`, non `Content-Type`.
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-17 — Google OAuth 2.0 Unificato (Email/Drive/Calendar)
|
||||
|
||||
**Obiettivo**: Implementare OAuth 2.0 centralizzato per tutti i servizi Google (Gmail email, Google Drive, Google Calendar), con UI nelle impostazioni e supporto XOAUTH2 per SMTP/IMAP via Symfony Mailer + ImapEngine. Sistema password attuale deve funzionare in parallelo.
|
||||
|
||||
### Cosa è stato fatto
|
||||
|
||||
**Enum** (`app/Enums/GoogleService.php`):
|
||||
- Definisce 3 casi: `Email`, `Drive`, `Calendar`
|
||||
- Metodi: `scopes()` (array scopes OAuth per servizio), `label()` (nome italiano), `icon()`, `description()` (testo help)
|
||||
|
||||
**Migration** (`2026_06_17_000001_create_google_oauth_connections_table.php`):
|
||||
- Tabella `google_oauth_connections`: id, user_id (FK), service, email, access_token, refresh_token, expires_at, timestamps
|
||||
- Unique constraint `[user_id, service, email]`
|
||||
- Già eseguita in batch 39
|
||||
|
||||
**Migration** (`2026_06_17_000002_add_oauth_columns.php`):
|
||||
- `email_settings`: aggiunti `auth_method` (varchar 20, default 'password') + `google_oauth_connection_id` (FK → google_oauth_connections, nullOnDelete)
|
||||
- `sender_accounts`: stesse colonne
|
||||
- Guardia `Schema::hasColumn` per idempotenza
|
||||
- Già eseguita in batch 39
|
||||
|
||||
**Model** (`app/Models/GoogleOAuthConnection.php`):
|
||||
- `$fillable`: user_id, service, email, access_token, refresh_token, expires_at
|
||||
- `$casts`: expires_at → datetime
|
||||
- `user()`: BelongsTo User
|
||||
- `isExpired()`: controllo scadenza token
|
||||
- `refresh()`: refresh automatico via GoogleOAuthService
|
||||
- `getValidAccessToken()`: refresh se expired, ritorna token valido
|
||||
|
||||
**Service** (`app/Services/GoogleOAuthService.php`):
|
||||
- `buildClient()`: configura Google_Client con client_id/secret/redirect_uri, access_type=offline, prompt=consent
|
||||
- `getAuthUrl(service)`: URL di autorizzazione con scopes specifici per servizio
|
||||
- `handleCallback(code, service)`: scambia code per token, crea/aggiorna GoogleOAuthConnection
|
||||
- `getAccessToken(connection)`: cache 5 min, refresh automatico se expired
|
||||
- `refreshToken(connection)`: fetch nuovo token via refresh_token, elimina se refresh fallisce
|
||||
- `revoke(connection)`: revoca token Google, elimina record DB
|
||||
- `createXoAuth2SmtpTransport(email, token)`: SMTP transport con solo XOAuth2Authenticator (evita tentativi PLAIN/LOGIN)
|
||||
- `getConnectionStatus()`: restituisce array con stato connessioni per ogni servizio
|
||||
|
||||
**Controller** (`app/Http/Controllers/GoogleOAuthController.php`):
|
||||
- `redirect(service)`: reindirizza a Google OAuth
|
||||
- `callback(request)`: gestisce risposta Google, crea connessione, redirect a impostazioni con success/error
|
||||
- `revoke(connection)`: revoca connessione (solo proprietario)
|
||||
- `status()`: JSON con stato connessioni
|
||||
|
||||
**Config/Route changes**:
|
||||
- `config/services.php`: sezione `google` con `client_id`, `client_secret`, `redirect` (usa `config('app.url')` per compatibilità CLI)
|
||||
- `routes/web.php`: 4 nuove route (`google-oauth.redirect`, `google-oauth.callback`, `google-oauth.revoke`, `google-oauth.status`) + `use App\Http\Controllers\GoogleOAuthController`
|
||||
|
||||
**View** (`resources/views/impostazioni/_google-oauth.blade.php`):
|
||||
- Card con lista servizi (Gmail, Drive, Calendar), badge connesso/non connesso, pulsanti Connetti/Disconnetti
|
||||
- JS: auto-click tab se `?tab=google` in URL (dopo callback)
|
||||
|
||||
**Modifiche EmailSetting.php**:
|
||||
- `$fillable` + `auth_method`, `google_oauth_connection_id`
|
||||
- `$casts` + `google_oauth_connection_id` → integer
|
||||
- `googleOAuthConnection()`: BelongsTo relationship
|
||||
- `getImapConfig()`: se auth_method=oauth, imposta `authentication => 'oauth'` e password = access token
|
||||
- `getDecryptedPassword()`: se OAuth, risolve token invece di decrittare password
|
||||
- `getDecryptedSmtpPassword()`: stessa logica per SMTP
|
||||
- `resolveOAuthToken()`: carica relazione + chiama GoogleOAuthService::getAccessToken
|
||||
- `getImapClient()`: passa `authentication => 'oauth'` se auth_method=oauth
|
||||
- `getSmtpMailer()`: se OAuth → `buildOAuthSmtpMailer()` (XOAuth2 solo), altrimenti DSN normale
|
||||
- `buildOAuthSmtpMailer()`: EsmtpTransport + XOAuth2Authenticator
|
||||
- `buildSmtpDsn()`: estratto da EmailSettingsController (password decrittata)
|
||||
|
||||
**Modifiche SenderAccount.php**:
|
||||
- Stesso pattern: `auth_method`, `google_oauth_connection_id`, relationship, `resolveOAuthToken()`
|
||||
- `getDecryptedPassword()`: OAuth-aware
|
||||
- `sendEmail()`: se OAuth → `buildOAuthMailer()` (XOAuth2), altrimenti DSN
|
||||
- `buildOAuthMailer()`: EsmtpTransport + XOAuth2Authenticator
|
||||
|
||||
**Modifiche EmailSettingsController.php**:
|
||||
- `index()`: passa `$googleStatus` → `GoogleOAuthService::getConnectionStatus()`
|
||||
- `save()`: validazione + salvataggio `auth_method`, `google_oauth_connection_id`
|
||||
- `testSmtp()`: ora usa `$settings->getSmtpMailer()` invece di costruire DSN manualmente
|
||||
- `senderStore()`/`senderUpdate()`: validazione + salvataggio `auth_method`, `google_oauth_connection_id`
|
||||
|
||||
**Modifiche impostazioni/index.blade.php**:
|
||||
- Sidebar link "Google" (icona `fab fa-google`) prima del link "Calendario"
|
||||
- Tab-pane con `@include("impostazioni._google-oauth")` prima del tab calendario
|
||||
|
||||
### Note tecniche
|
||||
- **XOAUTH2**: Symfony Mailer ha già `XOAuth2Authenticator` in vendor (`vendor/symfony/mailer/Transport/Smtp/Auth/XOAuth2Authenticator.php`), invia `AUTH XOAUTH2 user=<email>\1auth=Bearer <token>\1\1`
|
||||
- **ImapEngine**: già supporta `'authentication' => 'oauth'` in config → `AUTHENTICATE XOAUTH2 <base64>`
|
||||
- **google/apiclient**: presente in vendor, usato per Google_Client (buildClient, fetchAccessTokenWithAuthCode, refresh, revoke). `Google\Service\Gmail` NON disponibile ma non necessario (XOAUTH2 bypassa API REST)
|
||||
- **Permessi filesystem**: nuovi file copiati con sudo (www-data:www-data, 644), index.blade.php modificato con sed
|
||||
- **refresh token**: richiede `access_type=offline` + `prompt=consent` + `includeGrantedScopes=true` per garantire refresh token sempre al primo auth
|
||||
- **config('app.url')** in services.php invece di `url()` helper (url() fallisce in CLI context)
|
||||
|
||||
### 2026-06-17 — Bug Fix: Tag massivo e per-riga per Eventi + Documenti
|
||||
|
||||
**Problemi riscontrati**:
|
||||
1. Eventi: modal mass tag HTML corrotto (`×` → `<div class="modal fade" id="deleteModal"`)
|
||||
2. Eventi + Documenti: nessun feedback visivo dopo submit mass tag (mancava `session('error')` su Eventi; mancavano ENTRAMBI su Documenti)
|
||||
3. Eventi + Documenti: nessuna icona TAG per-riga nelle azioni
|
||||
4. Documenti: mancava funzione `openSingleTag()` JS
|
||||
|
||||
**Fix**:
|
||||
1. `resources/views/eventi/index.blade.php:340` — Ripristinato `×` nel close button del mass tag modal
|
||||
2. `resources/views/eventi/index.blade.php:22-27` — Aggiunto `@if(session('error'))` alert danger
|
||||
3. `resources/views/eventi/index.blade.php:283-285` — Aggiunto pulsante per-riga `fa-tags` → `openSingleTag(eventId)`
|
||||
4. `resources/views/eventi/index.blade.php:427-431` — Aggiunta funzione JS `openSingleTag(eventId)`
|
||||
5. `resources/views/documenti/index.blade.php:21-32` — Aggiunti `@if(session('success'))` + `@if(session('error'))` alert
|
||||
6. `resources/views/documenti/index.blade.php:283-285` e `484-486` — Aggiunto pulsante per-riga `fa-tags` in vista griglia + lista
|
||||
7. `resources/views/documenti/index.blade.php:1318-1323` — Aggiunta funzione JS `openSingleTag(docId)` con querySelector per `.doc-checkbox[value=...]`
|
||||
8. Verifica JS brace balance: OK su entrambi (diff=0)
|
||||
9. Verifica PHP lint: OK su entrambi i controller
|
||||
|
||||
## 2026-06-17 — Bug Fix: Documenti + Eventi (syntax error, strict_types, validazione)
|
||||
|
||||
### Problemi trovati e fixati
|
||||
|
||||
**1. CRITICO — Syntax error PHP in DocumentoController.php:239**
|
||||
- **Problema**: Parentesi `)` extra alla fine del ternario: `/documenti');` invece di `/documenti';`
|
||||
- **Root cause**: `$redirect = ... : '/documenti');` — parentesi di chiusura in eccesso
|
||||
- **Effetto**: PHP parse error: `Unclosed '{' on line 197 does not match ')'` — l'intero metodo `update()` era rotto
|
||||
- **Fix**: Rimosso `)` extra
|
||||
|
||||
**2. declare(strict_types=1) mancante** (violazione AGENTS.md)
|
||||
- **Problema**: 5 file del dominio documenti/eventi non avevano `declare(strict_types=1)`, obbligatorio per PHP 8.4
|
||||
- **File fixati**: `Documento.php`, `Evento.php`, `EventoController.php`, `TipologiaDocumento.php`, `EventoDocumentoController.php`
|
||||
|
||||
**3. Validazione assente `contesto_tipo` in DocumentoController@update**
|
||||
- **Problema**: Il campo `contesto_tipo` veniva letto dal raw request (`$request->contesto_tipo`) senza validazione. Un utente poteva forzare valori arbitrari.
|
||||
- **Fix**: Aggiunto `'contesto_tipo' => 'nullable|in:individuo,gruppo,evento,mailing'` alle regole di validazione. Ora si usa il valore validato (`$contestoTipo`) invece del raw request.
|
||||
|
||||
**4. Tipologia hardcoded in DocumentoController@store**
|
||||
- **Problema**: `store()` usava `'tipologia' => 'required|in:avatar,galleria,documento,statuto,altro'` (hardcoded), mentre `update()` usava `TipologiaDocumento::opzioni()` (dinamico). Nuove tipologie aggiunte via UI non sarebbero state accettate da `store()`.
|
||||
- **Fix**: Allineato `store()` a usare `TipologiaDocumento::opzioni()` come `update()`.
|
||||
|
||||
**5. JS brace balance verificato**
|
||||
- `resources/views/documenti/index.blade.php` → OK (diff=0)
|
||||
- `resources/views/eventi/index.blade.php` → OK (diff=0)
|
||||
|
||||
### Verifica
|
||||
- `php -l` su tutti e 6 i file modificati: nessun errore di sintassi
|
||||
|
||||
## 2026-06-17 — Fix: Eventi mass tag "nessun evento selezionato"
|
||||
**Problema**: La toolbar button chiamava `$('#massTagModal').modal('show')` direttamente, affidandosi a un listener `show.bs.modal` per popolare l'hidden field `massTagIds`. Se l'evento non veniva intercettato, `ids` restava vuoto e il server rispondeva "Nessun evento selezionato".
|
||||
|
||||
**Fix** (`resources/views/eventi/index.blade.php`):
|
||||
1. Toolbar button: `onclick="$('#massTagModal').modal('show')"` → `onclick="showMassTagModal()"`
|
||||
2. Rimosso listener `show.bs.modal` che popolava i campi
|
||||
3. Aggiunta funzione `showMassTagModal()` (stesso pattern di Gruppi/Individui) che valida `getSelectedIds()` e popola i campi **prima** di aprire la modale
|
||||
4. `openSingleTag()` ora chiama `showMassTagModal()` invece di mostrare la modale direttamente
|
||||
|
||||
## 2026-06-17 — Fix: Documenti — root mostra documenti di tutte le cartelle
|
||||
**Problema**: Alla root (`/documenti`), la query includeva tutti i documenti locali (`WHERE repository_id IS NULL`), mostrando documenti di tutte le cartelle in un unico elenco piatto.
|
||||
|
||||
**Fix** (`app/Http/Controllers/DocumentoController.php`):
|
||||
- Aggiunto `->whereNull('cartella_id')` alla query di root, così mostra solo documenti **senza cartella** (radice). Per vedere i documenti dentro una cartella bisogna navigarci dentro con `?folder_id=X`.
|
||||
|
||||
## 2026-06-17 — Fix: CalendarioConnessione — salvataggio perdeva password/client_secret in edit + is_active sempre true
|
||||
|
||||
**Problema**: Salvando una connessione calendario esistente (CalDAV o Google Calendar), i campi sensibili (password, client_secret) venivano sovrascritti con stringa vuota. Il test connessione e la sincronizzazione fallivano silenziosamente.
|
||||
|
||||
**Root cause 1 — `encryptAndSetConfig()` sovrascrive campi vuoti**:
|
||||
- Il JS di edit (`editCalendario()`) azzera `config[password]` e `config[client_secret]` per sicurezza (valore `''`, placeholder `......`)
|
||||
- `encryptAndSetConfig()` in `CalendarioConnessione.php` controllava `$config[$field] !== ''` e saltava la crittografia, ma poi eseguiva `$this->config = $config` — la stringa vuota sovrascriveva il valore crittato precedente
|
||||
- `StorageRepository` NON aveva questo bug: `StorageRepositoryService::encryptSensitiveConfig()` preserva i campi esistenti con `} elseif (empty($config[$field]) && !empty($existingConfig[$field])) { $config[$field] = $existingConfig[$field]; }`
|
||||
|
||||
**Root cause 2 — `is_active` checkbox senza hidden fallback**:
|
||||
- Stesso bug già fixato per email (2026-06-08): checkbox `is_active` senza `<input type="hidden" name="is_active" value="0">`
|
||||
- Quando non spuntato, il campo non veniva inviato → `$request->boolean('is_active', true)` restituiva sempre `true`
|
||||
- La connessione restava sempre attiva indipendentemente dallo switch
|
||||
|
||||
**Root cause 3 — Query duplicata nella view**:
|
||||
- `resources/views/impostazioni/index.blade.php:773` ri-eseguiva `CalendarioConnessione::orderBy('ordine')->get()` nonostante il controller lo passasse già via `compact()`
|
||||
|
||||
**Fix**:
|
||||
1. **`app/Models/CalendarioConnessione.php:56`** — `encryptAndSetConfig()` ora accetta `array $existingConfig = []` e preserva i valori crittati esistenti quando il campo submitted è vuoto (stesso pattern di `StorageRepositoryService::encryptSensitiveConfig()`)
|
||||
2. **`app/Http/Controllers/CalendarioConnessioneController.php:67`** — `update()` passa `$connessione->config` come secondo parametro a `encryptAndSetConfig()`
|
||||
3. **`resources/views/impostazioni/index.blade.php:1576`** — Aggiunto `<input type="hidden" name="is_active" value="0">` prima della checkbox
|
||||
4. **`resources/views/impostazioni/index.blade.php:773`** — Rimossa query duplicata `@php $calendarioConnessioni = ...`
|
||||
|
||||
**Verifica**:
|
||||
- PHP lint: OK su entrambi i file modificati
|
||||
- JS brace balance: OK
|
||||
|
||||
## 2026-06-17 — Fix: Documenti mass tag "nessun documento selezionato"
|
||||
**Problema**: Stesso identico bug di Eventi — toolbar button chiamava `$('#massTagModal').modal('show')` e il listener `show.bs.modal` non sempre popolava i campi.
|
||||
|
||||
**Fix** (`resources/views/documenti/index.blade.php`):
|
||||
1. Rimosso listener `show.bs.modal`
|
||||
2. Creata funzione `showMassTagModal()` (con fallback per `data-pre-selected`)
|
||||
3. `openSingleTag()` ora usa `showMassTagModal()` invece di mostrare la modale direttamente
|
||||
|
||||
## 2026-06-17 — Fix: ColumnManager pin colonne select/azioni con `cells[]` invece di `querySelector`
|
||||
**Problema**: Il pinning di `select` e `azioni` usava `row.querySelector('[data-column="..."]')` che funziona solo sul `<thead>` (dove `<th>` ha `data-column`), ma non sul `<tbody>` (dove i `<td>` non hanno `data-column`).
|
||||
|
||||
**Fix** (`public/js/column-manager.js`): Sostituito con `cells[keyToThIndex['select']]` che usa l'indice della colonna, valido sia per `th` che per `td`.
|
||||
|
||||
### DA FARE
|
||||
- Configurare `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` in `.env`
|
||||
- Test end-to-end: flusso OAuth completo (redirect → auth → callback → connessione creata)
|
||||
- Test XOAUTH2 SMTP: invio email via connessione OAuth
|
||||
- Test IMAP OAuth: sync email via connessione OAuth
|
||||
- Verificare refresh token automatico allo scadere
|
||||
- Verificare revoca e riconnessione
|
||||
|
||||
## 2026-06-17 — Fix: tutte le migration + seeders idempotenti
|
||||
|
||||
### Problema
|
||||
`php artisan migrate` falliva su tabelle già esistenti (`tenants`, `tipologie_documenti`, ecc.).
|
||||
`php artisan db:seed` creava duplicati su re-run (admin user, diocesi, comuni).
|
||||
Migrations con `insert` dopo il guard `hasTable` violavano unique constraint al re-run.
|
||||
|
||||
### Migration: hasTable guard su 28 CREATE mancanti
|
||||
Aggiunto `if (!Schema::hasTable('table_name')) { ... }` wrapper a 28 migration file (45 `Schema::create()` calls) via script Node.js, più `2026_06_17_000001_create_google_oauth_connections_table.php` manualmente.
|
||||
|
||||
### Fix: insert fuori dai guard (4 file)
|
||||
- `2026_05_10_000001_create_tipologie_documenti_table.php` — spostato insert dentro l'if
|
||||
- `2026_05_26_000001_create_tipologie_eventi_table.php` — spostato insert dentro l'if
|
||||
- `2026_05_12_000001_create_ruoli_table.php` — spostato insert dentro l'if
|
||||
- `2026_05_11_000006_add_email_attachment_tipologia.php` — `insert()` → `updateOrInsert()`
|
||||
|
||||
### Seeder: firstOrCreate
|
||||
- `DiocesiSeeder.php`: `Diocesi::create()` → `::firstOrCreate(['nome' => ...])`
|
||||
- `ComuniSeeder.php`: `Comune::create()` → `::firstOrCreate(['codice_istat' => ...])`
|
||||
- `DatabaseSeeder.php`: `User::create()` → `::firstOrCreate(['email' => ...])` + guardia `role_preset_id`
|
||||
|
||||
### Risultato
|
||||
`php artisan migrate --seed` è ora completamente idempotente. Zero errori su 64 migration + 6 seeder. Re-run è un no-op.
|
||||
|
||||
## 2026-06-17 — Fix: MailingList tag management (mass tag + per-riga + attiva checkbox)
|
||||
|
||||
**Problema**: Nella GUI non era possibile assegnare tag alle mailing list né singolarmente né via azione massiva.
|
||||
|
||||
**Root cause**:
|
||||
1. **Nessuna route mass-tag per mailing-liste** — Eventi, Documenti, Individui, Gruppi avevano `POST /{entity}/mass-tag` ma MailingList no
|
||||
2. **Nessun metodo `massTag()` in MailingListController** — assente rispetto agli altri 4 controller
|
||||
3. **Nessun pulsante per-riga tag** nella index view (mancava `<button type="button" onclick="openSingleTag()">`)
|
||||
4. **Nessun pulsante toolbar "Tag"** nella index view (mancava il mass tag button)
|
||||
5. **Checkbox `attiva` senza hidden fallback** in create/edit — stesso bug ricorrente: deselezionando, il campo non veniva inviato
|
||||
|
||||
**Fix**:
|
||||
1. **`app/Http/Controllers/MailingListController.php:189-214`** — Aggiunto metodo `massTag()` (stesso pattern di `EventoController@massTag`):
|
||||
- Autorizzazione via `$this->authorizeWrite('mailing')`
|
||||
- Validazione: `tags` required array, `mode` required in:assign,remove
|
||||
- Processa in chunk(100) via `syncWithoutDetaching()` (assign) / `detach()` (remove)
|
||||
- Restituisce `back()->with('success', ...)`
|
||||
2. **`routes/web.php:177`** — Aggiunta route `POST mailing-liste/mass-tag` → `MailingListController@massTag` (name: `mailing-liste.mass-tag`)
|
||||
3. **`resources/views/mailing-liste/index.blade.php`**:
|
||||
- `select-all` checkbox: permesso esteso da `canDeleteMailing` a `canWriteMailing || canDeleteMailing`
|
||||
- `row-checkbox`: stessa estensione permesso
|
||||
- Toolbar: aggiunto pulsante "Tag" (btn-sm btn-info, `showMassTagModal()`) prima di "Elimina Selezionati"
|
||||
- Azioni per-riga: aggiunto pulsante `fa-tags` con `openSingleTag(lista.id)` dopo il pulsante show
|
||||
- Aggiunto `@if(session('error'))` alert
|
||||
- Aggiunta modale `#massTagModal` (stesso pattern di eventi: titolo, counter, radio assign/remove, `_tag-selector`, hidden `ids`)
|
||||
- Aggiunte funzioni JS: `showMassTagModal()`, `openSingleTag(listId)`
|
||||
4. **`resources/views/mailing-liste/create.blade.php:23`** — Aggiunto `<input type="hidden" name="attiva" value="0">` prima della checkbox
|
||||
5. **`resources/views/mailing-liste/edit.blade.php:23`** — Stesso hidden fallback
|
||||
6. **`resources/views/mailing-liste/edit.blade.php:11`** — Fix percorso hardcoded: `url('/mailing-liste/' . ...)` → `route('mailing-liste.update', ...)`
|
||||
|
||||
**Verifica**:
|
||||
- PHP lint: OK su controller + routes
|
||||
- JS brace balance: OK su index view
|
||||
- Route list: `POST mailing-liste/mass-tag` → `MailingListController@massTag` (name: `mailing-liste.mass-tag`)
|
||||
|
||||
## 2026-06-18 — Fix import CSV: freeze server + bug IndividuoController
|
||||
|
||||
### Problema freeze server (Gruppi + Individui)
|
||||
Importando CSV con molte righe, il server si bloccava:
|
||||
1. **PHP max_execution_time (30s)** — killava il processo su import grandi
|
||||
2. **Session lock** — altre richieste dello stesso utente restavano in attesa
|
||||
3. **Nessuna transazione DB** — ogni `create()` era una INSERT individuale autocommit
|
||||
4. **fgetcsv length=1000** — righe CSV più lunghe di 1000 caratteri venivano troncate
|
||||
|
||||
### Fix applicati a `GruppoController@importStore` e `IndividuoController@importStore`
|
||||
```php
|
||||
set_time_limit(0);
|
||||
session_write_close();
|
||||
$header = fgetcsv($handle, 0, ','); // length=0 = nessun limite
|
||||
DB::beginTransaction();
|
||||
// ... loop ...
|
||||
DB::commit();
|
||||
```
|
||||
|
||||
### Bug trovati in `IndividuoController@importStore`
|
||||
| Bug | Fix |
|
||||
|-----|-----|
|
||||
| `declare(strict_types=1)` mancante | ✅ Aggiunto |
|
||||
| Indentazione errata negli import (spazi extra su 8 righe) | ✅ Corretto |
|
||||
| `use Illuminate\Support\Facades\Log` mancante | ✅ Aggiunto |
|
||||
| Nessun controllo `$header === false` — CSV vuoto causava TypeError | ✅ Aggiunto |
|
||||
| Nessun `array_map('trim', $header)` | ✅ Aggiunto |
|
||||
| Nessun `Log::warning()` su eccezioni riga | ✅ Aggiunto |
|
||||
| `fgetcsv` length=1000 (vs 0) | ✅ Portato a 0 |
|
||||
| Nessun `set_time_limit(0)` / `session_write_close()` | ✅ Aggiunto |
|
||||
| Nessun `DB::beginTransaction()` / `DB::commit()` | ✅ Aggiunto |
|
||||
|
||||
### File modificati
|
||||
- `app/Http/Controllers/GruppoController.php` — aggiunti `use DB`, set_time_limit, session_write_close, fgetcsv length=0, DB::transaction
|
||||
- `app/Http/Controllers/IndividuoController.php` — strict_types, Log import, DB import, header check, trim, set_time_limit, session_write_close, DB::transaction
|
||||
|
||||
### Verifica
|
||||
- `php -l` su entrambi: nessun errore di sintassi
|
||||
|
||||
## 2026-06-18 — Diagnose script + .env.example + build-dist.sh
|
||||
|
||||
### `diagnose.php` — Script standalone di diagnostica
|
||||
Creato script standalone per verificare installazione su server remoto. Non richiede Laravel.
|
||||
|
||||
**Cosa verifica**:
|
||||
- PHP version (≥ 8.2), 15 estensioni required, 4 opzionali
|
||||
- INI settings (execution_time, memory, upload/post max)
|
||||
- .env: APP_KEY decodifica (32 bytes), APP_URL, SESSION_SECURE_COOKIE, DB, FORCE_HTTPS
|
||||
- 13 directory in `storage/` e `bootstrap/cache`: esistenza, permessi, scrivibilità
|
||||
- Symlink `public/storage` (supporto sia link assoluti che relativi)
|
||||
- Test scrittura/lettura file sessione
|
||||
- Connessione DB via PDO, tabelle principali, conteggio records, engine InnoDB
|
||||
- Middleware bootstrap/app.php: ForceHttps registrato, TrustProxies, duplicazioni web group
|
||||
- Vendor: autoload, composer, node_modules, public/build, artisan
|
||||
- CSRF: HTTP GET login page, estrazione token, POST con token, verifica 419
|
||||
- Spazio disco: totale, libero, percentuale
|
||||
|
||||
**Uso**:
|
||||
```bash
|
||||
php diagnose.php
|
||||
php diagnose.php --verbose # output dettagliato
|
||||
```
|
||||
Exit code: 0 = OK, 1 = errori critici.
|
||||
|
||||
### `.env.example` aggiornato
|
||||
Aggiunte variabili mancanti:
|
||||
- `APP_PROTOCOL`, `APP_SUBFOLDER` — supporto subfolder
|
||||
- `FORCE_HTTPS`, `TRUSTED_PROXIES` — proxy/HTTPS configurabile
|
||||
- `SESSION_SECURE_COOKIE`, `SESSION_SAME_SITE` — sessione cross-protocol
|
||||
- `APP_ENV=production`, `APP_DEBUG=false` — safe defaults per produzione
|
||||
|
||||
### `build-dist.sh`
|
||||
- Aggiunto `php diagnose.php` come passo #0 (pre-installazione) e #7 (verifica finale)
|
||||
- Istruzioni post-estrazione complete
|
||||
|
||||
### File modificati
|
||||
- `diagnose.php` (nuovo) — standalone diagnostica
|
||||
- `.env.example` — variabili mancanti aggiunte
|
||||
- `build-dist.sh` — passo diagnose
|
||||
|
||||
### Verifica
|
||||
- `php -l diagnose.php`: OK
|
||||
- `php diagnose.php`: eseguito con 58/68 check superati su server locale
|
||||
|
||||
## 2026-06-19 — Fix 419: Duplicated middleware + session env vars + diagnose.php fixes
|
||||
|
||||
### Problema
|
||||
`POST /login` restituiva **419 Page Expired** su HTTP. La causa era doppia:
|
||||
|
||||
1. **Critico — Middleware duplicato in `bootstrap/app.php`**: `EncryptCookies`, `AddQueuedCookiesToResponse`, `StartSession`, `ShareErrorsFromSession` erano appesi al web group via `$middleware->web(append: [...])`. Questi middleware sono **già attivi di default** in Laravel 13. Appenderli una seconda volta causava doppia crittografia/decrittografia dei cookie di sessione → session ID corrotto → token CSRF non riconosciuto → 419.
|
||||
2. **`SESSION_SECURE_COOKIE` non impostato** nel `.env` — con `APP_URL=https` + `FORCE_HTTPS=true`, Laravel impostava il flag `Secure` sul cookie di sessione. Poiché il server serviva solo HTTP, il browser non inviava il cookie → session persa → 419.
|
||||
|
||||
### Fix apportati
|
||||
|
||||
**`bootstrap/app.php`** (righe 22-27):
|
||||
- Rimosso l'intero blocco `$middleware->web(append: [...])` che duplicava EncryptCookies, AddQueuedCookiesToResponse, StartSession, ShareErrorsFromSession.
|
||||
- Sostituito con commento esplicativo per prevenire future reintroduzioni.
|
||||
|
||||
**`.env`**:
|
||||
- Aggiunto `SESSION_SECURE_COOKIE=false` — impedisce il flag Secure su HTTP
|
||||
- Aggiunto `SESSION_SAME_SITE=lax` — esplicito per consistenza cross-protocol
|
||||
- Cambiato `APP_URL` da HTTPS a HTTP (ambiente locale senza HTTPS)
|
||||
- Cambiato `APP_PROTOCOL` da `https` a `http`
|
||||
- Cambiato `FORCE_HTTPS` da `true` a `false`
|
||||
- Cambiato `TRUSTED_PROXIES` da `*` a CIDR espliciti
|
||||
|
||||
**`diagnose.php`** — 4 bug fix:
|
||||
1. **`unlink()` senza `file_exists()`** nelle linee 863 e 900: causava PHP Warning quando curl non creava il cookie jar (es. HTTPS non raggiungibile). Aggiunto `file_exists($cookieJar) && unlink($cookieJar)`.
|
||||
2. **Regex middleware duplicato non matchava array multi-riga**: il pattern `[^\]]*` non matcha newline. Sostituito con `(.+?)` con flag `s`. Classe name extraction semplificata a `([a-zA-Z]+)::class`.
|
||||
3. **`CURLOPT_COOKIEJAR` non funzionante** in questo ambiente (curl 8.14.1 + PHP 8.4 — il file non viene creato da `curl_close()`). Riscritto `testCsrfLogin()` per parsare manualmente gli header `Set-Cookie` dalla prima risposta e reinviarli via `CURLOPT_COOKIE` nella POST. Eliminata dipendenza da `COOKIEJAR`/`COOKIEFILE`.
|
||||
4. Aggiunto verbose debug per codici HTTP POST anomali (≠ 302/200).
|
||||
|
||||
### Risultato finale
|
||||
```
|
||||
✅ Passed: 63
|
||||
⚠️ Warnings: 8 (upload/post_max_size, engine opzionali mancanti)
|
||||
❌ Failed: 1 (spazio disco 94.6% — server admin)
|
||||
```
|
||||
- Test CSRF: ✅ POST login → HTTP 302 (nessun 419)
|
||||
- Solo il disco (94.6%) è critico — non risolvibile via script.
|
||||
|
||||
### File modificati
|
||||
- `bootstrap/app.php` — rimosso middleware web duplicato
|
||||
- `.env` — aggiunte SESSION_SECURE_COOKIE, SESSION_SAME_SITE; APP_URL/APP_PROTOCOL/FORCE_HTTPS adeguati
|
||||
- `diagnose.php` — fix unlink warning, regex middleware multi-line, CSRF test manual-cookie, verbose debug
|
||||
|
||||
## 2026-06-19 — diagnose.php: storage false positivo + ForceHttps registration check
|
||||
|
||||
### Fix 1 — Storage permission false positive
|
||||
**Problema**: `diagnose.php` segnava 15 storage directory come ❌ NON SCRIVIBILE quando `php diagnose.php` veniva eseguito da CLI (utente `opencode`). Le directory sono di proprietà `www-data:www-data` con permessi `775`, quindi il web server **può** scrivere — il falso positivo generava confusione.
|
||||
|
||||
**Diagnosi**:
|
||||
- CLI esegue come `opencode` → `is_writable()` controlla i permessi "other" (5 = r-x) → `false`
|
||||
- Web server esegue come `www-data` → owner `rwx` → `true`
|
||||
- Questo è normale su server condivisi/Debian standard
|
||||
|
||||
**Fix** (`diagnose.php:659-660`):
|
||||
- Aggiunto `elseif ($owner === WEB_USER && in_array($perms, ['775', '777', '755', '770']))`
|
||||
- Se il proprietario è `www-data` e permessi sono `775`/`777`/`755`/`770`, mostra ⚠️ **warning** invece di ❌ **fail**, con messaggio "NON scrivibile da CLI ma WEB server sì"
|
||||
- Altrimenti mostra fail come prima
|
||||
|
||||
### Fix 2 — ForceHttps registration check
|
||||
**Problema**: Il controllo ForceHttps usava `str_contains($appContent, 'ForceHttps')` che matchava anche il semplice `use App\Http\Middleware\ForceHttps` (import), segnando ✅ "registrato" anche quando il middleware non era attivo in alcun gruppo.
|
||||
|
||||
**Stato attuale**: `ForceHttps::class` è **importato** ma **non registrato** in nessun middleware group (`$middleware->append()`, `->web()`, ecc.) in `bootstrap/app.php`.
|
||||
|
||||
**Fix** (`diagnose.php:871-879`):
|
||||
- Sostituito con `preg_match('/\$middleware\s*->\s*\w+\s*\(.*?ForceHttps::class/s', $appContent)`
|
||||
- 3 stati:
|
||||
1. ✅ `"registrato correttamente"` — se ForceHttps::class è argomento di una chiamata `$middleware->xxx()`
|
||||
2. ⚠️ `"IMPORTATO ma NON registrato"` — se la stringa ForceHttps esiste (use import) ma non in una chiamata
|
||||
3. ⚠️ `"NON presente"` — se ForceHttps non esiste nel file
|
||||
|
||||
### Verifica
|
||||
- `php -l diagnose.php`: No syntax errors detected
|
||||
- `php diagnose.php` su locale: ForceHttps ora segnala ⚠️ "IMPORTATO ma NON registrato" (corretto)
|
||||
|
||||
### File modificati
|
||||
- `diagnose.php` — storage false positive + ForceHttps registration check
|
||||
|
||||
## 2026-06-22 — build-dist.sh: genera post-deploy.sh per upgrade/install automatico
|
||||
|
||||
**Obiettivo**: Includere nell'archivio di distribuzione uno script `post-deploy.sh` che esegue controllo e setup dell'installazione sul server target, sia per fresh install che per upgrade.
|
||||
|
||||
### Cosa è stato fatto
|
||||
|
||||
**`build-dist.sh`** riscritto:
|
||||
- Prima del `tar`, genera `post-deploy.sh` (heredoc) e lo include nell'archivio
|
||||
- Dopo il `tar`, `post-deploy.sh` viene rimosso localmente (non sporca la working dir)
|
||||
- Istruzioni finali aggiornate: mostrano i tre comandi principali
|
||||
|
||||
**`post-deploy.sh`** (generato, non versionato):
|
||||
Tre modalità d'uso:
|
||||
|
||||
| Comando | Cosa fa |
|
||||
|---------|---------|
|
||||
| `bash post-deploy.sh check` | Diagnostica stato installazione: migration pendenti, permessi directory, symlink, cache. Nessuna modifica. |
|
||||
| `bash post-deploy.sh upgrade` | Check + upgrade esistente: crea directory, permessi, symlink, `migrate --force`, `key:generate` (se mancante), cache clear, verifica finale. Chiede conferma prima di eseguire. |
|
||||
| `bash post-deploy.sh install` | Fresh install completo: come upgrade + copia `.env.example → .env` con pausa per modifica utente. |
|
||||
| `bash post-deploy.sh upgrade --yes` | Batch mode (nessuna conferma richiesta) |
|
||||
|
||||
Fasi upgrade (8 step): directory → .gitignore → permessi → symlink → migrazioni → key → cache → diagnose
|
||||
Fasi install (9 step): come upgrade + setup .env con pausa interattiva
|
||||
|
||||
### Vantaggi
|
||||
- **Idempotente**: `migrate --force` esegue solo migration pendenti
|
||||
- **Sicuro**: modalità `check` non modifica nulla
|
||||
- **Interattivo**: chiede conferma prima di azioni distruttive
|
||||
- **Batch**: `--yes` per automazione CI/CD
|
||||
- **Compatibile**: integra `diagnose.php` già esistente (check + fix)
|
||||
- **Autopulente**: `post-deploy.sh` non resta nella working dir di build
|
||||
|
||||
### Verifica
|
||||
- `bash -n build-dist.sh`: syntax OK
|
||||
- `bash -n post-deploy.sh`: syntax OK
|
||||
- `bash post-deploy.sh check`: eseguito correttamente
|
||||
- `tar tzf glastree-*.tar.gz | grep post-deploy`: presente nell'archivio
|
||||
- `rm -f post-deploy.sh` dopo tar: pulizia OK
|
||||
|
||||
## 2026-06-22 — Estensione Gruppi: contatti propri + diocesi multiple
|
||||
|
||||
**Obiettivo**: Aggiungere contatti (email, telefono) ai gruppi e supportare assegnazione di più diocesi.
|
||||
|
||||
### Cosa è stato fatto
|
||||
|
||||
**Migration** (2 nuove tabelle):
|
||||
- `2026_06_22_000002_create_gruppo_contatti_table.php` — `gruppo_contatti` con: id, gruppo_id (FK cascade), tipo, valore, etichetta, is_primary, timestamps
|
||||
- `2026_06_22_000003_create_diocesi_gruppo_table.php` — pivot `diocesi_gruppo` con: id, gruppo_id (FK cascade), diocesi_id (FK cascade), unique(gruppo_id, diocesi_id)
|
||||
|
||||
**Model `GruppoContatto.php`** (nuovo):
|
||||
- `$table = 'gruppo_contatti'`, fillable: gruppo_id, tipo, valore, etichetta, is_primary
|
||||
- `gruppo()`: BelongsTo Gruppo
|
||||
|
||||
**Model `Gruppo.php`**:
|
||||
- `diocesi()`: cambiata da `BelongsTo` a `BelongsToMany` via `diocesi_gruppo`
|
||||
- `gruppoContatti()`: nuova `HasMany` relation
|
||||
- Accessor `getEmailPrimariaAttribute()`: primo contatto email (is_primary preferito)
|
||||
- Accessor `getTelefonoPrimarioAttribute()`: primo contatto telefono/cellulare (is_primary preferito)
|
||||
- `$appends`: già presente `email_primaria`, `telefono_primario` (invariato)
|
||||
- `$fillable`: `diocesi_id` mantenuto (nullable) per backward compat con import CSV
|
||||
|
||||
**Model `Diocesi.php`**:
|
||||
- `gruppi()`: cambiata da `HasMany` a `BelongsToMany` via `diocesi_gruppo`
|
||||
|
||||
**`GruppoController.php`**:
|
||||
- `store()`/`update()`: validazione cambiata da `diocesi_id` (nullable|exists) a `diocesi_ids` (nullable|array|exists)
|
||||
- `store()`/`update()`: nuova validazione `contatti` array con tipo (in:email,telefono,cellulare), valore, etichetta, is_primary
|
||||
- `store()`: crea contatti dopo create; sync diocesi
|
||||
- `update()`: cancella+ricrea contatti se presenti; sync diocesi (o detach se assenti)
|
||||
- `edit()`: eager-load `gruppoContatti`, passa `$selectedDiocesiIds`
|
||||
- `show()`: eager-load `gruppoContatti`
|
||||
- `index()`: eager-load `gruppoContatti`
|
||||
- `create()`: invariato (passa già `$diocesi`)
|
||||
|
||||
**`ReportController.php`**:
|
||||
- 4 occorrenze `$g->diocesi?->nome` → collection pattern `$g->diocesi->count() > 0 ? $g->diocesi->pluck('nome')->implode(', ') : '-'`
|
||||
|
||||
**View `gruppi/create.blade.php`**:
|
||||
- Select diocesi: cambiato da single `select name="diocesi_id"` a multiple `select name="diocesi_ids[]" class="select2-multi"`
|
||||
- Aggiunta card "Contatti del Gruppo" con tabella inline (tipo select, valore input, etichetta, checkbox primario, elimina)
|
||||
- Select2 CSS/JS caricati via CDN
|
||||
|
||||
**View `gruppi/edit.blade.php`**:
|
||||
- Stessa modifica diocesi → Select2 multi con `$selectedDiocesiIds` pre-selezionati
|
||||
- Aggiunta card "Contatti del Gruppo" con righe pre-popolate da `$gruppo->gruppoContatti`
|
||||
- Select2 CSS/JS caricati via CDN
|
||||
|
||||
**View `gruppi/show.blade.php`**:
|
||||
- Diocesi: da `$gruppo->diocesi?->nome` a collection implode
|
||||
- Aggiunte righe Email/Telefono (da accessor) e "Altri Contatti" nella card info
|
||||
|
||||
**View `gruppi/index.blade.php`**:
|
||||
- Diocesi: da `$gruppo->diocesi?->nome` a collection implode
|
||||
- Telefono/Email: da hardcoded `-` a `$gruppo->telefono_primario` / `$gruppo->email_primaria`
|
||||
|
||||
**View `gruppi/partials/tree-item.blade.php`**:
|
||||
- Diocesi: da `$gruppo->diocesi?->nome` a collection implode
|
||||
|
||||
**View `individui/show.blade.php` + `individui/edit.blade.php`**:
|
||||
- Diocesi nei gruppi dell'individuo: da `$gruppo->diocesi?->nome` a collection implode
|
||||
|
||||
### Backward compatibility
|
||||
- Colonna `diocesi_id` su `gruppi` mantenuta (nullable) per import CSV
|
||||
- CSV import (`importStore()`) usa ancora `diocesi_id` → singola diocesi
|
||||
- `$fillable` include ancora `diocesi_id`
|
||||
- Nessuna modifica a `Individuo` o `Contatto` esistenti
|
||||
|
||||
### Verifica
|
||||
- `php -l` su tutti i file modificati: OK
|
||||
- Migrations eseguite: ✅
|
||||
- Relazioni verificate: `diocesi()` BelongsToMany, `gruppoContatti()` HasMany, `email_primaria`/`telefono_primario` accessors funzionanti
|
||||
|
||||
## 2026-06-22 — Fix produzione: 500 pagina gruppi (migration pending + autoloader stale)
|
||||
|
||||
**Problema**: Dopo deploy su server produzione (`192.168.222.177`):
|
||||
- Migration `diocesi_gruppo` non eseguita (PENDING)
|
||||
- `GruppoContatto.php` non nell'autoloader (`composer dump-autoload` non eseguito)
|
||||
- `storage/logs/laravel.log` non scrivibile da www-data
|
||||
- 3 migration pre-esistenti non marcate come "ran" (google_oauth_connections, add_auth_method, add_google_id) — tabelle/colonne già esistenti ma migration record mancanti → bloccavano `migrate --force`
|
||||
|
||||
**Fix**:
|
||||
1. `sudo chmod -R 775 storage bootstrap/cache` — permessi
|
||||
2. `sudo usermod -a -G www-data opencode` — utente CLI nel gruppo www-data
|
||||
3. Inseriti manualmente 3 migration record mancanti in `migrations` table (già eseguite in passato ma mai registrate)
|
||||
4. `php artisan migrate --force` — eseguita `2026_06_22_000003_create_diocesi_gruppo_table`
|
||||
5. `composer dump-autoload` — autoloader rigenerato (41464 classi)
|
||||
6. `php artisan view:clear && php artisan route:clear && php artisan config:clear`
|
||||
|
||||
**Risultato**:
|
||||
- `class_exists(App\Models\GruppoContatto)` → ✅
|
||||
- `gruppi` page HTTP 200 (after login redirect 302) — 500 risolto
|
||||
- Tutte le migration marked as Ran (batch 40-42)
|
||||
- Views/routes/config cache pulite
|
||||
|
||||
**Lezione**: `build-dist.sh` fa `composer install --no-scripts` che non rigenera autoloader ottimizzato per il target. `post-deploy.sh upgrade` dovrebbe includere `composer dump-autoload` (o almeno `composer install --no-dev --optimize-autoloader`).
|
||||
|
||||
## 2026-06-22 — Fix: Select2 search field visibile sotto le diocesi selezionate
|
||||
|
||||
**Problema**: In create/edit gruppi, il `<textarea class="select2-search__field">` creato da Select2 in modalità multiple era visibile sotto i tag delle diocesi selezionate, con altezza 65px (ereditata da Bootstrap form-control).
|
||||
|
||||
**Causa**:
|
||||
1. `allowClear: true` in Select2 multiple mode non serve (ogni tag ha già la X per rimuoverlo) e causa conflitti di rendering
|
||||
2. Bootstrap 4 applica `height: 65px` al textarea del search field
|
||||
3. Nessun CSS specifico per normalizzare il search field inline
|
||||
|
||||
**Fix**:
|
||||
1. Rimosso `allowClear: true` da entrambi i file (`create.blade.php`, `edit.blade.php`)
|
||||
2. Aggiunto CSS per normalizzare `height: 28px`, `border: none`, `background: transparent`, `width: auto` con `min-width: 30px`
|
||||
|
||||
**File modificati**:
|
||||
- `resources/views/gruppi/create.blade.php` — rimosso allowClear, aggiunto CSS search field
|
||||
- `resources/views/gruppi/edit.blade.php` — rimosso allowClear, aggiunto CSS search field
|
||||
|
||||
## 2026-06-23 — Fix: Select2 counter rimosso (utente vuole vedere tutti i nomi)
|
||||
|
||||
**Problema**: Il `templateSelection` con counter mostrava "5 diocesi selezionate" invece dei nomi.
|
||||
|
||||
**Fix**: Rimosso l'intero blocco `templateSelection` da entrambi create/edit. Ogni diocesi selezionata mostra ora il proprio nome.
|
||||
|
||||
**File modificati**:
|
||||
- `resources/views/gruppi/create.blade.php` — rimosso templateSelection
|
||||
- `resources/views/gruppi/edit.blade.php` — rimosso templateSelection
|
||||
|
||||
## 2026-06-23 — Fix: DiocesiSeeder + build-dist.sh per deploy
|
||||
|
||||
**Problema**: La tabella `diocesi` con 225 record importati da ODS non veniva popolata sul server target dopo deploy. Il seeder `DiocesiSeeder.php` aveva solo ~100 nomi obsoleti/inaccurati (es. "Mongolia", "Donegal", "Tirana").
|
||||
|
||||
**Fix**:
|
||||
1. **`DiocesiSeeder.php`** riscritto completamente:
|
||||
- 225 nomi corretti (Arcidiocesi/Diocesi/Sede/Patriarcato/Abbazia/Eparachia)
|
||||
- Encoding: 7 nomi con mojibake da ODS fixati via `where('id', ...)->update()` (Trinità, Cefalù, Città, Forlì, Mondovì, Nardò, Perugia-Città)
|
||||
- Apostrofo: "Val d Elsa" → "Val d'Elsa"
|
||||
- `declare(strict_types=1)` aggiunto
|
||||
- `firstOrCreate` per idempotenza
|
||||
2. **`build-dist.sh`** (`post-deploy.sh` generato):
|
||||
- Upgrade: aggiunto step `[6/10] Seed diocesi` dopo migrazioni (rinumerati da [1-5/9] → [1-10/10])
|
||||
- Install: aggiunto step `[9/11] Seed diocesi` dopo migrazioni (rinumerato da [1-8/10] → [1-11/11])
|
||||
- Usa `--force` per bypassare conferma in produzione
|
||||
|
||||
**File modificati**:
|
||||
- `database/seeders/DiocesiSeeder.php` — riscritto con 225 nomi + fix encoding
|
||||
- `build-dist.sh` — aggiunto step seed diocesi in upgrade/install
|
||||
|
||||
## 2026-06-23 — Fix 419: SESSION_DRIVER=database + diagnose CSRF su IP locale
|
||||
|
||||
**Problema**: Accesso via IP di rete locale (`http://192.168.222.174`) poteva causare 419 Page Expired. Sessione su file vulnerabile a permessi/LOCK del filesystem.
|
||||
|
||||
**Fix**:
|
||||
1. **`SESSION_DRIVER=database`**: cambiato da `file` a `database`. La sessione su DB non soffre di:
|
||||
- Permessi filesystem errati
|
||||
- Lock concorrente su file
|
||||
- Pulizia sessioni scadute lottery-based
|
||||
2. **Migration `2026_06_23_000001_create_sessions_table.php`**: con guard `Schema::hasTable` per idempotenza cross-DB
|
||||
3. **`diagnose.php`**: aggiunto test CSRF su IP locale automatico. Rileva `hostname -I` ed esegue POST login → verifica 419 su `http://<lan-ip>/login`
|
||||
4. **`.env` e `.env.example`**: `SESSION_DRIVER=file` → `SESSION_DRIVER=database`
|
||||
5. Vecchi file di sessione in `storage/framework/sessions/` eliminati
|
||||
|
||||
**Risultato**:
|
||||
- diagnose.php: `✅ HTTP: POST login → HTTP 302 (nessun 419)`
|
||||
- diagnose.php: `✅ LOCAL IP: POST login → HTTP 302 (nessun 419)`
|
||||
- Sessioni persistono in tabella `sessions` (DB) invece di file
|
||||
|
||||
**File modificati**:
|
||||
- `database/migrations/2026_06_23_000001_create_sessions_table.php` (nuovo)
|
||||
- `.env` — SESSION_DRIVER=database
|
||||
- `.env.example` — SESSION_DRIVER=database
|
||||
- `diagnose.php` — test CSRF su IP locale
|
||||
|
||||
## 2026-06-23 — Email compose: firma dopo corpo + Mailing list: mittente e firma
|
||||
|
||||
**Obiettivo**: Spostare la select firma dopo il corpo email nella pagina di composizione email. Aggiungere mittente predefinito (`sender_account_id`) alle mailing list (salvato in creazione/edit, usato come fallback all'invio).
|
||||
|
||||
### Cosa è stato fatto
|
||||
|
||||
**Migration** (`2026_06_23_000002_add_sender_account_id_to_mailing_lists_table.php`):
|
||||
- Aggiunta colonna `sender_account_id` (FK → sender_accounts, nullOnDelete) a `mailing_lists`
|
||||
|
||||
**Model `MailingList.php`**:
|
||||
- `sender_account_id` aggiunto a `$fillable`
|
||||
- `senderAccount()`: nuova relazione BelongsTo → `SenderAccount`
|
||||
|
||||
**Controller `MailingListController.php`**:
|
||||
- `create()`: passa `$senderAccounts` (SenderAccount::active()->get()) alla view
|
||||
- `edit()`: passa `$senderAccounts` alla view, eager-load `senderAccount`
|
||||
- `store()`: validazione `sender_account_id` nullable|exists, salvato in create
|
||||
- `update()`: validazione `sender_account_id` nullable|exists, salvato in update
|
||||
|
||||
**View `mailing-liste/create.blade.php`**:
|
||||
- Aggiunto select per `sender_account_id` dopo il select firma
|
||||
|
||||
**View `mailing-liste/edit.blade.php`**:
|
||||
- Aggiunto select per `sender_account_id` dopo il select firma, con `selected` se uguale a `$mailingList->sender_account_id`
|
||||
|
||||
**View `email/compose.blade.php`**:
|
||||
- Spostato blocco firma (prima del body) → dopo il textarea body (prima della sezione allegati)
|
||||
|
||||
**Controller `MailingController.php`**:
|
||||
- `invia()`: fallback a `$lista->senderAccount` se `mittente_id` non fornito esplicitamente (stesso pattern del firma_id fallback già esistente)
|
||||
- `invioElabora()`: fallback a `mittente_id`/`firma_id` dalla mailing list se non forniti esplicitamente (solo quando una singola lista è selezionata)
|
||||
|
||||
**File modificati**:
|
||||
- `database/migrations/2026_06_23_000002_add_sender_account_id_to_mailing_lists_table.php` (nuovo)
|
||||
- `app/Models/MailingList.php` — fillable + senderAccount relazione
|
||||
- `app/Http/Controllers/MailingListController.php` — sender_account in create/edit/store/update
|
||||
- `app/Http/Controllers/MailingController.php` — fallback mittente/firma da mailing list
|
||||
- `resources/views/email/compose.blade.php` — firma spostata dopo il body
|
||||
- `resources/views/mailing-liste/create.blade.php` — select mittente aggiunto
|
||||
- `resources/views/mailing-liste/edit.blade.php` — select mittente aggiunto
|
||||
|
||||
## 2026-06-23 — Fix document move: modal event listener + PHP empty-ids guard
|
||||
|
||||
**Problema**: Spostare documenti (singolo via icona cartella o mass move via toolbar) mostrava "successo" ma non spostava nulla. Il documento rimaneva nella cartella originale.
|
||||
|
||||
**Root cause**:
|
||||
1. **JS — Bootstrap 4 event compatibility**: `addEventListener('show.bs.modal', ...)` al mass move modal usava native DOM listener, ma Bootstrap 4.6.2 dispone `show.bs.modal` come evento jQuery. Il listener non veniva mai eseguito → hidden input `massMoveIds` mai popolato → `explode(',', '')` → `['']` → `empty([''])` è `false` → `Documento::whereIn('id', [''])` (0 rows affected) → falso successo.
|
||||
2. **PHP — No guardia su array vuoto dopo explode**: `massMove()`, `massDownload()`, `massDestroy()`, `massAssociate()`, `massTag()` in `DocumentoController.php` non filtravano valori vuoti/non-numerici da `$ids`.
|
||||
|
||||
**Fix**:
|
||||
- `resources/views/documenti/index.blade.php:1306`: `document.getElementById('massMoveModal')?.addEventListener('show.bs.modal', ...)` → `$('#massMoveModal').on('show.bs.modal', ...)` (jQuery event, coerente con tutti gli altri listener nello stesso file)
|
||||
- `app/Http/Controllers/DocumentoController.php`: aggiunto `array_values(array_filter($ids, fn($v) => is_numeric($v)))` in `massMove()`, `massDownload()`, `massDestroy()`, `massAssociate()`, `massTag()` — filtra stringhe vuote e non-numeriche prima di ogni `whereIn`
|
||||
- `massMove()`: aggiunto controllo esplicito `count($ids)` per validare singolo documento nel flusso per-documento
|
||||
|
||||
**File modificati**:
|
||||
- `resources/views/documenti/index.blade.php` — `addEventListener` → jQuery `.on()` a riga 1306
|
||||
- `app/Http/Controllers/DocumentoController.php` — `array_filter` guard in 5 mass-action metodi
|
||||
|
||||
**Verifica**:
|
||||
- PHP lint: OK su controller
|
||||
- JS brace balance: OK (diff=0)
|
||||
- Tutti gli altri listener modal nel file usano già jQuery `.on()` — il fix allinea `massMoveModal` al pattern esistente
|
||||
- Nessun test automatizzato presente nel progetto (no Pest, no PHPUnit config)
|
||||
|
||||
## 2026-06-23 — Test + Fix migration per compatibilità SQLite + massDownload unlink fix
|
||||
|
||||
### Tests scritti
|
||||
- **`tests/Unit/DocumentoLinkTest.php`**: 6 unit test per `Documento::isLink()` — copre tutti i valori `tipo` (link, upload, allegato, null), verifica `url` fillable, verifica link-doc senza file fields
|
||||
- **`tests/Feature/DocumentoLinkTest.php`**: 14 feature test per link-doc CRUD:
|
||||
- `store` con link: creazione base, validazione required url, validazione formato url, skip file, set tipo=link, visibilità individuo, visibilità gruppo
|
||||
- `update`: modifica URL, preserva URL se non inviato, validazione formato
|
||||
- `download`: redirect away all'URL esterno
|
||||
- `preview`: vista fallback con header X-Preview-Fallback
|
||||
- `destroy`: delete senza chiamate Storage
|
||||
- `massDownload`: skip link-doc con errore "Nessun file disponibile"
|
||||
- **Totale**: 20 test, 45 assertion, tutti ✅ passanti su SQLite `:memory:`
|
||||
|
||||
### Fix migration per compatibilità SQLite (test cross-DB)
|
||||
3 migration non compatibili con SQLite fixate per permettere test su `:memory:`:
|
||||
|
||||
1. **`2026_05_12_000002_change_ruolo_to_multi.php`**:
|
||||
- Aggiunto `Schema::hasColumn('gruppo_individuo', 'ruolo_id')` guard prima di `dropForeign`/`dropColumn`
|
||||
- Aggiunto `dropIndex(['ruolo_id'])` prima di `dropColumn` (SQLite richiede drop index esplicito)
|
||||
|
||||
2. **`2024_01_01_000008_create_documenti_table.php`**:
|
||||
- `$table->enum('tipologia', [...])` → `$table->string('tipologia', 50)` (app-level validation)
|
||||
- `$table->enum('visibilita', [...])` → `$table->string('visibilita', 50)` (app-level validation)
|
||||
- `$table->string('file_path')` → `$table->string('file_path')->nullable()` (necessario per link-docs)
|
||||
|
||||
3. **`2026_05_11_000005_add_email_attachment_to_documenti.php`**: reso no-op (colonna già string, app-level validation)
|
||||
4. **`2026_05_13_072203_add_programma_locandina_to_tipologia_enum.php`**: reso no-op (colonna già string, app-level validation)
|
||||
|
||||
### Fix massDownload unlink edge case
|
||||
- **`app/Http/Controllers/DocumentoController.php:710`**: Aggiunto `file_exists($zipPath)` guard prima di `@unlink($zipPath)` — preveniva `ErrorException` quando ZipArchive non creava file su alcuni filesystem (documenti tutti link → addedCount=0 → file zip non creato fisicamente → unlink falliva)
|
||||
|
||||
### Fix produzione: file_path NOT NULL su MySQL
|
||||
- **Problema**: La colonna `file_path` su MySQL è `NOT NULL` (migrazione originale `$table->string('file_path')`). Inserendo un link-doc (`tipo='link'`) senza `file_path`, MySQL rifiuta: `Field 'file_path' doesn't have a default value`.
|
||||
- **Fix**: Nuova migration `2026_06_23_085637_make_file_path_nullable_in_documenti.php` → `ALTER TABLE documenti MODIFY COLUMN file_path VARCHAR(255) NULL` su MySQL
|
||||
- **Eseguita** localmente ✅ (80ms)
|
||||
|
||||
### build-dist.sh
|
||||
- Nessuna modifica necessaria: `php artisan migrate --force` è già presente in `post-deploy.sh` sia per upgrade (`[5/10]`) che per install (`[8/11]`)
|
||||
|
||||
### Prossimi Passi
|
||||
- ✅ Eseguire `php artisan migrate` su produzione per rendere `file_path` nullable
|
||||
- Verificare in ambiente di staging: creazione link-doc (tutte le visibilità), visualizzazione grid/list, preview fallback, download (redirect), modifica URL, eliminazione, mass-download con skip link, email/mailing con link nel corpo
|
||||
- Test edge case: URL malformato (validazione update), link senza url, link con cartella_id, link con visibilità individuo/gruppo
|
||||
|
||||
@@ -28,16 +28,17 @@ class BackupRunCommand extends Command
|
||||
{
|
||||
$this->info('Avvio backup...');
|
||||
|
||||
$options = [];
|
||||
if ($this->option('no-files')) {
|
||||
$this->backupService->saveConfig(['backup_include_files' => false]);
|
||||
$options['include_files'] = false;
|
||||
$this->warn('Files esclusi dal backup.');
|
||||
}
|
||||
if ($this->option('no-env')) {
|
||||
$this->backupService->saveConfig(['backup_include_env' => false]);
|
||||
$options['include_env'] = false;
|
||||
$this->warn('.env escluso dal backup.');
|
||||
}
|
||||
|
||||
$result = $this->backupService->run();
|
||||
$result = $this->backupService->run($options);
|
||||
|
||||
if ($result['success']) {
|
||||
$this->info(' Backup completato con successo!');
|
||||
@@ -45,6 +46,13 @@ class BackupRunCommand extends Command
|
||||
$this->line('Dimensione: ' . $result['size_formatted']);
|
||||
$this->line('Percorso: ' . storage_path('app/backups/' . $result['filename']));
|
||||
|
||||
if (!empty($result['steps'])) {
|
||||
$this->line('Contenuto:');
|
||||
foreach ($result['steps'] as $step) {
|
||||
$this->line(' - ' . $step);
|
||||
}
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum GoogleService: string
|
||||
{
|
||||
case Email = 'email';
|
||||
case Drive = 'drive';
|
||||
case Calendar = 'calendar';
|
||||
|
||||
public function scopes(): array
|
||||
{
|
||||
return match ($this) {
|
||||
self::Email => ['https://mail.google.com/'],
|
||||
self::Drive => ['https://www.googleapis.com/auth/drive'],
|
||||
self::Calendar => ['https://www.googleapis.com/auth/calendar'],
|
||||
};
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Email => 'Gmail',
|
||||
self::Drive => 'Google Drive',
|
||||
self::Calendar => 'Google Calendar',
|
||||
};
|
||||
}
|
||||
|
||||
public function icon(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Email => 'fas fa-envelope',
|
||||
self::Drive => 'fab fa-google-drive',
|
||||
self::Calendar => 'fas fa-calendar-alt',
|
||||
};
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Email => 'Invia e ricevi email tramite Gmail API (alternativa a SMTP/IMAP con password)',
|
||||
self::Drive => 'Accedi ai file su Google Drive per documenti e allegati',
|
||||
self::Calendar => 'Sincronizza eventi con Google Calendar',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -34,8 +34,9 @@ class BackupController extends Controller
|
||||
$result = $this->backupService->run();
|
||||
|
||||
if ($result['success']) {
|
||||
$steps = !empty($result['steps']) ? ' — ' . implode(', ', $result['steps']) : '';
|
||||
return redirect()->route('admin.backup.index')
|
||||
->with('success', 'Backup completato: ' . ($result['filename'] ?? '') . ' (' . ($result['size_formatted'] ?? '') . ')');
|
||||
->with('success', 'Backup completato: ' . ($result['filename'] ?? '') . ' (' . ($result['size_formatted'] ?? '') . ')' . $steps);
|
||||
}
|
||||
|
||||
return redirect()->route('admin.backup.index')
|
||||
|
||||
@@ -2,15 +2,20 @@
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Enums\GoogleService;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\EmailAttachment;
|
||||
use App\Models\EmailFolder;
|
||||
use App\Models\EmailSetting;
|
||||
use App\Models\Firma;
|
||||
use App\Models\SenderAccount;
|
||||
use App\Services\GoogleOAuthService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\Email;
|
||||
|
||||
class EmailSettingsController extends Controller
|
||||
{
|
||||
@@ -19,7 +24,9 @@ class EmailSettingsController extends Controller
|
||||
$this->authorizeWrite('settings');
|
||||
$settings = EmailSetting::first() ?? new EmailSetting();
|
||||
$senderAccounts = SenderAccount::orderBy('email_address')->get();
|
||||
return view('admin.email-settings.index', compact('settings', 'senderAccounts'));
|
||||
$googleStatus = app(GoogleOAuthService::class)->getConnectionStatus();
|
||||
$firme = $settings->exists ? $settings->firme : collect();
|
||||
return view('admin.email-settings.index', compact('settings', 'senderAccounts', 'googleStatus', 'firme'));
|
||||
}
|
||||
|
||||
public function save(Request $request)
|
||||
@@ -38,11 +45,15 @@ class EmailSettingsController extends Controller
|
||||
'smtp_password' => 'nullable|string',
|
||||
'email_address' => 'required|email',
|
||||
'email_name' => 'nullable|string|max:255',
|
||||
'from_email' => 'nullable|email|max:255',
|
||||
'from_name' => 'nullable|string|max:255',
|
||||
'reply_to' => 'nullable|email',
|
||||
'sync_interval_minutes' => 'nullable|integer|min:1|max:60',
|
||||
'is_active' => 'boolean',
|
||||
'signature' => 'nullable|string',
|
||||
'signature_enabled' => 'boolean',
|
||||
'auth_method' => 'nullable|in:password,oauth',
|
||||
'google_oauth_connection_id' => 'nullable|integer|exists:google_oauth_connections,id',
|
||||
]);
|
||||
|
||||
if (!empty($validated['imap_password'])) {
|
||||
@@ -61,29 +72,17 @@ class EmailSettingsController extends Controller
|
||||
$validated
|
||||
);
|
||||
|
||||
// Verifica che i dati siano stati salvati
|
||||
$verify = EmailSetting::find($saved->id);
|
||||
if ($verify &&
|
||||
|
||||
$allGood = $verify &&
|
||||
$verify->imap_host === $validated['imap_host'] &&
|
||||
$verify->email_address === $validated['email_address']) {
|
||||
$verify->email_address === $validated['email_address'];
|
||||
|
||||
$signatureMatch = !array_key_exists('signature', $validated) || $verify->signature === $validated['signature'];
|
||||
$signatureEnabledMatch = !array_key_exists('signature_enabled', $validated) || (bool)$verify->signature_enabled === (bool)$validated['signature_enabled'];
|
||||
|
||||
if (!$signatureMatch) {
|
||||
\Illuminate\Support\Facades\Log::warning('Signature mismatch after save', [
|
||||
'expected' => $validated['signature'] ?? null,
|
||||
'actual' => $verify->signature,
|
||||
]);
|
||||
}
|
||||
|
||||
if (!$signatureEnabledMatch) {
|
||||
\Illuminate\Support\Facades\Log::warning('Signature enabled mismatch after save', [
|
||||
'expected' => $validated['signature_enabled'] ?? null,
|
||||
'actual' => $verify->signature_enabled,
|
||||
]);
|
||||
}
|
||||
$allGood = $allGood &&
|
||||
(!array_key_exists('signature', $validated) || $verify->signature === $validated['signature']) &&
|
||||
(!array_key_exists('signature_enabled', $validated) || (bool)$verify->signature_enabled === (bool)$validated['signature_enabled']);
|
||||
|
||||
if ($allGood) {
|
||||
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.');
|
||||
@@ -99,7 +98,6 @@ class EmailSettingsController extends Controller
|
||||
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.']);
|
||||
}
|
||||
@@ -129,60 +127,27 @@ class EmailSettingsController extends Controller
|
||||
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);
|
||||
$mailer = $settings->getSmtpMailer();
|
||||
|
||||
if ($mailer === null) {
|
||||
return response()->json(['success' => false, 'message' => 'Impossibile creare il mailer SMTP. Verifica la configurazione.']);
|
||||
}
|
||||
|
||||
$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
|
||||
);
|
||||
$smtpUser = $settings->smtp_username ?: $settings->email_address;
|
||||
$fromName = $settings->getEffectiveFromName();
|
||||
$replyToEmail = $settings->from_email ?: $smtpUser;
|
||||
|
||||
$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)
|
||||
$email = (new Email())
|
||||
->from(Address::create($smtpUser, $fromName))
|
||||
->to($request->email)
|
||||
->subject('Test Configurazione SMTP - Glastree')
|
||||
->text('Test email da Glastree - Configurazione SMTP funziona!');
|
||||
|
||||
|
||||
if ($replyToEmail !== $smtpUser) {
|
||||
$email->replyTo(Address::create($replyToEmail, $fromName));
|
||||
}
|
||||
|
||||
$mailer->send($email);
|
||||
|
||||
\Illuminate\Support\Facades\Log::info('Test SMTP ok', ['to' => $request->email, 'from' => $settings->email_address]);
|
||||
@@ -227,6 +192,8 @@ class EmailSettingsController extends Controller
|
||||
'verify_email' => 'nullable|email|max:255',
|
||||
'note' => 'nullable|string|max:500',
|
||||
'is_active' => 'boolean',
|
||||
'auth_method' => 'nullable|in:password,oauth',
|
||||
'google_oauth_connection_id' => 'nullable|integer|exists:google_oauth_connections,id',
|
||||
]);
|
||||
|
||||
if (!empty($data['smtp_password'])) {
|
||||
@@ -257,6 +224,8 @@ class EmailSettingsController extends Controller
|
||||
'verify_email' => 'nullable|email|max:255',
|
||||
'note' => 'nullable|string|max:500',
|
||||
'is_active' => 'boolean',
|
||||
'auth_method' => 'nullable|in:password,oauth',
|
||||
'google_oauth_connection_id' => 'nullable|integer|exists:google_oauth_connections,id',
|
||||
]);
|
||||
|
||||
if (!empty($data['smtp_password'])) {
|
||||
@@ -326,4 +295,66 @@ class EmailSettingsController extends Controller
|
||||
return response()->json(['success' => false, 'message' => 'Errore invio: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function firmaStore(Request $request)
|
||||
{
|
||||
$this->authorizeWrite('settings');
|
||||
$data = $request->validate([
|
||||
'nome' => 'required|string|max:255',
|
||||
'contenuto_html' => 'nullable|string',
|
||||
'is_default' => 'boolean',
|
||||
]);
|
||||
|
||||
$settings = EmailSetting::getActive();
|
||||
if (!$settings) {
|
||||
return redirect('/impostazioni/email')->with('error', 'Account email non configurato.');
|
||||
}
|
||||
|
||||
$firma = $settings->firme()->create($data);
|
||||
|
||||
if ($data['is_default'] ?? false) {
|
||||
$settings->firme()->where('id', '!=', $firma->id)->update(['is_default' => false]);
|
||||
}
|
||||
|
||||
return redirect('/impostazioni/email#firme')->with('success', 'Firma "' . $firma->nome . '" creata.');
|
||||
}
|
||||
|
||||
public function firmaUpdate(Request $request, int $id)
|
||||
{
|
||||
$this->authorizeWrite('settings');
|
||||
$data = $request->validate([
|
||||
'nome' => 'required|string|max:255',
|
||||
'contenuto_html' => 'nullable|string',
|
||||
'is_default' => 'boolean',
|
||||
]);
|
||||
|
||||
$firma = Firma::findOrFail($id);
|
||||
$firma->update($data);
|
||||
|
||||
if ($data['is_default'] ?? false) {
|
||||
Firma::where('email_setting_id', $firma->email_setting_id)
|
||||
->where('id', '!=', $firma->id)
|
||||
->update(['is_default' => false]);
|
||||
}
|
||||
|
||||
return redirect('/impostazioni/email#firme')->with('success', 'Firma "' . $firma->nome . '" aggiornata.');
|
||||
}
|
||||
|
||||
public function firmaSetDefault(int $id)
|
||||
{
|
||||
$this->authorizeWrite('settings');
|
||||
$firma = Firma::findOrFail($id);
|
||||
Firma::where('email_setting_id', $firma->email_setting_id)->update(['is_default' => false]);
|
||||
$firma->update(['is_default' => true]);
|
||||
return redirect('/impostazioni/email#firme')->with('success', 'Firma "' . $firma->nome . '" impostata come predefinita.');
|
||||
}
|
||||
|
||||
public function firmaDestroy(int $id)
|
||||
{
|
||||
$this->authorizeWrite('settings');
|
||||
$firma = Firma::findOrFail($id);
|
||||
$nome = $firma->nome;
|
||||
$firma->delete();
|
||||
return redirect('/impostazioni/email#firme')->with('success', 'Firma "' . $nome . '" eliminata.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ class CalendarioConnessioneController extends Controller
|
||||
$connessione->is_active = $request->boolean('is_active', true);
|
||||
|
||||
if (!empty($data['config'])) {
|
||||
$connessione->encryptAndSetConfig($data['config']);
|
||||
$connessione->encryptAndSetConfig($data['config'], $connessione->config ?? []);
|
||||
}
|
||||
|
||||
$connessione->save();
|
||||
|
||||
@@ -56,7 +56,7 @@ class DocumentoController extends Controller
|
||||
} elseif ($folderId) {
|
||||
$query->where('cartella_id', $folderId);
|
||||
} else {
|
||||
$query->whereNull('repository_id');
|
||||
$query->whereNull('repository_id')->whereNull('cartella_id');
|
||||
}
|
||||
|
||||
$documenti = $query->paginate(20);
|
||||
@@ -201,33 +201,43 @@ class DocumentoController extends Controller
|
||||
$tipologieValidi = TipologiaDocumento::opzioni();
|
||||
$tipologieRule = 'in:' . implode(',', $tipologieValidi);
|
||||
|
||||
$data = $request->validate([
|
||||
$rules = [
|
||||
'nome_file' => 'required|string|max:255',
|
||||
'tipologia' => 'required|' . $tipologieRule,
|
||||
'visibilita' => 'nullable|string',
|
||||
'contesto_tipo' => 'nullable|in:individuo,gruppo,evento,mailing',
|
||||
'visibilita_target_id' => 'nullable|integer',
|
||||
'visibilita_target_type' => 'nullable|string',
|
||||
'note' => 'nullable|string',
|
||||
'tags' => 'nullable|array',
|
||||
'tags.*' => 'exists:tags,id',
|
||||
]);
|
||||
];
|
||||
|
||||
$data['visibilita'] = $request->contesto_tipo ?: 'pubblico';
|
||||
if ($documento->isLink()) {
|
||||
$rules['url'] = 'nullable|url|max:2048';
|
||||
}
|
||||
|
||||
if ($request->contesto_tipo) {
|
||||
$data = $request->validate($rules);
|
||||
|
||||
$contestoTipo = $data['contesto_tipo'] ?? null;
|
||||
$data['visibilita'] = $contestoTipo ?: 'pubblico';
|
||||
|
||||
if ($contestoTipo) {
|
||||
$typeMap = [
|
||||
'individuo' => Individuo::class,
|
||||
'gruppo' => Gruppo::class,
|
||||
'evento' => Evento::class,
|
||||
'mailing' => \App\Models\MailingList::class,
|
||||
];
|
||||
$data['visibilita_target_type'] = $typeMap[$request->contesto_tipo] ?? null;
|
||||
$data['visibilita_target_type'] = $typeMap[$contestoTipo] ?? null;
|
||||
} else {
|
||||
$data['visibilita'] = 'pubblico';
|
||||
$data['visibilita_target_id'] = null;
|
||||
$data['visibilita_target_type'] = null;
|
||||
}
|
||||
|
||||
if ($documento->isLink()) {
|
||||
$data['url'] = $data['url'] ?? $documento->url;
|
||||
}
|
||||
|
||||
$documento->update($data);
|
||||
|
||||
if ($request->has('tags')) {
|
||||
@@ -236,22 +246,69 @@ class DocumentoController extends Controller
|
||||
$documento->tags()->detach();
|
||||
}
|
||||
|
||||
return redirect('/documenti')->with('success', 'Documento aggiornato.');
|
||||
$redirect = $request->input('folder_id') ? '/documenti?folder_id=' . $request->input('folder_id') : '/documenti';
|
||||
return redirect($redirect)->with('success', 'Documento aggiornato.');
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->authorizeWrite('documenti');
|
||||
$data = $request->validate([
|
||||
$tipologieValidi = TipologiaDocumento::opzioni();
|
||||
$tipologieRule = 'in:' . implode(',', $tipologieValidi);
|
||||
|
||||
$isLink = $request->input('tipo_caricamento') === 'link';
|
||||
|
||||
$rules = [
|
||||
'nome_file' => 'required|string|max:255',
|
||||
'tipologia' => 'required|in:avatar,galleria,documento,statuto,altro',
|
||||
'file' => 'required|file|max:10240',
|
||||
'tipologia' => 'required|' . $tipologieRule,
|
||||
'visibilita' => 'required|in:pubblico,individuo,gruppo',
|
||||
'visibilita_target_id' => 'nullable|integer',
|
||||
'visibilita_target_type' => 'nullable|string',
|
||||
'cartella_id' => 'nullable|integer|exists:documenti_cartelle,id',
|
||||
'repository_id' => 'nullable|integer|exists:storage_repositories,id',
|
||||
]);
|
||||
'tags' => 'nullable|array',
|
||||
'tags.*' => 'exists:tags,id',
|
||||
'tipo_caricamento' => 'required|in:file,link',
|
||||
'url' => 'required_if:tipo_caricamento,link|nullable|url|max:2048',
|
||||
];
|
||||
|
||||
if (!$isLink) {
|
||||
$rules['file'] = 'required|file|max:10240';
|
||||
}
|
||||
|
||||
$data = $request->validate($rules);
|
||||
|
||||
if ($isLink) {
|
||||
$linkVisibilitaTargetType = null;
|
||||
if ($data['visibilita'] === 'individuo' && $data['visibilita_target_id']) {
|
||||
$linkVisibilitaTargetType = Individuo::class;
|
||||
} elseif ($data['visibilita'] === 'gruppo' && $data['visibilita_target_id']) {
|
||||
$linkVisibilitaTargetType = Gruppo::class;
|
||||
}
|
||||
|
||||
$doc = Documento::create([
|
||||
'nome_file' => $data['nome_file'],
|
||||
'url' => $data['url'],
|
||||
'tipo' => 'link',
|
||||
'tipologia' => $data['tipologia'],
|
||||
'visibilita' => $data['visibilita'],
|
||||
'visibilita_target_id' => $data['visibilita_target_id'] ?? null,
|
||||
'visibilita_target_type' => $linkVisibilitaTargetType,
|
||||
'user_id' => auth()->id(),
|
||||
'cartella_id' => $data['cartella_id'] ?? null,
|
||||
'repository_id' => null,
|
||||
]);
|
||||
|
||||
if ($doc && $request->has('tags')) {
|
||||
$doc->tags()->sync($request->tags);
|
||||
}
|
||||
|
||||
$redirect = $request->_redirect ?? url()->previous();
|
||||
if ($request->filled('folder_id')) {
|
||||
$redirect = '/documenti?folder_id=' . $request->input('folder_id');
|
||||
}
|
||||
return redirect($redirect)->with('success', 'Documento (link) creato.');
|
||||
}
|
||||
|
||||
$repositoryId = $data['repository_id'] ?? null;
|
||||
|
||||
@@ -284,7 +341,7 @@ class DocumentoController extends Controller
|
||||
$visibilitaTargetType = Gruppo::class;
|
||||
}
|
||||
|
||||
Documento::create([
|
||||
$doc = Documento::create([
|
||||
'nome_file' => $data['nome_file'],
|
||||
'file_path' => $filePath,
|
||||
'storage_disk' => $disk ?? null,
|
||||
@@ -300,7 +357,13 @@ class DocumentoController extends Controller
|
||||
'repository_id' => $repositoryId,
|
||||
]);
|
||||
|
||||
if ($doc && $request->has('tags')) {
|
||||
$doc->tags()->sync($request->tags);
|
||||
}
|
||||
$redirect = $request->_redirect ?? url()->previous();
|
||||
if ($request->filled('folder_id')) {
|
||||
$redirect = '/documenti?folder_id=' . $request->input('folder_id');
|
||||
}
|
||||
return redirect($redirect)->with('success', 'Documento caricato.');
|
||||
}
|
||||
|
||||
@@ -309,6 +372,10 @@ class DocumentoController extends Controller
|
||||
$this->authorizeRead('documenti');
|
||||
$documento = Documento::findOrFail($documento);
|
||||
|
||||
if ($documento->isLink()) {
|
||||
return redirect()->away($documento->url);
|
||||
}
|
||||
|
||||
$extension = pathinfo($documento->file_path, PATHINFO_EXTENSION);
|
||||
$filename = $extension ? $documento->nome_file . '.' . $extension : $documento->nome_file;
|
||||
|
||||
@@ -354,6 +421,12 @@ class DocumentoController extends Controller
|
||||
$this->authorizeRead('documenti');
|
||||
$documento = Documento::findOrFail($documento);
|
||||
|
||||
if ($documento->isLink()) {
|
||||
return response()->view('documenti.preview-fallback', [
|
||||
'documento' => $documento,
|
||||
])->header('X-Preview-Fallback', 'true');
|
||||
}
|
||||
|
||||
$previewableMimeTypes = [
|
||||
'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
|
||||
'application/pdf',
|
||||
@@ -404,7 +477,8 @@ class DocumentoController extends Controller
|
||||
$this->authorizeDelete('documenti');
|
||||
$documento = Documento::findOrFail($documento);
|
||||
|
||||
if ($documento->repository_id) {
|
||||
if (!$documento->isLink()) {
|
||||
if ($documento->repository_id) {
|
||||
$repo = StorageRepository::find($documento->repository_id);
|
||||
if ($repo) {
|
||||
$filesystem = $this->repoService->buildFilesystem($repo);
|
||||
@@ -412,10 +486,11 @@ class DocumentoController extends Controller
|
||||
$filesystem->delete($documento->file_path);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$disk = $this->resolveStorageDisk($documento);
|
||||
if (Storage::disk($disk)->exists($documento->file_path)) {
|
||||
Storage::disk($disk)->delete($documento->file_path);
|
||||
} else {
|
||||
$disk = $this->resolveStorageDisk($documento);
|
||||
if (Storage::disk($disk)->exists($documento->file_path)) {
|
||||
Storage::disk($disk)->delete($documento->file_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,9 +569,10 @@ class DocumentoController extends Controller
|
||||
{
|
||||
$idsInput = $request->input('ids', '');
|
||||
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
|
||||
$ids = array_values(array_filter($ids, fn($v) => is_numeric($v)));
|
||||
|
||||
if (empty($ids)) {
|
||||
return response()->json(['linked_count' => 0, 'links' => []]);
|
||||
return back()->with('error', 'Nessun documento selezionato.');
|
||||
}
|
||||
|
||||
$documenti = Documento::whereIn('id', $ids)->get();
|
||||
@@ -572,6 +648,7 @@ class DocumentoController extends Controller
|
||||
$this->authorizeRead('documenti');
|
||||
$idsInput = $request->input('ids', '');
|
||||
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
|
||||
$ids = array_values(array_filter($ids, fn($v) => is_numeric($v)));
|
||||
|
||||
if (empty($ids)) {
|
||||
return back()->with('error', 'Nessun documento selezionato.');
|
||||
@@ -590,8 +667,14 @@ class DocumentoController extends Controller
|
||||
return back()->with('error', 'Impossibile creare l\'archivio ZIP.');
|
||||
}
|
||||
|
||||
$skippedLinks = 0;
|
||||
$addedCount = 0;
|
||||
foreach ($documenti as $documento) {
|
||||
if ($documento->isLink()) {
|
||||
$skippedLinks++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$extension = pathinfo($documento->file_path, PATHINFO_EXTENSION);
|
||||
$filename = $extension ? $documento->nome_file . '.' . $extension : $documento->nome_file;
|
||||
|
||||
@@ -624,11 +707,23 @@ class DocumentoController extends Controller
|
||||
$zip->close();
|
||||
|
||||
if ($addedCount === 0) {
|
||||
unlink($zipPath);
|
||||
return back()->with('error', 'Nessun file disponibile per il download.');
|
||||
if (file_exists($zipPath)) {
|
||||
@unlink($zipPath);
|
||||
}
|
||||
$message = 'Nessun file disponibile per il download.';
|
||||
if ($skippedLinks > 0) {
|
||||
$message .= " ({$skippedLinks} link esterni saltati)";
|
||||
}
|
||||
return back()->with('error', $message);
|
||||
}
|
||||
|
||||
return response()->download($zipPath, 'documenti.zip')->deleteFileAfterSend(true);
|
||||
$message = $addedCount . ' file scaricati.';
|
||||
if ($skippedLinks > 0) {
|
||||
$message .= " {$skippedLinks} link esterni saltati.";
|
||||
}
|
||||
|
||||
return response()->download($zipPath, 'documenti.zip')->deleteFileAfterSend(true)
|
||||
->with('success', $message);
|
||||
}
|
||||
|
||||
public function massDestroy(Request $request)
|
||||
@@ -636,6 +731,7 @@ class DocumentoController extends Controller
|
||||
$this->authorizeDelete('documenti');
|
||||
$idsInput = $request->input('ids', '');
|
||||
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
|
||||
$ids = array_values(array_filter($ids, fn($v) => is_numeric($v)));
|
||||
|
||||
if (empty($ids)) {
|
||||
return back()->with('error', 'Nessun documento selezionato.');
|
||||
@@ -644,18 +740,20 @@ class DocumentoController extends Controller
|
||||
$documenti = Documento::whereIn('id', $ids)->get();
|
||||
|
||||
foreach ($documenti as $documento) {
|
||||
if ($documento->repository_id) {
|
||||
$repo = StorageRepository::find($documento->repository_id);
|
||||
if ($repo) {
|
||||
$filesystem = $this->repoService->buildFilesystem($repo);
|
||||
if ($filesystem && $filesystem->fileExists($documento->file_path)) {
|
||||
$filesystem->delete($documento->file_path);
|
||||
if (!$documento->isLink()) {
|
||||
if ($documento->repository_id) {
|
||||
$repo = StorageRepository::find($documento->repository_id);
|
||||
if ($repo) {
|
||||
$filesystem = $this->repoService->buildFilesystem($repo);
|
||||
if ($filesystem && $filesystem->fileExists($documento->file_path)) {
|
||||
$filesystem->delete($documento->file_path);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$disk = $this->resolveStorageDisk($documento);
|
||||
if (Storage::disk($disk)->exists($documento->file_path)) {
|
||||
Storage::disk($disk)->delete($documento->file_path);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$disk = $this->resolveStorageDisk($documento);
|
||||
if (Storage::disk($disk)->exists($documento->file_path)) {
|
||||
Storage::disk($disk)->delete($documento->file_path);
|
||||
}
|
||||
}
|
||||
$documento->delete();
|
||||
@@ -731,6 +829,7 @@ class DocumentoController extends Controller
|
||||
$this->authorizeWrite('documenti');
|
||||
$idsInput = $request->input('ids', '');
|
||||
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
|
||||
$ids = array_values(array_filter($ids, fn($v) => is_numeric($v)));
|
||||
|
||||
if (empty($ids)) {
|
||||
return back()->with('error', 'Nessun documento selezionato.');
|
||||
@@ -762,6 +861,7 @@ class DocumentoController extends Controller
|
||||
$this->authorizeWrite('documenti');
|
||||
$idsInput = $request->input('ids', '');
|
||||
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
|
||||
$ids = array_values(array_filter($ids, fn($v) => is_numeric($v)));
|
||||
|
||||
if (empty($ids)) {
|
||||
return back()->with('error', 'Nessun documento selezionato.');
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\Models\MailingList;
|
||||
use App\Models\Gruppo;
|
||||
use App\Models\Individuo;
|
||||
use App\Models\Documento;
|
||||
use App\Models\Firma;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
@@ -96,6 +97,9 @@ class EmailController extends Controller
|
||||
|
||||
$documenti = Documento::orderBy('nome_file')->get();
|
||||
|
||||
$settings = EmailSetting::getActive();
|
||||
$firme = $settings?->firme ?? collect();
|
||||
|
||||
$prefill = [];
|
||||
if ($request->reply_to) {
|
||||
$original = EmailMessage::find($request->reply_to);
|
||||
@@ -113,7 +117,7 @@ class EmailController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
return view('email.compose', compact('folders', 'mailingLists', 'gruppi', 'individui', 'documenti', 'prefill', 'senderAccounts'));
|
||||
return view('email.compose', compact('folders', 'mailingLists', 'gruppi', 'individui', 'documenti', 'prefill', 'senderAccounts', 'firme'));
|
||||
}
|
||||
|
||||
public function send(Request $request)
|
||||
@@ -128,6 +132,7 @@ class EmailController extends Controller
|
||||
$rules['to'] = 'required';
|
||||
}
|
||||
|
||||
$rules['firma_id'] = 'nullable|integer|exists:firme,id';
|
||||
$validated = $request->validate($rules);
|
||||
|
||||
$recipients = $this->resolveRecipients($request);
|
||||
@@ -157,12 +162,18 @@ class EmailController extends Controller
|
||||
|
||||
$imapSuccess = false;
|
||||
$imapError = null;
|
||||
$attachments = $this->processAttachments($request);
|
||||
$attachmentResult = $this->processAttachments($request);
|
||||
$attachmentPaths = $attachmentResult['paths'];
|
||||
$linkDocs = $attachmentResult['links'];
|
||||
|
||||
$firmaId = $request->firma_id;
|
||||
|
||||
$body = $this->appendLinksToBody($validated['body'], $linkDocs);
|
||||
|
||||
foreach ($recipients as $recipient) {
|
||||
try {
|
||||
$bodyWithSig = $this->appendSignature($validated['body'], $settings->getSignature(), $settings->signature_enabled ?? false);
|
||||
$this->sendViaImap($settings, $recipient, $validated['subject'], $bodyWithSig, $request->cc, $attachments);
|
||||
$bodyWithSig = $this->appendSignature($body, $firmaId, $settings);
|
||||
$this->sendViaImap($settings, $recipient, $validated['subject'], $bodyWithSig, $request->cc, $attachmentPaths);
|
||||
$imapSuccess = true;
|
||||
} catch (\Exception $e) {
|
||||
$imapError = $e->getMessage();
|
||||
@@ -171,7 +182,7 @@ class EmailController extends Controller
|
||||
}
|
||||
|
||||
try {
|
||||
$bodyWithSig = $this->appendSignature($validated['body'], $settings->getSignature(), $settings->signature_enabled ?? false);
|
||||
$bodyWithSig = $this->appendSignature($body, $firmaId, $settings);
|
||||
$this->storeSentMessage($settings, $recipients, $validated['subject'], $bodyWithSig, $request->cc);
|
||||
} catch (\Exception $e) {
|
||||
\Illuminate\Support\Facades\Log::error('Store sent message error', ['error' => $e->getMessage()]);
|
||||
@@ -188,13 +199,20 @@ class EmailController extends Controller
|
||||
private function sendWithSender(Request $request, array $validated, array $recipients)
|
||||
{
|
||||
$sender = SenderAccount::findOrFail($request->mittente_id);
|
||||
$attachmentPaths = $this->resolveAttachmentPaths($request);
|
||||
$attachmentResult = $this->resolveAttachmentPaths($request);
|
||||
$attachmentPaths = $attachmentResult['paths'];
|
||||
$linkDocs = $attachmentResult['links'];
|
||||
|
||||
$settings = EmailSetting::getActive();
|
||||
$body = $this->appendLinksToBody($validated['body'], $linkDocs);
|
||||
$body = $this->appendSignature($body, $request->firma_id, $settings);
|
||||
|
||||
$ok = 0;
|
||||
$errors = [];
|
||||
|
||||
foreach ($recipients as $recipient) {
|
||||
try {
|
||||
$sender->sendEmail($recipient, $validated['subject'], $validated['body'], $attachmentPaths);
|
||||
$sender->sendEmail($recipient, $validated['subject'], $body, $attachmentPaths);
|
||||
$ok++;
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = $recipient . ': ' . $e->getMessage();
|
||||
@@ -237,8 +255,8 @@ class EmailController extends Controller
|
||||
'body_text' => $request->body,
|
||||
'to_email' => $request->to,
|
||||
'is_draft' => true,
|
||||
'from_email' => $settings->email_address ?? '',
|
||||
'from_name' => $settings->email_name ?? '',
|
||||
'from_email' => $settings->getEffectiveFromAddress(),
|
||||
'from_name' => $settings->getEffectiveFromName(),
|
||||
]);
|
||||
|
||||
return response()->json(['success' => true, 'draft_id' => $message->id]);
|
||||
@@ -496,12 +514,20 @@ class EmailController extends Controller
|
||||
return array_filter(array_unique($recipients));
|
||||
}
|
||||
|
||||
private function appendSignature(string $body, ?string $signature, bool $enabled): string
|
||||
private function appendSignature(string $body, int|string|null $firmaId = null, ?EmailSetting $settings = null): string
|
||||
{
|
||||
if (empty($signature) || !$enabled) {
|
||||
return $body;
|
||||
if ($firmaId) {
|
||||
$firma = Firma::find($firmaId);
|
||||
if ($firma && $firma->contenuto_html) {
|
||||
return $body . "\n\n---\n" . $firma->contenuto_html;
|
||||
}
|
||||
}
|
||||
return $body . "\n\n---\n" . $signature;
|
||||
|
||||
if ($settings && $settings->signature && ($settings->signature_enabled ?? false)) {
|
||||
return $body . "\n\n---\n" . $settings->signature;
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
private function sendViaImap($settings, $to, $subject, $body, $cc = null, array $attachmentPaths = [])
|
||||
@@ -526,26 +552,23 @@ class EmailController extends Controller
|
||||
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);
|
||||
$fromAddress = \Symfony\Component\Mime\Address::create(
|
||||
$settings->getEffectiveFromAddress(),
|
||||
$settings->getEffectiveFromName()
|
||||
);
|
||||
|
||||
$email = (new \Symfony\Component\Mime\Email())
|
||||
->from($fromAddress)
|
||||
->to($to)
|
||||
->subject($subject)
|
||||
->text($body);
|
||||
|
||||
|
||||
$replyTo = $settings->getEffectiveReplyTo();
|
||||
if ($replyTo) {
|
||||
$email->replyTo($replyTo);
|
||||
}
|
||||
|
||||
if ($cc) {
|
||||
$email->cc($cc);
|
||||
}
|
||||
@@ -567,6 +590,7 @@ class EmailController extends Controller
|
||||
private function processAttachments(Request $request): array
|
||||
{
|
||||
$paths = [];
|
||||
$links = [];
|
||||
|
||||
if ($request->hasFile('allegati')) {
|
||||
foreach ($request->file('allegati') as $file) {
|
||||
@@ -581,18 +605,21 @@ class EmailController extends Controller
|
||||
$ids = is_array($documentIds) ? $documentIds : explode(',', $documentIds);
|
||||
$docs = Documento::whereIn('id', $ids)->get();
|
||||
foreach ($docs as $doc) {
|
||||
if ($doc->file_path && Storage::exists($doc->file_path)) {
|
||||
if ($doc->isLink()) {
|
||||
$links[] = ['nome' => $doc->nome_file, 'url' => $doc->url];
|
||||
} elseif ($doc->file_path && Storage::exists($doc->file_path)) {
|
||||
$paths[] = Storage::path($doc->file_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $paths;
|
||||
return ['paths' => $paths, 'links' => $links];
|
||||
}
|
||||
|
||||
private function resolveAttachmentPaths(Request $request): array
|
||||
{
|
||||
$paths = [];
|
||||
$links = [];
|
||||
|
||||
if ($request->hasFile('allegati')) {
|
||||
foreach ($request->file('allegati') as $file) {
|
||||
@@ -607,26 +634,42 @@ class EmailController extends Controller
|
||||
$ids = is_array($documentIds) ? $documentIds : explode(',', $documentIds);
|
||||
$docs = Documento::whereIn('id', $ids)->get();
|
||||
foreach ($docs as $doc) {
|
||||
if ($doc->file_path && \Illuminate\Support\Facades\Storage::exists($doc->file_path)) {
|
||||
if ($doc->isLink()) {
|
||||
$links[] = ['nome' => $doc->nome_file, 'url' => $doc->url];
|
||||
} elseif ($doc->file_path && \Illuminate\Support\Facades\Storage::exists($doc->file_path)) {
|
||||
$paths[] = \Illuminate\Support\Facades\Storage::path($doc->file_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $paths;
|
||||
return ['paths' => $paths, 'links' => $links];
|
||||
}
|
||||
|
||||
private function appendLinksToBody(string $body, array $linkDocs): string
|
||||
{
|
||||
if (empty($linkDocs)) {
|
||||
return $body;
|
||||
}
|
||||
|
||||
$linksText = "\n\n---\nDocumenti collegati:\n";
|
||||
foreach ($linkDocs as $link) {
|
||||
$linksText .= "- {$link['nome']}: {$link['url']}\n";
|
||||
}
|
||||
|
||||
return $body . $linksText;
|
||||
}
|
||||
|
||||
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);
|
||||
$bodyWithSignature = $body;
|
||||
|
||||
$message = EmailMessage::create([
|
||||
'email_folder_id' => $sentFolder->id,
|
||||
'subject' => $subject,
|
||||
'body_text' => $bodyWithSignature,
|
||||
'from_email' => $settings->email_address,
|
||||
'from_name' => $settings->email_name,
|
||||
'from_email' => $settings->getEffectiveFromAddress(),
|
||||
'from_name' => $settings->getEffectiveFromName(),
|
||||
'to_email' => implode(', ', $recipients),
|
||||
'cc' => $cc,
|
||||
'is_sent' => true,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Evento;
|
||||
@@ -630,4 +632,39 @@ class EventoController extends Controller
|
||||
|
||||
return redirect('/eventi')->with('success', $message);
|
||||
}
|
||||
|
||||
public function massTag(Request $request)
|
||||
{
|
||||
$this->authorizeWrite('eventi');
|
||||
$idsInput = $request->input('ids', '');
|
||||
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
|
||||
|
||||
if (empty($ids)) {
|
||||
return back()->with('error', 'Nessun evento selezionato.');
|
||||
}
|
||||
|
||||
$data = $request->validate([
|
||||
'tags' => 'required|array',
|
||||
'tags.*' => 'exists:tags,id',
|
||||
'mode' => 'required|in:assign,remove',
|
||||
]);
|
||||
|
||||
$tagIds = $data['tags'];
|
||||
$mode = $data['mode'];
|
||||
$count = 0;
|
||||
|
||||
Evento::whereIn('id', $ids)->chunk(100, function ($eventi) use ($tagIds, $mode, &$count) {
|
||||
foreach ($eventi as $evento) {
|
||||
if ($mode === 'assign') {
|
||||
$evento->tags()->syncWithoutDetaching($tagIds);
|
||||
} else {
|
||||
$evento->tags()->detach($tagIds);
|
||||
}
|
||||
$count++;
|
||||
}
|
||||
});
|
||||
|
||||
$actionLabel = $mode === 'assign' ? 'assegnati' : 'rimossi';
|
||||
return back()->with('success', "Tag $actionLabel per $count eventi.");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Evento;
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\GoogleService;
|
||||
use App\Models\GoogleOAuthConnection;
|
||||
use App\Services\GoogleOAuthService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class GoogleOAuthController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly GoogleOAuthService $googleOAuthService,
|
||||
) {}
|
||||
|
||||
public function redirect(string $service): RedirectResponse
|
||||
{
|
||||
try {
|
||||
$serviceEnum = GoogleService::from($service);
|
||||
} catch (\ValueError $e) {
|
||||
return redirect()->back()->withErrors(['error' => 'Servizio Google non valido: ' . $service]);
|
||||
}
|
||||
|
||||
$authUrl = $this->googleOAuthService->getAuthUrl($service);
|
||||
|
||||
return redirect()->away($authUrl);
|
||||
}
|
||||
|
||||
public function callback(Request $request): RedirectResponse
|
||||
{
|
||||
$error = $request->input('error');
|
||||
if ($error) {
|
||||
$message = match ($error) {
|
||||
'access_denied' => 'Accesso negato. Autorizzazione non concessa.',
|
||||
default => 'Errore Google: ' . $error,
|
||||
};
|
||||
return redirect()->route('impostazioni.index', ['tab' => 'google'])
|
||||
->withErrors(['error' => $message]);
|
||||
}
|
||||
|
||||
$code = $request->input('code');
|
||||
if (!$code) {
|
||||
return redirect()->route('impostazioni.index', ['tab' => 'google'])
|
||||
->withErrors(['error' => 'Nessun codice di autorizzazione ricevuto']);
|
||||
}
|
||||
|
||||
$state = $request->input('state', 'email');
|
||||
try {
|
||||
$service = GoogleService::from($state);
|
||||
} catch (\ValueError $e) {
|
||||
$service = GoogleService::Email;
|
||||
}
|
||||
|
||||
try {
|
||||
$connection = $this->googleOAuthService->handleCallback($code, $service);
|
||||
|
||||
$message = match ($service) {
|
||||
GoogleService::Email => 'Account email Gmail connesso con successo!',
|
||||
GoogleService::Drive => 'Account Google Drive connesso con successo!',
|
||||
GoogleService::Calendar => 'Account Google Calendar connesso con successo!',
|
||||
};
|
||||
|
||||
return redirect()->route('impostazioni.index', ['tab' => 'google'])
|
||||
->with('success', $message);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Google OAuth callback error', [
|
||||
'error' => $e->getMessage(),
|
||||
'service' => $service->value,
|
||||
]);
|
||||
|
||||
return redirect()->route('impostazioni.index', ['tab' => 'google'])
|
||||
->withErrors(['error' => 'Errore durante la connessione: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function revoke(GoogleOAuthConnection $connection): RedirectResponse
|
||||
{
|
||||
if ($connection->user_id !== auth()->id()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
try {
|
||||
$service = GoogleService::from($connection->service);
|
||||
|
||||
$message = match ($service) {
|
||||
GoogleService::Email => 'Account email Gmail disconnesso.',
|
||||
GoogleService::Drive => 'Account Google Drive disconnesso.',
|
||||
GoogleService::Calendar => 'Account Google Calendar disconnesso.',
|
||||
};
|
||||
|
||||
$this->googleOAuthService->revoke($connection);
|
||||
|
||||
return redirect()->route('impostazioni.index', ['tab' => 'google'])
|
||||
->with('success', $message);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Google OAuth revoke error', [
|
||||
'error' => $e->getMessage(),
|
||||
'connection_id' => $connection->id,
|
||||
]);
|
||||
|
||||
return redirect()->route('impostazioni.index', ['tab' => 'google'])
|
||||
->withErrors(['error' => 'Errore durante la disconnessione: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function status(): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$status = $this->googleOAuthService->getConnectionStatus();
|
||||
|
||||
return response()->json($status);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ use App\Models\Tag;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Pagination\Paginator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class GruppoController extends Controller
|
||||
@@ -36,6 +37,7 @@ class GruppoController extends Controller
|
||||
|
||||
$allColumnDefs = [
|
||||
['key' => 'nome', 'label' => 'Nome'],
|
||||
['key' => 'anno_fondazione', 'label' => 'Anno Fondazione'],
|
||||
['key' => 'descrizione', 'label' => 'Descrizione'],
|
||||
['key' => 'diocesi', 'label' => 'Diocesi'],
|
||||
['key' => 'livello', 'label' => 'Livello'],
|
||||
@@ -65,7 +67,7 @@ class GruppoController extends Controller
|
||||
$entityType = 'gruppi';
|
||||
$columnWidths = $vista && $vista->colonne_larghezze ? $vista->colonne_larghezze : [];
|
||||
|
||||
$gruppiQuery = Gruppo::with(['diocesi', 'parent', 'children', 'individui', 'avatar', 'tags']);
|
||||
$gruppiQuery = Gruppo::with(['diocesi', 'gruppoContatti', 'parent', 'children', 'individui', 'avatar', 'tags']);
|
||||
|
||||
if (request()->filled('tag')) {
|
||||
$tagSlugs = (array) request()->input('tag');
|
||||
@@ -141,7 +143,7 @@ class GruppoController extends Controller
|
||||
public function create(Request $request)
|
||||
{
|
||||
$this->authorizeWrite('gruppi');
|
||||
$diocesi = Diocesi::orderBy('nome')->get();
|
||||
$diocesi = Diocesi::orderBy('id')->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();
|
||||
@@ -154,9 +156,11 @@ class GruppoController extends Controller
|
||||
$this->authorizeWrite('gruppi');
|
||||
$data = $request->validate([
|
||||
'nome' => 'required|string|max:255',
|
||||
'anno_fondazione' => 'nullable|integer|min:1800|max:' . (date('Y') + 1),
|
||||
'descrizione' => 'nullable|string',
|
||||
'parent_id' => 'nullable|exists:gruppi,id',
|
||||
'diocesi_id' => 'nullable|exists:diocesi,id',
|
||||
'diocesi_ids' => 'nullable|array',
|
||||
'diocesi_ids.*' => 'exists:diocesi,id',
|
||||
'responsabile_ids' => 'nullable|array',
|
||||
'responsabile_ids.*' => 'exists:individui,id',
|
||||
'indirizzo_incontro' => 'nullable|string|max:500',
|
||||
@@ -164,6 +168,11 @@ class GruppoController extends Controller
|
||||
'città_incontro' => 'nullable|string|max:255',
|
||||
'sigla_provincia_incontro' => 'nullable|string|max:2',
|
||||
'mappa_posizione' => 'nullable|string',
|
||||
'contatti' => 'nullable|array',
|
||||
'contatti.*.tipo' => 'required|string|in:email,telefono,cellulare',
|
||||
'contatti.*.valore' => 'required|string|max:255',
|
||||
'contatti.*.etichetta' => 'nullable|string|max:255',
|
||||
'contatti.*.is_primary' => 'nullable|boolean',
|
||||
'tags' => 'nullable|array',
|
||||
'tags.*' => 'exists:tags,id',
|
||||
]);
|
||||
@@ -174,6 +183,21 @@ class GruppoController extends Controller
|
||||
|
||||
$gruppo = Gruppo::create($data);
|
||||
|
||||
if (!empty($data['diocesi_ids'])) {
|
||||
$gruppo->diocesi()->sync($data['diocesi_ids']);
|
||||
}
|
||||
|
||||
if (!empty($data['contatti'])) {
|
||||
foreach ($data['contatti'] as $contattoData) {
|
||||
$gruppo->gruppoContatti()->create([
|
||||
'tipo' => $contattoData['tipo'],
|
||||
'valore' => $contattoData['valore'],
|
||||
'etichetta' => $contattoData['etichetta'] ?? null,
|
||||
'is_primary' => $contattoData['is_primary'] ?? false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->has('tags')) {
|
||||
$gruppo->tags()->sync($request->tags);
|
||||
}
|
||||
@@ -195,7 +219,7 @@ class GruppoController extends Controller
|
||||
public function show($id)
|
||||
{
|
||||
$this->authorizeRead('gruppi');
|
||||
$gruppo = Gruppo::with(['parent', 'children', 'diocesi', 'individui.contatti', 'individui.documenti', 'individui' => function ($q) {
|
||||
$gruppo = Gruppo::with(['parent', 'children', 'diocesi', 'gruppoContatti', 'individui.contatti', 'individui.documenti', 'individui' => function ($q) {
|
||||
$q->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione');
|
||||
}, 'avatar', 'tags', 'eventi' => function ($q) {
|
||||
$q->where('is_incontro_gruppo', true);
|
||||
@@ -206,13 +230,14 @@ class GruppoController extends Controller
|
||||
public function edit($id)
|
||||
{
|
||||
$this->authorizeWrite('gruppi');
|
||||
$gruppo = Gruppo::with(['individui.contatti', 'individui.documenti', 'avatar', 'tags'])->findOrFail($id);
|
||||
$diocesi = Diocesi::orderBy('nome')->get();
|
||||
$gruppo = Gruppo::with(['individui.contatti', 'individui.documenti', 'gruppoContatti', 'avatar', 'tags'])->findOrFail($id);
|
||||
$diocesi = Diocesi::orderBy('id')->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();
|
||||
$tags = Tag::orderBy('name')->get();
|
||||
$selectedTags = $gruppo->tags->pluck('id')->toArray();
|
||||
return view('gruppi.edit', compact('gruppo', 'diocesi', 'gruppi', 'membri', 'tags', 'selectedTags'));
|
||||
$selectedDiocesiIds = $gruppo->diocesi()->pluck('diocesi.id')->toArray();
|
||||
return view('gruppi.edit', compact('gruppo', 'diocesi', 'gruppi', 'membri', 'tags', 'selectedTags', 'selectedDiocesiIds'));
|
||||
}
|
||||
|
||||
public function update(Request $request, $id)
|
||||
@@ -221,9 +246,11 @@ class GruppoController extends Controller
|
||||
$gruppo = Gruppo::findOrFail($id);
|
||||
$data = $request->validate([
|
||||
'nome' => 'required|string|max:255',
|
||||
'anno_fondazione' => 'nullable|integer|min:1800|max:' . (date('Y') + 1),
|
||||
'descrizione' => 'nullable|string',
|
||||
'parent_id' => 'nullable|exists:gruppi,id',
|
||||
'diocesi_id' => 'nullable|exists:diocesi,id',
|
||||
'diocesi_ids' => 'nullable|array',
|
||||
'diocesi_ids.*' => 'exists:diocesi,id',
|
||||
'responsabile_ids' => 'nullable|array',
|
||||
'responsabile_ids.*' => 'exists:individui,id',
|
||||
'indirizzo_incontro' => 'nullable|string|max:500',
|
||||
@@ -231,6 +258,11 @@ class GruppoController extends Controller
|
||||
'città_incontro' => 'nullable|string|max:255',
|
||||
'sigla_provincia_incontro' => 'nullable|string|max:2',
|
||||
'mappa_posizione' => 'nullable|string',
|
||||
'contatti' => 'nullable|array',
|
||||
'contatti.*.tipo' => 'required|string|in:email,telefono,cellulare',
|
||||
'contatti.*.valore' => 'required|string|max:255',
|
||||
'contatti.*.etichetta' => 'nullable|string|max:255',
|
||||
'contatti.*.is_primary' => 'nullable|boolean',
|
||||
'tags' => 'nullable|array',
|
||||
'tags.*' => 'exists:tags,id',
|
||||
]);
|
||||
@@ -243,6 +275,24 @@ class GruppoController extends Controller
|
||||
|
||||
$gruppo->update($data);
|
||||
|
||||
if ($request->has('diocesi_ids')) {
|
||||
$gruppo->diocesi()->sync($data['diocesi_ids'] ?? []);
|
||||
} else {
|
||||
$gruppo->diocesi()->detach();
|
||||
}
|
||||
|
||||
if ($request->has('contatti')) {
|
||||
$gruppo->gruppoContatti()->delete();
|
||||
foreach ($data['contatti'] as $contattoData) {
|
||||
$gruppo->gruppoContatti()->create([
|
||||
'tipo' => $contattoData['tipo'],
|
||||
'valore' => $contattoData['valore'],
|
||||
'etichetta' => $contattoData['etichetta'] ?? null,
|
||||
'is_primary' => $contattoData['is_primary'] ?? false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->has('tags')) {
|
||||
$gruppo->tags()->sync($request->tags);
|
||||
} else {
|
||||
@@ -385,9 +435,12 @@ class GruppoController extends Controller
|
||||
'file' => 'required|file|mimes:csv,txt',
|
||||
]);
|
||||
|
||||
set_time_limit(0);
|
||||
session_write_close();
|
||||
|
||||
$file = $request->file('file');
|
||||
$handle = fopen($file->path(), 'r');
|
||||
$header = fgetcsv($handle, 1000, ',');
|
||||
$header = fgetcsv($handle, 0, ',');
|
||||
|
||||
if ($header === false) {
|
||||
fclose($handle);
|
||||
@@ -400,7 +453,9 @@ class GruppoController extends Controller
|
||||
$errors = [];
|
||||
$rowNumber = 1;
|
||||
|
||||
while (($row = fgetcsv($handle, 1000, ',')) !== false) {
|
||||
DB::beginTransaction();
|
||||
|
||||
while (($row = fgetcsv($handle, 0, ',')) !== false) {
|
||||
$rowNumber++;
|
||||
|
||||
if (count($row) < count($header)) {
|
||||
@@ -426,6 +481,7 @@ class GruppoController extends Controller
|
||||
try {
|
||||
Gruppo::create([
|
||||
'nome' => $nome,
|
||||
'anno_fondazione' => !empty($data['anno_fondazione']) ? (int) $data['anno_fondazione'] : null,
|
||||
'descrizione' => $data['descrizione'] ?? null,
|
||||
'parent_id' => !empty($data['parent_id']) ? (int) $data['parent_id'] : null,
|
||||
'diocesi_id' => !empty($data['diocesi_id']) ? (int) $data['diocesi_id'] : null,
|
||||
@@ -444,6 +500,8 @@ class GruppoController extends Controller
|
||||
|
||||
fclose($handle);
|
||||
|
||||
DB::commit();
|
||||
|
||||
$message = 'Importati ' . $imported . ' gruppi.';
|
||||
if ($skipped > 0) {
|
||||
$message .= ' Saltate ' . $skipped . ' righe (vuote o incomplete).';
|
||||
@@ -458,8 +516,8 @@ class GruppoController extends Controller
|
||||
public function downloadTemplate()
|
||||
{
|
||||
$this->authorizeRead('gruppi');
|
||||
$headers = ['nome', 'descrizione', 'parent_id', 'diocesi_id', 'indirizzo_incontro', 'cap_incontro', 'città_incontro', 'sigla_provincia_incontro'];
|
||||
$example = ['Gruppo Giovani', 'Giovani dai 18 ai 30 anni', '', '', 'Via Roma 10', '00100', 'Roma', 'RM'];
|
||||
$headers = ['nome', 'anno_fondazione', 'descrizione', 'parent_id', 'diocesi_id', 'indirizzo_incontro', 'cap_incontro', 'città_incontro', 'sigla_provincia_incontro'];
|
||||
$example = ['Gruppo Giovani', '2020', 'Giovani dai 18 ai 30 anni', '', '', 'Via Roma 10', '00100', 'Roma', 'RM'];
|
||||
|
||||
$csv = implode(',', $headers) . "\n" . implode(',', $example);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Models\Ruolo;
|
||||
use App\Models\SenderAccount;
|
||||
use App\Models\StorageRepository;
|
||||
use App\Models\Tag;
|
||||
use App\Services\GoogleOAuthService;
|
||||
use App\Models\TipologiaDocumento;
|
||||
use App\Models\TipologiaEvento;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@@ -34,8 +35,9 @@ class ImpostazioniController extends Controller
|
||||
$repositories = StorageRepository::orderBy('ordine')->get();
|
||||
$googleDriveNewToken = session('google_drive_new_token');
|
||||
$calendarioConnessioni = CalendarioConnessione::orderBy('ordine')->get();
|
||||
$googleStatus = app(GoogleOAuthService::class)->getConnectionStatus();
|
||||
|
||||
return view('impostazioni.index', compact('tipologie', 'tipologieEventi', 'ruoli', 'tags', 'appSettings', 'emailSettings', 'senderAccounts', 'repositories', 'googleDriveNewToken', 'calendarioConnessioni'));
|
||||
return view('impostazioni.index', compact('tipologie', 'tipologieEventi', 'ruoli', 'tags', 'appSettings', 'emailSettings', 'senderAccounts', 'repositories', 'googleDriveNewToken', 'calendarioConnessioni', 'googleStatus'));
|
||||
}
|
||||
|
||||
public function saveAppSettings(Request $request)
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Individuo;
|
||||
use App\Models\Contatto;
|
||||
use App\Models\Documento;
|
||||
use App\Models\Gruppo;
|
||||
use App\Models\Comune;
|
||||
use App\Models\Diocesi;
|
||||
use App\Models\Tag;
|
||||
use App\Models\VistaReport;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use App\Models\Gruppo;
|
||||
use App\Models\Comune;
|
||||
use App\Models\Diocesi;
|
||||
use App\Models\Tag;
|
||||
use App\Models\VistaReport;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class IndividuoController extends Controller
|
||||
{
|
||||
@@ -148,7 +152,7 @@ public function create()
|
||||
{
|
||||
$this->authorizeWrite('individui');
|
||||
$comuni = Comune::orderBy('nome')->get();
|
||||
$diocesi = Diocesi::orderBy('nome')->get();
|
||||
$diocesi = Diocesi::orderBy('id')->get();
|
||||
$tags = Tag::orderBy('name')->get();
|
||||
return view('individui.create', compact('comuni', 'diocesi', 'tags'));
|
||||
}
|
||||
@@ -497,16 +501,27 @@ public function emailList(Request $request)
|
||||
'file' => 'required|file|mimes:csv,txt',
|
||||
]);
|
||||
|
||||
set_time_limit(0);
|
||||
session_write_close();
|
||||
|
||||
$file = $request->file('file');
|
||||
$handle = fopen($file->path(), 'r');
|
||||
$header = fgetcsv($handle, 1000, ',');
|
||||
$header = fgetcsv($handle, 0, ',');
|
||||
|
||||
if ($header === false) {
|
||||
fclose($handle);
|
||||
return back()->with('error', 'Il file CSV non è valido.');
|
||||
}
|
||||
|
||||
$header = array_map('trim', $header);
|
||||
$imported = 0;
|
||||
$skipped = 0;
|
||||
$errors = [];
|
||||
$rowNumber = 1;
|
||||
|
||||
while (($row = fgetcsv($handle, 1000, ',')) !== false) {
|
||||
DB::beginTransaction();
|
||||
|
||||
while (($row = fgetcsv($handle, 0, ',')) !== false) {
|
||||
$rowNumber++;
|
||||
|
||||
if (count($row) < count($header)) {
|
||||
@@ -565,12 +580,15 @@ public function emailList(Request $request)
|
||||
|
||||
$imported++;
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Errore import individuo riga ' . $rowNumber . ': ' . $e->getMessage());
|
||||
$errors[] = 'Errore riga ' . $rowNumber . ': ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
DB::commit();
|
||||
|
||||
$message = 'Importati ' . $imported . ' individui.';
|
||||
if ($skipped > 0) {
|
||||
$message .= ' Saltate ' . $skipped . ' righe (vuote o incomplete).';
|
||||
|
||||
@@ -10,6 +10,8 @@ use App\Models\MailingMessaggio;
|
||||
use App\Models\SenderAccount;
|
||||
use App\Models\Individuo;
|
||||
use App\Models\Documento;
|
||||
use App\Models\EmailSetting;
|
||||
use App\Models\Firma;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
@@ -30,6 +32,8 @@ class MailingController extends Controller
|
||||
$liste = MailingList::all();
|
||||
$documenti = Documento::orderBy('nome_file')->get();
|
||||
$senderAccounts = SenderAccount::active()->get();
|
||||
$settings = EmailSetting::getActive();
|
||||
$firme = $settings?->firme ?? collect();
|
||||
|
||||
$documentiSelezionati = collect();
|
||||
if ($request->has('documenti_selezionati')) {
|
||||
@@ -37,7 +41,7 @@ class MailingController extends Controller
|
||||
$documentiSelezionati = Documento::whereIn('id', $docIds)->get();
|
||||
}
|
||||
|
||||
return view('mailing.nuovo', compact('individui', 'liste', 'documenti', 'documentiSelezionati', 'senderAccounts'));
|
||||
return view('mailing.nuovo', compact('individui', 'liste', 'documenti', 'documentiSelezionati', 'senderAccounts', 'firme'));
|
||||
}
|
||||
|
||||
public function invia(Request $request)
|
||||
@@ -46,16 +50,22 @@ class MailingController extends Controller
|
||||
$data = $request->validate([
|
||||
'oggetto' => 'required|string|max:255',
|
||||
'corpo' => 'required',
|
||||
'lista_id' => 'nullable|exists:mailing_liste,id',
|
||||
'lista_id' => 'nullable|exists:mailing_lists,id',
|
||||
'destinatari_ids' => 'nullable|string',
|
||||
'documenti_selezionati' => 'nullable|string',
|
||||
'allegato' => 'nullable|file|max:10240',
|
||||
'mittente_id' => 'nullable|integer|exists:sender_accounts,id',
|
||||
'firma_id' => 'nullable|integer|exists:firme,id',
|
||||
]);
|
||||
|
||||
$sender = null;
|
||||
if (!empty($data['mittente_id'])) {
|
||||
$sender = SenderAccount::findOrFail($data['mittente_id']);
|
||||
} elseif (!empty($data['lista_id'])) {
|
||||
$lista = MailingList::with('senderAccount')->find($data['lista_id']);
|
||||
if ($lista && $lista->senderAccount) {
|
||||
$sender = $lista->senderAccount;
|
||||
}
|
||||
}
|
||||
|
||||
$documentiAllegati = [];
|
||||
@@ -110,7 +120,23 @@ class MailingController extends Controller
|
||||
return back()->with('error', 'Nessun destinatario email valido trovato.')->withInput();
|
||||
}
|
||||
|
||||
$attachmentPaths = $this->resolveMailingAttachmentPaths($documentiAllegati);
|
||||
$attachmentResult = $this->resolveMailingAttachmentPaths($documentiAllegati);
|
||||
$attachmentPaths = $attachmentResult['paths'];
|
||||
$linkDocs = $attachmentResult['links'];
|
||||
|
||||
$firmaId = $data['firma_id'] ?? null;
|
||||
if (!$firmaId && !empty($data['lista_id'])) {
|
||||
$lista = MailingList::with('firma')->find($data['lista_id']);
|
||||
$firmaId = $lista?->firma?->id;
|
||||
}
|
||||
|
||||
$corpoConFirma = $this->appendMailingLinksToBody($data['corpo'], $linkDocs);
|
||||
if ($firmaId) {
|
||||
$firma = Firma::find($firmaId);
|
||||
if ($firma && $firma->contenuto_html) {
|
||||
$corpoConFirma .= "\n\n---\n" . $firma->contenuto_html;
|
||||
}
|
||||
}
|
||||
|
||||
$messaggio = MailingMessaggio::create([
|
||||
'mailing_list_id' => $data['lista_id'] ?? null,
|
||||
@@ -121,6 +147,7 @@ class MailingController extends Controller
|
||||
'totale_destinatari' => $emails->count(),
|
||||
'mittente_nome' => $sender?->email_name,
|
||||
'mittente_email' => $sender?->email_address,
|
||||
'firma_id' => $firmaId,
|
||||
]);
|
||||
|
||||
$ok = 0;
|
||||
@@ -130,9 +157,9 @@ class MailingController extends Controller
|
||||
foreach ($emails as $email) {
|
||||
try {
|
||||
if ($sender) {
|
||||
$sender->sendEmail($email, $data['oggetto'], $data['corpo'], $attachmentPaths);
|
||||
$sender->sendEmail($email, $data['oggetto'], $corpoConFirma, $attachmentPaths);
|
||||
} else {
|
||||
$this->sendViaSystem($email, $data['oggetto'], $data['corpo'], $attachmentPaths);
|
||||
$this->sendViaSystem($email, $data['oggetto'], $corpoConFirma, $attachmentPaths);
|
||||
}
|
||||
$ok++;
|
||||
} catch (\Throwable $e) {
|
||||
@@ -181,11 +208,13 @@ class MailingController extends Controller
|
||||
public function invio(Request $request)
|
||||
{
|
||||
$this->authorizeWrite('mailing');
|
||||
$liste = MailingList::where('attiva', true)->orderBy('nome')->get();
|
||||
$liste = MailingList::with('firma')->where('attiva', true)->orderBy('nome')->get();
|
||||
$documenti = Documento::orderBy('nome_file')->get();
|
||||
$senderAccounts = SenderAccount::active()->get();
|
||||
$settings = EmailSetting::getActive();
|
||||
$firme = $settings?->firme ?? collect();
|
||||
|
||||
return view('mailing.invio', compact('liste', 'documenti', 'senderAccounts'));
|
||||
return view('mailing.invio', compact('liste', 'documenti', 'senderAccounts', 'firme'));
|
||||
}
|
||||
|
||||
public function invioElabora(Request $request)
|
||||
@@ -193,18 +222,23 @@ class MailingController extends Controller
|
||||
$this->authorizeWrite('mailing');
|
||||
$data = $request->validate([
|
||||
'liste' => 'required|array',
|
||||
'liste.*' => 'exists:mailing_liste,id',
|
||||
'liste.*' => 'exists:mailing_lists,id',
|
||||
'oggetto' => 'required|string|max:255',
|
||||
'corpo' => 'required',
|
||||
'documenti_selezionati' => 'nullable|array',
|
||||
'documenti_selezionati.*' => 'exists:documenti,id',
|
||||
'allegato' => 'nullable|file|max:10240',
|
||||
'mittente_id' => 'nullable|integer|exists:sender_accounts,id',
|
||||
'firma_id' => 'nullable|integer|exists:firme,id',
|
||||
]);
|
||||
|
||||
$listeModels = MailingList::with('firma', 'senderAccount')->whereIn('id', $data['liste'])->get();
|
||||
|
||||
$sender = null;
|
||||
if (!empty($data['mittente_id'])) {
|
||||
$sender = SenderAccount::findOrFail($data['mittente_id']);
|
||||
} elseif ($listeModels->count() === 1) {
|
||||
$sender = $listeModels->first()->senderAccount;
|
||||
}
|
||||
|
||||
$documentiAllegati = [];
|
||||
@@ -252,9 +286,24 @@ class MailingController extends Controller
|
||||
return back()->with('error', 'Nessun destinatario email valido trovato nelle liste selezionate.');
|
||||
}
|
||||
|
||||
$attachmentPaths = $this->resolveMailingAttachmentPaths($documentiAllegati);
|
||||
$attachmentResult = $this->resolveMailingAttachmentPaths($documentiAllegati);
|
||||
$attachmentPaths = $attachmentResult['paths'];
|
||||
$linkDocs = $attachmentResult['links'];
|
||||
|
||||
$listeNomi = MailingList::whereIn('id', $data['liste'])->pluck('nome')->implode(', ');
|
||||
$firmaId = $data['firma_id'] ?? null;
|
||||
if (!$firmaId && $listeModels->count() === 1) {
|
||||
$firmaId = $listeModels->first()->firma_id;
|
||||
}
|
||||
|
||||
$corpoConFirma = $this->appendMailingLinksToBody($data['corpo'], $linkDocs);
|
||||
if ($firmaId) {
|
||||
$firma = Firma::find($firmaId);
|
||||
if ($firma && $firma->contenuto_html) {
|
||||
$corpoConFirma .= "\n\n---\n" . $firma->contenuto_html;
|
||||
}
|
||||
}
|
||||
|
||||
$listeNomi = $listeModels->pluck('nome')->implode(', ');
|
||||
|
||||
$messaggio = MailingMessaggio::create([
|
||||
'mailing_list_id' => null,
|
||||
@@ -265,6 +314,7 @@ class MailingController extends Controller
|
||||
'totale_destinatari' => $emails->count(),
|
||||
'mittente_nome' => $sender?->email_name,
|
||||
'mittente_email' => $sender?->email_address,
|
||||
'firma_id' => $firmaId,
|
||||
]);
|
||||
|
||||
$ok = 0;
|
||||
@@ -274,9 +324,9 @@ class MailingController extends Controller
|
||||
foreach ($emails as $email) {
|
||||
try {
|
||||
if ($sender) {
|
||||
$sender->sendEmail($email, $data['oggetto'], $data['corpo'], $attachmentPaths);
|
||||
$sender->sendEmail($email, $data['oggetto'], $corpoConFirma, $attachmentPaths);
|
||||
} else {
|
||||
$this->sendViaSystem($email, $data['oggetto'], $data['corpo'], $attachmentPaths);
|
||||
$this->sendViaSystem($email, $data['oggetto'], $corpoConFirma, $attachmentPaths);
|
||||
}
|
||||
$ok++;
|
||||
} catch (\Throwable $e) {
|
||||
@@ -347,18 +397,10 @@ class MailingController extends Controller
|
||||
$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);
|
||||
$fromAddress = \Symfony\Component\Mime\Address::create(
|
||||
$settings->getEffectiveFromAddress(),
|
||||
$settings->getEffectiveFromName()
|
||||
);
|
||||
|
||||
$email = (new \Symfony\Component\Mime\Email())
|
||||
->from($fromAddress)
|
||||
@@ -366,6 +408,11 @@ class MailingController extends Controller
|
||||
->subject($subject)
|
||||
->text($body);
|
||||
|
||||
$replyTo = $settings->getEffectiveReplyTo();
|
||||
if ($replyTo) {
|
||||
$email->replyTo($replyTo);
|
||||
}
|
||||
|
||||
foreach ($attachmentPaths as $path) {
|
||||
$email->attachFromPath($path);
|
||||
}
|
||||
@@ -376,17 +423,34 @@ class MailingController extends Controller
|
||||
private function resolveMailingAttachmentPaths(array $documentIds): array
|
||||
{
|
||||
$paths = [];
|
||||
$links = [];
|
||||
if (empty($documentIds)) {
|
||||
return $paths;
|
||||
return ['paths' => $paths, 'links' => $links];
|
||||
}
|
||||
|
||||
$docs = Documento::whereIn('id', $documentIds)->get();
|
||||
foreach ($docs as $doc) {
|
||||
if ($doc->file_path && Storage::exists($doc->file_path)) {
|
||||
if ($doc->isLink()) {
|
||||
$links[] = ['nome' => $doc->nome_file, 'url' => $doc->url];
|
||||
} elseif ($doc->file_path && Storage::exists($doc->file_path)) {
|
||||
$paths[] = Storage::path($doc->file_path);
|
||||
}
|
||||
}
|
||||
|
||||
return $paths;
|
||||
return ['paths' => $paths, 'links' => $links];
|
||||
}
|
||||
|
||||
private function appendMailingLinksToBody(string $body, array $linkDocs): string
|
||||
{
|
||||
if (empty($linkDocs)) {
|
||||
return $body;
|
||||
}
|
||||
|
||||
$linksText = "\n\n---\nDocumenti collegati:\n";
|
||||
foreach ($linkDocs as $link) {
|
||||
$linksText .= "- {$link['nome']}: {$link['url']}\n";
|
||||
}
|
||||
|
||||
return $body . $linksText;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\MailingList;
|
||||
use App\Models\SenderAccount;
|
||||
use App\Models\Tag;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@@ -60,7 +61,10 @@ class MailingListController extends Controller
|
||||
{
|
||||
$this->authorizeWrite('mailing');
|
||||
$tags = Tag::orderBy('name')->get();
|
||||
return view('mailing-liste.create', compact('tags'));
|
||||
$settings = \App\Models\EmailSetting::getActive();
|
||||
$firme = $settings?->firme ?? collect();
|
||||
$senderAccounts = SenderAccount::active()->get();
|
||||
return view('mailing-liste.create', compact('tags', 'firme', 'senderAccounts'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
@@ -73,6 +77,8 @@ class MailingListController extends Controller
|
||||
'contatti_json' => 'nullable|string',
|
||||
'tags' => 'nullable|array',
|
||||
'tags.*' => 'exists:tags,id',
|
||||
'firma_id' => 'nullable|integer|exists:firme,id',
|
||||
'sender_account_id' => 'nullable|integer|exists:sender_accounts,id',
|
||||
]);
|
||||
|
||||
$data['user_id'] = auth()->id();
|
||||
@@ -83,6 +89,8 @@ class MailingListController extends Controller
|
||||
'descrizione' => $data['descrizione'] ?? null,
|
||||
'attiva' => $data['attiva'],
|
||||
'user_id' => $data['user_id'],
|
||||
'firma_id' => $data['firma_id'] ?? null,
|
||||
'sender_account_id' => $data['sender_account_id'] ?? null,
|
||||
]);
|
||||
|
||||
if ($request->has('tags')) {
|
||||
@@ -117,10 +125,13 @@ class MailingListController extends Controller
|
||||
public function edit($mailingList)
|
||||
{
|
||||
$this->authorizeWrite('mailing');
|
||||
$mailingList = MailingList::with(['contatti.individuo.contatti', 'tags'])->findOrFail($mailingList);
|
||||
$mailingList = MailingList::with(['contatti.individuo.contatti', 'tags', 'firma', 'senderAccount'])->findOrFail($mailingList);
|
||||
$tags = Tag::orderBy('name')->get();
|
||||
$selectedTags = $mailingList->tags->pluck('id')->toArray();
|
||||
return view('mailing-liste.edit', compact('mailingList', 'tags', 'selectedTags'));
|
||||
$settings = \App\Models\EmailSetting::getActive();
|
||||
$firme = $settings?->firme ?? collect();
|
||||
$senderAccounts = SenderAccount::active()->get();
|
||||
return view('mailing-liste.edit', compact('mailingList', 'tags', 'selectedTags', 'firme', 'senderAccounts'));
|
||||
}
|
||||
|
||||
public function update(Request $request, $mailingList)
|
||||
@@ -135,6 +146,8 @@ class MailingListController extends Controller
|
||||
'contatti_json' => 'nullable|string',
|
||||
'tags' => 'nullable|array',
|
||||
'tags.*' => 'exists:tags,id',
|
||||
'firma_id' => 'nullable|integer|exists:firme,id',
|
||||
'sender_account_id' => 'nullable|integer|exists:sender_accounts,id',
|
||||
]);
|
||||
|
||||
$data['attiva'] = $data['attiva'] ?? false;
|
||||
@@ -143,6 +156,8 @@ class MailingListController extends Controller
|
||||
'nome' => $data['nome'],
|
||||
'descrizione' => $data['descrizione'] ?? null,
|
||||
'attiva' => $data['attiva'],
|
||||
'firma_id' => $data['firma_id'] ?? null,
|
||||
'sender_account_id' => $data['sender_account_id'] ?? null,
|
||||
]);
|
||||
|
||||
if ($request->has('tags')) {
|
||||
@@ -186,6 +201,41 @@ class MailingListController extends Controller
|
||||
return redirect()->route('mailing-liste.index')->with('success', 'Lista eliminata.');
|
||||
}
|
||||
|
||||
public function massTag(Request $request)
|
||||
{
|
||||
$this->authorizeWrite('mailing');
|
||||
$idsInput = $request->input('ids', '');
|
||||
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
|
||||
|
||||
if (empty($ids)) {
|
||||
return back()->with('error', 'Nessuna mailing list selezionata.');
|
||||
}
|
||||
|
||||
$data = $request->validate([
|
||||
'tags' => 'required|array',
|
||||
'tags.*' => 'exists:tags,id',
|
||||
'mode' => 'required|in:assign,remove',
|
||||
]);
|
||||
|
||||
$tagIds = $data['tags'];
|
||||
$mode = $data['mode'];
|
||||
$count = 0;
|
||||
|
||||
MailingList::whereIn('id', $ids)->chunk(100, function ($lists) use ($tagIds, $mode, &$count) {
|
||||
foreach ($lists as $list) {
|
||||
if ($mode === 'assign') {
|
||||
$list->tags()->syncWithoutDetaching($tagIds);
|
||||
} else {
|
||||
$list->tags()->detach($tagIds);
|
||||
}
|
||||
$count++;
|
||||
}
|
||||
});
|
||||
|
||||
$actionLabel = $mode === 'assign' ? 'assegnati' : 'rimossi';
|
||||
return back()->with('success', "Tag $actionLabel per $count mailing list.");
|
||||
}
|
||||
|
||||
public function massElimina(Request $request)
|
||||
{
|
||||
$this->authorizeDelete('mailing');
|
||||
|
||||
@@ -515,7 +515,7 @@ class ReportController extends Controller
|
||||
$rows[] = [
|
||||
'livello' => 0,
|
||||
'nome' => $gruppo->nome,
|
||||
'diocesi' => $gruppo->diocesi?->nome ?? '-',
|
||||
'diocesi' => $gruppo->diocesi->count() > 0 ? $gruppo->diocesi->pluck('nome')->implode(', ') : '-',
|
||||
'membri' => $gruppo->individui()->count(),
|
||||
'padre' => '-',
|
||||
];
|
||||
@@ -538,7 +538,7 @@ class ReportController extends Controller
|
||||
$rows[] = [
|
||||
'livello' => $level,
|
||||
'nome' => str_repeat(' ', $level) . $child->nome,
|
||||
'diocesi' => $child->diocesi?->nome ?? '-',
|
||||
'diocesi' => $child->diocesi->count() > 0 ? $child->diocesi->pluck('nome')->implode(', ') : '-',
|
||||
'membri' => $child->individui()->count(),
|
||||
'padre' => $parent?->nome ?? '-',
|
||||
];
|
||||
@@ -554,7 +554,7 @@ class ReportController extends Controller
|
||||
$parent = $g->parent;
|
||||
return [
|
||||
'nome' => $g->full_path,
|
||||
'diocesi' => $g->diocesi?->nome ?? '-',
|
||||
'diocesi' => $g->diocesi->count() > 0 ? $g->diocesi->pluck('nome')->implode(', ') : '-',
|
||||
'membri' => $g->individui_count,
|
||||
'padre' => $parent?->nome ?? '-',
|
||||
'responsabili' => $g->getResponsabili()->pluck('cognome')->implode(', ') ?: '-',
|
||||
@@ -895,7 +895,7 @@ class ReportController extends Controller
|
||||
return [
|
||||
'nome' => $g->nome,
|
||||
'descrizione' => $g->descrizione ?? '-',
|
||||
'diocesi' => $g->diocesi?->nome ?? '-',
|
||||
'diocesi' => $g->diocesi->count() > 0 ? $g->diocesi->pluck('nome')->implode(', ') : '-',
|
||||
'livello' => $g->parent_id ? (count($g->getAncestors()) + 1) : 0,
|
||||
'membri' => $g->individui_count,
|
||||
'padre' => $parent?->nome ?? '-',
|
||||
|
||||
@@ -53,11 +53,13 @@ class CalendarioConnessione extends Model
|
||||
return $config;
|
||||
}
|
||||
|
||||
public function encryptAndSetConfig(array $config): void
|
||||
public function encryptAndSetConfig(array $config, array $existingConfig = []): void
|
||||
{
|
||||
foreach (['username', 'password', 'client_secret', 'refresh_token', 'api_key'] as $field) {
|
||||
if (isset($config[$field]) && $config[$field] !== '' && !str_starts_with($config[$field], 'eyJpdiI')) {
|
||||
if (!empty($config[$field]) && !str_starts_with($config[$field], 'eyJpdiI')) {
|
||||
$config[$field] = Crypt::encryptString($config[$field]);
|
||||
} elseif (empty($config[$field]) && !empty($existingConfig[$field])) {
|
||||
$config[$field] = $existingConfig[$field];
|
||||
}
|
||||
}
|
||||
$this->config = $config;
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
|
||||
class Diocesi extends Model
|
||||
{
|
||||
protected $table = 'diocesi';
|
||||
protected $fillable = ['nome', 'regione'];
|
||||
|
||||
public function gruppi(): HasMany
|
||||
public function gruppi(): BelongsToMany
|
||||
{
|
||||
return $this->hasMany(Gruppo::class);
|
||||
return $this->belongsToMany(Gruppo::class, 'diocesi_gruppo');
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\HasTagsLight;
|
||||
@@ -16,9 +18,14 @@ class Documento extends Model
|
||||
'tenant_id', 'user_id', 'cartella_id', 'repository_id', 'storage_disk',
|
||||
'nome_file', 'file_path', 'tipo', 'tipologia',
|
||||
'visibilita', 'visibilita_target_id', 'visibilita_target_type',
|
||||
'mime_type', 'dimensione', 'note'
|
||||
'mime_type', 'dimensione', 'note', 'url'
|
||||
];
|
||||
|
||||
public function isLink(): bool
|
||||
{
|
||||
return $this->tipo === 'link';
|
||||
}
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
|
||||
+156
-8
@@ -2,7 +2,9 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Services\GoogleOAuthService;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
||||
@@ -13,14 +15,17 @@ class EmailSetting extends Model
|
||||
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'
|
||||
'email_address', 'email_name', 'from_email', 'from_name', 'reply_to',
|
||||
'sync_interval_minutes',
|
||||
'last_sync_at', 'is_active', 'signature', 'signature_enabled',
|
||||
'auth_method', 'google_oauth_connection_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'signature_enabled' => 'boolean',
|
||||
'last_sync_at' => 'datetime',
|
||||
'google_oauth_connection_id' => 'integer',
|
||||
];
|
||||
|
||||
public function folders(): HasMany
|
||||
@@ -28,8 +33,25 @@ class EmailSetting extends Model
|
||||
return $this->hasMany(EmailFolder::class)->orderBy('sort_order');
|
||||
}
|
||||
|
||||
public function googleOAuthConnection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(GoogleOAuthConnection::class);
|
||||
}
|
||||
|
||||
public function getImapConfig(): array
|
||||
{
|
||||
if ($this->auth_method === 'oauth') {
|
||||
$token = $this->resolveOAuthToken();
|
||||
return [
|
||||
'host' => $this->imap_host,
|
||||
'port' => $this->imap_port,
|
||||
'encryption' => $this->imap_encryption,
|
||||
'username' => $this->imap_username,
|
||||
'authentication' => 'oauth',
|
||||
'password' => $token,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'host' => $this->imap_host,
|
||||
'port' => $this->imap_port,
|
||||
@@ -41,6 +63,10 @@ class EmailSetting extends Model
|
||||
|
||||
public function getDecryptedPassword(): string
|
||||
{
|
||||
if ($this->auth_method === 'oauth') {
|
||||
return $this->resolveOAuthToken();
|
||||
}
|
||||
|
||||
if (empty($this->imap_password)) {
|
||||
return '';
|
||||
}
|
||||
@@ -53,6 +79,10 @@ class EmailSetting extends Model
|
||||
|
||||
public function getDecryptedSmtpPassword(): string
|
||||
{
|
||||
if ($this->auth_method === 'oauth') {
|
||||
return $this->resolveOAuthToken();
|
||||
}
|
||||
|
||||
if (empty($this->smtp_password)) {
|
||||
return $this->getDecryptedPassword();
|
||||
}
|
||||
@@ -63,6 +93,22 @@ class EmailSetting extends Model
|
||||
}
|
||||
}
|
||||
|
||||
protected function resolveOAuthToken(): string
|
||||
{
|
||||
if (!$this->relationLoaded('googleOAuthConnection')) {
|
||||
$this->load('googleOAuthConnection');
|
||||
}
|
||||
$connection = $this->googleOAuthConnection;
|
||||
if (!$connection) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
return app(GoogleOAuthService::class)->getAccessToken($connection);
|
||||
} catch (\Exception $e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
public function getSmtpConfig(): array
|
||||
{
|
||||
return [
|
||||
@@ -87,33 +133,101 @@ class EmailSetting extends Model
|
||||
default => 'none',
|
||||
};
|
||||
|
||||
return new \DirectoryTree\ImapEngine\Mailbox([
|
||||
$config = [
|
||||
'host' => $this->imap_host,
|
||||
'port' => $this->imap_port ?? 993,
|
||||
'encryption' => $encryption,
|
||||
'validate_cert' => false,
|
||||
'username' => $this->imap_username,
|
||||
'password' => $this->getDecryptedPassword(),
|
||||
]);
|
||||
];
|
||||
|
||||
if ($this->auth_method === 'oauth') {
|
||||
$config['authentication'] = 'oauth';
|
||||
}
|
||||
|
||||
return new \DirectoryTree\ImapEngine\Mailbox($config);
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function getSmtpMailer(): ?\Symfony\Component\Mailer\Mailer
|
||||
{
|
||||
if ($this->auth_method === 'oauth') {
|
||||
return $this->buildOAuthSmtpMailer();
|
||||
}
|
||||
|
||||
$dsn = $this->buildSmtpDsn();
|
||||
if (!$dsn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$transport = \Symfony\Component\Mailer\Transport::fromDsn($dsn);
|
||||
return new \Symfony\Component\Mailer\Mailer($transport);
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected function buildOAuthSmtpMailer(): ?\Symfony\Component\Mailer\Mailer
|
||||
{
|
||||
$token = $this->resolveOAuthToken();
|
||||
if (empty($token)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$username = $this->smtp_username ?? $this->email_address;
|
||||
$host = $this->smtp_host ?? 'smtp.gmail.com';
|
||||
$port = $this->smtp_port ?? 587;
|
||||
|
||||
$transport = new \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport($host, $port, true);
|
||||
$transport->setUsername($username);
|
||||
$transport->setPassword($token);
|
||||
$transport->setAuthenticators([
|
||||
new \Symfony\Component\Mailer\Transport\Smtp\Auth\XOAuth2Authenticator(),
|
||||
]);
|
||||
|
||||
return new \Symfony\Component\Mailer\Mailer($transport);
|
||||
}
|
||||
|
||||
public function buildSmtpDsn(): ?string
|
||||
{
|
||||
$password = $this->getDecryptedSmtpPassword();
|
||||
if (empty($password)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$username = $this->smtp_username ?? $this->email_address;
|
||||
$encryption = $this->smtp_encryption ?? 'tls';
|
||||
$scheme = ($encryption === 'ssl') ? 'smtps' : 'smtp';
|
||||
|
||||
return sprintf(
|
||||
'%s://%s:%s@%s:%d?encryption=%s',
|
||||
$scheme,
|
||||
rawurlencode($username),
|
||||
rawurlencode($password),
|
||||
$this->smtp_host ?? 'smtp.gmail.com',
|
||||
$this->smtp_port ?? 587,
|
||||
$encryption
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
'success' => true,
|
||||
'message' => 'Connessione IMAP riuscita! Server: ' . $this->imap_host
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
@@ -134,4 +248,38 @@ class EmailSetting extends Model
|
||||
{
|
||||
return $this->signature;
|
||||
}
|
||||
}
|
||||
|
||||
public function getEffectiveFromAddress(): string
|
||||
{
|
||||
return $this->from_email ?: $this->email_address;
|
||||
}
|
||||
|
||||
public function getEffectiveFromName(): string
|
||||
{
|
||||
$name = $this->from_name ?: $this->email_name;
|
||||
if (filter_var($name, FILTER_VALIDATE_EMAIL)) {
|
||||
return 'Glastree';
|
||||
}
|
||||
$name = preg_replace('/[^\p{L}\p{N}\s]/u', '', $name);
|
||||
$name = trim($name);
|
||||
return empty($name) ? 'Glastree' : $name;
|
||||
}
|
||||
|
||||
public function getEffectiveReplyTo(): ?string
|
||||
{
|
||||
if ($this->from_email) {
|
||||
return $this->from_email;
|
||||
}
|
||||
return $this->reply_to ?: null;
|
||||
}
|
||||
|
||||
public function firme(): HasMany
|
||||
{
|
||||
return $this->hasMany(Firma::class);
|
||||
}
|
||||
|
||||
public function firmaPredefinita(): ?Firma
|
||||
{
|
||||
return $this->firme()->where('is_default', true)->first() ?? $this->firme()->first();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\HasTagsLight;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Firma extends Model
|
||||
{
|
||||
protected $table = 'firme';
|
||||
|
||||
protected $fillable = [
|
||||
'email_setting_id',
|
||||
'nome',
|
||||
'contenuto_html',
|
||||
'is_default',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_default' => 'boolean',
|
||||
];
|
||||
|
||||
public function emailSetting(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(EmailSetting::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Services\GoogleOAuthService;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class GoogleOAuthConnection extends Model
|
||||
{
|
||||
protected $table = 'google_oauth_connections';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'service',
|
||||
'email',
|
||||
'access_token',
|
||||
'refresh_token',
|
||||
'expires_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'expires_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function isExpired(): bool
|
||||
{
|
||||
return $this->expires_at && $this->expires_at->isPast();
|
||||
}
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
if (!$this->refresh_token) {
|
||||
throw new \RuntimeException('Nessun refresh token disponibile per ' . $this->email);
|
||||
}
|
||||
app(GoogleOAuthService::class)->refreshToken($this);
|
||||
}
|
||||
|
||||
public function getValidAccessToken(): string
|
||||
{
|
||||
if ($this->isExpired() && $this->refresh_token) {
|
||||
$this->refresh();
|
||||
}
|
||||
return $this->access_token;
|
||||
}
|
||||
}
|
||||
+23
-3
@@ -15,15 +15,18 @@ class Gruppo extends Model
|
||||
protected $table = 'gruppi';
|
||||
protected $fillable = [
|
||||
'tenant_id', 'parent_id', 'diocesi_id', 'responsabile_ids',
|
||||
'nome', 'descrizione', 'indirizzo_incontro', 'cap_incontro',
|
||||
'nome', 'anno_fondazione', 'descrizione', 'indirizzo_incontro', 'cap_incontro',
|
||||
'città_incontro', 'sigla_provincia_incontro', 'mappa_posizione', 'metadata'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'metadata' => 'array',
|
||||
'responsabile_ids' => 'array',
|
||||
'anno_fondazione' => 'integer',
|
||||
];
|
||||
|
||||
protected $appends = ['email_primaria', 'telefono_primario'];
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
@@ -39,9 +42,14 @@ class Gruppo extends Model
|
||||
return $this->hasMany(Gruppo::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function diocesi(): BelongsTo
|
||||
public function diocesi(): BelongsToMany
|
||||
{
|
||||
return $this->belongsTo(Diocesi::class);
|
||||
return $this->belongsToMany(Diocesi::class, 'diocesi_gruppo');
|
||||
}
|
||||
|
||||
public function gruppoContatti(): HasMany
|
||||
{
|
||||
return $this->hasMany(GruppoContatto::class, 'gruppo_id');
|
||||
}
|
||||
|
||||
public function individui(): BelongsToMany
|
||||
@@ -66,6 +74,18 @@ class Gruppo extends Model
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getEmailPrimariaAttribute(): ?string
|
||||
{
|
||||
return $this->gruppoContatti()->where('tipo', 'email')->where('is_primary', true)->first()?->valore
|
||||
?? $this->gruppoContatti()->where('tipo', 'email')->first()?->valore;
|
||||
}
|
||||
|
||||
public function getTelefonoPrimarioAttribute(): ?string
|
||||
{
|
||||
return $this->gruppoContatti()->whereIn('tipo', ['telefono', 'cellulare'])->where('is_primary', true)->first()?->valore
|
||||
?? $this->gruppoContatti()->whereIn('tipo', ['telefono', 'cellulare'])->first()?->valore;
|
||||
}
|
||||
|
||||
public function getResponsabili()
|
||||
{
|
||||
if (empty($this->responsabile_ids)) {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class GruppoContatto extends Model
|
||||
{
|
||||
protected $table = 'gruppo_contatti';
|
||||
protected $fillable = ['gruppo_id', 'tipo', 'valore', 'etichetta', 'is_primary'];
|
||||
|
||||
protected $casts = [
|
||||
'is_primary' => 'boolean',
|
||||
];
|
||||
|
||||
public function gruppo(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Gruppo::class);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ class MailingList extends Model
|
||||
use HasTagsLight;
|
||||
|
||||
protected $table = 'mailing_lists';
|
||||
protected $fillable = ['tenant_id', 'user_id', 'nome', 'descrizione', 'attiva'];
|
||||
protected $fillable = ['tenant_id', 'user_id', 'nome', 'descrizione', 'attiva', 'firma_id', 'sender_account_id'];
|
||||
|
||||
protected $casts = ['attiva' => 'boolean'];
|
||||
|
||||
@@ -36,6 +36,16 @@ class MailingList extends Model
|
||||
return $this->hasMany(MailingMessaggio::class);
|
||||
}
|
||||
|
||||
public function firma(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Firma::class);
|
||||
}
|
||||
|
||||
public function senderAccount(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(SenderAccount::class);
|
||||
}
|
||||
|
||||
public function getIndividui()
|
||||
{
|
||||
return Individuo::whereHas('mailingContacts', function ($q) {
|
||||
|
||||
@@ -11,7 +11,7 @@ class MailingMessaggio extends Model
|
||||
protected $fillable = [
|
||||
'tenant_id', 'mailing_list_id', 'user_id', 'oggetto', 'corpo',
|
||||
'canale', 'stato', 'inviato_al', 'totale_destinatari', 'invii_ok', 'invii_falliti',
|
||||
'log_falliti', 'mittente_nome', 'mittente_email',
|
||||
'log_falliti', 'mittente_nome', 'mittente_email', 'firma_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
|
||||
@@ -4,8 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Services\GoogleOAuthService;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
||||
class SenderAccount extends Model
|
||||
@@ -24,11 +26,14 @@ class SenderAccount extends Model
|
||||
'verify_email',
|
||||
'note',
|
||||
'is_active',
|
||||
'auth_method',
|
||||
'google_oauth_connection_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'smtp_port' => 'integer',
|
||||
'google_oauth_connection_id' => 'integer',
|
||||
];
|
||||
|
||||
public function scopeActive(Builder $query): Builder
|
||||
@@ -36,8 +41,33 @@ class SenderAccount extends Model
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
public function googleOAuthConnection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(GoogleOAuthConnection::class);
|
||||
}
|
||||
|
||||
protected function resolveOAuthToken(): string
|
||||
{
|
||||
if (!$this->relationLoaded('googleOAuthConnection')) {
|
||||
$this->load('googleOAuthConnection');
|
||||
}
|
||||
$connection = $this->googleOAuthConnection;
|
||||
if (!$connection) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
return app(GoogleOAuthService::class)->getAccessToken($connection);
|
||||
} catch (\Exception $e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
public function getDecryptedPassword(): string
|
||||
{
|
||||
if ($this->auth_method === 'oauth') {
|
||||
return $this->resolveOAuthToken();
|
||||
}
|
||||
|
||||
if (empty($this->smtp_password)) {
|
||||
return '';
|
||||
}
|
||||
@@ -121,11 +151,33 @@ class SenderAccount extends Model
|
||||
$email->attachFromPath($path);
|
||||
}
|
||||
|
||||
$transport = \Symfony\Component\Mailer\Transport::fromDsn($this->buildDsn());
|
||||
$mailer = new \Symfony\Component\Mailer\Mailer($transport);
|
||||
if ($this->auth_method === 'oauth') {
|
||||
$mailer = $this->buildOAuthMailer();
|
||||
} else {
|
||||
$transport = \Symfony\Component\Mailer\Transport::fromDsn($this->buildDsn());
|
||||
$mailer = new \Symfony\Component\Mailer\Mailer($transport);
|
||||
}
|
||||
|
||||
$mailer->send($email);
|
||||
}
|
||||
|
||||
protected function buildOAuthMailer(): \Symfony\Component\Mailer\Mailer
|
||||
{
|
||||
$token = $this->resolveOAuthToken();
|
||||
$username = $this->smtp_username ?? $this->email_address;
|
||||
$host = $this->smtp_host ?? 'smtp.gmail.com';
|
||||
$port = $this->smtp_port ?? 587;
|
||||
|
||||
$transport = new \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport($host, $port, true);
|
||||
$transport->setUsername($username);
|
||||
$transport->setPassword($token);
|
||||
$transport->setAuthenticators([
|
||||
new \Symfony\Component\Mailer\Transport\Smtp\Auth\XOAuth2Authenticator(),
|
||||
]);
|
||||
|
||||
return new \Symfony\Component\Mailer\Mailer($transport);
|
||||
}
|
||||
|
||||
public function sendReport(string $subject, string $body): bool
|
||||
{
|
||||
if (empty($this->verify_email)) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
+5
-4
@@ -197,12 +197,13 @@ class User extends Authenticatable implements CanResetPasswordContract
|
||||
|
||||
$resetUrl = url('/password/reset/' . $token);
|
||||
|
||||
$fromName = $settings->email_name ?? config('app.name');
|
||||
$fromName = preg_replace('/[^\p{L}\p{N}\s]/u', '', $fromName);
|
||||
$fromName = trim($fromName) ?: config('app.name');
|
||||
$fromAddress = new Address(
|
||||
$settings->getEffectiveFromAddress(),
|
||||
$settings->getEffectiveFromName()
|
||||
);
|
||||
|
||||
$email = (new Email())
|
||||
->from(new Address($settings->email_address, $fromName))
|
||||
->from($fromAddress)
|
||||
->to($this->email)
|
||||
->subject('Reset della password - ' . config('app.name'))
|
||||
->html(view('auth.emails.reset-password', [
|
||||
|
||||
@@ -60,7 +60,7 @@ class BackupService
|
||||
return $backups;
|
||||
}
|
||||
|
||||
public function run(): array
|
||||
public function run(array $options = []): array
|
||||
{
|
||||
$result = ['success' => true, 'message' => '', 'filename' => ''];
|
||||
|
||||
@@ -74,12 +74,28 @@ class BackupService
|
||||
$filename = $appName . '_' . $timestamp . '.zip';
|
||||
$filepath = $this->backupDir . '/' . $filename;
|
||||
|
||||
$includeFiles = $options['include_files'] ?? (bool) AppSetting::getSetting('backup_include_files', true);
|
||||
$includeEnv = $options['include_env'] ?? (bool) AppSetting::getSetting('backup_include_env', true);
|
||||
|
||||
$steps = [];
|
||||
$includedComponents = [];
|
||||
|
||||
$steps[] = $this->backupDatabase();
|
||||
$steps[] = $this->backupEnv();
|
||||
$steps[] = $this->backupFiles();
|
||||
$this->writeManifest($timestamp);
|
||||
$includedComponents[] = 'database';
|
||||
|
||||
$stepEnv = $this->backupEnv($includeEnv);
|
||||
$steps[] = $stepEnv;
|
||||
if ($includeEnv) {
|
||||
$includedComponents[] = 'env';
|
||||
}
|
||||
|
||||
$stepFiles = $this->backupFiles($includeFiles);
|
||||
$steps[] = $stepFiles;
|
||||
if ($includeFiles) {
|
||||
$includedComponents[] = 'files';
|
||||
}
|
||||
|
||||
$this->writeManifest($timestamp, $includedComponents);
|
||||
|
||||
$zipResult = $this->createZip($filepath);
|
||||
if (!$zipResult) {
|
||||
@@ -90,7 +106,7 @@ class BackupService
|
||||
|
||||
$size = file_exists($filepath) ? filesize($filepath) : 0;
|
||||
|
||||
Log::info("Backup completato: {$filename} (" . $this->formatBytes($size) . ")");
|
||||
Log::info("Backup completato: {$filename} (" . $this->formatBytes($size) . ')');
|
||||
|
||||
$this->cleanupOldBackups();
|
||||
|
||||
@@ -100,6 +116,7 @@ class BackupService
|
||||
'filename' => $filename,
|
||||
'size' => $size,
|
||||
'size_formatted' => $this->formatBytes($size),
|
||||
'steps' => $steps,
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
$this->cleanTemp();
|
||||
@@ -109,6 +126,7 @@ class BackupService
|
||||
'success' => false,
|
||||
'message' => 'Backup fallito: ' . $e->getMessage(),
|
||||
'filename' => '',
|
||||
'steps' => [],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -181,9 +199,9 @@ class BackupService
|
||||
return 'database.sql (' . $this->formatBytes(filesize($sqlFile)) . ')';
|
||||
}
|
||||
|
||||
private function backupEnv(): string
|
||||
private function backupEnv(bool $includeEnv = true): string
|
||||
{
|
||||
if (!AppSetting::getSetting('backup_include_env', true)) {
|
||||
if (!$includeEnv) {
|
||||
return '.env (escluso)';
|
||||
}
|
||||
|
||||
@@ -200,9 +218,9 @@ class BackupService
|
||||
return '.env copiato';
|
||||
}
|
||||
|
||||
private function backupFiles(): string
|
||||
private function backupFiles(bool $includeFiles = true): string
|
||||
{
|
||||
if (!AppSetting::getSetting('backup_include_files', true)) {
|
||||
if (!$includeFiles) {
|
||||
return 'files (esclusi)';
|
||||
}
|
||||
|
||||
@@ -220,7 +238,7 @@ class BackupService
|
||||
return 'files copiati';
|
||||
}
|
||||
|
||||
private function writeManifest(string $timestamp): void
|
||||
private function writeManifest(string $timestamp, array $includedComponents = ['database']): void
|
||||
{
|
||||
$manifest = [
|
||||
'created_at' => $timestamp,
|
||||
@@ -230,6 +248,7 @@ class BackupService
|
||||
'db_name' => config('database.connections.mysql.database'),
|
||||
'php_version' => PHP_VERSION,
|
||||
'laravel_version' => app()->version(),
|
||||
'included_components' => $includedComponents,
|
||||
];
|
||||
|
||||
File::put($this->tempDir . '/manifest.json', json_encode($manifest, JSON_PRETTY_PRINT));
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\GoogleOAuthConnection;
|
||||
use App\Enums\GoogleService;
|
||||
use Google_Client;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class GoogleOAuthService
|
||||
{
|
||||
private const TOKEN_CACHE_TTL = 300;
|
||||
private const MAX_RETRIES = 2;
|
||||
|
||||
public function buildClient(): Google_Client
|
||||
{
|
||||
$client = new Google_Client();
|
||||
$client->setClientId(config('services.google.client_id'));
|
||||
$client->setClientSecret(config('services.google.client_secret'));
|
||||
$client->setRedirectUri(route('google-oauth.callback'));
|
||||
$client->setAccessType('offline');
|
||||
$client->setPrompt('consent');
|
||||
$client->setIncludeGrantedScopes(true);
|
||||
|
||||
return $client;
|
||||
}
|
||||
|
||||
public function getAuthUrl(string $service): string
|
||||
{
|
||||
$serviceEnum = GoogleService::from($service);
|
||||
$client = $this->buildClient();
|
||||
$client->setScopes($serviceEnum->scopes());
|
||||
$client->setState($service);
|
||||
|
||||
return $client->createAuthUrl();
|
||||
}
|
||||
|
||||
public function handleCallback(string $authorizationCode, GoogleService $service): GoogleOAuthConnection
|
||||
{
|
||||
$client = $this->buildClient();
|
||||
$client->setScopes($service->scopes());
|
||||
$client->fetchAccessTokenWithAuthCode($authorizationCode);
|
||||
|
||||
$tokenData = $client->getAccessToken();
|
||||
|
||||
if (isset($tokenData['error'])) {
|
||||
throw new \RuntimeException('Google OAuth error: ' . ($tokenData['error_description'] ?? $tokenData['error']));
|
||||
}
|
||||
|
||||
$token = $tokenData['access_token'];
|
||||
$refreshToken = $tokenData['refresh_token'] ?? null;
|
||||
$expiresAt = now()->addSeconds($tokenData['expires_in']);
|
||||
|
||||
$email = $this->getEmailFromToken($token);
|
||||
|
||||
$connection = GoogleOAuthConnection::updateOrCreate([
|
||||
'user_id' => auth()->id(),
|
||||
'service' => $service->value,
|
||||
'email' => $email,
|
||||
], [
|
||||
'access_token' => $token,
|
||||
'refresh_token' => $refreshToken,
|
||||
'expires_at' => $expiresAt,
|
||||
]);
|
||||
|
||||
return $connection;
|
||||
}
|
||||
|
||||
public function getAccessToken(GoogleOAuthConnection $connection): string
|
||||
{
|
||||
$cacheKey = "google_oauth_token_{$connection->id}";
|
||||
|
||||
return Cache::remember($cacheKey, self::TOKEN_CACHE_TTL, function () use ($connection) {
|
||||
if ($connection->isExpired()) {
|
||||
$this->refreshToken($connection);
|
||||
}
|
||||
return $connection->access_token;
|
||||
});
|
||||
}
|
||||
|
||||
public function refreshToken(GoogleOAuthConnection $connection): void
|
||||
{
|
||||
if (!$connection->refresh_token) {
|
||||
throw new \RuntimeException('Impossibile rinnovare: nessun refresh token');
|
||||
}
|
||||
|
||||
$client = $this->buildClient();
|
||||
$client->fetchAccessTokenWithRefreshToken($connection->refresh_token);
|
||||
|
||||
$tokenData = $client->getAccessToken();
|
||||
|
||||
if (isset($tokenData['error'])) {
|
||||
$connection->delete();
|
||||
Cache::forget("google_oauth_token_{$connection->id}");
|
||||
|
||||
throw new \RuntimeException(
|
||||
'Refresh token scaduto o revocato per ' . $connection->email
|
||||
);
|
||||
}
|
||||
|
||||
$connection->access_token = $tokenData['access_token'];
|
||||
$connection->expires_at = now()->addSeconds($tokenData['expires_in']);
|
||||
|
||||
if (isset($tokenData['refresh_token'])) {
|
||||
$connection->refresh_token = $tokenData['refresh_token'];
|
||||
}
|
||||
|
||||
$connection->save();
|
||||
Cache::forget("google_oauth_token_{$connection->id}");
|
||||
}
|
||||
|
||||
public function revoke(GoogleOAuthConnection $connection): void
|
||||
{
|
||||
$client = $this->buildClient();
|
||||
$client->revokeToken($connection->access_token);
|
||||
|
||||
$connection->delete();
|
||||
Cache::forget("google_oauth_token_{$connection->id}");
|
||||
}
|
||||
|
||||
public function createXoAuth2SmtpTransport(
|
||||
string $email,
|
||||
string $accessToken
|
||||
): \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport {
|
||||
$transport = new \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport(
|
||||
'smtp.gmail.com',
|
||||
587,
|
||||
true,
|
||||
);
|
||||
|
||||
$transport->setUsername($email);
|
||||
$transport->setPassword($accessToken);
|
||||
$transport->setAuthenticators([
|
||||
new \Symfony\Component\Mailer\Transport\Smtp\Auth\XOAuth2Authenticator(),
|
||||
]);
|
||||
|
||||
return $transport;
|
||||
}
|
||||
|
||||
private function getEmailFromToken(string $accessToken): string
|
||||
{
|
||||
$client = $this->buildClient();
|
||||
$payload = $client->verifyIdToken($accessToken);
|
||||
|
||||
if ($payload && isset($payload['email'])) {
|
||||
return $payload['email'];
|
||||
}
|
||||
|
||||
$http = new \GuzzleHttp\Client();
|
||||
$response = $http->get('https://www.googleapis.com/oauth2/v1/userinfo?alt=json', [
|
||||
'headers' => ['Authorization' => "Bearer $accessToken"],
|
||||
]);
|
||||
$data = json_decode((string) $response->getBody(), true);
|
||||
|
||||
return $data['email'];
|
||||
}
|
||||
|
||||
public function getConnectionStatus(): array
|
||||
{
|
||||
$connections = GoogleOAuthConnection::where('user_id', auth()->id())->get();
|
||||
$status = [];
|
||||
|
||||
foreach (GoogleService::cases() as $service) {
|
||||
$connection = $connections->firstWhere('service', $service->value);
|
||||
$status[$service->value] = [
|
||||
'connected' => $connection !== null,
|
||||
'connection' => $connection,
|
||||
'email' => $connection?->email,
|
||||
'expired' => $connection?->isExpired() ?? false,
|
||||
];
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
}
|
||||
+3
-6
@@ -19,12 +19,9 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
'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,
|
||||
]);
|
||||
// Web middleware defaults are already applied by the framework.
|
||||
// Do NOT append EncryptCookies, StartSession, etc. here — they
|
||||
// would run twice and corrupt session cookies, causing 419 errors.
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
//
|
||||
|
||||
+227
-21
@@ -20,6 +20,223 @@ echo "==> Pulizia cache..."
|
||||
rm -rf bootstrap/cache/*.php storage/framework/cache/data/* storage/framework/sessions/* storage/framework/views/* storage/logs/* 2>/dev/null || true
|
||||
echo " OK"
|
||||
|
||||
echo "==> Generazione post-deploy.sh..."
|
||||
cat > post-deploy.sh << 'POSTDEPLOY'
|
||||
#!/bin/bash
|
||||
# post-deploy.sh — Generated by build-dist.sh
|
||||
# Usage: bash post-deploy.sh {check|upgrade|install} [--yes]
|
||||
set -u
|
||||
APP_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ARTISAN="php ${APP_DIR}/artisan"
|
||||
DIAGNOSE="php ${APP_DIR}/diagnose.php"
|
||||
RED='\033[31m'; GREEN='\033[32m'; YELLOW='\033[33m'
|
||||
CYAN='\033[36m'; BOLD='\033[1m'; NC='\033[0m'
|
||||
|
||||
MODE="${1:-check}"
|
||||
BATCH=false; [ "${2:-}" = "--yes" ] && BATCH=true
|
||||
|
||||
echo -e "${CYAN}${BOLD}============================================${NC}"
|
||||
echo -e "${CYAN}${BOLD} Glastree Post-Deploy — ${MODE}${NC}"
|
||||
echo -e "${CYAN}${BOLD}============================================${NC}"
|
||||
echo ""
|
||||
|
||||
IS_FRESH=false; [ ! -f "${APP_DIR}/.env" ] && IS_FRESH=true
|
||||
|
||||
log() { echo -e " ${BOLD}$1${NC}"; }
|
||||
ok() { echo -e " ${GREEN}✅ $1${NC}"; }
|
||||
warn() { echo -e " ${YELLOW}⚠ $1${NC}"; }
|
||||
fail() { echo -e " ${RED}❌ $1${NC}"; }
|
||||
|
||||
check_artisan() {
|
||||
if [ ! -f "${APP_DIR}/artisan" ]; then
|
||||
fail "artisan non trovato. Esegui da root dell'applicazione."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
run_step() {
|
||||
local step="$1" desc="$2"; shift 2
|
||||
log "${step} ${desc}"
|
||||
if "$@" 2>&1 | sed 's/^/ /'; then
|
||||
ok "${desc}"
|
||||
return 0
|
||||
else
|
||||
local rc=$?
|
||||
fail "${desc} — exit code ${rc}"
|
||||
return ${rc}
|
||||
fi
|
||||
}
|
||||
|
||||
run_diagnose() {
|
||||
if ${DIAGNOSE} 2>&1 | tail -5; then
|
||||
ok "Diagnostica OK"
|
||||
else
|
||||
warn "Diagnostica ha rilevato problemi (non bloccante)"
|
||||
fi
|
||||
}
|
||||
|
||||
run_check() {
|
||||
echo -e "${YELLOW}═══════════ CONTROLLO ═══════════${NC}" ; echo ""
|
||||
|
||||
echo -e "${BOLD}[0] Diagnostica sistema${NC}"
|
||||
run_diagnose ; echo ""
|
||||
|
||||
echo -e "${BOLD}[1] Ambiente${NC}"
|
||||
if $IS_FRESH; then warn ".env non trovato — fresh install necessario"
|
||||
else ok ".env presente"
|
||||
fi ; echo ""
|
||||
|
||||
echo -e "${BOLD}[2] Migration pendenti${NC}"
|
||||
if [ -f "${APP_DIR}/artisan" ]; then
|
||||
$ARTISAN migrate:status 2>&1 | grep -E '(Pending|Ran)' | head -5 || true
|
||||
PENDING=$(php -r "echo trim(shell_exec('${ARTISAN} migrate:status 2>&1 | grep -c Pending || true'));" 2>/dev/null || echo "0")
|
||||
if [ "${PENDING:-0}" -gt 0 ] 2>/dev/null; then
|
||||
warn "${PENDING} migration da eseguire"
|
||||
else
|
||||
ok "Nessuna migration pendente"
|
||||
fi
|
||||
else
|
||||
warn "artisan non disponibile — skip migration check"
|
||||
fi ; echo ""
|
||||
|
||||
echo -e "${BOLD}[3] Storage & Permessi${NC}"
|
||||
for d in storage/framework/cache/data storage/framework/sessions \
|
||||
storage/framework/views storage/logs bootstrap/cache \
|
||||
storage/app/public storage/app/backups; do
|
||||
p="${APP_DIR}/${d}"
|
||||
if [ -d "$p" ]; then
|
||||
[ -w "$p" ] && ok "${d}" || warn "${d} — non scrivibile"
|
||||
else
|
||||
warn "${d} — mancante"
|
||||
fi
|
||||
done ; echo ""
|
||||
|
||||
echo -e "${BOLD}[4] Symlink storage${NC}"
|
||||
if [ -L "${APP_DIR}/public/storage" ]; then
|
||||
ok "public/storage → $(readlink "${APP_DIR}/public/storage")"
|
||||
else
|
||||
warn "public/storage non presente"
|
||||
fi ; echo ""
|
||||
|
||||
echo -e "${BOLD}[5] Cache${NC}"
|
||||
for d in bootstrap/cache storage/framework/cache/data \
|
||||
storage/framework/sessions storage/framework/views; do
|
||||
c=$(find "${APP_DIR}/${d}" -type f 2>/dev/null | wc -l)
|
||||
[ "$c" -gt 0 ] && warn "${d}: ${c} file" || ok "${d}: vuoto"
|
||||
done
|
||||
echo "" ; echo -e "${YELLOW}═══════════ FINE CONTROLLO ═══════════${NC}"
|
||||
}
|
||||
|
||||
run_upgrade() {
|
||||
echo -e "${YELLOW}═══════════ UPGRADE ═══════════${NC}" ; check_artisan
|
||||
|
||||
run_step "[1/9]" "Directory storage" mkdir -p \
|
||||
"${APP_DIR}/storage/framework/cache/data" \
|
||||
"${APP_DIR}/storage/framework/sessions" \
|
||||
"${APP_DIR}/storage/framework/views" \
|
||||
"${APP_DIR}/storage/logs" "${APP_DIR}/bootstrap/cache" \
|
||||
"${APP_DIR}/storage/app/public" \
|
||||
"${APP_DIR}/storage/app/public/logos" \
|
||||
"${APP_DIR}/storage/app/public/documenti/eventi" \
|
||||
"${APP_DIR}/storage/app/backups" \
|
||||
"${APP_DIR}/storage/app/documenti" \
|
||||
"${APP_DIR}/storage/app/private/avatars/gruppi" || true
|
||||
|
||||
run_step "[2/9]" ".gitignore storage" bash -c \
|
||||
"echo '*'>'${APP_DIR}/storage/app/public/.gitignore'; echo '!.gitignore'>>'${APP_DIR}/storage/app/public/.gitignore'" || true
|
||||
|
||||
run_step "[3/9]" "Permessi" chmod -R 775 \
|
||||
"${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true
|
||||
|
||||
run_step "[4/9]" "Symlink storage" bash -c \
|
||||
"rm -f '${APP_DIR}/public/storage'; ln -sf '../storage/app/public' '${APP_DIR}/public/storage'" || true
|
||||
|
||||
run_step "[5/10]" "Migrazioni DB" $ARTISAN migrate --force || true
|
||||
|
||||
run_step "[6/10]" "Seed diocesi" $ARTISAN db:seed --class=DiocesiSeeder --force || true
|
||||
|
||||
run_step "[7/10]" "Dump autoload" composer dump-autoload --no-dev --optimize || true
|
||||
|
||||
run_step "[8/10]" "Key generate" $ARTISAN key:generate --force || true
|
||||
|
||||
run_step "[9/10]" "Pulizia cache" bash -c \
|
||||
"$ARTISAN config:clear && $ARTISAN route:clear && $ARTISAN view:clear" || true
|
||||
|
||||
run_step "[10/10]" "Verifica finale" $DIAGNOSE || true
|
||||
|
||||
echo "" ; echo -e "${GREEN}${BOLD}✅ Upgrade completato.${NC}"
|
||||
}
|
||||
|
||||
run_install() {
|
||||
echo -e "${YELLOW}═══════════ INSTALL (fresh) ═══════════${NC}" ; check_artisan
|
||||
|
||||
run_step "[1/10]" "Directory storage" mkdir -p \
|
||||
"${APP_DIR}/storage/framework/cache/data" \
|
||||
"${APP_DIR}/storage/framework/sessions" \
|
||||
"${APP_DIR}/storage/framework/views" \
|
||||
"${APP_DIR}/storage/logs" "${APP_DIR}/bootstrap/cache" \
|
||||
"${APP_DIR}/storage/app/public" \
|
||||
"${APP_DIR}/storage/app/public/logos" \
|
||||
"${APP_DIR}/storage/app/public/documenti/eventi" \
|
||||
"${APP_DIR}/storage/app/backups" \
|
||||
"${APP_DIR}/storage/app/documenti" \
|
||||
"${APP_DIR}/storage/app/private/avatars/gruppi" || true
|
||||
|
||||
run_step "[2/10]" ".gitignore storage" bash -c \
|
||||
"echo '*'>'${APP_DIR}/storage/app/public/.gitignore'; echo '!.gitignore'>>'${APP_DIR}/storage/app/public/.gitignore'" || true
|
||||
|
||||
run_step "[3/10]" "Permessi" chmod -R 775 \
|
||||
"${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true
|
||||
|
||||
run_step "[4/10]" "File .env" cp "${APP_DIR}/.env.example" "${APP_DIR}/.env" || true
|
||||
warn "MODIFICA .env con i tuoi dati DB/URL prima di proseguire"
|
||||
if ! $BATCH; then read -p " Fatto? (Invio per continuare) " _; fi
|
||||
|
||||
run_step "[5/10]" "Symlink storage" bash -c \
|
||||
"rm -f '${APP_DIR}/public/storage'; ln -sf '../storage/app/public' '${APP_DIR}/public/storage'" || true
|
||||
|
||||
run_step "[6/10]" "Dump autoload" composer dump-autoload --no-dev --optimize || true
|
||||
|
||||
run_step "[7/10]" "Key generate" $ARTISAN key:generate --force || true
|
||||
|
||||
run_step "[8/11]" "Migrazioni DB" $ARTISAN migrate --force || true
|
||||
|
||||
run_step "[9/11]" "Seed diocesi" $ARTISAN db:seed --class=DiocesiSeeder --force || true
|
||||
|
||||
run_step "[10/11]" "Pulizia cache" bash -c \
|
||||
"$ARTISAN config:clear && $ARTISAN route:clear && $ARTISAN view:clear" || true
|
||||
|
||||
run_step "[11/11]" "Verifica finale" $DIAGNOSE || true
|
||||
|
||||
echo "" ; echo -e "${GREEN}${BOLD}✅ Installazione completata.${NC}"
|
||||
}
|
||||
|
||||
case "$MODE" in
|
||||
check) run_check ;;
|
||||
upgrade)
|
||||
run_check ; echo ""
|
||||
if $BATCH; then run_upgrade
|
||||
else
|
||||
read -p "Procedere con l'upgrade? (s/N) " c
|
||||
[ "$c" = "s" ] || [ "$c" = "S" ] && run_upgrade || echo "Annullato."
|
||||
fi
|
||||
;;
|
||||
install) run_install ;;
|
||||
*)
|
||||
echo "Uso: bash post-deploy.sh {check|upgrade|install} [--yes]"
|
||||
echo " check — diagnostica (nessuna modifica)"
|
||||
echo " upgrade — aggiorna installazione esistente"
|
||||
echo " install — fresh install"
|
||||
echo " --yes — batch (nessuna conferma)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
POSTDEPLOY
|
||||
|
||||
chmod +x post-deploy.sh
|
||||
|
||||
echo " OK"
|
||||
|
||||
echo "==> Creazione archivio: ${ARCHIVE}"
|
||||
|
||||
tar czf "${ARCHIVE}" \
|
||||
@@ -60,6 +277,8 @@ tar czf "${ARCHIVE}" \
|
||||
--warning=no-file-changed \
|
||||
.
|
||||
|
||||
rm -f post-deploy.sh
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Archivio creato: ${ARCHIVE}"
|
||||
@@ -70,27 +289,14 @@ echo "Per estrarre sul server di destinazione:"
|
||||
echo " tar xzf ${ARCHIVE}"
|
||||
echo ""
|
||||
echo "Poi esegui:"
|
||||
echo " # 1. Crea directory necessarie (se non esistono già)"
|
||||
echo ' mkdir -p storage/framework/cache/data storage/framework/sessions storage/framework/views storage/logs bootstrap/cache storage/app/public storage/app/public/logos storage/app/public/documenti/eventi storage/app/backups storage/app/documenti storage/app/private/avatars/gruppi'
|
||||
echo " # Upgrade di un'installazione esistente:"
|
||||
echo " bash post-deploy.sh upgrade"
|
||||
echo ""
|
||||
echo " # 2. Crea file .gitignore in storage/app/public (serve a Laravel)"
|
||||
echo ' echo "*" > storage/app/public/.gitignore'
|
||||
echo ' echo "!.gitignore" >> storage/app/public/.gitignore'
|
||||
echo " # Fresh install:"
|
||||
echo " bash post-deploy.sh install"
|
||||
echo ""
|
||||
echo " # 3. Permessi (web server = www-data)"
|
||||
echo ' chmod -R 775 storage bootstrap/cache'
|
||||
echo ' chown -R www-data:www-data storage bootstrap/cache'
|
||||
echo " # Solo controllo (nessuna modifica):"
|
||||
echo " bash post-deploy.sh check"
|
||||
echo ""
|
||||
echo " # 4. Configura ambiente"
|
||||
echo " cp .env.example .env # modifica DB, APP_URL, etc."
|
||||
echo " php artisan key:generate"
|
||||
echo ""
|
||||
echo " # 5. Pulisci cache e ricrea symlink storage (relativo per portabilità)"
|
||||
echo ' rm -f public/storage'
|
||||
echo ' ln -sf ../storage/app/public public/storage'
|
||||
echo " php artisan config:clear"
|
||||
echo " php artisan route:clear"
|
||||
echo " php artisan view:clear"
|
||||
echo ""
|
||||
echo " # 6. Avvia installazione MySQL"
|
||||
echo " php install.php"
|
||||
echo " # Batch mode (nessuna conferma):"
|
||||
echo " bash post-deploy.sh upgrade --yes"
|
||||
|
||||
@@ -35,4 +35,10 @@ return [
|
||||
],
|
||||
],
|
||||
|
||||
'google' => [
|
||||
'client_id' => env('GOOGLE_CLIENT_ID'),
|
||||
'client_secret' => env('GOOGLE_CLIENT_SECRET'),
|
||||
'redirect' => env('GOOGLE_REDIRECT_URI', config('app.url') . '/auth/google/callback'),
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -8,6 +8,10 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasTable('tenants')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::create('tenants', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nome');
|
||||
|
||||
@@ -8,26 +8,30 @@ 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();
|
||||
});
|
||||
if (!Schema::hasTable('diocesi')) {
|
||||
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');
|
||||
});
|
||||
if (!Schema::hasTable('comuni')) {
|
||||
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
|
||||
|
||||
@@ -8,27 +8,29 @@ 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');
|
||||
});
|
||||
if (!Schema::hasTable('individui')) {
|
||||
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
|
||||
|
||||
@@ -8,16 +8,18 @@ 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');
|
||||
});
|
||||
if (!Schema::hasTable('contatti')) {
|
||||
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
|
||||
|
||||
@@ -8,24 +8,26 @@ 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');
|
||||
});
|
||||
if (!Schema::hasTable('gruppi')) {
|
||||
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
|
||||
|
||||
@@ -8,15 +8,17 @@ 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']);
|
||||
});
|
||||
if (!Schema::hasTable('gruppo_individuo')) {
|
||||
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
|
||||
|
||||
@@ -8,35 +8,41 @@ 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');
|
||||
});
|
||||
if (!Schema::hasTable('eventi')) {
|
||||
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();
|
||||
});
|
||||
if (!Schema::hasTable('eventi_gruppi')) {
|
||||
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();
|
||||
});
|
||||
if (!Schema::hasTable('eventi_responsabili')) {
|
||||
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
|
||||
|
||||
@@ -8,22 +8,24 @@ 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');
|
||||
});
|
||||
if (!Schema::hasTable('documenti')) {
|
||||
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')->nullable();
|
||||
$table->string('tipo')->nullable();
|
||||
$table->string('tipologia', 50)->default('documento');
|
||||
$table->string('visibilita', 50)->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
|
||||
|
||||
@@ -8,43 +8,49 @@ 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');
|
||||
});
|
||||
if (!Schema::hasTable('mailing_lists')) {
|
||||
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']);
|
||||
});
|
||||
if (!Schema::hasTable('mailing_contacts')) {
|
||||
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');
|
||||
});
|
||||
if (!Schema::hasTable('mailing_messaggi')) {
|
||||
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
|
||||
|
||||
@@ -8,18 +8,20 @@ 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');
|
||||
});
|
||||
if (!Schema::hasTable('notifiche')) {
|
||||
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
|
||||
|
||||
@@ -8,34 +8,40 @@ 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');
|
||||
});
|
||||
if (!Schema::hasTable('users')) {
|
||||
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();
|
||||
});
|
||||
if (!Schema::hasTable('password_reset_tokens')) {
|
||||
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();
|
||||
});
|
||||
if (!Schema::hasTable('sessions')) {
|
||||
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
|
||||
|
||||
@@ -8,50 +8,60 @@ 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');
|
||||
});
|
||||
if (!Schema::hasTable('cache')) {
|
||||
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');
|
||||
});
|
||||
if (!Schema::hasTable('cache_locks')) {
|
||||
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');
|
||||
});
|
||||
if (!Schema::hasTable('jobs')) {
|
||||
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();
|
||||
});
|
||||
if (!Schema::hasTable('job_batches')) {
|
||||
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');
|
||||
});
|
||||
if (!Schema::hasTable('failed_jobs')) {
|
||||
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
|
||||
|
||||
@@ -8,12 +8,14 @@ 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();
|
||||
});
|
||||
if (!Schema::hasTable('eventi_documenti')) {
|
||||
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
|
||||
|
||||
@@ -8,19 +8,21 @@ 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');
|
||||
});
|
||||
if (!Schema::hasTable('viste_report')) {
|
||||
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
|
||||
|
||||
@@ -8,25 +8,27 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('tipologie_documenti', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nome', 50)->unique();
|
||||
$table->string('descrizione', 255)->nullable();
|
||||
$table->integer('ordine')->default(0);
|
||||
$table->boolean('attiva')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
if (!Schema::hasTable('tipologie_documenti')) {
|
||||
Schema::create('tipologie_documenti', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nome', 50)->unique();
|
||||
$table->string('descrizione', 255)->nullable();
|
||||
$table->integer('ordine')->default(0);
|
||||
$table->boolean('attiva')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
$defaultTipologie = ['avatar', 'galleria', 'documento', 'statuto', 'altro'];
|
||||
foreach ($defaultTipologie as $i => $nome) {
|
||||
DB::table('tipologie_documenti')->insert([
|
||||
'nome' => $nome,
|
||||
'descrizione' => ucfirst($nome),
|
||||
'ordine' => $i,
|
||||
'attiva' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
$defaultTipologie = ['avatar', 'galleria', 'documento', 'statuto', 'altro'];
|
||||
foreach ($defaultTipologie as $i => $nome) {
|
||||
DB::table('tipologie_documenti')->insert([
|
||||
'nome' => $nome,
|
||||
'descrizione' => ucfirst($nome),
|
||||
'ordine' => $i,
|
||||
'attiva' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,28 +14,32 @@ return new class extends Migration
|
||||
});
|
||||
}
|
||||
|
||||
Schema::create('activity_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
$table->string('action');
|
||||
$table->string('module');
|
||||
$table->string('description')->nullable();
|
||||
$table->json('details')->nullable();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
if (!Schema::hasTable('activity_logs')) {
|
||||
Schema::create('activity_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
$table->string('action');
|
||||
$table->string('module');
|
||||
$table->string('description')->nullable();
|
||||
$table->json('details')->nullable();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
|
||||
$table->index(['user_id', 'module']);
|
||||
$table->index(['created_at']);
|
||||
$table->index(['action']);
|
||||
});
|
||||
}
|
||||
|
||||
$table->index(['user_id', 'module']);
|
||||
$table->index(['created_at']);
|
||||
$table->index(['action']);
|
||||
});
|
||||
|
||||
Schema::create('role_presets', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name', 50)->unique();
|
||||
$table->string('description')->nullable();
|
||||
$table->json('permissions');
|
||||
$table->timestamps();
|
||||
});
|
||||
if (!Schema::hasTable('role_presets')) {
|
||||
Schema::create('role_presets', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name', 50)->unique();
|
||||
$table->string('description')->nullable();
|
||||
$table->json('permissions');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
|
||||
@@ -23,96 +23,106 @@ return new class extends Migration
|
||||
/**
|
||||
* See `docs/prerequisites.md` for suggested lengths on 'name' and 'guard_name' if "1071 Specified key was too long" errors are encountered.
|
||||
*/
|
||||
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
|
||||
$table->id(); // permission id
|
||||
$table->string('name');
|
||||
$table->string('guard_name');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['name', 'guard_name']);
|
||||
});
|
||||
if (!Schema::hasTable($tableNames['permissions'])) {
|
||||
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
|
||||
$table->id(); // permission id
|
||||
$table->string('name');
|
||||
$table->string('guard_name');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['name', 'guard_name']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* See `docs/prerequisites.md` for suggested lengths on 'name' and 'guard_name' if "1071 Specified key was too long" errors are encountered.
|
||||
*/
|
||||
Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
|
||||
$table->id(); // role id
|
||||
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
|
||||
if (!Schema::hasTable($tableNames['roles'])) {
|
||||
Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
|
||||
$table->id(); // role id
|
||||
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
|
||||
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
|
||||
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
|
||||
}
|
||||
$table->string('name');
|
||||
$table->string('guard_name');
|
||||
$table->timestamps();
|
||||
if ($teams || config('permission.testing')) {
|
||||
}
|
||||
$table->string('name');
|
||||
$table->string('guard_name');
|
||||
$table->timestamps();
|
||||
if ($teams || config('permission.testing')) {
|
||||
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
|
||||
} else {
|
||||
} else {
|
||||
$table->unique(['name', 'guard_name']);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
|
||||
$table->unsignedBigInteger($pivotPermission);
|
||||
|
||||
$table->string('model_type');
|
||||
$table->unsignedBigInteger($columnNames['model_morph_key']);
|
||||
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
|
||||
|
||||
$table->foreign($pivotPermission)
|
||||
if (!Schema::hasTable($tableNames['model_has_permissions'])) {
|
||||
Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
|
||||
$table->unsignedBigInteger($pivotPermission);
|
||||
|
||||
$table->string('model_type');
|
||||
$table->unsignedBigInteger($columnNames['model_morph_key']);
|
||||
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
|
||||
|
||||
$table->foreign($pivotPermission)
|
||||
->references('id') // permission id
|
||||
->on($tableNames['permissions'])
|
||||
->cascadeOnDelete();
|
||||
if ($teams) {
|
||||
if ($teams) {
|
||||
$table->unsignedBigInteger($columnNames['team_foreign_key']);
|
||||
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
|
||||
|
||||
|
||||
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
|
||||
'model_has_permissions_permission_model_type_primary');
|
||||
} else {
|
||||
} else {
|
||||
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
|
||||
'model_has_permissions_permission_model_type_primary');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
|
||||
$table->unsignedBigInteger($pivotRole);
|
||||
|
||||
$table->string('model_type');
|
||||
$table->unsignedBigInteger($columnNames['model_morph_key']);
|
||||
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
|
||||
|
||||
$table->foreign($pivotRole)
|
||||
if (!Schema::hasTable($tableNames['model_has_roles'])) {
|
||||
Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
|
||||
$table->unsignedBigInteger($pivotRole);
|
||||
|
||||
$table->string('model_type');
|
||||
$table->unsignedBigInteger($columnNames['model_morph_key']);
|
||||
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
|
||||
|
||||
$table->foreign($pivotRole)
|
||||
->references('id') // role id
|
||||
->on($tableNames['roles'])
|
||||
->cascadeOnDelete();
|
||||
if ($teams) {
|
||||
if ($teams) {
|
||||
$table->unsignedBigInteger($columnNames['team_foreign_key']);
|
||||
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
|
||||
|
||||
|
||||
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
|
||||
'model_has_roles_role_model_type_primary');
|
||||
} else {
|
||||
} else {
|
||||
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'],
|
||||
'model_has_roles_role_model_type_primary');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
|
||||
$table->unsignedBigInteger($pivotPermission);
|
||||
$table->unsignedBigInteger($pivotRole);
|
||||
|
||||
$table->foreign($pivotPermission)
|
||||
if (!Schema::hasTable($tableNames['role_has_permissions'])) {
|
||||
Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
|
||||
$table->unsignedBigInteger($pivotPermission);
|
||||
$table->unsignedBigInteger($pivotRole);
|
||||
|
||||
$table->foreign($pivotPermission)
|
||||
->references('id') // permission id
|
||||
->on($tableNames['permissions'])
|
||||
->cascadeOnDelete();
|
||||
|
||||
$table->foreign($pivotRole)
|
||||
|
||||
$table->foreign($pivotRole)
|
||||
->references('id') // role id
|
||||
->on($tableNames['roles'])
|
||||
->cascadeOnDelete();
|
||||
|
||||
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
|
||||
});
|
||||
|
||||
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
|
||||
});
|
||||
}
|
||||
|
||||
app('cache')
|
||||
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
|
||||
|
||||
@@ -8,26 +8,28 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('email_settings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('imap_host')->nullable();
|
||||
$table->integer('imap_port')->nullable()->default(993);
|
||||
$table->string('imap_encryption')->nullable()->default('ssl');
|
||||
$table->string('imap_username')->nullable();
|
||||
$table->string('imap_password')->nullable();
|
||||
$table->string('smtp_host')->nullable();
|
||||
$table->integer('smtp_port')->nullable()->default(587);
|
||||
$table->string('smtp_encryption')->nullable()->default('tls');
|
||||
$table->string('smtp_username')->nullable();
|
||||
$table->string('smtp_password')->nullable();
|
||||
$table->string('email_address')->nullable();
|
||||
$table->string('email_name')->nullable();
|
||||
$table->string('reply_to')->nullable();
|
||||
$table->integer('sync_interval_minutes')->default(5);
|
||||
$table->timestamp('last_sync_at')->nullable();
|
||||
$table->boolean('is_active')->default(false);
|
||||
$table->timestamps();
|
||||
});
|
||||
if (!Schema::hasTable('email_settings')) {
|
||||
Schema::create('email_settings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('imap_host')->nullable();
|
||||
$table->integer('imap_port')->nullable()->default(993);
|
||||
$table->string('imap_encryption')->nullable()->default('ssl');
|
||||
$table->string('imap_username')->nullable();
|
||||
$table->string('imap_password')->nullable();
|
||||
$table->string('smtp_host')->nullable();
|
||||
$table->integer('smtp_port')->nullable()->default(587);
|
||||
$table->string('smtp_encryption')->nullable()->default('tls');
|
||||
$table->string('smtp_username')->nullable();
|
||||
$table->string('smtp_password')->nullable();
|
||||
$table->string('email_address')->nullable();
|
||||
$table->string('email_name')->nullable();
|
||||
$table->string('reply_to')->nullable();
|
||||
$table->integer('sync_interval_minutes')->default(5);
|
||||
$table->timestamp('last_sync_at')->nullable();
|
||||
$table->boolean('is_active')->default(false);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
|
||||
@@ -8,17 +8,19 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('email_folders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('imap_folder_name')->nullable();
|
||||
$table->string('type')->default('custom');
|
||||
$table->string('icon')->default('fa-folder');
|
||||
$table->string('color')->nullable();
|
||||
$table->integer('sort_order')->default(0);
|
||||
$table->boolean('is_system')->default(false);
|
||||
$table->timestamps();
|
||||
});
|
||||
if (!Schema::hasTable('email_folders')) {
|
||||
Schema::create('email_folders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('imap_folder_name')->nullable();
|
||||
$table->string('type')->default('custom');
|
||||
$table->string('icon')->default('fa-folder');
|
||||
$table->string('color')->nullable();
|
||||
$table->integer('sort_order')->default(0);
|
||||
$table->boolean('is_system')->default(false);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
|
||||
@@ -8,35 +8,37 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('email_messages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('email_folder_id')->constrained('email_folders')->onDelete('cascade');
|
||||
$table->string('message_id')->nullable()->index();
|
||||
$table->string('subject')->nullable();
|
||||
$table->string('from_name')->nullable();
|
||||
$table->string('from_email')->nullable();
|
||||
$table->string('to_name')->nullable();
|
||||
$table->string('to_email')->nullable();
|
||||
$table->text('cc')->nullable();
|
||||
$table->text('bcc')->nullable();
|
||||
$table->longText('body_text')->nullable();
|
||||
$table->longText('body_html')->nullable();
|
||||
$table->boolean('is_read')->default(false);
|
||||
$table->boolean('is_starred')->default(false);
|
||||
$table->boolean('is_important')->default(false);
|
||||
$table->boolean('is_sent')->default(false);
|
||||
$table->boolean('is_draft')->default(false);
|
||||
$table->boolean('is_trash')->default(false);
|
||||
$table->timestamp('received_at')->nullable();
|
||||
$table->timestamp('sent_at')->nullable();
|
||||
$table->unsignedBigInteger('imap_uid')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('is_read');
|
||||
$table->index('is_starred');
|
||||
$table->index('is_sent');
|
||||
$table->index('received_at');
|
||||
});
|
||||
if (!Schema::hasTable('email_messages')) {
|
||||
Schema::create('email_messages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('email_folder_id')->constrained('email_folders')->onDelete('cascade');
|
||||
$table->string('message_id')->nullable()->index();
|
||||
$table->string('subject')->nullable();
|
||||
$table->string('from_name')->nullable();
|
||||
$table->string('from_email')->nullable();
|
||||
$table->string('to_name')->nullable();
|
||||
$table->string('to_email')->nullable();
|
||||
$table->text('cc')->nullable();
|
||||
$table->text('bcc')->nullable();
|
||||
$table->longText('body_text')->nullable();
|
||||
$table->longText('body_html')->nullable();
|
||||
$table->boolean('is_read')->default(false);
|
||||
$table->boolean('is_starred')->default(false);
|
||||
$table->boolean('is_important')->default(false);
|
||||
$table->boolean('is_sent')->default(false);
|
||||
$table->boolean('is_draft')->default(false);
|
||||
$table->boolean('is_trash')->default(false);
|
||||
$table->timestamp('received_at')->nullable();
|
||||
$table->timestamp('sent_at')->nullable();
|
||||
$table->unsignedBigInteger('imap_uid')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('is_read');
|
||||
$table->index('is_starred');
|
||||
$table->index('is_sent');
|
||||
$table->index('received_at');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('documenti', function (Blueprint $table) {
|
||||
$table->enum('tipologia', ['avatar', 'galleria', 'documento', 'statuto', 'altro', 'email_attachment'])->default('documento')->change();
|
||||
});
|
||||
// Column type changed to string in 2024_01_01_000008_create_documenti_table.php.
|
||||
// Application-level validation handles allowed values.
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('documenti', function (Blueprint $table) {
|
||||
$table->enum('tipologia', ['avatar', 'galleria', 'documento', 'statuto', 'altro'])->default('documento')->change();
|
||||
});
|
||||
// No-op: column is string, not enum.
|
||||
}
|
||||
};
|
||||
@@ -7,14 +7,15 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('tipologie_documenti')->insert([
|
||||
'nome' => 'email_attachment',
|
||||
'descrizione' => 'Allegato email',
|
||||
'ordine' => 100,
|
||||
'attiva' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
DB::table('tipologie_documenti')->updateOrInsert(
|
||||
['nome' => 'email_attachment'],
|
||||
[
|
||||
'descrizione' => 'Allegato email',
|
||||
'ordine' => 100,
|
||||
'attiva' => true,
|
||||
'updated_at' => now(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
|
||||
@@ -8,17 +8,19 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('email_attachments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('email_message_id')->constrained('email_messages')->onDelete('cascade');
|
||||
$table->string('filename');
|
||||
$table->string('mime_type')->nullable();
|
||||
$table->bigInteger('size')->default(0);
|
||||
$table->string('file_path');
|
||||
$table->boolean('is_saved_to_documenti')->default(false);
|
||||
$table->foreignId('documenti_id')->nullable()->constrained('documenti')->onDelete('set null');
|
||||
$table->timestamps();
|
||||
});
|
||||
if (!Schema::hasTable('email_attachments')) {
|
||||
Schema::create('email_attachments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('email_message_id')->constrained('email_messages')->onDelete('cascade');
|
||||
$table->string('filename');
|
||||
$table->string('mime_type')->nullable();
|
||||
$table->bigInteger('size')->default(0);
|
||||
$table->string('file_path');
|
||||
$table->boolean('is_saved_to_documenti')->default(false);
|
||||
$table->foreignId('documenti_id')->nullable()->constrained('documenti')->onDelete('set null');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
|
||||
@@ -8,34 +8,36 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('ruoli', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nome', 50)->unique();
|
||||
$table->string('descrizione', 255)->nullable();
|
||||
$table->integer('ordine')->default(0);
|
||||
$table->boolean('attiva')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
if (!Schema::hasTable('ruoli')) {
|
||||
Schema::create('ruoli', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nome', 50)->unique();
|
||||
$table->string('descrizione', 255)->nullable();
|
||||
$table->integer('ordine')->default(0);
|
||||
$table->boolean('attiva')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
$defaultRuoli = [
|
||||
['nome' => 'Membro', 'descrizione' => 'Membro del gruppo'],
|
||||
['nome' => 'Catechista', 'descrizione' => 'Catechista del gruppo'],
|
||||
['nome' => 'Animatore', 'descrizione' => 'Animatore del gruppo'],
|
||||
['nome' => 'Responsabile', 'descrizione' => 'Responsabile del gruppo'],
|
||||
['nome' => 'Vice Responsabile', 'descrizione' => 'Vice responsabile del gruppo'],
|
||||
['nome' => 'Segretario', 'descrizione' => 'Segretario del gruppo'],
|
||||
['nome' => 'Tesoriere', 'descrizione' => 'Tesoriere del gruppo'],
|
||||
];
|
||||
$defaultRuoli = [
|
||||
['nome' => 'Membro', 'descrizione' => 'Membro del gruppo'],
|
||||
['nome' => 'Catechista', 'descrizione' => 'Catechista del gruppo'],
|
||||
['nome' => 'Animatore', 'descrizione' => 'Animatore del gruppo'],
|
||||
['nome' => 'Responsabile', 'descrizione' => 'Responsabile del gruppo'],
|
||||
['nome' => 'Vice Responsabile', 'descrizione' => 'Vice responsabile del gruppo'],
|
||||
['nome' => 'Segretario', 'descrizione' => 'Segretario del gruppo'],
|
||||
['nome' => 'Tesoriere', 'descrizione' => 'Tesoriere del gruppo'],
|
||||
];
|
||||
|
||||
foreach ($defaultRuoli as $i => $ruolo) {
|
||||
DB::table('ruoli')->insert([
|
||||
'nome' => $ruolo['nome'],
|
||||
'descrizione' => $ruolo['descrizione'],
|
||||
'ordine' => $i,
|
||||
'attiva' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
foreach ($defaultRuoli as $i => $ruolo) {
|
||||
DB::table('ruoli')->insert([
|
||||
'nome' => $ruolo['nome'],
|
||||
'descrizione' => $ruolo['descrizione'],
|
||||
'ordine' => $i,
|
||||
'attiva' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('gruppo_individuo', 'ruolo_id')) {
|
||||
|
||||
@@ -8,10 +8,13 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('gruppo_individuo', function (Blueprint $table) {
|
||||
$table->dropForeign(['ruolo_id']);
|
||||
$table->dropColumn('ruolo_id');
|
||||
});
|
||||
if (Schema::hasColumn('gruppo_individuo', 'ruolo_id')) {
|
||||
Schema::table('gruppo_individuo', function (Blueprint $table) {
|
||||
$table->dropIndex(['ruolo_id']);
|
||||
$table->dropForeign(['ruolo_id']);
|
||||
$table->dropColumn('ruolo_id');
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('gruppo_individuo', 'ruolo_ids')) {
|
||||
Schema::table('gruppo_individuo', function (Blueprint $table) {
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
DB::statement("ALTER TABLE documenti MODIFY COLUMN tipologia ENUM('avatar', 'galleria', 'documento', 'statuto', 'altro', 'programma', 'locandina') DEFAULT 'documento'");
|
||||
// Column type changed to string in 2024_01_01_000008_create_documenti_table.php.
|
||||
// Application-level validation handles allowed values.
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::statement("ALTER TABLE documenti MODIFY COLUMN tipologia ENUM('avatar', 'galleria', 'documento', 'statuto', 'altro') DEFAULT 'documento'");
|
||||
// No-op: column is string, not enum.
|
||||
}
|
||||
};
|
||||
@@ -8,15 +8,17 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('report_custom', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
|
||||
$table->string('nome');
|
||||
$table->text('descrizione')->nullable();
|
||||
$table->string('tipo_report');
|
||||
$table->json('config')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
if (!Schema::hasTable('report_custom')) {
|
||||
Schema::create('report_custom', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
|
||||
$table->string('nome');
|
||||
$table->text('descrizione')->nullable();
|
||||
$table->string('tipo_report');
|
||||
$table->json('config')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
|
||||
@@ -10,25 +10,27 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('tipologie_eventi', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nome', 50)->unique();
|
||||
$table->string('descrizione', 255)->nullable();
|
||||
$table->integer('ordine')->default(0);
|
||||
$table->boolean('attiva')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
if (!Schema::hasTable('tipologie_eventi')) {
|
||||
Schema::create('tipologie_eventi', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nome', 50)->unique();
|
||||
$table->string('descrizione', 255)->nullable();
|
||||
$table->integer('ordine')->default(0);
|
||||
$table->boolean('attiva')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
DB::table('tipologie_eventi')->insert([
|
||||
['nome' => 'catechesi', 'descrizione' => 'Catechesi', 'ordine' => 0, 'attiva' => true],
|
||||
['nome' => 'liturgia', 'descrizione' => 'Liturgia', 'ordine' => 1, 'attiva' => true],
|
||||
['nome' => 'animazione', 'descrizione' => 'Animazione', 'ordine' => 2, 'attiva' => true],
|
||||
['nome' => 'formazione', 'descrizione' => 'Formazione', 'ordine' => 3, 'attiva' => true],
|
||||
['nome' => 'incontro', 'descrizione' => 'Incontro', 'ordine' => 4, 'attiva' => true],
|
||||
['nome' => 'riunione', 'descrizione' => 'Riunione', 'ordine' => 5, 'attiva' => true],
|
||||
['nome' => 'festa', 'descrizione' => 'Festa / Sagra', 'ordine' => 6, 'attiva' => true],
|
||||
['nome' => 'altro', 'descrizione' => 'Altro', 'ordine' => 7, 'attiva' => true],
|
||||
]);
|
||||
DB::table('tipologie_eventi')->insert([
|
||||
['nome' => 'catechesi', 'descrizione' => 'Catechesi', 'ordine' => 0, 'attiva' => true],
|
||||
['nome' => 'liturgia', 'descrizione' => 'Liturgia', 'ordine' => 1, 'attiva' => true],
|
||||
['nome' => 'animazione', 'descrizione' => 'Animazione', 'ordine' => 2, 'attiva' => true],
|
||||
['nome' => 'formazione', 'descrizione' => 'Formazione', 'ordine' => 3, 'attiva' => true],
|
||||
['nome' => 'incontro', 'descrizione' => 'Incontro', 'ordine' => 4, 'attiva' => true],
|
||||
['nome' => 'riunione', 'descrizione' => 'Riunione', 'ordine' => 5, 'attiva' => true],
|
||||
['nome' => 'festa', 'descrizione' => 'Festa / Sagra', 'ordine' => 6, 'attiva' => true],
|
||||
['nome' => 'altro', 'descrizione' => 'Altro', 'ordine' => 7, 'attiva' => true],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
|
||||
@@ -10,21 +10,23 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('sender_accounts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('email_address')->unique();
|
||||
$table->string('email_name')->nullable();
|
||||
$table->string('smtp_host')->nullable();
|
||||
$table->integer('smtp_port')->default(587);
|
||||
$table->string('smtp_encryption')->default('tls');
|
||||
$table->string('smtp_username')->nullable();
|
||||
$table->text('smtp_password')->nullable();
|
||||
$table->string('reply_to')->nullable();
|
||||
$table->string('verify_email')->nullable();
|
||||
$table->text('note')->nullable();
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
if (!Schema::hasTable('sender_accounts')) {
|
||||
Schema::create('sender_accounts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('email_address')->unique();
|
||||
$table->string('email_name')->nullable();
|
||||
$table->string('smtp_host')->nullable();
|
||||
$table->integer('smtp_port')->default(587);
|
||||
$table->string('smtp_encryption')->default('tls');
|
||||
$table->string('smtp_username')->nullable();
|
||||
$table->text('smtp_password')->nullable();
|
||||
$table->string('reply_to')->nullable();
|
||||
$table->string('verify_email')->nullable();
|
||||
$table->text('note')->nullable();
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
|
||||
@@ -8,12 +8,14 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('documenti_cartelle', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('parent_id')->nullable()->constrained('documenti_cartelle')->onDelete('cascade');
|
||||
$table->string('nome');
|
||||
$table->timestamps();
|
||||
});
|
||||
if (!Schema::hasTable('documenti_cartelle')) {
|
||||
Schema::create('documenti_cartelle', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('parent_id')->nullable()->constrained('documenti_cartelle')->onDelete('cascade');
|
||||
$table->string('nome');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
|
||||
@@ -8,15 +8,17 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('storage_repositories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nome');
|
||||
$table->string('tipo', 50);
|
||||
$table->text('config');
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->integer('ordine')->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
if (!Schema::hasTable('storage_repositories')) {
|
||||
Schema::create('storage_repositories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nome');
|
||||
$table->string('tipo', 50);
|
||||
$table->text('config');
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->integer('ordine')->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
|
||||
@@ -10,21 +10,23 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('calendario_connessioni', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nome');
|
||||
$table->string('tipo'); // google_calendar, caldav
|
||||
$table->text('config'); // JSON
|
||||
$table->string('stato')->default('disconnesso'); // connesso, disconnesso, errore
|
||||
$table->string('sync_direction')->default('bidirectional'); // import, export, bidirectional
|
||||
$table->integer('sync_interval_minutes')->default(60);
|
||||
$table->timestamp('last_sync_at')->nullable();
|
||||
$table->timestamp('last_error_at')->nullable();
|
||||
$table->text('last_error_message')->nullable();
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->integer('ordine')->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
if (!Schema::hasTable('calendario_connessioni')) {
|
||||
Schema::create('calendario_connessioni', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nome');
|
||||
$table->string('tipo'); // google_calendar, caldav
|
||||
$table->text('config'); // JSON
|
||||
$table->string('stato')->default('disconnesso'); // connesso, disconnesso, errore
|
||||
$table->string('sync_direction')->default('bidirectional'); // import, export, bidirectional
|
||||
$table->integer('sync_interval_minutes')->default(60);
|
||||
$table->timestamp('last_sync_at')->nullable();
|
||||
$table->timestamp('last_error_at')->nullable();
|
||||
$table->text('last_error_message')->nullable();
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->integer('ordine')->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
|
||||
@@ -18,7 +18,7 @@ return new class extends Migration
|
||||
$table->string('color', 7)->nullable();
|
||||
$table->integer('order_column')->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('taggables')) {
|
||||
@@ -26,7 +26,7 @@ return new class extends Migration
|
||||
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
|
||||
$table->morphs('taggable');
|
||||
$table->primary(['tag_id', 'taggable_id', 'taggable_type']);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasTable('google_oauth_connections')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::create('google_oauth_connections', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('service'); // email, drive, calendar
|
||||
$table->string('email');
|
||||
$table->text('access_token');
|
||||
$table->text('refresh_token')->nullable();
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['user_id', 'service', 'email']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('google_oauth_connections');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasColumn('email_settings', 'auth_method')) {
|
||||
Schema::table('email_settings', function (Blueprint $table) {
|
||||
$table->string('auth_method', 20)->default('password');
|
||||
$table->foreignId('google_oauth_connection_id')
|
||||
->nullable()
|
||||
->constrained('google_oauth_connections')
|
||||
->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('sender_accounts', 'auth_method')) {
|
||||
Schema::table('sender_accounts', function (Blueprint $table) {
|
||||
$table->string('auth_method', 20)->default('password');
|
||||
$table->foreignId('google_oauth_connection_id')
|
||||
->nullable()
|
||||
->constrained('google_oauth_connections')
|
||||
->nullOnDelete();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('email_settings', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('email_settings', 'google_oauth_connection_id')) {
|
||||
$table->dropForeign(['google_oauth_connection_id']);
|
||||
}
|
||||
if (Schema::hasColumn('email_settings', 'auth_method')) {
|
||||
$table->dropColumn(['auth_method', 'google_oauth_connection_id']);
|
||||
}
|
||||
});
|
||||
|
||||
Schema::table('sender_accounts', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('sender_accounts', 'google_oauth_connection_id')) {
|
||||
$table->dropForeign(['google_oauth_connection_id']);
|
||||
}
|
||||
if (Schema::hasColumn('sender_accounts', 'auth_method')) {
|
||||
$table->dropColumn(['auth_method', 'google_oauth_connection_id']);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('email_settings')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('email_settings', 'from_email')) {
|
||||
Schema::table('email_settings', function (Blueprint $table) {
|
||||
$table->string('from_email')->nullable()->after('email_name');
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('email_settings', 'from_name')) {
|
||||
Schema::table('email_settings', function (Blueprint $table) {
|
||||
$table->string('from_name')->nullable()->after('from_email');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('email_settings', function (Blueprint $table) {
|
||||
$table->dropColumn(['from_email', 'from_name']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('gruppo_contatti')) {
|
||||
Schema::create('gruppo_contatti', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('gruppo_id')->constrained('gruppi')->onDelete('cascade');
|
||||
$table->string('tipo');
|
||||
$table->string('valore');
|
||||
$table->string('etichetta')->nullable();
|
||||
$table->boolean('is_primary')->default(false);
|
||||
$table->timestamps();
|
||||
$table->index('gruppo_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('gruppo_contatti');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('diocesi_gruppo')) {
|
||||
Schema::create('diocesi_gruppo', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('gruppo_id')->constrained('gruppi')->onDelete('cascade');
|
||||
$table->foreignId('diocesi_id')->constrained('diocesi')->onDelete('cascade');
|
||||
$table->unique(['gruppo_id', 'diocesi_id']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('diocesi_gruppo');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('firme', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('email_setting_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('nome', 255);
|
||||
$table->longText('contenuto_html')->nullable();
|
||||
$table->boolean('is_default')->default(false);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('firme');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('mailing_lists', function (Blueprint $table) {
|
||||
$table->foreignId('firma_id')->nullable()->constrained('firme')->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('mailing_lists', function (Blueprint $table) {
|
||||
$table->dropForeign(['firma_id']);
|
||||
$table->dropColumn('firma_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('mailing_messaggi', function (Blueprint $table) {
|
||||
$table->foreignId('firma_id')->nullable()->constrained('firme')->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('mailing_messaggi', function (Blueprint $table) {
|
||||
$table->dropForeign(['firma_id']);
|
||||
$table->dropColumn('firma_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('sessions')) {
|
||||
Schema::create('sessions', function (Blueprint $table): void {
|
||||
$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');
|
||||
}
|
||||
};
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('mailing_lists', function (Blueprint $table) {
|
||||
$table->foreignId('sender_account_id')->nullable()->constrained('sender_accounts')->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('mailing_lists', function (Blueprint $table) {
|
||||
$table->dropForeign(['sender_account_id']);
|
||||
$table->dropColumn('sender_account_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasColumn('gruppi', 'anno_fondazione')) {
|
||||
Schema::table('gruppi', function (Blueprint $table) {
|
||||
$table->year('anno_fondazione')->nullable()->after('nome');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (Schema::hasColumn('gruppi', 'anno_fondazione')) {
|
||||
Schema::table('gruppi', function (Blueprint $table) {
|
||||
$table->dropColumn('anno_fondazione');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('documenti', function (Blueprint $table) {
|
||||
$table->text('url')->nullable()->after('note');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('documenti', function (Blueprint $table) {
|
||||
$table->dropColumn('url');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasColumn('documenti', 'file_path') && DB::getDriverName() === 'mysql') {
|
||||
DB::statement('ALTER TABLE documenti MODIFY COLUMN file_path VARCHAR(255) NULL');
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (Schema::hasColumn('documenti', 'file_path') && DB::getDriverName() === 'mysql') {
|
||||
DB::statement('ALTER TABLE documenti MODIFY COLUMN file_path VARCHAR(255) NOT NULL');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -68,7 +68,7 @@ class ComuniSeeder extends Seeder
|
||||
];
|
||||
|
||||
foreach ($comuni as $c) {
|
||||
Comune::create($c);
|
||||
Comune::firstOrCreate(['codice_istat' => $c['codice_istat']], $c);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,23 +20,27 @@ class DatabaseSeeder extends Seeder
|
||||
DockerBaseDataSeeder::class,
|
||||
]);
|
||||
|
||||
$admin = User::create([
|
||||
'name' => 'Admin',
|
||||
'email' => 'admin@glastree.local',
|
||||
'password' => bcrypt('password'),
|
||||
'is_admin' => true,
|
||||
'status' => 'active',
|
||||
'permissions' => [
|
||||
'individui' => 2,
|
||||
'gruppi' => 2,
|
||||
'eventi' => 2,
|
||||
'documenti' => 2,
|
||||
'mailing' => 2,
|
||||
'viste' => 2,
|
||||
],
|
||||
]);
|
||||
$admin = User::firstOrCreate(
|
||||
['email' => 'admin@glastree.local'],
|
||||
[
|
||||
'name' => 'Admin',
|
||||
'password' => bcrypt('password'),
|
||||
'is_admin' => true,
|
||||
'status' => 'active',
|
||||
'permissions' => [
|
||||
'individui' => 2,
|
||||
'gruppi' => 2,
|
||||
'eventi' => 2,
|
||||
'documenti' => 2,
|
||||
'mailing' => 2,
|
||||
'viste' => 2,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$admin->role_preset_id = 1;
|
||||
$admin->save();
|
||||
if (!$admin->role_preset_id) {
|
||||
$admin->role_preset_id = 1;
|
||||
$admin->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
+244
-139
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
@@ -9,147 +11,250 @@ class DiocesiSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$fixes = [
|
||||
50 => 'Abbazia territoriale della Santissima Trinità di Cava de Tirreni',
|
||||
51 => 'Diocesi di Cefalù',
|
||||
58 => 'Diocesi di Città di Castello',
|
||||
79 => 'Diocesi di Forlì-Bertinoro',
|
||||
121 => 'Diocesi di Mondovì',
|
||||
128 => 'Diocesi di Nardò-Gallipoli',
|
||||
148 => 'Arcidiocesi di Perugia-Città della Pieve',
|
||||
185 => "Arcidiocesi di Siena-Colle di Val d'Elsa-Montalcino",
|
||||
];
|
||||
foreach ($fixes as $id => $correctNome) {
|
||||
Diocesi::where('id', $id)->update(['nome' => $correctNome]);
|
||||
}
|
||||
|
||||
$diocesi = [
|
||||
['nome' => 'Acqui Terme', 'regione' => 'Piemonte'],
|
||||
['nome' => 'Afragola', 'regione' => 'Campania'],
|
||||
['nome' => 'Agrigento', 'regione' => 'Sicilia'],
|
||||
['nome' => 'Alba', 'regione' => 'Piemonte'],
|
||||
['nome' => 'Alessandria', 'regione' => 'Piemonte'],
|
||||
['nome' => 'Ancona', 'regione' => 'Marche'],
|
||||
['nome' => 'Aosta', 'regione' => 'Valle d\'Aosta'],
|
||||
['nome' => 'Aquila', 'regione' => 'Abruzzo'],
|
||||
['nome' => 'Arezzo', 'regione' => 'Toscana'],
|
||||
['nome' => 'Ascoli Piceno', 'regione' => 'Marche'],
|
||||
['nome' => 'Assisi', 'regione' => 'Umbria'],
|
||||
['nome' => 'Asti', 'regione' => 'Piemonte'],
|
||||
['nome' => 'Avellino', 'regione' => 'Campania'],
|
||||
['nome' => 'Bari', 'regione' => 'Puglia'],
|
||||
['nome' => 'Barletta', 'regione' => 'Puglia'],
|
||||
['nome' => 'Belluno', 'regione' => 'Veneto'],
|
||||
['nome' => 'Benevento', 'regione' => 'Campania'],
|
||||
['nome' => 'Bergamo', 'regione' => 'Lombardia'],
|
||||
['nome' => 'Biella', 'regione' => 'Piemonte'],
|
||||
['nome' => 'Bologna', 'regione' => 'Emilia-Romagna'],
|
||||
['nome' => 'Bolzano', 'regione' => 'Trentino-Alto Adige'],
|
||||
['nome' => 'Brescia', 'regione' => 'Lombardia'],
|
||||
['nome' => 'Brindisi', 'regione' => 'Puglia'],
|
||||
['nome' => 'Cagliari', 'regione' => 'Sardegna'],
|
||||
['nome' => 'Caltanissetta', 'regione' => 'Sicilia'],
|
||||
['nome' => 'Campobasso', 'regione' => 'Molise'],
|
||||
['nome' => 'Caserta', 'regione' => 'Campania'],
|
||||
['nome' => 'Cassano allo Ionio', 'regione' => 'Calabria'],
|
||||
['nome' => 'Castelvetrano', 'regione' => 'Sicilia'],
|
||||
['nome' => 'Catania', 'regione' => 'Sicilia'],
|
||||
['nome' => 'Catanzaro', 'regione' => 'Calabria'],
|
||||
['nome' => 'Cefalù', 'regione' => 'Sicilia'],
|
||||
['nome' => 'Cerro al Lambro', 'regione' => 'Lombardia'],
|
||||
['nome' => 'Cesena', 'regione' => 'Emilia-Romagna'],
|
||||
['nome' => 'Chieti', 'regione' => 'Abruzzo'],
|
||||
['nome' => 'Civitavecchia', 'regione' => 'Lazio'],
|
||||
['nome' => 'Cosenza', 'regione' => 'Calabria'],
|
||||
['nome' => 'Cremona', 'regione' => 'Lombardia'],
|
||||
['nome' => 'Crotone', 'regione' => 'Calabria'],
|
||||
['nome' => 'Cuneo', 'regione' => 'Piemonte'],
|
||||
['nome' => 'Donegal', 'regione' => 'Irlanda'],
|
||||
['nome' => 'Enna', 'regione' => 'Sicilia'],
|
||||
['nome' => 'Fano', 'regione' => 'Marche'],
|
||||
['nome' => 'Fermo', 'regione' => 'Marche'],
|
||||
['nome' => 'Ferrara', 'regione' => 'Emilia-Romagna'],
|
||||
['nome' => 'Firenze', 'regione' => 'Toscana'],
|
||||
['nome' => 'Foggia', 'regione' => 'Puglia'],
|
||||
['nome' => 'Foligno', 'regione' => 'Umbria'],
|
||||
['nome' => 'Forlì', 'regione' => 'Emilia-Romagna'],
|
||||
['nome' => 'Frosinone', 'regione' => 'Lazio'],
|
||||
['nome' => 'Genova', 'regione' => 'Liguria'],
|
||||
['nome' => 'Gorizia', 'regione' => 'Friuli-Venezia Giulia'],
|
||||
['nome' => 'Grosseto', 'regione' => 'Toscana'],
|
||||
['nome' => 'Gubbio', 'regione' => 'Umbria'],
|
||||
['nome' => 'Imola', 'regione' => 'Emilia-Romagna'],
|
||||
['nome' => 'Isernia', 'regione' => 'Molise'],
|
||||
['nome' => 'L\'Aquila', 'regione' => 'Abruzzo'],
|
||||
['nome' => 'La Spezia', 'regione' => 'Liguria'],
|
||||
['nome' => 'Lamezia Terme', 'regione' => 'Calabria'],
|
||||
['nome' => 'Latina', 'regione' => 'Lazio'],
|
||||
['nome' => 'Lecce', 'regione' => 'Puglia'],
|
||||
['nome' => 'Lecco', 'regione' => 'Lombardia'],
|
||||
['nome' => 'Livorno', 'regione' => 'Toscana'],
|
||||
['nome' => 'Lodi', 'regione' => 'Lombardia'],
|
||||
['nome' => 'Lucca', 'regione' => 'Toscana'],
|
||||
['nome' => 'Lunedì', 'regione' => 'Marche'],
|
||||
['nome' => 'Macerata', 'regione' => 'Marche'],
|
||||
['nome' => 'Mantova', 'regione' => 'Lombardia'],
|
||||
['nome' => 'Massa', 'regione' => 'Toscana'],
|
||||
['nome' => 'Matera', 'regione' => 'Basilicata'],
|
||||
['nome' => 'Messina', 'regione' => 'Sicilia'],
|
||||
['nome' => 'Milano', 'regione' => 'Lombardia'],
|
||||
['nome' => 'Modena', 'regione' => 'Emilia-Romagna'],
|
||||
['nome' => 'Monaco', 'regione' => 'Germania'],
|
||||
['nome' => 'Mongolia', 'regione' => 'Asia'],
|
||||
['nome' => 'Monreale', 'regione' => 'Sicilia'],
|
||||
['nome' => 'Montefeltro', 'regione' => 'Marche'],
|
||||
['nome' => 'Montepulciano', 'regione' => 'Toscana'],
|
||||
['nome' => 'Monza', 'regione' => 'Lombardia'],
|
||||
['nome' => 'Napoli', 'regione' => 'Campania'],
|
||||
['nome' => 'Novara', 'regione' => 'Piemonte'],
|
||||
['nome' => 'Nuoro', 'regione' => 'Sardegna'],
|
||||
['nome' => 'Oristano', 'regione' => 'Sardegna'],
|
||||
['nome' => 'Otranto', 'regione' => 'Puglia'],
|
||||
['nome' => 'Padova', 'regione' => 'Veneto'],
|
||||
['nome' => 'Palermo', 'regione' => 'Sicilia'],
|
||||
['nome' => 'Parma', 'regione' => 'Emilia-Romagna'],
|
||||
['nome' => 'Pavia', 'regione' => 'Lombardia'],
|
||||
['nome' => 'Perugia', 'regione' => 'Umbria'],
|
||||
['nome' => 'Pesaro', 'regione' => 'Marche'],
|
||||
['nome' => 'Piacenza', 'regione' => 'Emilia-Romagna'],
|
||||
['nome' => 'Pisa', 'regione' => 'Toscana'],
|
||||
['nome' => 'Pistoia', 'regione' => 'Toscana'],
|
||||
['nome' => 'Pordenone', 'regione' => 'Friuli-Venezia Giulia'],
|
||||
['nome' => 'Potenza', 'regione' => 'Basilicata'],
|
||||
['nome' => 'Prato', 'regione' => 'Toscana'],
|
||||
['nome' => 'Ragusa', 'regione' => 'Sicilia'],
|
||||
['nome' => 'Ravenna', 'regione' => 'Emilia-Romagna'],
|
||||
['nome' => 'Reggio Calabria', 'regione' => 'Calabria'],
|
||||
['nome' => 'Reggio Emilia', 'regione' => 'Emilia-Romagna'],
|
||||
['nome' => 'Rieti', 'regione' => 'Lazio'],
|
||||
['nome' => 'Rimini', 'regione' => 'Emilia-Romagna'],
|
||||
['nome' => 'Roma', 'regione' => 'Lazio'],
|
||||
['nome' => 'Rosario', 'regione' => 'Argentina'],
|
||||
['nome' => 'Rovigo', 'regione' => 'Veneto'],
|
||||
['nome' => 'Salerno', 'regione' => 'Campania'],
|
||||
['nome' => 'San Severo', 'regione' => 'Puglia'],
|
||||
['nome' => 'Sassari', 'regione' => 'Sardegna'],
|
||||
['nome' => 'Savona', 'regione' => 'Liguria'],
|
||||
['nome' => 'Siena', 'regione' => 'Toscana'],
|
||||
['nome' => 'Siracusa', 'regione' => 'Sicilia'],
|
||||
['nome' => 'Sondrio', 'regione' => 'Lombardia'],
|
||||
['nome' => 'Spezia', 'regione' => 'Liguria'],
|
||||
['nome' => 'Spoleto', 'regione' => 'Umbria'],
|
||||
['nome' => 'Syracuse', 'regione' => 'USA'],
|
||||
['nome' => 'Taranto', 'regione' => 'Puglia'],
|
||||
['nome' => 'Tempio Ampurias', 'regione' => 'Sardegna'],
|
||||
['nome' => 'Teramo', 'regione' => 'Abruzzo'],
|
||||
['nome' => 'Terni', 'regione' => 'Umbria'],
|
||||
['nome' => 'Tirana', 'regione' => 'Albania'],
|
||||
['nome' => 'Torino', 'regione' => 'Piemonte'],
|
||||
['nome' => 'Tortona', 'regione' => 'Piemonte'],
|
||||
['nome' => 'Trani', 'regione' => 'Puglia'],
|
||||
['nome' => 'Trapani', 'regione' => 'Sicilia'],
|
||||
['nome' => 'Trento', 'regione' => 'Trentino-Alto Adige'],
|
||||
['nome' => 'Treviso', 'regione' => 'Veneto'],
|
||||
['nome' => 'Trieste', 'regione' => 'Friuli-Venezia Giulia'],
|
||||
['nome' => 'Udine', 'regione' => 'Friuli-Venezia Giulia'],
|
||||
['nome' => 'Urbino', 'regione' => 'Marche'],
|
||||
['nome' => 'Vaticano', 'regione' => 'Lazio'],
|
||||
['nome' => 'Vercelli', 'regione' => 'Piemonte'],
|
||||
['nome' => 'Verona', 'regione' => 'Veneto'],
|
||||
['nome' => 'Vibo Valentia', 'regione' => 'Calabria'],
|
||||
['nome' => 'Vicenza', 'regione' => 'Veneto'],
|
||||
['nome' => 'Viterbo', 'regione' => 'Lazio'],
|
||||
['nome' => 'Vittorio Veneto', 'regione' => 'Veneto'],
|
||||
'Arcidiocesi di Acerenza',
|
||||
'Diocesi di Acerra',
|
||||
'Diocesi di Acireale',
|
||||
'Diocesi di Acqui',
|
||||
'Diocesi di Adria-Rovigo',
|
||||
'Arcidiocesi di Agrigento',
|
||||
'Diocesi di Alba',
|
||||
'Sede suburbicaria di Albano',
|
||||
'Diocesi di Albenga-Imperia',
|
||||
'Diocesi di Ales-Terralba',
|
||||
'Diocesi di Alessandria',
|
||||
'Diocesi di Alghero-Bosa',
|
||||
'Diocesi di Alife-Caiazzo',
|
||||
'Diocesi di Altamura-Gravina-Acquaviva delle Fonti',
|
||||
'Arcidiocesi di Amalfi-Cava dei Tirreni',
|
||||
'Diocesi di Anagni-Alatri',
|
||||
'Arcidiocesi di Ancona-Osimo',
|
||||
'Diocesi di Andria',
|
||||
'Diocesi di Aosta',
|
||||
'Diocesi di Arezzo-Cortona-Sansepolcro',
|
||||
'Diocesi di Ariano Irpino-Lacedonia',
|
||||
'Diocesi di Ascoli Piceno',
|
||||
'Diocesi di Assisi-Nocera Umbra-Gualdo Tadino',
|
||||
'Diocesi di Asti',
|
||||
'Diocesi di Avellino',
|
||||
'Diocesi di Aversa',
|
||||
'Diocesi di Avezzano',
|
||||
'Arcidiocesi di Bari-Bitonto',
|
||||
'Diocesi di Belluno-Feltre',
|
||||
'Arcidiocesi di Benevento',
|
||||
'Diocesi di Bergamo',
|
||||
'Diocesi di Biella',
|
||||
'Arcidiocesi di Bologna',
|
||||
'Diocesi di Bolzano-Bressanone',
|
||||
'Diocesi di Brescia',
|
||||
'Arcidiocesi di Brindisi-Ostuni',
|
||||
'Arcidiocesi di Cagliari',
|
||||
'Diocesi di Caltagirone',
|
||||
'Diocesi di Caltanissetta',
|
||||
'Arcidiocesi di Camerino-San Severino Marche',
|
||||
'Arcidiocesi di Campobasso-Boiano',
|
||||
'Arcidiocesi di Capua',
|
||||
'Diocesi di Carpi',
|
||||
'Diocesi di Casale Monferrato',
|
||||
'Diocesi di Caserta',
|
||||
'Diocesi di Cassano allo Ionio',
|
||||
'Diocesi di Castellaneta',
|
||||
'Arcidiocesi di Catania',
|
||||
'Arcidiocesi di Catanzaro-Squillace',
|
||||
'Abbazia territoriale della Santissima Trinità di Cava de Tirreni',
|
||||
'Diocesi di Cefalù',
|
||||
'Diocesi di Cerignola-Ascoli Satriano',
|
||||
'Diocesi di Cerreto Sannita-Telese-Sant\'Agata de Goti',
|
||||
'Diocesi di Cesena-Sarsina',
|
||||
'Diocesi di Chiavari',
|
||||
'Arcidiocesi di Chieti-Vasto',
|
||||
'Diocesi di Chioggia',
|
||||
'Diocesi di Città di Castello',
|
||||
'Diocesi di Civita Castellana',
|
||||
'Diocesi di Civitavecchia-Tarquinia',
|
||||
'Diocesi di Como',
|
||||
'Diocesi di Concordia-Pordenone',
|
||||
'Diocesi di Conversano-Monopoli',
|
||||
'Arcidiocesi di Cosenza-Bisignano',
|
||||
'Diocesi di Crema',
|
||||
'Diocesi di Cremona',
|
||||
'Arcidiocesi di Crotone-Santa Severina',
|
||||
'Diocesi di Cuneo',
|
||||
'Diocesi di Fabriano-Matelica',
|
||||
'Diocesi di Faenza-Modigliana',
|
||||
'Diocesi di Fano-Fossombrone-Cagli-Pergola',
|
||||
'Arcidiocesi di Fermo',
|
||||
'Arcidiocesi di Ferrara-Comacchio',
|
||||
'Diocesi di Fidenza',
|
||||
'Diocesi di Fiesole',
|
||||
'Arcidiocesi di Firenze',
|
||||
'Arcidiocesi di Foggia-Bovino',
|
||||
'Diocesi di Foligno',
|
||||
'Diocesi di Forlì-Bertinoro',
|
||||
'Diocesi di Fossano',
|
||||
'Sede suburbicaria di Frascati',
|
||||
'Diocesi di Frosinone-Veroli-Ferentino',
|
||||
'Arcidiocesi di Gaeta',
|
||||
'Arcidiocesi di Genova',
|
||||
'Arcidiocesi di Gorizia',
|
||||
'Diocesi di Grosseto',
|
||||
'Diocesi di Gubbio',
|
||||
'Diocesi di Iglesias',
|
||||
'Diocesi di Imola',
|
||||
'Diocesi di Ischia',
|
||||
'Diocesi di Isernia-Venafro',
|
||||
'Diocesi di Ivrea',
|
||||
'Diocesi di Jesi',
|
||||
'Diocesi della Spezia-Sarzana-Brugnato',
|
||||
'Diocesi di Lamezia Terme',
|
||||
'Arcidiocesi di Lanciano-Ortona',
|
||||
'Diocesi di Lanusei',
|
||||
'Arcidiocesi dell\'Aquila',
|
||||
'Diocesi di Latina-Terracina-Sezze-Priverno',
|
||||
'Arcidiocesi di Lecce',
|
||||
'Diocesi di Livorno',
|
||||
'Diocesi di Locri-Gerace',
|
||||
'Diocesi di Lodi',
|
||||
'Prelatura territoriale di Loreto',
|
||||
'Arcidiocesi di Lucca',
|
||||
'Diocesi di Lucera-Troia',
|
||||
'Eparchia di Lungro',
|
||||
'Diocesi di Macerata-Tolentino-Recanati-Cingoli-Treia',
|
||||
'Arcidiocesi di Manfredonia-Vieste-San Giovanni Rotondo',
|
||||
'Diocesi di Mantova',
|
||||
'Diocesi di Massa Carrara-Pontremoli',
|
||||
'Diocesi di Massa Marittima-Piombino',
|
||||
'Arcidiocesi di Matera-Irsina',
|
||||
'Diocesi di Mazara del Vallo',
|
||||
'Diocesi di Melfi-Rapolla-Venosa',
|
||||
'Arcidiocesi di Messina-Lipari-Santa Lucia del Mela',
|
||||
'Arcidiocesi di Milano',
|
||||
'Diocesi di Mileto-Nicotera-Tropea',
|
||||
'Arcidiocesi di Modena-Nonantola',
|
||||
'Diocesi di Molfetta-Ruvo-Giovinazzo-Terlizzi',
|
||||
'Diocesi di Mondovì',
|
||||
'Arcidiocesi di Monreale',
|
||||
'Abbazia territoriale di Montecassino',
|
||||
'Abbazia territoriale di Monte Oliveto Maggiore',
|
||||
'Diocesi di Montepulciano-Chiusi-Pienza',
|
||||
'Abbazia territoriale di Montevergine',
|
||||
'Arcidiocesi di Napoli',
|
||||
'Diocesi di Nardò-Gallipoli',
|
||||
'Diocesi di Nicosia',
|
||||
'Diocesi di Nocera Inferiore-Sarno',
|
||||
'Diocesi di Nola',
|
||||
'Diocesi di Noto',
|
||||
'Diocesi di Novara',
|
||||
'Diocesi di Nuoro',
|
||||
'Diocesi di Oppido Mamertina-Palmi',
|
||||
'Diocesi di Oria',
|
||||
'Arcidiocesi di Oristano',
|
||||
'Diocesi di Orvieto-Todi',
|
||||
'Sede suburbicaria di Ostia',
|
||||
'Arcidiocesi di Otranto',
|
||||
'Diocesi di Ozieri',
|
||||
'Diocesi di Padova',
|
||||
'Arcidiocesi di Palermo',
|
||||
'Sede suburbicaria di Palestrina',
|
||||
'Diocesi di Parma',
|
||||
'Diocesi di Patti',
|
||||
'Diocesi di Pavia',
|
||||
'Arcidiocesi di Perugia-Città della Pieve',
|
||||
'Arcidiocesi di Pesaro',
|
||||
'Arcidiocesi di Pescara-Penne',
|
||||
'Diocesi di Pescia',
|
||||
'Eparchia di Piana degli Albanesi',
|
||||
'Diocesi di Piazza Armerina',
|
||||
'Diocesi di Pinerolo',
|
||||
'Arcidiocesi di Pisa',
|
||||
'Diocesi di Pistoia',
|
||||
'Diocesi di Pitigliano-Sovana-Orbetello',
|
||||
'Prelatura territoriale di Pompei',
|
||||
'Sede suburbicaria di Porto-Santa Rufina',
|
||||
'Arcidiocesi di Potenza-Muro Lucano-Marsico Nuovo',
|
||||
'Diocesi di Pozzuoli',
|
||||
'Diocesi di Prato',
|
||||
'Diocesi di Ragusa',
|
||||
'Arcidiocesi di Ravenna-Cervia',
|
||||
'Arcidiocesi di Reggio Calabria-Bova',
|
||||
'Diocesi di Reggio Emilia-Guastalla',
|
||||
'Diocesi di Rieti',
|
||||
'Diocesi di Rimini',
|
||||
'Diocesi di Roma',
|
||||
'Arcidiocesi di Rossano-Cariati',
|
||||
'Sede suburbicaria di Sabina-Poggio Mirteto',
|
||||
'Arcidiocesi di Salerno-Campagna-Acerno',
|
||||
'Diocesi di Saluzzo',
|
||||
'Diocesi di San Benedetto del Tronto-Ripatransone-Montalto',
|
||||
'Diocesi di San Marco Argentano-Scalea',
|
||||
'Diocesi di San Marino-Montefeltro',
|
||||
'Diocesi di San Miniato',
|
||||
'Diocesi di San Severo',
|
||||
'Abbazia territoriale di Santa Maria di Grottaferrata',
|
||||
'Arcidiocesi di Sant\'Angelo dei Lombardi-Conza-Nusco-Bisaccia',
|
||||
'Arcidiocesi di Sassari',
|
||||
'Diocesi di Savona-Noli',
|
||||
'Diocesi di Senigallia',
|
||||
'Diocesi di Sessa Aurunca',
|
||||
'Arcidiocesi di Siena-Colle di Val d\'Elsa-Montalcino',
|
||||
'Arcidiocesi di Siracusa',
|
||||
'Diocesi di Sora-Cassino-Aquino-Pontecorvo',
|
||||
'Arcidiocesi di Sorrento-Castellammare di Stabia',
|
||||
'Arcidiocesi di Spoleto-Norcia',
|
||||
'Abbazia territoriale di Subiaco',
|
||||
'Diocesi di Sulmona-Valva',
|
||||
'Diocesi di Susa',
|
||||
'Arcidiocesi di Taranto',
|
||||
'Diocesi di Teano-Calvi',
|
||||
'Diocesi di Teggiano-Policastro',
|
||||
'Diocesi di Tempio-Ampurias',
|
||||
'Diocesi di Teramo-Atri',
|
||||
'Diocesi di Termoli-Larino',
|
||||
'Diocesi di Terni-Narni-Amelia',
|
||||
'Diocesi di Tivoli',
|
||||
'Arcidiocesi di Torino',
|
||||
'Diocesi di Tortona',
|
||||
'Arcidiocesi di Trani-Barletta-Bisceglie-Nazareth',
|
||||
'Diocesi di Trapani',
|
||||
'Arcidiocesi di Trento',
|
||||
'Diocesi di Treviso',
|
||||
'Diocesi di Tricarico',
|
||||
'Diocesi di Trieste',
|
||||
'Diocesi di Trivento',
|
||||
'Diocesi di Tursi-Lagonegro',
|
||||
'Arcidiocesi di Udine',
|
||||
'Diocesi di Ugento-Santa Maria di Leuca',
|
||||
'Arcidiocesi di Urbino-Urbania-Sant\'Angelo in Vado',
|
||||
'Diocesi di Vallo della Lucania',
|
||||
'Sede suburbicaria di Velletri-Segni',
|
||||
'Patriarcato di Venezia',
|
||||
'Diocesi di Ventimiglia-Sanremo',
|
||||
'Arcidiocesi di Vercelli',
|
||||
'Diocesi di Verona',
|
||||
'Diocesi di Vicenza',
|
||||
'Diocesi di Vigevano',
|
||||
'Diocesi di Viterbo',
|
||||
'Diocesi di Vittorio Veneto',
|
||||
'Diocesi di Volterra',
|
||||
'Diocesi di Piacenza-Bobbio',
|
||||
];
|
||||
|
||||
foreach ($diocesi as $d) {
|
||||
Diocesi::create($d);
|
||||
foreach ($diocesi as $nome) {
|
||||
Diocesi::firstOrCreate(['nome' => $nome]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1162
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -52,7 +52,7 @@
|
||||
"src": "_instrument-sans-600-normal-DMks36a2.woff"
|
||||
},
|
||||
"resources/css/app.css": {
|
||||
"file": "assets/app-XidZn-hP.css",
|
||||
"file": "assets/app-CwsUKJAO.css",
|
||||
"name": "app",
|
||||
"names": [
|
||||
"app.css"
|
||||
|
||||
@@ -145,8 +145,11 @@ class ColumnManager {
|
||||
let dragIndex = -1;
|
||||
let placeholder = null;
|
||||
|
||||
const pinnedKeys = ['select', 'azioni'];
|
||||
|
||||
thead.querySelectorAll('th').forEach(th => {
|
||||
if (th.querySelector('.col-resize-handle')) return;
|
||||
const colKey = th.dataset.column;
|
||||
if (!colKey || pinnedKeys.includes(colKey)) return;
|
||||
th.draggable = true;
|
||||
|
||||
th.addEventListener('dragstart', (e) => {
|
||||
@@ -228,8 +231,10 @@ class ColumnManager {
|
||||
const ths = this.table.querySelectorAll('thead th');
|
||||
this.columnOrder = [];
|
||||
const keysInOrder = [];
|
||||
const pinnedKeys = ['select', 'azioni'];
|
||||
ths.forEach((th, idx) => {
|
||||
const colKey = th.dataset.column || `col_${idx}`;
|
||||
if (pinnedKeys.includes(colKey)) return;
|
||||
keysInOrder.push(colKey);
|
||||
this.columnOrder.push(colKey);
|
||||
});
|
||||
@@ -240,6 +245,11 @@ class ColumnManager {
|
||||
|
||||
reorderColumns(keyOrder, persist = true) {
|
||||
if (!keyOrder || keyOrder.length === 0) return;
|
||||
|
||||
const pinnedKeys = ['select', 'azioni'];
|
||||
const dataOrder = keyOrder.filter(k => !pinnedKeys.includes(k));
|
||||
if (dataOrder.length === 0) return;
|
||||
|
||||
const thead = this.table.querySelector('thead');
|
||||
if (!thead) return;
|
||||
|
||||
@@ -257,7 +267,7 @@ class ColumnManager {
|
||||
const reordered = [];
|
||||
let lastRef = null;
|
||||
|
||||
keyOrder.forEach(key => {
|
||||
dataOrder.forEach(key => {
|
||||
const srcIdx = keyToThIndex[key];
|
||||
if (srcIdx !== undefined && cells[srcIdx]) {
|
||||
reordered.push(cells[srcIdx]);
|
||||
@@ -272,10 +282,26 @@ class ColumnManager {
|
||||
}
|
||||
lastRef = cell;
|
||||
});
|
||||
|
||||
const selectIdx = keyToThIndex['select'];
|
||||
if (selectIdx !== undefined) {
|
||||
const selectCell = cells[selectIdx];
|
||||
if (selectCell && row.firstChild !== selectCell) {
|
||||
row.insertBefore(selectCell, row.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
const azioniIdx = keyToThIndex['azioni'];
|
||||
if (azioniIdx !== undefined) {
|
||||
const azioniCell = cells[azioniIdx];
|
||||
if (azioniCell) {
|
||||
row.appendChild(azioniCell);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.columnOrder = [...keyOrder];
|
||||
this.columns.sort((a, b) => keyOrder.indexOf(a.key) - keyOrder.indexOf(b.key));
|
||||
this.columnOrder = [...dataOrder];
|
||||
this.columns.sort((a, b) => dataOrder.indexOf(a.key) - dataOrder.indexOf(b.key));
|
||||
this.columns.forEach((col, idx) => col.index = idx);
|
||||
|
||||
if (persist) this.persistState();
|
||||
|
||||
@@ -46,9 +46,20 @@
|
||||
<td>{{ $backup['size_formatted'] }}</td>
|
||||
<td>
|
||||
@if($backup['manifest'])
|
||||
@php $components = $backup['manifest']['included_components'] ?? ['database']; @endphp
|
||||
<span class="badge badge-info" title="PHP {{ $backup['manifest']['php_version'] ?? '?' }}, Laravel {{ $backup['manifest']['laravel_version'] ?? '?' }}">
|
||||
<i class="fas fa-database mr-1"></i>DB
|
||||
</span>
|
||||
@if(in_array('env', $components))
|
||||
<span class="badge badge-secondary" title="Configurazione .env">
|
||||
<i class="fas fa-cog mr-1"></i>.env
|
||||
</span>
|
||||
@endif
|
||||
@if(in_array('files', $components))
|
||||
<span class="badge badge-warning" title="File caricati">
|
||||
<i class="fas fa-file mr-1"></i>Files
|
||||
</span>
|
||||
@endif
|
||||
@if(isset($backup['manifest']['db_name']))
|
||||
<small class="text-muted">{{ $backup['manifest']['db_name'] }}</small>
|
||||
@endif
|
||||
|
||||
@@ -31,6 +31,9 @@
|
||||
<a href="#signature" class="list-group-item list-group-item-action" data-toggle="tab">
|
||||
<i class="nav-icon fas fa-signature"></i> Firma
|
||||
</a>
|
||||
<a href="#firme" class="list-group-item list-group-item-action" data-toggle="tab">
|
||||
<i class="nav-icon fas fa-pen-fancy"></i> Firme Multiple
|
||||
</a>
|
||||
<a href="#mittenti" class="list-group-item list-group-item-action" data-toggle="tab">
|
||||
<i class="nav-icon fas fa-user-plus"></i> Mittenti Aggiuntivi
|
||||
</a>
|
||||
@@ -197,12 +200,32 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="from_email">Email Mittente Ufficiale <small class="text-muted">(se diversa dall'indirizzo IMAP)</small></label>
|
||||
<input type="email" name="from_email" id="from_email" class="form-control"
|
||||
value="{{ $settings->from_email ?? '' }}"
|
||||
placeholder="es. ufficiale@esempio.it">
|
||||
<small class="text-muted">Se impostata, viene usata come mittente e Reply-To nelle email in uscita</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="from_name">Nome Mittente Ufficiale</label>
|
||||
<input type="text" name="from_name" id="from_name" class="form-control"
|
||||
value="{{ $settings->from_name ?? '' }}"
|
||||
placeholder="es. Nome Ufficiale">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="reply_to">Email Risposta (Reply-To)</label>
|
||||
<input type="email" name="reply_to" id="reply_to" class="form-control"
|
||||
value="{{ $settings->reply_to ?? '' }}">
|
||||
<small class="text-muted">Usato solo se "Email Mittente Ufficiale" non è impostata</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -284,6 +307,64 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane" id="firme">
|
||||
<div class="card card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Firme Multiple</h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-sm btn-success" data-toggle="modal" data-target="#firmaModal" onclick="resetFirmaForm()">
|
||||
<i class="fas fa-plus mr-1"></i> Nuova Firma
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if($firme->count() > 0)
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nome</th>
|
||||
<th>Anteprima</th>
|
||||
<th>Predefinita</th>
|
||||
<th style="width: 140px;">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($firme as $firma)
|
||||
<tr>
|
||||
<td><strong>{{ $firma->nome }}</strong></td>
|
||||
<td><small class="text-muted">{!! Str::limit(strip_tags($firma->contenuto_html), 80) !!}</small></td>
|
||||
<td>
|
||||
@if($firma->is_default)
|
||||
<span class="badge badge-success"><i class="fas fa-check"></i> Default</span>
|
||||
@else
|
||||
<form action="{{ route('impostazioni.firma.default', $firma->id) }}" method="POST" class="d-inline" onsubmit="return confirm('Impostare questa firma come predefinita?')">
|
||||
@csrf @method('PUT')
|
||||
<button type="submit" class="btn btn-xs btn-outline-secondary">Imposta default</button>
|
||||
</form>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-xs btn-info" onclick="editFirma({{ $firma->id }}, '{{ addslashes($firma->nome) }}', '{{ addslashes($firma->contenuto_html ?? '') }}')">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<form action="{{ route('impostazioni.firma.destroy', $firma->id) }}" method="POST" class="d-inline" onsubmit="return confirm('Eliminare questa firma?')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn btn-xs btn-danger"><i class="fas fa-trash"></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<p class="text-muted">Nessuna firma multipla configurata. La firma predefinita nella scheda "Firma" verrà usata come fallback.</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane" id="mittenti">
|
||||
<div class="card card-primary">
|
||||
<div class="card-header">
|
||||
@@ -513,9 +594,47 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="firmaModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<form id="firmaForm" method="POST" action="{{ route('impostazioni.firma.store') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="_method" id="firmaMethod" value="POST">
|
||||
<input type="hidden" name="id" id="firmaId" value="">
|
||||
<div class="modal-header bg-primary">
|
||||
<h5 class="modal-title" id="firmaModalTitle"><i class="fas fa-pen-fancy mr-2"></i>Nuova Firma</h5>
|
||||
<button type="button" class="close text-white" data-dismiss="modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="firma_nome">Nome *</label>
|
||||
<input type="text" name="nome" id="firma_nome" class="form-control" required placeholder="Es: Default, Personale, Ufficio">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Contenuto Firma (HTML)</label>
|
||||
<div id="firma-quill-editor" style="height: 200px;"></div>
|
||||
<input type="hidden" name="contenuto_html" id="firma_contenuto_input" value="">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="firma_is_default" name="is_default" value="1">
|
||||
<label class="custom-control-label" for="firma_is_default">Imposta come predefinita</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annulla</button>
|
||||
<button type="submit" class="btn btn-primary"><i class="fas fa-save mr-1"></i> Salva Firma</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
let senderAccounts = @json($senderAccounts);
|
||||
let firmaQuillInstance = null;
|
||||
|
||||
function resetSenderForm() {
|
||||
document.getElementById('senderForm').action = '{{ route('impostazioni.sender.store') }}';
|
||||
@@ -696,13 +815,89 @@ function createEditor() {
|
||||
quillEditorInstance = quill;
|
||||
}
|
||||
|
||||
function resetFirmaForm() {
|
||||
document.getElementById('firmaForm').action = '{{ route('impostazioni.firma.store') }}';
|
||||
document.getElementById('firmaMethod').value = 'POST';
|
||||
document.getElementById('firmaModalTitle').textContent = 'Nuova Firma';
|
||||
document.getElementById('firmaForm').reset();
|
||||
document.getElementById('firmaId').value = '';
|
||||
if (firmaQuillInstance) {
|
||||
firmaQuillInstance.root.innerHTML = '';
|
||||
}
|
||||
document.getElementById('firma_contenuto_input').value = '';
|
||||
}
|
||||
|
||||
function editFirma(id, nome, contenuto) {
|
||||
document.getElementById('firmaForm').action = '{{ route('impostazioni.firma.update', '__ID__') }}'.replace('__ID__', id);
|
||||
document.getElementById('firmaMethod').value = 'PUT';
|
||||
document.getElementById('firmaModalTitle').textContent = 'Modifica Firma - ' + nome;
|
||||
document.getElementById('firmaId').value = id;
|
||||
document.getElementById('firma_nome').value = nome;
|
||||
if (firmaQuillInstance && contenuto) {
|
||||
firmaQuillInstance.root.innerHTML = contenuto;
|
||||
document.getElementById('firma_contenuto_input').value = contenuto;
|
||||
}
|
||||
$('#firmaModal').modal('show');
|
||||
}
|
||||
|
||||
function initFirmaQuill() {
|
||||
var container = document.querySelector('#firma-quill-editor');
|
||||
if (!container || firmaQuillInstance) return;
|
||||
|
||||
var quill = new Quill('#firma-quill-editor', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
['bold', 'italic', 'underline'],
|
||||
[{ 'color': [] }, { 'background': [] }],
|
||||
[{ 'header': 1 }, { 'header': 2 }, { 'header': 3 }],
|
||||
[{ 'align': [] }],
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
['link', 'image'],
|
||||
['clean']
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
var input = document.getElementById('firma_contenuto_input');
|
||||
|
||||
quill.on('text-change', function() {
|
||||
input.value = quill.root.innerHTML;
|
||||
});
|
||||
|
||||
firmaQuillInstance = quill;
|
||||
}
|
||||
|
||||
$('#firmaModal').on('shown.bs.modal', function () {
|
||||
if (typeof Quill === 'undefined') {
|
||||
var script = document.createElement('script');
|
||||
script.src = 'https://cdn.quilljs.com/1.3.7/quill.min.js';
|
||||
script.onload = function() {
|
||||
var link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = 'https://cdn.quilljs.com/1.3.7/quill.snow.css';
|
||||
document.head.appendChild(link);
|
||||
setTimeout(initFirmaQuill, 200);
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
} else {
|
||||
setTimeout(initFirmaQuill, 100);
|
||||
}
|
||||
});
|
||||
|
||||
function initSignatureEditor() {
|
||||
if (quillEditorInstance) return;
|
||||
|
||||
if (typeof Quill === 'undefined') {
|
||||
var script = document.createElement('script');
|
||||
script.src = 'https://cdn.quilljs.com/1.3.7/quill.min.js';
|
||||
script.onload = createEditor;
|
||||
script.onload = function() {
|
||||
var link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = 'https://cdn.quilljs.com/1.3.7/quill.snow.css';
|
||||
document.head.appendChild(link);
|
||||
setTimeout(createEditor, 200);
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
|
||||
var link = document.createElement('link');
|
||||
|
||||
@@ -16,6 +16,13 @@
|
||||
<label>Nome *</label>
|
||||
<input type="text" name="nome_file" class="form-control" value="{{ old('nome_file', $documento->nome_file) }}" required>
|
||||
</div>
|
||||
@if($documento->isLink())
|
||||
<div class="form-group">
|
||||
<label>URL esterno</label>
|
||||
<input type="url" name="url" class="form-control" value="{{ old('url', $documento->url) }}" placeholder="https://esempio.com/documento.pdf">
|
||||
<small class="text-muted">Modifica l'indirizzo web del documento</small>
|
||||
</div>
|
||||
@endif
|
||||
<div class="form-group">
|
||||
<label>Tipologia *</label>
|
||||
<select name="tipologia" class="form-control" required>
|
||||
@@ -72,6 +79,12 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm table-borderless">
|
||||
@if($documento->isLink())
|
||||
<tr>
|
||||
<td style="width: 100px;"><strong>URL:</strong></td>
|
||||
<td><a href="{{ $documento->url }}" target="_blank" class="text-truncate d-inline-block" style="max-width: 200px;">{{ $documento->url }}</a></td>
|
||||
</tr>
|
||||
@else
|
||||
<tr>
|
||||
<td style="width: 100px;"><strong>File:</strong></td>
|
||||
<td>{{ $documento->file_path }}</td>
|
||||
@@ -84,6 +97,11 @@
|
||||
<td><strong>Dimensione:</strong></td>
|
||||
<td>{{ $documento->dimensione ? number_format($documento->dimensione / 1024, 1) . ' KB' : '-' }}</td>
|
||||
</tr>
|
||||
@endif
|
||||
<tr>
|
||||
<td><strong>Tipo:</strong></td>
|
||||
<td>@if($documento->isLink()) <span class="badge badge-info">Link esterno</span> @else Upload @endif</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Creato:</strong></td>
|
||||
<td>{{ $documento->created_at->format('d/m/Y H:i') }}</td>
|
||||
@@ -95,7 +113,12 @@
|
||||
</tr>
|
||||
@endif
|
||||
</table>
|
||||
@if($documento->file_path)
|
||||
@if($documento->isLink())
|
||||
<hr>
|
||||
<a href="{{ $documento->url }}" target="_blank" class="btn btn-info btn-block">
|
||||
<i class="fas fa-external-link-alt mr-1"></i> Apri link esterno
|
||||
</a>
|
||||
@elseif($documento->file_path)
|
||||
<hr>
|
||||
<a href="/documenti/{{ $documento->id }}/download" class="btn btn-primary btn-block">
|
||||
<i class="fas fa-download mr-1"></i> Scarica
|
||||
@@ -105,40 +128,6 @@
|
||||
</div>
|
||||
|
||||
@include('partials._tag-selector', ['selectedTags' => $selectedTags ?? [], 'label' => 'Etichette'])
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm table-borderless">
|
||||
<tr>
|
||||
<td style="width: 100px;"><strong>File:</strong></td>
|
||||
<td>{{ $documento->file_path }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Tipo MIME:</strong></td>
|
||||
<td>{{ $documento->mime_type ?? '-' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Dimensione:</strong></td>
|
||||
<td>{{ $documento->dimensione ? number_format($documento->dimensione / 1024, 1) . ' KB' : '-' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Creato:</strong></td>
|
||||
<td>{{ $documento->created_at->format('d/m/Y H:i') }}</td>
|
||||
</tr>
|
||||
@if($documento->user)
|
||||
<tr>
|
||||
<td><strong>Utente:</strong></td>
|
||||
<td>{{ $documento->user->name }}</td>
|
||||
</tr>
|
||||
@endif
|
||||
</table>
|
||||
@if($documento->file_path)
|
||||
<hr>
|
||||
<a href="/documenti/{{ $documento->id }}/download" class="btn btn-primary btn-block">
|
||||
<i class="fas fa-download mr-1"></i> Scarica
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -18,6 +18,18 @@ $tableColumnsJson = json_encode($tableColumns ?? []);
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success alert-dismissible">
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
@if(session('error'))
|
||||
<div class="alert alert-danger alert-dismissible">
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
<div class="row">
|
||||
{{-- Sidebar cartelle --}}
|
||||
<div class="col-md-3">
|
||||
@@ -133,7 +145,7 @@ $tableColumnsJson = json_encode($tableColumns ?? []);
|
||||
<form id="massForm" method="POST">
|
||||
@csrf
|
||||
<input type="hidden" name="_method" id="formMethod" value="POST">
|
||||
@if($canDeleteDocumenti)
|
||||
@if($canWriteDocumenti)
|
||||
<div class="p-3 bg-light border-bottom d-flex align-items-center flex-wrap gap-2">
|
||||
<span class="font-weight-bold mr-2">Azioni:</span>
|
||||
<button type="button" class="btn btn-sm btn-danger" onclick="massAction('delete')">
|
||||
@@ -149,7 +161,7 @@ $tableColumnsJson = json_encode($tableColumns ?? []);
|
||||
<button type="button" class="btn btn-sm btn-secondary" onclick="$('#massMoveModal').modal('show')">
|
||||
<i class="fas fa-folder-open mr-1"></i> Sposta selezionati in...
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-info" onclick="$('#massTagModal').modal('show')">
|
||||
<button type="button" class="btn btn-sm btn-info" onclick="showMassTagModal()">
|
||||
<i class="fas fa-tags mr-1"></i> Tag
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-success" onclick="massDownload()">
|
||||
@@ -186,7 +198,7 @@ $tableColumnsJson = json_encode($tableColumns ?? []);
|
||||
<i class="fas fa-folder-open"></i>
|
||||
</button>
|
||||
@endif
|
||||
@if($canDeleteDocumenti)
|
||||
@if($canWriteDocumenti)
|
||||
<button type="button" class="btn btn-xs btn-danger" onclick='deleteFolder({{ $cartella->id }}, {{ json_encode($cartella->nome) }})' title="Elimina">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
@@ -213,16 +225,20 @@ $tableColumnsJson = json_encode($tableColumns ?? []);
|
||||
<div class="col-md-4 col-sm-6 mb-3">
|
||||
<div class="card card-outline card-secondary h-100">
|
||||
<div class="card-body p-3">
|
||||
@if($canDeleteDocumenti)
|
||||
@if($canWriteDocumenti)
|
||||
<div class="float-right">
|
||||
<input type="checkbox" name="ids[]" value="{{ $documento->id }}" class="doc-checkbox" onchange="updateCount()">
|
||||
</div>
|
||||
@endif
|
||||
<h6 class="card-title font-weight-bold text-truncate mb-2" title="{{ $documento->nome_file }}">
|
||||
{{ $documento->nome_file }}
|
||||
<a href="/documenti/{{ $documento->id }}/edit">{{ $documento->nome_file }}</a>
|
||||
</h6>
|
||||
<div class="text-center py-2" style="font-size: 2.5rem;">
|
||||
<i class="fas {{ $icon }}"></i>
|
||||
@if($documento->isLink())
|
||||
<i class="fas fa-link text-info"></i>
|
||||
@else
|
||||
<i class="fas {{ $icon }}"></i>
|
||||
@endif
|
||||
</div>
|
||||
<div class="text-center mb-2">
|
||||
<span class="badge badge-{{ $documento->tipologia === 'avatar' ? 'primary' : ($documento->tipologia === 'galleria' ? 'success' : 'secondary') }}">
|
||||
@@ -247,7 +263,11 @@ $tableColumnsJson = json_encode($tableColumns ?? []);
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer p-2 text-center">
|
||||
@if($documento->file_path)
|
||||
@if($documento->isLink())
|
||||
<a href="{{ $documento->url }}" target="_blank" class="btn btn-xs btn-info" title="Apri link esterno">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</a>
|
||||
@elseif($documento->file_path)
|
||||
<button type="button" class="btn btn-xs btn-primary" onclick="previewDoc({{ $documento->id }}, '{{ $documento->mime_type }}')" title="Anteprima">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
@@ -259,17 +279,22 @@ $tableColumnsJson = json_encode($tableColumns ?? []);
|
||||
<a href="/documenti/{{ $documento->id }}/edit" class="btn btn-xs btn-warning" title="Modifica">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
@if(!$documento->isLink())
|
||||
<button type="button" class="btn btn-xs btn-secondary" onclick="showMoveModal({{ $documento->id }}, '{{ e($documento->nome_file) }}')" title="Sposta in altra cartella">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
</button>
|
||||
@endif
|
||||
<button type="button" class="btn btn-xs btn-info" onclick="showAssociateModal({{ $documento->id }})" title="Associa a...">
|
||||
<i class="fas fa-link"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-xs btn-secondary" onclick="showUpdateModal({{ $documento->id }})" title="Cambia tipo/contesto">
|
||||
<i class="fas fa-tag"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-xs btn-info" onclick="openSingleTag({{ $documento->id }})" title="Assegna tag">
|
||||
<i class="fas fa-tags"></i>
|
||||
</button>
|
||||
@endif
|
||||
@if($canDeleteDocumenti)
|
||||
@if($canWriteDocumenti)
|
||||
<button type="button" class="btn btn-xs btn-danger" onclick="deleteDocumento({{ $documento->id }}, '{{ e($documento->nome_file) }}')" title="Elimina">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
@@ -299,7 +324,7 @@ $tableColumnsJson = json_encode($tableColumns ?? []);
|
||||
<table class="table table-bordered table-hover table-striped mb-0" id="documenti-table">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
@if($canDeleteDocumenti)
|
||||
@if($canWriteDocumenti)
|
||||
<th style="width: 40px;" data-column="select">
|
||||
<input type="checkbox" id="selectAll" onchange="toggleAll(this)">
|
||||
</th>
|
||||
@@ -316,7 +341,7 @@ $tableColumnsJson = json_encode($tableColumns ?? []);
|
||||
<tbody>
|
||||
@foreach($sottoCartelle as $cartella)
|
||||
<tr class="table-warning">
|
||||
@if($canDeleteDocumenti)
|
||||
@if($canWriteDocumenti)
|
||||
<td></td>
|
||||
@endif
|
||||
<td>
|
||||
@@ -341,7 +366,7 @@ $tableColumnsJson = json_encode($tableColumns ?? []);
|
||||
<i class="fas fa-folder-open"></i>
|
||||
</button>
|
||||
@endif
|
||||
@if($canDeleteDocumenti)
|
||||
@if($canWriteDocumenti)
|
||||
<button type="button" class="btn btn-xs btn-danger" onclick='deleteFolder({{ $cartella->id }}, {{ json_encode($cartella->nome) }})' title="Elimina">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
@@ -362,13 +387,17 @@ $tableColumnsJson = json_encode($tableColumns ?? []);
|
||||
};
|
||||
@endphp
|
||||
<tr>
|
||||
@if($canDeleteDocumenti)
|
||||
@if($canWriteDocumenti)
|
||||
<td>
|
||||
<input type="checkbox" name="ids[]" value="{{ $documento->id }}" class="doc-checkbox" onchange="updateCount()">
|
||||
</td>
|
||||
@endif
|
||||
<td>
|
||||
<i class="fas {{ $icon }} mr-2"></i>
|
||||
@if($documento->isLink())
|
||||
<i class="fas fa-link text-info mr-2"></i>
|
||||
@else
|
||||
<i class="fas {{ $icon }} mr-2"></i>
|
||||
@endif
|
||||
<span class="font-weight-bold">{{ $documento->nome_file }}</span>
|
||||
<div class="small text-muted">
|
||||
@if($documento->user){{ $documento->user->name }}@endif
|
||||
@@ -445,7 +474,11 @@ $tableColumnsJson = json_encode($tableColumns ?? []);
|
||||
</td>
|
||||
<td class="small">{{ $documento->created_at->format('d/m/Y') }}</td>
|
||||
<td>
|
||||
@if($documento->file_path)
|
||||
@if($documento->isLink())
|
||||
<a href="{{ $documento->url }}" target="_blank" class="btn btn-xs btn-info" title="Apri link esterno">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</a>
|
||||
@elseif($documento->file_path)
|
||||
<button type="button" class="btn btn-xs btn-primary" onclick="previewDoc({{ $documento->id }}, '{{ $documento->mime_type }}')" title="Anteprima">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
@@ -457,17 +490,22 @@ $tableColumnsJson = json_encode($tableColumns ?? []);
|
||||
<a href="/documenti/{{ $documento->id }}/edit" class="btn btn-xs btn-warning" title="Modifica">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
@if(!$documento->isLink())
|
||||
<button type="button" class="btn btn-xs btn-secondary" onclick="showMoveModal({{ $documento->id }}, '{{ e($documento->nome_file) }}')" title="Sposta in altra cartella">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
</button>
|
||||
@endif
|
||||
<button type="button" class="btn btn-xs btn-info" onclick="showAssociateModal({{ $documento->id }})" title="Associa a...">
|
||||
<i class="fas fa-link"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-xs btn-secondary" onclick="showUpdateModal({{ $documento->id }})" title="Cambia tipo/contesto">
|
||||
<i class="fas fa-tag"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-xs btn-info" onclick="openSingleTag({{ $documento->id }})" title="Assegna tag">
|
||||
<i class="fas fa-tags"></i>
|
||||
</button>
|
||||
@endif
|
||||
@if($canDeleteDocumenti)
|
||||
@if($canWriteDocumenti)
|
||||
<button type="button" class="btn btn-xs btn-danger" onclick="deleteDocumento({{ $documento->id }}, '{{ e($documento->nome_file) }}')" title="Elimina">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
@@ -613,10 +651,28 @@ $tableColumnsJson = json_encode($tableColumns ?? []);
|
||||
</div>
|
||||
@endif
|
||||
<div class="form-group">
|
||||
<label>Tipo caricamento</label>
|
||||
<div class="btn-group btn-group-toggle d-block" data-toggle="buttons">
|
||||
<label class="btn btn-outline-primary active" id="tipoFileLabel">
|
||||
<input type="radio" name="tipo_caricamento" value="file" checked onchange="toggleTipoCaricamento()">
|
||||
<i class="fas fa-upload mr-1"></i> File
|
||||
</label>
|
||||
<label class="btn btn-outline-info" id="tipoLinkLabel">
|
||||
<input type="radio" name="tipo_caricamento" value="link" onchange="toggleTipoCaricamento()">
|
||||
<i class="fas fa-link mr-1"></i> URL esterno
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" id="fileUploadGroup">
|
||||
<label>File *</label>
|
||||
<input type="file" name="file" class="form-control" required>
|
||||
<input type="file" name="file" class="form-control" id="fileInput">
|
||||
<small class="text-muted">Max 10MB</small>
|
||||
</div>
|
||||
<div class="form-group" id="urlUploadGroup" style="display:none;">
|
||||
<label>URL esterno *</label>
|
||||
<input type="url" name="url" class="form-control" id="urlInput" placeholder="https://esempio.com/documento.pdf">
|
||||
<small class="text-muted">Inserisci l'indirizzo web del documento</small>
|
||||
</div>
|
||||
<div class="form-group" id="uploadFolderGroup">
|
||||
<label>Cartella</label>
|
||||
<select name="cartella_id" id="uploadCartellaId" class="form-control">
|
||||
@@ -635,6 +691,10 @@ $tableColumnsJson = json_encode($tableColumns ?? []);
|
||||
</button>
|
||||
</div>
|
||||
@if($currentRepo)
|
||||
<div class="form-group">
|
||||
<label>Tag (opzionale)</label>
|
||||
@include("partials._tag-selector", ["selectedTags" => [], "label" => "Tag"])
|
||||
</div>
|
||||
<input type="hidden" name="repository_id" value="{{ $currentRepo->id }}">
|
||||
<p class="text-muted small mb-0">Caricamento su repository remoto: <strong>{{ $currentRepo->nome }}</strong></p>
|
||||
@endif
|
||||
@@ -1143,6 +1203,26 @@ function previewDoc(id, mimeType) {
|
||||
$('#previewModal').modal('show');
|
||||
}
|
||||
|
||||
function toggleTipoCaricamento() {
|
||||
var tipo = document.querySelector('input[name="tipo_caricamento"]:checked').value;
|
||||
var fileGroup = document.getElementById('fileUploadGroup');
|
||||
var urlGroup = document.getElementById('urlUploadGroup');
|
||||
var fileInput = document.getElementById('fileInput');
|
||||
var urlInput = document.getElementById('urlInput');
|
||||
|
||||
if (tipo === 'link') {
|
||||
fileGroup.style.display = 'none';
|
||||
urlGroup.style.display = 'block';
|
||||
fileInput.removeAttribute('required');
|
||||
urlInput.setAttribute('required', 'required');
|
||||
} else {
|
||||
fileGroup.style.display = 'block';
|
||||
urlGroup.style.display = 'none';
|
||||
fileInput.setAttribute('required', 'required');
|
||||
urlInput.removeAttribute('required');
|
||||
}
|
||||
}
|
||||
|
||||
function loadUploadTargets() {
|
||||
const visibilita = document.getElementById('uploadVisibilita').value;
|
||||
const group = document.getElementById('uploadTargetGroup');
|
||||
@@ -1281,17 +1361,44 @@ function showMoveModal(id, nome) {
|
||||
$('#moveModal').modal('show');
|
||||
}
|
||||
|
||||
document.getElementById('massMoveModal')?.addEventListener('show.bs.modal', function() {
|
||||
$('#massMoveModal').on('show.bs.modal', function() {
|
||||
const ids = getSelectedIds();
|
||||
document.getElementById('massMoveIds').value = ids.join(',');
|
||||
document.getElementById('massMoveCount').textContent = ids.length;
|
||||
});
|
||||
|
||||
document.getElementById('massTagModal')?.addEventListener('show.bs.modal', function() {
|
||||
const ids = getSelectedIds();
|
||||
function showMassTagModal() {
|
||||
var ids = getSelectedIds();
|
||||
|
||||
if (ids.length === 0) {
|
||||
const preSelected = document.querySelector('.doc-checkbox[data-pre-selected="true"]');
|
||||
if (preSelected) {
|
||||
ids = [preSelected.value];
|
||||
preSelected.removeAttribute('data-pre-selected');
|
||||
}
|
||||
}
|
||||
|
||||
if (ids.length === 0) {
|
||||
alert('Seleziona almeno un documento');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('massTagIds').value = ids.join(',');
|
||||
document.getElementById('massTagCount').textContent = ids.length;
|
||||
});
|
||||
$('#massTagModal').modal('show');
|
||||
}
|
||||
|
||||
function openSingleTag(docId) {
|
||||
document.querySelectorAll('.doc-checkbox').forEach(cb => cb.checked = false);
|
||||
|
||||
let cb = document.querySelector('.doc-checkbox[value="' + docId + '"]');
|
||||
if (cb) {
|
||||
cb.checked = true;
|
||||
cb.dispatchEvent(new Event('change'));
|
||||
}
|
||||
|
||||
showMassTagModal();
|
||||
}
|
||||
|
||||
document.getElementById('massMoveNewFolderBtn')?.addEventListener('click', function() {
|
||||
Swal.fire({
|
||||
|
||||
@@ -49,38 +49,63 @@
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.download-btn {
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 10px 24px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.download-btn:hover {
|
||||
.btn-primary {
|
||||
background-color: #007bff;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background-color: #0056b3;
|
||||
color: white;
|
||||
}
|
||||
.btn-info {
|
||||
background-color: #17a2b8;
|
||||
}
|
||||
.btn-info:hover {
|
||||
background-color: #138496;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="fallback-container">
|
||||
<div class="fallback-icon">📄</div>
|
||||
<div class="fallback-title">Anteprima non disponibile</div>
|
||||
<div class="fallback-message">
|
||||
Questo tipo di file non può essere visualizzato nell'anteprima.
|
||||
Scarica il file per visualizzarlo con l'applicazione appropriata.
|
||||
</div>
|
||||
<div class="fallback-file-info">
|
||||
<strong>{{ $documento->nome_file }}</strong>
|
||||
{{ strtoupper(pathinfo($documento->file_path, PATHINFO_EXTENSION)) }} ·
|
||||
{{ number_format($documento->dimensione / 1024, 1) }} KB
|
||||
</div>
|
||||
<a href="/documenti/{{ $documento->id }}/download" class="download-btn">
|
||||
⬇️ Scarica File
|
||||
</a>
|
||||
@if($documento->isLink())
|
||||
<div class="fallback-icon">🔗</div>
|
||||
<div class="fallback-title">Documento esterno</div>
|
||||
<div class="fallback-message">
|
||||
Questo è un collegamento a una risorsa esterna.
|
||||
Clicca il pulsante qui sotto per aprirla.
|
||||
</div>
|
||||
<div class="fallback-file-info">
|
||||
<strong>{{ $documento->nome_file }}</strong>
|
||||
URL esterno
|
||||
</div>
|
||||
<a href="{{ $documento->url }}" target="_blank" class="btn btn-info">
|
||||
Apri link esterno
|
||||
</a>
|
||||
@else
|
||||
<div class="fallback-icon">📄</div>
|
||||
<div class="fallback-title">Anteprima non disponibile</div>
|
||||
<div class="fallback-message">
|
||||
Questo tipo di file non può essere visualizzato nell'anteprima.
|
||||
Scarica il file per visualizzarlo con l'applicazione appropriata.
|
||||
</div>
|
||||
<div class="fallback-file-info">
|
||||
<strong>{{ $documento->nome_file }}</strong>
|
||||
{{ $documento->file_path ? strtoupper(pathinfo($documento->file_path, PATHINFO_EXTENSION)) : '-' }} ·
|
||||
{{ $documento->dimensione ? number_format($documento->dimensione / 1024, 1) . ' KB' : '-' }}
|
||||
</div>
|
||||
<a href="/documenti/{{ $documento->id }}/download" class="btn btn-primary">
|
||||
Scarica File
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="form-group">
|
||||
<label for="mittente_id">Da (mittente)</label>
|
||||
<select name="mittente_id" id="mittente_id" class="form-control">
|
||||
<option value="0">Sistema — {{ optional(\App\Models\EmailSetting::getActive())->email_address ?? 'Configura email' }}</option>
|
||||
<option value="0">Sistema — {{ optional(\App\Models\EmailSetting::getActive())->getEffectiveFromAddress() ?? 'Configura email' }}</option>
|
||||
@foreach($senderAccounts as $sender)
|
||||
<option value="{{ $sender->id }}">{{ $sender->email_name ?: $sender->email_address }} <{{ $sender->email_address }}></option>
|
||||
@endforeach
|
||||
@@ -45,7 +45,7 @@
|
||||
<div id="individui-input" class="destinatari-section" style="display: none;">
|
||||
<select name="individui[]" id="individui-select" class="form-control" multiple size="5">
|
||||
@foreach($individui as $ind)
|
||||
<option value="{{ $ind->id }}">{{ $ind->cognito }} {{ $ind->nome }} ({{ $ind->email_primaria ?? 'senza email' }})</option>
|
||||
<option value="{{ $ind->id }}">{{ $ind->nome }} {{ $ind->cognome }} ({{ $ind->email_primaria ?? 'senza email' }})</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<small class="text-muted">Tieni premuto Ctrl per selezionare più individui</small>
|
||||
@@ -84,6 +84,19 @@
|
||||
<textarea name="body" id="body" class="form-control" rows="15" required>{{ $prefill['body'] ?? '' }}</textarea>
|
||||
</div>
|
||||
|
||||
@if($firme->count() > 0)
|
||||
<div class="form-group">
|
||||
<label for="firma_id">Firma</label>
|
||||
<select name="firma_id" id="firma_id" class="form-control">
|
||||
<option value="">Nessuna</option>
|
||||
@foreach($firme as $firma)
|
||||
<option value="{{ $firma->id }}">{{ $firma->nome }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<small class="text-muted">Seleziona la firma da appender al messaggio.</small>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="form-group">
|
||||
<label>Allegati</label>
|
||||
<div class="border rounded p-3 bg-light">
|
||||
@@ -147,7 +160,7 @@
|
||||
<label class="custom-control-label" for="doc-{{ $doc->id }}"></label>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ $doc->nome_file }}</td>
|
||||
<td>{{ $doc->nome_file }} @if($doc->isLink()) <span class="badge badge-info"><i class="fas fa-link mr-1"></i>URL</span> @endif</td>
|
||||
<td><span class="badge badge-info">{{ $doc->tipologia ?: $doc->tipo }}</span></td>
|
||||
</tr>
|
||||
@endforeach
|
||||
|
||||
@@ -19,6 +19,12 @@ $entityType = $entityType ?? 'eventi';
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
@if(session('error'))
|
||||
<div class="alert alert-danger alert-dismissible">
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
@@ -33,12 +39,15 @@ $entityType = $entityType ?? 'eventi';
|
||||
<button type="button" class="btn btn-sm btn-success mr-2" onclick="exportSelectedIcs()">
|
||||
<i class="fas fa-calendar-plus mr-1"></i> Esporta ICS
|
||||
</button>
|
||||
@if($canWriteEventi)
|
||||
<button type="button" class="btn btn-sm btn-info mr-2" onclick="showMassTagModal()">
|
||||
<i class="fas fa-tags mr-1"></i> Tag
|
||||
</button>
|
||||
@endif
|
||||
@if($canDeleteEventi)
|
||||
<div class="btn-group mr-2">
|
||||
<button type="button" class="btn btn-sm btn-danger" onclick="deleteSelected()">
|
||||
<i class="fas fa-trash mr-1"></i> Elimina Selezionati
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-danger" onclick="deleteSelected()">
|
||||
<i class="fas fa-trash mr-1"></i> Elimina Selezionati
|
||||
</button>
|
||||
@endif
|
||||
@if($canWriteEventi)
|
||||
<a href="/eventi/import" class="btn btn-sm btn-warning mr-2">
|
||||
@@ -86,7 +95,7 @@ $entityType = $entityType ?? 'eventi';
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th style="width: 40px;" data-column="select">
|
||||
@if($canDeleteEventi)
|
||||
@if($canWriteEventi)
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="select-all" onchange="toggleSelectAll(this)">
|
||||
<label class="custom-control-label" for="select-all"></label>
|
||||
@@ -187,7 +196,7 @@ $entityType = $entityType ?? 'eventi';
|
||||
@forelse($eventi as $evento)
|
||||
<tr data-id="{{ $evento->id }}">
|
||||
<td>
|
||||
@if($canDeleteEventi)
|
||||
@if($canWriteEventi)
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input row-checkbox" id="row-checkbox-{{ $evento->id }}" value="{{ $evento->id }}">
|
||||
<label class="custom-control-label" for="row-checkbox-{{ $evento->id }}"></label>
|
||||
@@ -277,6 +286,9 @@ $entityType = $entityType ?? 'eventi';
|
||||
<a href="{{ url('/eventi/' . $evento->id . '/edit') }}" class="btn btn-xs btn-warning" title="Modifica">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-xs btn-info" onclick="openSingleTag({{ $evento->id }})" title="Assegna tag">
|
||||
<i class="fas fa-tags"></i>
|
||||
</button>
|
||||
<form action="{{ url('/eventi/' . $evento->id) }}" method="POST" class="d-inline">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn btn-xs btn-danger" onclick="return confirm('Confermi l\'eliminazione?')" title="Elimina">
|
||||
@@ -327,8 +339,51 @@ $entityType = $entityType ?? 'eventi';
|
||||
|
||||
<div id="vista-data" style="display:none;">{{ $vistaDefaultJson }}</div>
|
||||
|
||||
{{-- MODAL: Gestione Tag massiva --}}
|
||||
<div class="modal fade" id="massTagModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="fas fa-tags mr-2"></i>Gestione Tag</h5>
|
||||
<button type="button" class="close" onclick="$('#massTagModal').modal('hide')">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
<form id="massTagForm" method="POST" action="/eventi/mass-tag">
|
||||
@csrf
|
||||
<div class="modal-body">
|
||||
<p class="mb-2">Operazione su <strong id="massTagCount">0</strong> eventi selezionati.</p>
|
||||
|
||||
<div class="form-group mb-3">
|
||||
<label class="font-weight-bold">Azione</label>
|
||||
<div class="d-flex" style="gap:1.5rem;">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="mode" id="mtModeAssign" value="assign" checked>
|
||||
<label class="form-check-label" for="mtModeAssign">Assegna tag</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="mode" id="mtModeRemove" value="remove">
|
||||
<label class="form-check-label" for="mtModeRemove">Rimuovi tag</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@include('partials._tag-selector', ['label' => 'Seleziona tag'])
|
||||
|
||||
<input type="hidden" name="ids" id="massTagIds" value="">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="$('#massTagModal').modal('hide')">Annulla</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-tags mr-1"></i> Applica
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog">
|
||||
@if($canDeleteEventi)
|
||||
@if($canWriteEventi)
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-danger">
|
||||
@@ -358,10 +413,28 @@ function toggleSelectAll(source) {
|
||||
document.querySelectorAll('.row-checkbox').forEach(cb => cb.checked = source.checked);
|
||||
}
|
||||
|
||||
function showMassTagModal() {
|
||||
const ids = getSelectedIds();
|
||||
if (ids.length === 0) {
|
||||
alert('Seleziona almeno un evento');
|
||||
return;
|
||||
}
|
||||
document.getElementById('massTagIds').value = ids.join(',');
|
||||
document.getElementById('massTagCount').textContent = ids.length;
|
||||
$('#massTagModal').modal('show');
|
||||
}
|
||||
|
||||
function getSelectedIds() {
|
||||
return Array.from(document.querySelectorAll('.row-checkbox:checked')).map(cb => cb.value);
|
||||
}
|
||||
|
||||
function openSingleTag(eventId) {
|
||||
document.querySelectorAll('.row-checkbox').forEach(cb => cb.checked = false);
|
||||
var cb = document.getElementById('row-checkbox-' + eventId);
|
||||
if (cb) cb.checked = true;
|
||||
showMassTagModal();
|
||||
}
|
||||
|
||||
function deleteSelected() {
|
||||
const selectedIds = getSelectedIds();
|
||||
if (selectedIds.length === 0) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user