Compare commits
10 Commits
4dd4da7f4b
...
Main_Ver1
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d7d384a0d | |||
| 8194b8c034 | |||
| 90093a086b | |||
| cbe3470c8c | |||
| 469e8c014f | |||
| 406985eb1e | |||
| 89382fe4ff | |||
| dd76b473f7 | |||
| 82c54002e4 | |||
| 139098bffc |
@@ -1,70 +1,162 @@
|
|||||||
# 🧠 MEMORY.md — Stato Progetto
|
# 🧠 MEMORY.md — Stato Progetto
|
||||||
|
|
||||||
## Goal
|
## Obiettivo
|
||||||
- Implementare page-length selector su liste (Individui, Eventi, Email)
|
App gestionale Laravel 13 con AdminLTE 4 per gestione Persone e Gruppi.
|
||||||
- Implementare CSV import per gruppi
|
|
||||||
- Implementare ICS import per eventi
|
|
||||||
- Creare script build-dist.sh per distribuzione
|
|
||||||
|
|
||||||
## Modifiche Effettuate (10 file + 3 nuovi)
|
## Funzionalità Implementate
|
||||||
|
|
||||||
### File modificati
|
### Page-length selector
|
||||||
| File | Cosa |
|
- **4 pagine**: Individui, Eventi, Email, Gruppi
|
||||||
|------|------|
|
|
||||||
| `app/Http/Controllers/IndividuoController.php` | perPage support (20 default, clamp [10,20,25,50,100]) |
|
|
||||||
| `app/Http/Controllers/EventoController.php` | perPage + importIcs/importIcsStore (Sabre\VObject) |
|
|
||||||
| `app/Http/Controllers/GruppoController.php` | import/importStore/downloadTemplate CSV gruppi |
|
|
||||||
| `app/Http/Controllers/EmailController.php` | perPage support |
|
|
||||||
| `app/Models/Evento.php` | `uid_esterno` aggiunto a $fillable (bug fix) |
|
|
||||||
| `resources/views/individui/index.blade.php` | dropdown perPage in card-footer |
|
|
||||||
| `resources/views/eventi/index.blade.php` | dropdown perPage + pulsante Importa ICS |
|
|
||||||
| `resources/views/gruppi/index.blade.php` | pulsante Importa CSV |
|
|
||||||
| `resources/views/email/index.blade.php` | dropdown perPage in card-footer |
|
|
||||||
| `routes/web.php` | route gruppi/import, gruppi/template, eventi/import |
|
|
||||||
|
|
||||||
### File nuovi
|
|
||||||
| File | Cosa |
|
|
||||||
|------|------|
|
|
||||||
| `resources/views/gruppi/import.blade.php` | form upload CSV gruppi |
|
|
||||||
| `resources/views/eventi/import.blade.php` | form upload ICS eventi |
|
|
||||||
| `build-dist.sh` | script build distribuzione |
|
|
||||||
|
|
||||||
## Feature
|
|
||||||
|
|
||||||
### 1. Page-length selector
|
|
||||||
- **3 pagine**: Individui, Eventi, Email
|
|
||||||
- **Valori**: 10, 20, 25, 50, 100 (default 20)
|
- **Valori**: 10, 20, 25, 50, 100 (default 20)
|
||||||
- Dropdown nel card-footer con hidden fields per preservare altri query param
|
- Dropdown nel card-footer con campi hidden per preservare query params
|
||||||
|
- GruppoController: usa `LengthAwarePaginator` per paginare la collezione ordinata gerarchicamente
|
||||||
|
|
||||||
### 2. CSV Import Gruppi
|
### CSV Import Gruppi
|
||||||
- Stesso pattern di `IndividuoController` (import GET, importStore POST, downloadTemplate GET)
|
- 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
|
- Colonne: nome, descrizione, parent_id, diocesi_id, indirizzo_incontro, cap_incontro, città_incontro, sigla_provincia_incontro
|
||||||
- Fault-tolerant: skip righe vuote, log errori
|
- Fault-tolerant: skip righe vuote, log errori, try/catch su ogni riga
|
||||||
|
|
||||||
### 3. ICS Import Eventi
|
### ICS Import Eventi
|
||||||
- Usa `Sabre\VObject\Reader::read()` già in vendor/
|
- 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
|
- Mapping: SUMMARY→nome_evento, DESCRIPTION→descrizione, DTSTART→data_specifica+ora_inizio, DTEND→durata_minuti, LOCATION→luogo_indirizzo, UID→uid_esterno
|
||||||
- Dedup via uid_esterno
|
- Dedup via uid_esterno
|
||||||
- RRULE: import come evento singolo con prima occorrenza
|
- RRULE: import come evento singolo con prima occorrenza
|
||||||
|
|
||||||
### 4. Script build-dist.sh
|
### build-dist.sh (script di distribuzione)
|
||||||
- Genera `glastree-YYYYMMDD_HHMM.tar.gz` (~26MB)
|
- Genera `glastree-YYYYMMDD_HHMM.tar.gz`
|
||||||
- Include: sorgenti, vendor (production), Dockerfile, docker-compose, install.php, install.sql
|
- Include: sorgenti, vendor (production)
|
||||||
- Esclude: .git, node_modules, tests, cache, storage content, vendor/docs/tests
|
- Esclude: .git, node_modules, tests, cache, storage content, vendor/docs/tests, Docker
|
||||||
|
- **Istruzioni post-estrazione complete**: mkdir, permessi, configurazione, cache clear
|
||||||
|
|
||||||
## Installazione su server remoto
|
## Bug Fix Recenti
|
||||||
|
|
||||||
|
### 2026-06-07 — Colonne mancanti in eventi e gruppo_individuo
|
||||||
|
- `descrizione_evento` e `is_incontro_gruppo` mancanti in install.sql → creata migration + ALTER TABLE
|
||||||
|
- `ruolo_nel_gruppo` mancante in install.sql per pivot `gruppo_individuo`
|
||||||
|
- `VistaReport.php`: aggiunto `is_default` a `$fillable`
|
||||||
|
|
||||||
|
### 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
|
```bash
|
||||||
# Estrarre
|
chmod -R 775 storage/framework/sessions
|
||||||
tar xzf glastree-20260607_2052.tar.gz
|
php artisan config:clear
|
||||||
|
|
||||||
# Configurare
|
|
||||||
cp .env.example .env
|
|
||||||
php artisan key:generate
|
|
||||||
php artisan storage:link
|
|
||||||
|
|
||||||
# Opzione A — Docker
|
|
||||||
docker compose up -d
|
|
||||||
|
|
||||||
# Opzione B — Apache nativo (MySQL)
|
|
||||||
php install.php
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
## Prossimi Passi
|
||||||
|
- *(nessuno — in attesa di nuove richieste)*
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,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'])) {
|
||||||
@@ -62,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.');
|
||||||
|
|||||||
@@ -126,6 +126,52 @@ class CalendarioConnessioneController extends Controller
|
|||||||
return redirect('/impostazioni#calendario')->with('error', 'Errore sync: ' . $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
|
public function reorder(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$this->authorizeWrite('settings');
|
$this->authorizeWrite('settings');
|
||||||
|
|||||||
@@ -398,6 +398,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.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+14
-12
@@ -5,7 +5,9 @@ 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\Schema;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
class AppSetting extends Model
|
class AppSetting extends Model
|
||||||
{
|
{
|
||||||
@@ -107,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;
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-8
@@ -37,11 +37,14 @@ tar czf "${ARCHIVE}" \
|
|||||||
--exclude='vendor/*/doc' \
|
--exclude='vendor/*/doc' \
|
||||||
--exclude='storage/app/backups' \
|
--exclude='storage/app/backups' \
|
||||||
--exclude='storage/app/documenti' \
|
--exclude='storage/app/documenti' \
|
||||||
--exclude='storage/framework/cache' \
|
--exclude='storage/app/public' \
|
||||||
--exclude='storage/framework/sessions' \
|
--exclude='storage/app/private' \
|
||||||
--exclude='storage/framework/views' \
|
--exclude='public/storage' \
|
||||||
--exclude='storage/logs' \
|
--exclude='storage/framework/cache/*' \
|
||||||
--exclude='storage/debugbar' \
|
--exclude='storage/framework/sessions/*' \
|
||||||
|
--exclude='storage/framework/views/*' \
|
||||||
|
--exclude='storage/logs/*' \
|
||||||
|
--exclude='storage/debugbar/*' \
|
||||||
--exclude='bootstrap/cache/*.php' \
|
--exclude='bootstrap/cache/*.php' \
|
||||||
--exclude='public/hot' \
|
--exclude='public/hot' \
|
||||||
--exclude='*.tar.gz' \
|
--exclude='*.tar.gz' \
|
||||||
@@ -67,7 +70,27 @@ echo "Per estrarre sul server di destinazione:"
|
|||||||
echo " tar xzf ${ARCHIVE}"
|
echo " tar xzf ${ARCHIVE}"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Poi esegui:"
|
echo "Poi esegui:"
|
||||||
echo " cp .env.example .env # configura il tuo ambiente"
|
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 " php artisan key:generate"
|
||||||
echo " php artisan storage:link"
|
echo ""
|
||||||
echo " php install.php # avvia installazione MySQL"
|
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"
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -390,14 +392,17 @@
|
|||||||
<a href="{{ route('impostazioni.email.sync') }}" class="btn btn-warning ml-2">
|
<a href="{{ route('impostazioni.email.sync') }}" class="btn btn-warning ml-2">
|
||||||
<i class="fas fa-sync mr-1"></i> Sincronizza Ora
|
<i class="fas fa-sync mr-1"></i> Sincronizza Ora
|
||||||
</a>
|
</a>
|
||||||
<form action="{{ route('impostazioni.email.destroy') }}" method="POST" class="ml-auto" 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">
|
|
||||||
<i class="fas fa-trash mr-1"></i> Elimina Configurazione
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</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
|
||||||
|
|||||||
@@ -17,6 +17,9 @@
|
|||||||
<a href="{{ route('eventi.create') }}" class="btn btn-success btn-sm">
|
<a href="{{ route('eventi.create') }}" class="btn btn-success btn-sm">
|
||||||
<i class="fas fa-plus mr-1"></i> Nuovo Evento
|
<i class="fas fa-plus mr-1"></i> Nuovo Evento
|
||||||
</a>
|
</a>
|
||||||
|
<button type="button" class="btn btn-info btn-sm" id="syncCalendarsBtn" onclick="syncCalendars()">
|
||||||
|
<i class="fas fa-sync mr-1"></i> Sync
|
||||||
|
</button>
|
||||||
<form action="{{ route('eventi.export-selected-ics') }}" method="POST" class="d-inline">
|
<form action="{{ route('eventi.export-selected-ics') }}" method="POST" class="d-inline">
|
||||||
@csrf
|
@csrf
|
||||||
<button type="submit" class="btn btn-success btn-sm">
|
<button type="submit" class="btn btn-success btn-sm">
|
||||||
@@ -40,10 +43,44 @@
|
|||||||
@section('scripts')
|
@section('scripts')
|
||||||
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.10/index.global.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.10/index.global.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
function syncCalendars() {
|
||||||
|
var btn = document.getElementById('syncCalendarsBtn');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i> Sync...';
|
||||||
|
|
||||||
|
fetch('{{ route('calendario-connessioni.sync-all') }}', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || '',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
var msg = data.connections + ' connessioni sincronizzate.';
|
||||||
|
msg += ' Importati: ' + data.total_imported + ', Esportati: ' + data.total_exported + '.';
|
||||||
|
if (data.success) {
|
||||||
|
alert('✅ Sync completato!\n' + msg);
|
||||||
|
} else {
|
||||||
|
alert('⚠️ Sync completato con errori.\n' + msg);
|
||||||
|
}
|
||||||
|
if (window.calendar) {
|
||||||
|
window.calendar.refetchEvents();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
alert('❌ Errore sync: ' + err.message);
|
||||||
|
})
|
||||||
|
.finally(function() {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = '<i class="fas fa-sync mr-1"></i> Sync';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
var calendarEl = document.getElementById('calendar');
|
var calendarEl = document.getElementById('calendar');
|
||||||
|
|
||||||
var calendar = new FullCalendar.Calendar(calendarEl, {
|
window.calendar = new FullCalendar.Calendar(calendarEl, {
|
||||||
initialView: 'dayGridMonth',
|
initialView: 'dayGridMonth',
|
||||||
locale: 'it',
|
locale: 'it',
|
||||||
headerToolbar: {
|
headerToolbar: {
|
||||||
@@ -97,7 +134,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
calendar.render();
|
window.calendar.render();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@endsection
|
@endsection
|
||||||
@@ -27,6 +27,11 @@
|
|||||||
<i class="fab fa-google-drive mr-2"></i> Google Drive
|
<i class="fab fa-google-drive mr-2"></i> Google Drive
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#help-calendario" data-toggle="tab">
|
||||||
|
<i class="fas fa-calendar-alt mr-2"></i> Calendario
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="#help-tipologie" data-toggle="tab">
|
<a class="nav-link" href="#help-tipologie" data-toggle="tab">
|
||||||
<i class="fas fa-tags mr-2"></i> Tipologie e Dati
|
<i class="fas fa-tags mr-2"></i> Tipologie e Dati
|
||||||
@@ -194,6 +199,75 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- Calendario --}}
|
||||||
|
<div class="tab-pane" id="help-calendario">
|
||||||
|
<div class="card card-outline card-info">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title"><i class="fas fa-calendar-alt mr-2"></i> Google Calendar</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h5>Cos'è</h5>
|
||||||
|
<p>Integrazione con Google Calendar per sincronizzare gli eventi tra l'app e il tuo calendario Google. Supporta import, export e sincronizzazione bidirezionale.</p>
|
||||||
|
|
||||||
|
<h5>Requisiti</h5>
|
||||||
|
<ol>
|
||||||
|
<li>Un account Google / Google Workspace</li>
|
||||||
|
<li>Un progetto sulla <strong>Google Cloud Console</strong> con l'API Google Calendar abilitata</li>
|
||||||
|
<li>Credenziali OAuth 2.0 (Client ID e Client Secret)</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h5>Passo 1: Creare il progetto Google Cloud</h5>
|
||||||
|
<ol>
|
||||||
|
<li>Vai su <a href="https://console.developers.google.com/" target="_blank">Google Cloud Console</a></li>
|
||||||
|
<li>Crea un nuovo progetto o selezionane uno esistente</li>
|
||||||
|
<li>Vai a <strong>API e servizi → Libreria</strong></li>
|
||||||
|
<li>Cerca "<strong>Google Calendar API</strong>" e <strong>abilitala</strong></li>
|
||||||
|
<li>Vai a <strong>API e servizi → Credenziali</strong></li>
|
||||||
|
<li>Clicca "<strong>Crea credenziali → ID client OAuth</strong>"</li>
|
||||||
|
<li>Tipo applicazione: "<strong>Applicazione Web</strong>"</li>
|
||||||
|
<li><strong>URI di reindirizzamento autorizzato:</strong> aggiungi <code>https://developers.google.com/oauthplayground</code></li>
|
||||||
|
<li>Completa la creazione e <strong>copia Client ID e Client Secret</strong></li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h5>Passo 2: Ottenere il Refresh Token (Google OAuth Playground)</h5>
|
||||||
|
<p>A differenza di Google Drive, per Google Calendar il refresh token va ottenuto manualmente tramite il Google OAuth Playground:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Vai su <a href="https://developers.google.com/oauthplayground" target="_blank">Google OAuth Playground</a></li>
|
||||||
|
<li>Clicca l'ingranaggio ⚙️ in alto a destra</li>
|
||||||
|
<li>Spunta "<strong>Use your own OAuth credentials</strong>"</li>
|
||||||
|
<li>Incolla <strong>Client ID</strong> e <strong>Client Secret</strong> creati al Passo 1</li>
|
||||||
|
<li>Nella colonna sinistra, espandi "<strong>Google Calendar API v3</strong>" e seleziona lo scope <code>https://www.googleapis.com/auth/calendar</code></li>
|
||||||
|
<li>Clicca "<strong>Authorize APIs</strong>" e autorizza con l'account Google</li>
|
||||||
|
<li>Clicca "<strong>Exchange authorization code for tokens</strong>"</li>
|
||||||
|
<li><strong>Copia il Refresh Token</strong> generato</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h5>Passo 3: Configurazione nell'app</h5>
|
||||||
|
<ol>
|
||||||
|
<li>Vai in <strong>Impostazioni → Calendario</strong></li>
|
||||||
|
<li>Clicca "<strong>Nuova Connessione</strong>"</li>
|
||||||
|
<li>Inserisci un nome descrittivo (es. "Calendario Lavoro")</li>
|
||||||
|
<li>Seleziona tipo <strong>Google Calendar</strong></li>
|
||||||
|
<li>Incolla <strong>Client ID</strong>, <strong>Client Secret</strong> e il <strong>Refresh Token</strong> ottenuto</li>
|
||||||
|
<li><strong>Calendar ID (opzionale):</strong> lascia <code>primary</code> per il calendario principale, oppure inserisci l'ID di un calendario specifico (lo trovi in Google Calendar → Impostazioni calendario → ID calendario)</li>
|
||||||
|
<li>Seleziona la <strong>direzione di sincronizzazione</strong> (import, export o bidirezionale)</li>
|
||||||
|
<li>Clicca "<strong>Salva</strong>"</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h5>Test connessione</h5>
|
||||||
|
<p>Dopo aver salvato, usa il pulsante <span class="badge badge-success"><i class="fas fa-plug"></i> Test</span> nella tabella delle connessioni per verificare che l'autorizzazione funzioni correttamente.</p>
|
||||||
|
|
||||||
|
<h5>Sincronizzazione</h5>
|
||||||
|
<p>Dopo aver configurato la connessione, puoi sincronizzare gli eventi manualmente cliccando <span class="badge badge-primary"><i class="fas fa-sync"></i> Sync</span>. La sincronizzazione automatica viene eseguita in base all'intervallo impostato (richiede il cron di Laravel configurato).</p>
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="fas fa-info-circle mr-2"></i>
|
||||||
|
Il Refresh Token è <strong>permanente</strong> finché non viene revocato dall'utente. Se la connessione smette di funzionare, potrebbe essere necessario rigenerarlo ripetendo il Passo 2.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{-- Tipologie e Dati --}}
|
{{-- Tipologie e Dati --}}
|
||||||
<div class="tab-pane" id="help-tipologie">
|
<div class="tab-pane" id="help-tipologie">
|
||||||
<div class="card card-outline card-success">
|
<div class="card card-outline card-success">
|
||||||
|
|||||||
@@ -62,6 +62,7 @@
|
|||||||
<ol>
|
<ol>
|
||||||
<li><a href="#webdav">WebDAV / Nextcloud</a></li>
|
<li><a href="#webdav">WebDAV / Nextcloud</a></li>
|
||||||
<li><a href="#googledrive">Google Drive</a></li>
|
<li><a href="#googledrive">Google Drive</a></li>
|
||||||
|
<li><a href="#calendario">Google Calendar</a></li>
|
||||||
<li><a href="#tipologie">Tipologie e Gestione Dati</a></li>
|
<li><a href="#tipologie">Tipologie e Gestione Dati</a></li>
|
||||||
<li><a href="#email">Email / SMTP</a></li>
|
<li><a href="#email">Email / SMTP</a></li>
|
||||||
<li><a href="#installazione">Guida all'Installazione</a></li>
|
<li><a href="#installazione">Guida all'Installazione</a></li>
|
||||||
@@ -182,8 +183,72 @@
|
|||||||
|
|
||||||
<div class="page-break"></div>
|
<div class="page-break"></div>
|
||||||
|
|
||||||
{{-- 3. Tipologie e Dati --}}
|
{{-- 3. Google Calendar --}}
|
||||||
<h2 id="tipologie">3. Tipologie e Gestione Dati</h2>
|
<h2 id="calendario">3. Google Calendar</h2>
|
||||||
|
|
||||||
|
<h3>Cos'è</h3>
|
||||||
|
<p>Integrazione con Google Calendar per sincronizzare gli eventi tra l'app e il tuo calendario Google. Supporta import, export e sincronizzazione bidirezionale.</p>
|
||||||
|
|
||||||
|
<h3>Requisiti</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Un account Google / Google Workspace</li>
|
||||||
|
<li>Un progetto sulla <strong>Google Cloud Console</strong> con l'API Google Calendar abilitata</li>
|
||||||
|
<li>Credenziali OAuth 2.0 (Client ID e Client Secret)</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Passo 1: Creare il progetto Google Cloud</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Vai su <strong>Google Cloud Console</strong> (console.developers.google.com)</li>
|
||||||
|
<li>Crea un nuovo progetto o selezionane uno esistente</li>
|
||||||
|
<li>Vai a <strong>API e servizi → Libreria</strong></li>
|
||||||
|
<li>Cerca "<strong>Google Calendar API</strong>" e <strong>abilitala</strong></li>
|
||||||
|
<li>Vai a <strong>API e servizi → Credenziali</strong></li>
|
||||||
|
<li>Clicca "<strong>Crea credenziali → ID client OAuth</strong>"</li>
|
||||||
|
<li>Tipo applicazione: "<strong>Applicazione Web</strong>"</li>
|
||||||
|
<li><strong>URI di reindirizzamento autorizzato:</strong> aggiungi <code>https://developers.google.com/oauthplayground</code></li>
|
||||||
|
<li>Completa la creazione e <strong>copia Client ID e Client Secret</strong></li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Passo 2: Ottenere il Refresh Token</h3>
|
||||||
|
<p>A differenza di Google Drive, per Google Calendar il refresh token va ottenuto manualmente tramite il Google OAuth Playground:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Vai su <strong>Google OAuth Playground</strong> (developers.google.com/oauthplayground)</li>
|
||||||
|
<li>Clicca l'ingranaggio ⚙️ in alto a destra</li>
|
||||||
|
<li>Spunta "<strong>Use your own OAuth credentials</strong>"</li>
|
||||||
|
<li>Incolla <strong>Client ID</strong> e <strong>Client Secret</strong> creati al Passo 1</li>
|
||||||
|
<li>Nella colonna sinistra, espandi "<strong>Google Calendar API v3</strong>" e seleziona lo scope <code>https://www.googleapis.com/auth/calendar</code></li>
|
||||||
|
<li>Clicca "<strong>Authorize APIs</strong>" e autorizza con l'account Google</li>
|
||||||
|
<li>Clicca "<strong>Exchange authorization code for tokens</strong>"</li>
|
||||||
|
<li><strong>Copia il Refresh Token</strong> generato</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Passo 3: Configurazione nell'app</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Vai in <strong>Impostazioni → Calendario</strong></li>
|
||||||
|
<li>Clicca "<strong>Nuova Connessione</strong>"</li>
|
||||||
|
<li>Inserisci un nome descrittivo (es. "Calendario Lavoro")</li>
|
||||||
|
<li>Seleziona tipo <strong>Google Calendar</strong></li>
|
||||||
|
<li>Incolla <strong>Client ID</strong>, <strong>Client Secret</strong> e il <strong>Refresh Token</strong> ottenuto</li>
|
||||||
|
<li><strong>Calendar ID (opzionale):</strong> lascia <code>primary</code> per il calendario principale, oppure inserisci l'ID di un calendario specifico</li>
|
||||||
|
<li>Seleziona la <strong>direzione di sincronizzazione</strong> (import, export o bidirezionale)</li>
|
||||||
|
<li>Clicca "<strong>Salva</strong>"</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Test connessione</h3>
|
||||||
|
<p>Dopo aver salvato, usa il pulsante <strong>Test</strong> nella tabella delle connessioni per verificare che l'autorizzazione funzioni correttamente.</p>
|
||||||
|
|
||||||
|
<h3>Sincronizzazione</h3>
|
||||||
|
<p>Dopo aver configurato la connessione, puoi sincronizzare gli eventi manualmente cliccando <strong>Sync</strong>. La sincronizzazione automatica viene eseguita in base all'intervallo impostato (richiede il cron di Laravel configurato).</p>
|
||||||
|
|
||||||
|
<div class="callout callout-info">
|
||||||
|
<h4>Refresh Token permanente</h4>
|
||||||
|
<p>Il Refresh Token è permanente finché non viene revocato dall'utente. Se la connessione smette di funzionare, potrebbe essere necessario rigenerarlo ripetendo il Passo 2.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="page-break"></div>
|
||||||
|
|
||||||
|
{{-- 4. Tipologie e Dati --}}
|
||||||
|
<h2 id="tipologie">4. Tipologie e Gestione Dati</h2>
|
||||||
|
|
||||||
<h3>Tipologie Documenti</h3>
|
<h3>Tipologie Documenti</h3>
|
||||||
<p>Le tipologie classificano i documenti (avatar, galleria, documento, statuto, altro).</p>
|
<p>Le tipologie classificano i documenti (avatar, galleria, documento, statuto, altro).</p>
|
||||||
@@ -210,8 +275,8 @@
|
|||||||
|
|
||||||
<div class="page-break"></div>
|
<div class="page-break"></div>
|
||||||
|
|
||||||
{{-- 4. Email / SMTP --}}
|
{{-- 5. Email / SMTP --}}
|
||||||
<h2 id="email">4. Email / SMTP</h2>
|
<h2 id="email">5. Email / SMTP</h2>
|
||||||
|
|
||||||
<h3>Configurazione Email</h3>
|
<h3>Configurazione Email</h3>
|
||||||
<p>Vai in <strong>Impostazioni → Email</strong> per configurare la posta elettronica.</p>
|
<p>Vai in <strong>Impostazioni → Email</strong> per configurare la posta elettronica.</p>
|
||||||
@@ -264,8 +329,8 @@
|
|||||||
|
|
||||||
<div class="page-break"></div>
|
<div class="page-break"></div>
|
||||||
|
|
||||||
{{-- 5. Installazione --}}
|
{{-- 6. Installazione --}}
|
||||||
<h2 id="installazione">5. Guida all'Installazione</h2>
|
<h2 id="installazione">6. Guida all'Installazione</h2>
|
||||||
|
|
||||||
<h3>Panoramica</h3>
|
<h3>Panoramica</h3>
|
||||||
<p>L'installazione avviene tramite l'interattivo <code>install.php</code> che guida passo-passo. Supporta due modalità:</p>
|
<p>L'installazione avviene tramite l'interattivo <code>install.php</code> che guida passo-passo. Supporta due modalità:</p>
|
||||||
@@ -411,8 +476,8 @@ sudo systemctl reload apache2</pre>
|
|||||||
|
|
||||||
<div class="page-break"></div>
|
<div class="page-break"></div>
|
||||||
|
|
||||||
{{-- 6. Backup e Migrazione --}}
|
{{-- 7. Backup e Migrazione --}}
|
||||||
<h2 id="backup">6. Backup e Migrazione Server</h2>
|
<h2 id="backup">7. Backup e Migrazione Server</h2>
|
||||||
|
|
||||||
<h3>Cos'è</h3>
|
<h3>Cos'è</h3>
|
||||||
<p>Il sistema di backup crea un archivio ZIP contenente tutto il necessario per ripristinare l'applicazione su un nuovo server. Include database, file caricati e configurazione.</p>
|
<p>Il sistema di backup crea un archivio ZIP contenente tutto il necessario per ripristinare l'applicazione su un nuovo server. Include database, file caricati e configurazione.</p>
|
||||||
@@ -591,8 +656,8 @@ sudo certbot --apache -d glastree.esempio.it</pre>
|
|||||||
|
|
||||||
<div class="page-break"></div>
|
<div class="page-break"></div>
|
||||||
|
|
||||||
{{-- 7. Docker --}}
|
{{-- 8. Docker --}}
|
||||||
<h2 id="docker">7. Docker</h2>
|
<h2 id="docker">8. Docker</h2>
|
||||||
|
|
||||||
<h3>Creare un'immagine Docker con dati di base</h3>
|
<h3>Creare un'immagine Docker con dati di base</h3>
|
||||||
<p>Puoi creare un'immagine Docker pronta all'uso che include l'applicazione e i dati di base (tipologie documenti, tipologie eventi, ruoli). Scegli la configurazione in base alle tue esigenze:</p>
|
<p>Puoi creare un'immagine Docker pronta all'uso che include l'applicazione e i dati di base (tipologie documenti, tipologie eventi, ruoli). Scegli la configurazione in base alle tue esigenze:</p>
|
||||||
|
|||||||
@@ -583,6 +583,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" {{ ($emailSettings->is_active ?? false) ? 'checked' : '' }}>
|
name="is_active" value="1" {{ ($emailSettings->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>
|
||||||
@@ -599,11 +600,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<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="checkbox" class="custom-control-input" id="signature_enabled"
|
<input type="hidden" name="signature_enabled" value="0">
|
||||||
name="signature_enabled" value="1" {{ ($emailSettings->signature_enabled ?? true) ? 'checked' : '' }}>
|
<input type="checkbox" class="custom-control-input" id="signature_enabled"
|
||||||
<label class="custom-control-label" for="signature_enabled">Attiva firma automatica</label>
|
name="signature_enabled" value="1" {{ ($emailSettings->signature_enabled ?? true) ? 'checked' : '' }}>
|
||||||
</div>
|
<label class="custom-control-label" for="signature_enabled">Attiva firma automatica</label>
|
||||||
|
</div>
|
||||||
<small class="text-muted">Quando attivo, la firma verrà aggiunta in fondo a ogni email inviata</small>
|
<small class="text-muted">Quando attivo, la firma verrà aggiunta in fondo a ogni email inviata</small>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
@@ -722,14 +724,17 @@
|
|||||||
<a href="{{ route('impostazioni.email.sync') }}" class="btn btn-warning ml-2">
|
<a href="{{ route('impostazioni.email.sync') }}" class="btn btn-warning ml-2">
|
||||||
<i class="fas fa-sync mr-1"></i> Sincronizza Ora
|
<i class="fas fa-sync mr-1"></i> Sincronizza Ora
|
||||||
</a>
|
</a>
|
||||||
<form action="{{ route('impostazioni.email.destroy') }}" method="POST" class="ml-auto" 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">
|
|
||||||
<i class="fas fa-trash mr-1"></i> Elimina Configurazione
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</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>
|
||||||
|
|
||||||
{{-- === CALENDARIO === --}}
|
{{-- === CALENDARIO === --}}
|
||||||
@@ -1626,6 +1631,9 @@ $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
|||||||
sortableCalendario.refresh();
|
sortableCalendario.refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (target === '#email-firma') {
|
||||||
|
setTimeout(initSignatureEditor, 100);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== MITTENTI AGGIUNTIVI =====
|
// ===== MITTENTI AGGIUNTIVI =====
|
||||||
|
|||||||
@@ -203,6 +203,7 @@ Route::put('/calendario-connessioni/{id}', [App\Http\Controllers\CalendarioConne
|
|||||||
Route::delete('/calendario-connessioni/{id}', [App\Http\Controllers\CalendarioConnessioneController::class, 'destroy'])->middleware('auth')->name('calendario-connessioni.destroy');
|
Route::delete('/calendario-connessioni/{id}', [App\Http\Controllers\CalendarioConnessioneController::class, 'destroy'])->middleware('auth')->name('calendario-connessioni.destroy');
|
||||||
Route::post('/calendario-connessioni/{id}/test', [App\Http\Controllers\CalendarioConnessioneController::class, 'test'])->middleware('auth')->name('calendario-connessioni.test');
|
Route::post('/calendario-connessioni/{id}/test', [App\Http\Controllers\CalendarioConnessioneController::class, 'test'])->middleware('auth')->name('calendario-connessioni.test');
|
||||||
Route::post('/calendario-connessioni/{id}/sync', [App\Http\Controllers\CalendarioConnessioneController::class, 'sync'])->middleware('auth')->name('calendario-connessioni.sync');
|
Route::post('/calendario-connessioni/{id}/sync', [App\Http\Controllers\CalendarioConnessioneController::class, 'sync'])->middleware('auth')->name('calendario-connessioni.sync');
|
||||||
|
Route::post('/calendario-connessioni/sync-all', [App\Http\Controllers\CalendarioConnessioneController::class, 'syncAll'])->middleware('auth')->name('calendario-connessioni.sync-all');
|
||||||
Route::post('/calendario-connessioni/reorder', [App\Http\Controllers\CalendarioConnessioneController::class, 'reorder'])->middleware('auth')->name('calendario-connessioni.reorder');
|
Route::post('/calendario-connessioni/reorder', [App\Http\Controllers\CalendarioConnessioneController::class, 'reorder'])->middleware('auth')->name('calendario-connessioni.reorder');
|
||||||
|
|
||||||
// Storage Repositories (Opzione B)
|
// Storage Repositories (Opzione B)
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\EmailSetting;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
EmailSetting::where('imap_host', 'test.feature')->delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('saves email settings via POST form', function () {
|
||||||
|
$user = User::first();
|
||||||
|
$response = $this->actingAs($user)->post(route('impostazioni.email.save'), [
|
||||||
|
'imap_host' => 'test.feature',
|
||||||
|
'imap_port' => '993',
|
||||||
|
'imap_encryption' => 'ssl',
|
||||||
|
'imap_username' => 'featureuser',
|
||||||
|
'imap_password' => 'featurepass',
|
||||||
|
'email_address' => 'feature@test.com',
|
||||||
|
'email_name' => 'Feature Test',
|
||||||
|
'is_active' => '1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertSessionHas('success');
|
||||||
|
|
||||||
|
$record = EmailSetting::where('imap_host', 'test.feature')->first();
|
||||||
|
expect($record)->not->toBeNull();
|
||||||
|
expect($record->email_address)->toBe('feature@test.com');
|
||||||
|
});
|
||||||
+1
@@ -7,6 +7,7 @@ $baseDir = dirname($vendorDir);
|
|||||||
|
|
||||||
return array(
|
return array(
|
||||||
'App\\Console\\Commands\\BackupRunCommand' => $baseDir . '/app/Console/Commands/BackupRunCommand.php',
|
'App\\Console\\Commands\\BackupRunCommand' => $baseDir . '/app/Console/Commands/BackupRunCommand.php',
|
||||||
|
'App\\Console\\Commands\\DiagnoseEmail' => $baseDir . '/app/Console/Commands/DiagnoseEmail.php',
|
||||||
'App\\Console\\Commands\\ExportHelpPdf' => $baseDir . '/app/Console/Commands/ExportHelpPdf.php',
|
'App\\Console\\Commands\\ExportHelpPdf' => $baseDir . '/app/Console/Commands/ExportHelpPdf.php',
|
||||||
'App\\Console\\Commands\\ResetUserPassword' => $baseDir . '/app/Console/Commands/ResetUserPassword.php',
|
'App\\Console\\Commands\\ResetUserPassword' => $baseDir . '/app/Console/Commands/ResetUserPassword.php',
|
||||||
'App\\Console\\Commands\\SyncUserPermissions' => $baseDir . '/app/Console/Commands/SyncUserPermissions.php',
|
'App\\Console\\Commands\\SyncUserPermissions' => $baseDir . '/app/Console/Commands/SyncUserPermissions.php',
|
||||||
|
|||||||
Vendored
+1
@@ -619,6 +619,7 @@ class ComposerStaticInit74199c0a3889e8d1256a25f93805ef56
|
|||||||
|
|
||||||
public static $classMap = array (
|
public static $classMap = array (
|
||||||
'App\\Console\\Commands\\BackupRunCommand' => __DIR__ . '/../..' . '/app/Console/Commands/BackupRunCommand.php',
|
'App\\Console\\Commands\\BackupRunCommand' => __DIR__ . '/../..' . '/app/Console/Commands/BackupRunCommand.php',
|
||||||
|
'App\\Console\\Commands\\DiagnoseEmail' => __DIR__ . '/../..' . '/app/Console/Commands/DiagnoseEmail.php',
|
||||||
'App\\Console\\Commands\\ExportHelpPdf' => __DIR__ . '/../..' . '/app/Console/Commands/ExportHelpPdf.php',
|
'App\\Console\\Commands\\ExportHelpPdf' => __DIR__ . '/../..' . '/app/Console/Commands/ExportHelpPdf.php',
|
||||||
'App\\Console\\Commands\\ResetUserPassword' => __DIR__ . '/../..' . '/app/Console/Commands/ResetUserPassword.php',
|
'App\\Console\\Commands\\ResetUserPassword' => __DIR__ . '/../..' . '/app/Console/Commands/ResetUserPassword.php',
|
||||||
'App\\Console\\Commands\\SyncUserPermissions' => __DIR__ . '/../..' . '/app/Console/Commands/SyncUserPermissions.php',
|
'App\\Console\\Commands\\SyncUserPermissions' => __DIR__ . '/../..' . '/app/Console/Commands/SyncUserPermissions.php',
|
||||||
|
|||||||
Vendored
+2
-2
@@ -3,7 +3,7 @@
|
|||||||
'name' => 'laravel/laravel',
|
'name' => 'laravel/laravel',
|
||||||
'pretty_version' => 'dev-main',
|
'pretty_version' => 'dev-main',
|
||||||
'version' => 'dev-main',
|
'version' => 'dev-main',
|
||||||
'reference' => 'f9c1268466e9f534c2f1ade6e5bb4bddfbaa56e5',
|
'reference' => '90093a086b35dce8edd97fce09d32d2be42853e8',
|
||||||
'type' => 'project',
|
'type' => 'project',
|
||||||
'install_path' => __DIR__ . '/../../',
|
'install_path' => __DIR__ . '/../../',
|
||||||
'aliases' => array(),
|
'aliases' => array(),
|
||||||
@@ -427,7 +427,7 @@
|
|||||||
'laravel/laravel' => array(
|
'laravel/laravel' => array(
|
||||||
'pretty_version' => 'dev-main',
|
'pretty_version' => 'dev-main',
|
||||||
'version' => 'dev-main',
|
'version' => 'dev-main',
|
||||||
'reference' => 'f9c1268466e9f534c2f1ade6e5bb4bddfbaa56e5',
|
'reference' => '90093a086b35dce8edd97fce09d32d2be42853e8',
|
||||||
'type' => 'project',
|
'type' => 'project',
|
||||||
'install_path' => __DIR__ . '/../../',
|
'install_path' => __DIR__ . '/../../',
|
||||||
'aliases' => array(),
|
'aliases' => array(),
|
||||||
|
|||||||
Reference in New Issue
Block a user