Compare commits
52 Commits
3fb653f4bf
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 005fa5b4a3 | |||
| 7302146a77 | |||
| 43a2c81312 | |||
| 0dd567cf7a | |||
| d9421c9159 | |||
| c959f72c66 | |||
| 7999c663fe | |||
| 708cadfac2 | |||
| a35b0056e1 | |||
| b7842bf2e0 | |||
| 06387568cb | |||
| d82b123bfd | |||
| 6f106e3130 | |||
| ce48481bc8 | |||
| 7d7d384a0d | |||
| 8194b8c034 | |||
| 90093a086b | |||
| cbe3470c8c | |||
| 469e8c014f | |||
| 4dd4da7f4b | |||
| cd10d100ec | |||
| 406985eb1e | |||
| f9c1268466 | |||
| 89382fe4ff | |||
| dd76b473f7 | |||
| cf0ca170ce | |||
| 82c54002e4 | |||
| bc57d0dcd0 | |||
| 139098bffc | |||
| a8c841bdbe | |||
| 00004d8b50 | |||
| 2fd1ed8295 | |||
| 8aa1d3d031 | |||
| d57431b10b | |||
| 05f1a4c396 | |||
| 2f454ab749 | |||
| da022bdf4a | |||
| 6b637fc12b | |||
| 72c48d301d | |||
| abb39e62f9 | |||
| f36a9906a2 | |||
| fcbb6fb061 | |||
| 25168debc0 | |||
| 7381d86362 | |||
| 8404cda475 | |||
| 75f6665252 | |||
| 81541a3af5 | |||
| c6f8403d1a | |||
| 6b913f330e | |||
| 1e9eb29428 | |||
| c9d66c2a80 | |||
| aa4e582925 |
@@ -0,0 +1,3 @@
|
|||||||
|
glastree*.tar.gz
|
||||||
|
glastree*.tar
|
||||||
|
.env
|
||||||
@@ -1,48 +1,425 @@
|
|||||||
# 🧠 MEMORY.md — Stato Progetto
|
# 🧠 MEMORY.md — Stato Progetto
|
||||||
|
|
||||||
## Goal
|
## Obiettivo
|
||||||
- Stabilizzare il modale "Carica Documento" e migliorare la gestione cartelle durante l'upload.
|
App gestionale Laravel 13 con AdminLTE 4 per gestione Persone e Gruppi.
|
||||||
|
|
||||||
## Constraints & Preferences
|
## Funzionalità Implementate
|
||||||
- (none)
|
|
||||||
|
|
||||||
## Progress
|
### Page-length selector
|
||||||
### Done
|
- **4 pagine**: Individui, Eventi, Email, Gruppi
|
||||||
- Aggiunto pulsante "Scarica Guida PDF" nella sidebar di `/help` con nuovo metodo `HelpController::downloadPdf()` e route `GET /help/pdf/download`
|
- **Valori**: 10, 20, 25, 50, 100 (default 20)
|
||||||
- Aggiunta card "Backup" in dashboard (small-box bg-navy, icona fa-hdd) con conteggio backup, visibile solo a utenti con `canManage('settings')`
|
- Dropdown nel card-footer con campi hidden per preservare query params
|
||||||
- Corretto `modal-body` nel modale `#uploadModal` che chiudeva prematuramente (pulsanti Annulla/Carica finivano fuori dal popup)
|
- GruppoController: usa `LengthAwarePaginator` per paginare la collezione ordinata gerarchicamente
|
||||||
- Aggiunto campo `visibilita` (select Pubblico/Individuo/Gruppo) nel form di upload documento, mancante e richiesto dal controller
|
|
||||||
- Aggiunto selettore cartelle visibile (`<select name="cartella_id" id="uploadCartellaId">`) nel modale di upload (sostituisce il vecchio hidden input)
|
|
||||||
- Aggiunto pulsante "Nuova cartella" sia nel modale upload (`#uploadNewFolderBtn`) che nel modale "Sposta" (`#massMoveNewFolderBtn`)
|
|
||||||
- Sostituito SweetAlert2 per creazione cartella con il modale Bootstrap `#newFolderModal` già esistente (risolve focus trap Bootstrap 4 che bloccava l'input)
|
|
||||||
- Aggiornato `toggleUploadRepo()` per nascondere/mostrare il gruppo cartella in base alla selezione del repository remoto
|
|
||||||
- Aggiunto flag `window._uploadNewFolder` per comportamento differenziato: da upload aggiunge opzione al select senza ricaricare, da tooltip ricarica la pagina
|
|
||||||
- **Fix backdrop grigio persistente dopo creazione cartella**: ora `#uploadModal` viene prima nascosto (quando si clicca "Nuova cartella") e poi ri-mostrato dopo la creazione nel success handler, risolvendo il conflitto di backdrop tra modali annidati Bootstrap 4
|
|
||||||
- **Fix scroll modal upload**: ristrutturato `<form>` per non avvolgere `.modal-body` e `.modal-footer` (ora è dentro `.modal-body`), così `modal-dialog-scrollable` funziona correttamente. Pulsante submit ha `form="uploadForm"` per collegamento cross-DOM
|
|
||||||
|
|
||||||
### In Progress
|
### CSV Import Gruppi
|
||||||
- (none)
|
- Stesso pattern di `IndividuoController` (import GET, importStore POST, downloadTemplate GET)
|
||||||
|
- Colonne: nome, descrizione, parent_id, diocesi_id, indirizzo_incontro, cap_incontro, città_incontro, sigla_provincia_incontro
|
||||||
|
- Fault-tolerant: skip righe vuote, log errori, try/catch su ogni riga
|
||||||
|
|
||||||
### Blocked
|
### ICS Import Eventi
|
||||||
- (none)
|
- Usa `Sabre\VObject\Reader::read()` già in vendor/
|
||||||
|
- Mapping: SUMMARY→nome_evento, DESCRIPTION→descrizione, DTSTART→data_specifica+ora_inizio, DTEND→durata_minuti, LOCATION→luogo_indirizzo, UID→uid_esterno
|
||||||
|
- Dedup via uid_esterno
|
||||||
|
- RRULE: import come evento singolo con prima occorrenza
|
||||||
|
|
||||||
## Key Decisions
|
### build-dist.sh (script di distribuzione)
|
||||||
- Replicato il codice di creazione cartella con `SweetAlert2` → sostituito con modale Bootstrap `#newFolderModal` perché Bootstrap 4 impedisce il focus su elementi fuori dal `.modal` (focus trap)
|
- Genera `glastree-YYYYMMDD_HHMM.tar.gz`
|
||||||
- Pulsante "Nuova cartella" nello stesso `<form>` del modal di upload riceve `e.preventDefault()` per sicurezza
|
- Include: sorgenti, vendor (production)
|
||||||
- Per evitare conflitti di backdrop Bootstrap 4 con modali annidati, si nasconde `#uploadModal` prima di mostrare `#newFolderModal` e lo si ripristina dopo il success
|
- Esclude: .git, node_modules, tests, cache, storage content, vendor/docs/tests, Docker
|
||||||
|
- **Istruzioni post-estrazione complete**: mkdir, permessi, configurazione, cache clear
|
||||||
|
|
||||||
## Next Steps
|
## Tag System
|
||||||
- (none)
|
|
||||||
|
|
||||||
## Critical Context
|
### Implementazione Completa (Opzione B)
|
||||||
- Bootstrap 4.6.2 con focus trap: impedisce focus su elementi fuori dal `.modal` — SweetAlert2 aggiunge dialog al `<body>`, quindi il suo input non riceve focus
|
- **Migration**: `2026_06_08_194155_create_tags_tables.php` — tabelle `tags` + `taggables`
|
||||||
- `#newFolderModal` già esisteva con submit handler AJAX a `/documenti/cartelle` e ricaricava pagina — ora con `window._uploadNewFolder` evita reload quando chiamato dall'upload
|
- **Model `Tag.php`**: morphToMany a 5 entity types, auto-slug su creating, color badge
|
||||||
- Bug: backdrop grigio non spariva dopo creazione cartella perché Bootstrap 4 gestisce male i modali annidati (due backdrop concorrenti)
|
- **Trait `HasTagsLight.php`**: `tags()` morphToMany, scopes `withAllTags`/`withAnyTags`
|
||||||
|
- **5 Models aggiornati**: Individuo, Gruppo, Evento, Documento, MailingList — usano `HasTagsLight`
|
||||||
|
- **ImpostazioniController**: CRUD completo per Tag (store, update, destroy, reorder) con tab nella pagina Impostazioni
|
||||||
|
- **Tag selector**: `_tag-selector.blade.php` — search + pill badges con colori, incluso in create/edit di Individui, Gruppi, Eventi, MailingList
|
||||||
|
- **Tag filtering**: `?tag[]=slug` supportato in `IndividuoController@index`, `GruppoController@index`, `EventoController@index`, `DocumentoController@index`, `MailingListController@index` via `withAnyTags` scope
|
||||||
|
- **Tag column in index views**: Colonna "Tag" con badge colorati cliccabili (collegamento a index filtrato) in individui, gruppi, eventi, documenti, mailing-liste
|
||||||
|
- **Tag filter widget**: `_tag-filter-bar.blade.php` — barra cliccabile con badge colorati sopra ogni index table (individui, gruppi, eventi, documenti, mailing-liste). Attiva/disattiva filtro `?tag[]=` con un click, evidenzia tag attivi, include "Cancella filtri" quando attivo
|
||||||
|
- **RicercaController**: Controller per ricerca unificata per tag — mostra risultati aggregati da tutte 5 entity types con info-box counters
|
||||||
|
- **Route `GET /ricerca`**: Registrata come `ricerca.index` → `RicercaController@index`
|
||||||
|
- **Sidebar link**: "Ricerca per Tag" (icona `fas fa-tags`) sotto la sezione Report, visibile con permesso `individui`
|
||||||
|
- **Mass tag action for Documenti**: `POST /documenti/mass-tag` → `DocumentoController@massTag` — modal with tag selector + assign/remove radio, processes selected documents in chunks of 100 via `syncWithoutDetaching()` (assign) or `detach()` (remove)
|
||||||
|
- **Report tag columns**: `tags.nome` disponibile come colonna per individui/gruppi/eventi/documenti nei report personalizzati
|
||||||
|
- **Report tag filters**: Custom report form include tag multi-select (Select2) + modalità OR/AND; `runCustomReport()` applica filtri via `withAllTags`/`withAnyTags`
|
||||||
|
|
||||||
## Relevant Files
|
## Bug Fix Recenti
|
||||||
- `app/Http/Controllers/HelpController.php`: metodo `downloadPdf()` + route `/help/pdf/download`
|
|
||||||
- `app/Http/Controllers/HomeController.php`: `$stats['backups']` via `BackupService::list()`
|
### 2026-06-07 — Colonne mancanti in eventi e gruppo_individuo
|
||||||
- `routes/web.php`: route `/help/pdf/download`
|
- `descrizione_evento` e `is_incontro_gruppo` mancanti in install.sql → creata migration + ALTER TABLE
|
||||||
- `resources/views/help/index.blade.php`: card-footer con pulsante download PDF
|
- `ruolo_nel_gruppo` mancante in install.sql per pivot `gruppo_individuo`
|
||||||
- `resources/views/home/dashboard.blade.php`: small-box Backup condizionale
|
- `VistaReport.php`: aggiunto `is_default` a `$fillable`
|
||||||
- `resources/views/documenti/index.blade.php`: intero modale `#uploadModal` riscritto (visibilita, cartella select, nuova cartella via `#newFolderModal`, toggleUploadRepo); fix backdrop persistente nascondendo/rimostrando `#uploadModal`
|
|
||||||
|
### 2026-06-08 — build-dist.sh: "provide valid cache path" su server remoto
|
||||||
|
**Problema**: Le esclusioni in `build-dist.sh` usavano `--exclude='storage/framework/cache'` (e simili per sessions, views, logs), che escludevano **l'intera directory** dall'archivio. Dopo l'estrazione, le directory necessarie a Laravel non esistevano.
|
||||||
|
|
||||||
|
**Fix**:
|
||||||
|
1. Cambiati exclude patterns per **contenuti soltanto** (non la directory):
|
||||||
|
- `--exclude='storage/framework/cache/*'` (non `/cache`)
|
||||||
|
- `--exclude='storage/framework/sessions/*'`
|
||||||
|
- `--exclude='storage/framework/views/*'`
|
||||||
|
- `--exclude='storage/logs/*'`
|
||||||
|
- `--exclude='storage/debugbar/*'`
|
||||||
|
2. Aggiunte esclusioni per upload utente:
|
||||||
|
- `--exclude='storage/app/public'`
|
||||||
|
- `--exclude='storage/app/private'`
|
||||||
|
3. Creato `.gitignore` in `storage/framework/views/` (mancante)
|
||||||
|
4. Istruzioni post-estrazione riscritte con:
|
||||||
|
- `mkdir -p` per tutte le directory necessarie (cache, sessions, views, logs, public, backups, documenti, bootstrap/cache)
|
||||||
|
- `chmod -R 775` e `chown -R www-data:www-data` per permessi
|
||||||
|
- `php artisan config:clear`, `route:clear`, `view:clear`
|
||||||
|
|
||||||
|
## Problema 419 (Page Expired) su Produzione
|
||||||
|
Il codice NON ha errori di CSRF — tutte le form hanno `@csrf`. Il problema è configurazione sessione del server:
|
||||||
|
1. `storage/framework/sessions/` non scrivibile dal web server
|
||||||
|
2. `APP_URL` in `.env` non corrisponde all'URL reale
|
||||||
|
3. `SESSION_SECURE_COOKIE=true` ma sito in HTTP
|
||||||
|
|
||||||
|
Soluzione:
|
||||||
|
```bash
|
||||||
|
chmod -R 775 storage/framework/sessions
|
||||||
|
php artisan config:clear
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2026-06-08 — build-dist.sh: impossibilità salvare logo su server remoto
|
||||||
|
**Problema**:
|
||||||
|
- `public/storage` era un symlink **assoluto** (`/var/www/html/glastree/storage/app/public`) incluso nell'archivio → all'estrazione su altro server puntava a path inesistente.
|
||||||
|
- `php artisan storage:link` falliva perché il symlink rotto esisteva già.
|
||||||
|
- Sottodirectory `logos/`, `documenti/eventi/`, `avatars/gruppi/` non create dal `mkdir -p`.
|
||||||
|
|
||||||
|
**Fix**:
|
||||||
|
1. Aggiunto `--exclude='public/storage'` al tar — il symlink va ricreato sul target.
|
||||||
|
2. Aggiunto `rm -f public/storage` prima di `php artisan storage:link` per evitare conflitto.
|
||||||
|
3. Usato `ln -sf ../storage/app/public public/storage` (invece di `php artisan storage:link --relative` che richiede `symfony/filesystem` non sempre presente).
|
||||||
|
4. Aggiunte al `mkdir -p` le sottodirectory mancanti:
|
||||||
|
- `storage/app/public/logos` (logo upload)
|
||||||
|
- `storage/app/public/documenti/eventi` (event documenti)
|
||||||
|
- `storage/app/private/avatars/gruppi` (avatar gruppi)
|
||||||
|
5. Aggiunta creazione `.gitignore` in `storage/app/public/` (placeholder Laravel).
|
||||||
|
|
||||||
|
### Fix form annidato salvataggio email
|
||||||
|
**2026-06-08 — Problema**: Il form di DELETE era **annidato dentro** il form di SAVE in entrambe le pagine:
|
||||||
|
- `resources/views/impostazioni/index.blade.php`
|
||||||
|
- `resources/views/admin/email-settings/index.blade.php`
|
||||||
|
- HTML invalido: il browser chiudeva implicitamente il form SAVE all'apertura del form DELETE, causando l'invio alla route sbagliata.
|
||||||
|
|
||||||
|
**Fix**: Spostato il form DELETE fuori dal form SAVE, separato in un `div` autonomo.
|
||||||
|
|
||||||
|
### Fix campi mancanti in validazione EmailSettingsController@save
|
||||||
|
**2026-06-08**: `signature`, `signature_enabled`, `smtp_username`, `smtp_password` non erano nelle regole di validazione di `save()`. Laravel scarta i campi non validati, quindi non venivano mai scritti nel DB.
|
||||||
|
|
||||||
|
**Fix**: Aggiunte le regole mancanti a `$request->validate()`.
|
||||||
|
|
||||||
|
### DiagnoseEmail Command
|
||||||
|
- `php artisan diagnose:email` — controlla APP_KEY, tabella email_settings, record, decrittazione password, sender_accounts, log errori, estensioni PHP.
|
||||||
|
|
||||||
|
### 2026-06-08 — Firma email ancora non salvata (secondo tentativo)
|
||||||
|
**Problema**: La firma (campo `signature`) non veniva salvata nonostante le regole di validazione fossero state aggiunte.
|
||||||
|
|
||||||
|
**Root cause analysis**:
|
||||||
|
1. HTML checkbox `signature_enabled` senza hidden fallback: quando non spuntato, il campo non veniva inviato → non presente in `$validated` → `updateOrCreate` non lo aggiornava mai a `false`.
|
||||||
|
2. La vecchia pagina `/impostazioni/` aveva l'handler `shown.bs.tab` per il tab Firma (`#email-firma`) in un solo punto su due, causando possibile mancata inizializzazione dell'editor Quill su alcuni flussi di navigazione.
|
||||||
|
3. La verifica nel controller controllava solo `imap_host` e `email_address` — anche se signature/signature_enabled fallivano, l'utente vedeva "salvato con successo".
|
||||||
|
|
||||||
|
**Fix**:
|
||||||
|
1. `resources/views/admin/email-settings/index.blade.php:268` — Aggiunto `<input type="hidden" name="signature_enabled" value="0">` prima della checkbox
|
||||||
|
2. `resources/views/impostazioni/index.blade.php:603` — Stesso hidden fallback
|
||||||
|
3. `resources/views/impostazioni/index.blade.php:1633` — Aggiunto `#email-firma` al primo handler `shown.bs.tab` (il secondo handler esisteva già ma il primo no)
|
||||||
|
4. `app/Http/Controllers/Admin/EmailSettingsController.php:70-82` — Aggiunto log di warning se signature o signature_enabled non corrispondono dopo il salvataggio
|
||||||
|
|
||||||
|
### 2026-06-08 — Sync email: "Nessuna configurazione email attiva" nonostante test IMAP funzioni
|
||||||
|
**Problema**: Cliccando "Sincronizza Ora" nelle impostazioni email o "Ricevi/Invia" nella pagina email, viene mostrato "Nessuna configurazione email attiva". Il test IMAP funziona correttamente.
|
||||||
|
|
||||||
|
**Root cause**: Stesso bug dei checkbox senza hidden fallback. Il campo `is_active` nel tab Sincronizzazione non veniva inviato quando non spuntato. Alla prima configurazione, se l'utente non visitava esplicitamente il tab Sync e attivava lo switch, `is_active` rimaneva `false` (default DB). Il test IMAP usa `EmailSetting::first()` (ignora `is_active`), mentre `syncEmails()` e `syncNow()` usano `EmailSetting::getActive()` che filtra per `is_active = true`.
|
||||||
|
|
||||||
|
**Fix**:
|
||||||
|
1. `resources/views/admin/email-settings/index.blade.php:251` — Aggiunto `<input type="hidden" name="is_active" value="0">` prima della checkbox
|
||||||
|
2. `resources/views/impostazioni/index.blade.php:586` — Stesso hidden fallback
|
||||||
|
3. `EmailSetting::getActive()` ora riceve sempre `is_active` dal form (0 o 1) grazie all'hidden fallback
|
||||||
|
|
||||||
|
## 2026-06-08 — Help page: aggiunto tab "Calendario" con documentazione OAuth
|
||||||
|
|
||||||
|
- **`resources/views/help/index.blade.php`**: Aggiunto tab "Calendario" nel menu laterale (dopo Google Drive) e relativo tab-pane content con:
|
||||||
|
- Requisiti (account Google, API Calendar, OAuth credentials)
|
||||||
|
- Passo 1: Google Cloud Console — abilitare Google Calendar API, creare OAuth Client ID (Web Application), redirect URI `https://developers.google.com/oauthplayground`
|
||||||
|
- Passo 2: Ottenere Refresh Token via Google OAuth Playground (Use your own OAuth credentials, scope `calendar`)
|
||||||
|
- Passo 3: Configurazione nell'app (Impostazioni → Calendario, nuova connessione Google Calendar, inserire Client ID/Secret/Refresh Token)
|
||||||
|
- Test connessione e sincronizzazione
|
||||||
|
- Nota: refresh token permanente finché non revocato
|
||||||
|
- **`resources/views/help/pdf.blade.php`**: Aggiunta sezione "3. Google Calendar" nel TOC e contenuto, rinumerazione da 4 a 8 per sezioni successive
|
||||||
|
|
||||||
|
### Differenza chiave Google Calendar vs Google Drive
|
||||||
|
- **Drive**: endpoint server `/auth/google-drive/callback` che gestisce automaticamente il callback OAuth e salva refresh token in `storage_repositories`
|
||||||
|
- **Calendar**: **NON** ha endpoint server dedicato. Refresh token ottenuto manualmente via Google OAuth Playground e incollato nel campo "Refresh Token" del form
|
||||||
|
|
||||||
|
## 2026-06-08 — Pulsante Sync nella pagina calendario eventi
|
||||||
|
|
||||||
|
- **`app/Http/Controllers/CalendarioConnessioneController.php`**: Aggiunto metodo `syncAll()` che itera tutte le connessioni attive (`is_active = true`), esegue `syncEvents()` per ognuna, e restituisce JSON con risultati aggregati.
|
||||||
|
- **`routes/web.php`**: Aggiunta route `POST /calendario-connessioni/sync-all` → `syncAll` (name: `calendario-connessioni.sync-all`).
|
||||||
|
- **`resources/views/eventi/calendar.blade.php`**: Aggiunto pulsante "Sync" nel card-tools che:
|
||||||
|
- Invia POST AJAX a `sync-all`
|
||||||
|
- Mostra spinner durante l'operazione
|
||||||
|
- Al completamento mostra alert con riepilogo (connessioni, importati, esportati)
|
||||||
|
- Richiama `window.calendar.refetchEvents()` per aggiornare la vista FullCalendar
|
||||||
|
- Esposto `window.calendar` globalmente per accesso dall'handler sync
|
||||||
|
|
||||||
|
## 2026-06-08 — Fix logo non visibile su installazione remota
|
||||||
|
|
||||||
|
**Problema**: Il logo uploadato non veniva visualizzato su installazioni remote, pur salvandosi correttamente su disco.
|
||||||
|
|
||||||
|
**Root cause**: `getLogoPath()` e `getLogoSmallPath()` in `AppSetting.php` usavano `file_exists(public_path('storage/' . $path))` per verificare l'esistenza del file prima di restituire l'URL. Questo controllo dipendeva dal symlink `public/storage → storage/app/public`. Su installazioni remote dove il symlink era assente o mal configurato, `file_exists` restituiva `false` → metodo restituiva `null` → logo non mostrato.
|
||||||
|
|
||||||
|
**Fix**:
|
||||||
|
1. **`app/Models/AppSetting.php`**: Sostituito `file_exists(public_path('storage/' . $path))` con `Storage::disk('public')->exists($path)` e `asset('storage/' . $path)` con `Storage::disk('public')->url($path)`. Il controllo ora avviene direttamente su `storage/app/public/` (disk root), bypassando il symlink.
|
||||||
|
2. **`app/Http/Controllers/ImpostazioniController.php`**: In `uploadLogo()`, aggiunta creazione automatica del symlink `public/storage → storage/app/public` se mancante, per garantire che il browser possa servire l'immagine.
|
||||||
|
|
||||||
|
## 2026-06-08 — Guardie Schema::hasColumn() aggiunte a 18 migration
|
||||||
|
|
||||||
|
**Problema**: 18 migration files aggiungevano colonne a tabelle esistenti senza `Schema::hasColumn()` guard. Se eseguite su DB dove le colonne già esistevano (es. da install.sql), causavano errore "Duplicate column".
|
||||||
|
|
||||||
|
**Fix**: Aggiunto `if (!Schema::hasColumn(...))` wrapper a ogni colonna aggiunta nelle seguenti migration:
|
||||||
|
1. `2026_06_02_000002_add_uid_esterno_to_eventi_table.php` — `uid_esterno`
|
||||||
|
2. `2026_05_12_000002_change_ruolo_to_multi.php` — `ruolo_ids` (up), `ruolo_id` (down)
|
||||||
|
3. `2026_05_28_000001_add_storage_disk_to_documenti.php` — `storage_disk`
|
||||||
|
4. `2026_05_27_113246_add_repository_id_to_documenti.php` — `repository_id`
|
||||||
|
5. `2026_05_26_000003_add_log_falliti_to_mailing_messaggi.php` — `log_falliti`, `mittente_nome`, `mittente_email`
|
||||||
|
6. `2026_05_27_000002_add_cartella_id_to_documenti.php` — `cartella_id`
|
||||||
|
7. `2026_05_16_000004_add_signature_enabled_to_email_settings.php` — `signature_enabled`
|
||||||
|
8. `2026_05_16_000003_add_signature_to_email_settings.php` — `signature`
|
||||||
|
9. `2024_01_01_000017_add_occorrenza_mese_to_eventi.php` — `occorrenza_mese`
|
||||||
|
10. `2024_01_01_000016_add_eventi_recurrence_fields.php` — `giorno_mese`, `mesi_recorrenza`, `mese_annuale`
|
||||||
|
11. `2026_05_10_000002_add_acl_tables.php` — `permissions` (users)
|
||||||
|
12. `2026_05_12_142009_add_location_to_eventi_table.php` — `luogo_indirizzo`, `luogo_url_maps`
|
||||||
|
13. `2026_05_12_000003_change_responsabile_to_multi.php` — `responsabile_ids` (up), `responsabile_id` (down)
|
||||||
|
14. `2024_01_01_000023_add_is_default_to_viste_report_table.php` — `is_default`
|
||||||
|
15. `2024_01_01_000024_add_user_id_to_mailing_lists_table.php` — `user_id`
|
||||||
|
16. `2026_05_12_000001_create_ruoli_table.php` — `ruolo_id` (gruppo_individuo) + data migration annessa
|
||||||
|
17. `2024_01_01_000021_add_user_id_to_documenti_table.php` — `user_id`
|
||||||
|
18. `2024_01_01_000013_add_numero_documento_to_individui_table.php` — `numero_documento`
|
||||||
|
|
||||||
|
Tutte verificate con `php -l` (nessun errore di sintassi).
|
||||||
|
|
||||||
|
### 2026-06-09 — `@stack('scripts')` mancante nel layout
|
||||||
|
|
||||||
|
**Problema**: Il partial `_tag-selector.blade.php` usa `@push('scripts')` per iniettare le funzioni JS `toggleTag`, `removeTag`, `filterTags`. Il layout `adminlte.blade.php` aveva solo `@yield('scripts')` (per `@section`), ma **non** `@stack('scripts')` (per `@push`). Di conseguenza il JavaScript interattivo del tag selector (clic sui badge, ricerca, rimozione) non veniva mai caricato su nessuna pagina create/edit.
|
||||||
|
|
||||||
|
**Fix**: `resources/views/layouts/adminlte.blade.php:305` — Aggiunto `@stack('scripts')` subito prima di `@yield('scripts')`.
|
||||||
|
|
||||||
|
### 2026-06-09 — Tag support completato per MailingList + Report + Ricerca
|
||||||
|
|
||||||
|
**MailingList — Controller & Views** (`app/Http/Controllers/MailingListController.php`):
|
||||||
|
- `index()`: eager-load `tags`, supporto `?tag[]=` filter via `withAnyTags`, pass `$allTags` per filter bar
|
||||||
|
- `create()`: pass `$tags` per tag selector
|
||||||
|
- `store()`: validazione `tags.* exists:tags,id`, sync dopo create. Auto-fetch email da `Individuo::contatti()` quando email non fornita
|
||||||
|
- `edit()`: eager-load `tags`, pass `$tags` + `$selectedTags`
|
||||||
|
- `update()`: validazione `tags.*`, sync se presente, detach se assente. Auto-fetch email da `Individuo::contatti()` quando email non fornita. Diff logica: `$toRemove = array_diff($existingIds, $newIds)` corretto — rimuove solo contatti deselezionati/esplicitamente rimossi
|
||||||
|
- `show()`: eager-load `tags`
|
||||||
|
|
||||||
|
**MailingList — Views**:
|
||||||
|
- `index.blade.php`: aggiunto `@include('partials._tag-filter-bar')`, colonna "Tag" con badge colorati cliccabili
|
||||||
|
- `create.blade.php`: incluso `_tag-selector` dopo descrizione
|
||||||
|
- `edit.blade.php`: incluso `_tag-selector` dopo descrizione (prima della sezione contatti). Submit handler include `#individui-select.selectedOptions` in `contatti_json` con `email: ''` (già presente da fix precedente)
|
||||||
|
- `show.blade.php`: aggiunta card Tag nel pannello sinistro con badge colorati (o "Nessun tag assegnato")
|
||||||
|
|
||||||
|
**ReportColumnRegistry** (`app/Helpers/ReportColumnRegistry.php`):
|
||||||
|
- Aggiunta colonna `'tags.nome'` con label 'Tag' a 4 entity types: individui, gruppi, eventi, documenti
|
||||||
|
|
||||||
|
**ReportController** (`app/Http/Controllers/ReportController.php`):
|
||||||
|
- `runCustomReport()`: supporta `config['tag_filter']` (array di tag IDs) + `config['tag_filter_mode']` ('any'/'all')
|
||||||
|
- `storeCustom()`: ora salva `tag_filter` e `tag_filter_mode` nella config del report salvato
|
||||||
|
- Carica `$allTags` nell'index e passa alla vista
|
||||||
|
- Converte IDs → slugs via `Tag::whereIn('id', ...)->pluck('slug')`, applica `withAllTags`/`withAnyTags`
|
||||||
|
- Eager-load `tags` automaticamente quando la colonna `tags.nome` è selezionata
|
||||||
|
|
||||||
|
**Report — View** (`resources/views/report/index.blade.php`):
|
||||||
|
- Aggiunto filtro tag multi-select nel form "Crea Report Personalizzato" (Select2 con badge colorati)
|
||||||
|
- Aggiunto select per modalità filtro (OR/AND)
|
||||||
|
- JS: `initTagFilterSelect2()` / `destroyTagFilterSelect2()` con template colorato
|
||||||
|
|
||||||
|
**RicercaController** (`app/Http/Controllers/RicercaController.php`):
|
||||||
|
- Aggiunta MailingList alle query per tag slug
|
||||||
|
- `results['mailingLists']` e `totals['mailingLists']` passati alla view
|
||||||
|
|
||||||
|
**Ricerca — View** (`resources/views/ricerca/index.blade.php`):
|
||||||
|
- Aggiunto info-box Mailing List (colore bg-purple) con link "Vedi tutti" → `mailing-liste.index?tag[]=`
|
||||||
|
- Aggiunta tabella dettaglio Mailing List nel risultato di ricerca (nome, contatti, stato, azione show)
|
||||||
|
- Fix: `route('documenti.edit')` → `url('/documenti/' . $doc->id . '/edit')` (route non esiste)
|
||||||
|
|
||||||
|
### 2026-06-09 — Fix: storeCustom() non salvava tag_filter
|
||||||
|
|
||||||
|
**Problema**: `ReportController@storeCustom()` non salvava `tag_filter` e `tag_filter_mode` nella config del report personalizzato. Quando un report salvato veniva eseguito, `runCustomReport()` non trovava i filtri tag nella config e non li applicava.
|
||||||
|
|
||||||
|
**Fix**: Aggiunte righe 143-148 in `ReportController.php`:
|
||||||
|
```php
|
||||||
|
if ($request->has('tag_filter')) {
|
||||||
|
$config['tag_filter'] = $request->input('tag_filter');
|
||||||
|
}
|
||||||
|
if ($request->filled('tag_filter_mode')) {
|
||||||
|
$config['tag_filter_mode'] = $request->input('tag_filter_mode');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2026-06-09 — Fix: MailingList edit — "Carica contatti" non inseriva i selezionati
|
||||||
|
|
||||||
|
**Problema**: Due bug nel JS `caricaSelezionati()` in `mailing-liste/edit.blade.php`:
|
||||||
|
1. **Typo**: `ind.cogname` invece di `ind.cognome` — il campo Cognome nella tabella era sempre vuoto/undefined (API restituisce `cognome`)
|
||||||
|
2. **Email gate**: `if (ind.emails && ind.emails.length > 0)` filtro bloccante — individui senza email salvata in `contatti` venivano SILENZIOSAMENTE scartati, mai aggiunti alla tabella
|
||||||
|
|
||||||
|
**Fix**:
|
||||||
|
1. `ind.cogname` → `ind.cognome` (righe 227 e 256)
|
||||||
|
2. Rimossa condizione `if (ind.emails...)` — ora anche individui senza email vengono aggiunti alla tabella
|
||||||
|
3. `email: ind.emails[0]` → `email: (ind.emails && ind.emails.length > 0) ? ind.emails[0] : ''` — email vuota se nessuna presente
|
||||||
|
4. Controller già fixato per auto-risolvere email da `Individuo::contatti()` al salvataggio
|
||||||
|
|
||||||
|
### 2026-06-09 — Fix: MailingListController email gate bloccava nuovi contatti senza email
|
||||||
|
|
||||||
|
**Problema**: `store()` e `update()` in `MailingListController` usavano `!empty($contatto['email'])` per decidere se creare un contatto. L'email era richiesta ma il MailingContact non memorizza l'email — la risolve dall'Individuo al momento del display/invio. Questo impediva di aggiungere individui senza email (o con email non nei contatti) alla mailing list.
|
||||||
|
|
||||||
|
**Fix**: Rimossa completamente la verifica email sia in `store()` che in `update()`. Ora ogni `individuo_id` valido viene aggiunto come MailingContact. L'email viene risolta al display dal model `$contact->individuo->contatti->where('tipo', 'email')->first()?->valore`.
|
||||||
|
|
||||||
|
Prima (bloccante):
|
||||||
|
```php
|
||||||
|
if (!empty($contatto['individuo_id']) && !empty($contatto['email'])) { create }
|
||||||
|
```
|
||||||
|
Dopo (semplice):
|
||||||
|
```php
|
||||||
|
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] 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`
|
||||||
|
- [DONE] Individui: `$allColumns` espanse da 6 a 17 colonne (aggiunte: data_nascita, genere, indirizzo, cap, citta, provincia, tipo_documento, numero_documento, scadenza_documento, note, created_at) — tutte con `<th>`/`<td>` condizionali nella vista
|
||||||
|
- [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
|
||||||
|
|
||||||
|
## 2026-06-10 — Per-User Column Views (colonne visibili, larghezze, ordine)
|
||||||
|
|
||||||
|
**Obiettivo**: Implementare viste colonne personalizzabili per utente su tutte le 5 entity list pages (individui, gruppi, eventi, documenti, mailing-liste), con persistenza tra login.
|
||||||
|
|
||||||
|
### Cosa è stato fatto
|
||||||
|
|
||||||
|
**Migration** (`2026_06_10_044946_add_column_widths_and_order_to_viste_report_table.php`):
|
||||||
|
- Aggiunge `colonne_larghezze` (json) e `colonne_ordine` (json) a `viste_report`
|
||||||
|
- Guardia `Schema::hasColumn` per compatibilità cross-DB (MySQL ↔ SQLite)
|
||||||
|
- Già eseguita in batch 38
|
||||||
|
|
||||||
|
**Backend**:
|
||||||
|
- `VistaReport.php`: `$fillable` + `$casts` (array) per entrambi i nuovi campi
|
||||||
|
- `VistaReportController@store`/`@update`: validazione estesa con `colonne_larghezze`, `colonne_ordine` (array)
|
||||||
|
- Tutti i 5 controller passano alla vista: `$tableColumns` (array `['key','label','visible']`), `$entityType`, `$columnWidths`, `$visibleColumns`, `$allColumns` (varia nome in base al controller)
|
||||||
|
- Tutti i 5 controller ora passano `$userVistas` (precaricate) invece di eseguire query nella view
|
||||||
|
- `IndividuoController`: `$allColumns` espanse con tutte le colonne DB disponibili (da 6 a 17)
|
||||||
|
- `IndividuoController`: aggiunto `use App\Models\VistaReport` per la query `$userVistas`
|
||||||
|
|
||||||
|
**Frontend JS** (`public/js/column-manager.js`):
|
||||||
|
- Classe `ColumnManager`: resize via mouse drag su handle, reorder via HTML5 DnD, persistenza larghezze in `sessionStorage`, fallback da `vista-data` DOM element, `printWithSettings()` che clona la tabella in finestra print-ready
|
||||||
|
|
||||||
|
**Modal** (`resources/views/partials/table-settings.blade.php`):
|
||||||
|
- Modal unificato con 3 tab: Colonne (checkbox visibilità + input larghezza + drag-reorder list), Viste (salva/carica/cancella), Stampa (pulsante print)
|
||||||
|
- Definisce `initTableSettings()` (NON auto-esegue) per evitare race condition con ColumnManager
|
||||||
|
- **RIMOSSA** query `VistaReport::where(...)->get()` dal `@php` block — ora usa `$userVistas` passato dal controller
|
||||||
|
- Non usa più `Auth::id()` direttamente — riceve `$userVistas` già popolato
|
||||||
|
|
||||||
|
**Views** — tutte e 5 aggiornate:
|
||||||
|
- `individui/index.blade.php`:
|
||||||
|
- `$allColumns` nella vista ora ha 17 colonne (vs 6 prima)
|
||||||
|
- `<th>` e `<td>` condizionali per tutte le nuove colonne (data_nascita, genere, indirizzo, cap, citta, provincia, tipo_documento, numero_documento, scadenza_documento, note, created_at)
|
||||||
|
- RIMOSSE modal legacy: `#saveVistaModal`, `#colonneModal`
|
||||||
|
- RIMOSSE funzioni JS legacy: `saveVista()`, `toggleColumn()`
|
||||||
|
- RIMOSSO sync checkboxes nel `DOMContentLoaded` (non più necessari)
|
||||||
|
- NUOVA: `getColumnIndex(colKey)` — lookup dinamico dell'indice colonna basato su `data-column`
|
||||||
|
- `sortTable()` e `applyColumnFilter()` ora usano `getColumnIndex()` invece di hardcoded mappa
|
||||||
|
- `gruppi/index.blade.php`:
|
||||||
|
- RIMOSSE modal legacy: `#saveVistaModal`, `#vistaListModal`
|
||||||
|
- RIMOSSO pulsante "Salva Vista" legacy
|
||||||
|
- RIMOSSE funzioni JS legacy: `showSaveVistaModal()`, `saveVista()`
|
||||||
|
- `eventi/index.blade.php`: colonne dinamiche via `$visibleColumns`, `@section('scripts')` con ColumnManager + `initTableSettings()`
|
||||||
|
- `mailing-liste/index.blade.php`: stessa struttura
|
||||||
|
- `documenti/index.blade.php`: ColumnManager init (non reorderable), column-manager.js incluso
|
||||||
|
- Tutti i `<th>` hanno `data-column` attributo per mapping JS
|
||||||
|
- Tutte chiamano `initTableSettings()` dopo `new ColumnManager()` nel `DOMContentLoaded`
|
||||||
|
|
||||||
|
**Performance fix**:
|
||||||
|
- Rimossa query `VistaReport::where(...)->get()` dal `@php` block in `table-settings.blade.php` (1 query extra per ogni pagina load su 5 entità)
|
||||||
|
- Spostata nei 5 controller come `$userVistas` passata via `compact()`
|
||||||
|
- Totale: -5 query per pagina load (una per ogni entity, anche su pagine non di index)
|
||||||
|
|
||||||
|
**Verifica**:
|
||||||
|
- JS brace balance: OK su tutti i 5 file (node check)
|
||||||
|
- PHP lint: OK su tutti i 5 controller
|
||||||
|
- Presenza `data-column`: individui=19, gruppi=14, eventi=9, mailing-liste=7, documenti=8
|
||||||
|
- Fallback `$visibleColumns` quando `$vista === null`: presente e corretto in tutti i 5 controller
|
||||||
|
- Nessun residuo di vecchie modal legacy in individui e gruppi
|
||||||
|
- Rimosso bottone "Salva Vista" orfano da `individui/index.blade.php` (chiamava `showSaveVistaModal()` non più esistente)
|
||||||
|
- Aggiunto pulsante modifica ✏️ nel tab Viste per ogni vista salvata: popola il form "Salva vista corrente" con nome, default flag e colonne visibili; cambia bottone in "Aggiorna" e fa PUT `/viste/{id}` invece di POST; zero query extra
|
||||||
|
### 2026-06-10 — Fix: editVista onclick → data-* attributes
|
||||||
|
|
||||||
|
**Problema**: Il pulsante modifica vista usava `onclick` con `addslashes(json_encode(...))`. In un attributo HTML delimitato da `"`, il backslash-escaping di `\"` non è gestito in modo standard dai browser — il JSON contenente `"` poteva rompere l'attributo HTML.
|
||||||
|
|
||||||
|
**Fix**:
|
||||||
|
1. Sostituito `onclick` con `data-*` attributi (`data-vista-id`, `data-vista-nome`, `data-vista-default`, `data-vista-colonne`) usando `{{ }}` di Blade (che applica `htmlspecialchars`, codificando `"` → `"` sicuro in HTML).
|
||||||
|
2. `editVista()` ora accetta un array direttamente (non più JSON string da parsare).
|
||||||
|
3. Click handler registrato in `initTableSettings()` via `document.querySelectorAll('.edit-vista-btn')` + `dataset` (browser decodifica automaticamente `"` → `"`).
|
||||||
|
|
||||||
|
### 2026-06-10 — Fix: 405 Method Not Allowed su update vista (via AJAX fetch)
|
||||||
|
|
||||||
|
**Problema**: Modificando una vista e cliccando "Aggiorna", `fetch()` inviava `PUT /viste/{id}` ma il controller `VistaReportController@update` restituiva `return back()` (302 redirect). `fetch()` seguiva il redirect — in alcuni browser il metodo PUT veniva preservato, ma la route `gruppi` accetta solo GET → 405 Method Not Allowed.
|
||||||
|
|
||||||
|
**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-10 — Fix: tutte le migration idempotenti + seeders idempotenti
|
||||||
|
|
||||||
|
**Problema**: `php artisan migrate` falliva su tabelle già esistenti. `php artisan db:seed` creava duplicati su re-run.
|
||||||
|
|
||||||
|
### Migration: hasTable guard su tutti i 27 file mancanti
|
||||||
|
|
||||||
|
Aggiunto `if (!Schema::hasTable('table_name')) { ... }` wrapper a tutte le 27 migration file (44 `Schema::create()` calls) che ne erano prive, usando script Node.js di trasformazione automatica:
|
||||||
|
- `2024_01_01_000002_create_comuni_diocesi_tables.php`
|
||||||
|
- `2024_01_01_000003_create_individui_table.php`
|
||||||
|
- `2024_01_01_000004_create_contatti_table.php`
|
||||||
|
- `2024_01_01_000005_create_gruppi_table.php`
|
||||||
|
- `2024_01_01_000006_create_gruppo_individuo_table.php`
|
||||||
|
- `2024_01_01_000007_create_eventi_table.php`
|
||||||
|
- `2024_01_01_000008_create_documenti_table.php`
|
||||||
|
- `2024_01_01_000009_create_mailing_tables.php`
|
||||||
|
- `2024_01_01_000010_create_notifiche_table.php`
|
||||||
|
- `2024_01_01_000011_create_users_table.php`
|
||||||
|
- `2024_01_01_000012_create_cache_and_jobs_tables.php`
|
||||||
|
- `2024_01_01_000019_create_eventi_documenti_table.php`
|
||||||
|
- `2024_01_01_000022_create_table_viste_report.php`
|
||||||
|
- `2026_05_10_000001_create_tipologie_documenti_table.php`
|
||||||
|
- `2026_05_10_000002_add_acl_tables.php`
|
||||||
|
- `2026_05_10_125210_create_permission_tables.php` (usa `$tableNames[...]` dinamici)
|
||||||
|
- `2026_05_11_000001_create_email_settings_table.php`
|
||||||
|
- `2026_05_11_000002_create_email_folders_table.php`
|
||||||
|
- `2026_05_11_000003_create_email_messages_table.php`
|
||||||
|
- `2026_05_11_000007_create_email_attachments_table.php`
|
||||||
|
- `2026_05_12_000001_create_ruoli_table.php`
|
||||||
|
- `2026_05_25_000001_create_report_custom_table.php`
|
||||||
|
- `2026_05_26_000001_create_tipologie_eventi_table.php`
|
||||||
|
- `2026_05_26_000002_create_sender_accounts_table.php`
|
||||||
|
- `2026_05_27_000001_create_documenti_cartelle_table.php`
|
||||||
|
- `2026_05_27_113245_create_storage_repositories_table.php`
|
||||||
|
- `2026_06_02_000001_create_calendario_connessioni_table.php`
|
||||||
|
|
||||||
|
### Seeder: firstOrCreate per idempotenza
|
||||||
|
|
||||||
|
- `DiocesiSeeder.php`: `Diocesi::create()` → `Diocesi::firstOrCreate(['nome' => $d['nome']], $d)`
|
||||||
|
- `ComuniSeeder.php`: `Comune::create()` → `Comune::firstOrCreate(['codice_istat' => $c['codice_istat']], $c)`
|
||||||
|
- `DatabaseSeeder.php`: `User::create()` → `User::firstOrCreate(['email' => 'admin@glastree.local'], [...])`, con guardia su `role_preset_id`
|
||||||
|
|
||||||
|
**Risultato**: `php artisan migrate --seed` è ora completamente idempotente. Zero errori su 62 migration + 6 seeder.
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\EmailSetting;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Crypt;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class DiagnoseEmail extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'diagnose:email';
|
||||||
|
|
||||||
|
protected $description = 'Diagnostica configurazione email e credenziali';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$this->info('=== Diagnostica Email Settings ===');
|
||||||
|
$this->newLine();
|
||||||
|
|
||||||
|
// 1. APP_KEY
|
||||||
|
$this->section('APP_KEY');
|
||||||
|
$appKey = config('app.key');
|
||||||
|
if ($appKey && str_starts_with($appKey, 'base64:')) {
|
||||||
|
$this->line('APP_KEY: presente e valida ✓');
|
||||||
|
} elseif ($appKey) {
|
||||||
|
$this->warn('APP_KEY: presente ma non in formato base64 (potrebbe funzionare lo stesso)');
|
||||||
|
} else {
|
||||||
|
$this->error('APP_KEY: NON IMPOSTATA — crittografia password non funzionerà!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Tabella email_settings
|
||||||
|
$this->newLine();
|
||||||
|
$this->section('Tabella email_settings');
|
||||||
|
if (Schema::hasTable('email_settings')) {
|
||||||
|
$this->line('Tabella email_settings: esiste ✓');
|
||||||
|
$columns = Schema::getColumnListing('email_settings');
|
||||||
|
$this->line('Colonne: ' . implode(', ', $columns));
|
||||||
|
|
||||||
|
$required = ['imap_host', 'imap_username', 'imap_password', 'email_address'];
|
||||||
|
$missing = array_diff($required, $columns);
|
||||||
|
if ($missing) {
|
||||||
|
$this->error('Colonne mancanti: ' . implode(', ', $missing));
|
||||||
|
} else {
|
||||||
|
$this->line('Colonne richieste presenti ✓');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->error('Tabella email_settings NON ESISTE! Esegui: php artisan migrate');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Record email_settings
|
||||||
|
$this->newLine();
|
||||||
|
$this->section('Record email_settings');
|
||||||
|
$settings = EmailSetting::first();
|
||||||
|
if ($settings) {
|
||||||
|
$this->line('Record trovato (ID: ' . $settings->id . ')');
|
||||||
|
$this->line(' imap_host: ' . ($settings->imap_host ?: 'VUOTO'));
|
||||||
|
$this->line(' imap_username: ' . ($settings->imap_username ?: 'VUOTO'));
|
||||||
|
$this->line(' imap_password: ' . ($settings->imap_password ? '[CRITTATO]' : 'VUOTO'));
|
||||||
|
$this->line(' email_address: ' . ($settings->email_address ?: 'VUOTO'));
|
||||||
|
$this->line(' smtp_host: ' . ($settings->smtp_host ?: 'VUOTO'));
|
||||||
|
|
||||||
|
// Prova decrittazione
|
||||||
|
if ($settings->imap_password) {
|
||||||
|
try {
|
||||||
|
$decrypted = Crypt::decryptString($settings->imap_password);
|
||||||
|
$this->line(' -> password IMAP decrittata: ' . (strlen($decrypted) > 0 ? 'OK (' . strlen($decrypted) . ' caratteri)' : 'VUOTA'));
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error(' -> ERRORE decrittazione password IMAP: ' . $e->getMessage());
|
||||||
|
$this->warn(' Causa probabile: APP_KEY diversa da quando è stata salvata.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->warn('NESSUN record in email_settings — nessuna configurazione salvata.');
|
||||||
|
$this->line('Vai su /impostazioni#email-imap, compila e clicca "Salva Impostazioni Email"');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Tabella sender_accounts
|
||||||
|
$this->newLine();
|
||||||
|
$this->section('Tabella sender_accounts');
|
||||||
|
if (Schema::hasTable('sender_accounts')) {
|
||||||
|
$this->line('Tabella sender_accounts: esiste ✓');
|
||||||
|
$count = DB::table('sender_accounts')->count();
|
||||||
|
$this->line('Record presenti: ' . $count);
|
||||||
|
} else {
|
||||||
|
$this->error('Tabella sender_accounts NON ESISTE!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Log recenti
|
||||||
|
$this->newLine();
|
||||||
|
$this->section('Errori recenti (storage/logs/laravel.log)');
|
||||||
|
$logPath = storage_path('logs/laravel.log');
|
||||||
|
if (file_exists($logPath)) {
|
||||||
|
$lines = $this->getLastErrorLines($logPath, 20);
|
||||||
|
if ($lines) {
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$this->line($line);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->line('Nessun errore recente trovato.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->warn('File di log non trovato: ' . $logPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Estensioni PHP
|
||||||
|
$this->newLine();
|
||||||
|
$this->section('Estensioni PHP');
|
||||||
|
$requiredExts = ['openssl', 'json', 'mbstring', 'pdo', 'fileinfo'];
|
||||||
|
foreach ($requiredExts as $ext) {
|
||||||
|
if (extension_loaded($ext)) {
|
||||||
|
$this->line($ext . ': OK ✓');
|
||||||
|
} else {
|
||||||
|
$this->error($ext . ': MANCANTE!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->newLine();
|
||||||
|
$this->info('=== Diagnostica completata ===');
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function section(string $title): void
|
||||||
|
{
|
||||||
|
$this->line("--- {$title} ---");
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getLastErrorLines(string $path, int $maxLines): array
|
||||||
|
{
|
||||||
|
$lines = [];
|
||||||
|
$fp = fopen($path, 'r');
|
||||||
|
if (!$fp) {
|
||||||
|
return $lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
fseek($fp, -1, SEEK_END);
|
||||||
|
$pos = ftell($fp);
|
||||||
|
$buffer = '';
|
||||||
|
|
||||||
|
while ($pos > 0 && count($lines) < $maxLines) {
|
||||||
|
$char = fgetc($fp);
|
||||||
|
if ($char === "\n") {
|
||||||
|
if (trim($buffer) !== '') {
|
||||||
|
$lines[] = trim($buffer);
|
||||||
|
}
|
||||||
|
$buffer = '';
|
||||||
|
} else {
|
||||||
|
$buffer = $char . $buffer;
|
||||||
|
}
|
||||||
|
$pos--;
|
||||||
|
fseek($fp, $pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trim($buffer) !== '') {
|
||||||
|
$lines[] = trim($buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose($fp);
|
||||||
|
|
||||||
|
return $lines;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
|
||||||
|
class ResetUserPassword extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'user:reset-password
|
||||||
|
{email? : Email dell\'utente}
|
||||||
|
{--password= : Nuova password (se omessa, richiesta interattiva)}';
|
||||||
|
|
||||||
|
protected $description = 'Cambia la password di un utente tramite email';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$email = $this->argument('email');
|
||||||
|
if (empty($email)) {
|
||||||
|
$email = $this->ask('Email dell\'utente');
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = User::where('email', $email)->first();
|
||||||
|
if ($user === null) {
|
||||||
|
$this->error("Nessun utente trovato con email: {$email}");
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->line("Utente trovato: {$user->name} ({$user->email})");
|
||||||
|
|
||||||
|
$password = $this->option('password');
|
||||||
|
if (empty($password)) {
|
||||||
|
$password = $this->secret('Nuova password');
|
||||||
|
$confirm = $this->secret('Conferma nuova password');
|
||||||
|
|
||||||
|
if ($password !== $confirm) {
|
||||||
|
$this->error('Le password non corrispondono');
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$validator = Validator::make(
|
||||||
|
['password' => $password],
|
||||||
|
['password' => ['required', 'string', 'min:12']]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
$this->error('Password troppo corta: minimo 12 caratteri');
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->password = Hash::make($password);
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
if ($user->wasChanged('password')) {
|
||||||
|
$this->info("Password aggiornata con successo per {$user->email}");
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->error('Errore durante l\'aggiornamento della password');
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -132,6 +132,7 @@ class ReportColumnRegistry
|
|||||||
'contatti.telefono' => ['label' => 'Telefono (da contatti)', 'type' => 'relation'],
|
'contatti.telefono' => ['label' => 'Telefono (da contatti)', 'type' => 'relation'],
|
||||||
'contatti.cellulare' => ['label' => 'Cellulare (da contatti)', 'type' => 'relation'],
|
'contatti.cellulare' => ['label' => 'Cellulare (da contatti)', 'type' => 'relation'],
|
||||||
'gruppi.nome' => ['label' => 'Gruppi', 'type' => 'relation'],
|
'gruppi.nome' => ['label' => 'Gruppi', 'type' => 'relation'],
|
||||||
|
'tags.nome' => ['label' => 'Tag', 'type' => 'relation'],
|
||||||
],
|
],
|
||||||
|
|
||||||
'gruppi' => [
|
'gruppi' => [
|
||||||
@@ -151,6 +152,7 @@ class ReportColumnRegistry
|
|||||||
'parent.nome' => ['label' => 'Gruppo Padre', 'type' => 'relation'],
|
'parent.nome' => ['label' => 'Gruppo Padre', 'type' => 'relation'],
|
||||||
'diocesi.nome' => ['label' => 'Diocesi', 'type' => 'relation'],
|
'diocesi.nome' => ['label' => 'Diocesi', 'type' => 'relation'],
|
||||||
'individui_count' => ['label' => 'Numero Membri', 'type' => 'relation'],
|
'individui_count' => ['label' => 'Numero Membri', 'type' => 'relation'],
|
||||||
|
'tags.nome' => ['label' => 'Tag', 'type' => 'relation'],
|
||||||
],
|
],
|
||||||
|
|
||||||
'eventi' => [
|
'eventi' => [
|
||||||
@@ -171,6 +173,7 @@ class ReportColumnRegistry
|
|||||||
'info_ricorrenza' => ['label' => 'Info Ricorrenza', 'type' => 'accessor'],
|
'info_ricorrenza' => ['label' => 'Info Ricorrenza', 'type' => 'accessor'],
|
||||||
'gruppi.nome' => ['label' => 'Gruppi', 'type' => 'relation'],
|
'gruppi.nome' => ['label' => 'Gruppi', 'type' => 'relation'],
|
||||||
'responsabili.nome_completo' => ['label' => 'Responsabili', 'type' => 'relation'],
|
'responsabili.nome_completo' => ['label' => 'Responsabili', 'type' => 'relation'],
|
||||||
|
'tags.nome' => ['label' => 'Tag', 'type' => 'relation'],
|
||||||
],
|
],
|
||||||
|
|
||||||
'documenti' => [
|
'documenti' => [
|
||||||
@@ -183,6 +186,7 @@ class ReportColumnRegistry
|
|||||||
'note' => ['label' => 'Note', 'type' => 'direct'],
|
'note' => ['label' => 'Note', 'type' => 'direct'],
|
||||||
'created_at' => ['label' => 'Data Caricamento', 'type' => 'direct'],
|
'created_at' => ['label' => 'Data Caricamento', 'type' => 'direct'],
|
||||||
'user.name' => ['label' => 'Caricato Da', 'type' => 'relation'],
|
'user.name' => ['label' => 'Caricato Da', 'type' => 'relation'],
|
||||||
|
'tags.nome' => ['label' => 'Tag', 'type' => 'relation'],
|
||||||
],
|
],
|
||||||
|
|
||||||
'contatti' => [
|
'contatti' => [
|
||||||
|
|||||||
@@ -39,4 +39,35 @@ class ActivityLogController extends Controller
|
|||||||
|
|
||||||
return view('admin.activity-logs.index', compact('logs', 'users', 'modules'));
|
return view('admin.activity-logs.index', compact('logs', 'users', 'modules'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function destroy(Request $request)
|
||||||
|
{
|
||||||
|
$query = ActivityLog::query();
|
||||||
|
|
||||||
|
if ($request->filled('user_id')) {
|
||||||
|
$query->where('user_id', $request->user_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('module')) {
|
||||||
|
$query->where('module', $request->module);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('action')) {
|
||||||
|
$query->where('action', $request->action);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('from')) {
|
||||||
|
$query->whereDate('created_at', '>=', $request->from);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('to')) {
|
||||||
|
$query->whereDate('created_at', '<=', $request->to);
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = $query->count();
|
||||||
|
$query->delete();
|
||||||
|
|
||||||
|
return redirect()->route('admin.activity-logs.index')
|
||||||
|
->with('success', "Eliminati {$count} log.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,10 +3,14 @@
|
|||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\EmailAttachment;
|
||||||
|
use App\Models\EmailFolder;
|
||||||
use App\Models\EmailSetting;
|
use App\Models\EmailSetting;
|
||||||
use App\Models\SenderAccount;
|
use App\Models\SenderAccount;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Crypt;
|
use Illuminate\Support\Facades\Crypt;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
class EmailSettingsController extends Controller
|
class EmailSettingsController extends Controller
|
||||||
{
|
{
|
||||||
@@ -30,11 +34,15 @@ class EmailSettingsController extends Controller
|
|||||||
'smtp_host' => 'nullable|string|max:255',
|
'smtp_host' => 'nullable|string|max:255',
|
||||||
'smtp_port' => 'nullable|integer|min:1|max:65535',
|
'smtp_port' => 'nullable|integer|min:1|max:65535',
|
||||||
'smtp_encryption' => 'nullable|in:ssl,tls,none',
|
'smtp_encryption' => 'nullable|in:ssl,tls,none',
|
||||||
|
'smtp_username' => 'nullable|string|max:255',
|
||||||
|
'smtp_password' => 'nullable|string',
|
||||||
'email_address' => 'required|email',
|
'email_address' => 'required|email',
|
||||||
'email_name' => 'nullable|string|max:255',
|
'email_name' => 'nullable|string|max:255',
|
||||||
'reply_to' => 'nullable|email',
|
'reply_to' => 'nullable|email',
|
||||||
'sync_interval_minutes' => 'nullable|integer|min:1|max:60',
|
'sync_interval_minutes' => 'nullable|integer|min:1|max:60',
|
||||||
'is_active' => 'boolean',
|
'is_active' => 'boolean',
|
||||||
|
'signature' => 'nullable|string',
|
||||||
|
'signature_enabled' => 'boolean',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!empty($validated['imap_password'])) {
|
if (!empty($validated['imap_password'])) {
|
||||||
@@ -58,6 +66,24 @@ class EmailSettingsController extends Controller
|
|||||||
if ($verify &&
|
if ($verify &&
|
||||||
$verify->imap_host === $validated['imap_host'] &&
|
$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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
return back()->with('success', 'Impostazioni email salvate e verificate nel database.');
|
return back()->with('success', 'Impostazioni email salvate e verificate nel database.');
|
||||||
} else {
|
} else {
|
||||||
return back()->with('error', 'Errore nel salvataggio: i dati non sono stati salvati correttamente.');
|
return back()->with('error', 'Errore nel salvataggio: i dati non sono stati salvati correttamente.');
|
||||||
@@ -257,6 +283,30 @@ class EmailSettingsController extends Controller
|
|||||||
return back()->with('success', 'Mittente eliminato.');
|
return back()->with('success', 'Mittente eliminato.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function destroy(): RedirectResponse
|
||||||
|
{
|
||||||
|
$this->authorizeDelete('settings');
|
||||||
|
|
||||||
|
EmailAttachment::chunk(100, function ($attachments) {
|
||||||
|
foreach ($attachments as $attachment) {
|
||||||
|
if ($attachment->file_path && Storage::exists($attachment->file_path)) {
|
||||||
|
Storage::delete($attachment->file_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
EmailFolder::whereIn('type', ['inbox', 'sent', 'drafts', 'trash', 'archive', 'starred'])->each(function ($folder) {
|
||||||
|
$folder->delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
$settings = EmailSetting::first();
|
||||||
|
if ($settings) {
|
||||||
|
$settings->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect('/impostazioni/email')->with('success', 'Configurazione email eliminata. Messaggi, cartelle e allegati rimossi completamente.');
|
||||||
|
}
|
||||||
|
|
||||||
public function senderTestSmtp(Request $request, int $id)
|
public function senderTestSmtp(Request $request, int $id)
|
||||||
{
|
{
|
||||||
$this->authorizeWrite('settings');
|
$this->authorizeWrite('settings');
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\CalendarioConnessione;
|
||||||
|
use App\Services\CalDavService;
|
||||||
|
use App\Services\GoogleCalendarSyncService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class CalendarioConnessioneController extends Controller
|
||||||
|
{
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$this->authorizeWrite('settings');
|
||||||
|
|
||||||
|
$data = $request->validate([
|
||||||
|
'nome' => 'required|string|max:255',
|
||||||
|
'tipo' => 'required|in:google_calendar,caldav',
|
||||||
|
'sync_direction' => 'nullable|in:import,export,bidirectional',
|
||||||
|
'sync_interval_minutes' => 'nullable|integer|min:5|max:1440',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
'config' => 'nullable|array',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$maxOrdine = CalendarioConnessione::max('ordine') ?? 0;
|
||||||
|
|
||||||
|
$connessione = new CalendarioConnessione();
|
||||||
|
$connessione->nome = $data['nome'];
|
||||||
|
$connessione->tipo = $data['tipo'];
|
||||||
|
$connessione->sync_direction = $data['sync_direction'] ?? 'bidirectional';
|
||||||
|
$connessione->sync_interval_minutes = $data['sync_interval_minutes'] ?? 60;
|
||||||
|
$connessione->is_active = $request->boolean('is_active', true);
|
||||||
|
$connessione->stato = 'disconnesso';
|
||||||
|
$connessione->ordine = $maxOrdine + 1;
|
||||||
|
$connessione->encryptAndSetConfig($data['config'] ?? []);
|
||||||
|
$connessione->save();
|
||||||
|
|
||||||
|
return redirect('/impostazioni#calendario')->with('success', 'Connessione calendario "' . $connessione->nome . '" creata.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, int $id): RedirectResponse
|
||||||
|
{
|
||||||
|
$this->authorizeWrite('settings');
|
||||||
|
|
||||||
|
$connessione = CalendarioConnessione::findOrFail($id);
|
||||||
|
|
||||||
|
$data = $request->validate([
|
||||||
|
'nome' => 'required|string|max:255',
|
||||||
|
'tipo' => 'required|in:google_calendar,caldav',
|
||||||
|
'sync_direction' => 'nullable|in:import,export,bidirectional',
|
||||||
|
'sync_interval_minutes' => 'nullable|integer|min:5|max:1440',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
'config' => 'nullable|array',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$connessione->nome = $data['nome'];
|
||||||
|
$connessione->tipo = $data['tipo'];
|
||||||
|
$connessione->sync_direction = $data['sync_direction'] ?? 'bidirectional';
|
||||||
|
$connessione->sync_interval_minutes = $data['sync_interval_minutes'] ?? 60;
|
||||||
|
$connessione->is_active = $request->boolean('is_active', true);
|
||||||
|
|
||||||
|
if (!empty($data['config'])) {
|
||||||
|
$connessione->encryptAndSetConfig($data['config']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$connessione->save();
|
||||||
|
|
||||||
|
return redirect('/impostazioni#calendario')->with('success', 'Connessione calendario "' . $connessione->nome . '" aggiornata.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(int $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->authorizeDelete('settings');
|
||||||
|
$connessione = CalendarioConnessione::findOrFail($id);
|
||||||
|
$connessione->delete();
|
||||||
|
|
||||||
|
return response()->json(['success' => true, 'message' => 'Connessione calendario eliminata.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test(int $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->authorizeWrite('settings');
|
||||||
|
$connessione = CalendarioConnessione::findOrFail($id);
|
||||||
|
|
||||||
|
$result = match ($connessione->tipo) {
|
||||||
|
'caldav' => app(CalDavService::class)->testConnection($connessione),
|
||||||
|
'google_calendar' => app(GoogleCalendarSyncService::class)->testConnection($connessione),
|
||||||
|
default => ['success' => false, 'message' => 'Tipo sconosciuto'],
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
$connessione->update(['stato' => 'connesso', 'last_error_at' => null, 'last_error_message' => null]);
|
||||||
|
} else {
|
||||||
|
$connessione->update(['stato' => 'errore', 'last_error_at' => now(), 'last_error_message' => $result['message'] ?? 'Errore sconosciuto']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sync(int $id): RedirectResponse
|
||||||
|
{
|
||||||
|
$this->authorizeWrite('settings');
|
||||||
|
$connessione = CalendarioConnessione::findOrFail($id);
|
||||||
|
|
||||||
|
$result = match ($connessione->tipo) {
|
||||||
|
'caldav' => app(CalDavService::class)->syncEvents($connessione),
|
||||||
|
'google_calendar' => app(GoogleCalendarSyncService::class)->syncEvents($connessione),
|
||||||
|
default => ['success' => false, 'imported' => 0, 'exported' => 0, 'errors' => ['Tipo sconosciuto']],
|
||||||
|
};
|
||||||
|
|
||||||
|
$connessione->update(['last_sync_at' => now()]);
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
$msg = 'Sincronizzazione completata: ' . $result['imported'] . ' importati, ' . $result['exported'] . ' esportati.';
|
||||||
|
$connessione->update(['stato' => 'connesso']);
|
||||||
|
return redirect('/impostazioni#calendario')->with('success', $msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
$errorMsg = implode('; ', $result['errors']);
|
||||||
|
$connessione->update(['stato' => 'errore', 'last_error_at' => now(), 'last_error_message' => $errorMsg]);
|
||||||
|
|
||||||
|
return redirect('/impostazioni#calendario')->with('error', 'Errore sync: ' . $errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function syncAll(): JsonResponse
|
||||||
|
{
|
||||||
|
$this->authorizeWrite('settings');
|
||||||
|
|
||||||
|
$connections = CalendarioConnessione::where('is_active', true)->get();
|
||||||
|
$results = [];
|
||||||
|
|
||||||
|
foreach ($connections as $connessione) {
|
||||||
|
$result = match ($connessione->tipo) {
|
||||||
|
'caldav' => app(CalDavService::class)->syncEvents($connessione),
|
||||||
|
'google_calendar' => app(GoogleCalendarSyncService::class)->syncEvents($connessione),
|
||||||
|
default => ['success' => false, 'imported' => 0, 'exported' => 0, 'errors' => ['Tipo sconosciuto']],
|
||||||
|
};
|
||||||
|
|
||||||
|
$connessione->update(['last_sync_at' => now()]);
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
$connessione->update(['stato' => 'connesso']);
|
||||||
|
} else {
|
||||||
|
$errorMsg = implode('; ', $result['errors']);
|
||||||
|
$connessione->update(['stato' => 'errore', 'last_error_at' => now(), 'last_error_message' => $errorMsg]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$results[] = [
|
||||||
|
'id' => $connessione->id,
|
||||||
|
'nome' => $connessione->nome,
|
||||||
|
'success' => $result['success'],
|
||||||
|
'imported' => $result['imported'],
|
||||||
|
'exported' => $result['exported'],
|
||||||
|
'errors' => $result['errors'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalImported = array_sum(array_column($results, 'imported'));
|
||||||
|
$totalExported = array_sum(array_column($results, 'exported'));
|
||||||
|
$hasErrors = in_array(false, array_column($results, 'success'), true);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => !$hasErrors,
|
||||||
|
'results' => $results,
|
||||||
|
'total_imported' => $totalImported,
|
||||||
|
'total_exported' => $totalExported,
|
||||||
|
'connections' => count($results),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reorder(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->authorizeWrite('settings');
|
||||||
|
$order = $request->input('order', []);
|
||||||
|
foreach ($order as $index => $id) {
|
||||||
|
CalendarioConnessione::where('id', $id)->update(['ordine' => $index]);
|
||||||
|
}
|
||||||
|
return response()->json(['success' => true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ use App\Models\Individuo;
|
|||||||
use App\Models\Gruppo;
|
use App\Models\Gruppo;
|
||||||
use App\Models\Evento;
|
use App\Models\Evento;
|
||||||
use App\Models\StorageRepository;
|
use App\Models\StorageRepository;
|
||||||
|
use App\Models\Tag;
|
||||||
use App\Models\TipologiaDocumento;
|
use App\Models\TipologiaDocumento;
|
||||||
use App\Services\StorageRepositoryService;
|
use App\Services\StorageRepositoryService;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
@@ -42,9 +43,14 @@ class DocumentoController extends Controller
|
|||||||
$folderId = $request->get('folder_id');
|
$folderId = $request->get('folder_id');
|
||||||
$repoId = $request->get('repository_id');
|
$repoId = $request->get('repository_id');
|
||||||
|
|
||||||
$query = Documento::with(['user', 'target', 'cartella', 'repository'])
|
$query = Documento::with(['user', 'target', 'cartella', 'repository', 'tags'])
|
||||||
->orderByDesc('created_at');
|
->orderByDesc('created_at');
|
||||||
|
|
||||||
|
if ($request->filled('tag')) {
|
||||||
|
$tagSlugs = (array) $request->input('tag');
|
||||||
|
$query->withAnyTags($tagSlugs);
|
||||||
|
}
|
||||||
|
|
||||||
if ($repoId) {
|
if ($repoId) {
|
||||||
$query->where('repository_id', $repoId);
|
$query->where('repository_id', $repoId);
|
||||||
} elseif ($folderId) {
|
} elseif ($folderId) {
|
||||||
@@ -54,6 +60,7 @@ class DocumentoController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$documenti = $query->paginate(20);
|
$documenti = $query->paginate(20);
|
||||||
|
$allTags = Tag::orderBy('name')->get();
|
||||||
$cartelle = DocumentoCartella::with('children')
|
$cartelle = DocumentoCartella::with('children')
|
||||||
->whereNull('parent_id')
|
->whereNull('parent_id')
|
||||||
->orderBy('nome')
|
->orderBy('nome')
|
||||||
@@ -85,11 +92,45 @@ class DocumentoController extends Controller
|
|||||||
'children' => $f->children->map(fn($c) => ['id' => $c->id, 'nome' => $c->nome]),
|
'children' => $f->children->map(fn($c) => ['id' => $c->id, 'nome' => $c->nome]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$vista = \App\Models\VistaReport::where('user_id', auth()->id())
|
||||||
|
->where('tipo', 'documenti')
|
||||||
|
->where('is_default', true)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$userVistas = \App\Models\VistaReport::where('user_id', auth()->id())
|
||||||
|
->where('tipo', 'documenti')
|
||||||
|
->orderBy('is_default', 'desc')
|
||||||
|
->orderBy('nome')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$allColumnDefs = [
|
||||||
|
['key' => 'nome_file', 'label' => 'Nome'],
|
||||||
|
['key' => 'tipologia', 'label' => 'Tipologia'],
|
||||||
|
['key' => 'dimensione', 'label' => 'Dimensione'],
|
||||||
|
['key' => 'visibilita', 'label' => 'Contesto'],
|
||||||
|
['key' => 'tag', 'label' => 'Tag'],
|
||||||
|
['key' => 'created_at', 'label' => 'Data'],
|
||||||
|
];
|
||||||
|
$defaultVisible = ['nome_file', 'tipologia', 'dimensione', 'visibilita', 'tag', 'created_at'];
|
||||||
|
$visibleColumns = $vista && !empty($vista->colonne_visibili)
|
||||||
|
? $vista->colonne_visibili
|
||||||
|
: $defaultVisible;
|
||||||
|
|
||||||
|
$tableColumns = array_map(fn($col) => [
|
||||||
|
'key' => $col['key'],
|
||||||
|
'label' => $col['label'],
|
||||||
|
'visible' => in_array($col['key'], $visibleColumns),
|
||||||
|
], $allColumnDefs);
|
||||||
|
$entityType = 'documenti';
|
||||||
|
$columnWidths = $vista && $vista->colonne_larghezze ? $vista->colonne_larghezze : [];
|
||||||
|
|
||||||
return view('documenti.index', compact(
|
return view('documenti.index', compact(
|
||||||
'documenti', 'cartelle', 'currentFolder', 'breadcrumb', 'sottoCartelle',
|
'documenti', 'cartelle', 'currentFolder', 'breadcrumb', 'sottoCartelle',
|
||||||
'currentRepo', 'repositories',
|
'currentRepo', 'repositories',
|
||||||
'individui', 'gruppi', 'eventi', 'mailingLists', 'tipologie',
|
'individui', 'gruppi', 'eventi', 'mailingLists', 'tipologie',
|
||||||
'cartelleMoveOptions'
|
'cartelleMoveOptions', 'allTags',
|
||||||
|
'vista', 'visibleColumns', 'tableColumns', 'entityType', 'columnWidths',
|
||||||
|
'userVistas'
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,13 +147,15 @@ class DocumentoController extends Controller
|
|||||||
public function edit($documento)
|
public function edit($documento)
|
||||||
{
|
{
|
||||||
$this->authorizeWrite('documenti');
|
$this->authorizeWrite('documenti');
|
||||||
$documento = Documento::with('target')->findOrFail($documento);
|
$documento = Documento::with(['target', 'tags'])->findOrFail($documento);
|
||||||
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
|
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
|
||||||
$gruppi = Gruppo::orderBy('nome')->get();
|
$gruppi = Gruppo::orderBy('nome')->get();
|
||||||
$eventi = Evento::orderBy('nome_evento')->get();
|
$eventi = Evento::orderBy('nome_evento')->get();
|
||||||
$mailingLists = \App\Models\MailingList::orderBy('nome')->get();
|
$mailingLists = \App\Models\MailingList::orderBy('nome')->get();
|
||||||
$tipologie = TipologiaDocumento::attive();
|
$tipologie = TipologiaDocumento::attive();
|
||||||
return view('documenti.edit', compact('documento', 'individui', 'gruppi', 'eventi', 'mailingLists', 'tipologie'));
|
$tags = Tag::orderBy('name')->get();
|
||||||
|
$selectedTags = $documento->tags->pluck('id')->toArray();
|
||||||
|
return view('documenti.edit', compact('documento', 'individui', 'gruppi', 'eventi', 'mailingLists', 'tipologie', 'tags', 'selectedTags'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function options(Request $request)
|
public function options(Request $request)
|
||||||
@@ -165,6 +208,8 @@ class DocumentoController extends Controller
|
|||||||
'visibilita_target_id' => 'nullable|integer',
|
'visibilita_target_id' => 'nullable|integer',
|
||||||
'visibilita_target_type' => 'nullable|string',
|
'visibilita_target_type' => 'nullable|string',
|
||||||
'note' => 'nullable|string',
|
'note' => 'nullable|string',
|
||||||
|
'tags' => 'nullable|array',
|
||||||
|
'tags.*' => 'exists:tags,id',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$data['visibilita'] = $request->contesto_tipo ?: 'pubblico';
|
$data['visibilita'] = $request->contesto_tipo ?: 'pubblico';
|
||||||
@@ -185,6 +230,12 @@ class DocumentoController extends Controller
|
|||||||
|
|
||||||
$documento->update($data);
|
$documento->update($data);
|
||||||
|
|
||||||
|
if ($request->has('tags')) {
|
||||||
|
$documento->tags()->sync($request->tags);
|
||||||
|
} else {
|
||||||
|
$documento->tags()->detach();
|
||||||
|
}
|
||||||
|
|
||||||
return redirect('/documenti')->with('success', 'Documento aggiornato.');
|
return redirect('/documenti')->with('success', 'Documento aggiornato.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -661,9 +712,13 @@ class DocumentoController extends Controller
|
|||||||
'evento' => Evento::class,
|
'evento' => Evento::class,
|
||||||
'mailing' => \App\Models\MailingList::class,
|
'mailing' => \App\Models\MailingList::class,
|
||||||
];
|
];
|
||||||
$updateData['visibilita'] = $data['visibilita_target_type'];
|
$targetType = $data['visibilita_target_type'];
|
||||||
|
if (!isset($typeMap[$targetType])) {
|
||||||
|
return back()->with('error', 'Tipo destinazione non valido.');
|
||||||
|
}
|
||||||
|
$updateData['visibilita'] = $targetType;
|
||||||
$updateData['visibilita_target_id'] = (int) $data['visibilita_target_id'];
|
$updateData['visibilita_target_id'] = (int) $data['visibilita_target_id'];
|
||||||
$updateData['visibilita_target_type'] = $typeMap[$data['visibilita_target_type']] ?? $data['visibilita_target_type'];
|
$updateData['visibilita_target_type'] = $typeMap[$targetType];
|
||||||
}
|
}
|
||||||
|
|
||||||
Documento::whereIn('id', $ids)->update($updateData);
|
Documento::whereIn('id', $ids)->update($updateData);
|
||||||
@@ -702,6 +757,41 @@ class DocumentoController extends Controller
|
|||||||
return back()->with('success', count($ids) . ' documenti associati.');
|
return back()->with('success', count($ids) . ' documenti associati.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function massTag(Request $request)
|
||||||
|
{
|
||||||
|
$this->authorizeWrite('documenti');
|
||||||
|
$idsInput = $request->input('ids', '');
|
||||||
|
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
|
||||||
|
|
||||||
|
if (empty($ids)) {
|
||||||
|
return back()->with('error', 'Nessun documento selezionato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $request->validate([
|
||||||
|
'tags' => 'required|array',
|
||||||
|
'tags.*' => 'exists:tags,id',
|
||||||
|
'mode' => 'required|in:assign,remove',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tagIds = $data['tags'];
|
||||||
|
$mode = $data['mode'];
|
||||||
|
$count = 0;
|
||||||
|
|
||||||
|
Documento::whereIn('id', $ids)->chunk(100, function ($documenti) use ($tagIds, $mode, &$count) {
|
||||||
|
foreach ($documenti as $documento) {
|
||||||
|
if ($mode === 'assign') {
|
||||||
|
$documento->tags()->syncWithoutDetaching($tagIds);
|
||||||
|
} else {
|
||||||
|
$documento->tags()->detach($tagIds);
|
||||||
|
}
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$actionLabel = $mode === 'assign' ? 'assegnati' : 'rimossi';
|
||||||
|
return back()->with('success', "Tag $actionLabel per $count documenti.");
|
||||||
|
}
|
||||||
|
|
||||||
public function massMove(Request $request): RedirectResponse
|
public function massMove(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$this->authorizeWrite('documenti');
|
$this->authorizeWrite('documenti');
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ class EmailController extends Controller
|
|||||||
|
|
||||||
$query->orderBy($sortField === 'sent_at' ? 'received_at' : $sortField, $sortDir);
|
$query->orderBy($sortField === 'sent_at' ? 'received_at' : $sortField, $sortDir);
|
||||||
|
|
||||||
$messages = $query->paginate(20)->withQueryString();
|
$perPage = in_array((int) $request->perPage, [10, 20, 25, 50, 100]) ? (int) $request->perPage : 20;
|
||||||
|
$messages = $query->paginate($perPage)->withQueryString();
|
||||||
$folders = EmailFolder::orderBy('sort_order')->get();
|
$folders = EmailFolder::orderBy('sort_order')->get();
|
||||||
$mailingLists = MailingList::where('attiva', true)->get();
|
$mailingLists = MailingList::where('attiva', true)->get();
|
||||||
$gruppi = Gruppo::orderBy('nome')->get();
|
$gruppi = Gruppo::orderBy('nome')->get();
|
||||||
@@ -93,6 +94,8 @@ class EmailController extends Controller
|
|||||||
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
|
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
|
||||||
$senderAccounts = SenderAccount::active()->get();
|
$senderAccounts = SenderAccount::active()->get();
|
||||||
|
|
||||||
|
$documenti = Documento::orderBy('nome_file')->get();
|
||||||
|
|
||||||
$prefill = [];
|
$prefill = [];
|
||||||
if ($request->reply_to) {
|
if ($request->reply_to) {
|
||||||
$original = EmailMessage::find($request->reply_to);
|
$original = EmailMessage::find($request->reply_to);
|
||||||
@@ -110,7 +113,7 @@ class EmailController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('email.compose', compact('folders', 'mailingLists', 'gruppi', 'individui', 'prefill', 'senderAccounts'));
|
return view('email.compose', compact('folders', 'mailingLists', 'gruppi', 'individui', 'documenti', 'prefill', 'senderAccounts'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function send(Request $request)
|
public function send(Request $request)
|
||||||
@@ -340,7 +343,7 @@ class EmailController extends Controller
|
|||||||
$message->delete();
|
$message->delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
return back()->with('success', 'Email spostata nel cestino.');
|
return redirect()->route('email.index', ['folder' => 'inbox'])->with('success', 'Email spostata nel cestino.');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function downloadAttachment($id)
|
public function downloadAttachment($id)
|
||||||
@@ -564,6 +567,7 @@ class EmailController extends Controller
|
|||||||
private function processAttachments(Request $request): array
|
private function processAttachments(Request $request): array
|
||||||
{
|
{
|
||||||
$paths = [];
|
$paths = [];
|
||||||
|
|
||||||
if ($request->hasFile('allegati')) {
|
if ($request->hasFile('allegati')) {
|
||||||
foreach ($request->file('allegati') as $file) {
|
foreach ($request->file('allegati') as $file) {
|
||||||
if ($file->isValid()) {
|
if ($file->isValid()) {
|
||||||
@@ -571,6 +575,18 @@ class EmailController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$documentIds = $request->input('documenti_selezionati');
|
||||||
|
if (!empty($documentIds)) {
|
||||||
|
$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)) {
|
||||||
|
$paths[] = Storage::path($doc->file_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $paths;
|
return $paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ namespace App\Http\Controllers;
|
|||||||
use App\Models\Evento;
|
use App\Models\Evento;
|
||||||
use App\Models\Gruppo;
|
use App\Models\Gruppo;
|
||||||
use App\Models\Individuo;
|
use App\Models\Individuo;
|
||||||
|
use App\Models\Tag;
|
||||||
use App\Models\TipologiaEvento;
|
use App\Models\TipologiaEvento;
|
||||||
use App\Services\IcsExportService;
|
use App\Services\IcsExportService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
|
|
||||||
class EventoController extends Controller
|
class EventoController extends Controller
|
||||||
@@ -15,7 +17,12 @@ class EventoController extends Controller
|
|||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$this->authorizeRead('eventi');
|
$this->authorizeRead('eventi');
|
||||||
$query = Evento::with(['gruppi', 'responsabili']);
|
$query = Evento::with(['gruppi', 'responsabili', 'tags']);
|
||||||
|
|
||||||
|
if ($request->filled('tag')) {
|
||||||
|
$tagSlugs = (array) $request->input('tag');
|
||||||
|
$query->withAnyTags($tagSlugs);
|
||||||
|
}
|
||||||
|
|
||||||
if ($request->filled('search')) {
|
if ($request->filled('search')) {
|
||||||
$query->where('nome_evento', 'like', '%' . $request->search . '%');
|
$query->where('nome_evento', 'like', '%' . $request->search . '%');
|
||||||
@@ -43,10 +50,47 @@ class EventoController extends Controller
|
|||||||
$query->orderByDesc('created_at');
|
$query->orderByDesc('created_at');
|
||||||
}
|
}
|
||||||
|
|
||||||
$eventi = $query->paginate(20);
|
$perPage = in_array((int) $request->perPage, [10, 20, 25, 50, 100]) ? (int) $request->perPage : 20;
|
||||||
|
$eventi = $query->paginate($perPage);
|
||||||
$gruppi = Gruppo::orderBy('nome')->get();
|
$gruppi = Gruppo::orderBy('nome')->get();
|
||||||
|
$allTags = Tag::orderBy('name')->get();
|
||||||
|
|
||||||
return view('eventi.index', compact('eventi', 'gruppi'));
|
$vista = \App\Models\VistaReport::where('user_id', auth()->id())
|
||||||
|
->where('tipo', 'eventi')
|
||||||
|
->where('is_default', true)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$userVistas = \App\Models\VistaReport::where('user_id', auth()->id())
|
||||||
|
->where('tipo', 'eventi')
|
||||||
|
->orderBy('is_default', 'desc')
|
||||||
|
->orderBy('nome')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$allColumnDefs = [
|
||||||
|
['key' => 'nome_evento', 'label' => 'Nome Evento'],
|
||||||
|
['key' => 'descrizione_evento', 'label' => 'Descrizione'],
|
||||||
|
['key' => 'tipo_evento', 'label' => 'Tipologia'],
|
||||||
|
['key' => 'tipo_recorrenza', 'label' => 'Ricorrenza'],
|
||||||
|
['key' => 'gruppi', 'label' => 'Gruppi'],
|
||||||
|
['key' => 'responsabili', 'label' => 'Responsabili'],
|
||||||
|
['key' => 'tag', 'label' => 'Tag'],
|
||||||
|
['key' => 'data_specifica', 'label' => 'Data'],
|
||||||
|
['key' => 'created_at', 'label' => 'Creato'],
|
||||||
|
];
|
||||||
|
$defaultVisible = ['nome_evento', 'descrizione_evento', 'tipo_evento', 'tipo_recorrenza', 'gruppi', 'responsabili', 'tag', 'created_at'];
|
||||||
|
$visibleColumns = $vista && !empty($vista->colonne_visibili)
|
||||||
|
? $vista->colonne_visibili
|
||||||
|
: $defaultVisible;
|
||||||
|
|
||||||
|
$tableColumns = array_map(fn($col) => [
|
||||||
|
'key' => $col['key'],
|
||||||
|
'label' => $col['label'],
|
||||||
|
'visible' => in_array($col['key'], $visibleColumns),
|
||||||
|
], $allColumnDefs);
|
||||||
|
$entityType = 'eventi';
|
||||||
|
$columnWidths = $vista && $vista->colonne_larghezze ? $vista->colonne_larghezze : [];
|
||||||
|
|
||||||
|
return view('eventi.index', compact('eventi', 'gruppi', 'allTags', 'vista', 'visibleColumns', 'tableColumns', 'entityType', 'columnWidths', 'userVistas'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create(Request $request)
|
public function create(Request $request)
|
||||||
@@ -56,8 +100,9 @@ class EventoController extends Controller
|
|||||||
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
|
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
|
||||||
$selectedGruppo = $request->query('gruppo_id') ? Gruppo::find($request->query('gruppo_id')) : null;
|
$selectedGruppo = $request->query('gruppo_id') ? Gruppo::find($request->query('gruppo_id')) : null;
|
||||||
$tipologieEventi = TipologiaEvento::attive();
|
$tipologieEventi = TipologiaEvento::attive();
|
||||||
|
$tags = Tag::orderBy('name')->get();
|
||||||
|
|
||||||
return view('eventi.create', compact('gruppi', 'individui', 'selectedGruppo', 'tipologieEventi'));
|
return view('eventi.create', compact('gruppi', 'individui', 'selectedGruppo', 'tipologieEventi', 'tags'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
@@ -86,6 +131,8 @@ class EventoController extends Controller
|
|||||||
'responsabili.*' => 'exists:individui,id',
|
'responsabili.*' => 'exists:individui,id',
|
||||||
'luogo_indirizzo' => 'nullable|string|max:500',
|
'luogo_indirizzo' => 'nullable|string|max:500',
|
||||||
'luogo_url_maps' => 'nullable|string',
|
'luogo_url_maps' => 'nullable|string',
|
||||||
|
'tags' => 'nullable|array',
|
||||||
|
'tags.*' => 'exists:tags,id',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$evento = Evento::create([
|
$evento = Evento::create([
|
||||||
@@ -108,6 +155,10 @@ class EventoController extends Controller
|
|||||||
'luogo_url_maps' => $data['luogo_url_maps'] ?? null,
|
'luogo_url_maps' => $data['luogo_url_maps'] ?? null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (!empty($data['tags'])) {
|
||||||
|
$evento->tags()->attach($data['tags']);
|
||||||
|
}
|
||||||
|
|
||||||
if (!empty($data['gruppi'])) {
|
if (!empty($data['gruppi'])) {
|
||||||
$evento->gruppi()->attach($data['gruppi']);
|
$evento->gruppi()->attach($data['gruppi']);
|
||||||
}
|
}
|
||||||
@@ -122,19 +173,21 @@ class EventoController extends Controller
|
|||||||
public function show($evento)
|
public function show($evento)
|
||||||
{
|
{
|
||||||
$this->authorizeRead('eventi');
|
$this->authorizeRead('eventi');
|
||||||
$evento = Evento::with(['gruppi', 'responsabili.contatti', 'documenti'])->findOrFail($evento);
|
$evento = Evento::with(['gruppi', 'responsabili.contatti', 'documenti', 'tags'])->findOrFail($evento);
|
||||||
return view('eventi.show', compact('evento'));
|
return view('eventi.show', compact('evento'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function edit($evento)
|
public function edit($evento)
|
||||||
{
|
{
|
||||||
$this->authorizeWrite('eventi');
|
$this->authorizeWrite('eventi');
|
||||||
$evento = Evento::with(['gruppi', 'responsabili', 'documenti'])->findOrFail($evento);
|
$evento = Evento::with(['gruppi', 'responsabili', 'documenti', 'tags'])->findOrFail($evento);
|
||||||
$gruppi = Gruppo::orderBy('nome')->get();
|
$gruppi = Gruppo::orderBy('nome')->get();
|
||||||
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
|
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
|
||||||
$tipologieEventi = TipologiaEvento::attive();
|
$tipologieEventi = TipologiaEvento::attive();
|
||||||
|
$tags = Tag::orderBy('name')->get();
|
||||||
|
$selectedTags = $evento->tags->pluck('id')->toArray();
|
||||||
|
|
||||||
return view('eventi.edit', compact('evento', 'gruppi', 'individui', 'tipologieEventi'));
|
return view('eventi.edit', compact('evento', 'gruppi', 'individui', 'tipologieEventi', 'tags', 'selectedTags'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(Request $request, $evento)
|
public function update(Request $request, $evento)
|
||||||
@@ -165,6 +218,8 @@ class EventoController extends Controller
|
|||||||
'responsabili.*' => 'exists:individui,id',
|
'responsabili.*' => 'exists:individui,id',
|
||||||
'luogo_indirizzo' => 'nullable|string|max:500',
|
'luogo_indirizzo' => 'nullable|string|max:500',
|
||||||
'luogo_url_maps' => 'nullable|string',
|
'luogo_url_maps' => 'nullable|string',
|
||||||
|
'tags' => 'nullable|array',
|
||||||
|
'tags.*' => 'exists:tags,id',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$evento->update([
|
$evento->update([
|
||||||
@@ -187,6 +242,7 @@ class EventoController extends Controller
|
|||||||
'luogo_url_maps' => $data['luogo_url_maps'] ?? null,
|
'luogo_url_maps' => $data['luogo_url_maps'] ?? null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$evento->tags()->sync($data['tags'] ?? []);
|
||||||
$evento->gruppi()->sync($data['gruppi'] ?? []);
|
$evento->gruppi()->sync($data['gruppi'] ?? []);
|
||||||
$evento->responsabili()->sync($data['responsabili'] ?? []);
|
$evento->responsabili()->sync($data['responsabili'] ?? []);
|
||||||
|
|
||||||
@@ -477,4 +533,101 @@ class EventoController extends Controller
|
|||||||
|
|
||||||
return $colors['primary'];
|
return $colors['primary'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function importIcs()
|
||||||
|
{
|
||||||
|
$this->authorizeWrite('eventi');
|
||||||
|
return view('eventi.import');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function importIcsStore(Request $request)
|
||||||
|
{
|
||||||
|
$this->authorizeWrite('eventi');
|
||||||
|
$request->validate([
|
||||||
|
'file' => 'required|file|mimes:ics,text/calendar',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$file = $request->file('file');
|
||||||
|
$content = file_get_contents($file->path());
|
||||||
|
|
||||||
|
try {
|
||||||
|
$vcalendar = \Sabre\VObject\Reader::read($content);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::warning('Errore lettura file ICS: ' . $e->getMessage());
|
||||||
|
return back()->with('error', 'Il file ICS non è valido: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
$imported = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
foreach ($vcalendar->VEVENT as $vevent) {
|
||||||
|
try {
|
||||||
|
$uid = (string) ($vevent->UID ?? '');
|
||||||
|
if (!empty($uid)) {
|
||||||
|
$existing = Evento::where('uid_esterno', $uid)->first();
|
||||||
|
if ($existing) {
|
||||||
|
$skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$nomeEvento = (string) ($vevent->SUMMARY ?? '');
|
||||||
|
if (empty($nomeEvento)) {
|
||||||
|
$skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dtStart = $vevent->DTSTART ?? null;
|
||||||
|
$dtEnd = $vevent->DTEND ?? null;
|
||||||
|
|
||||||
|
$dataSpecifica = null;
|
||||||
|
$oraInizio = null;
|
||||||
|
$durataMinuti = null;
|
||||||
|
|
||||||
|
if ($dtStart) {
|
||||||
|
$dtStartDateTime = new \DateTime((string) $dtStart);
|
||||||
|
$dataSpecifica = $dtStartDateTime->format('Y-m-d');
|
||||||
|
$oraInizio = $dtStartDateTime->format('H:i');
|
||||||
|
|
||||||
|
if ($dtEnd) {
|
||||||
|
$dtEndDateTime = new \DateTime((string) $dtEnd);
|
||||||
|
$durataMinuti = (int) ($dtEndDateTime->getTimestamp() - $dtStartDateTime->getTimestamp()) / 60;
|
||||||
|
if ($durataMinuti < 1) {
|
||||||
|
$durataMinuti = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$descrizione = (string) ($vevent->DESCRIPTION ?? '');
|
||||||
|
$luogo = (string) ($vevent->LOCATION ?? '');
|
||||||
|
|
||||||
|
Evento::create([
|
||||||
|
'nome_evento' => $nomeEvento,
|
||||||
|
'descrizione' => !empty($descrizione) ? $descrizione : null,
|
||||||
|
'tipo_recorrenza' => 'singolo',
|
||||||
|
'data_specifica' => $dataSpecifica,
|
||||||
|
'ora_inizio' => $oraInizio,
|
||||||
|
'durata_minuti' => $durataMinuti,
|
||||||
|
'luogo_indirizzo' => !empty($luogo) ? $luogo : null,
|
||||||
|
'uid_esterno' => !empty($uid) ? $uid : null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$imported++;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::warning('Errore import evento ICS: ' . $e->getMessage());
|
||||||
|
$errors[] = 'Errore: ' . $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = 'Importati ' . $imported . ' eventi.';
|
||||||
|
if ($skipped > 0) {
|
||||||
|
$message .= ' Saltati ' . $skipped . ' eventi (già presenti o senza nome).';
|
||||||
|
}
|
||||||
|
if (count($errors) > 0) {
|
||||||
|
return back()->with('error', $message . ' Errori: ' . implode('; ', $errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect('/eventi')->with('success', $message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,11 @@ namespace App\Http\Controllers;
|
|||||||
use App\Models\Gruppo;
|
use App\Models\Gruppo;
|
||||||
use App\Models\Individuo;
|
use App\Models\Individuo;
|
||||||
use App\Models\Diocesi;
|
use App\Models\Diocesi;
|
||||||
|
use App\Models\Tag;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Pagination\LengthAwarePaginator;
|
||||||
|
use Illuminate\Pagination\Paginator;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class GruppoController extends Controller
|
class GruppoController extends Controller
|
||||||
{
|
{
|
||||||
@@ -16,6 +20,7 @@ class GruppoController extends Controller
|
|||||||
$this->authorizeRead('gruppi');
|
$this->authorizeRead('gruppi');
|
||||||
|
|
||||||
$viewMode = request('view', 'table');
|
$viewMode = request('view', 'table');
|
||||||
|
$perPage = in_array((int) request('perPage'), [10, 20, 25, 50, 100]) ? (int) request('perPage') : 20;
|
||||||
$user = auth()->user();
|
$user = auth()->user();
|
||||||
|
|
||||||
$vista = \App\Models\VistaReport::where('user_id', $user->id)
|
$vista = \App\Models\VistaReport::where('user_id', $user->id)
|
||||||
@@ -23,8 +28,28 @@ class GruppoController extends Controller
|
|||||||
->where('is_default', true)
|
->where('is_default', true)
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
$allColumns = ['nome', 'descrizione', 'diocesi', 'livello', 'padre', 'membri', 'responsabili', 'indirizzo', 'citta', 'telefono', 'email'];
|
$userVistas = \App\Models\VistaReport::where('user_id', $user->id)
|
||||||
$defaultVisible = ['nome', 'livello', 'padre', 'membri', 'responsabili'];
|
->where('tipo', 'gruppi')
|
||||||
|
->orderBy('is_default', 'desc')
|
||||||
|
->orderBy('nome')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$allColumnDefs = [
|
||||||
|
['key' => 'nome', 'label' => 'Nome'],
|
||||||
|
['key' => 'descrizione', 'label' => 'Descrizione'],
|
||||||
|
['key' => 'diocesi', 'label' => 'Diocesi'],
|
||||||
|
['key' => 'livello', 'label' => 'Livello'],
|
||||||
|
['key' => 'padre', 'label' => 'Gruppo Padre'],
|
||||||
|
['key' => 'membri', 'label' => 'Membri'],
|
||||||
|
['key' => 'responsabili', 'label' => 'Responsabili'],
|
||||||
|
['key' => 'indirizzo', 'label' => 'Indirizzo'],
|
||||||
|
['key' => 'citta', 'label' => 'Città'],
|
||||||
|
['key' => 'telefono', 'label' => 'Telefono'],
|
||||||
|
['key' => 'email', 'label' => 'Email'],
|
||||||
|
['key' => 'tag', 'label' => 'Tag'],
|
||||||
|
];
|
||||||
|
$allColumns = array_column($allColumnDefs, 'key');
|
||||||
|
$defaultVisible = ['nome', 'livello', 'padre', 'membri', 'responsabili', 'tag'];
|
||||||
|
|
||||||
if ($vista && !empty($vista->colonne_visibili)) {
|
if ($vista && !empty($vista->colonne_visibili)) {
|
||||||
$visibleColumns = $vista->colonne_visibili;
|
$visibleColumns = $vista->colonne_visibili;
|
||||||
@@ -32,9 +57,22 @@ class GruppoController extends Controller
|
|||||||
$visibleColumns = $defaultVisible;
|
$visibleColumns = $defaultVisible;
|
||||||
}
|
}
|
||||||
|
|
||||||
$allGruppi = Gruppo::with(['diocesi', 'parent', 'children', 'individui', 'avatar'])
|
$tableColumns = array_map(fn($col) => [
|
||||||
->orderBy('nome')
|
'key' => $col['key'],
|
||||||
->get()
|
'label' => $col['label'],
|
||||||
|
'visible' => in_array($col['key'], $visibleColumns),
|
||||||
|
], $allColumnDefs);
|
||||||
|
$entityType = 'gruppi';
|
||||||
|
$columnWidths = $vista && $vista->colonne_larghezze ? $vista->colonne_larghezze : [];
|
||||||
|
|
||||||
|
$gruppiQuery = Gruppo::with(['diocesi', 'parent', 'children', 'individui', 'avatar', 'tags']);
|
||||||
|
|
||||||
|
if (request()->filled('tag')) {
|
||||||
|
$tagSlugs = (array) request()->input('tag');
|
||||||
|
$gruppiQuery->withAnyTags($tagSlugs);
|
||||||
|
}
|
||||||
|
|
||||||
|
$allGruppi = $gruppiQuery->orderBy('nome')->get()
|
||||||
->map(function ($gruppo) {
|
->map(function ($gruppo) {
|
||||||
$depth = 0;
|
$depth = 0;
|
||||||
$parent = $gruppo->parent;
|
$parent = $gruppo->parent;
|
||||||
@@ -47,17 +85,33 @@ class GruppoController extends Controller
|
|||||||
});
|
});
|
||||||
|
|
||||||
if ($viewMode === 'table') {
|
if ($viewMode === 'table') {
|
||||||
$gruppi = $this->sortGruppiHierarchically($allGruppi);
|
$sorted = $this->sortGruppiHierarchically($allGruppi);
|
||||||
|
$page = Paginator::resolveCurrentPage();
|
||||||
|
$gruppi = new LengthAwarePaginator(
|
||||||
|
$sorted->forPage($page, $perPage)->values(),
|
||||||
|
$sorted->count(),
|
||||||
|
$perPage,
|
||||||
|
$page,
|
||||||
|
['path' => Paginator::resolveCurrentPath()]
|
||||||
|
);
|
||||||
|
$gruppi->appends(request()->except('page'));
|
||||||
} else {
|
} else {
|
||||||
$gruppi = $allGruppi;
|
$gruppi = $allGruppi;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$allTags = Tag::orderBy('name')->get();
|
||||||
|
|
||||||
return view('gruppi.index', compact(
|
return view('gruppi.index', compact(
|
||||||
'gruppi',
|
'gruppi',
|
||||||
'allColumns',
|
'allColumns',
|
||||||
'visibleColumns',
|
'visibleColumns',
|
||||||
'viewMode',
|
'viewMode',
|
||||||
'vista'
|
'vista',
|
||||||
|
'allTags',
|
||||||
|
'tableColumns',
|
||||||
|
'entityType',
|
||||||
|
'columnWidths',
|
||||||
|
'userVistas'
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +145,8 @@ class GruppoController extends Controller
|
|||||||
$allGruppi = Gruppo::orderBy('nome')->get();
|
$allGruppi = Gruppo::orderBy('nome')->get();
|
||||||
$selectedParent = $request->query('parent_id') ? Gruppo::find($request->query('parent_id')) : null;
|
$selectedParent = $request->query('parent_id') ? Gruppo::find($request->query('parent_id')) : null;
|
||||||
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
|
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
|
||||||
return view('gruppi.create', compact('diocesi', 'allGruppi', 'selectedParent', 'individui'));
|
$tags = Tag::orderBy('name')->get();
|
||||||
|
return view('gruppi.create', compact('diocesi', 'allGruppi', 'selectedParent', 'individui', 'tags'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
@@ -109,6 +164,8 @@ class GruppoController extends Controller
|
|||||||
'città_incontro' => 'nullable|string|max:255',
|
'città_incontro' => 'nullable|string|max:255',
|
||||||
'sigla_provincia_incontro' => 'nullable|string|max:2',
|
'sigla_provincia_incontro' => 'nullable|string|max:2',
|
||||||
'mappa_posizione' => 'nullable|string',
|
'mappa_posizione' => 'nullable|string',
|
||||||
|
'tags' => 'nullable|array',
|
||||||
|
'tags.*' => 'exists:tags,id',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!empty($data['responsabile_ids'])) {
|
if (!empty($data['responsabile_ids'])) {
|
||||||
@@ -117,6 +174,10 @@ class GruppoController extends Controller
|
|||||||
|
|
||||||
$gruppo = Gruppo::create($data);
|
$gruppo = Gruppo::create($data);
|
||||||
|
|
||||||
|
if ($request->has('tags')) {
|
||||||
|
$gruppo->tags()->sync($request->tags);
|
||||||
|
}
|
||||||
|
|
||||||
if ($request->has('individui')) {
|
if ($request->has('individui')) {
|
||||||
foreach ($request->individui as $individuoData) {
|
foreach ($request->individui as $individuoData) {
|
||||||
if (!empty($individuoData['individuo_id'])) {
|
if (!empty($individuoData['individuo_id'])) {
|
||||||
@@ -136,7 +197,7 @@ class GruppoController extends Controller
|
|||||||
$this->authorizeRead('gruppi');
|
$this->authorizeRead('gruppi');
|
||||||
$gruppo = Gruppo::with(['parent', 'children', 'diocesi', 'individui.contatti', 'individui.documenti', 'individui' => function ($q) {
|
$gruppo = Gruppo::with(['parent', 'children', 'diocesi', 'individui.contatti', 'individui.documenti', 'individui' => function ($q) {
|
||||||
$q->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione');
|
$q->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione');
|
||||||
}, 'avatar', 'eventi' => function ($q) {
|
}, 'avatar', 'tags', 'eventi' => function ($q) {
|
||||||
$q->where('is_incontro_gruppo', true);
|
$q->where('is_incontro_gruppo', true);
|
||||||
}, 'eventi.responsabili.contatti'])->findOrFail($id);
|
}, 'eventi.responsabili.contatti'])->findOrFail($id);
|
||||||
return view('gruppi.show', compact('gruppo'));
|
return view('gruppi.show', compact('gruppo'));
|
||||||
@@ -145,11 +206,13 @@ class GruppoController extends Controller
|
|||||||
public function edit($id)
|
public function edit($id)
|
||||||
{
|
{
|
||||||
$this->authorizeWrite('gruppi');
|
$this->authorizeWrite('gruppi');
|
||||||
$gruppo = Gruppo::with(['individui.contatti', 'individui.documenti', 'avatar'])->findOrFail($id);
|
$gruppo = Gruppo::with(['individui.contatti', 'individui.documenti', 'avatar', 'tags'])->findOrFail($id);
|
||||||
$diocesi = Diocesi::orderBy('nome')->get();
|
$diocesi = Diocesi::orderBy('nome')->get();
|
||||||
$gruppi = Gruppo::where('id', '!=', $gruppo->id)->orderBy('nome')->get();
|
$gruppi = Gruppo::where('id', '!=', $gruppo->id)->orderBy('nome')->get();
|
||||||
$membri = $gruppo->individui()->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione')->orderBy('cognome')->orderBy('nome')->get();
|
$membri = $gruppo->individui()->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione')->orderBy('cognome')->orderBy('nome')->get();
|
||||||
return view('gruppi.edit', compact('gruppo', 'diocesi', 'gruppi', 'membri'));
|
$tags = Tag::orderBy('name')->get();
|
||||||
|
$selectedTags = $gruppo->tags->pluck('id')->toArray();
|
||||||
|
return view('gruppi.edit', compact('gruppo', 'diocesi', 'gruppi', 'membri', 'tags', 'selectedTags'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(Request $request, $id)
|
public function update(Request $request, $id)
|
||||||
@@ -168,6 +231,8 @@ class GruppoController extends Controller
|
|||||||
'città_incontro' => 'nullable|string|max:255',
|
'città_incontro' => 'nullable|string|max:255',
|
||||||
'sigla_provincia_incontro' => 'nullable|string|max:2',
|
'sigla_provincia_incontro' => 'nullable|string|max:2',
|
||||||
'mappa_posizione' => 'nullable|string',
|
'mappa_posizione' => 'nullable|string',
|
||||||
|
'tags' => 'nullable|array',
|
||||||
|
'tags.*' => 'exists:tags,id',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!empty($data['responsabile_ids'])) {
|
if (!empty($data['responsabile_ids'])) {
|
||||||
@@ -178,6 +243,12 @@ class GruppoController extends Controller
|
|||||||
|
|
||||||
$gruppo->update($data);
|
$gruppo->update($data);
|
||||||
|
|
||||||
|
if ($request->has('tags')) {
|
||||||
|
$gruppo->tags()->sync($request->tags);
|
||||||
|
} else {
|
||||||
|
$gruppo->tags()->detach();
|
||||||
|
}
|
||||||
|
|
||||||
if ($request->has('individui')) {
|
if ($request->has('individui')) {
|
||||||
$gruppo->individui()->detach();
|
$gruppo->individui()->detach();
|
||||||
foreach ($request->individui as $individuoData) {
|
foreach ($request->individui as $individuoData) {
|
||||||
@@ -300,4 +371,156 @@ class GruppoController extends Controller
|
|||||||
|
|
||||||
return redirect()->route('gruppi.index')->with('success', 'Vista eliminata.');
|
return redirect()->route('gruppi.index')->with('success', 'Vista eliminata.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function import()
|
||||||
|
{
|
||||||
|
$this->authorizeWrite('gruppi');
|
||||||
|
return view('gruppi.import');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function importStore(Request $request)
|
||||||
|
{
|
||||||
|
$this->authorizeWrite('gruppi');
|
||||||
|
$request->validate([
|
||||||
|
'file' => 'required|file|mimes:csv,txt',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$file = $request->file('file');
|
||||||
|
$handle = fopen($file->path(), 'r');
|
||||||
|
$header = fgetcsv($handle, 1000, ',');
|
||||||
|
|
||||||
|
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) {
|
||||||
|
$rowNumber++;
|
||||||
|
|
||||||
|
if (count($row) < count($header)) {
|
||||||
|
$skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = array_combine($header, $row);
|
||||||
|
|
||||||
|
if ($data === false) {
|
||||||
|
$skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = array_map('trim', $data);
|
||||||
|
$nome = $data['nome'] ?? '';
|
||||||
|
|
||||||
|
if (empty($nome)) {
|
||||||
|
$skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Gruppo::create([
|
||||||
|
'nome' => $nome,
|
||||||
|
'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,
|
||||||
|
'indirizzo_incontro' => $data['indirizzo_incontro'] ?? null,
|
||||||
|
'cap_incontro' => $data['cap_incontro'] ?? null,
|
||||||
|
'città_incontro' => $data['città_incontro'] ?? null,
|
||||||
|
'sigla_provincia_incontro' => $data['sigla_provincia_incontro'] ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$imported++;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::warning('Errore import gruppo riga ' . $rowNumber . ': ' . $e->getMessage());
|
||||||
|
$errors[] = 'Errore riga ' . $rowNumber . ': ' . $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
$message = 'Importati ' . $imported . ' gruppi.';
|
||||||
|
if ($skipped > 0) {
|
||||||
|
$message .= ' Saltate ' . $skipped . ' righe (vuote o incomplete).';
|
||||||
|
}
|
||||||
|
if (count($errors) > 0) {
|
||||||
|
return back()->with('error', $message . ' Errori: ' . implode('; ', $errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('gruppi.index')->with('success', $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
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'];
|
||||||
|
|
||||||
|
$csv = implode(',', $headers) . "\n" . implode(',', $example);
|
||||||
|
|
||||||
|
return response($csv, 200, [
|
||||||
|
'Content-Type' => 'text/csv',
|
||||||
|
'Content-Disposition' => 'attachment; filename="template_gruppi.csv"',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function massTag(Request $request)
|
||||||
|
{
|
||||||
|
$this->authorizeWrite('gruppi');
|
||||||
|
$idsInput = $request->input('ids', '');
|
||||||
|
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
|
||||||
|
|
||||||
|
if (empty($ids)) {
|
||||||
|
return redirect()->route('gruppi.index')->with('error', 'Nessun gruppo selezionato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $request->validate([
|
||||||
|
'tags' => 'required|array',
|
||||||
|
'tags.*' => 'exists:tags,id',
|
||||||
|
'mode' => 'required|in:assign,remove',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tagIds = $data['tags'];
|
||||||
|
$mode = $data['mode'];
|
||||||
|
$count = 0;
|
||||||
|
|
||||||
|
Gruppo::whereIn('id', $ids)->chunk(100, function ($gruppi) use ($tagIds, $mode, &$count) {
|
||||||
|
foreach ($gruppi as $gruppo) {
|
||||||
|
if ($mode === 'assign') {
|
||||||
|
$gruppo->tags()->syncWithoutDetaching($tagIds);
|
||||||
|
} else {
|
||||||
|
$gruppo->tags()->detach($tagIds);
|
||||||
|
}
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$actionLabel = $mode === 'assign' ? 'assegnati' : 'rimossi';
|
||||||
|
return redirect()->route('gruppi.index')->with('success', "Tag {$actionLabel} per {$count} gruppi.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function massElimina(Request $request)
|
||||||
|
{
|
||||||
|
$this->authorizeDelete('gruppi');
|
||||||
|
$ids = $request->input('ids', []);
|
||||||
|
if (!is_array($ids) || empty($ids)) {
|
||||||
|
return redirect()->route('gruppi.index')->with('error', 'Nessun gruppo selezionato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$gruppi = Gruppo::whereIn('id', $ids)->get();
|
||||||
|
$count = 0;
|
||||||
|
|
||||||
|
foreach ($gruppi as $gruppo) {
|
||||||
|
$gruppo->individui()->detach();
|
||||||
|
$gruppo->delete();
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('gruppi.index')->with('success', "{$count} gruppi eliminati con successo.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,13 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\AppSetting;
|
use App\Models\AppSetting;
|
||||||
|
use App\Models\CalendarioConnessione;
|
||||||
|
use App\Models\Documento;
|
||||||
use App\Models\EmailSetting;
|
use App\Models\EmailSetting;
|
||||||
use App\Models\Ruolo;
|
use App\Models\Ruolo;
|
||||||
use App\Models\SenderAccount;
|
use App\Models\SenderAccount;
|
||||||
use App\Models\StorageRepository;
|
use App\Models\StorageRepository;
|
||||||
|
use App\Models\Tag;
|
||||||
use App\Models\TipologiaDocumento;
|
use App\Models\TipologiaDocumento;
|
||||||
use App\Models\TipologiaEvento;
|
use App\Models\TipologiaEvento;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
@@ -24,13 +27,15 @@ class ImpostazioniController extends Controller
|
|||||||
$tipologie = TipologiaDocumento::orderBy('ordine')->get();
|
$tipologie = TipologiaDocumento::orderBy('ordine')->get();
|
||||||
$tipologieEventi = TipologiaEvento::orderBy('ordine')->get();
|
$tipologieEventi = TipologiaEvento::orderBy('ordine')->get();
|
||||||
$ruoli = Ruolo::orderBy('ordine')->get();
|
$ruoli = Ruolo::orderBy('ordine')->get();
|
||||||
|
$tags = Tag::orderBy('order_column')->get();
|
||||||
$appSettings = AppSetting::first();
|
$appSettings = AppSetting::first();
|
||||||
$emailSettings = EmailSetting::first() ?? new EmailSetting();
|
$emailSettings = EmailSetting::first() ?? new EmailSetting();
|
||||||
$senderAccounts = SenderAccount::orderBy('email_address')->get();
|
$senderAccounts = SenderAccount::orderBy('email_address')->get();
|
||||||
$repositories = StorageRepository::orderBy('ordine')->get();
|
$repositories = StorageRepository::orderBy('ordine')->get();
|
||||||
$googleDriveNewToken = session('google_drive_new_token');
|
$googleDriveNewToken = session('google_drive_new_token');
|
||||||
|
$calendarioConnessioni = CalendarioConnessione::orderBy('ordine')->get();
|
||||||
|
|
||||||
return view('impostazioni.index', compact('tipologie', 'tipologieEventi', 'ruoli', 'appSettings', 'emailSettings', 'senderAccounts', 'repositories', 'googleDriveNewToken'));
|
return view('impostazioni.index', compact('tipologie', 'tipologieEventi', 'ruoli', 'tags', 'appSettings', 'emailSettings', 'senderAccounts', 'repositories', 'googleDriveNewToken', 'calendarioConnessioni'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function saveAppSettings(Request $request)
|
public function saveAppSettings(Request $request)
|
||||||
@@ -268,10 +273,17 @@ class ImpostazioniController extends Controller
|
|||||||
|
|
||||||
$ruolo = Ruolo::findOrFail($id);
|
$ruolo = Ruolo::findOrFail($id);
|
||||||
|
|
||||||
$count = \DB::table('gruppo_individuo')
|
$allRows = \DB::table('gruppo_individuo')
|
||||||
->whereNotNull('ruolo_ids')
|
->whereNotNull('ruolo_ids')
|
||||||
->whereRaw("JSON_CONTAINS(ruolo_ids, ?)", [json_encode($id)])
|
->get(['id', 'ruolo_ids']);
|
||||||
->count();
|
|
||||||
|
$count = 0;
|
||||||
|
foreach ($allRows as $row) {
|
||||||
|
$ids = json_decode($row->ruolo_ids, true);
|
||||||
|
if (is_array($ids) && in_array((int) $id, $ids)) {
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($count > 0) {
|
if ($count > 0) {
|
||||||
return redirect('/impostazioni#ruoli')->with('error', 'Impossibile eliminare: ' . $count . ' membri associati a questo ruolo.');
|
return redirect('/impostazioni#ruoli')->with('error', 'Impossibile eliminare: ' . $count . ' membri associati a questo ruolo.');
|
||||||
@@ -361,6 +373,70 @@ class ImpostazioniController extends Controller
|
|||||||
return response()->json(['success' => true]);
|
return response()->json(['success' => true]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function tagStore(Request $request)
|
||||||
|
{
|
||||||
|
$this->authorizeWrite('settings');
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:100|unique:tags,name',
|
||||||
|
'color' => 'nullable|string|max:7',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$maxOrdine = Tag::max('order_column') ?? 0;
|
||||||
|
|
||||||
|
Tag::create([
|
||||||
|
'name' => $validated['name'],
|
||||||
|
'slug' => str()->slug($validated['name']),
|
||||||
|
'color' => $validated['color'] ?? null,
|
||||||
|
'order_column' => $maxOrdine + 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect('/impostazioni#tag')->with('success', 'Tag aggiunto.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tagUpdate(Request $request, $id)
|
||||||
|
{
|
||||||
|
$this->authorizeWrite('settings');
|
||||||
|
|
||||||
|
$tag = Tag::findOrFail($id);
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:100|unique:tags,name,' . $id,
|
||||||
|
'color' => 'nullable|string|max:7',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tag->update([
|
||||||
|
'name' => $validated['name'],
|
||||||
|
'slug' => str()->slug($validated['name']),
|
||||||
|
'color' => $validated['color'] ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect('/impostazioni#tag')->with('success', 'Tag aggiornato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tagDestroy($id)
|
||||||
|
{
|
||||||
|
$this->authorizeDelete('settings');
|
||||||
|
|
||||||
|
$tag = Tag::findOrFail($id);
|
||||||
|
$tag->delete();
|
||||||
|
|
||||||
|
return redirect('/impostazioni#tag')->with('success', 'Tag eliminato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tagReorder(Request $request)
|
||||||
|
{
|
||||||
|
$this->authorizeWrite('settings');
|
||||||
|
|
||||||
|
$order = $request->input('order', []);
|
||||||
|
|
||||||
|
foreach ($order as $index => $id) {
|
||||||
|
Tag::where('id', $id)->update(['order_column' => $index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json(['success' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
public function uploadLogo(Request $request)
|
public function uploadLogo(Request $request)
|
||||||
{
|
{
|
||||||
$this->authorizeWrite('settings');
|
$this->authorizeWrite('settings');
|
||||||
@@ -388,6 +464,15 @@ class ImpostazioniController extends Controller
|
|||||||
$settings->save();
|
$settings->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$linkPath = public_path('storage');
|
||||||
|
if (!file_exists($linkPath) && !is_link($linkPath)) {
|
||||||
|
try {
|
||||||
|
symlink(storage_path('app/public'), $linkPath);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::warning('Impossibile creare symlink storage: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return redirect('/impostazioni#logo')->with('success', 'Logo caricato con successo.');
|
return redirect('/impostazioni#logo')->with('success', 'Logo caricato con successo.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,19 +5,32 @@ namespace App\Http\Controllers;
|
|||||||
use App\Models\Individuo;
|
use App\Models\Individuo;
|
||||||
use App\Models\Contatto;
|
use App\Models\Contatto;
|
||||||
use App\Models\Documento;
|
use App\Models\Documento;
|
||||||
use App\Models\Comune;
|
use App\Models\Gruppo;
|
||||||
use App\Models\Diocesi;
|
use App\Models\Comune;
|
||||||
use Illuminate\Http\Request;
|
use App\Models\Diocesi;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use App\Models\Tag;
|
||||||
use Illuminate\Validation\Rule;
|
use App\Models\VistaReport;
|
||||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
|
|
||||||
class IndividuoController extends Controller
|
class IndividuoController extends Controller
|
||||||
{
|
{
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$this->authorizeRead('individui');
|
$this->authorizeRead('individui');
|
||||||
$individui = Individuo::with('contatti')->orderBy('cognome')->orderBy('nome')->paginate(20);
|
$perPage = in_array((int) $request->perPage, [10, 20, 25, 50, 100]) ? (int) $request->perPage : 20;
|
||||||
|
|
||||||
|
$query = Individuo::with('contatti', 'tags');
|
||||||
|
|
||||||
|
if ($request->filled('tag')) {
|
||||||
|
$tagSlugs = (array) $request->input('tag');
|
||||||
|
$query->withAnyTags($tagSlugs);
|
||||||
|
}
|
||||||
|
|
||||||
|
$individui = $query->orderBy('cognome')->orderBy('nome')->paginate($perPage);
|
||||||
|
$allTags = Tag::orderBy('name')->get();
|
||||||
|
|
||||||
$vista = null;
|
$vista = null;
|
||||||
|
|
||||||
@@ -38,16 +51,43 @@ class IndividuoController extends Controller
|
|||||||
['key' => 'codice', 'label' => 'Codice'],
|
['key' => 'codice', 'label' => 'Codice'],
|
||||||
['key' => 'cognome', 'label' => 'Cognome'],
|
['key' => 'cognome', 'label' => 'Cognome'],
|
||||||
['key' => 'nome', 'label' => 'Nome'],
|
['key' => 'nome', 'label' => 'Nome'],
|
||||||
|
['key' => 'data_nascita', 'label' => 'Data Nascita'],
|
||||||
|
['key' => 'genere', 'label' => 'Genere'],
|
||||||
|
['key' => 'indirizzo', 'label' => 'Indirizzo'],
|
||||||
|
['key' => 'cap', 'label' => 'CAP'],
|
||||||
|
['key' => 'citta', 'label' => 'Città'],
|
||||||
|
['key' => 'provincia', 'label' => 'Provincia'],
|
||||||
|
['key' => 'tipo_documento', 'label' => 'Tipo Documento'],
|
||||||
|
['key' => 'numero_documento', 'label' => 'Numero Documento'],
|
||||||
|
['key' => 'scadenza_documento', 'label' => 'Scadenza Documento'],
|
||||||
['key' => 'email', 'label' => 'Email'],
|
['key' => 'email', 'label' => 'Email'],
|
||||||
['key' => 'telefono', 'label' => 'Telefono'],
|
['key' => 'telefono', 'label' => 'Telefono'],
|
||||||
|
['key' => 'note', 'label' => 'Note'],
|
||||||
|
['key' => 'tag', 'label' => 'Tag'],
|
||||||
|
['key' => 'created_at', 'label' => 'Creato il'],
|
||||||
];
|
];
|
||||||
|
|
||||||
$defaultVisible = ['codice', 'cognome', 'nome', 'email', 'telefono'];
|
$defaultVisible = ['codice', 'cognome', 'nome', 'email', 'telefono', 'tag'];
|
||||||
$visibleColumns = $vista && !empty($vista->colonne_visibili)
|
$visibleColumns = $vista && !empty($vista->colonne_visibili)
|
||||||
? $vista->colonne_visibili
|
? $vista->colonne_visibili
|
||||||
: $defaultVisible;
|
: $defaultVisible;
|
||||||
|
|
||||||
return view('individui.index', compact('individui', 'vista', 'allColumns', 'visibleColumns'));
|
$entityType = 'individui';
|
||||||
|
$tableColumns = array_map(fn($col) => [
|
||||||
|
'key' => $col['key'],
|
||||||
|
'label' => $col['label'],
|
||||||
|
'visible' => in_array($col['key'], $visibleColumns),
|
||||||
|
], $allColumns);
|
||||||
|
$columnWidths = $vista && $vista->colonne_larghezze ? $vista->colonne_larghezze : [];
|
||||||
|
|
||||||
|
$userVistas = VistaReport::where('user_id', auth()->id())
|
||||||
|
->where('tipo', 'individui')
|
||||||
|
->orderBy('is_default', 'desc')
|
||||||
|
->orderBy('nome')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$gruppi = Gruppo::orderBy('nome')->get(['id', 'nome']);
|
||||||
|
return view('individui.index', compact('individui', 'vista', 'allColumns', 'visibleColumns', 'allTags', 'gruppi', 'entityType', 'tableColumns', 'columnWidths', 'userVistas'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function exportCSV(Request $request)
|
public function exportCSV(Request $request)
|
||||||
@@ -109,7 +149,8 @@ public function create()
|
|||||||
$this->authorizeWrite('individui');
|
$this->authorizeWrite('individui');
|
||||||
$comuni = Comune::orderBy('nome')->get();
|
$comuni = Comune::orderBy('nome')->get();
|
||||||
$diocesi = Diocesi::orderBy('nome')->get();
|
$diocesi = Diocesi::orderBy('nome')->get();
|
||||||
return view('individui.create', compact('comuni', 'diocesi'));
|
$tags = Tag::orderBy('name')->get();
|
||||||
|
return view('individui.create', compact('comuni', 'diocesi', 'tags'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
@@ -128,10 +169,16 @@ public function create()
|
|||||||
'numero_documento' => 'nullable|string|max:50',
|
'numero_documento' => 'nullable|string|max:50',
|
||||||
'scadenza_documento' => 'nullable|date',
|
'scadenza_documento' => 'nullable|date',
|
||||||
'note' => 'nullable|string',
|
'note' => 'nullable|string',
|
||||||
|
'tags' => 'nullable|array',
|
||||||
|
'tags.*' => 'exists:tags,id',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$individuo = Individuo::create($data);
|
$individuo = Individuo::create($data);
|
||||||
|
|
||||||
|
if ($request->has('tags')) {
|
||||||
|
$individuo->tags()->sync($request->tags);
|
||||||
|
}
|
||||||
|
|
||||||
if ($request->has('contatti')) {
|
if ($request->has('contatti')) {
|
||||||
foreach ($request->contatti as $contatto) {
|
foreach ($request->contatti as $contatto) {
|
||||||
if (!empty($contatto['tipo']) && !empty($contatto['valore'])) {
|
if (!empty($contatto['tipo']) && !empty($contatto['valore'])) {
|
||||||
@@ -153,7 +200,7 @@ public function create()
|
|||||||
$this->authorizeRead('individui');
|
$this->authorizeRead('individui');
|
||||||
$individuo = Individuo::with(['contatti', 'gruppi' => function ($q) {
|
$individuo = Individuo::with(['contatti', 'gruppi' => function ($q) {
|
||||||
$q->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione');
|
$q->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione');
|
||||||
}, 'avatar'])->findOrFail($individuo);
|
}, 'avatar', 'tags'])->findOrFail($individuo);
|
||||||
return view('individui.show', compact('individuo'));
|
return view('individui.show', compact('individuo'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,8 +226,10 @@ public function create()
|
|||||||
public function edit($individuo)
|
public function edit($individuo)
|
||||||
{
|
{
|
||||||
$this->authorizeWrite('individui');
|
$this->authorizeWrite('individui');
|
||||||
$individuo = Individuo::with(['contatti', 'gruppi', 'avatar'])->findOrFail($individuo);
|
$individuo = Individuo::with(['contatti', 'gruppi', 'avatar', 'tags'])->findOrFail($individuo);
|
||||||
return view('individui.edit', compact('individuo'));
|
$tags = Tag::orderBy('name')->get();
|
||||||
|
$selectedTags = $individuo->tags->pluck('id')->toArray();
|
||||||
|
return view('individui.edit', compact('individuo', 'tags', 'selectedTags'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(Request $request, $individuo)
|
public function update(Request $request, $individuo)
|
||||||
@@ -204,17 +253,29 @@ public function create()
|
|||||||
'numero_documento' => 'nullable|string|max:50',
|
'numero_documento' => 'nullable|string|max:50',
|
||||||
'scadenza_documento' => 'nullable|date',
|
'scadenza_documento' => 'nullable|date',
|
||||||
'note' => 'nullable|string',
|
'note' => 'nullable|string',
|
||||||
|
'tags' => 'nullable|array',
|
||||||
|
'tags.*' => 'exists:tags,id',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$individuo->update($data);
|
$individuo->update($data);
|
||||||
|
|
||||||
|
if ($request->has('tags')) {
|
||||||
|
$individuo->tags()->sync($request->tags);
|
||||||
|
} else {
|
||||||
|
$individuo->tags()->detach();
|
||||||
|
}
|
||||||
|
|
||||||
\Log::info('Individuo dati salvati', ['id' => $individuo->id]);
|
\Log::info('Individuo dati salvati', ['id' => $individuo->id]);
|
||||||
|
|
||||||
$contattiData = $request->input('contatti');
|
if ($request->has('contatti')) {
|
||||||
\Log::info('Contatti ricevuti:', ['contatti' => $contattiData, 'tipo' => gettype($contattiData)]);
|
$contattiData = $request->input('contatti');
|
||||||
|
\Log::info('Contatti ricevuti:', ['contatti' => $contattiData, 'tipo' => gettype($contattiData)]);
|
||||||
|
|
||||||
$individuo->contatti()->delete();
|
$individuo->contatti()->delete();
|
||||||
\Log::info('Contatti precedenti eliminati, count ora:', ['count' => $individuo->contatti()->count()]);
|
\Log::info('Contatti precedenti eliminati, count ora:', ['count' => $individuo->contatti()->count()]);
|
||||||
|
} else {
|
||||||
|
$contattiData = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!empty($contattiData) && is_array($contattiData)) {
|
if (!empty($contattiData) && is_array($contattiData)) {
|
||||||
$created = 0;
|
$created = 0;
|
||||||
@@ -303,6 +364,75 @@ public function create()
|
|||||||
return redirect()->route('individui.index')->with('success', count($individui) . ' individui eliminati con successo.');
|
return redirect()->route('individui.index')->with('success', count($individui) . ' individui eliminati con successo.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function massGruppo(Request $request)
|
||||||
|
{
|
||||||
|
$this->authorizeWrite('individui');
|
||||||
|
$idsInput = $request->input('ids', '');
|
||||||
|
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
|
||||||
|
if (empty($ids)) {
|
||||||
|
return redirect()->route('individui.index')->with('error', 'Nessun individuo selezionato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $request->validate([
|
||||||
|
'gruppi' => 'required|array',
|
||||||
|
'gruppi.*' => 'exists:gruppi,id',
|
||||||
|
'mode' => 'required|in:assign,remove',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$gruppiIds = $data['gruppi'];
|
||||||
|
$mode = $data['mode'];
|
||||||
|
$count = 0;
|
||||||
|
|
||||||
|
foreach ($ids as $id) {
|
||||||
|
$individuo = Individuo::find($id);
|
||||||
|
if (!$individuo) continue;
|
||||||
|
|
||||||
|
if ($mode === 'assign') {
|
||||||
|
$individuo->gruppi()->syncWithoutDetaching($gruppiIds);
|
||||||
|
} else {
|
||||||
|
$individuo->gruppi()->detach($gruppiIds);
|
||||||
|
}
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$actionLabel = $mode === 'assign' ? 'assegnati' : 'rimossi';
|
||||||
|
return redirect()->route('individui.index')->with('success', "Gruppi {$actionLabel} per {$count} individui.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function massTag(Request $request)
|
||||||
|
{
|
||||||
|
$this->authorizeWrite('individui');
|
||||||
|
$idsInput = $request->input('ids', '');
|
||||||
|
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
|
||||||
|
if (empty($ids)) {
|
||||||
|
return redirect()->route('individui.index')->with('error', 'Nessun individuo selezionato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $request->validate([
|
||||||
|
'tags' => 'required|array',
|
||||||
|
'tags.*' => 'exists:tags,id',
|
||||||
|
'mode' => 'required|in:assign,remove',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tagIds = $data['tags'];
|
||||||
|
$mode = $data['mode'];
|
||||||
|
$count = 0;
|
||||||
|
|
||||||
|
Individuo::whereIn('id', $ids)->chunk(100, function ($individui) use ($tagIds, $mode, &$count) {
|
||||||
|
foreach ($individui as $individuo) {
|
||||||
|
if ($mode === 'assign') {
|
||||||
|
$individuo->tags()->syncWithoutDetaching($tagIds);
|
||||||
|
} else {
|
||||||
|
$individuo->tags()->detach($tagIds);
|
||||||
|
}
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$actionLabel = $mode === 'assign' ? 'assegnati' : 'rimossi';
|
||||||
|
return redirect()->route('individui.index')->with('success', "Tag {$actionLabel} per {$count} individui.");
|
||||||
|
}
|
||||||
|
|
||||||
public function elementiCollegati($individuo)
|
public function elementiCollegati($individuo)
|
||||||
{
|
{
|
||||||
$individuo = Individuo::findOrFail($individuo);
|
$individuo = Individuo::findOrFail($individuo);
|
||||||
|
|||||||
@@ -95,9 +95,17 @@ class MailingController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$emails = $destinatari->flatMap(fn($ind) => $ind->contatti->where('tipo', 'email')->pluck('valore'))
|
$totaleDestinatari = $destinatari->count();
|
||||||
|
$emails = $destinatari->map(fn($ind) => $ind->contatti->where('tipo', 'email')->first()?->valore)
|
||||||
->filter()->unique()->values();
|
->filter()->unique()->values();
|
||||||
|
|
||||||
|
$skipped = $totaleDestinatari - $emails->count();
|
||||||
|
if ($skipped > 0) {
|
||||||
|
Log::info("Mailing invia: {$skipped} contatti saltati perché senza email", [
|
||||||
|
'lista_id' => $data['lista_id'] ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
if ($emails->isEmpty()) {
|
if ($emails->isEmpty()) {
|
||||||
return back()->with('error', 'Nessun destinatario email valido trovato.')->withInput();
|
return back()->with('error', 'Nessun destinatario email valido trovato.')->withInput();
|
||||||
}
|
}
|
||||||
@@ -229,9 +237,17 @@ class MailingController extends Controller
|
|||||||
->with('individuo.contatti')
|
->with('individuo.contatti')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
|
$totaleContatti = $contatti->count();
|
||||||
$emails = $contatti->map(fn($c) => $c->individuo?->contatti->where('tipo', 'email')->first()?->valore)
|
$emails = $contatti->map(fn($c) => $c->individuo?->contatti->where('tipo', 'email')->first()?->valore)
|
||||||
->filter()->unique()->values();
|
->filter()->unique()->values();
|
||||||
|
|
||||||
|
$skipped = $totaleContatti - $emails->count();
|
||||||
|
if ($skipped > 0) {
|
||||||
|
Log::info("Mailing invio elabora: {$skipped} contatti saltati perché senza email", [
|
||||||
|
'liste_ids' => $data['liste'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
if ($emails->isEmpty()) {
|
if ($emails->isEmpty()) {
|
||||||
return back()->with('error', 'Nessun destinatario email valido trovato nelle liste selezionate.');
|
return back()->with('error', 'Nessun destinatario email valido trovato nelle liste selezionate.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,23 +3,64 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\MailingList;
|
use App\Models\MailingList;
|
||||||
|
use App\Models\Tag;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class MailingListController extends Controller
|
class MailingListController extends Controller
|
||||||
{
|
{
|
||||||
public function index()
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$this->authorizeRead('mailing');
|
$this->authorizeRead('mailing');
|
||||||
$mailingLists = MailingList::with(['user', 'contatti'])
|
$query = MailingList::with(['user', 'contatti', 'tags']);
|
||||||
|
|
||||||
|
if ($request->filled('tag')) {
|
||||||
|
$tagSlugs = (array) $request->input('tag');
|
||||||
|
$query->withAnyTags($tagSlugs);
|
||||||
|
}
|
||||||
|
|
||||||
|
$mailingLists = $query->orderBy('nome')->get();
|
||||||
|
$allTags = Tag::orderBy('name')->get();
|
||||||
|
|
||||||
|
$vista = \App\Models\VistaReport::where('user_id', auth()->id())
|
||||||
|
->where('tipo', 'mailing-liste')
|
||||||
|
->where('is_default', true)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$userVistas = \App\Models\VistaReport::where('user_id', auth()->id())
|
||||||
|
->where('tipo', 'mailing-liste')
|
||||||
|
->orderBy('is_default', 'desc')
|
||||||
->orderBy('nome')
|
->orderBy('nome')
|
||||||
->get();
|
->get();
|
||||||
return view('mailing-liste.index', compact('mailingLists'));
|
|
||||||
|
$allColumnDefs = [
|
||||||
|
['key' => 'nome', 'label' => 'Nome'],
|
||||||
|
['key' => 'descrizione', 'label' => 'Descrizione'],
|
||||||
|
['key' => 'contatti_count', 'label' => 'Contatti'],
|
||||||
|
['key' => 'tag', 'label' => 'Tag'],
|
||||||
|
['key' => 'attiva', 'label' => 'Stato'],
|
||||||
|
['key' => 'created_at', 'label' => 'Creata il'],
|
||||||
|
];
|
||||||
|
$defaultVisible = ['nome', 'descrizione', 'contatti_count', 'tag', 'attiva', 'created_at'];
|
||||||
|
$visibleColumns = $vista && !empty($vista->colonne_visibili)
|
||||||
|
? $vista->colonne_visibili
|
||||||
|
: $defaultVisible;
|
||||||
|
|
||||||
|
$tableColumns = array_map(fn($col) => [
|
||||||
|
'key' => $col['key'],
|
||||||
|
'label' => $col['label'],
|
||||||
|
'visible' => in_array($col['key'], $visibleColumns),
|
||||||
|
], $allColumnDefs);
|
||||||
|
$entityType = 'mailing-liste';
|
||||||
|
$columnWidths = $vista && $vista->colonne_larghezze ? $vista->colonne_larghezze : [];
|
||||||
|
|
||||||
|
return view('mailing-liste.index', compact('mailingLists', 'allTags', 'vista', 'visibleColumns', 'tableColumns', 'entityType', 'columnWidths', 'userVistas'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create()
|
public function create()
|
||||||
{
|
{
|
||||||
$this->authorizeWrite('mailing');
|
$this->authorizeWrite('mailing');
|
||||||
return view('mailing-liste.create');
|
$tags = Tag::orderBy('name')->get();
|
||||||
|
return view('mailing-liste.create', compact('tags'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
@@ -30,6 +71,8 @@ class MailingListController extends Controller
|
|||||||
'descrizione' => 'nullable|string',
|
'descrizione' => 'nullable|string',
|
||||||
'attiva' => 'boolean',
|
'attiva' => 'boolean',
|
||||||
'contatti_json' => 'nullable|string',
|
'contatti_json' => 'nullable|string',
|
||||||
|
'tags' => 'nullable|array',
|
||||||
|
'tags.*' => 'exists:tags,id',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$data['user_id'] = auth()->id();
|
$data['user_id'] = auth()->id();
|
||||||
@@ -42,11 +85,15 @@ class MailingListController extends Controller
|
|||||||
'user_id' => $data['user_id'],
|
'user_id' => $data['user_id'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if ($request->has('tags')) {
|
||||||
|
$lista->tags()->sync($request->tags);
|
||||||
|
}
|
||||||
|
|
||||||
if (!empty($data['contatti_json'])) {
|
if (!empty($data['contatti_json'])) {
|
||||||
$contatti = json_decode($data['contatti_json'], true);
|
$contatti = json_decode($data['contatti_json'], true);
|
||||||
if (is_array($contatti)) {
|
if (is_array($contatti)) {
|
||||||
foreach ($contatti as $contatto) {
|
foreach ($contatti as $contatto) {
|
||||||
if (!empty($contatto['individuo_id']) && !empty($contatto['email'])) {
|
if (!empty($contatto['individuo_id'])) {
|
||||||
$lista->contatti()->create([
|
$lista->contatti()->create([
|
||||||
'individuo_id' => $contatto['individuo_id'],
|
'individuo_id' => $contatto['individuo_id'],
|
||||||
'opt_in' => true,
|
'opt_in' => true,
|
||||||
@@ -63,15 +110,17 @@ class MailingListController extends Controller
|
|||||||
public function show($mailing_liste)
|
public function show($mailing_liste)
|
||||||
{
|
{
|
||||||
$this->authorizeRead('mailing');
|
$this->authorizeRead('mailing');
|
||||||
$mailingList = MailingList::with(['contatti.individuo.contatti'])->findOrFail($mailing_liste);
|
$mailingList = MailingList::with(['contatti.individuo.contatti', 'tags'])->findOrFail($mailing_liste);
|
||||||
return view('mailing-liste.show', compact('mailingList'));
|
return view('mailing-liste.show', compact('mailingList'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function edit($mailingList)
|
public function edit($mailingList)
|
||||||
{
|
{
|
||||||
$this->authorizeWrite('mailing');
|
$this->authorizeWrite('mailing');
|
||||||
$mailingList = MailingList::with('contatti.individuo.contatti')->findOrFail($mailingList);
|
$mailingList = MailingList::with(['contatti.individuo.contatti', 'tags'])->findOrFail($mailingList);
|
||||||
return view('mailing-liste.edit', compact('mailingList'));
|
$tags = Tag::orderBy('name')->get();
|
||||||
|
$selectedTags = $mailingList->tags->pluck('id')->toArray();
|
||||||
|
return view('mailing-liste.edit', compact('mailingList', 'tags', 'selectedTags'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(Request $request, $mailingList)
|
public function update(Request $request, $mailingList)
|
||||||
@@ -84,6 +133,8 @@ class MailingListController extends Controller
|
|||||||
'descrizione' => 'nullable|string',
|
'descrizione' => 'nullable|string',
|
||||||
'attiva' => 'boolean',
|
'attiva' => 'boolean',
|
||||||
'contatti_json' => 'nullable|string',
|
'contatti_json' => 'nullable|string',
|
||||||
|
'tags' => 'nullable|array',
|
||||||
|
'tags.*' => 'exists:tags,id',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$data['attiva'] = $data['attiva'] ?? false;
|
$data['attiva'] = $data['attiva'] ?? false;
|
||||||
@@ -94,6 +145,12 @@ class MailingListController extends Controller
|
|||||||
'attiva' => $data['attiva'],
|
'attiva' => $data['attiva'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if ($request->has('tags')) {
|
||||||
|
$mailingList->tags()->sync($request->tags);
|
||||||
|
} else {
|
||||||
|
$mailingList->tags()->detach();
|
||||||
|
}
|
||||||
|
|
||||||
if (!empty($data['contatti_json'])) {
|
if (!empty($data['contatti_json'])) {
|
||||||
$newContatti = json_decode($data['contatti_json'], true);
|
$newContatti = json_decode($data['contatti_json'], true);
|
||||||
if (is_array($newContatti)) {
|
if (is_array($newContatti)) {
|
||||||
@@ -107,7 +164,7 @@ class MailingListController extends Controller
|
|||||||
|
|
||||||
$toAdd = array_diff($newIds, $existingIds);
|
$toAdd = array_diff($newIds, $existingIds);
|
||||||
foreach ($newContatti as $contatto) {
|
foreach ($newContatti as $contatto) {
|
||||||
if (in_array($contatto['individuo_id'], $toAdd) && !empty($contatto['email'])) {
|
if (in_array($contatto['individuo_id'], $toAdd)) {
|
||||||
$mailingList->contatti()->create([
|
$mailingList->contatti()->create([
|
||||||
'individuo_id' => $contatto['individuo_id'],
|
'individuo_id' => $contatto['individuo_id'],
|
||||||
'opt_in' => true,
|
'opt_in' => true,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use App\Models\Evento;
|
|||||||
use App\Models\Documento;
|
use App\Models\Documento;
|
||||||
use App\Models\MailingList;
|
use App\Models\MailingList;
|
||||||
use App\Models\Contatto;
|
use App\Models\Contatto;
|
||||||
|
use App\Models\Tag;
|
||||||
use App\Models\TipologiaDocumento;
|
use App\Models\TipologiaDocumento;
|
||||||
use App\Models\ReportCustom;
|
use App\Models\ReportCustom;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -47,12 +48,15 @@ class ReportController extends Controller
|
|||||||
$columnOptions[$type] = ReportColumnRegistry::getColumns($type);
|
$columnOptions[$type] = ReportColumnRegistry::getColumns($type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$allTags = Tag::orderBy('name')->get();
|
||||||
|
|
||||||
return view('report.index', compact(
|
return view('report.index', compact(
|
||||||
'stats',
|
'stats',
|
||||||
'prebuiltReports',
|
'prebuiltReports',
|
||||||
'customReports',
|
'customReports',
|
||||||
'tipologieDocumento',
|
'tipologieDocumento',
|
||||||
'columnOptions'
|
'columnOptions',
|
||||||
|
'allTags'
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +140,14 @@ class ReportController extends Controller
|
|||||||
$config['search_columns'] = $request->input('search_columns');
|
$config['search_columns'] = $request->input('search_columns');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($request->has('tag_filter')) {
|
||||||
|
$config['tag_filter'] = $request->input('tag_filter');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('tag_filter_mode')) {
|
||||||
|
$config['tag_filter_mode'] = $request->input('tag_filter_mode');
|
||||||
|
}
|
||||||
|
|
||||||
ReportCustom::create([
|
ReportCustom::create([
|
||||||
'user_id' => auth()->id(),
|
'user_id' => auth()->id(),
|
||||||
'nome' => $data['nome'],
|
'nome' => $data['nome'],
|
||||||
@@ -252,6 +264,9 @@ class ReportController extends Controller
|
|||||||
'documenti_completo' => $this->reportDocumentiCompleto($tipologiaFilter),
|
'documenti_completo' => $this->reportDocumentiCompleto($tipologiaFilter),
|
||||||
'contatti_completo' => $this->reportContattiCompleto(),
|
'contatti_completo' => $this->reportContattiCompleto(),
|
||||||
'scadenze_documenti' => $this->reportScadenzeDocumenti($tipologiaFilter),
|
'scadenze_documenti' => $this->reportScadenzeDocumenti($tipologiaFilter),
|
||||||
|
'tag_statistiche' => $this->reportTagStatistiche(),
|
||||||
|
'individui_per_tag' => $this->reportIndividuiPerTag(),
|
||||||
|
'documenti_per_tag' => $this->reportDocumentiPerTag(),
|
||||||
default => null,
|
default => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -403,6 +418,30 @@ class ReportController extends Controller
|
|||||||
'colore' => 'teal',
|
'colore' => 'teal',
|
||||||
'has_tipologia_filter' => false,
|
'has_tipologia_filter' => false,
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'id' => 'tag_statistiche',
|
||||||
|
'nome' => 'Statistiche Tag',
|
||||||
|
'descrizione' => 'Distribuzione degli elementi per tag (individui, gruppi, eventi, documenti, mailing list)',
|
||||||
|
'icona' => 'fa-tags',
|
||||||
|
'colore' => 'indigo',
|
||||||
|
'has_tipologia_filter' => false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'individui_per_tag',
|
||||||
|
'nome' => 'Individui per Tag',
|
||||||
|
'descrizione' => 'Elenco completo degli individui raggruppati per tag assegnato',
|
||||||
|
'icona' => 'fa-user-tag',
|
||||||
|
'colore' => 'info',
|
||||||
|
'has_tipologia_filter' => false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'documenti_per_tag',
|
||||||
|
'nome' => 'Documenti per Tag',
|
||||||
|
'descrizione' => 'Elenco completo dei documenti raggruppati per tag assegnato',
|
||||||
|
'icona' => 'fa-tags',
|
||||||
|
'colore' => 'secondary',
|
||||||
|
'has_tipologia_filter' => false,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -602,7 +641,7 @@ class ReportController extends Controller
|
|||||||
'lista' => $lista->nome,
|
'lista' => $lista->nome,
|
||||||
'stato' => $lista->attiva ? 'Attiva' : 'Disattiva',
|
'stato' => $lista->attiva ? 'Attiva' : 'Disattiva',
|
||||||
'individuo' => $contatto->individuo?->nome_completo ?? 'N/D',
|
'individuo' => $contatto->individuo?->nome_completo ?? 'N/D',
|
||||||
'email' => $contatto->email ?? '-',
|
'email' => $contatto->valore ?? '-',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -927,6 +966,102 @@ class ReportController extends Controller
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function reportTagStatistiche(): array
|
||||||
|
{
|
||||||
|
$tags = Tag::orderBy('name')->get();
|
||||||
|
|
||||||
|
$rows = $tags->map(function ($tag) {
|
||||||
|
return [
|
||||||
|
'name' => $tag->name,
|
||||||
|
'color' => $tag->color ?? '#6c757d',
|
||||||
|
'individui' => $tag->individui()->count(),
|
||||||
|
'gruppi' => $tag->gruppi()->count(),
|
||||||
|
'eventi' => $tag->eventi()->count(),
|
||||||
|
'documenti' => $tag->documenti()->count(),
|
||||||
|
'mailing' => $tag->mailingLists()->count(),
|
||||||
|
'totale' => $tag->count,
|
||||||
|
];
|
||||||
|
})->toArray();
|
||||||
|
|
||||||
|
$totalIndividui = array_sum(array_column($rows, 'individui'));
|
||||||
|
$totalGruppi = array_sum(array_column($rows, 'gruppi'));
|
||||||
|
$totalEventi = array_sum(array_column($rows, 'eventi'));
|
||||||
|
$totalDocumenti = array_sum(array_column($rows, 'documenti'));
|
||||||
|
$totalMailing = array_sum(array_column($rows, 'mailing'));
|
||||||
|
|
||||||
|
return [
|
||||||
|
'title' => 'Statistiche Tag',
|
||||||
|
'headers' => ['Tag', 'Individui', 'Gruppi', 'Eventi', 'Documenti', 'Mailing List', 'Totale Elementi'],
|
||||||
|
'rows' => array_map(fn($r) => [$r['name'], $r['individui'], $r['gruppi'], $r['eventi'], $r['documenti'], $r['mailing'], $r['totale']], $rows),
|
||||||
|
'summary' => 'Tag: ' . count($tags) . ' | Individui: ' . $totalIndividui . ' | Gruppi: ' . $totalGruppi . ' | Eventi: ' . $totalEventi . ' | Documenti: ' . $totalDocumenti . ' | Mailing List: ' . $totalMailing,
|
||||||
|
'detail_rows' => $rows,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function reportIndividuiPerTag(): array
|
||||||
|
{
|
||||||
|
$individui = Individuo::with('tags')
|
||||||
|
->whereHas('tags')
|
||||||
|
->orderBy('cognome')
|
||||||
|
->orderBy('nome')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$rows = $individui->flatMap(function ($ind) {
|
||||||
|
if ($ind->tags->isEmpty()) {
|
||||||
|
return collect();
|
||||||
|
}
|
||||||
|
return $ind->tags->map(function ($tag) use ($ind) {
|
||||||
|
return [
|
||||||
|
'tag' => $tag->name,
|
||||||
|
'tag_color' => $tag->color ?? '#6c757d',
|
||||||
|
'codice' => $ind->codice_id,
|
||||||
|
'cognome' => $ind->cognome,
|
||||||
|
'nome' => $ind->nome,
|
||||||
|
'email' => $ind->email_primaria ?? '-',
|
||||||
|
];
|
||||||
|
});
|
||||||
|
})->toArray();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'title' => 'Individui per Tag',
|
||||||
|
'headers' => ['Tag', 'Codice', 'Cognome', 'Nome', 'Email'],
|
||||||
|
'rows' => array_map(fn($r) => [$r['tag'], $r['codice'], $r['cognome'], $r['nome'], $r['email']], $rows),
|
||||||
|
'summary' => 'Totale righe (individuo × tag): ' . count($rows),
|
||||||
|
'detail_rows' => $rows,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function reportDocumentiPerTag(): array
|
||||||
|
{
|
||||||
|
$documenti = Documento::with('tags')
|
||||||
|
->whereHas('tags')
|
||||||
|
->orderBy('nome_file')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$rows = $documenti->flatMap(function ($doc) {
|
||||||
|
if ($doc->tags->isEmpty()) {
|
||||||
|
return collect();
|
||||||
|
}
|
||||||
|
return $doc->tags->map(function ($tag) use ($doc) {
|
||||||
|
return [
|
||||||
|
'tag' => $tag->name,
|
||||||
|
'tag_color' => $tag->color ?? '#6c757d',
|
||||||
|
'nome_file' => $doc->nome_file,
|
||||||
|
'tipologia' => ucfirst((string) $doc->tipologia),
|
||||||
|
'data' => $doc->created_at->format('d/m/Y'),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
})->toArray();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'title' => 'Documenti per Tag',
|
||||||
|
'headers' => ['Tag', 'Nome File', 'Tipologia', 'Data Caricamento'],
|
||||||
|
'rows' => array_map(fn($r) => [$r['tag'], $r['nome_file'], $r['tipologia'], $r['data']], $rows),
|
||||||
|
'summary' => 'Totale righe (documento × tag): ' . count($rows),
|
||||||
|
'detail_rows' => $rows,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
protected function runCustomReport(ReportCustom $report): array
|
protected function runCustomReport(ReportCustom $report): array
|
||||||
{
|
{
|
||||||
$tipo = $report->tipo_report;
|
$tipo = $report->tipo_report;
|
||||||
@@ -954,11 +1089,32 @@ class ReportController extends Controller
|
|||||||
$columns = $config['columns'] ?? [];
|
$columns = $config['columns'] ?? [];
|
||||||
$hasRelations = false;
|
$hasRelations = false;
|
||||||
|
|
||||||
|
// Always load tags if tags column or tag filter is present
|
||||||
|
$needsTags = in_array('tags.nome', $columns) || !empty($config['tag_filter']);
|
||||||
|
if ($needsTags) {
|
||||||
|
$query->with('tags');
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($columns as $col) {
|
foreach ($columns as $col) {
|
||||||
if (str_contains($col, '.')) {
|
if (str_contains($col, '.')) {
|
||||||
$hasRelations = true;
|
$hasRelations = true;
|
||||||
$relation = explode('.', $col)[0];
|
$relation = explode('.', $col)[0];
|
||||||
$query->with($relation);
|
if ($relation !== 'tags') {
|
||||||
|
$query->with($relation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply tag filter
|
||||||
|
if (!empty($config['tag_filter'])) {
|
||||||
|
$tagSlugs = Tag::whereIn('id', (array) $config['tag_filter'])->pluck('slug')->toArray();
|
||||||
|
if (!empty($tagSlugs)) {
|
||||||
|
$mode = $config['tag_filter_mode'] ?? 'any';
|
||||||
|
if ($mode === 'all') {
|
||||||
|
$query->withAllTags($tagSlugs);
|
||||||
|
} else {
|
||||||
|
$query->withAnyTags($tagSlugs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Tag;
|
||||||
|
use App\Models\Individuo;
|
||||||
|
use App\Models\Gruppo;
|
||||||
|
use App\Models\Evento;
|
||||||
|
use App\Models\Documento;
|
||||||
|
use App\Models\MailingList;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class RicercaController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$this->authorizeRead('individui');
|
||||||
|
|
||||||
|
$allTags = Tag::orderBy('name')->get();
|
||||||
|
|
||||||
|
$tagSlugs = (array) $request->input('tag', []);
|
||||||
|
|
||||||
|
if (empty($tagSlugs)) {
|
||||||
|
return view('ricerca.index', compact('allTags'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$tagSlugs = array_filter($tagSlugs);
|
||||||
|
$selectedTags = Tag::whereIn('slug', $tagSlugs)->get();
|
||||||
|
|
||||||
|
$results = [];
|
||||||
|
$totals = [];
|
||||||
|
|
||||||
|
if (count($tagSlugs) === 1) {
|
||||||
|
$tag = $selectedTags->first();
|
||||||
|
$results['individui'] = $tag->individui()->orderBy('cognome')->orderBy('nome')->get();
|
||||||
|
$results['gruppi'] = $tag->gruppi()->orderBy('nome')->get();
|
||||||
|
$results['eventi'] = $tag->eventi()->orderBy('data_specifica')->orderBy('nome_evento')->get();
|
||||||
|
$results['documenti'] = $tag->documenti()->orderBy('nome_file')->get();
|
||||||
|
$results['mailingLists'] = $tag->mailingLists()->orderBy('nome')->get();
|
||||||
|
} else {
|
||||||
|
$results['individui'] = Individuo::withAllTags($tagSlugs)->orderBy('cognome')->orderBy('nome')->get();
|
||||||
|
$results['gruppi'] = Gruppo::withAllTags($tagSlugs)->orderBy('nome')->get();
|
||||||
|
$results['eventi'] = Evento::withAllTags($tagSlugs)->orderBy('data_specifica')->orderBy('nome_evento')->get();
|
||||||
|
$results['documenti'] = Documento::withAllTags($tagSlugs)->orderBy('nome_file')->get();
|
||||||
|
$results['mailingLists'] = MailingList::withAllTags($tagSlugs)->orderBy('nome')->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
$totals = [
|
||||||
|
'individui' => $results['individui']->count(),
|
||||||
|
'gruppi' => $results['gruppi']->count(),
|
||||||
|
'eventi' => $results['eventi']->count(),
|
||||||
|
'documenti' => $results['documenti']->count(),
|
||||||
|
'mailingLists' => $results['mailingLists']->count(),
|
||||||
|
];
|
||||||
|
|
||||||
|
return view('ricerca.index', compact('selectedTags', 'tagSlugs', 'results', 'totals', 'allTags'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,9 +33,11 @@ class VistaReportController extends Controller
|
|||||||
$this->authorizeWrite('viste');
|
$this->authorizeWrite('viste');
|
||||||
$data = $request->validate([
|
$data = $request->validate([
|
||||||
'nome' => 'required|string|max:255',
|
'nome' => 'required|string|max:255',
|
||||||
'tipo' => 'required|in:individui,gruppi,documenti,eventi',
|
'tipo' => 'required|in:individui,gruppi,documenti,eventi,mailing-liste',
|
||||||
'colonne_visibili' => 'nullable|array',
|
'colonne_visibili' => 'nullable|array',
|
||||||
'colonne_ordinamento' => 'nullable|array',
|
'colonne_ordinamento' => 'nullable|array',
|
||||||
|
'colonne_larghezze' => 'nullable|array',
|
||||||
|
'colonne_ordine' => 'nullable|array',
|
||||||
'filtri' => 'nullable|array',
|
'filtri' => 'nullable|array',
|
||||||
'ricerca' => 'nullable|string',
|
'ricerca' => 'nullable|string',
|
||||||
'is_default' => 'nullable|boolean',
|
'is_default' => 'nullable|boolean',
|
||||||
@@ -55,6 +57,8 @@ class VistaReportController extends Controller
|
|||||||
'tipo' => $data['tipo'],
|
'tipo' => $data['tipo'],
|
||||||
'colonne_visibili' => $data['colonne_visibili'] ?? null,
|
'colonne_visibili' => $data['colonne_visibili'] ?? null,
|
||||||
'colonne_ordinamento' => $data['colonne_ordinamento'] ?? null,
|
'colonne_ordinamento' => $data['colonne_ordinamento'] ?? null,
|
||||||
|
'colonne_larghezze' => $data['colonne_larghezze'] ?? null,
|
||||||
|
'colonne_ordine' => $data['colonne_ordine'] ?? null,
|
||||||
'filtri' => $data['filtri'] ?? null,
|
'filtri' => $data['filtri'] ?? null,
|
||||||
'ricerca' => $data['ricerca'] ?? null,
|
'ricerca' => $data['ricerca'] ?? null,
|
||||||
'is_default' => $isDefault,
|
'is_default' => $isDefault,
|
||||||
@@ -78,6 +82,8 @@ class VistaReportController extends Controller
|
|||||||
'nome' => 'required|string|max:255',
|
'nome' => 'required|string|max:255',
|
||||||
'colonne_visibili' => 'nullable|array',
|
'colonne_visibili' => 'nullable|array',
|
||||||
'colonne_ordinamento' => 'nullable|array',
|
'colonne_ordinamento' => 'nullable|array',
|
||||||
|
'colonne_larghezze' => 'nullable|array',
|
||||||
|
'colonne_ordine' => 'nullable|array',
|
||||||
'filtri' => 'nullable|array',
|
'filtri' => 'nullable|array',
|
||||||
'ricerca' => 'nullable|string',
|
'ricerca' => 'nullable|string',
|
||||||
'is_default' => 'nullable|boolean',
|
'is_default' => 'nullable|boolean',
|
||||||
@@ -92,6 +98,10 @@ class VistaReportController extends Controller
|
|||||||
|
|
||||||
$vistaReport->update($data);
|
$vistaReport->update($data);
|
||||||
|
|
||||||
|
if ($request->expectsJson()) {
|
||||||
|
return response()->json(['success' => true, 'message' => 'Vista aggiornata.']);
|
||||||
|
}
|
||||||
|
|
||||||
return back()->with('success', 'Vista aggiornata.');
|
return back()->with('success', 'Vista aggiornata.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+29
-30
@@ -1,31 +1,25 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
class AppSetting extends Model
|
class AppSetting extends Model
|
||||||
{
|
{
|
||||||
protected $table = 'app_settings';
|
protected $table = 'app_settings';
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'logo',
|
'logo', 'logo_small', 'logo_path', 'logo_small_path',
|
||||||
'logo_small',
|
'nome_applicazione', 'nome_organizzazione',
|
||||||
'logo_path',
|
'footer_text', 'app_version', 'dashboard_welcome', 'show_version',
|
||||||
'logo_small_path',
|
'backup_path', 'backup_retention_days', 'backup_include_files',
|
||||||
'nome_applicazione',
|
'backup_include_env', 'backup_auto_enabled', 'backup_auto_frequency', 'backup_auto_hour',
|
||||||
'nome_organizzazione',
|
'documenti_storage_disk', 'documenti_storage_path',
|
||||||
'footer_text',
|
|
||||||
'app_version',
|
|
||||||
'dashboard_welcome',
|
|
||||||
'show_version',
|
|
||||||
'backup_path',
|
|
||||||
'backup_retention_days',
|
|
||||||
'backup_include_files',
|
|
||||||
'backup_include_env',
|
|
||||||
'backup_auto_enabled',
|
|
||||||
'backup_auto_frequency',
|
|
||||||
'backup_auto_hour',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
@@ -36,11 +30,16 @@ class AppSetting extends Model
|
|||||||
'backup_include_env' => 'boolean',
|
'backup_include_env' => 'boolean',
|
||||||
'backup_auto_enabled' => 'boolean',
|
'backup_auto_enabled' => 'boolean',
|
||||||
'backup_auto_hour' => 'integer',
|
'backup_auto_hour' => 'integer',
|
||||||
|
'show_version' => 'boolean',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getSetting(string $key, $default = null)
|
public static function getSetting(string $key, mixed $default = null): mixed
|
||||||
{
|
{
|
||||||
|
if (!Schema::hasTable('app_settings')) {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
$setting = self::first();
|
$setting = self::first();
|
||||||
return $setting?->{$key} ?? $default;
|
return $setting?->{$key} ?? $default;
|
||||||
}
|
}
|
||||||
@@ -110,38 +109,38 @@ class AppSetting extends Model
|
|||||||
|
|
||||||
public function getLogoUrlInstance(): ?string
|
public function getLogoUrlInstance(): ?string
|
||||||
{
|
{
|
||||||
if ($this->logo && file_exists(public_path('storage/' . $this->logo))) {
|
if ($this->logo && Storage::disk('public')->exists($this->logo)) {
|
||||||
return asset('storage/' . $this->logo);
|
return Storage::disk('public')->url($this->logo);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLogoSmallUrlInstance(): ?string
|
public function getLogoSmallUrlInstance(): ?string
|
||||||
{
|
{
|
||||||
if ($this->logo_small && file_exists(public_path('storage/' . $this->logo_small))) {
|
if ($this->logo_small && Storage::disk('public')->exists($this->logo_small)) {
|
||||||
return asset('storage/' . $this->logo_small);
|
return Storage::disk('public')->url($this->logo_small);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLogoPath(): ?string
|
public function getLogoPath(): ?string
|
||||||
{
|
{
|
||||||
if ($this->logo_path && file_exists(public_path('storage/' . $this->logo_path))) {
|
if ($this->logo_path && Storage::disk('public')->exists($this->logo_path)) {
|
||||||
return asset('storage/' . $this->logo_path);
|
return Storage::disk('public')->url($this->logo_path);
|
||||||
}
|
}
|
||||||
if ($this->logo && file_exists(public_path('storage/' . $this->logo))) {
|
if ($this->logo && Storage::disk('public')->exists($this->logo)) {
|
||||||
return asset('storage/' . $this->logo);
|
return Storage::disk('public')->url($this->logo);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLogoSmallPath(): ?string
|
public function getLogoSmallPath(): ?string
|
||||||
{
|
{
|
||||||
if ($this->logo_small_path && file_exists(public_path('storage/' . $this->logo_small_path))) {
|
if ($this->logo_small_path && Storage::disk('public')->exists($this->logo_small_path)) {
|
||||||
return asset('storage/' . $this->logo_small_path);
|
return Storage::disk('public')->url($this->logo_small_path);
|
||||||
}
|
}
|
||||||
if ($this->logo_small && file_exists(public_path('storage/' . $this->logo_small))) {
|
if ($this->logo_small && Storage::disk('public')->exists($this->logo_small)) {
|
||||||
return asset('storage/' . $this->logo_small);
|
return Storage::disk('public')->url($this->logo_small);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Facades\Crypt;
|
||||||
|
|
||||||
|
class CalendarioConnessione extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'calendario_connessioni';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'nome',
|
||||||
|
'tipo',
|
||||||
|
'config',
|
||||||
|
'stato',
|
||||||
|
'sync_direction',
|
||||||
|
'sync_interval_minutes',
|
||||||
|
'last_sync_at',
|
||||||
|
'last_error_at',
|
||||||
|
'last_error_message',
|
||||||
|
'is_active',
|
||||||
|
'ordine',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'config' => 'array',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
'sync_interval_minutes' => 'integer',
|
||||||
|
'last_sync_at' => 'datetime',
|
||||||
|
'last_error_at' => 'datetime',
|
||||||
|
'ordine' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function scopeAttivi($query)
|
||||||
|
{
|
||||||
|
return $query->where('is_active', true)->orderBy('ordine');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDecryptedConfig(): array
|
||||||
|
{
|
||||||
|
$config = $this->config;
|
||||||
|
foreach (['username', 'password', 'client_secret', 'refresh_token', 'api_key'] as $field) {
|
||||||
|
if (isset($config[$field])) {
|
||||||
|
try {
|
||||||
|
$config[$field] = Crypt::decryptString($config[$field]);
|
||||||
|
} catch (\Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function encryptAndSetConfig(array $config): void
|
||||||
|
{
|
||||||
|
foreach (['username', 'password', 'client_secret', 'refresh_token', 'api_key'] as $field) {
|
||||||
|
if (isset($config[$field]) && $config[$field] !== '' && !str_starts_with($config[$field], 'eyJpdiI')) {
|
||||||
|
$config[$field] = Crypt::encryptString($config[$field]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->config = $config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function opzioniTipo(): array
|
||||||
|
{
|
||||||
|
return ['google_calendar', 'caldav'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function etichettaTipo(string $tipo): string
|
||||||
|
{
|
||||||
|
return match ($tipo) {
|
||||||
|
'google_calendar' => 'Google Calendar',
|
||||||
|
'caldav' => 'CalDAV (Infomaniak/NextCloud)',
|
||||||
|
default => ucfirst($tipo),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Traits\HasTagsLight;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
@@ -9,6 +10,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
|||||||
|
|
||||||
class Documento extends Model
|
class Documento extends Model
|
||||||
{
|
{
|
||||||
|
use HasTagsLight;
|
||||||
protected $table = 'documenti';
|
protected $table = 'documenti';
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'tenant_id', 'user_id', 'cartella_id', 'repository_id', 'storage_disk',
|
'tenant_id', 'user_id', 'cartella_id', 'repository_id', 'storage_disk',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Traits\HasTagsLight;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
@@ -9,6 +10,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
|||||||
|
|
||||||
class Evento extends Model
|
class Evento extends Model
|
||||||
{
|
{
|
||||||
|
use HasTagsLight;
|
||||||
protected $table = 'eventi';
|
protected $table = 'eventi';
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
@@ -30,6 +32,7 @@ class Evento extends Model
|
|||||||
'is_incontro_gruppo',
|
'is_incontro_gruppo',
|
||||||
'luogo_indirizzo',
|
'luogo_indirizzo',
|
||||||
'luogo_url_maps',
|
'luogo_url_maps',
|
||||||
|
'uid_esterno',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Traits\HasTagsLight;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
@@ -10,6 +11,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
|||||||
|
|
||||||
class Gruppo extends Model
|
class Gruppo extends Model
|
||||||
{
|
{
|
||||||
|
use HasTagsLight;
|
||||||
protected $table = 'gruppi';
|
protected $table = 'gruppi';
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'tenant_id', 'parent_id', 'diocesi_id', 'responsabile_ids',
|
'tenant_id', 'parent_id', 'diocesi_id', 'responsabile_ids',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Traits\HasTagsLight;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
@@ -10,6 +11,7 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
|
|||||||
|
|
||||||
class Individuo extends Model
|
class Individuo extends Model
|
||||||
{
|
{
|
||||||
|
use HasTagsLight;
|
||||||
protected $table = 'individui';
|
protected $table = 'individui';
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'tenant_id', 'codice_id', 'cognome', 'nome', 'data_nascita',
|
'tenant_id', 'codice_id', 'cognome', 'nome', 'data_nascita',
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ namespace App\Models;
|
|||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use App\Traits\HasTagsLight;
|
||||||
|
|
||||||
class MailingList extends Model
|
class MailingList extends Model
|
||||||
{
|
{
|
||||||
|
use HasTagsLight;
|
||||||
|
|
||||||
protected $table = 'mailing_lists';
|
protected $table = 'mailing_lists';
|
||||||
protected $fillable = ['tenant_id', 'user_id', 'nome', 'descrizione', 'attiva'];
|
protected $fillable = ['tenant_id', 'user_id', 'nome', 'descrizione', 'attiva'];
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||||
|
|
||||||
|
class Tag extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'tags';
|
||||||
|
|
||||||
|
protected $fillable = ['name', 'slug', 'color', 'order_column'];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'order_column' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected static function boot(): void
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
|
||||||
|
static::creating(function (self $tag) {
|
||||||
|
if (empty($tag->slug)) {
|
||||||
|
$tag->slug = str()->slug($tag->name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function individui(): MorphToMany
|
||||||
|
{
|
||||||
|
return $this->morphedByMany(Individuo::class, 'taggable');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function gruppi(): MorphToMany
|
||||||
|
{
|
||||||
|
return $this->morphedByMany(Gruppo::class, 'taggable');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function eventi(): MorphToMany
|
||||||
|
{
|
||||||
|
return $this->morphedByMany(Evento::class, 'taggable');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function documenti(): MorphToMany
|
||||||
|
{
|
||||||
|
return $this->morphedByMany(Documento::class, 'taggable');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mailingLists(): MorphToMany
|
||||||
|
{
|
||||||
|
return $this->morphedByMany(MailingList::class, 'taggable');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCountAttribute(): int
|
||||||
|
{
|
||||||
|
return $this->individui()->count()
|
||||||
|
+ $this->gruppi()->count()
|
||||||
|
+ $this->eventi()->count()
|
||||||
|
+ $this->documenti()->count()
|
||||||
|
+ $this->mailingLists()->count();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,13 +10,16 @@ class VistaReport extends Model
|
|||||||
protected $table = 'viste_report';
|
protected $table = 'viste_report';
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'user_id', 'nome', 'tipo', 'colonne_visibili',
|
'user_id', 'nome', 'tipo', 'colonne_visibili',
|
||||||
'colonne_ordinamento', 'filtri', 'ricerca'
|
'colonne_ordinamento', 'filtri', 'ricerca', 'is_default',
|
||||||
|
'colonne_larghezze', 'colonne_ordine',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'colonne_visibili' => 'array',
|
'colonne_visibili' => 'array',
|
||||||
'colonne_ordinamento' => 'array',
|
'colonne_ordinamento' => 'array',
|
||||||
'filtri' => 'array',
|
'filtri' => 'array',
|
||||||
|
'colonne_larghezze' => 'array',
|
||||||
|
'colonne_ordine' => 'array',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function user(): BelongsTo
|
public function user(): BelongsTo
|
||||||
|
|||||||
@@ -0,0 +1,389 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\CalendarioConnessione;
|
||||||
|
use App\Models\Evento;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class CalDavService
|
||||||
|
{
|
||||||
|
private const CALDAV_NS = 'urn:ietf:params:xml:ns:caldav';
|
||||||
|
private const DAV_NS = 'DAV:';
|
||||||
|
|
||||||
|
public function testConnection(CalendarioConnessione $connessione): array
|
||||||
|
{
|
||||||
|
$config = $connessione->getDecryptedConfig();
|
||||||
|
$client = $this->buildClient($config);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = $client->request('PROPFIND', $config['url'], [
|
||||||
|
'headers' => ['Depth' => '0'],
|
||||||
|
'body' => '<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<d:propfind xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/">
|
||||||
|
<d:prop>
|
||||||
|
<d:displayname/>
|
||||||
|
<cs:getctag/>
|
||||||
|
</d:prop>
|
||||||
|
</d:propfind>',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($response->getStatusCode() < 300) {
|
||||||
|
return ['success' => true, 'message' => 'Connessione CalDAV riuscita.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['success' => false, 'message' => 'Errore HTTP: ' . $response->getStatusCode()];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ['success' => false, 'message' => 'Errore: ' . $e->getMessage()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function listCalendars(CalendarioConnessione $connessione): array
|
||||||
|
{
|
||||||
|
$config = $connessione->getDecryptedConfig();
|
||||||
|
$client = $this->buildClient($config);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = $client->request('PROPFIND', $config['url'], [
|
||||||
|
'headers' => ['Depth' => '1'],
|
||||||
|
'body' => '<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
||||||
|
<d:prop>
|
||||||
|
<d:resourcetype/>
|
||||||
|
<d:displayname/>
|
||||||
|
<c:supported-calendar-component-set/>
|
||||||
|
</d:prop>
|
||||||
|
</d:propfind>',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$body = (string) $response->getBody();
|
||||||
|
return $this->parseCalendarList($body);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('CalDAV listCalendars: ' . $e->getMessage());
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function syncEvents(CalendarioConnessione $connessione): array
|
||||||
|
{
|
||||||
|
$config = $connessione->getDecryptedConfig();
|
||||||
|
$client = $this->buildClient($config);
|
||||||
|
$calendarUrl = $config['calendar_url'] ?? $config['url'];
|
||||||
|
$imported = 0;
|
||||||
|
$exported = 0;
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
$start = Carbon::now()->subYear()->format('Ymd\THis\Z');
|
||||||
|
$end = Carbon::now()->addYear()->format('Ymd\THis\Z');
|
||||||
|
|
||||||
|
$direction = $connessione->sync_direction ?? 'bidirectional';
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($direction === 'import' || $direction === 'bidirectional') {
|
||||||
|
$result = $this->fetchRemoteEvents($client, $calendarUrl, $start, $end);
|
||||||
|
$imported = $this->importEvents($result, $errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($direction === 'export' || $direction === 'bidirectional') {
|
||||||
|
$localEvents = Evento::where(function ($q) use ($connessione) {
|
||||||
|
$q->whereNotNull('uid_esterno')
|
||||||
|
->orWhereNull('uid_esterno');
|
||||||
|
})->get();
|
||||||
|
$exported = $this->exportEvents($client, $calendarUrl, $localEvents, $connessione, $errors);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$errors[] = $e->getMessage();
|
||||||
|
Log::error('CalDAV syncEvents: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => empty($errors),
|
||||||
|
'imported' => $imported,
|
||||||
|
'exported' => $exported,
|
||||||
|
'errors' => $errors,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fetchRemoteEvents($client, string $url, string $start, string $end): array
|
||||||
|
{
|
||||||
|
$body = '<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
||||||
|
<d:prop>
|
||||||
|
<d:getetag/>
|
||||||
|
<c:calendar-data/>
|
||||||
|
</d:prop>
|
||||||
|
<c:filter>
|
||||||
|
<c:comp-filter name="VCALENDAR">
|
||||||
|
<c:comp-filter name="VEVENT">
|
||||||
|
<c:time-range start="' . $start . '" end="' . $end . '"/>
|
||||||
|
</c:comp-filter>
|
||||||
|
</c:comp-filter>
|
||||||
|
</c:filter>
|
||||||
|
</c:calendar-query>';
|
||||||
|
|
||||||
|
$response = $client->request('REPORT', $url, [
|
||||||
|
'headers' => ['Depth' => '1', 'Content-Type' => 'application/xml; charset=utf-8'],
|
||||||
|
'body' => $body,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->parseCalendarData((string) $response->getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function importEvents(array $remoteEvents, array &$errors): int
|
||||||
|
{
|
||||||
|
$count = 0;
|
||||||
|
foreach ($remoteEvents as $event) {
|
||||||
|
try {
|
||||||
|
$vcalendar = \Sabre\VObject\Reader::read($event['data']);
|
||||||
|
foreach ($vcalendar->VEVENT as $vevent) {
|
||||||
|
$uid = (string) ($vevent->UID ?? Str::uuid());
|
||||||
|
$existing = Evento::where('uid_esterno', $uid)->first();
|
||||||
|
if ($existing) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$summary = (string) ($vevent->SUMMARY ?? 'Evento importato');
|
||||||
|
$description = (string) ($vevent->DESCRIPTION ?? '');
|
||||||
|
$location = (string) ($vevent->LOCATION ?? '');
|
||||||
|
|
||||||
|
$dtstart = $vevent->DTSTART ? $this->parseDateTime($vevent->DTSTART) : null;
|
||||||
|
$dtend = $vevent->DTEND ? $this->parseDateTime($vevent->DTEND) : null;
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'nome_evento' => $summary,
|
||||||
|
'descrizione_evento' => mb_substr($description, 0, 255),
|
||||||
|
'tipo_recorrenza' => 'singolo',
|
||||||
|
'luogo_indirizzo' => $location,
|
||||||
|
'uid_esterno' => $uid,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($dtstart) {
|
||||||
|
$data['data_specifica'] = $dtstart->format('Y-m-d');
|
||||||
|
$data['ora_inizio'] = $dtstart->format('H:i');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dtend && $dtstart) {
|
||||||
|
$data['durata_minuti'] = (int) $dtstart->diffInMinutes($dtend);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rrule = $vevent->RRULE ? (string) $vevent->RRULE : null;
|
||||||
|
if ($rrule) {
|
||||||
|
$parsed = $this->parseRRule($rrule);
|
||||||
|
$data = array_merge($data, $parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
Evento::create($data);
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$errors[] = 'Errore parsing evento: ' . $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function exportEvents($client, string $url, iterable $localEvents, CalendarioConnessione $connessione, array &$errors): int
|
||||||
|
{
|
||||||
|
$count = 0;
|
||||||
|
$existingUids = $this->getExistingEventUids($client, $url);
|
||||||
|
|
||||||
|
foreach ($localEvents as $evento) {
|
||||||
|
try {
|
||||||
|
if ($evento->uid_esterno && !in_array($evento->uid_esterno, $existingUids)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$uid = $evento->uid_esterno ?? ('local-' . $evento->id . '-' . Str::uuid());
|
||||||
|
$icsData = $this->buildIcsForExport($evento, $uid);
|
||||||
|
|
||||||
|
$eventUrl = rtrim($url, '/') . '/' . $uid . '.ics';
|
||||||
|
|
||||||
|
$response = $client->request('PUT', $eventUrl, [
|
||||||
|
'headers' => ['Content-Type' => 'text/calendar; charset=utf-8'],
|
||||||
|
'body' => $icsData,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($response->getStatusCode() < 300) {
|
||||||
|
if (!$evento->uid_esterno) {
|
||||||
|
$evento->updateQuietly(['uid_esterno' => $uid]);
|
||||||
|
}
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$errors[] = 'Errore export ' . $evento->nome_evento . ': ' . $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getExistingEventUids($client, string $url): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$response = $client->request('REPORT', $url, [
|
||||||
|
'headers' => ['Depth' => '1'],
|
||||||
|
'body' => '<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
||||||
|
<d:prop>
|
||||||
|
<d:getetag/>
|
||||||
|
<d:resourcetype/>
|
||||||
|
</d:prop>
|
||||||
|
<c:filter>
|
||||||
|
<c:comp-filter name="VCALENDAR">
|
||||||
|
<c:comp-filter name="VEVENT"/>
|
||||||
|
</c:comp-filter>
|
||||||
|
</c:filter>
|
||||||
|
</c:calendar-query>',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$body = (string) $response->getBody();
|
||||||
|
$xml = simplexml_load_string($body);
|
||||||
|
$xml->registerXPathNamespace('d', self::DAV_NS);
|
||||||
|
$uids = [];
|
||||||
|
foreach ($xml->xpath('//d:href') as $href) {
|
||||||
|
$path = (string) $href;
|
||||||
|
$name = basename($path);
|
||||||
|
if (str_ends_with($name, '.ics')) {
|
||||||
|
$uids[] = str_replace('.ics', '', $name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $uids;
|
||||||
|
} catch (\Exception) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildIcsForExport(Evento $evento, string $uid): string
|
||||||
|
{
|
||||||
|
$service = app(IcsExportService::class);
|
||||||
|
return $service->generateSingle($evento);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseCalendarList(string $xml): array
|
||||||
|
{
|
||||||
|
$calendars = [];
|
||||||
|
try {
|
||||||
|
$dom = new \DOMDocument();
|
||||||
|
$dom->loadXML($xml);
|
||||||
|
$xpath = new \DOMXPath($dom);
|
||||||
|
$xpath->registerNamespace('d', self::DAV_NS);
|
||||||
|
$xpath->registerNamespace('c', self::CALDAV_NS);
|
||||||
|
|
||||||
|
$responses = $xpath->query('//d:multistatus/d:response');
|
||||||
|
foreach ($responses as $response) {
|
||||||
|
$href = $xpath->query('d:href', $response)->item(0)?->textContent ?? '';
|
||||||
|
$displayName = $xpath->query('d:propstat/d:prop/d:displayname', $response)->item(0)?->textContent ?? $href;
|
||||||
|
|
||||||
|
$hasCalendar = false;
|
||||||
|
$resourcetypes = $xpath->query('d:propstat/d:prop/d:resourcetype/*', $response);
|
||||||
|
foreach ($resourcetypes as $type) {
|
||||||
|
if ($type->localName === 'calendar' && $type->namespaceURI === self::CALDAV_NS) {
|
||||||
|
$hasCalendar = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasCalendar && $href) {
|
||||||
|
$calendars[] = [
|
||||||
|
'href' => $href,
|
||||||
|
'displayname' => $displayName,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('CalDAV parseCalendarList: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
return $calendars;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseCalendarData(string $xml): array
|
||||||
|
{
|
||||||
|
$events = [];
|
||||||
|
try {
|
||||||
|
$dom = new \DOMDocument();
|
||||||
|
$dom->loadXML($xml);
|
||||||
|
$xpath = new \DOMXPath($dom);
|
||||||
|
$xpath->registerNamespace('d', self::DAV_NS);
|
||||||
|
$xpath->registerNamespace('c', self::CALDAV_NS);
|
||||||
|
|
||||||
|
$responses = $xpath->query('//d:multistatus/d:response');
|
||||||
|
foreach ($responses as $response) {
|
||||||
|
$dataNodes = $xpath->query('d:propstat/d:prop/c:calendar-data', $response);
|
||||||
|
foreach ($dataNodes as $node) {
|
||||||
|
$events[] = ['data' => $node->textContent];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('CalDAV parseCalendarData: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
return $events;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseDateTime($dt): ?Carbon
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if ($dt instanceof \Sabre\VObject\Property\ICalendar\DateTime) {
|
||||||
|
$dtValue = $dt->getDateTime();
|
||||||
|
return Carbon::instance($dtValue);
|
||||||
|
}
|
||||||
|
} catch (\Exception) {
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseRRule(string $rrule): array
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
try {
|
||||||
|
$parts = explode(';', $rrule);
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
[$key, $value] = explode('=', $part, 2);
|
||||||
|
if ($key === 'FREQ') {
|
||||||
|
$result['tipo_recorrenza'] = match (strtolower($value)) {
|
||||||
|
'weekly' => 'settimanale',
|
||||||
|
'monthly' => 'mensile',
|
||||||
|
'yearly' => 'annuale',
|
||||||
|
default => 'singolo',
|
||||||
|
};
|
||||||
|
} elseif ($key === 'BYDAY' && isset($result['tipo_recorrenza'])) {
|
||||||
|
$dayMap = ['SU' => 0, 'MO' => 1, 'TU' => 2, 'WE' => 3, 'TH' => 4, 'FR' => 5, 'SA' => 6];
|
||||||
|
$byDay = strtoupper($value);
|
||||||
|
if (preg_match('/^(-?\d+)?([A-Z]+)$/', $byDay, $m)) {
|
||||||
|
$result['giorno_settimana'] = $dayMap[$m[2]] ?? null;
|
||||||
|
if (!empty($m[1]) && $result['tipo_recorrenza'] === 'mensile') {
|
||||||
|
$result['occorrenza_mese'] = $m[1] . ',' . ($dayMap[$m[2]] ?? '1');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ($key === 'BYMONTHDAY') {
|
||||||
|
$result['giorno_mese'] = (int) $value;
|
||||||
|
} elseif ($key === 'BYMONTH') {
|
||||||
|
if ($result['tipo_recorrenza'] ?? '' === 'annuale') {
|
||||||
|
$result['mese_annuale'] = (int) $value;
|
||||||
|
} else {
|
||||||
|
$result['mesi_recorrenza'] = explode(',', $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception) {
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildClient(array $config): \GuzzleHttp\Client
|
||||||
|
{
|
||||||
|
$options = [
|
||||||
|
'verify' => false,
|
||||||
|
'timeout' => 30,
|
||||||
|
'http_errors' => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!empty($config['username']) && !empty($config['password'])) {
|
||||||
|
$options['auth'] = [$config['username'], $config['password'], 'basic'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new \GuzzleHttp\Client($options);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,350 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\CalendarioConnessione;
|
||||||
|
use App\Models\Evento;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Google\Client as GoogleClient;
|
||||||
|
use Google\Service\Calendar as GoogleCalendar;
|
||||||
|
use Google\Service\Calendar\Event as GoogleEvent;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class GoogleCalendarSyncService
|
||||||
|
{
|
||||||
|
private ?GoogleCalendar $calendarService = null;
|
||||||
|
private ?string $calendarId = null;
|
||||||
|
|
||||||
|
public function testConnection(CalendarioConnessione $connessione): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$client = $this->buildClient($connessione);
|
||||||
|
$service = new GoogleCalendar($client);
|
||||||
|
$calendarList = $service->calendarList->listCalendarList();
|
||||||
|
return ['success' => true, 'message' => 'Connessione Google Calendar riuscita. Trovati ' . count($calendarList->getItems()) . ' calendari.'];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ['success' => false, 'message' => 'Errore: ' . $e->getMessage()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function listCalendars(CalendarioConnessione $connessione): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$client = $this->buildClient($connessione);
|
||||||
|
$service = new GoogleCalendar($client);
|
||||||
|
$calendarList = $service->calendarList->listCalendarList();
|
||||||
|
$calendars = [];
|
||||||
|
foreach ($calendarList->getItems() as $cal) {
|
||||||
|
$calendars[] = [
|
||||||
|
'id' => $cal->getId(),
|
||||||
|
'summary' => $cal->getSummary(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $calendars;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('GoogleCalendar listCalendars: ' . $e->getMessage());
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function syncEvents(CalendarioConnessione $connessione): array
|
||||||
|
{
|
||||||
|
$config = $connessione->getDecryptedConfig();
|
||||||
|
$imported = 0;
|
||||||
|
$exported = 0;
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$client = $this->buildClient($connessione);
|
||||||
|
$this->calendarService = new GoogleCalendar($client);
|
||||||
|
$this->calendarId = $config['calendar_id'] ?? 'primary';
|
||||||
|
|
||||||
|
$direction = $connessione->sync_direction ?? 'bidirectional';
|
||||||
|
|
||||||
|
if ($direction === 'import' || $direction === 'bidirectional') {
|
||||||
|
$result = $this->importRemoteEvents($connessione);
|
||||||
|
$imported = $result['count'];
|
||||||
|
foreach ($result['errors'] as $err) {
|
||||||
|
$errors[] = $err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($direction === 'export' || $direction === 'bidirectional') {
|
||||||
|
$result = $this->exportLocalEvents($connessione);
|
||||||
|
$exported = $result['count'];
|
||||||
|
foreach ($result['errors'] as $err) {
|
||||||
|
$errors[] = $err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$errors[] = $e->getMessage();
|
||||||
|
Log::error('GoogleCalendar syncEvents: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => empty($errors),
|
||||||
|
'imported' => $imported,
|
||||||
|
'exported' => $exported,
|
||||||
|
'errors' => $errors,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function importRemoteEvents(CalendarioConnessione $connessione): array
|
||||||
|
{
|
||||||
|
$count = 0;
|
||||||
|
$errors = [];
|
||||||
|
$service = $this->calendarService;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$timeMin = Carbon::now()->subYear()->toRfc3339String();
|
||||||
|
$timeMax = Carbon::now()->addYear()->toRfc3339String();
|
||||||
|
|
||||||
|
$optParams = [
|
||||||
|
'timeMin' => $timeMin,
|
||||||
|
'timeMax' => $timeMax,
|
||||||
|
'singleEvents' => true,
|
||||||
|
'orderBy' => 'startTime',
|
||||||
|
];
|
||||||
|
|
||||||
|
$events = $service->events->listEvents($this->calendarId, $optParams);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
foreach ($events->getItems() as $event) {
|
||||||
|
try {
|
||||||
|
$uid = $event->getId();
|
||||||
|
$existing = Evento::where('uid_esterno', $uid)->first();
|
||||||
|
if ($existing) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$summary = $event->getSummary() ?? 'Evento importato';
|
||||||
|
$description = strip_tags($event->getDescription() ?? '');
|
||||||
|
$location = $event->getLocation() ?? '';
|
||||||
|
|
||||||
|
$start = $event->getStart();
|
||||||
|
$end = $event->getEnd();
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'nome_evento' => mb_substr($summary, 0, 255),
|
||||||
|
'descrizione_evento' => mb_substr($description, 0, 255),
|
||||||
|
'tipo_recorrenza' => 'singolo',
|
||||||
|
'luogo_indirizzo' => $location,
|
||||||
|
'uid_esterno' => $uid,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($start && $start->getDateTime()) {
|
||||||
|
$dt = Carbon::parse($start->getDateTime());
|
||||||
|
$data['data_specifica'] = $dt->format('Y-m-d');
|
||||||
|
$data['ora_inizio'] = $dt->format('H:i');
|
||||||
|
} elseif ($start && $start->getDate()) {
|
||||||
|
$data['data_specifica'] = $start->getDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($end && $start && $end->getDateTime() && $start->getDateTime()) {
|
||||||
|
$data['durata_minuti'] = (int) Carbon::parse($start->getDateTime())->diffInMinutes(Carbon::parse($end->getDateTime()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($event->getRecurrence()) {
|
||||||
|
foreach ($event->getRecurrence() as $rrule) {
|
||||||
|
if (str_starts_with($rrule, 'RRULE:')) {
|
||||||
|
$parsed = $this->parseRRule(substr($rrule, 6));
|
||||||
|
$data = array_merge($data, $parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Evento::create($data);
|
||||||
|
$count++;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$errors[] = 'Errore import evento: ' . $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageToken = $events->getNextPageToken();
|
||||||
|
if ($pageToken) {
|
||||||
|
$optParams['pageToken'] = $pageToken;
|
||||||
|
$events = $service->events->listEvents($this->calendarId, $optParams);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$errors[] = 'Errore import Google Calendar: ' . $e->getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['count' => $count, 'errors' => $errors];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function exportLocalEvents(CalendarioConnessione $connessione): array
|
||||||
|
{
|
||||||
|
$count = 0;
|
||||||
|
$errors = [];
|
||||||
|
$service = $this->calendarService;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$localEvents = Evento::all();
|
||||||
|
$remoteEventMap = $this->getRemoteEventMap($service);
|
||||||
|
|
||||||
|
foreach ($localEvents as $evento) {
|
||||||
|
try {
|
||||||
|
$googleEvent = new GoogleEvent();
|
||||||
|
$googleEvent->setSummary($evento->nome_evento);
|
||||||
|
|
||||||
|
if ($evento->descrizione_evento) {
|
||||||
|
$googleEvent->setDescription($evento->descrizione_evento);
|
||||||
|
}
|
||||||
|
if ($evento->luogo_indirizzo) {
|
||||||
|
$googleEvent->setLocation($evento->luogo_indirizzo);
|
||||||
|
}
|
||||||
|
|
||||||
|
$startData = $this->buildGoogleDateTime($evento, 'start');
|
||||||
|
$endData = $this->buildGoogleDateTime($evento, 'end');
|
||||||
|
$googleEvent->setStart($startData);
|
||||||
|
$googleEvent->setEnd($endData);
|
||||||
|
|
||||||
|
if ($evento->tipo_recorrenza !== 'singolo') {
|
||||||
|
$rrule = $this->buildGoogleRRule($evento);
|
||||||
|
if ($rrule) {
|
||||||
|
$googleEvent->setRecurrence(['RRULE:' . $rrule]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($evento->uid_esterno && isset($remoteEventMap[$evento->uid_esterno])) {
|
||||||
|
$service->events->update($this->calendarId, $evento->uid_esterno, $googleEvent);
|
||||||
|
} else {
|
||||||
|
$created = $service->events->insert($this->calendarId, $googleEvent);
|
||||||
|
if (!$evento->uid_esterno) {
|
||||||
|
$evento->updateQuietly(['uid_esterno' => $created->getId()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$count++;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$errors[] = 'Errore export ' . $evento->nome_evento . ': ' . $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$errors[] = 'Errore export Google Calendar: ' . $e->getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['count' => $count, 'errors' => $errors];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getRemoteEventMap(GoogleCalendar $service): array
|
||||||
|
{
|
||||||
|
$map = [];
|
||||||
|
try {
|
||||||
|
$events = $service->events->listEvents($this->calendarId);
|
||||||
|
foreach ($events->getItems() as $event) {
|
||||||
|
$map[$event->getId()] = true;
|
||||||
|
}
|
||||||
|
} catch (\Exception) {
|
||||||
|
}
|
||||||
|
return $map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildGoogleDateTime(Evento $evento, string $type): \Google\Service\Calendar\EventDateTime
|
||||||
|
{
|
||||||
|
$dt = new \Google\Service\Calendar\EventDateTime();
|
||||||
|
|
||||||
|
if ($type === 'start') {
|
||||||
|
if ($evento->data_specifica) {
|
||||||
|
$date = $evento->data_specifica instanceof Carbon ? $evento->data_specifica : Carbon::parse($evento->data_specifica);
|
||||||
|
if ($evento->ora_inizio) {
|
||||||
|
$time = $evento->ora_inizio instanceof Carbon ? $evento->ora_inizio : Carbon::parse($evento->ora_inizio);
|
||||||
|
$dt->setDateTime($date->format('Y-m-d') . 'T' . $time->format('H:i:s'));
|
||||||
|
$dt->setTimeZone('Europe/Rome');
|
||||||
|
} else {
|
||||||
|
$dt->setDate($date->format('Y-m-d'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($evento->data_specifica) {
|
||||||
|
$date = $evento->data_specifica instanceof Carbon ? $evento->data_specifica : Carbon::parse($evento->data_specifica);
|
||||||
|
$durata = (int) ($evento->durata_minuti ?? 60);
|
||||||
|
if ($evento->ora_inizio) {
|
||||||
|
$time = $evento->ora_inizio instanceof Carbon ? $evento->ora_inizio : Carbon::parse($evento->ora_inizio);
|
||||||
|
$endTime = $time->copy()->addMinutes($durata);
|
||||||
|
$dt->setDateTime($date->format('Y-m-d') . 'T' . $endTime->format('H:i:s'));
|
||||||
|
$dt->setTimeZone('Europe/Rome');
|
||||||
|
} else {
|
||||||
|
$dt->setDate($date->copy()->addDay()->format('Y-m-d'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $dt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildGoogleRRule(Evento $evento): ?string
|
||||||
|
{
|
||||||
|
$dayMap = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'];
|
||||||
|
|
||||||
|
return match ($evento->tipo_recorrenza) {
|
||||||
|
'settimanale' => $evento->giorno_settimana !== null
|
||||||
|
? 'FREQ=WEEKLY;BYDAY=' . ($dayMap[(int) $evento->giorno_settimana] ?? 'MO')
|
||||||
|
: null,
|
||||||
|
'mensile' => $evento->occorrenza_mese
|
||||||
|
? 'FREQ=MONTHLY;BYSETPOS=' . $evento->occorrenza_mese
|
||||||
|
: null,
|
||||||
|
'annuale' => $evento->mese_annuale
|
||||||
|
? 'FREQ=YEARLY;BYMONTH=' . $evento->mese_annuale
|
||||||
|
: null,
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseRRule(string $rrule): array
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
$parts = explode(';', $rrule);
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
[$key, $value] = explode('=', $part, 2);
|
||||||
|
if ($key === 'FREQ') {
|
||||||
|
$result['tipo_recorrenza'] = match (strtolower($value)) {
|
||||||
|
'weekly' => 'settimanale',
|
||||||
|
'monthly' => 'mensile',
|
||||||
|
'yearly' => 'annuale',
|
||||||
|
default => 'singolo',
|
||||||
|
};
|
||||||
|
} elseif ($key === 'BYDAY') {
|
||||||
|
$dayMap = ['SU' => 0, 'MO' => 1, 'TU' => 2, 'WE' => 3, 'TH' => 4, 'FR' => 5, 'SA' => 6];
|
||||||
|
if (preg_match('/^(-?\d+)?([A-Z]+)$/', $value, $m)) {
|
||||||
|
$result['giorno_settimana'] = $dayMap[$m[2]] ?? null;
|
||||||
|
if (!empty($m[1])) {
|
||||||
|
$result['occorrenza_mese'] = $m[1] . ',' . ($dayMap[$m[2]] ?? '1');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ($key === 'BYMONTHDAY') {
|
||||||
|
$result['giorno_mese'] = (int) $value;
|
||||||
|
} elseif ($key === 'BYMONTH') {
|
||||||
|
$result['mese_annuale'] = (int) $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildClient(CalendarioConnessione $connessione): GoogleClient
|
||||||
|
{
|
||||||
|
$config = $connessione->getDecryptedConfig();
|
||||||
|
|
||||||
|
$client = new GoogleClient();
|
||||||
|
$client->setApplicationName(config('app.name'));
|
||||||
|
$client->setScopes([GoogleCalendar::CALENDAR]);
|
||||||
|
$client->setAuthConfig([
|
||||||
|
'web' => [
|
||||||
|
'client_id' => $config['client_id'] ?? '',
|
||||||
|
'client_secret' => $config['client_secret'] ?? '',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$client->setAccessType('offline');
|
||||||
|
$client->setPrompt('consent');
|
||||||
|
|
||||||
|
if (!empty($config['refresh_token'])) {
|
||||||
|
$client->fetchAccessTokenWithRefreshToken($config['refresh_token']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $client;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Traits;
|
||||||
|
|
||||||
|
use App\Models\Tag;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||||
|
|
||||||
|
trait HasTagsLight
|
||||||
|
{
|
||||||
|
public function tags(): MorphToMany
|
||||||
|
{
|
||||||
|
return $this->morphToMany(Tag::class, 'taggable');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeWithAllTags($query, array $tags)
|
||||||
|
{
|
||||||
|
foreach ($tags as $tag) {
|
||||||
|
$query->whereHas('tags', fn ($q) => $q->where('slug', $tag));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeWithAnyTags($query, array $tags)
|
||||||
|
{
|
||||||
|
$query->whereHas('tags', fn ($q) => $q->whereIn('slug', $tags));
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+96
@@ -0,0 +1,96 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
NAME="${APP_NAME:-glastree}"
|
||||||
|
VERSION="$(date +%Y%m%d_%H%M)"
|
||||||
|
ARCHIVE="${NAME}-${VERSION}.tar.gz"
|
||||||
|
|
||||||
|
echo "==> Installazione dipendenze PHP production..."
|
||||||
|
composer install --no-dev --optimize-autoloader --no-scripts --quiet
|
||||||
|
echo " OK"
|
||||||
|
|
||||||
|
echo "==> Build asset Vite..."
|
||||||
|
if [ -f "node_modules/.package-lock.json" ] || [ -d "node_modules" ]; then
|
||||||
|
npm run build --quiet 2>/dev/null && echo " OK" || echo " (skip — node_modules non presente)"
|
||||||
|
else
|
||||||
|
echo " (skip — node_modules assente, build Vite non eseguita)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
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 "==> Creazione archivio: ${ARCHIVE}"
|
||||||
|
|
||||||
|
tar czf "${ARCHIVE}" \
|
||||||
|
--exclude='.git' \
|
||||||
|
--exclude='.gitignore' \
|
||||||
|
--exclude='.env' \
|
||||||
|
--exclude='.env.example' \
|
||||||
|
--exclude='node_modules' \
|
||||||
|
--exclude='tests' \
|
||||||
|
--exclude='vendor/bin' \
|
||||||
|
--exclude='vendor/*/tests' \
|
||||||
|
--exclude='vendor/*/test' \
|
||||||
|
--exclude='vendor/*/.git' \
|
||||||
|
--exclude='vendor/*/docs' \
|
||||||
|
--exclude='vendor/*/doc' \
|
||||||
|
--exclude='storage/app/backups' \
|
||||||
|
--exclude='storage/app/documenti' \
|
||||||
|
--exclude='storage/app/public' \
|
||||||
|
--exclude='storage/app/private' \
|
||||||
|
--exclude='public/storage' \
|
||||||
|
--exclude='storage/framework/cache/*' \
|
||||||
|
--exclude='storage/framework/sessions/*' \
|
||||||
|
--exclude='storage/framework/views/*' \
|
||||||
|
--exclude='storage/logs/*' \
|
||||||
|
--exclude='storage/debugbar/*' \
|
||||||
|
--exclude='bootstrap/cache/*.php' \
|
||||||
|
--exclude='public/hot' \
|
||||||
|
--exclude='*.tar.gz' \
|
||||||
|
--exclude='*.tar' \
|
||||||
|
--exclude='MEMORY.md' \
|
||||||
|
--exclude='AGENTS.md' \
|
||||||
|
--exclude='build-dist.sh' \
|
||||||
|
--exclude='Dockerfile' \
|
||||||
|
--exclude='docker-compose.yml' \
|
||||||
|
--exclude='docker-compose.mysql.yml' \
|
||||||
|
--exclude='docker-entrypoint.sh' \
|
||||||
|
--exclude='.dockerignore' \
|
||||||
|
--warning=no-file-changed \
|
||||||
|
.
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "============================================"
|
||||||
|
echo " Archivio creato: ${ARCHIVE}"
|
||||||
|
echo " Dimensione: $(du -h "${ARCHIVE}" | cut -f1)"
|
||||||
|
echo "============================================"
|
||||||
|
echo ""
|
||||||
|
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 ""
|
||||||
|
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 ""
|
||||||
|
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 ""
|
||||||
|
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"
|
||||||
@@ -9,9 +9,11 @@
|
|||||||
"php": "^8.3",
|
"php": "^8.3",
|
||||||
"as247/flysystem-google-drive": "^3.0",
|
"as247/flysystem-google-drive": "^3.0",
|
||||||
"directorytree/imapengine-laravel": "^1.2",
|
"directorytree/imapengine-laravel": "^1.2",
|
||||||
|
"google/apiclient": "^2.19",
|
||||||
"laravel/framework": "^13.7",
|
"laravel/framework": "^13.7",
|
||||||
"laravel/tinker": "^3.0",
|
"laravel/tinker": "^3.0",
|
||||||
"league/flysystem-webdav": "^3.31",
|
"league/flysystem-webdav": "^3.31",
|
||||||
|
"sabre/vobject": "^4.6",
|
||||||
"spatie/laravel-permission": "^7.4",
|
"spatie/laravel-permission": "^7.4",
|
||||||
"webklex/php-imap": "^6.2"
|
"webklex/php-imap": "^6.2"
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+6
-6
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "c6ab297804fe432e1792b166459e6469",
|
"content-hash": "c594ad5c5f9ff96dc8644231135f694e",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "as247/cloud-storages",
|
"name": "as247/cloud-storages",
|
||||||
@@ -4271,16 +4271,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "sabre/vobject",
|
"name": "sabre/vobject",
|
||||||
"version": "4.5.8",
|
"version": "4.6.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/sabre-io/vobject.git",
|
"url": "https://github.com/sabre-io/vobject.git",
|
||||||
"reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1"
|
"reference": "9432544fc369851fb8202c5d91159b2e669f0c88"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/sabre-io/vobject/zipball/d554eb24d64232922e1eab5896cc2f84b3b9ffb1",
|
"url": "https://api.github.com/repos/sabre-io/vobject/zipball/9432544fc369851fb8202c5d91159b2e669f0c88",
|
||||||
"reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1",
|
"reference": "9432544fc369851fb8202c5d91159b2e669f0c88",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -4371,7 +4371,7 @@
|
|||||||
"issues": "https://github.com/sabre-io/vobject/issues",
|
"issues": "https://github.com/sabre-io/vobject/issues",
|
||||||
"source": "https://github.com/fruux/sabre-vobject"
|
"source": "https://github.com/fruux/sabre-vobject"
|
||||||
},
|
},
|
||||||
"time": "2026-01-12T10:45:19+00:00"
|
"time": "2026-05-31T13:04:55+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "sabre/xml",
|
"name": "sabre/xml",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,10 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
|
if (Schema::hasTable('tenants')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Schema::create('tenants', function (Blueprint $table) {
|
Schema::create('tenants', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->string('nome');
|
$table->string('nome');
|
||||||
|
|||||||
@@ -8,26 +8,30 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('diocesi', function (Blueprint $table) {
|
if (!Schema::hasTable('diocesi')) {
|
||||||
$table->id();
|
Schema::create('diocesi', function (Blueprint $table) {
|
||||||
$table->string('nome');
|
$table->id();
|
||||||
$table->string('regione')->nullable();
|
$table->string('nome');
|
||||||
$table->timestamps();
|
$table->string('regione')->nullable();
|
||||||
});
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Schema::create('comuni', function (Blueprint $table) {
|
if (!Schema::hasTable('comuni')) {
|
||||||
$table->id();
|
Schema::create('comuni', function (Blueprint $table) {
|
||||||
$table->string('nome');
|
$table->id();
|
||||||
$table->string('codice_istat', 10)->nullable();
|
$table->string('nome');
|
||||||
$table->string('cap')->nullable();
|
$table->string('codice_istat', 10)->nullable();
|
||||||
$table->string('sigla_provincia', 2)->nullable();
|
$table->string('cap')->nullable();
|
||||||
$table->string('regione')->nullable();
|
$table->string('sigla_provincia', 2)->nullable();
|
||||||
$table->float('latitudine')->nullable();
|
$table->string('regione')->nullable();
|
||||||
$table->float('longitudine')->nullable();
|
$table->float('latitudine')->nullable();
|
||||||
$table->timestamps();
|
$table->float('longitudine')->nullable();
|
||||||
$table->index('nome');
|
$table->timestamps();
|
||||||
$table->index('sigla_provincia');
|
$table->index('nome');
|
||||||
});
|
$table->index('sigla_provincia');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,27 +8,29 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('individui', function (Blueprint $table) {
|
if (!Schema::hasTable('individui')) {
|
||||||
$table->id();
|
Schema::create('individui', function (Blueprint $table) {
|
||||||
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
|
$table->id();
|
||||||
$table->string('codice_id', 5)->unique();
|
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
|
||||||
$table->string('cognome');
|
$table->string('codice_id', 5)->unique();
|
||||||
$table->string('nome');
|
$table->string('cognome');
|
||||||
$table->date('data_nascita')->nullable();
|
$table->string('nome');
|
||||||
$table->string('indirizzo')->nullable();
|
$table->date('data_nascita')->nullable();
|
||||||
$table->string('cap', 10)->nullable();
|
$table->string('indirizzo')->nullable();
|
||||||
$table->string('città')->nullable();
|
$table->string('cap', 10)->nullable();
|
||||||
$table->string('sigla_provincia', 2)->nullable();
|
$table->string('città')->nullable();
|
||||||
$table->enum('genere', ['M', 'F'])->nullable();
|
$table->string('sigla_provincia', 2)->nullable();
|
||||||
$table->enum('tipo_documento', ['carta_identita', 'patente'])->nullable();
|
$table->enum('genere', ['M', 'F'])->nullable();
|
||||||
$table->date('scadenza_documento')->nullable();
|
$table->enum('tipo_documento', ['carta_identita', 'patente'])->nullable();
|
||||||
$table->text('note')->nullable();
|
$table->date('scadenza_documento')->nullable();
|
||||||
$table->string('avatar_path')->nullable();
|
$table->text('note')->nullable();
|
||||||
$table->timestamps();
|
$table->string('avatar_path')->nullable();
|
||||||
$table->index('tenant_id');
|
$table->timestamps();
|
||||||
$table->index('codice_id');
|
$table->index('tenant_id');
|
||||||
$table->index('cognome');
|
$table->index('codice_id');
|
||||||
});
|
$table->index('cognome');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,16 +8,18 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('contatti', function (Blueprint $table) {
|
if (!Schema::hasTable('contatti')) {
|
||||||
$table->id();
|
Schema::create('contatti', function (Blueprint $table) {
|
||||||
$table->foreignId('individuo_id')->constrained('individui')->onDelete('cascade');
|
$table->id();
|
||||||
$table->enum('tipo', ['telefono', 'cellulare', 'email', 'fax', 'web', 'telegram', 'whatsapp', 'altro']);
|
$table->foreignId('individuo_id')->constrained('individui')->onDelete('cascade');
|
||||||
$table->string('valore');
|
$table->enum('tipo', ['telefono', 'cellulare', 'email', 'fax', 'web', 'telegram', 'whatsapp', 'altro']);
|
||||||
$table->string('etichetta')->nullable();
|
$table->string('valore');
|
||||||
$table->boolean('is_primary')->default(false);
|
$table->string('etichetta')->nullable();
|
||||||
$table->timestamps();
|
$table->boolean('is_primary')->default(false);
|
||||||
$table->index('individuo_id');
|
$table->timestamps();
|
||||||
});
|
$table->index('individuo_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,24 +8,26 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('gruppi', function (Blueprint $table) {
|
if (!Schema::hasTable('gruppi')) {
|
||||||
$table->id();
|
Schema::create('gruppi', function (Blueprint $table) {
|
||||||
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
|
$table->id();
|
||||||
$table->foreignId('parent_id')->nullable()->constrained('gruppi')->onDelete('set null');
|
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
|
||||||
$table->foreignId('diocesi_id')->nullable()->constrained('diocesi')->onDelete('set null');
|
$table->foreignId('parent_id')->nullable()->constrained('gruppi')->onDelete('set null');
|
||||||
$table->foreignId('responsabile_id')->nullable()->constrained('individui')->onDelete('set null');
|
$table->foreignId('diocesi_id')->nullable()->constrained('diocesi')->onDelete('set null');
|
||||||
$table->string('nome');
|
$table->foreignId('responsabile_id')->nullable()->constrained('individui')->onDelete('set null');
|
||||||
$table->text('descrizione')->nullable();
|
$table->string('nome');
|
||||||
$table->string('indirizzo_incontro')->nullable();
|
$table->text('descrizione')->nullable();
|
||||||
$table->string('cap_incontro', 10)->nullable();
|
$table->string('indirizzo_incontro')->nullable();
|
||||||
$table->string('città_incontro')->nullable();
|
$table->string('cap_incontro', 10)->nullable();
|
||||||
$table->string('sigla_provincia_incontro', 2)->nullable();
|
$table->string('città_incontro')->nullable();
|
||||||
$table->string('mappa_posizione')->nullable();
|
$table->string('sigla_provincia_incontro', 2)->nullable();
|
||||||
$table->json('metadata')->nullable();
|
$table->string('mappa_posizione')->nullable();
|
||||||
$table->timestamps();
|
$table->json('metadata')->nullable();
|
||||||
$table->index('tenant_id');
|
$table->timestamps();
|
||||||
$table->index('parent_id');
|
$table->index('tenant_id');
|
||||||
});
|
$table->index('parent_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,15 +8,17 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('gruppo_individuo', function (Blueprint $table) {
|
if (!Schema::hasTable('gruppo_individuo')) {
|
||||||
$table->id();
|
Schema::create('gruppo_individuo', function (Blueprint $table) {
|
||||||
$table->foreignId('gruppo_id')->constrained('gruppi')->onDelete('cascade');
|
$table->id();
|
||||||
$table->foreignId('individuo_id')->constrained('individui')->onDelete('cascade');
|
$table->foreignId('gruppo_id')->constrained('gruppi')->onDelete('cascade');
|
||||||
$table->string('ruolo_nel_gruppo')->nullable();
|
$table->foreignId('individuo_id')->constrained('individui')->onDelete('cascade');
|
||||||
$table->date('data_adesione')->nullable();
|
$table->string('ruolo_nel_gruppo')->nullable();
|
||||||
$table->timestamps();
|
$table->date('data_adesione')->nullable();
|
||||||
$table->unique(['gruppo_id', 'individuo_id']);
|
$table->timestamps();
|
||||||
});
|
$table->unique(['gruppo_id', 'individuo_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,35 +8,41 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('eventi', function (Blueprint $table) {
|
if (!Schema::hasTable('eventi')) {
|
||||||
$table->id();
|
Schema::create('eventi', function (Blueprint $table) {
|
||||||
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
|
$table->id();
|
||||||
$table->string('nome_evento');
|
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
|
||||||
$table->string('tipo_evento')->nullable();
|
$table->string('nome_evento');
|
||||||
$table->enum('tipo_recorrenza', ['singolo', 'ricorrente'])->default('singolo');
|
$table->string('tipo_evento')->nullable();
|
||||||
$table->unsignedTinyInteger('giorno_settimana')->nullable();
|
$table->enum('tipo_recorrenza', ['singolo', 'ricorrente'])->default('singolo');
|
||||||
$table->time('ora_inizio')->nullable();
|
$table->unsignedTinyInteger('giorno_settimana')->nullable();
|
||||||
$table->date('data_specifica')->nullable();
|
$table->time('ora_inizio')->nullable();
|
||||||
$table->unsignedInteger('durata_minuti')->nullable();
|
$table->date('data_specifica')->nullable();
|
||||||
$table->text('descrizione')->nullable();
|
$table->unsignedInteger('durata_minuti')->nullable();
|
||||||
$table->text('note')->nullable();
|
$table->text('descrizione')->nullable();
|
||||||
$table->timestamps();
|
$table->text('note')->nullable();
|
||||||
$table->index('tenant_id');
|
$table->timestamps();
|
||||||
});
|
$table->index('tenant_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Schema::create('eventi_gruppi', function (Blueprint $table) {
|
if (!Schema::hasTable('eventi_gruppi')) {
|
||||||
$table->id();
|
Schema::create('eventi_gruppi', function (Blueprint $table) {
|
||||||
$table->foreignId('evento_id')->constrained('eventi')->onDelete('cascade');
|
$table->id();
|
||||||
$table->foreignId('gruppo_id')->constrained('gruppi')->onDelete('cascade');
|
$table->foreignId('evento_id')->constrained('eventi')->onDelete('cascade');
|
||||||
$table->timestamps();
|
$table->foreignId('gruppo_id')->constrained('gruppi')->onDelete('cascade');
|
||||||
});
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Schema::create('eventi_responsabili', function (Blueprint $table) {
|
if (!Schema::hasTable('eventi_responsabili')) {
|
||||||
$table->id();
|
Schema::create('eventi_responsabili', function (Blueprint $table) {
|
||||||
$table->foreignId('evento_id')->constrained('eventi')->onDelete('cascade');
|
$table->id();
|
||||||
$table->foreignId('individuo_id')->constrained('individui')->onDelete('cascade');
|
$table->foreignId('evento_id')->constrained('eventi')->onDelete('cascade');
|
||||||
$table->timestamps();
|
$table->foreignId('individuo_id')->constrained('individui')->onDelete('cascade');
|
||||||
});
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,22 +8,24 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('documenti', function (Blueprint $table) {
|
if (!Schema::hasTable('documenti')) {
|
||||||
$table->id();
|
Schema::create('documenti', function (Blueprint $table) {
|
||||||
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
|
$table->id();
|
||||||
$table->string('nome_file');
|
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
|
||||||
$table->string('file_path');
|
$table->string('nome_file');
|
||||||
$table->string('tipo')->nullable();
|
$table->string('file_path');
|
||||||
$table->enum('tipologia', ['avatar', 'galleria', 'documento', 'statuto', 'altro'])->default('documento');
|
$table->string('tipo')->nullable();
|
||||||
$table->enum('visibilita', ['pubblico', 'gruppo', 'individuo', 'associazione', 'federazione'])->default('gruppo');
|
$table->enum('tipologia', ['avatar', 'galleria', 'documento', 'statuto', 'altro'])->default('documento');
|
||||||
$table->unsignedBigInteger('visibilita_target_id')->nullable();
|
$table->enum('visibilita', ['pubblico', 'gruppo', 'individuo', 'associazione', 'federazione'])->default('gruppo');
|
||||||
$table->string('visibilita_target_type')->nullable();
|
$table->unsignedBigInteger('visibilita_target_id')->nullable();
|
||||||
$table->string('mime_type')->nullable();
|
$table->string('visibilita_target_type')->nullable();
|
||||||
$table->unsignedBigInteger('dimensione')->nullable();
|
$table->string('mime_type')->nullable();
|
||||||
$table->text('note')->nullable();
|
$table->unsignedBigInteger('dimensione')->nullable();
|
||||||
$table->timestamps();
|
$table->text('note')->nullable();
|
||||||
$table->index('tenant_id');
|
$table->timestamps();
|
||||||
});
|
$table->index('tenant_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,43 +8,49 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('mailing_lists', function (Blueprint $table) {
|
if (!Schema::hasTable('mailing_lists')) {
|
||||||
$table->id();
|
Schema::create('mailing_lists', function (Blueprint $table) {
|
||||||
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
|
$table->id();
|
||||||
$table->string('nome');
|
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
|
||||||
$table->text('descrizione')->nullable();
|
$table->string('nome');
|
||||||
$table->boolean('attiva')->default(true);
|
$table->text('descrizione')->nullable();
|
||||||
$table->timestamps();
|
$table->boolean('attiva')->default(true);
|
||||||
$table->index('tenant_id');
|
$table->timestamps();
|
||||||
});
|
$table->index('tenant_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Schema::create('mailing_contacts', function (Blueprint $table) {
|
if (!Schema::hasTable('mailing_contacts')) {
|
||||||
$table->id();
|
Schema::create('mailing_contacts', function (Blueprint $table) {
|
||||||
$table->foreignId('mailing_list_id')->constrained('mailing_lists')->onDelete('cascade');
|
$table->id();
|
||||||
$table->foreignId('individuo_id')->constrained('individui')->onDelete('cascade');
|
$table->foreignId('mailing_list_id')->constrained('mailing_lists')->onDelete('cascade');
|
||||||
$table->boolean('opt_in')->default(true);
|
$table->foreignId('individuo_id')->constrained('individui')->onDelete('cascade');
|
||||||
$table->dateTime('opt_in_data')->nullable();
|
$table->boolean('opt_in')->default(true);
|
||||||
$table->dateTime('opt_out_data')->nullable();
|
$table->dateTime('opt_in_data')->nullable();
|
||||||
$table->timestamps();
|
$table->dateTime('opt_out_data')->nullable();
|
||||||
$table->unique(['mailing_list_id', 'individuo_id']);
|
$table->timestamps();
|
||||||
});
|
$table->unique(['mailing_list_id', 'individuo_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Schema::create('mailing_messaggi', function (Blueprint $table) {
|
if (!Schema::hasTable('mailing_messaggi')) {
|
||||||
$table->id();
|
Schema::create('mailing_messaggi', function (Blueprint $table) {
|
||||||
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
|
$table->id();
|
||||||
$table->foreignId('mailing_list_id')->nullable()->constrained('mailing_lists')->onDelete('set null');
|
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
|
||||||
$table->unsignedBigInteger('user_id')->nullable();
|
$table->foreignId('mailing_list_id')->nullable()->constrained('mailing_lists')->onDelete('set null');
|
||||||
$table->string('oggetto');
|
$table->unsignedBigInteger('user_id')->nullable();
|
||||||
$table->text('corpo')->nullable();
|
$table->string('oggetto');
|
||||||
$table->enum('canale', ['email', 'telegram'])->default('email');
|
$table->text('corpo')->nullable();
|
||||||
$table->enum('stato', ['bozza', 'in_coda', 'inviato', 'fallito'])->default('bozza');
|
$table->enum('canale', ['email', 'telegram'])->default('email');
|
||||||
$table->timestamp('inviato_al')->nullable();
|
$table->enum('stato', ['bozza', 'in_coda', 'inviato', 'fallito'])->default('bozza');
|
||||||
$table->integer('totale_destinatari')->default(0);
|
$table->timestamp('inviato_al')->nullable();
|
||||||
$table->integer('invii_ok')->default(0);
|
$table->integer('totale_destinatari')->default(0);
|
||||||
$table->integer('invii_falliti')->default(0);
|
$table->integer('invii_ok')->default(0);
|
||||||
$table->timestamps();
|
$table->integer('invii_falliti')->default(0);
|
||||||
$table->index('tenant_id');
|
$table->timestamps();
|
||||||
});
|
$table->index('tenant_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,18 +8,20 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('notifiche', function (Blueprint $table) {
|
if (!Schema::hasTable('notifiche')) {
|
||||||
$table->id();
|
Schema::create('notifiche', function (Blueprint $table) {
|
||||||
$table->unsignedBigInteger('user_id')->nullable();
|
$table->id();
|
||||||
$table->string('tipo');
|
$table->unsignedBigInteger('user_id')->nullable();
|
||||||
$table->string('titolo');
|
$table->string('tipo');
|
||||||
$table->text('messaggio');
|
$table->string('titolo');
|
||||||
$table->string('link')->nullable();
|
$table->text('messaggio');
|
||||||
$table->boolean('is_read')->default(false);
|
$table->string('link')->nullable();
|
||||||
$table->timestamp('read_at')->nullable();
|
$table->boolean('is_read')->default(false);
|
||||||
$table->timestamps();
|
$table->timestamp('read_at')->nullable();
|
||||||
$table->index('user_id');
|
$table->timestamps();
|
||||||
});
|
$table->index('user_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,34 +8,40 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('users', function (Blueprint $table) {
|
if (!Schema::hasTable('users')) {
|
||||||
$table->id();
|
Schema::create('users', function (Blueprint $table) {
|
||||||
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
|
$table->id();
|
||||||
$table->string('name');
|
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
|
||||||
$table->string('email')->unique();
|
$table->string('name');
|
||||||
$table->timestamp('email_verified_at')->nullable();
|
$table->string('email')->unique();
|
||||||
$table->string('password');
|
$table->timestamp('email_verified_at')->nullable();
|
||||||
$table->boolean('is_admin')->default(false);
|
$table->string('password');
|
||||||
$table->rememberToken();
|
$table->boolean('is_admin')->default(false);
|
||||||
$table->timestamps();
|
$table->rememberToken();
|
||||||
$table->index('tenant_id');
|
$table->timestamps();
|
||||||
$table->index('email');
|
$table->index('tenant_id');
|
||||||
});
|
$table->index('email');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
if (!Schema::hasTable('password_reset_tokens')) {
|
||||||
$table->string('email')->primary();
|
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||||
$table->string('token');
|
$table->string('email')->primary();
|
||||||
$table->timestamp('created_at')->nullable();
|
$table->string('token');
|
||||||
});
|
$table->timestamp('created_at')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Schema::create('sessions', function (Blueprint $table) {
|
if (!Schema::hasTable('sessions')) {
|
||||||
$table->string('id')->primary();
|
Schema::create('sessions', function (Blueprint $table) {
|
||||||
$table->foreignId('user_id')->nullable()->index();
|
$table->string('id')->primary();
|
||||||
$table->string('ip_address', 45)->nullable();
|
$table->foreignId('user_id')->nullable()->index();
|
||||||
$table->text('user_agent')->nullable();
|
$table->string('ip_address', 45)->nullable();
|
||||||
$table->longText('payload');
|
$table->text('user_agent')->nullable();
|
||||||
$table->integer('last_activity')->index();
|
$table->longText('payload');
|
||||||
});
|
$table->integer('last_activity')->index();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,50 +8,60 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('cache', function (Blueprint $table) {
|
if (!Schema::hasTable('cache')) {
|
||||||
$table->string('key')->primary();
|
Schema::create('cache', function (Blueprint $table) {
|
||||||
$table->mediumText('value');
|
$table->string('key')->primary();
|
||||||
$table->integer('expiration');
|
$table->mediumText('value');
|
||||||
});
|
$table->integer('expiration');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Schema::create('cache_locks', function (Blueprint $table) {
|
if (!Schema::hasTable('cache_locks')) {
|
||||||
$table->string('key')->primary();
|
Schema::create('cache_locks', function (Blueprint $table) {
|
||||||
$table->string('owner');
|
$table->string('key')->primary();
|
||||||
$table->integer('expiration');
|
$table->string('owner');
|
||||||
});
|
$table->integer('expiration');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Schema::create('jobs', function (Blueprint $table) {
|
if (!Schema::hasTable('jobs')) {
|
||||||
$table->id();
|
Schema::create('jobs', function (Blueprint $table) {
|
||||||
$table->string('queue')->index();
|
$table->id();
|
||||||
$table->longText('payload');
|
$table->string('queue')->index();
|
||||||
$table->unsignedTinyInteger('attempts');
|
$table->longText('payload');
|
||||||
$table->unsignedInteger('reserved_at')->nullable();
|
$table->unsignedTinyInteger('attempts');
|
||||||
$table->unsignedInteger('available_at');
|
$table->unsignedInteger('reserved_at')->nullable();
|
||||||
$table->unsignedInteger('created_at');
|
$table->unsignedInteger('available_at');
|
||||||
});
|
$table->unsignedInteger('created_at');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Schema::create('job_batches', function (Blueprint $table) {
|
if (!Schema::hasTable('job_batches')) {
|
||||||
$table->string('id')->primary();
|
Schema::create('job_batches', function (Blueprint $table) {
|
||||||
$table->string('name');
|
$table->string('id')->primary();
|
||||||
$table->integer('total_jobs');
|
$table->string('name');
|
||||||
$table->integer('pending_jobs');
|
$table->integer('total_jobs');
|
||||||
$table->integer('failed_jobs');
|
$table->integer('pending_jobs');
|
||||||
$table->longText('failed_job_ids');
|
$table->integer('failed_jobs');
|
||||||
$table->mediumText('options')->nullable();
|
$table->longText('failed_job_ids');
|
||||||
$table->integer('cancelled_at')->nullable();
|
$table->mediumText('options')->nullable();
|
||||||
$table->integer('created_at');
|
$table->integer('cancelled_at')->nullable();
|
||||||
$table->integer('finished_at')->nullable();
|
$table->integer('created_at');
|
||||||
});
|
$table->integer('finished_at')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
if (!Schema::hasTable('failed_jobs')) {
|
||||||
$table->id();
|
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||||
$table->string('uuid')->unique();
|
$table->id();
|
||||||
$table->text('connection');
|
$table->string('uuid')->unique();
|
||||||
$table->text('queue');
|
$table->text('connection');
|
||||||
$table->longText('payload');
|
$table->text('queue');
|
||||||
$table->longText('exception');
|
$table->longText('payload');
|
||||||
$table->timestamp('failed_at');
|
$table->longText('exception');
|
||||||
});
|
$table->timestamp('failed_at');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::table('individui', function (Blueprint $table) {
|
if (!Schema::hasColumn('individui', 'numero_documento')) {
|
||||||
$table->string('numero_documento', 50)->nullable()->after('tipo_documento');
|
Schema::table('individui', function (Blueprint $table) {
|
||||||
});
|
$table->string('numero_documento', 50)->nullable()->after('tipo_documento');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::table('eventi', function (Blueprint $table) {
|
if (!Schema::hasColumn('eventi', 'giorno_mese')) {
|
||||||
$table->string('giorno_mese')->nullable()->after('giorno_settimana');
|
Schema::table('eventi', function (Blueprint $table) {
|
||||||
$table->string('mesi_recorrenza')->nullable()->after('giorno_mese');
|
$table->string('giorno_mese')->nullable()->after('giorno_settimana');
|
||||||
$table->string('mese_annuale')->nullable()->after('mesi_recorrenza');
|
$table->string('mesi_recorrenza')->nullable()->after('giorno_mese');
|
||||||
});
|
$table->string('mese_annuale')->nullable()->after('mesi_recorrenza');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::table('eventi', function (Blueprint $table) {
|
if (!Schema::hasColumn('eventi', 'occorrenza_mese')) {
|
||||||
$table->unsignedTinyInteger('occorrenza_mese')->nullable()->after('giorno_mese');
|
Schema::table('eventi', function (Blueprint $table) {
|
||||||
});
|
$table->unsignedTinyInteger('occorrenza_mese')->nullable()->after('giorno_mese');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('eventi_documenti', function (Blueprint $table) {
|
if (!Schema::hasTable('eventi_documenti')) {
|
||||||
$table->id();
|
Schema::create('eventi_documenti', function (Blueprint $table) {
|
||||||
$table->foreignId('evento_id')->constrained('eventi')->onDelete('cascade');
|
$table->id();
|
||||||
$table->foreignId('documento_id')->constrained('documenti')->onDelete('cascade');
|
$table->foreignId('evento_id')->constrained('eventi')->onDelete('cascade');
|
||||||
$table->timestamps();
|
$table->foreignId('documento_id')->constrained('documenti')->onDelete('cascade');
|
||||||
});
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::table('documenti', function (Blueprint $table) {
|
if (!Schema::hasColumn('documenti', 'user_id')) {
|
||||||
$table->foreignId('user_id')->nullable()->constrained('users')->onDelete('set null')->after('tenant_id');
|
Schema::table('documenti', function (Blueprint $table) {
|
||||||
});
|
$table->foreignId('user_id')->nullable()->constrained('users')->onDelete('set null')->after('tenant_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,19 +8,21 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('viste_report', function (Blueprint $table) {
|
if (!Schema::hasTable('viste_report')) {
|
||||||
$table->id();
|
Schema::create('viste_report', function (Blueprint $table) {
|
||||||
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
|
$table->id();
|
||||||
$table->string('nome');
|
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
|
||||||
$table->string('tipo'); // individui, gruppi, documenti, eventi
|
$table->string('nome');
|
||||||
$table->json('colonne_visibili')->nullable();
|
$table->string('tipo'); // individui, gruppi, documenti, eventi
|
||||||
$table->json('colonne_ordinamento')->nullable(); // [['colonna', 'direzione']]
|
$table->json('colonne_visibili')->nullable();
|
||||||
$table->json('filtri')->nullable(); // [['colonna', 'operatore', 'valore']]
|
$table->json('colonne_ordinamento')->nullable(); // [['colonna', 'direzione']]
|
||||||
$table->string('ricerca')->nullable();
|
$table->json('filtri')->nullable(); // [['colonna', 'operatore', 'valore']]
|
||||||
$table->timestamps();
|
$table->string('ricerca')->nullable();
|
||||||
$table->index('tipo');
|
$table->timestamps();
|
||||||
$table->index('user_id');
|
$table->index('tipo');
|
||||||
});
|
$table->index('user_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::table('viste_report', function (Blueprint $table) {
|
if (!Schema::hasColumn('viste_report', 'is_default')) {
|
||||||
$table->boolean('is_default')->default(false)->after('nome');
|
Schema::table('viste_report', function (Blueprint $table) {
|
||||||
$table->index('is_default');
|
$table->boolean('is_default')->default(false)->after('nome');
|
||||||
});
|
$table->index('is_default');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::table('mailing_lists', function (Blueprint $table) {
|
if (!Schema::hasColumn('mailing_lists', 'user_id')) {
|
||||||
$table->foreignId('user_id')->nullable()->constrained('users')->onDelete('set null')->after('tenant_id');
|
Schema::table('mailing_lists', function (Blueprint $table) {
|
||||||
$table->index('user_id');
|
$table->foreignId('user_id')->nullable()->constrained('users')->onDelete('set null')->after('tenant_id');
|
||||||
});
|
$table->index('user_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,14 +8,16 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('tipologie_documenti', function (Blueprint $table) {
|
if (!Schema::hasTable('tipologie_documenti')) {
|
||||||
$table->id();
|
Schema::create('tipologie_documenti', function (Blueprint $table) {
|
||||||
$table->string('nome', 50)->unique();
|
$table->id();
|
||||||
$table->string('descrizione', 255)->nullable();
|
$table->string('nome', 50)->unique();
|
||||||
$table->integer('ordine')->default(0);
|
$table->string('descrizione', 255)->nullable();
|
||||||
$table->boolean('attiva')->default(true);
|
$table->integer('ordine')->default(0);
|
||||||
$table->timestamps();
|
$table->boolean('attiva')->default(true);
|
||||||
});
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$defaultTipologie = ['avatar', 'galleria', 'documento', 'statuto', 'altro'];
|
$defaultTipologie = ['avatar', 'galleria', 'documento', 'statuto', 'altro'];
|
||||||
foreach ($defaultTipologie as $i => $nome) {
|
foreach ($defaultTipologie as $i => $nome) {
|
||||||
|
|||||||
@@ -8,32 +8,38 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::table('users', function (Blueprint $table) {
|
if (!Schema::hasColumn('users', 'permissions')) {
|
||||||
$table->json('permissions')->nullable()->after('is_admin');
|
Schema::table('users', function (Blueprint $table) {
|
||||||
});
|
$table->json('permissions')->nullable()->after('is_admin');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Schema::create('activity_logs', function (Blueprint $table) {
|
if (!Schema::hasTable('activity_logs')) {
|
||||||
$table->id();
|
Schema::create('activity_logs', function (Blueprint $table) {
|
||||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
$table->id();
|
||||||
$table->string('action');
|
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||||
$table->string('module');
|
$table->string('action');
|
||||||
$table->string('description')->nullable();
|
$table->string('module');
|
||||||
$table->json('details')->nullable();
|
$table->string('description')->nullable();
|
||||||
$table->string('ip_address', 45)->nullable();
|
$table->json('details')->nullable();
|
||||||
$table->timestamp('created_at')->useCurrent();
|
$table->string('ip_address', 45)->nullable();
|
||||||
|
$table->timestamp('created_at')->useCurrent();
|
||||||
|
|
||||||
$table->index(['user_id', 'module']);
|
$table->index(['user_id', 'module']);
|
||||||
$table->index(['created_at']);
|
$table->index(['created_at']);
|
||||||
$table->index(['action']);
|
$table->index(['action']);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Schema::create('role_presets', function (Blueprint $table) {
|
if (!Schema::hasTable('role_presets')) {
|
||||||
$table->id();
|
Schema::create('role_presets', function (Blueprint $table) {
|
||||||
$table->string('name', 50)->unique();
|
$table->id();
|
||||||
$table->string('description')->nullable();
|
$table->string('name', 50)->unique();
|
||||||
$table->json('permissions');
|
$table->string('description')->nullable();
|
||||||
$table->timestamps();
|
$table->json('permissions');
|
||||||
});
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
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.
|
* 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) {
|
if (!Schema::hasTable($tableNames['permissions'])) {
|
||||||
$table->id(); // permission id
|
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
|
||||||
$table->string('name');
|
$table->id(); // permission id
|
||||||
$table->string('guard_name');
|
$table->string('name');
|
||||||
$table->timestamps();
|
$table->string('guard_name');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
$table->unique(['name', 'guard_name']);
|
$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.
|
* 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) {
|
if (!Schema::hasTable($tableNames['roles'])) {
|
||||||
$table->id(); // role id
|
Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
|
||||||
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
|
$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->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
|
||||||
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
|
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
|
||||||
}
|
}
|
||||||
$table->string('name');
|
$table->string('name');
|
||||||
$table->string('guard_name');
|
$table->string('guard_name');
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
if ($teams || config('permission.testing')) {
|
if ($teams || config('permission.testing')) {
|
||||||
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
|
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
|
||||||
} else {
|
} else {
|
||||||
$table->unique(['name', 'guard_name']);
|
$table->unique(['name', 'guard_name']);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
|
if (!Schema::hasTable($tableNames['model_has_permissions'])) {
|
||||||
$table->unsignedBigInteger($pivotPermission);
|
Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
|
||||||
|
$table->unsignedBigInteger($pivotPermission);
|
||||||
|
|
||||||
$table->string('model_type');
|
$table->string('model_type');
|
||||||
$table->unsignedBigInteger($columnNames['model_morph_key']);
|
$table->unsignedBigInteger($columnNames['model_morph_key']);
|
||||||
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
|
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
|
||||||
|
|
||||||
$table->foreign($pivotPermission)
|
$table->foreign($pivotPermission)
|
||||||
->references('id') // permission id
|
->references('id') // permission id
|
||||||
->on($tableNames['permissions'])
|
->on($tableNames['permissions'])
|
||||||
->cascadeOnDelete();
|
->cascadeOnDelete();
|
||||||
if ($teams) {
|
if ($teams) {
|
||||||
$table->unsignedBigInteger($columnNames['team_foreign_key']);
|
$table->unsignedBigInteger($columnNames['team_foreign_key']);
|
||||||
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
|
$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'],
|
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
|
||||||
'model_has_permissions_permission_model_type_primary');
|
'model_has_permissions_permission_model_type_primary');
|
||||||
} else {
|
} else {
|
||||||
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
|
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
|
||||||
'model_has_permissions_permission_model_type_primary');
|
'model_has_permissions_permission_model_type_primary');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
|
if (!Schema::hasTable($tableNames['model_has_roles'])) {
|
||||||
$table->unsignedBigInteger($pivotRole);
|
Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
|
||||||
|
$table->unsignedBigInteger($pivotRole);
|
||||||
|
|
||||||
$table->string('model_type');
|
$table->string('model_type');
|
||||||
$table->unsignedBigInteger($columnNames['model_morph_key']);
|
$table->unsignedBigInteger($columnNames['model_morph_key']);
|
||||||
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
|
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
|
||||||
|
|
||||||
$table->foreign($pivotRole)
|
$table->foreign($pivotRole)
|
||||||
->references('id') // role id
|
->references('id') // role id
|
||||||
->on($tableNames['roles'])
|
->on($tableNames['roles'])
|
||||||
->cascadeOnDelete();
|
->cascadeOnDelete();
|
||||||
if ($teams) {
|
if ($teams) {
|
||||||
$table->unsignedBigInteger($columnNames['team_foreign_key']);
|
$table->unsignedBigInteger($columnNames['team_foreign_key']);
|
||||||
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
|
$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'],
|
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
|
||||||
'model_has_roles_role_model_type_primary');
|
'model_has_roles_role_model_type_primary');
|
||||||
} else {
|
} else {
|
||||||
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'],
|
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'],
|
||||||
'model_has_roles_role_model_type_primary');
|
'model_has_roles_role_model_type_primary');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
|
if (!Schema::hasTable($tableNames['role_has_permissions'])) {
|
||||||
$table->unsignedBigInteger($pivotPermission);
|
Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
|
||||||
$table->unsignedBigInteger($pivotRole);
|
$table->unsignedBigInteger($pivotPermission);
|
||||||
|
$table->unsignedBigInteger($pivotRole);
|
||||||
|
|
||||||
$table->foreign($pivotPermission)
|
$table->foreign($pivotPermission)
|
||||||
->references('id') // permission id
|
->references('id') // permission id
|
||||||
->on($tableNames['permissions'])
|
->on($tableNames['permissions'])
|
||||||
->cascadeOnDelete();
|
->cascadeOnDelete();
|
||||||
|
|
||||||
$table->foreign($pivotRole)
|
$table->foreign($pivotRole)
|
||||||
->references('id') // role id
|
->references('id') // role id
|
||||||
->on($tableNames['roles'])
|
->on($tableNames['roles'])
|
||||||
->cascadeOnDelete();
|
->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')
|
app('cache')
|
||||||
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
|
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
|
||||||
|
|||||||
@@ -8,26 +8,28 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('email_settings', function (Blueprint $table) {
|
if (!Schema::hasTable('email_settings')) {
|
||||||
$table->id();
|
Schema::create('email_settings', function (Blueprint $table) {
|
||||||
$table->string('imap_host')->nullable();
|
$table->id();
|
||||||
$table->integer('imap_port')->nullable()->default(993);
|
$table->string('imap_host')->nullable();
|
||||||
$table->string('imap_encryption')->nullable()->default('ssl');
|
$table->integer('imap_port')->nullable()->default(993);
|
||||||
$table->string('imap_username')->nullable();
|
$table->string('imap_encryption')->nullable()->default('ssl');
|
||||||
$table->string('imap_password')->nullable();
|
$table->string('imap_username')->nullable();
|
||||||
$table->string('smtp_host')->nullable();
|
$table->string('imap_password')->nullable();
|
||||||
$table->integer('smtp_port')->nullable()->default(587);
|
$table->string('smtp_host')->nullable();
|
||||||
$table->string('smtp_encryption')->nullable()->default('tls');
|
$table->integer('smtp_port')->nullable()->default(587);
|
||||||
$table->string('smtp_username')->nullable();
|
$table->string('smtp_encryption')->nullable()->default('tls');
|
||||||
$table->string('smtp_password')->nullable();
|
$table->string('smtp_username')->nullable();
|
||||||
$table->string('email_address')->nullable();
|
$table->string('smtp_password')->nullable();
|
||||||
$table->string('email_name')->nullable();
|
$table->string('email_address')->nullable();
|
||||||
$table->string('reply_to')->nullable();
|
$table->string('email_name')->nullable();
|
||||||
$table->integer('sync_interval_minutes')->default(5);
|
$table->string('reply_to')->nullable();
|
||||||
$table->timestamp('last_sync_at')->nullable();
|
$table->integer('sync_interval_minutes')->default(5);
|
||||||
$table->boolean('is_active')->default(false);
|
$table->timestamp('last_sync_at')->nullable();
|
||||||
$table->timestamps();
|
$table->boolean('is_active')->default(false);
|
||||||
});
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,17 +8,19 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('email_folders', function (Blueprint $table) {
|
if (!Schema::hasTable('email_folders')) {
|
||||||
$table->id();
|
Schema::create('email_folders', function (Blueprint $table) {
|
||||||
$table->string('name');
|
$table->id();
|
||||||
$table->string('imap_folder_name')->nullable();
|
$table->string('name');
|
||||||
$table->string('type')->default('custom');
|
$table->string('imap_folder_name')->nullable();
|
||||||
$table->string('icon')->default('fa-folder');
|
$table->string('type')->default('custom');
|
||||||
$table->string('color')->nullable();
|
$table->string('icon')->default('fa-folder');
|
||||||
$table->integer('sort_order')->default(0);
|
$table->string('color')->nullable();
|
||||||
$table->boolean('is_system')->default(false);
|
$table->integer('sort_order')->default(0);
|
||||||
$table->timestamps();
|
$table->boolean('is_system')->default(false);
|
||||||
});
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,35 +8,37 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('email_messages', function (Blueprint $table) {
|
if (!Schema::hasTable('email_messages')) {
|
||||||
$table->id();
|
Schema::create('email_messages', function (Blueprint $table) {
|
||||||
$table->foreignId('email_folder_id')->constrained('email_folders')->onDelete('cascade');
|
$table->id();
|
||||||
$table->string('message_id')->nullable()->index();
|
$table->foreignId('email_folder_id')->constrained('email_folders')->onDelete('cascade');
|
||||||
$table->string('subject')->nullable();
|
$table->string('message_id')->nullable()->index();
|
||||||
$table->string('from_name')->nullable();
|
$table->string('subject')->nullable();
|
||||||
$table->string('from_email')->nullable();
|
$table->string('from_name')->nullable();
|
||||||
$table->string('to_name')->nullable();
|
$table->string('from_email')->nullable();
|
||||||
$table->string('to_email')->nullable();
|
$table->string('to_name')->nullable();
|
||||||
$table->text('cc')->nullable();
|
$table->string('to_email')->nullable();
|
||||||
$table->text('bcc')->nullable();
|
$table->text('cc')->nullable();
|
||||||
$table->longText('body_text')->nullable();
|
$table->text('bcc')->nullable();
|
||||||
$table->longText('body_html')->nullable();
|
$table->longText('body_text')->nullable();
|
||||||
$table->boolean('is_read')->default(false);
|
$table->longText('body_html')->nullable();
|
||||||
$table->boolean('is_starred')->default(false);
|
$table->boolean('is_read')->default(false);
|
||||||
$table->boolean('is_important')->default(false);
|
$table->boolean('is_starred')->default(false);
|
||||||
$table->boolean('is_sent')->default(false);
|
$table->boolean('is_important')->default(false);
|
||||||
$table->boolean('is_draft')->default(false);
|
$table->boolean('is_sent')->default(false);
|
||||||
$table->boolean('is_trash')->default(false);
|
$table->boolean('is_draft')->default(false);
|
||||||
$table->timestamp('received_at')->nullable();
|
$table->boolean('is_trash')->default(false);
|
||||||
$table->timestamp('sent_at')->nullable();
|
$table->timestamp('received_at')->nullable();
|
||||||
$table->unsignedBigInteger('imap_uid')->nullable();
|
$table->timestamp('sent_at')->nullable();
|
||||||
$table->timestamps();
|
$table->unsignedBigInteger('imap_uid')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
$table->index('is_read');
|
$table->index('is_read');
|
||||||
$table->index('is_starred');
|
$table->index('is_starred');
|
||||||
$table->index('is_sent');
|
$table->index('is_sent');
|
||||||
$table->index('received_at');
|
$table->index('received_at');
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,17 +8,19 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('email_attachments', function (Blueprint $table) {
|
if (!Schema::hasTable('email_attachments')) {
|
||||||
$table->id();
|
Schema::create('email_attachments', function (Blueprint $table) {
|
||||||
$table->foreignId('email_message_id')->constrained('email_messages')->onDelete('cascade');
|
$table->id();
|
||||||
$table->string('filename');
|
$table->foreignId('email_message_id')->constrained('email_messages')->onDelete('cascade');
|
||||||
$table->string('mime_type')->nullable();
|
$table->string('filename');
|
||||||
$table->bigInteger('size')->default(0);
|
$table->string('mime_type')->nullable();
|
||||||
$table->string('file_path');
|
$table->bigInteger('size')->default(0);
|
||||||
$table->boolean('is_saved_to_documenti')->default(false);
|
$table->string('file_path');
|
||||||
$table->foreignId('documenti_id')->nullable()->constrained('documenti')->onDelete('set null');
|
$table->boolean('is_saved_to_documenti')->default(false);
|
||||||
$table->timestamps();
|
$table->foreignId('documenti_id')->nullable()->constrained('documenti')->onDelete('set null');
|
||||||
});
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,14 +8,16 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('ruoli', function (Blueprint $table) {
|
if (!Schema::hasTable('ruoli')) {
|
||||||
$table->id();
|
Schema::create('ruoli', function (Blueprint $table) {
|
||||||
$table->string('nome', 50)->unique();
|
$table->id();
|
||||||
$table->string('descrizione', 255)->nullable();
|
$table->string('nome', 50)->unique();
|
||||||
$table->integer('ordine')->default(0);
|
$table->string('descrizione', 255)->nullable();
|
||||||
$table->boolean('attiva')->default(true);
|
$table->integer('ordine')->default(0);
|
||||||
$table->timestamps();
|
$table->boolean('attiva')->default(true);
|
||||||
});
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$defaultRuoli = [
|
$defaultRuoli = [
|
||||||
['nome' => 'Membro', 'descrizione' => 'Membro del gruppo'],
|
['nome' => 'Membro', 'descrizione' => 'Membro del gruppo'],
|
||||||
@@ -38,25 +40,27 @@ return new class extends Migration
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Schema::table('gruppo_individuo', function (Blueprint $table) {
|
if (!Schema::hasColumn('gruppo_individuo', 'ruolo_id')) {
|
||||||
$table->unsignedBigInteger('ruolo_id')->nullable()->after('ruolo_nel_gruppo');
|
Schema::table('gruppo_individuo', function (Blueprint $table) {
|
||||||
$table->foreign('ruolo_id')->references('id')->on('ruoli')->onDelete('set null');
|
$table->unsignedBigInteger('ruolo_id')->nullable()->after('ruolo_nel_gruppo');
|
||||||
$table->index('ruolo_id');
|
$table->foreign('ruolo_id')->references('id')->on('ruoli')->onDelete('set null');
|
||||||
});
|
$table->index('ruolo_id');
|
||||||
|
});
|
||||||
|
|
||||||
$ruoliMap = DB::table('ruoli')->pluck('id', 'nome')->toArray();
|
$ruoliMap = DB::table('ruoli')->pluck('id', 'nome')->toArray();
|
||||||
|
|
||||||
$pivotRecords = DB::table('gruppo_individuo')
|
$pivotRecords = DB::table('gruppo_individuo')
|
||||||
->whereNotNull('ruolo_nel_gruppo')
|
->whereNotNull('ruolo_nel_gruppo')
|
||||||
->where('ruolo_nel_gruppo', '!=', '')
|
->where('ruolo_nel_gruppo', '!=', '')
|
||||||
->distinct()
|
->distinct()
|
||||||
->pluck('ruolo_nel_gruppo');
|
->pluck('ruolo_nel_gruppo');
|
||||||
|
|
||||||
foreach ($pivotRecords as $ruoloNome) {
|
foreach ($pivotRecords as $ruoloNome) {
|
||||||
if (isset($ruoliMap[$ruoloNome])) {
|
if (isset($ruoliMap[$ruoloNome])) {
|
||||||
DB::table('gruppo_individuo')
|
DB::table('gruppo_individuo')
|
||||||
->where('ruolo_nel_gruppo', $ruoloNome)
|
->where('ruolo_nel_gruppo', $ruoloNome)
|
||||||
->update(['ruolo_id' => $ruoliMap[$ruoloNome]]);
|
->update(['ruolo_id' => $ruoliMap[$ruoloNome]]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,11 @@ return new class extends Migration
|
|||||||
$table->dropColumn('ruolo_id');
|
$table->dropColumn('ruolo_id');
|
||||||
});
|
});
|
||||||
|
|
||||||
Schema::table('gruppo_individuo', function (Blueprint $table) {
|
if (!Schema::hasColumn('gruppo_individuo', 'ruolo_ids')) {
|
||||||
$table->json('ruolo_ids')->nullable()->after('individuo_id');
|
Schema::table('gruppo_individuo', function (Blueprint $table) {
|
||||||
});
|
$table->json('ruolo_ids')->nullable()->after('individuo_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
@@ -24,9 +26,11 @@ return new class extends Migration
|
|||||||
$table->dropColumn('ruolo_ids');
|
$table->dropColumn('ruolo_ids');
|
||||||
});
|
});
|
||||||
|
|
||||||
Schema::table('gruppo_individuo', function (Blueprint $table) {
|
if (!Schema::hasColumn('gruppo_individuo', 'ruolo_id')) {
|
||||||
$table->unsignedBigInteger('ruolo_id')->nullable()->after('individuo_id');
|
Schema::table('gruppo_individuo', function (Blueprint $table) {
|
||||||
$table->foreign('ruolo_id')->references('id')->on('ruoli')->onDelete('set null');
|
$table->unsignedBigInteger('ruolo_id')->nullable()->after('individuo_id');
|
||||||
});
|
$table->foreign('ruolo_id')->references('id')->on('ruoli')->onDelete('set null');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -13,9 +13,11 @@ return new class extends Migration
|
|||||||
$table->dropColumn('responsabile_id');
|
$table->dropColumn('responsabile_id');
|
||||||
});
|
});
|
||||||
|
|
||||||
Schema::table('gruppi', function (Blueprint $table) {
|
if (!Schema::hasColumn('gruppi', 'responsabile_ids')) {
|
||||||
$table->json('responsabile_ids')->nullable()->after('diocesi_id');
|
Schema::table('gruppi', function (Blueprint $table) {
|
||||||
});
|
$table->json('responsabile_ids')->nullable()->after('diocesi_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
@@ -24,9 +26,11 @@ return new class extends Migration
|
|||||||
$table->dropColumn('responsabile_ids');
|
$table->dropColumn('responsabile_ids');
|
||||||
});
|
});
|
||||||
|
|
||||||
Schema::table('gruppi', function (Blueprint $table) {
|
if (!Schema::hasColumn('gruppi', 'responsabile_id')) {
|
||||||
$table->unsignedBigInteger('responsabile_id')->nullable()->after('diocesi_id');
|
Schema::table('gruppi', function (Blueprint $table) {
|
||||||
$table->foreign('responsabile_id')->references('id')->on('individui')->onDelete('set null');
|
$table->unsignedBigInteger('responsabile_id')->nullable()->after('diocesi_id');
|
||||||
});
|
$table->foreign('responsabile_id')->references('id')->on('individui')->onDelete('set null');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -8,10 +8,12 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::table('eventi', function (Blueprint $table) {
|
if (!Schema::hasColumn('eventi', 'luogo_indirizzo')) {
|
||||||
$table->string('luogo_indirizzo', 500)->nullable()->after('note');
|
Schema::table('eventi', function (Blueprint $table) {
|
||||||
$table->text('luogo_url_maps')->nullable()->after('luogo_indirizzo');
|
$table->string('luogo_indirizzo', 500)->nullable()->after('note');
|
||||||
});
|
$table->text('luogo_url_maps')->nullable()->after('luogo_indirizzo');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?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('app_settings')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema::create('app_settings', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('nome_applicazione')->nullable();
|
||||||
|
$table->string('nome_organizzazione')->nullable();
|
||||||
|
$table->string('logo')->nullable();
|
||||||
|
$table->string('logo_small')->nullable();
|
||||||
|
$table->string('logo_path')->nullable();
|
||||||
|
$table->string('logo_small_path')->nullable();
|
||||||
|
$table->string('footer_text')->nullable();
|
||||||
|
$table->string('app_version')->nullable();
|
||||||
|
$table->string('dashboard_welcome')->nullable();
|
||||||
|
$table->boolean('show_version')->default(false);
|
||||||
|
$table->string('documenti_storage_disk')->default('local');
|
||||||
|
$table->string('documenti_storage_path')->default('documenti');
|
||||||
|
$table->string('backup_path')->default('backups');
|
||||||
|
$table->integer('backup_retention_days')->default(30);
|
||||||
|
$table->boolean('backup_include_files')->default(true);
|
||||||
|
$table->boolean('backup_include_env')->default(true);
|
||||||
|
$table->boolean('backup_auto_enabled')->default(false);
|
||||||
|
$table->string('backup_auto_frequency', 20)->default('daily');
|
||||||
|
$table->integer('backup_auto_hour')->default(3);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('app_settings');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -8,10 +8,21 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::table('app_settings', function (Blueprint $table) {
|
if (!Schema::hasTable('app_settings')) {
|
||||||
$table->string('logo_path')->nullable()->after('logo');
|
return;
|
||||||
$table->string('logo_small_path')->nullable()->after('logo_small');
|
}
|
||||||
});
|
|
||||||
|
if (!Schema::hasColumn('app_settings', 'logo_path')) {
|
||||||
|
Schema::table('app_settings', function (Blueprint $table) {
|
||||||
|
$table->string('logo_path')->nullable()->after('logo');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Schema::hasColumn('app_settings', 'logo_small_path')) {
|
||||||
|
Schema::table('app_settings', function (Blueprint $table) {
|
||||||
|
$table->string('logo_small_path')->nullable()->after('logo_small');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,12 +8,24 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::table('app_settings', function (Blueprint $table) {
|
if (!Schema::hasTable('app_settings')) {
|
||||||
$table->string('footer_text')->nullable()->after('nome_organizzazione');
|
return;
|
||||||
$table->string('app_version')->nullable()->after('footer_text');
|
}
|
||||||
$table->string('dashboard_welcome')->nullable()->after('app_version');
|
|
||||||
$table->boolean('show_version')->default(false)->after('dashboard_welcome');
|
$columns = ['footer_text', 'app_version', 'dashboard_welcome', 'show_version'];
|
||||||
});
|
|
||||||
|
foreach ($columns as $column) {
|
||||||
|
if (!Schema::hasColumn('app_settings', $column)) {
|
||||||
|
Schema::table('app_settings', function (Blueprint $table) use ($column) {
|
||||||
|
match ($column) {
|
||||||
|
'footer_text' => $table->string('footer_text')->nullable()->after('nome_organizzazione'),
|
||||||
|
'app_version' => $table->string('app_version')->nullable()->after('footer_text'),
|
||||||
|
'dashboard_welcome' => $table->string('dashboard_welcome')->nullable()->after('app_version'),
|
||||||
|
'show_version' => $table->boolean('show_version')->default(false)->after('dashboard_welcome'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::table('email_settings', function (Blueprint $table) {
|
if (!Schema::hasColumn('email_settings', 'signature')) {
|
||||||
$table->longText('signature')->nullable()->after('reply_to');
|
Schema::table('email_settings', function (Blueprint $table) {
|
||||||
});
|
$table->longText('signature')->nullable()->after('reply_to');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::table('email_settings', function (Blueprint $table) {
|
if (!Schema::hasColumn('email_settings', 'signature_enabled')) {
|
||||||
$table->boolean('signature_enabled')->default(true)->after('signature');
|
Schema::table('email_settings', function (Blueprint $table) {
|
||||||
});
|
$table->boolean('signature_enabled')->default(true)->after('signature');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,15 +8,17 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('report_custom', function (Blueprint $table) {
|
if (!Schema::hasTable('report_custom')) {
|
||||||
$table->id();
|
Schema::create('report_custom', function (Blueprint $table) {
|
||||||
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
|
$table->id();
|
||||||
$table->string('nome');
|
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
|
||||||
$table->text('descrizione')->nullable();
|
$table->string('nome');
|
||||||
$table->string('tipo_report');
|
$table->text('descrizione')->nullable();
|
||||||
$table->json('config')->nullable();
|
$table->string('tipo_report');
|
||||||
$table->timestamps();
|
$table->json('config')->nullable();
|
||||||
});
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -10,14 +10,16 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('tipologie_eventi', function (Blueprint $table) {
|
if (!Schema::hasTable('tipologie_eventi')) {
|
||||||
$table->id();
|
Schema::create('tipologie_eventi', function (Blueprint $table) {
|
||||||
$table->string('nome', 50)->unique();
|
$table->id();
|
||||||
$table->string('descrizione', 255)->nullable();
|
$table->string('nome', 50)->unique();
|
||||||
$table->integer('ordine')->default(0);
|
$table->string('descrizione', 255)->nullable();
|
||||||
$table->boolean('attiva')->default(true);
|
$table->integer('ordine')->default(0);
|
||||||
$table->timestamps();
|
$table->boolean('attiva')->default(true);
|
||||||
});
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
DB::table('tipologie_eventi')->insert([
|
DB::table('tipologie_eventi')->insert([
|
||||||
['nome' => 'catechesi', 'descrizione' => 'Catechesi', 'ordine' => 0, 'attiva' => true],
|
['nome' => 'catechesi', 'descrizione' => 'Catechesi', 'ordine' => 0, 'attiva' => true],
|
||||||
|
|||||||
@@ -10,21 +10,23 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('sender_accounts', function (Blueprint $table) {
|
if (!Schema::hasTable('sender_accounts')) {
|
||||||
$table->id();
|
Schema::create('sender_accounts', function (Blueprint $table) {
|
||||||
$table->string('email_address')->unique();
|
$table->id();
|
||||||
$table->string('email_name')->nullable();
|
$table->string('email_address')->unique();
|
||||||
$table->string('smtp_host')->nullable();
|
$table->string('email_name')->nullable();
|
||||||
$table->integer('smtp_port')->default(587);
|
$table->string('smtp_host')->nullable();
|
||||||
$table->string('smtp_encryption')->default('tls');
|
$table->integer('smtp_port')->default(587);
|
||||||
$table->string('smtp_username')->nullable();
|
$table->string('smtp_encryption')->default('tls');
|
||||||
$table->text('smtp_password')->nullable();
|
$table->string('smtp_username')->nullable();
|
||||||
$table->string('reply_to')->nullable();
|
$table->text('smtp_password')->nullable();
|
||||||
$table->string('verify_email')->nullable();
|
$table->string('reply_to')->nullable();
|
||||||
$table->text('note')->nullable();
|
$table->string('verify_email')->nullable();
|
||||||
$table->boolean('is_active')->default(true);
|
$table->text('note')->nullable();
|
||||||
$table->timestamps();
|
$table->boolean('is_active')->default(true);
|
||||||
});
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::table('mailing_messaggi', function (Blueprint $table) {
|
if (!Schema::hasColumn('mailing_messaggi', 'log_falliti')) {
|
||||||
$table->json('log_falliti')->nullable()->after('invii_falliti');
|
Schema::table('mailing_messaggi', function (Blueprint $table) {
|
||||||
$table->string('mittente_nome')->nullable()->after('log_falliti');
|
$table->json('log_falliti')->nullable()->after('invii_falliti');
|
||||||
$table->string('mittente_email')->nullable()->after('mittente_nome');
|
$table->string('mittente_nome')->nullable()->after('log_falliti');
|
||||||
});
|
$table->string('mittente_email')->nullable()->after('mittente_nome');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('documenti_cartelle', function (Blueprint $table) {
|
if (!Schema::hasTable('documenti_cartelle')) {
|
||||||
$table->id();
|
Schema::create('documenti_cartelle', function (Blueprint $table) {
|
||||||
$table->foreignId('parent_id')->nullable()->constrained('documenti_cartelle')->onDelete('cascade');
|
$table->id();
|
||||||
$table->string('nome');
|
$table->foreignId('parent_id')->nullable()->constrained('documenti_cartelle')->onDelete('cascade');
|
||||||
$table->timestamps();
|
$table->string('nome');
|
||||||
});
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::table('documenti', function (Blueprint $table) {
|
if (!Schema::hasColumn('documenti', 'cartella_id')) {
|
||||||
$table->foreignId('cartella_id')->nullable()->constrained('documenti_cartelle')->onDelete('set null');
|
Schema::table('documenti', function (Blueprint $table) {
|
||||||
});
|
$table->foreignId('cartella_id')->nullable()->constrained('documenti_cartelle')->onDelete('set null');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,10 +8,21 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::table('app_settings', function (Blueprint $table) {
|
if (!Schema::hasTable('app_settings')) {
|
||||||
$table->string('documenti_storage_disk')->default('local')->after('show_version');
|
return;
|
||||||
$table->string('documenti_storage_path')->default('documenti')->after('documenti_storage_disk');
|
}
|
||||||
});
|
|
||||||
|
if (!Schema::hasColumn('app_settings', 'documenti_storage_disk')) {
|
||||||
|
Schema::table('app_settings', function (Blueprint $table) {
|
||||||
|
$table->string('documenti_storage_disk')->default('local')->after('show_version');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Schema::hasColumn('app_settings', 'documenti_storage_path')) {
|
||||||
|
Schema::table('app_settings', function (Blueprint $table) {
|
||||||
|
$table->string('documenti_storage_path')->default('documenti')->after('documenti_storage_disk');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,15 +8,17 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('storage_repositories', function (Blueprint $table) {
|
if (!Schema::hasTable('storage_repositories')) {
|
||||||
$table->id();
|
Schema::create('storage_repositories', function (Blueprint $table) {
|
||||||
$table->string('nome');
|
$table->id();
|
||||||
$table->string('tipo', 50);
|
$table->string('nome');
|
||||||
$table->text('config');
|
$table->string('tipo', 50);
|
||||||
$table->boolean('is_active')->default(true);
|
$table->text('config');
|
||||||
$table->integer('ordine')->default(0);
|
$table->boolean('is_active')->default(true);
|
||||||
$table->timestamps();
|
$table->integer('ordine')->default(0);
|
||||||
});
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -8,13 +8,15 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::table('documenti', function (Blueprint $table) {
|
if (!Schema::hasColumn('documenti', 'repository_id')) {
|
||||||
$table->foreignId('repository_id')
|
Schema::table('documenti', function (Blueprint $table) {
|
||||||
->nullable()
|
$table->foreignId('repository_id')
|
||||||
->constrained('storage_repositories')
|
->nullable()
|
||||||
->nullOnDelete()
|
->constrained('storage_repositories')
|
||||||
->after('cartella_id');
|
->nullOnDelete()
|
||||||
});
|
->after('cartella_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::table('documenti', function (Blueprint $table) {
|
if (!Schema::hasColumn('documenti', 'storage_disk')) {
|
||||||
$table->string('storage_disk', 50)->nullable()->after('repository_id');
|
Schema::table('documenti', function (Blueprint $table) {
|
||||||
});
|
$table->string('storage_disk', 50)->nullable()->after('repository_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -10,15 +10,31 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::table('app_settings', function (Blueprint $table) {
|
if (!Schema::hasTable('app_settings')) {
|
||||||
$table->string('backup_path')->default('backups')->after('show_version');
|
return;
|
||||||
$table->integer('backup_retention_days')->default(30)->after('backup_path');
|
}
|
||||||
$table->boolean('backup_include_files')->default(true)->after('backup_retention_days');
|
|
||||||
$table->boolean('backup_include_env')->default(true)->after('backup_include_files');
|
$columns = [
|
||||||
$table->boolean('backup_auto_enabled')->default(false)->after('backup_include_env');
|
'backup_path' => ['type' => 'string', 'default' => 'backups'],
|
||||||
$table->string('backup_auto_frequency', 20)->default('daily')->after('backup_auto_enabled');
|
'backup_retention_days' => ['type' => 'integer', 'default' => 30],
|
||||||
$table->integer('backup_auto_hour')->default(3)->after('backup_auto_frequency');
|
'backup_include_files' => ['type' => 'boolean', 'default' => true],
|
||||||
});
|
'backup_include_env' => ['type' => 'boolean', 'default' => true],
|
||||||
|
'backup_auto_enabled' => ['type' => 'boolean', 'default' => false],
|
||||||
|
'backup_auto_frequency' => ['type' => 'string', 'length' => 20, 'default' => 'daily'],
|
||||||
|
'backup_auto_hour' => ['type' => 'integer', 'default' => 3],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($columns as $column => $config) {
|
||||||
|
if (!Schema::hasColumn('app_settings', $column)) {
|
||||||
|
Schema::table('app_settings', function (Blueprint $table) use ($column, $config) {
|
||||||
|
match ($config['type']) {
|
||||||
|
'string' => $table->string($column, $config['length'] ?? 255)->default($config['default'])->after('show_version'),
|
||||||
|
'integer' => $table->integer($column)->default($config['default'])->after('show_version'),
|
||||||
|
'boolean' => $table->boolean($column)->default($config['default'])->after('show_version'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@@ -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('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
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('calendario_connessioni');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?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('eventi', 'uid_esterno')) {
|
||||||
|
Schema::table('eventi', function (Blueprint $table) {
|
||||||
|
$table->string('uid_esterno', 500)->nullable()->after('luogo_url_maps');
|
||||||
|
$table->index('uid_esterno');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('eventi', function (Blueprint $table) {
|
||||||
|
$table->dropIndex(['uid_esterno']);
|
||||||
|
$table->dropColumn('uid_esterno');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (!Schema::hasColumn('eventi', 'descrizione_evento')) {
|
||||||
|
Schema::table('eventi', function (Blueprint $table) {
|
||||||
|
$table->text('descrizione_evento')->nullable()->after('nome_evento');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Schema::hasColumn('eventi', 'is_incontro_gruppo')) {
|
||||||
|
Schema::table('eventi', function (Blueprint $table) {
|
||||||
|
$table->boolean('is_incontro_gruppo')->default(false)->after('uid_esterno');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?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('gruppo_individuo', 'ruolo_nel_gruppo')) {
|
||||||
|
Schema::table('gruppo_individuo', function (Blueprint $table) {
|
||||||
|
$table->string('ruolo_nel_gruppo')->nullable()->after('ruolo_ids');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?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('tags')) {
|
||||||
|
Schema::create('tags', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name', 100);
|
||||||
|
$table->string('slug', 100)->unique();
|
||||||
|
$table->string('color', 7)->nullable();
|
||||||
|
$table->integer('order_column')->default(0);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Schema::hasTable('taggables')) {
|
||||||
|
Schema::create('taggables', function (Blueprint $table) {
|
||||||
|
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->morphs('taggable');
|
||||||
|
$table->primary(['tag_id', 'taggable_id', 'taggable_type']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('taggables');
|
||||||
|
Schema::dropIfExists('tags');
|
||||||
|
}
|
||||||
|
};
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('viste_report', function (Blueprint $table) {
|
||||||
|
if (!Schema::hasColumn('viste_report', 'colonne_larghezze')) {
|
||||||
|
$table->json('colonne_larghezze')->nullable()->after('colonne_visibili');
|
||||||
|
}
|
||||||
|
if (!Schema::hasColumn('viste_report', 'colonne_ordine')) {
|
||||||
|
$table->json('colonne_ordine')->nullable()->after('colonne_larghezze');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('viste_report', function (Blueprint $table) {
|
||||||
|
if (Schema::hasColumn('viste_report', 'colonne_larghezze')) {
|
||||||
|
$table->dropColumn('colonne_larghezze');
|
||||||
|
}
|
||||||
|
if (Schema::hasColumn('viste_report', 'colonne_ordine')) {
|
||||||
|
$table->dropColumn('colonne_ordine');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -68,7 +68,7 @@ class ComuniSeeder extends Seeder
|
|||||||
];
|
];
|
||||||
|
|
||||||
foreach ($comuni as $c) {
|
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,
|
DockerBaseDataSeeder::class,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$admin = User::create([
|
$admin = User::firstOrCreate(
|
||||||
'name' => 'Admin',
|
['email' => 'admin@glastree.local'],
|
||||||
'email' => 'admin@glastree.local',
|
[
|
||||||
'password' => bcrypt('password'),
|
'name' => 'Admin',
|
||||||
'is_admin' => true,
|
'password' => bcrypt('password'),
|
||||||
'status' => 'active',
|
'is_admin' => true,
|
||||||
'permissions' => [
|
'status' => 'active',
|
||||||
'individui' => 2,
|
'permissions' => [
|
||||||
'gruppi' => 2,
|
'individui' => 2,
|
||||||
'eventi' => 2,
|
'gruppi' => 2,
|
||||||
'documenti' => 2,
|
'eventi' => 2,
|
||||||
'mailing' => 2,
|
'documenti' => 2,
|
||||||
'viste' => 2,
|
'mailing' => 2,
|
||||||
],
|
'viste' => 2,
|
||||||
]);
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
$admin->role_preset_id = 1;
|
if (!$admin->role_preset_id) {
|
||||||
$admin->save();
|
$admin->role_preset_id = 1;
|
||||||
|
$admin->save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,7 +149,7 @@ class DiocesiSeeder extends Seeder
|
|||||||
];
|
];
|
||||||
|
|
||||||
foreach ($diocesi as $d) {
|
foreach ($diocesi as $d) {
|
||||||
Diocesi::create($d);
|
Diocesi::firstOrCreate(['nome' => $d['nome']], $d);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Binary file not shown.
+848
@@ -0,0 +1,848 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Glastree — Installer interattivo per amministratori (PHP CLI)
|
||||||
|
*
|
||||||
|
* Uso:
|
||||||
|
* php install.php
|
||||||
|
*
|
||||||
|
* Richiede: PHP 8.3+, Composer, MySQL o SQLite
|
||||||
|
* Opzionale: npm (per asset frontend)
|
||||||
|
*
|
||||||
|
* Modalità:
|
||||||
|
* 1) Fresh Install su Apache (LAMP)
|
||||||
|
* 2) Fresh Install via Docker
|
||||||
|
* 3) Restore da Backup (Apache)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ── Safety ──────────────────────────────────────────
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
// ── Constants ────────────────────────────────────────
|
||||||
|
const REQUIREMENTS = ['pdo_mysql', 'mbstring', 'xml', 'curl', 'zip', 'gd', 'fileinfo'];
|
||||||
|
const MIN_PHP = '8.3.0';
|
||||||
|
|
||||||
|
// ── Utilities ────────────────────────────────────────
|
||||||
|
function println(string $msg = ''): void { echo $msg . PHP_EOL; }
|
||||||
|
|
||||||
|
function color(string $text, string $code): string {
|
||||||
|
$map = [
|
||||||
|
'red' => '0;31',
|
||||||
|
'green' => '0;32',
|
||||||
|
'yellow' => '1;33',
|
||||||
|
'cyan' => '0;36',
|
||||||
|
'white' => '1;37',
|
||||||
|
];
|
||||||
|
$c = $map[$code] ?? '0';
|
||||||
|
return "\033[{$c}m{$text}\033[0m";
|
||||||
|
}
|
||||||
|
|
||||||
|
function info(string $msg): void { println(color('[INFO]', 'cyan') . ' ' . $msg); }
|
||||||
|
function ok(string $msg): void { println(color('[OK]', 'green') . ' ' . $msg); }
|
||||||
|
function warn(string $msg): void { println(color('[WARN]', 'yellow') . ' ' . $msg); }
|
||||||
|
function error(string $msg): void { println(color('[ERR]', 'red') . ' ' . $msg); }
|
||||||
|
|
||||||
|
function fail(string $msg): never { error($msg); exit(1); }
|
||||||
|
|
||||||
|
function ask(string $prompt, ?string $default = null): string {
|
||||||
|
$defaultStr = $default !== null ? " [{$default}]" : '';
|
||||||
|
echo color('?', 'cyan') . " {$prompt}{$defaultStr}: ";
|
||||||
|
$val = trim(fgets(STDIN) ?: '');
|
||||||
|
return $val !== '' ? $val : ($default ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function askRequired(string $prompt): string {
|
||||||
|
while (true) {
|
||||||
|
$val = ask($prompt);
|
||||||
|
if ($val !== '') break;
|
||||||
|
warn('Valore obbligatorio.');
|
||||||
|
}
|
||||||
|
return $val;
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirm(string $prompt): bool {
|
||||||
|
$resp = strtolower(ask($prompt . ' [S/n]', 'S'));
|
||||||
|
return $resp === 's' || $resp === '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function menu(string $prompt, array $options): string {
|
||||||
|
foreach ($options as $i => $opt) {
|
||||||
|
println(' ' . ($i + 1) . ') ' . $opt);
|
||||||
|
}
|
||||||
|
while (true) {
|
||||||
|
$val = ask($prompt, '1');
|
||||||
|
if (isset($options[(int)$val - 1])) return $options[(int)$val - 1];
|
||||||
|
warn('Scelta non valida.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkCmd(string $cmd): bool {
|
||||||
|
$found = !(trim(shell_exec("command -v $cmd 2>/dev/null") ?: '') === '');
|
||||||
|
if (!$found) warn("Comando '$cmd' non trovato.");
|
||||||
|
return $found;
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(string $cmd, ?string $label = null): bool {
|
||||||
|
if ($label) info($label);
|
||||||
|
println(" \$ {$cmd}");
|
||||||
|
passthru($cmd, $exitCode);
|
||||||
|
if ($exitCode !== 0) warn("Comando terminato con codice {$exitCode}");
|
||||||
|
return $exitCode === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCapture(string $cmd): string {
|
||||||
|
return trim(shell_exec($cmd) ?: '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crea l'utente amministratore via PDO prepared statement.
|
||||||
|
* Nessun escaping PHP necessario (tutto via prepared statement).
|
||||||
|
*/
|
||||||
|
function createAdminUser(
|
||||||
|
string $dbHost, string $dbPort, string $dbName,
|
||||||
|
string $dbUser, string $dbPass,
|
||||||
|
string $adminName, string $adminEmail, string $adminPass
|
||||||
|
): bool {
|
||||||
|
try {
|
||||||
|
$pdo = new PDO(
|
||||||
|
"mysql:host={$dbHost};port={$dbPort};dbname={$dbName}",
|
||||||
|
$dbUser, $dbPass,
|
||||||
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||||
|
);
|
||||||
|
|
||||||
|
$hash = password_hash($adminPass, PASSWORD_BCRYPT);
|
||||||
|
$permissions = json_encode([
|
||||||
|
'individui' => 2, 'gruppi' => 2, 'eventi' => 2,
|
||||||
|
'documenti' => 2, 'mailing' => 2, 'viste' => 2,
|
||||||
|
'report' => 2, 'settings' => 2,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Elimina eventuale utente con stessa email (seeder)
|
||||||
|
$stmt = $pdo->prepare('DELETE FROM users WHERE email = ?');
|
||||||
|
$stmt->execute([$adminEmail]);
|
||||||
|
|
||||||
|
// Inserisce admin con is_admin=1, status='active', permissions completi
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"INSERT INTO users (name, email, password, is_admin, status, permissions, role_preset_id, created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, 1, 'active', ?, 1, NOW(), NOW())"
|
||||||
|
);
|
||||||
|
$stmt->execute([$adminName, $adminEmail, $hash, $permissions]);
|
||||||
|
|
||||||
|
$userId = $pdo->lastInsertId();
|
||||||
|
|
||||||
|
// Assegna ruolo Spatie 'admin' (role_id = 1)
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"INSERT INTO model_has_roles (role_id, model_type, model_id) VALUES (1, ?, ?)"
|
||||||
|
);
|
||||||
|
$stmt->execute(['App\\Models\\User', $userId]);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (\PDOException $e) {
|
||||||
|
error("Errore creazione admin: " . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Banner ───────────────────────────────────────────
|
||||||
|
println(color(' ____ _ _ _', 'white'));
|
||||||
|
println(color(' / ___| | __ _ ___| |_ ___ _ __ ___ | |_ ___', 'white'));
|
||||||
|
println(color('| | _| |/ _` / __| __/ _ \'__/ _ \ | __/ _ \\', 'white'));
|
||||||
|
println(color('| |_| | | (_| \__ \ || __/ | | __/ | || __/', 'white'));
|
||||||
|
println(color(' \____|_|\__,_|___/\__\___|_| \___| \__\___|', 'white'));
|
||||||
|
println(color(' Installer interattivo per sysadmin', 'cyan'));
|
||||||
|
println();
|
||||||
|
|
||||||
|
// ── Preflight ────────────────────────────────────────
|
||||||
|
$scriptDir = dirname(__FILE__);
|
||||||
|
chdir($scriptDir);
|
||||||
|
|
||||||
|
if (!file_exists('.env.example')) {
|
||||||
|
fail("File .env.example non trovato in {$scriptDir}. Sei nella cartella dell'applicazione?");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Select mode ──────────────────────────────────────
|
||||||
|
println('Seleziona la modalità di installazione:');
|
||||||
|
$mode = menu('Scegli', [
|
||||||
|
'Fresh Install su Apache (LAMP tradizionale)',
|
||||||
|
'Fresh Install via Docker',
|
||||||
|
'Restore da Backup (Apache)',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ── Common params ────────────────────────────────────
|
||||||
|
println();
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
println(color(' PARAMETRI GENERALI', 'yellow'));
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
|
||||||
|
$appName = askRequired('Nome del sito/app (es. Glastree MyChurch)');
|
||||||
|
$adminName = askRequired('Nome del primo utente amministratore');
|
||||||
|
$adminEmail = askRequired('Email dell\'amministratore');
|
||||||
|
$adminPass = askRequired('Password per l\'amministratore');
|
||||||
|
$appUrl = askRequired('URL pubblico del sito (es. https://glastree.esempio.it)');
|
||||||
|
|
||||||
|
// ── DB params (Apache / Restore) ─────────────────────
|
||||||
|
$dbType = 'mysql';
|
||||||
|
$dbHost = '127.0.0.1';
|
||||||
|
$dbPort = '3306';
|
||||||
|
$dbName = 'glastree';
|
||||||
|
$dbUser = 'glastree';
|
||||||
|
$dbPass = '';
|
||||||
|
|
||||||
|
if (in_array($mode, ['Fresh Install su Apache (LAMP tradizionale)', 'Restore da Backup (Apache)'])) {
|
||||||
|
println();
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
println(color(' DATABASE', 'yellow'));
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
|
||||||
|
$dbType = menu('Tipo database', ['mysql', 'sqlite']);
|
||||||
|
|
||||||
|
if ($dbType === 'mysql') {
|
||||||
|
$dbHost = askRequired('Host database');
|
||||||
|
$dbPort = ask('Porta database', '3306');
|
||||||
|
$dbName = ask('Nome database (verrà creato se non esiste)', 'glastree');
|
||||||
|
$dbUser = askRequired('Utente database');
|
||||||
|
$dbPass = askRequired('Password database');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Web server user detection ─────────────────────────
|
||||||
|
$wwwUser = 'www-data';
|
||||||
|
$sudo = '';
|
||||||
|
if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
|
||||||
|
$detected = trim(shell_exec('ps aux | grep -E "apache|httpd" | grep -v grep | head -1 | awk \'{print $1}\'') ?: '');
|
||||||
|
if ($detected !== '' && $detected !== 'root') {
|
||||||
|
$wwwUser = $detected;
|
||||||
|
}
|
||||||
|
if (function_exists('posix_getuid') && posix_getuid() === 0) {
|
||||||
|
$sudo = "sudo -u {$wwwUser} ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════
|
||||||
|
// MODE 1: FRESH INSTALL APACHE
|
||||||
|
// ══════════════════════════════════════════════════════
|
||||||
|
if ($mode === 'Fresh Install su Apache (LAMP tradizionale)') {
|
||||||
|
println();
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
println(color(' PRE-FLIGHT CHECKS', 'yellow'));
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
|
||||||
|
// PHP version
|
||||||
|
$phpVer = PHP_VERSION;
|
||||||
|
info("PHP versione: {$phpVer}");
|
||||||
|
if (version_compare($phpVer, MIN_PHP, '<')) {
|
||||||
|
fail("PHP >= " . MIN_PHP . " richiesto (trovato {$phpVer})");
|
||||||
|
}
|
||||||
|
ok("PHP {$phpVer}");
|
||||||
|
|
||||||
|
// Extensions
|
||||||
|
foreach (REQUIREMENTS as $ext) {
|
||||||
|
if (!extension_loaded($ext)) {
|
||||||
|
warn("Estensione PHP '{$ext}' non trovata (alcune funzionalità potrebbero non funzionare)");
|
||||||
|
} else {
|
||||||
|
ok("Estensione PHP '{$ext}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Composer
|
||||||
|
checkCmd('composer');
|
||||||
|
checkCmd('node');
|
||||||
|
checkCmd('npm');
|
||||||
|
|
||||||
|
// ── Step 1: Environment ─────────────────────────
|
||||||
|
println();
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
println(color(' 1/7 — CONFIGURAZIONE AMBIENTE', 'yellow'));
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
|
||||||
|
if (!file_exists('.env')) {
|
||||||
|
copy('.env.example', '.env');
|
||||||
|
ok('.env creato da .env.example');
|
||||||
|
} else {
|
||||||
|
info('.env già esistente, lo aggiorno');
|
||||||
|
}
|
||||||
|
|
||||||
|
// APP_KEY
|
||||||
|
runCapture("{$sudo}php artisan key:generate --force");
|
||||||
|
ok('APP_KEY generata');
|
||||||
|
|
||||||
|
// Config
|
||||||
|
$dotenv = file_get_contents('.env');
|
||||||
|
$replacements = [
|
||||||
|
'/^APP_NAME=.*/m' => 'APP_NAME="' . addslashes($appName) . '"',
|
||||||
|
'/^APP_URL=.*/m' => "APP_URL={$appUrl}",
|
||||||
|
'/^APP_ENV=.*/m' => 'APP_ENV=production',
|
||||||
|
'/^APP_DEBUG=.*/m' => 'APP_DEBUG=false',
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($dbType === 'mysql') {
|
||||||
|
$replacements['/^DB_CONNECTION=.*/m'] = 'DB_CONNECTION=mysql';
|
||||||
|
$replacements['/^DB_HOST=.*/m'] = "DB_HOST={$dbHost}";
|
||||||
|
$replacements['/^DB_PORT=.*/m'] = "DB_PORT={$dbPort}";
|
||||||
|
$replacements['/^DB_DATABASE=.*/m'] = "DB_DATABASE={$dbName}";
|
||||||
|
$replacements['/^DB_USERNAME=.*/m'] = "DB_USERNAME={$dbUser}";
|
||||||
|
$replacements['/^DB_PASSWORD=.*/m'] = "DB_PASSWORD={$dbPass}";
|
||||||
|
} else {
|
||||||
|
$replacements['/^DB_CONNECTION=.*/m'] = 'DB_CONNECTION=sqlite';
|
||||||
|
$replacements['/^DB_HOST=.*/m'] = '# DB_HOST=';
|
||||||
|
$replacements['/^DB_PORT=.*/m'] = '# DB_PORT=';
|
||||||
|
$replacements['/^DB_DATABASE=.*/m'] = 'DB_DATABASE=' . $scriptDir . '/database/database.sqlite';
|
||||||
|
$replacements['/^DB_USERNAME=.*/m'] = '# DB_USERNAME=';
|
||||||
|
$replacements['/^DB_PASSWORD=.*/m'] = '# DB_PASSWORD=';
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($replacements as $pattern => $replacement) {
|
||||||
|
$dotenv = preg_replace($pattern, $replacement, $dotenv);
|
||||||
|
}
|
||||||
|
file_put_contents('.env', $dotenv);
|
||||||
|
ok('.env configurato');
|
||||||
|
|
||||||
|
// ── Step 2: Database ────────────────────────────
|
||||||
|
println();
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
println(color(' 2/7 — DATABASE', 'yellow'));
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
|
||||||
|
if ($dbType === 'mysql') {
|
||||||
|
info("Connessione a MySQL {$dbHost}:{$dbPort}, database '{$dbName}'...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
new PDO(
|
||||||
|
"mysql:host={$dbHost};port={$dbPort};dbname={$dbName}",
|
||||||
|
$dbUser, $dbPass,
|
||||||
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 5]
|
||||||
|
);
|
||||||
|
ok("Accesso a '{$dbName}' come '{$dbUser}' riuscito");
|
||||||
|
} catch (\PDOException $e) {
|
||||||
|
// Diagnostica l'errore
|
||||||
|
$errMsg = $e->getMessage();
|
||||||
|
$diagnosi = match (true) {
|
||||||
|
(bool)preg_match('/1049|Unknown database/', $errMsg) => "Database '{$dbName}' non esiste",
|
||||||
|
(bool)preg_match('/1045|Access denied for user/', $errMsg) => "Utente '{$dbUser}' o password non validi",
|
||||||
|
(bool)preg_match('/1044/', $errMsg) => "Utente '{$dbUser}' non ha accesso al database '{$dbName}'",
|
||||||
|
default => $errMsg,
|
||||||
|
};
|
||||||
|
warn($diagnosi);
|
||||||
|
|
||||||
|
info("Connessione come root per creare/riparare utente e database...");
|
||||||
|
$rootPdo = null;
|
||||||
|
try {
|
||||||
|
$rootPdo = new PDO(
|
||||||
|
"mysql:host={$dbHost};port={$dbPort}",
|
||||||
|
'root', '',
|
||||||
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 5]
|
||||||
|
);
|
||||||
|
} catch (\PDOException) {
|
||||||
|
$rootPass = askRequired('Password utente root MySQL (lascia vuoto se senza password)');
|
||||||
|
try {
|
||||||
|
$rootPdo = new PDO(
|
||||||
|
"mysql:host={$dbHost};port={$dbPort}",
|
||||||
|
'root', $rootPass,
|
||||||
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 5]
|
||||||
|
);
|
||||||
|
} catch (\PDOException $r) {
|
||||||
|
fail("Connessione come root fallita: " . $r->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$safeUser = str_replace(["'", '"', '`', "\0", '\\'], '', $dbUser);
|
||||||
|
$safePass = str_replace(["'", '"', '`', "\0", '\\'], '', $dbPass);
|
||||||
|
$safeName = str_replace(["'", '"', '`', "\0", '\\'], '', $dbName);
|
||||||
|
|
||||||
|
$rootPdo->exec("CREATE DATABASE IF NOT EXISTS `{$safeName}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
|
||||||
|
foreach (['localhost', '%'] as $host) {
|
||||||
|
$rootPdo->exec("CREATE USER IF NOT EXISTS '{$safeUser}'@'{$host}' IDENTIFIED BY '{$safePass}'");
|
||||||
|
$rootPdo->exec("GRANT ALL PRIVILEGES ON `{$safeName}`.* TO '{$safeUser}'@'{$host}'");
|
||||||
|
}
|
||||||
|
$rootPdo->exec("FLUSH PRIVILEGES");
|
||||||
|
ok("Database '{$dbName}' e utente '{$dbUser}' configurati con permessi completi");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$sqlitePath = $scriptDir . '/database/database.sqlite';
|
||||||
|
if (!file_exists($sqlitePath)) {
|
||||||
|
touch($sqlitePath);
|
||||||
|
chmod($sqlitePath, 0664);
|
||||||
|
}
|
||||||
|
ok('SQLite pronto: ' . $sqlitePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Step 3: Composer ────────────────────────────
|
||||||
|
println();
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
println(color(' 3/7 — INSTALLAZIONE DIPENDENZE', 'yellow'));
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
|
||||||
|
run("COMPOSER_ROOT_VERSION=dev-main composer install --no-dev --optimize-autoloader", 'Installazione dipendenze Composer...');
|
||||||
|
ok('Dipendenze PHP installate');
|
||||||
|
|
||||||
|
// ── Step 4: Package discovery ────────────────────
|
||||||
|
println();
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
println(color(' 4/7 — REGISTRAZIONE PACCHETTI', 'yellow'));
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
|
||||||
|
runCapture("{$sudo}php artisan package:discover --ansi 2>/dev/null");
|
||||||
|
ok('Pacchetti registrati');
|
||||||
|
|
||||||
|
// ── Step 5: Schema + Seed ────────────────────────
|
||||||
|
println();
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
println(color(' 5/7 — SCHEMA DATABASE E DATI BASE', 'yellow'));
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
|
||||||
|
$sqlFile = $scriptDir . '/database/install.sql';
|
||||||
|
if (!file_exists($sqlFile)) {
|
||||||
|
fail("File database/install.sql non trovato in {$scriptDir}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dbType === 'mysql') {
|
||||||
|
$importCmd = "mysql -h {$dbHost} -P {$dbPort} -u {$dbUser} -p{$dbPass} {$dbName} < {$sqlFile}";
|
||||||
|
if (!run($importCmd, 'Importazione struttura database e dati base...')) {
|
||||||
|
fail("Importazione SQL fallita. Verifica il file database/install.sql e le credenziali.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// SQLite fallback: usa le migration (install.sql è solo per MySQL)
|
||||||
|
if (!run("{$sudo}php artisan migrate --seed --force", 'Esecuzione migration e seed...')) {
|
||||||
|
fail('Migration fallita. Verifica la configurazione SQLite.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ok('Database popolato con struttura e dati base');
|
||||||
|
|
||||||
|
runCapture("{$sudo}php artisan storage:link --force 2>/dev/null");
|
||||||
|
|
||||||
|
// ── Step 6: Admin user (via PDO — no tinker) ─────
|
||||||
|
println();
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
println(color(' 6/7 — CREAZIONE AMMINISTRATORE', 'yellow'));
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
|
||||||
|
if ($dbType === 'mysql') {
|
||||||
|
if (createAdminUser($dbHost, $dbPort, $dbName, $dbUser, $dbPass, $adminName, $adminEmail, $adminPass)) {
|
||||||
|
ok("Amministratore {$adminEmail} creato con permessi completi");
|
||||||
|
} else {
|
||||||
|
fail("Creazione admin fallita. Verifica i dati inseriti.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// SQLite: usa seeder (l'admin verrà creato da DatabaseSeeder)
|
||||||
|
// Poi aggiorna la password con quella scelta dall'utente
|
||||||
|
runCapture("{$sudo}php artisan tinker --execute=" . escapeshellarg(
|
||||||
|
"\$u = \App\Models\User::first(); " .
|
||||||
|
"if (\$u) { " .
|
||||||
|
"\$u->name = " . var_export($adminName, true) . "; " .
|
||||||
|
"\$u->email = " . var_export($adminEmail, true) . "; " .
|
||||||
|
"\$u->password = bcrypt(" . var_export($adminPass, true) . "); " .
|
||||||
|
"\$u->is_admin = true; " .
|
||||||
|
"\$u->status = 'active'; " .
|
||||||
|
"\$u->permissions = " . var_export([
|
||||||
|
'individui' => 2, 'gruppi' => 2, 'eventi' => 2,
|
||||||
|
'documenti' => 2, 'mailing' => 2, 'viste' => 2,
|
||||||
|
'report' => 2, 'settings' => 2,
|
||||||
|
], true) . "; " .
|
||||||
|
"\$u->role_preset_id = 1; " .
|
||||||
|
"\$u->save(); " .
|
||||||
|
"echo 'OK'; " .
|
||||||
|
"}"
|
||||||
|
) . " 2>/dev/null");
|
||||||
|
ok("Amministratore {$adminEmail} configurato");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all caches after schema + admin creation
|
||||||
|
runCapture("{$sudo}php artisan optimize:clear 2>/dev/null");
|
||||||
|
ok('Cache ottimizzate');
|
||||||
|
|
||||||
|
// ── Step 7: Asset ────────────────────────────────
|
||||||
|
println();
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
println(color(' 7/7 — ASSET FRONTEND', 'yellow'));
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
|
||||||
|
$hasNpm = checkCmd('npm');
|
||||||
|
if ($hasNpm && confirm('Compilare gli asset frontend con npm?')) {
|
||||||
|
run("{$sudo}npm install", 'Installazione dipendenze npm...');
|
||||||
|
run("{$sudo}npm run build", 'Compilazione asset...');
|
||||||
|
ok('Asset compilati');
|
||||||
|
} else {
|
||||||
|
if (!$hasNpm) warn('npm non disponibile — asset non compilati (AdminLTE via CDN)');
|
||||||
|
else info('Asset non compilati (AdminLTE via CDN)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Permissions ──────────────────────────────────
|
||||||
|
if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
|
||||||
|
info('Impostazione permessi...');
|
||||||
|
runCapture("chown -R {$wwwUser}:{$wwwUser} vendor storage bootstrap/cache 2>/dev/null");
|
||||||
|
chmod("{$scriptDir}/storage", 0775);
|
||||||
|
chmod("{$scriptDir}/bootstrap/cache", 0775);
|
||||||
|
ok("Permessi impostati (utente: {$wwwUser})");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Summary ──────────────────────────────────────
|
||||||
|
println();
|
||||||
|
println(color('══════════════════════════════════════════', 'green'));
|
||||||
|
println(color(' INSTALLAZIONE COMPLETATA!', 'green'));
|
||||||
|
println(color('══════════════════════════════════════════', 'green'));
|
||||||
|
println();
|
||||||
|
println(" URL: {$appUrl}");
|
||||||
|
println(" Admin: {$adminEmail}");
|
||||||
|
println(" Cartella: {$scriptDir}");
|
||||||
|
println();
|
||||||
|
warn('Configura Apache con il VirtualHost (vedi Guida → Installazione, Passo 4).');
|
||||||
|
if ($dbType === 'mysql') {
|
||||||
|
warn('Se hai appena creato il database, assicurati che l\'utente abbia privilegi completi.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════
|
||||||
|
// MODE 2: DOCKER
|
||||||
|
// ══════════════════════════════════════════════════════
|
||||||
|
if ($mode === 'Fresh Install via Docker') {
|
||||||
|
println();
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
println(color(' PRE-FLIGHT CHECKS (Docker)', 'yellow'));
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
|
||||||
|
checkCmd('docker');
|
||||||
|
|
||||||
|
println();
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
println(color(' CONFIGURAZIONE DOCKER', 'yellow'));
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
|
||||||
|
$dockerDb = menu('Tipo database per Docker', ['sqlite', 'mysql']);
|
||||||
|
$dockerPort = ask('Porta host (es. 8080)', '8080');
|
||||||
|
|
||||||
|
$composeFile = $dockerDb === 'mysql' ? 'docker-compose.mysql.yml' : 'docker-compose.yml';
|
||||||
|
|
||||||
|
println();
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
println(color(' INSTALLAZIONE DOCKER', 'yellow'));
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
|
||||||
|
if (!run("docker compose -f {$composeFile} build", 'Build immagine Docker...')) {
|
||||||
|
fail('Build fallita');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override compose con variabili d'ambiente
|
||||||
|
$override = [
|
||||||
|
'services:',
|
||||||
|
' glastree:',
|
||||||
|
' environment:',
|
||||||
|
" - APP_NAME={$appName}",
|
||||||
|
" - APP_URL={$appUrl}",
|
||||||
|
' - APP_ENV=production',
|
||||||
|
' - APP_DEBUG=false',
|
||||||
|
];
|
||||||
|
file_put_contents('docker-compose.override.yml', implode(PHP_EOL, $override) . PHP_EOL);
|
||||||
|
|
||||||
|
if (!run(
|
||||||
|
"docker compose -f {$composeFile} -f docker-compose.override.yml up -d",
|
||||||
|
'Avvio container...'
|
||||||
|
)) {
|
||||||
|
@unlink('docker-compose.override.yml');
|
||||||
|
fail('Avvio container fallito');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attendi container pronto
|
||||||
|
info('Attendo che il container sia pronto...');
|
||||||
|
$containerId = runCapture("docker compose -f {$composeFile} ps -q glastree 2>/dev/null");
|
||||||
|
$adminCreated = false;
|
||||||
|
|
||||||
|
if ($containerId !== '') {
|
||||||
|
for ($i = 0; $i < 30; $i++) {
|
||||||
|
$status = runCapture("docker inspect -f '{{.State.Status}}' {$containerId} 2>/dev/null");
|
||||||
|
if ($status === 'running') {
|
||||||
|
$http = runCapture("docker exec {$containerId} sh -c 'wget -q -O- http://localhost/ 2>/dev/null && echo OK'");
|
||||||
|
if (str_contains($http, 'OK')) {
|
||||||
|
info('Container pronto');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sleep(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import schema SQL nel container
|
||||||
|
$sqlFile = $scriptDir . '/database/install.sql';
|
||||||
|
if (file_exists($sqlFile) && $dockerDb === 'mysql') {
|
||||||
|
info('Importazione schema nel container...');
|
||||||
|
runCapture("docker cp {$sqlFile} {$containerId}:/tmp/install.sql 2>/dev/null");
|
||||||
|
$mysqlHost = 'mysql';
|
||||||
|
$mysqlUser = 'glastree';
|
||||||
|
$mysqlPass = 'secret';
|
||||||
|
$mysqlDb = 'glastree';
|
||||||
|
$importResult = runCapture(
|
||||||
|
"docker exec {$containerId} sh -c 'mysql -h {$mysqlHost} -u {$mysqlUser} -p{$mysqlPass} {$mysqlDb} < /tmp/install.sql' 2>&1"
|
||||||
|
);
|
||||||
|
if ($importResult !== '') {
|
||||||
|
warn("Import SQL: {$importResult}");
|
||||||
|
} else {
|
||||||
|
ok('Schema database importato');
|
||||||
|
}
|
||||||
|
} elseif ($dockerDb === 'sqlite') {
|
||||||
|
run("docker exec {$containerId} php artisan migrate --seed --force", 'Migration e seed...');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crea admin
|
||||||
|
if ($dockerDb === 'mysql') {
|
||||||
|
// Script PHP temporaneo con var_export (safe)
|
||||||
|
$adminScriptPath = sys_get_temp_dir() . '/glastree-create-admin-' . uniqid() . '.php';
|
||||||
|
$hash = password_hash($adminPass, PASSWORD_BCRYPT);
|
||||||
|
$perm = json_encode([
|
||||||
|
'individui' => 2, 'gruppi' => 2, 'eventi' => 2,
|
||||||
|
'documenti' => 2, 'mailing' => 2, 'viste' => 2,
|
||||||
|
'report' => 2, 'settings' => 2,
|
||||||
|
]);
|
||||||
|
$nameExp = var_export($adminName, true);
|
||||||
|
$emailExp = var_export($adminEmail, true);
|
||||||
|
$hashExp = var_export($hash, true);
|
||||||
|
$userExp = var_export($mysqlUser, true);
|
||||||
|
$passExp = var_export($mysqlPass, true);
|
||||||
|
$dbExp = var_export($mysqlDb, true);
|
||||||
|
$hostExp = var_export($mysqlHost, true);
|
||||||
|
|
||||||
|
$adminPhp = <<<PHP
|
||||||
|
<?php
|
||||||
|
\$pdo = new PDO("mysql:host=" . {$hostExp} . ";dbname=" . {$dbExp}, {$userExp}, {$passExp}, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||||
|
\$pdo->prepare("DELETE FROM users WHERE email = ?")->execute([{$emailExp}]);
|
||||||
|
\$pdo->prepare("INSERT INTO users (name, email, password, is_admin, status, permissions, role_preset_id, created_at, updated_at) VALUES (?, ?, ?, 1, 'active', ?, 1, NOW(), NOW())")->execute([{$nameExp}, {$emailExp}, {$hashExp}, '{$perm}']);
|
||||||
|
\$uid = \$pdo->lastInsertId();
|
||||||
|
\$pdo->prepare("INSERT INTO model_has_roles (role_id, model_type, model_id) VALUES (1, ?, ?)")->execute(['App\\\\Models\\\\User', \$uid]);
|
||||||
|
echo "OK:" . \$uid;
|
||||||
|
PHP;
|
||||||
|
file_put_contents($adminScriptPath, $adminPhp);
|
||||||
|
runCapture("docker cp {$adminScriptPath} {$containerId}:/tmp/create_admin.php 2>/dev/null");
|
||||||
|
$adminResult = runCapture("docker exec {$containerId} php /tmp/create_admin.php 2>/dev/null");
|
||||||
|
@unlink($adminScriptPath);
|
||||||
|
} else {
|
||||||
|
// SQLite: usa tinker via docker exec
|
||||||
|
$adminResult = runCapture("docker exec {$containerId} php artisan tinker --execute=" . escapeshellarg(
|
||||||
|
"\$u = \App\Models\User::first(); " .
|
||||||
|
"if (\$u) { " .
|
||||||
|
"\$u->name = " . var_export($adminName, true) . "; " .
|
||||||
|
"\$u->email = " . var_export($adminEmail, true) . "; " .
|
||||||
|
"\$u->password = bcrypt(" . var_export($adminPass, true) . "); " .
|
||||||
|
"\$u->is_admin = true; " .
|
||||||
|
"\$u->status = 'active'; " .
|
||||||
|
"\$u->permissions = " . var_export([
|
||||||
|
'individui' => 2, 'gruppi' => 2, 'eventi' => 2,
|
||||||
|
'documenti' => 2, 'mailing' => 2, 'viste' => 2,
|
||||||
|
'report' => 2, 'settings' => 2,
|
||||||
|
], true) . "; " .
|
||||||
|
"\$u->role_preset_id = 1; " .
|
||||||
|
"\$u->save(); " .
|
||||||
|
"echo 'OK'; " .
|
||||||
|
"}"
|
||||||
|
) . " 2>/dev/null");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($adminResult ?? '', 'OK')) {
|
||||||
|
ok("Amministratore {$adminEmail} creato nel container");
|
||||||
|
$adminCreated = true;
|
||||||
|
} else {
|
||||||
|
warn("Creazione admin fallita — output: " . (($adminResult ?? '') ?: 'nessun output'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@unlink('docker-compose.override.yml');
|
||||||
|
|
||||||
|
println();
|
||||||
|
println(color('══════════════════════════════════════════', 'green'));
|
||||||
|
println(color(' INSTALLAZIONE DOCKER COMPLETATA!', 'green'));
|
||||||
|
println(color('══════════════════════════════════════════', 'green'));
|
||||||
|
println();
|
||||||
|
println(" URL: http://localhost:{$dockerPort}");
|
||||||
|
println(" Admin: {$adminEmail}");
|
||||||
|
println(" Container: {$containerId}");
|
||||||
|
println();
|
||||||
|
warn('Per URL pubblici, imposta APP_URL nelle environment del container.');
|
||||||
|
warn('Dati persistenti: volumi Docker (glastree_storage, glastree_db o mysql_data).');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════
|
||||||
|
// MODE 3: RESTORE DA BACKUP
|
||||||
|
// ══════════════════════════════════════════════════════
|
||||||
|
if ($mode === 'Restore da Backup (Apache)') {
|
||||||
|
println();
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
println(color(' PRE-FLIGHT CHECKS (Restore)', 'yellow'));
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
|
||||||
|
checkCmd('php');
|
||||||
|
checkCmd('composer');
|
||||||
|
checkCmd('unzip');
|
||||||
|
|
||||||
|
if ($dbType === 'mysql') {
|
||||||
|
checkCmd('mysql');
|
||||||
|
}
|
||||||
|
|
||||||
|
$backupZip = askRequired('Percorso del file ZIP di backup');
|
||||||
|
if (!file_exists($backupZip)) {
|
||||||
|
fail("File non trovato: {$backupZip}");
|
||||||
|
}
|
||||||
|
|
||||||
|
println();
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
println(color(' RESTORE DA BACKUP', 'yellow'));
|
||||||
|
println(color('══════════════════════════════════════════', 'yellow'));
|
||||||
|
|
||||||
|
// Extract
|
||||||
|
$tmpDir = sys_get_temp_dir() . '/glastree-restore-' . uniqid();
|
||||||
|
mkdir($tmpDir, 0700, true);
|
||||||
|
info("Estrazione backup in {$tmpDir}...");
|
||||||
|
|
||||||
|
if (!run("unzip -qo {$backupZip} -d {$tmpDir}", null)) {
|
||||||
|
// pulisci tmp dir prima di fallire
|
||||||
|
runCapture("rm -rf {$tmpDir}");
|
||||||
|
fail('Estrazione backup fallita');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment
|
||||||
|
if (!file_exists('.env')) {
|
||||||
|
copy('.env.example', '.env');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore .env from backup preserving DB credentials
|
||||||
|
if (file_exists("{$tmpDir}/.env")) {
|
||||||
|
$currentEnv = file_get_contents('.env');
|
||||||
|
$backupEnv = file_get_contents("{$tmpDir}/.env");
|
||||||
|
|
||||||
|
// Save current DB_* values
|
||||||
|
$currentDb = [];
|
||||||
|
foreach (['DB_CONNECTION', 'DB_HOST', 'DB_PORT', 'DB_DATABASE', 'DB_USERNAME', 'DB_PASSWORD'] as $key) {
|
||||||
|
if (preg_match("/^{$key}=(.*)$/m", $currentEnv, $m)) {
|
||||||
|
$currentDb[$key] = $m[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use backup .env as base
|
||||||
|
file_put_contents('.env', $backupEnv);
|
||||||
|
|
||||||
|
// Restore current DB credentials
|
||||||
|
foreach ($currentDb as $key => $val) {
|
||||||
|
$dotenv = file_get_contents('.env');
|
||||||
|
$dotenv = preg_replace("/^{$key}=.*/m", "{$key}={$val}", $dotenv);
|
||||||
|
file_put_contents('.env', $dotenv);
|
||||||
|
}
|
||||||
|
|
||||||
|
ok('.env ripristinato dal backup (credenziali DB preservate)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update APP_* config
|
||||||
|
$dotenv = file_get_contents('.env');
|
||||||
|
$dotenv = preg_replace('/^APP_NAME=.*/m', 'APP_NAME="' . addslashes($appName) . '"', $dotenv);
|
||||||
|
$dotenv = preg_replace('/^APP_URL=.*/m', "APP_URL={$appUrl}", $dotenv);
|
||||||
|
$dotenv = preg_replace('/^APP_ENV=.*/m', 'APP_ENV=production', $dotenv);
|
||||||
|
$dotenv = preg_replace('/^APP_DEBUG=.*/m', 'APP_DEBUG=false', $dotenv);
|
||||||
|
file_put_contents('.env', $dotenv);
|
||||||
|
|
||||||
|
// Generate APP_KEY
|
||||||
|
runCapture("{$sudo}php artisan key:generate --force");
|
||||||
|
ok('APP_KEY rigenerata');
|
||||||
|
|
||||||
|
// Import database
|
||||||
|
if ($dbType === 'mysql') {
|
||||||
|
$sqlFile = "{$tmpDir}/database.sql";
|
||||||
|
if (file_exists($sqlFile)) {
|
||||||
|
info("Importazione database MySQL: {$sqlFile}");
|
||||||
|
$cmd = "mysql -h {$dbHost} -P {$dbPort} -u {$dbUser} -p{$dbPass} {$dbName} < {$sqlFile}";
|
||||||
|
if (run($cmd, null)) {
|
||||||
|
ok('Database importato');
|
||||||
|
} else {
|
||||||
|
warn('Import database fallito — importa manualmente');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn('database.sql non trovato nel backup');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn('Restore automatico supportato solo con MySQL. Per SQLite ripristina manualmente i file.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore storage
|
||||||
|
$storageBackup = "{$tmpDir}/storage";
|
||||||
|
if (is_dir($storageBackup)) {
|
||||||
|
info('Ripristino file storage...');
|
||||||
|
runCapture("cp -r {$storageBackup}/* {$scriptDir}/storage/ 2>/dev/null");
|
||||||
|
ok('Storage ripristinato');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Composer
|
||||||
|
run("COMPOSER_ROOT_VERSION=dev-main composer install --no-dev --optimize-autoloader", 'Installazione dipendenze Composer...');
|
||||||
|
runCapture("{$sudo}php artisan package:discover --ansi 2>/dev/null");
|
||||||
|
|
||||||
|
// Storage link + cache
|
||||||
|
runCapture("{$sudo}php artisan storage:link --force 2>/dev/null");
|
||||||
|
runCapture("{$sudo}php artisan optimize:clear");
|
||||||
|
|
||||||
|
// Permissions
|
||||||
|
if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
|
||||||
|
runCapture("chown -R {$wwwUser}:{$wwwUser} vendor storage bootstrap/cache 2>/dev/null");
|
||||||
|
chmod("{$scriptDir}/storage", 0775);
|
||||||
|
chmod("{$scriptDir}/bootstrap/cache", 0775);
|
||||||
|
ok("Permessi impostati (utente: {$wwwUser})");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create admin if not exists (via PDO)
|
||||||
|
if ($dbType === 'mysql') {
|
||||||
|
$exists = false;
|
||||||
|
try {
|
||||||
|
$pdo = new PDO("mysql:host={$dbHost};port={$dbPort};dbname={$dbName}", $dbUser, $dbPass);
|
||||||
|
$stmt = $pdo->prepare('SELECT COUNT(*) FROM users WHERE email = ?');
|
||||||
|
$stmt->execute([$adminEmail]);
|
||||||
|
$exists = (int) $stmt->fetchColumn() > 0;
|
||||||
|
} catch (\PDOException) {
|
||||||
|
// Ignora — tenteremo la creazione
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($exists) {
|
||||||
|
ok("Amministratore {$adminEmail} già esistente");
|
||||||
|
} elseif (createAdminUser($dbHost, $dbPort, $dbName, $dbUser, $dbPass, $adminName, $adminEmail, $adminPass)) {
|
||||||
|
ok("Amministratore {$adminEmail} creato");
|
||||||
|
} else {
|
||||||
|
warn("Creazione admin fallita — crealo manualmente dal pannello admin");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// SQLite fallback: usa tinker
|
||||||
|
$result = runCapture("{$sudo}php artisan tinker --execute=" . escapeshellarg(
|
||||||
|
"\$e = \App\Models\User::where('email', " . var_export($adminEmail, true) . ")->exists(); " .
|
||||||
|
"if (!\$e) { " .
|
||||||
|
"\$u = new \App\Models\User; " .
|
||||||
|
"\$u->name = " . var_export($adminName, true) . "; " .
|
||||||
|
"\$u->email = " . var_export($adminEmail, true) . "; " .
|
||||||
|
"\$u->password = bcrypt(" . var_export($adminPass, true) . "); " .
|
||||||
|
"\$u->is_admin = true; " .
|
||||||
|
"\$u->status = 'active'; " .
|
||||||
|
"\$u->permissions = " . var_export([
|
||||||
|
'individui' => 2, 'gruppi' => 2, 'eventi' => 2,
|
||||||
|
'documenti' => 2, 'mailing' => 2, 'viste' => 2,
|
||||||
|
'report' => 2, 'settings' => 2,
|
||||||
|
], true) . "; " .
|
||||||
|
"\$u->role_preset_id = 1; " .
|
||||||
|
"\$u->save(); " .
|
||||||
|
"echo 'OK'; " .
|
||||||
|
"} else { echo 'EXISTS'; }"
|
||||||
|
) . " 2>/dev/null");
|
||||||
|
if (str_contains($result, 'OK')) {
|
||||||
|
ok("Amministratore {$adminEmail} creato");
|
||||||
|
} elseif (str_contains($result, 'EXISTS')) {
|
||||||
|
ok("Amministratore {$adminEmail} già esistente");
|
||||||
|
} else {
|
||||||
|
warn("Creazione admin fallita (output: " . ($result ?: 'vuoto') . ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
runCapture("rm -rf {$tmpDir}");
|
||||||
|
|
||||||
|
println();
|
||||||
|
println(color('══════════════════════════════════════════', 'green'));
|
||||||
|
println(color(' RESTORE COMPLETATO!', 'green'));
|
||||||
|
println(color('══════════════════════════════════════════', 'green'));
|
||||||
|
println();
|
||||||
|
println(" URL: {$appUrl}");
|
||||||
|
println(" Admin: {$adminEmail}");
|
||||||
|
println(" Cartella: {$scriptDir}");
|
||||||
|
println();
|
||||||
|
warn('Verifica che APP_KEY non abbia invalidato dati criptati (token OAuth, password IMAP).');
|
||||||
|
warn('Se necessario, riconfigura SMTP e Google Drive dalle Impostazioni.');
|
||||||
|
}
|
||||||
|
|
||||||
|
println();
|
||||||
@@ -0,0 +1,384 @@
|
|||||||
|
class ColumnManager {
|
||||||
|
constructor(tableId, options = {}) {
|
||||||
|
this.tableId = tableId;
|
||||||
|
this.table = document.getElementById(tableId);
|
||||||
|
if (!this.table) return;
|
||||||
|
|
||||||
|
this.options = Object.assign({
|
||||||
|
entityType: 'individui',
|
||||||
|
storageKey: null,
|
||||||
|
resizable: true,
|
||||||
|
reorderable: true,
|
||||||
|
persistWidths: true,
|
||||||
|
persistOrder: true,
|
||||||
|
}, options);
|
||||||
|
|
||||||
|
this.columns = [];
|
||||||
|
this.columnWidths = {};
|
||||||
|
this.columnOrder = [];
|
||||||
|
this.dragState = null;
|
||||||
|
this.resizeState = null;
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.loadState();
|
||||||
|
this.initColumns();
|
||||||
|
if (this.options.resizable) this.initResize();
|
||||||
|
if (this.options.reorderable) this.initReorder();
|
||||||
|
this.applyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
initColumns() {
|
||||||
|
const ths = this.table.querySelectorAll('thead th');
|
||||||
|
ths.forEach((th, idx) => {
|
||||||
|
const colKey = th.dataset.column || `col_${idx}`;
|
||||||
|
const col = {
|
||||||
|
index: idx,
|
||||||
|
key: colKey,
|
||||||
|
element: th,
|
||||||
|
width: th.style.width || '',
|
||||||
|
visible: th.style.display !== 'none',
|
||||||
|
label: th.textContent.trim(),
|
||||||
|
};
|
||||||
|
this.columns.push(col);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadState() {
|
||||||
|
if (this.options.storageKey) {
|
||||||
|
try {
|
||||||
|
const saved = sessionStorage.getItem(this.options.storageKey);
|
||||||
|
if (saved) {
|
||||||
|
const data = JSON.parse(saved);
|
||||||
|
this.columnWidths = data.widths || {};
|
||||||
|
this.columnOrder = data.order || [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
const vistaEl = document.getElementById('vista-data');
|
||||||
|
if (vistaEl && vistaEl.textContent && vistaEl.textContent !== 'null') {
|
||||||
|
try {
|
||||||
|
const vista = JSON.parse(vistaEl.textContent);
|
||||||
|
this.columnWidths = vista.colonne_larghezze || {};
|
||||||
|
this.columnOrder = vista.colonne_ordine || [];
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
persistState() {
|
||||||
|
if (!this.options.storageKey) return;
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(this.options.storageKey, JSON.stringify({
|
||||||
|
widths: this.columnWidths,
|
||||||
|
order: this.columnOrder,
|
||||||
|
}));
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyState() {
|
||||||
|
if (this.columnOrder.length > 0) {
|
||||||
|
this.reorderColumns(this.columnOrder, false);
|
||||||
|
}
|
||||||
|
if (Object.keys(this.columnWidths).length > 0) {
|
||||||
|
this.applyWidths();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyWidths() {
|
||||||
|
this.columns.forEach(col => {
|
||||||
|
const w = this.columnWidths[col.key];
|
||||||
|
if (w) {
|
||||||
|
col.element.style.width = w;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initResize() {
|
||||||
|
this.table.querySelectorAll('thead th').forEach(th => {
|
||||||
|
const handle = document.createElement('div');
|
||||||
|
handle.className = 'col-resize-handle';
|
||||||
|
handle.style.cssText = 'position:absolute;right:0;top:0;bottom:0;width:5px;cursor:col-resize;z-index:10;';
|
||||||
|
th.style.position = 'relative';
|
||||||
|
th.appendChild(handle);
|
||||||
|
|
||||||
|
handle.addEventListener('mousedown', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const thEl = th;
|
||||||
|
const startX = e.clientX;
|
||||||
|
const startWidth = thEl.offsetWidth;
|
||||||
|
|
||||||
|
const onMouseMove = (ev) => {
|
||||||
|
const diff = ev.clientX - startX;
|
||||||
|
const newWidth = Math.max(30, startWidth + diff);
|
||||||
|
thEl.style.width = newWidth + 'px';
|
||||||
|
const colKey = thEl.dataset.column;
|
||||||
|
if (colKey) {
|
||||||
|
this.columnWidths[colKey] = newWidth + 'px';
|
||||||
|
this.persistState();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseUp = () => {
|
||||||
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
|
document.removeEventListener('mouseup', onMouseUp);
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
document.body.style.userSelect = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
|
document.addEventListener('mouseup', onMouseUp);
|
||||||
|
document.body.style.cursor = 'col-resize';
|
||||||
|
document.body.style.userSelect = 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initReorder() {
|
||||||
|
const thead = this.table.querySelector('thead');
|
||||||
|
if (!thead) return;
|
||||||
|
|
||||||
|
let dragCol = null;
|
||||||
|
let dragIndex = -1;
|
||||||
|
let placeholder = null;
|
||||||
|
|
||||||
|
thead.querySelectorAll('th').forEach(th => {
|
||||||
|
if (th.querySelector('.col-resize-handle')) return;
|
||||||
|
th.draggable = true;
|
||||||
|
|
||||||
|
th.addEventListener('dragstart', (e) => {
|
||||||
|
dragCol = th;
|
||||||
|
dragIndex = Array.from(thead.querySelectorAll('th')).indexOf(th);
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
e.dataTransfer.setData('text/plain', '');
|
||||||
|
th.classList.add('dragging');
|
||||||
|
});
|
||||||
|
|
||||||
|
th.addEventListener('dragend', () => {
|
||||||
|
th.classList.remove('dragging');
|
||||||
|
if (placeholder && placeholder.parentNode) {
|
||||||
|
placeholder.parentNode.removeChild(placeholder);
|
||||||
|
}
|
||||||
|
dragCol = null;
|
||||||
|
dragIndex = -1;
|
||||||
|
placeholder = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
th.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
if (!dragCol || dragCol === th) return;
|
||||||
|
|
||||||
|
if (!placeholder) {
|
||||||
|
placeholder = document.createElement('th');
|
||||||
|
placeholder.style.cssText = 'border:2px dashed #007bff;background:rgba(0,123,255,0.1);width:0;padding:0;';
|
||||||
|
dragCol.parentNode.insertBefore(placeholder, dragCol);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = th.getBoundingClientRect();
|
||||||
|
const midX = rect.left + rect.width / 2;
|
||||||
|
const parent = th.parentNode;
|
||||||
|
|
||||||
|
if (e.clientX < midX) {
|
||||||
|
parent.insertBefore(placeholder, th);
|
||||||
|
} else {
|
||||||
|
parent.insertBefore(placeholder, th.nextSibling);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
th.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!dragCol || dragCol === th) return;
|
||||||
|
|
||||||
|
const parent = th.parentNode;
|
||||||
|
const allTh = Array.from(parent.querySelectorAll('th')).filter(el => el !== placeholder);
|
||||||
|
|
||||||
|
const fromIdx = allTh.indexOf(dragCol);
|
||||||
|
let toIdx = allTh.indexOf(th);
|
||||||
|
|
||||||
|
if (placeholder) {
|
||||||
|
toIdx = Array.from(parent.children).indexOf(placeholder);
|
||||||
|
if (placeholder.parentNode) placeholder.parentNode.removeChild(placeholder);
|
||||||
|
placeholder = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromIdx < 0) return;
|
||||||
|
|
||||||
|
const actualRows = this.table.querySelectorAll('tr');
|
||||||
|
actualRows.forEach(row => {
|
||||||
|
const cells = row.querySelectorAll('td, th');
|
||||||
|
if (cells[fromIdx] && cells[toIdx]) {
|
||||||
|
if (toIdx > fromIdx) {
|
||||||
|
row.insertBefore(cells[fromIdx], cells[toIdx].nextSibling);
|
||||||
|
} else {
|
||||||
|
row.insertBefore(cells[fromIdx], cells[toIdx]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateColumnOrder();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateColumnOrder() {
|
||||||
|
const ths = this.table.querySelectorAll('thead th');
|
||||||
|
this.columnOrder = [];
|
||||||
|
const keysInOrder = [];
|
||||||
|
ths.forEach((th, idx) => {
|
||||||
|
const colKey = th.dataset.column || `col_${idx}`;
|
||||||
|
keysInOrder.push(colKey);
|
||||||
|
this.columnOrder.push(colKey);
|
||||||
|
});
|
||||||
|
this.columns.sort((a, b) => keysInOrder.indexOf(a.key) - keysInOrder.indexOf(b.key));
|
||||||
|
this.columns.forEach((col, idx) => col.index = idx);
|
||||||
|
this.persistState();
|
||||||
|
}
|
||||||
|
|
||||||
|
reorderColumns(keyOrder, persist = true) {
|
||||||
|
if (!keyOrder || keyOrder.length === 0) return;
|
||||||
|
const thead = this.table.querySelector('thead');
|
||||||
|
if (!thead) return;
|
||||||
|
|
||||||
|
const rows = this.table.querySelectorAll('tr');
|
||||||
|
const keyToThIndex = {};
|
||||||
|
const allTh = Array.from(thead.querySelectorAll('th'));
|
||||||
|
|
||||||
|
allTh.forEach((th, idx) => {
|
||||||
|
const key = th.dataset.column || `col_${idx}`;
|
||||||
|
keyToThIndex[key] = idx;
|
||||||
|
});
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
const cells = row.querySelectorAll('td, th');
|
||||||
|
const reordered = [];
|
||||||
|
let lastRef = null;
|
||||||
|
|
||||||
|
keyOrder.forEach(key => {
|
||||||
|
const srcIdx = keyToThIndex[key];
|
||||||
|
if (srcIdx !== undefined && cells[srcIdx]) {
|
||||||
|
reordered.push(cells[srcIdx]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
reordered.forEach(cell => {
|
||||||
|
if (lastRef) {
|
||||||
|
row.insertBefore(cell, lastRef.nextSibling);
|
||||||
|
} else {
|
||||||
|
row.insertBefore(cell, row.firstChild);
|
||||||
|
}
|
||||||
|
lastRef = cell;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.columnOrder = [...keyOrder];
|
||||||
|
this.columns.sort((a, b) => keyOrder.indexOf(a.key) - keyOrder.indexOf(b.key));
|
||||||
|
this.columns.forEach((col, idx) => col.index = idx);
|
||||||
|
|
||||||
|
if (persist) this.persistState();
|
||||||
|
}
|
||||||
|
|
||||||
|
setVisible(columnKey, visible) {
|
||||||
|
const th = this.table.querySelector(`th[data-column="${columnKey}"]`);
|
||||||
|
if (th) th.style.display = visible ? '' : 'none';
|
||||||
|
|
||||||
|
const colIdx = this.columns.findIndex(c => c.key === columnKey);
|
||||||
|
if (colIdx >= 0) {
|
||||||
|
this.columns[colIdx].visible = visible;
|
||||||
|
const rows = this.table.querySelectorAll('tbody tr, thead tr');
|
||||||
|
rows.forEach(row => {
|
||||||
|
const cells = row.querySelectorAll('td, th');
|
||||||
|
if (cells[colIdx]) {
|
||||||
|
cells[colIdx].style.display = visible ? '' : 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getColumnWidths() {
|
||||||
|
return { ...this.columnWidths };
|
||||||
|
}
|
||||||
|
|
||||||
|
getColumnOrder() {
|
||||||
|
return [...this.columnOrder];
|
||||||
|
}
|
||||||
|
|
||||||
|
getVisibleColumns() {
|
||||||
|
return this.columns.filter(c => c.visible).map(c => c.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
getColumnMetadata() {
|
||||||
|
return {
|
||||||
|
larghezze: this.getColumnWidths(),
|
||||||
|
ordine: this.getColumnOrder(),
|
||||||
|
visibili: this.getVisibleColumns(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
printWithSettings() {
|
||||||
|
const printWindow = window.open('', '_blank');
|
||||||
|
if (!printWindow) return;
|
||||||
|
|
||||||
|
const styles = Array.from(document.styleSheets)
|
||||||
|
.filter(sheet => {
|
||||||
|
try { return sheet.cssRules; } catch (e) { return false; }
|
||||||
|
})
|
||||||
|
.map(sheet => Array.from(sheet.cssRules).map(rule => rule.cssText).join('\n'))
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
const printStyles = `
|
||||||
|
@media print {
|
||||||
|
body { font-family: 'DejaVu Sans', Arial, sans-serif; font-size: 10pt; }
|
||||||
|
.no-print { display: none !important; }
|
||||||
|
.card { border: none !important; box-shadow: none !important; }
|
||||||
|
.table { border-collapse: collapse; width: 100%; }
|
||||||
|
.table th { background: #f5f5f5 !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||||
|
.table th, .table td { border: 1px solid #ddd; padding: 4px 6px; }
|
||||||
|
a { text-decoration: none; color: #000; }
|
||||||
|
.badge { border: 1px solid #999; padding: 1px 4px; font-size: 8pt; }
|
||||||
|
.pagination, .card-header .card-tools { display: none !important; }
|
||||||
|
.page-break { page-break-before: always; }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const title = document.title || 'Stampa';
|
||||||
|
const pageTitle = document.querySelector('.page_title, .card-title h1, h1')?.textContent?.trim() || title;
|
||||||
|
|
||||||
|
let content = '';
|
||||||
|
const printArea = this.table?.cloneNode(true);
|
||||||
|
if (printArea) {
|
||||||
|
printArea.querySelectorAll('.col-resize-handle').forEach(h => h.remove());
|
||||||
|
content = printArea.outerHTML;
|
||||||
|
} else {
|
||||||
|
content = document.getElementById(this.tableId)?.outerHTML || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
printWindow.document.write(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>${title}</title>
|
||||||
|
<style>${styles}${printStyles}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="no-print" style="text-align:right;margin-bottom:10px;">
|
||||||
|
<button onclick="window.print()" style="padding:6px 16px;font-size:12pt;">🖨️ Stampa</button>
|
||||||
|
<button onclick="window.close()" style="padding:6px 16px;font-size:12pt;">✖ Chiudi</button>
|
||||||
|
</div>
|
||||||
|
<h2 style="margin-bottom:16px;">${pageTitle}</h2>
|
||||||
|
${content}
|
||||||
|
<p style="text-align:center;margin-top:16px;color:#999;font-size:8pt;">
|
||||||
|
Stampato il ${new Date().toLocaleDateString('it-IT')} — ${document.location.href}
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
printWindow.document.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.ColumnManager = ColumnManager;
|
||||||
@@ -14,9 +14,14 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3 class="card-title">Attività di Sistema</h3>
|
<h3 class="card-title">Attività di Sistema</h3>
|
||||||
|
<div class="card-tools">
|
||||||
|
<button type="button" class="btn btn-danger btn-sm" onclick="deleteLogs()">
|
||||||
|
<i class="fas fa-trash mr-1"></i> Cancella Log
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="GET" class="mb-3">
|
<form method="GET" class="mb-3" id="filter-form">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<select name="user_id" class="form-control">
|
<select name="user_id" class="form-control">
|
||||||
@@ -95,3 +100,58 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
|
@section('scripts')
|
||||||
|
<script>
|
||||||
|
function deleteLogs() {
|
||||||
|
var filterForm = document.getElementById('filter-form');
|
||||||
|
var params = new URLSearchParams(new FormData(filterForm));
|
||||||
|
var filterDescription = [];
|
||||||
|
|
||||||
|
var userFilter = params.get('user_id');
|
||||||
|
var moduleFilter = params.get('module');
|
||||||
|
var actionFilter = params.get('action');
|
||||||
|
var fromFilter = params.get('from');
|
||||||
|
var toFilter = params.get('to');
|
||||||
|
|
||||||
|
if (userFilter) filterDescription.push('utente=' + userFilter);
|
||||||
|
if (moduleFilter) filterDescription.push('modulo=' + moduleFilter);
|
||||||
|
if (actionFilter) filterDescription.push('azione=' + actionFilter);
|
||||||
|
if (fromFilter) filterDescription.push('dal=' + fromFilter);
|
||||||
|
if (toFilter) filterDescription.push('al=' + toFilter);
|
||||||
|
|
||||||
|
var message = filterDescription.length > 0
|
||||||
|
? 'Eliminare i log filtrati? (' + filterDescription.join(', ') + ')'
|
||||||
|
: 'Eliminare TUTTI i log?';
|
||||||
|
|
||||||
|
if (!confirm(message)) return;
|
||||||
|
|
||||||
|
var form = document.createElement('form');
|
||||||
|
form.method = 'POST';
|
||||||
|
form.action = '/admin/activity-logs';
|
||||||
|
form.style.display = 'none';
|
||||||
|
|
||||||
|
var csrf = document.createElement('input');
|
||||||
|
csrf.name = '_token';
|
||||||
|
csrf.value = '{{ csrf_token() }}';
|
||||||
|
form.appendChild(csrf);
|
||||||
|
|
||||||
|
var method = document.createElement('input');
|
||||||
|
method.name = '_method';
|
||||||
|
method.value = 'DELETE';
|
||||||
|
form.appendChild(method);
|
||||||
|
|
||||||
|
filterForm.querySelectorAll('select, input').forEach(function(el) {
|
||||||
|
if (el.name && el.value) {
|
||||||
|
var input = document.createElement('input');
|
||||||
|
input.name = el.name;
|
||||||
|
input.value = el.value;
|
||||||
|
form.appendChild(input);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(form);
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
@@ -248,6 +248,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="custom-control custom-switch">
|
<div class="custom-control custom-switch">
|
||||||
|
<input type="hidden" name="is_active" value="0">
|
||||||
<input type="checkbox" class="custom-control-input" id="is_active"
|
<input type="checkbox" class="custom-control-input" id="is_active"
|
||||||
name="is_active" value="1" {{ ($settings->is_active ?? false) ? 'checked' : '' }}>
|
name="is_active" value="1" {{ ($settings->is_active ?? false) ? 'checked' : '' }}>
|
||||||
<label class="custom-control-label" for="is_active">Account Email Attivo</label>
|
<label class="custom-control-label" for="is_active">Account Email Attivo</label>
|
||||||
@@ -265,6 +266,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="custom-control custom-switch">
|
<div class="custom-control custom-switch">
|
||||||
|
<input type="hidden" name="signature_enabled" value="0">
|
||||||
<input type="checkbox" class="custom-control-input" id="signature_enabled"
|
<input type="checkbox" class="custom-control-input" id="signature_enabled"
|
||||||
name="signature_enabled" value="1" {{ ($settings->signature_enabled ?? true) ? 'checked' : '' }}>
|
name="signature_enabled" value="1" {{ ($settings->signature_enabled ?? true) ? 'checked' : '' }}>
|
||||||
<label class="custom-control-label" for="signature_enabled">Attiva firma automatica</label>
|
<label class="custom-control-label" for="signature_enabled">Attiva firma automatica</label>
|
||||||
@@ -377,7 +379,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-footer">
|
<div class="card-footer d-flex flex-wrap align-items-center gap-2">
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
<i class="fas fa-save mr-1"></i> Salva Impostazioni
|
<i class="fas fa-save mr-1"></i> Salva Impostazioni
|
||||||
</button>
|
</button>
|
||||||
@@ -392,6 +394,15 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end mt-2">
|
||||||
|
<form action="{{ route('impostazioni.email.destroy') }}" method="POST" onsubmit="return confirm('Eliminare definitivamente la configurazione email? Tutti i dati di connessione verranno rimossi.')">
|
||||||
|
@csrf @method('DELETE')
|
||||||
|
<button type="submit" class="btn btn-danger btn-sm">
|
||||||
|
<i class="fas fa-trash mr-1"></i> Elimina Configurazione
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
@@ -521,7 +532,7 @@ function editSender(id) {
|
|||||||
const sender = senderAccounts.find(s => s.id === id);
|
const sender = senderAccounts.find(s => s.id === id);
|
||||||
if (!sender) return;
|
if (!sender) return;
|
||||||
|
|
||||||
document.getElementById('senderForm').action = '{{ route('impostazioni.sender.update', '') }}/' + id;
|
document.getElementById('senderForm').action = '{{ route('impostazioni.sender.update', '__ID__') }}'.replace('__ID__', id);
|
||||||
document.getElementById('senderMethod').value = 'PUT';
|
document.getElementById('senderMethod').value = 'PUT';
|
||||||
document.getElementById('senderModalTitle').textContent = 'Modifica Mittente - ' + sender.email_address;
|
document.getElementById('senderModalTitle').textContent = 'Modifica Mittente - ' + sender.email_address;
|
||||||
document.getElementById('senderId').value = sender.id;
|
document.getElementById('senderId').value = sender.id;
|
||||||
@@ -551,7 +562,7 @@ function testSenderSmtp(id) {
|
|||||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '{{ route('impostazioni.sender.test', '') }}/' + id,
|
url: '{{ route('impostazioni.sender.test', '__ID__') }}'.replace('__ID__', id),
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
|
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
|
||||||
data: { email: email },
|
data: { email: email },
|
||||||
@@ -628,9 +639,11 @@ function testSmtp() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var quillEditorInstance = null;
|
||||||
|
|
||||||
function createEditor() {
|
function createEditor() {
|
||||||
var container = document.querySelector('#signature-editor');
|
var container = document.querySelector('#signature-editor');
|
||||||
if (!container || container.querySelector('.ql-toolbar')) return;
|
if (!container || quillEditorInstance) return;
|
||||||
|
|
||||||
var quill = new Quill('#signature-editor', {
|
var quill = new Quill('#signature-editor', {
|
||||||
theme: 'snow',
|
theme: 'snow',
|
||||||
@@ -679,10 +692,12 @@ function createEditor() {
|
|||||||
signatureInput.value = quill.root.innerHTML;
|
signatureInput.value = quill.root.innerHTML;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
quillEditorInstance = quill;
|
||||||
}
|
}
|
||||||
|
|
||||||
function initSignatureEditor() {
|
function initSignatureEditor() {
|
||||||
if (document.querySelector('#signature-editor .ql-toolbar')) return;
|
if (quillEditorInstance) return;
|
||||||
|
|
||||||
if (typeof Quill === 'undefined') {
|
if (typeof Quill === 'undefined') {
|
||||||
var script = document.createElement('script');
|
var script = document.createElement('script');
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ $appOrgName = AppSetting::getOrgName() ?? '';
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
<div class="icheck-primary">
|
<div class="icheck-primary">
|
||||||
<input type="checkbox" id="remember">
|
<input type="checkbox" id="remember" name="remember">
|
||||||
<label for="remember">Ricordami</label>
|
<label for="remember">Ricordami</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -103,6 +103,42 @@
|
|||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
@php
|
@php
|
||||||
$canWriteDocumenti = Auth::user()->canManage('documenti');
|
$canWriteDocumenti = Auth::user()->canManage('documenti');
|
||||||
$canDeleteDocumenti = Auth::user()->canDelete('documenti');
|
$canDeleteDocumenti = Auth::user()->canDelete('documenti');
|
||||||
|
$vistaDefaultJson = isset($vista) ? $vista->toJson() : 'null';
|
||||||
|
$visibleColumnsJson = json_encode($visibleColumns ?? []);
|
||||||
|
$tableColumnsJson = json_encode($tableColumns ?? []);
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
@section('css')
|
@section('css')
|
||||||
@@ -123,6 +126,8 @@ $canDeleteDocumenti = Auth::user()->canDelete('documenti');
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@include('partials._tag-filter-bar', ['filterTags' => $allTags])
|
||||||
|
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
{{-- Toolbar azioni massive --}}
|
{{-- Toolbar azioni massive --}}
|
||||||
<form id="massForm" method="POST">
|
<form id="massForm" method="POST">
|
||||||
@@ -144,6 +149,9 @@ $canDeleteDocumenti = Auth::user()->canDelete('documenti');
|
|||||||
<button type="button" class="btn btn-sm btn-secondary" onclick="$('#massMoveModal').modal('show')">
|
<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...
|
<i class="fas fa-folder-open mr-1"></i> Sposta selezionati in...
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-info" onclick="$('#massTagModal').modal('show')">
|
||||||
|
<i class="fas fa-tags mr-1"></i> Tag
|
||||||
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-success" onclick="massDownload()">
|
<button type="button" class="btn btn-sm btn-success" onclick="massDownload()">
|
||||||
<i class="fas fa-download mr-1"></i> Scarica
|
<i class="fas fa-download mr-1"></i> Scarica
|
||||||
</button>
|
</button>
|
||||||
@@ -226,6 +234,11 @@ $canDeleteDocumenti = Auth::user()->canDelete('documenti');
|
|||||||
</span>
|
</span>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
<div class="text-center mb-1" style="display:flex;flex-wrap:wrap;gap:2px;justify-content:center;">
|
||||||
|
@foreach($documento->tags as $tag)
|
||||||
|
<a href="{{ route('documenti.index', ['tag[]' => $tag->slug]) }}" class="badge" style="background-color: {{ $tag->color ?? '#6c757d' }}; color: #fff; font-size:0.7rem;">{{ $tag->name }}</a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
<div class="small text-muted text-center">
|
<div class="small text-muted text-center">
|
||||||
@if($documento->user)
|
@if($documento->user)
|
||||||
{{ $documento->user->name }}
|
{{ $documento->user->name }}
|
||||||
@@ -283,20 +296,21 @@ $canDeleteDocumenti = Auth::user()->canDelete('documenti');
|
|||||||
{{-- View: Lista --}}
|
{{-- View: Lista --}}
|
||||||
<div id="viewList" style="display:none;">
|
<div id="viewList" style="display:none;">
|
||||||
@if($sottoCartelle->count() > 0 || $documenti->count() > 0)
|
@if($sottoCartelle->count() > 0 || $documenti->count() > 0)
|
||||||
<table class="table table-bordered table-hover table-striped mb-0">
|
<table class="table table-bordered table-hover table-striped mb-0" id="documenti-table">
|
||||||
<thead class="thead-light">
|
<thead class="thead-light">
|
||||||
<tr>
|
<tr>
|
||||||
@if($canDeleteDocumenti)
|
@if($canDeleteDocumenti)
|
||||||
<th style="width: 40px;">
|
<th style="width: 40px;" data-column="select">
|
||||||
<input type="checkbox" id="selectAll" onchange="toggleAll(this)">
|
<input type="checkbox" id="selectAll" onchange="toggleAll(this)">
|
||||||
</th>
|
</th>
|
||||||
@endif
|
@endif
|
||||||
<th>Nome</th>
|
<th data-column="nome">Nome</th>
|
||||||
<th style="width: 100px;">Tipologia</th>
|
<th style="width: 100px;" data-column="tipologia">Tipologia</th>
|
||||||
<th style="width: 90px;">Dimensione</th>
|
<th style="width: 90px;" data-column="dimensione">Dimensione</th>
|
||||||
<th style="width: 120px;">Contesto</th>
|
<th style="width: 120px;" data-column="visibilita">Contesto</th>
|
||||||
<th style="width: 110px;">Data</th>
|
<th style="width: 100px;" data-column="tag">Tag</th>
|
||||||
<th style="width: 130px;">Azioni</th>
|
<th style="width: 110px;" data-column="created_at">Data</th>
|
||||||
|
<th style="width: 130px;" data-column="azioni">Azioni</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -314,6 +328,7 @@ $canDeleteDocumenti = Auth::user()->canDelete('documenti');
|
|||||||
<td class="small text-muted">-</td>
|
<td class="small text-muted">-</td>
|
||||||
<td class="small text-muted">-</td>
|
<td class="small text-muted">-</td>
|
||||||
<td class="small text-muted">-</td>
|
<td class="small text-muted">-</td>
|
||||||
|
<td class="small text-muted">-</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="/documenti?folder_id={{ $cartella->id }}" class="btn btn-xs btn-warning" title="Apri cartella">
|
<a href="/documenti?folder_id={{ $cartella->id }}" class="btn btn-xs btn-warning" title="Apri cartella">
|
||||||
<i class="fas fa-folder-open"></i>
|
<i class="fas fa-folder-open"></i>
|
||||||
@@ -423,6 +438,11 @@ $canDeleteDocumenti = Auth::user()->canDelete('documenti');
|
|||||||
<span class="text-muted small">-</span>
|
<span class="text-muted small">-</span>
|
||||||
@endswitch
|
@endswitch
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
@foreach($documento->tags as $tag)
|
||||||
|
<a href="{{ route('documenti.index', ['tag[]' => $tag->slug]) }}" class="badge" style="background-color: {{ $tag->color ?? '#6c757d' }}; color: #fff;">{{ $tag->name }}</a>
|
||||||
|
@endforeach
|
||||||
|
</td>
|
||||||
<td class="small">{{ $documento->created_at->format('d/m/Y') }}</td>
|
<td class="small">{{ $documento->created_at->format('d/m/Y') }}</td>
|
||||||
<td>
|
<td>
|
||||||
@if($documento->file_path)
|
@if($documento->file_path)
|
||||||
@@ -498,6 +518,10 @@ $canDeleteDocumenti = Auth::user()->canDelete('documenti');
|
|||||||
</div>
|
</div>
|
||||||
</form>{{-- /massForm --}}
|
</form>{{-- /massForm --}}
|
||||||
|
|
||||||
|
@include('partials.table-settings')
|
||||||
|
|
||||||
|
<div id="vista-data" style="display:none;">{{ $vistaDefaultJson }}</div>
|
||||||
|
|
||||||
{{-- MODAL: Nuova Cartella --}}
|
{{-- MODAL: Nuova Cartella --}}
|
||||||
<div class="modal fade" id="newFolderModal" tabindex="-1" role="dialog">
|
<div class="modal fade" id="newFolderModal" tabindex="-1" role="dialog">
|
||||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
||||||
@@ -834,6 +858,50 @@ $canDeleteDocumenti = Auth::user()->canDelete('documenti');
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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="/documenti/mass-tag">
|
||||||
|
@csrf
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="mb-2">Operazione su <strong id="massTagCount">0</strong> documenti 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>
|
||||||
|
|
||||||
{{-- MODAL: Sposta documento --}}
|
{{-- MODAL: Sposta documento --}}
|
||||||
<div class="modal fade" id="moveModal" tabindex="-1" role="dialog">
|
<div class="modal fade" id="moveModal" tabindex="-1" role="dialog">
|
||||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
||||||
@@ -877,6 +945,7 @@ $canDeleteDocumenti = Auth::user()->canDelete('documenti');
|
|||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section('scripts')
|
@section('scripts')
|
||||||
|
<script src="{{ asset('js/column-manager.js') }}"></script>
|
||||||
<script>
|
<script>
|
||||||
const individui = @json($individui->map(fn($i) => ['id' => $i->id, 'label' => $i->cognome . ' ' . $i->nome]));
|
const individui = @json($individui->map(fn($i) => ['id' => $i->id, 'label' => $i->cognome . ' ' . $i->nome]));
|
||||||
const gruppi = @json($gruppi->map(fn($g) => ['id' => $g->id, 'label' => $g->nome]));
|
const gruppi = @json($gruppi->map(fn($g) => ['id' => $g->id, 'label' => $g->nome]));
|
||||||
@@ -1218,6 +1287,12 @@ document.getElementById('massMoveModal')?.addEventListener('show.bs.modal', func
|
|||||||
document.getElementById('massMoveCount').textContent = ids.length;
|
document.getElementById('massMoveCount').textContent = ids.length;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('massTagModal')?.addEventListener('show.bs.modal', function() {
|
||||||
|
const ids = getSelectedIds();
|
||||||
|
document.getElementById('massTagIds').value = ids.join(',');
|
||||||
|
document.getElementById('massTagCount').textContent = ids.length;
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('massMoveNewFolderBtn')?.addEventListener('click', function() {
|
document.getElementById('massMoveNewFolderBtn')?.addEventListener('click', function() {
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: 'Nuova cartella',
|
title: 'Nuova cartella',
|
||||||
@@ -1874,5 +1949,24 @@ function deleteFolder(id, nome) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const table = document.getElementById('documenti-table');
|
||||||
|
if (table) {
|
||||||
|
window.columnManager = new ColumnManager('documenti-table', {
|
||||||
|
entityType: 'documenti',
|
||||||
|
storageKey: 'cm_documenti',
|
||||||
|
resizable: true,
|
||||||
|
reorderable: false,
|
||||||
|
});
|
||||||
|
initTableSettings();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.printWithCurrentSettings = function () {
|
||||||
|
if (window.columnManager) {
|
||||||
|
window.columnManager.printWithSettings();
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -87,10 +87,17 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Allegati</label>
|
<label>Allegati</label>
|
||||||
<div class="border rounded p-3 bg-light">
|
<div class="border rounded p-3 bg-light">
|
||||||
<label class="btn btn-sm btn-success mb-0">
|
<div class="mb-3">
|
||||||
<i class="fas fa-upload mr-1"></i> Carica file
|
<button type="button" class="btn btn-sm btn-primary" data-toggle="modal" data-target="#documentiModal">
|
||||||
<input type="file" name="allegati[]" multiple class="d-none">
|
<i class="fas fa-folder-open mr-1"></i> Seleziona da Documenti
|
||||||
</label>
|
</button>
|
||||||
|
<label class="btn btn-sm btn-success mb-0">
|
||||||
|
<i class="fas fa-upload mr-1"></i> Carica file
|
||||||
|
<input type="file" name="allegati[]" id="allegati" multiple class="d-none" onchange="handleFileUpload(this)">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div id="allegati-list"></div>
|
||||||
|
<input type="hidden" name="documenti_selezionati" id="documenti_selezionati" value="">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -110,10 +117,62 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="documentiModal" tabindex="-1" role="dialog">
|
||||||
|
<div class="modal-dialog modal-lg" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title"><i class="fas fa-folder-open mr-2"></i>Seleziona Documenti</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal">
|
||||||
|
<span>×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
@if($documenti->count() > 0)
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered table-hover table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 40px;"></th>
|
||||||
|
<th>Nome File</th>
|
||||||
|
<th>Tipologia</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($documenti as $doc)
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="custom-control custom-checkbox">
|
||||||
|
<input type="checkbox" class="custom-control-input doc-checkbox" id="doc-{{ $doc->id }}" value="{{ $doc->id }}" data-nome="{{ $doc->nome_file }}">
|
||||||
|
<label class="custom-control-label" for="doc-{{ $doc->id }}"></label>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>{{ $doc->nome_file }}</td>
|
||||||
|
<td><span class="badge badge-info">{{ $doc->tipologia ?: $doc->tipo }}</span></td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<p class="text-muted">Nessun documento disponibile.</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annulla</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="aggiungiDocumenti()">
|
||||||
|
<i class="fas fa-plus mr-1"></i> Aggiungi
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section('scripts')
|
@section('scripts')
|
||||||
<script>
|
<script>
|
||||||
|
let allegatiSelezionati = [];
|
||||||
|
|
||||||
function toggleDestinatari() {
|
function toggleDestinatari() {
|
||||||
const tipo = document.getElementById('destinatario_tipo').value;
|
const tipo = document.getElementById('destinatario_tipo').value;
|
||||||
|
|
||||||
@@ -160,6 +219,53 @@ function loadGruppiContatti() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function aggiungiDocumenti() {
|
||||||
|
const checkboxes = document.querySelectorAll('.doc-checkbox:checked');
|
||||||
|
checkboxes.forEach(function(cb) {
|
||||||
|
const id = parseInt(cb.value);
|
||||||
|
const nome = cb.dataset.nome;
|
||||||
|
|
||||||
|
if (!allegatiSelezionati.includes(id)) {
|
||||||
|
allegatiSelezionati.push(id);
|
||||||
|
|
||||||
|
const container = document.getElementById('allegati-list');
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'd-flex align-items-center justify-content-between bg-white p-2 mb-2 rounded';
|
||||||
|
div.id = 'allegato-' + id;
|
||||||
|
div.innerHTML = '<span><i class="fas fa-file mr-2"></i>' + nome + '</span>' +
|
||||||
|
'<button type="button" class="btn btn-xs btn-danger" onclick="removeAllegato(' + id + ')">' +
|
||||||
|
'<i class="fas fa-times"></i></button>';
|
||||||
|
container.appendChild(div);
|
||||||
|
}
|
||||||
|
|
||||||
|
cb.checked = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('documenti_selezionati').value = allegatiSelezionati.join(',');
|
||||||
|
$('#documentiModal').modal('hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeAllegato(id) {
|
||||||
|
allegatiSelezionati = allegatiSelezionati.filter(function(a) { return a !== id; });
|
||||||
|
const el = document.getElementById('allegato-' + id);
|
||||||
|
if (el) el.remove();
|
||||||
|
document.getElementById('documenti_selezionati').value = allegatiSelezionati.join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileUpload(input) {
|
||||||
|
if (!input.files) return;
|
||||||
|
const container = document.getElementById('allegati-list');
|
||||||
|
for (const file of input.files) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'd-flex align-items-center justify-content-between bg-white p-2 mb-2 rounded';
|
||||||
|
div.innerHTML = '<span><i class="fas fa-file mr-2"></i>' + file.name + ' (' + (file.size / 1024).toFixed(1) + ' KB)</span>' +
|
||||||
|
'<button type="button" class="btn btn-xs btn-danger" onclick="this.parentElement.remove()">' +
|
||||||
|
'<i class="fas fa-times"></i></button>';
|
||||||
|
container.appendChild(div);
|
||||||
|
}
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
function saveDraft() {
|
function saveDraft() {
|
||||||
const form = document.getElementById('emailForm');
|
const form = document.getElementById('emailForm');
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user