Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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,162 @@
|
||||
# 🧠 MEMORY.md — Stato Progetto
|
||||
|
||||
## Goal
|
||||
- Stabilizzare il modale "Carica Documento" e migliorare la gestione cartelle durante l'upload.
|
||||
## Obiettivo
|
||||
App gestionale Laravel 13 con AdminLTE 4 per gestione Persone e Gruppi.
|
||||
|
||||
## Constraints & Preferences
|
||||
- (none)
|
||||
## Funzionalità Implementate
|
||||
|
||||
## Progress
|
||||
### Done
|
||||
- Aggiunto pulsante "Scarica Guida PDF" nella sidebar di `/help` con nuovo metodo `HelpController::downloadPdf()` e route `GET /help/pdf/download`
|
||||
- Aggiunta card "Backup" in dashboard (small-box bg-navy, icona fa-hdd) con conteggio backup, visibile solo a utenti con `canManage('settings')`
|
||||
- Corretto `modal-body` nel modale `#uploadModal` che chiudeva prematuramente (pulsanti Annulla/Carica finivano fuori dal popup)
|
||||
- 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
|
||||
### Page-length selector
|
||||
- **4 pagine**: Individui, Eventi, Email, Gruppi
|
||||
- **Valori**: 10, 20, 25, 50, 100 (default 20)
|
||||
- Dropdown nel card-footer con campi hidden per preservare query params
|
||||
- GruppoController: usa `LengthAwarePaginator` per paginare la collezione ordinata gerarchicamente
|
||||
|
||||
### In Progress
|
||||
- (none)
|
||||
### CSV Import Gruppi
|
||||
- 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
|
||||
- (none)
|
||||
### ICS Import Eventi
|
||||
- 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
|
||||
- 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)
|
||||
- Pulsante "Nuova cartella" nello stesso `<form>` del modal di upload riceve `e.preventDefault()` per sicurezza
|
||||
- Per evitare conflitti di backdrop Bootstrap 4 con modali annidati, si nasconde `#uploadModal` prima di mostrare `#newFolderModal` e lo si ripristina dopo il success
|
||||
### build-dist.sh (script di distribuzione)
|
||||
- Genera `glastree-YYYYMMDD_HHMM.tar.gz`
|
||||
- Include: sorgenti, vendor (production)
|
||||
- Esclude: .git, node_modules, tests, cache, storage content, vendor/docs/tests, Docker
|
||||
- **Istruzioni post-estrazione complete**: mkdir, permessi, configurazione, cache clear
|
||||
|
||||
## Next Steps
|
||||
- (none)
|
||||
## Bug Fix Recenti
|
||||
|
||||
## Critical Context
|
||||
- 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
|
||||
- `#newFolderModal` già esisteva con submit handler AJAX a `/documenti/cartelle` e ricaricava pagina — ora con `window._uploadNewFolder` evita reload quando chiamato dall'upload
|
||||
- Bug: backdrop grigio non spariva dopo creazione cartella perché Bootstrap 4 gestisce male i modali annidati (due backdrop concorrenti)
|
||||
### 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`
|
||||
|
||||
## Relevant Files
|
||||
- `app/Http/Controllers/HelpController.php`: metodo `downloadPdf()` + route `/help/pdf/download`
|
||||
- `app/Http/Controllers/HomeController.php`: `$stats['backups']` via `BackupService::list()`
|
||||
- `routes/web.php`: route `/help/pdf/download`
|
||||
- `resources/views/help/index.blade.php`: card-footer con pulsante download PDF
|
||||
- `resources/views/home/dashboard.blade.php`: small-box Backup condizionale
|
||||
- `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.
|
||||
|
||||
## 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -39,4 +39,35 @@ class ActivityLogController extends Controller
|
||||
|
||||
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;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\EmailAttachment;
|
||||
use App\Models\EmailFolder;
|
||||
use App\Models\EmailSetting;
|
||||
use App\Models\SenderAccount;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class EmailSettingsController extends Controller
|
||||
{
|
||||
@@ -30,11 +34,15 @@ class EmailSettingsController extends Controller
|
||||
'smtp_host' => 'nullable|string|max:255',
|
||||
'smtp_port' => 'nullable|integer|min:1|max:65535',
|
||||
'smtp_encryption' => 'nullable|in:ssl,tls,none',
|
||||
'smtp_username' => 'nullable|string|max:255',
|
||||
'smtp_password' => 'nullable|string',
|
||||
'email_address' => 'required|email',
|
||||
'email_name' => 'nullable|string|max:255',
|
||||
'reply_to' => 'nullable|email',
|
||||
'sync_interval_minutes' => 'nullable|integer|min:1|max:60',
|
||||
'is_active' => 'boolean',
|
||||
'signature' => 'nullable|string',
|
||||
'signature_enabled' => 'boolean',
|
||||
]);
|
||||
|
||||
if (!empty($validated['imap_password'])) {
|
||||
@@ -58,6 +66,24 @@ class EmailSettingsController extends Controller
|
||||
if ($verify &&
|
||||
$verify->imap_host === $validated['imap_host'] &&
|
||||
$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.');
|
||||
} else {
|
||||
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.');
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
$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]);
|
||||
}
|
||||
}
|
||||
@@ -661,9 +661,13 @@ class DocumentoController extends Controller
|
||||
'evento' => Evento::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_type'] = $typeMap[$data['visibilita_target_type']] ?? $data['visibilita_target_type'];
|
||||
$updateData['visibilita_target_type'] = $typeMap[$targetType];
|
||||
}
|
||||
|
||||
Documento::whereIn('id', $ids)->update($updateData);
|
||||
|
||||
@@ -59,7 +59,8 @@ class EmailController extends Controller
|
||||
|
||||
$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();
|
||||
$mailingLists = MailingList::where('attiva', true)->get();
|
||||
$gruppi = Gruppo::orderBy('nome')->get();
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Models\Individuo;
|
||||
use App\Models\TipologiaEvento;
|
||||
use App\Services\IcsExportService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class EventoController extends Controller
|
||||
@@ -43,7 +44,8 @@ class EventoController extends Controller
|
||||
$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();
|
||||
|
||||
return view('eventi.index', compact('eventi', 'gruppi'));
|
||||
@@ -477,4 +479,101 @@ class EventoController extends Controller
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,9 @@ use App\Models\Gruppo;
|
||||
use App\Models\Individuo;
|
||||
use App\Models\Diocesi;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Pagination\Paginator;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class GruppoController extends Controller
|
||||
{
|
||||
@@ -16,6 +19,7 @@ class GruppoController extends Controller
|
||||
$this->authorizeRead('gruppi');
|
||||
|
||||
$viewMode = request('view', 'table');
|
||||
$perPage = in_array((int) request('perPage'), [10, 20, 25, 50, 100]) ? (int) request('perPage') : 20;
|
||||
$user = auth()->user();
|
||||
|
||||
$vista = \App\Models\VistaReport::where('user_id', $user->id)
|
||||
@@ -47,7 +51,16 @@ class GruppoController extends Controller
|
||||
});
|
||||
|
||||
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 {
|
||||
$gruppi = $allGruppi;
|
||||
}
|
||||
@@ -300,4 +313,101 @@ class GruppoController extends Controller
|
||||
|
||||
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"',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\AppSetting;
|
||||
use App\Models\CalendarioConnessione;
|
||||
use App\Models\Documento;
|
||||
use App\Models\EmailSetting;
|
||||
use App\Models\Ruolo;
|
||||
use App\Models\SenderAccount;
|
||||
@@ -29,8 +31,9 @@ class ImpostazioniController extends Controller
|
||||
$senderAccounts = SenderAccount::orderBy('email_address')->get();
|
||||
$repositories = StorageRepository::orderBy('ordine')->get();
|
||||
$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', 'appSettings', 'emailSettings', 'senderAccounts', 'repositories', 'googleDriveNewToken', 'calendarioConnessioni'));
|
||||
}
|
||||
|
||||
public function saveAppSettings(Request $request)
|
||||
@@ -268,10 +271,17 @@ class ImpostazioniController extends Controller
|
||||
|
||||
$ruolo = Ruolo::findOrFail($id);
|
||||
|
||||
$count = \DB::table('gruppo_individuo')
|
||||
$allRows = \DB::table('gruppo_individuo')
|
||||
->whereNotNull('ruolo_ids')
|
||||
->whereRaw("JSON_CONTAINS(ruolo_ids, ?)", [json_encode($id)])
|
||||
->count();
|
||||
->get(['id', 'ruolo_ids']);
|
||||
|
||||
$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) {
|
||||
return redirect('/impostazioni#ruoli')->with('error', 'Impossibile eliminare: ' . $count . ' membri associati a questo ruolo.');
|
||||
@@ -388,6 +398,15 @@ class ImpostazioniController extends Controller
|
||||
$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.');
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@ class IndividuoController extends Controller
|
||||
public function index(Request $request)
|
||||
{
|
||||
$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;
|
||||
$individui = Individuo::with('contatti')->orderBy('cognome')->orderBy('nome')->paginate($perPage);
|
||||
|
||||
$vista = null;
|
||||
|
||||
@@ -210,11 +211,15 @@ public function create()
|
||||
|
||||
\Log::info('Individuo dati salvati', ['id' => $individuo->id]);
|
||||
|
||||
$contattiData = $request->input('contatti');
|
||||
\Log::info('Contatti ricevuti:', ['contatti' => $contattiData, 'tipo' => gettype($contattiData)]);
|
||||
|
||||
$individuo->contatti()->delete();
|
||||
\Log::info('Contatti precedenti eliminati, count ora:', ['count' => $individuo->contatti()->count()]);
|
||||
if ($request->has('contatti')) {
|
||||
$contattiData = $request->input('contatti');
|
||||
\Log::info('Contatti ricevuti:', ['contatti' => $contattiData, 'tipo' => gettype($contattiData)]);
|
||||
|
||||
$individuo->contatti()->delete();
|
||||
\Log::info('Contatti precedenti eliminati, count ora:', ['count' => $individuo->contatti()->count()]);
|
||||
} else {
|
||||
$contattiData = null;
|
||||
}
|
||||
|
||||
if (!empty($contattiData) && is_array($contattiData)) {
|
||||
$created = 0;
|
||||
|
||||
@@ -602,7 +602,7 @@ class ReportController extends Controller
|
||||
'lista' => $lista->nome,
|
||||
'stato' => $lista->attiva ? 'Attiva' : 'Disattiva',
|
||||
'individuo' => $contatto->individuo?->nome_completo ?? 'N/D',
|
||||
'email' => $contatto->email ?? '-',
|
||||
'email' => $contatto->valore ?? '-',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
+29
-30
@@ -1,31 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class AppSetting extends Model
|
||||
{
|
||||
protected $table = 'app_settings';
|
||||
|
||||
protected $fillable = [
|
||||
'logo',
|
||||
'logo_small',
|
||||
'logo_path',
|
||||
'logo_small_path',
|
||||
'nome_applicazione',
|
||||
'nome_organizzazione',
|
||||
'footer_text',
|
||||
'app_version',
|
||||
'dashboard_welcome',
|
||||
'show_version',
|
||||
'backup_path',
|
||||
'backup_retention_days',
|
||||
'backup_include_files',
|
||||
'backup_include_env',
|
||||
'backup_auto_enabled',
|
||||
'backup_auto_frequency',
|
||||
'backup_auto_hour',
|
||||
'logo', 'logo_small', 'logo_path', 'logo_small_path',
|
||||
'nome_applicazione', 'nome_organizzazione',
|
||||
'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',
|
||||
'documenti_storage_disk', 'documenti_storage_path',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
@@ -36,11 +30,16 @@ class AppSetting extends Model
|
||||
'backup_include_env' => 'boolean',
|
||||
'backup_auto_enabled' => 'boolean',
|
||||
'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();
|
||||
return $setting?->{$key} ?? $default;
|
||||
}
|
||||
@@ -110,38 +109,38 @@ class AppSetting extends Model
|
||||
|
||||
public function getLogoUrlInstance(): ?string
|
||||
{
|
||||
if ($this->logo && file_exists(public_path('storage/' . $this->logo))) {
|
||||
return asset('storage/' . $this->logo);
|
||||
if ($this->logo && Storage::disk('public')->exists($this->logo)) {
|
||||
return Storage::disk('public')->url($this->logo);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getLogoSmallUrlInstance(): ?string
|
||||
{
|
||||
if ($this->logo_small && file_exists(public_path('storage/' . $this->logo_small))) {
|
||||
return asset('storage/' . $this->logo_small);
|
||||
if ($this->logo_small && Storage::disk('public')->exists($this->logo_small)) {
|
||||
return Storage::disk('public')->url($this->logo_small);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getLogoPath(): ?string
|
||||
{
|
||||
if ($this->logo_path && file_exists(public_path('storage/' . $this->logo_path))) {
|
||||
return asset('storage/' . $this->logo_path);
|
||||
if ($this->logo_path && Storage::disk('public')->exists($this->logo_path)) {
|
||||
return Storage::disk('public')->url($this->logo_path);
|
||||
}
|
||||
if ($this->logo && file_exists(public_path('storage/' . $this->logo))) {
|
||||
return asset('storage/' . $this->logo);
|
||||
if ($this->logo && Storage::disk('public')->exists($this->logo)) {
|
||||
return Storage::disk('public')->url($this->logo);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getLogoSmallPath(): ?string
|
||||
{
|
||||
if ($this->logo_small_path && file_exists(public_path('storage/' . $this->logo_small_path))) {
|
||||
return asset('storage/' . $this->logo_small_path);
|
||||
if ($this->logo_small_path && Storage::disk('public')->exists($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))) {
|
||||
return asset('storage/' . $this->logo_small);
|
||||
if ($this->logo_small && Storage::disk('public')->exists($this->logo_small)) {
|
||||
return Storage::disk('public')->url($this->logo_small);
|
||||
}
|
||||
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),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ class Evento extends Model
|
||||
'is_incontro_gruppo',
|
||||
'luogo_indirizzo',
|
||||
'luogo_url_maps',
|
||||
'uid_esterno',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
|
||||
@@ -9,8 +9,8 @@ class VistaReport extends Model
|
||||
{
|
||||
protected $table = 'viste_report';
|
||||
protected $fillable = [
|
||||
'user_id', 'nome', 'tipo', 'colonne_visibili',
|
||||
'colonne_ordinamento', 'filtri', 'ricerca'
|
||||
'user_id', 'nome', 'tipo', 'colonne_visibili',
|
||||
'colonne_ordinamento', 'filtri', 'ricerca', 'is_default'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
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",
|
||||
"as247/flysystem-google-drive": "^3.0",
|
||||
"directorytree/imapengine-laravel": "^1.2",
|
||||
"google/apiclient": "^2.19",
|
||||
"laravel/framework": "^13.7",
|
||||
"laravel/tinker": "^3.0",
|
||||
"league/flysystem-webdav": "^3.31",
|
||||
"sabre/vobject": "^4.6",
|
||||
"spatie/laravel-permission": "^7.4",
|
||||
"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",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "c6ab297804fe432e1792b166459e6469",
|
||||
"content-hash": "c594ad5c5f9ff96dc8644231135f694e",
|
||||
"packages": [
|
||||
{
|
||||
"name": "as247/cloud-storages",
|
||||
@@ -4271,16 +4271,16 @@
|
||||
},
|
||||
{
|
||||
"name": "sabre/vobject",
|
||||
"version": "4.5.8",
|
||||
"version": "4.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sabre-io/vobject.git",
|
||||
"reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1"
|
||||
"reference": "9432544fc369851fb8202c5d91159b2e669f0c88"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sabre-io/vobject/zipball/d554eb24d64232922e1eab5896cc2f84b3b9ffb1",
|
||||
"reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1",
|
||||
"url": "https://api.github.com/repos/sabre-io/vobject/zipball/9432544fc369851fb8202c5d91159b2e669f0c88",
|
||||
"reference": "9432544fc369851fb8202c5d91159b2e669f0c88",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4371,7 +4371,7 @@
|
||||
"issues": "https://github.com/sabre-io/vobject/issues",
|
||||
"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",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
{
|
||||
Schema::table('app_settings', function (Blueprint $table) {
|
||||
$table->string('logo_path')->nullable()->after('logo');
|
||||
$table->string('logo_small_path')->nullable()->after('logo_small');
|
||||
});
|
||||
if (!Schema::hasTable('app_settings')) {
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -8,12 +8,24 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('app_settings', function (Blueprint $table) {
|
||||
$table->string('footer_text')->nullable()->after('nome_organizzazione');
|
||||
$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');
|
||||
});
|
||||
if (!Schema::hasTable('app_settings')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$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
|
||||
|
||||
@@ -8,10 +8,21 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('app_settings', function (Blueprint $table) {
|
||||
$table->string('documenti_storage_disk')->default('local')->after('show_version');
|
||||
$table->string('documenti_storage_path')->default('documenti')->after('documenti_storage_disk');
|
||||
});
|
||||
if (!Schema::hasTable('app_settings')) {
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -10,15 +10,31 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('app_settings', function (Blueprint $table) {
|
||||
$table->string('backup_path')->default('backups')->after('show_version');
|
||||
$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');
|
||||
$table->boolean('backup_auto_enabled')->default(false)->after('backup_include_env');
|
||||
$table->string('backup_auto_frequency', 20)->default('daily')->after('backup_auto_enabled');
|
||||
$table->integer('backup_auto_hour')->default(3)->after('backup_auto_frequency');
|
||||
});
|
||||
if (!Schema::hasTable('app_settings')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$columns = [
|
||||
'backup_path' => ['type' => 'string', 'default' => 'backups'],
|
||||
'backup_retention_days' => ['type' => 'integer', 'default' => 30],
|
||||
'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
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('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,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('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
|
||||
{
|
||||
}
|
||||
};
|
||||
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();
|
||||
@@ -14,9 +14,14 @@
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<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 class="card-body">
|
||||
<form method="GET" class="mb-3">
|
||||
<form method="GET" class="mb-3" id="filter-form">
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<select name="user_id" class="form-control">
|
||||
@@ -94,4 +99,59 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@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 class="form-group">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="hidden" name="is_active" value="0">
|
||||
<input type="checkbox" class="custom-control-input" id="is_active"
|
||||
name="is_active" value="1" {{ ($settings->is_active ?? false) ? 'checked' : '' }}>
|
||||
<label class="custom-control-label" for="is_active">Account Email Attivo</label>
|
||||
@@ -265,6 +266,7 @@
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="hidden" name="signature_enabled" value="0">
|
||||
<input type="checkbox" class="custom-control-input" id="signature_enabled"
|
||||
name="signature_enabled" value="1" {{ ($settings->signature_enabled ?? true) ? 'checked' : '' }}>
|
||||
<label class="custom-control-label" for="signature_enabled">Attiva firma automatica</label>
|
||||
@@ -377,7 +379,7 @@
|
||||
</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">
|
||||
<i class="fas fa-save mr-1"></i> Salva Impostazioni
|
||||
</button>
|
||||
@@ -392,6 +394,15 @@
|
||||
</a>
|
||||
</div>
|
||||
</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>
|
||||
@endsection
|
||||
@@ -521,7 +532,7 @@ function editSender(id) {
|
||||
const sender = senderAccounts.find(s => s.id === id);
|
||||
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('senderModalTitle').textContent = 'Modifica Mittente - ' + sender.email_address;
|
||||
document.getElementById('senderId').value = sender.id;
|
||||
@@ -551,7 +562,7 @@ function testSenderSmtp(id) {
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
|
||||
$.ajax({
|
||||
url: '{{ route('impostazioni.sender.test', '') }}/' + id,
|
||||
url: '{{ route('impostazioni.sender.test', '__ID__') }}'.replace('__ID__', id),
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
|
||||
data: { email: email },
|
||||
@@ -628,9 +639,11 @@ function testSmtp() {
|
||||
});
|
||||
}
|
||||
|
||||
var quillEditorInstance = null;
|
||||
|
||||
function createEditor() {
|
||||
var container = document.querySelector('#signature-editor');
|
||||
if (!container || container.querySelector('.ql-toolbar')) return;
|
||||
if (!container || quillEditorInstance) return;
|
||||
|
||||
var quill = new Quill('#signature-editor', {
|
||||
theme: 'snow',
|
||||
@@ -679,10 +692,12 @@ function createEditor() {
|
||||
signatureInput.value = quill.root.innerHTML;
|
||||
}
|
||||
});
|
||||
|
||||
quillEditorInstance = quill;
|
||||
}
|
||||
|
||||
function initSignatureEditor() {
|
||||
if (document.querySelector('#signature-editor .ql-toolbar')) return;
|
||||
if (quillEditorInstance) return;
|
||||
|
||||
if (typeof Quill === 'undefined') {
|
||||
var script = document.createElement('script');
|
||||
|
||||
@@ -59,7 +59,7 @@ $appOrgName = AppSetting::getOrgName() ?? '';
|
||||
<div class="row">
|
||||
<div class="col-8">
|
||||
<div class="icheck-primary">
|
||||
<input type="checkbox" id="remember">
|
||||
<input type="checkbox" id="remember" name="remember">
|
||||
<label for="remember">Ricordami</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -131,8 +131,29 @@ $currentFolder = $folders->firstWhere('type', $folder);
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
{{ $messages->withQueryString()->links('vendor.pagination.simple-bootstrap-4') }}
|
||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<form method="GET" class="form-inline" id="perPageForm">
|
||||
<label class="mr-2 small">Righe per pagina:</label>
|
||||
<select name="perPage" class="form-control form-control-sm" onchange="this.form.submit()" style="width: auto;">
|
||||
@foreach([10, 20, 25, 50, 100] as $p)
|
||||
<option value="{{ $p }}" {{ (int) request('perPage', 20) === $p ? 'selected' : '' }}>{{ $p }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@foreach(request()->except(['perPage', 'page']) as $key => $value)
|
||||
@if(is_array($value))
|
||||
@foreach($value as $v)
|
||||
<input type="hidden" name="{{ $key }}[]" value="{{ $v }}">
|
||||
@endforeach
|
||||
@else
|
||||
<input type="hidden" name="{{ $key }}" value="{{ $value }}">
|
||||
@endif
|
||||
@endforeach
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
{{ $messages->links('vendor.pagination.simple-bootstrap-4') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
<a href="{{ route('eventi.create') }}" class="btn btn-success btn-sm">
|
||||
<i class="fas fa-plus mr-1"></i> Nuovo Evento
|
||||
</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">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-success btn-sm">
|
||||
@@ -40,10 +43,44 @@
|
||||
@section('scripts')
|
||||
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.10/index.global.min.js"></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() {
|
||||
var calendarEl = document.getElementById('calendar');
|
||||
|
||||
var calendar = new FullCalendar.Calendar(calendarEl, {
|
||||
window.calendar = new FullCalendar.Calendar(calendarEl, {
|
||||
initialView: 'dayGridMonth',
|
||||
locale: 'it',
|
||||
headerToolbar: {
|
||||
@@ -97,7 +134,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
|
||||
calendar.render();
|
||||
window.calendar.render();
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
@@ -0,0 +1,55 @@
|
||||
@extends('layouts.adminlte')
|
||||
@section('title', 'Importa Eventi')
|
||||
@section('page_title', 'Importa Eventi da ICS')
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-upload mr-2"></i>Carica file ICS</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ route('eventi.import.store') }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<div class="form-group">
|
||||
<label for="file">Seleziona file ICS *</label>
|
||||
<input type="file" name="file" id="file" class="form-control" accept=".ics" required>
|
||||
<small class="form-text text-muted">
|
||||
Il file deve essere in formato ICS (iCalendar).
|
||||
</small>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-upload mr-1"></i> Importa
|
||||
</button>
|
||||
<a href="/eventi" class="btn btn-secondary ml-2">
|
||||
Annulla
|
||||
</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-info-circle mr-2"></i>Informazioni</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6>Campi importati:</h6>
|
||||
<ul>
|
||||
<li><code>SUMMARY</code> → Nome evento (obbligatorio)</li>
|
||||
<li><code>DESCRIPTION</code> → Descrizione</li>
|
||||
<li><code>DTSTART</code> → Data/Ora inizio</li>
|
||||
<li><code>DTEND</code> → Durata</li>
|
||||
<li><code>LOCATION</code> → Luogo</li>
|
||||
<li><code>UID</code> → ID esterno (per evitare duplicati)</li>
|
||||
</ul>
|
||||
<p class="text-muted small mt-2">
|
||||
<i class="fas fa-info-circle mr-1"></i>
|
||||
Gli eventi con UID già presente nel sistema verranno saltati automaticamente.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -34,6 +34,9 @@ $canDeleteEventi = Auth::user()->canDelete('eventi');
|
||||
</div>
|
||||
@endif
|
||||
@if($canWriteEventi)
|
||||
<a href="/eventi/import" class="btn btn-sm btn-warning mr-2">
|
||||
<i class="fas fa-file-import mr-1"></i> Importa ICS
|
||||
</a>
|
||||
<a href="/eventi/create" class="btn btn-xs btn-success">
|
||||
<i class="fas fa-plus mr-1"></i> Nuovo Evento
|
||||
</a>
|
||||
@@ -247,8 +250,29 @@ $canDeleteEventi = Auth::user()->canDelete('eventi');
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
{{ $eventi->withQueryString()->links() }}
|
||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<form method="GET" class="form-inline" id="perPageForm">
|
||||
<label class="mr-2 small">Righe per pagina:</label>
|
||||
<select name="perPage" class="form-control form-control-sm" onchange="this.form.submit()" style="width: auto;">
|
||||
@foreach([10, 20, 25, 50, 100] as $p)
|
||||
<option value="{{ $p }}" {{ (int) request('perPage', 20) === $p ? 'selected' : '' }}>{{ $p }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@foreach(request()->except(['perPage', 'page']) as $key => $value)
|
||||
@if(is_array($value))
|
||||
@foreach($value as $v)
|
||||
<input type="hidden" name="{{ $key }}[]" value="{{ $v }}">
|
||||
@endforeach
|
||||
@else
|
||||
<input type="hidden" name="{{ $key }}" value="{{ $value }}">
|
||||
@endif
|
||||
@endforeach
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
{{ $eventi->links() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -211,11 +211,6 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-file mr-2"></i>Documenti</h3>
|
||||
@@ -304,7 +299,7 @@
|
||||
</a>
|
||||
<form action="/eventi/{{ $evento->id }}" method="POST" class="d-inline">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn btn-danger" onclick="return confirm('Confermi l\'eliminazione di {{ $evento->nome_evento }}?')">
|
||||
<button type="submit" class="btn btn-danger" onclick="return confirm('Confermi l\'eliminazione di {{ $evento->nome_evento }}?'.replace(/'/g, '\\\''))">
|
||||
<i class="fas fa-trash mr-1"></i> Elimina
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -152,8 +152,7 @@
|
||||
</tr>
|
||||
<tr id="membro-edit-{{ $membro->id }}" style="display:none;">
|
||||
<td colspan="6">
|
||||
<form action="/gruppi/{{ $gruppo->id }}/membri/{{ $membro->id }}" method="POST" class="mb-0">
|
||||
@csrf @method('PUT')
|
||||
<div class="mb-0">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<input type="text" class="form-control form-control-sm" value="{{ $membro->cognome }} {{ $membro->nome }}" disabled>
|
||||
@@ -162,7 +161,7 @@
|
||||
<input type="text" class="form-control form-control-sm" value="{{ $membro->codice_id }}" disabled>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select name="ruolo_ids[]" class="form-control form-control-sm" multiple size="3">
|
||||
<select id="edit-membro-ruoli-{{ $membro->id }}" class="form-control form-control-sm" multiple size="3">
|
||||
@foreach(\App\Models\Ruolo::attive() as $ruolo)
|
||||
@php $selected = in_array($ruolo->id, $membro->getRuoloIdsForGruppo($gruppo->id)) @endphp
|
||||
<option value="{{ $ruolo->id }}" {{ $selected ? 'selected' : '' }}>{{ $ruolo->nome }}</option>
|
||||
@@ -170,14 +169,14 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="date" name="data_adesione" class="form-control form-control-sm" value="{{ $membro->pivot->data_adesione }}">
|
||||
<input type="date" id="edit-membro-data-{{ $membro->id }}" class="form-control form-control-sm" value="{{ $membro->pivot->data_adesione }}">
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<button type="submit" class="btn btn-xs btn-success"><i class="fas fa-check"></i></button>
|
||||
<button type="button" class="btn btn-xs btn-success" onclick="submitEditMembro({{ $membro->id }})"><i class="fas fa-check"></i></button>
|
||||
<button type="button" class="btn btn-xs btn-secondary" onclick="cancelEditMembro({{ $membro->id }})"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@@ -201,17 +200,13 @@
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div id="documento-add-form" style="display:none;" class="p-3 bg-light border-bottom">
|
||||
<form action="/documenti" method="POST" enctype="multipart/form-data" class="mb-0">
|
||||
@csrf
|
||||
<input type="hidden" name="visibilita" value="gruppo">
|
||||
<input type="hidden" name="visibilita_target_id" value="{{ $gruppo->id }}">
|
||||
<input type="hidden" name="visibilita_target_type" value="App\Models\Gruppo">
|
||||
<div class="mb-0">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<input type="text" name="nome_file" class="form-control form-control-sm" placeholder="Nome documento" required>
|
||||
<input type="text" id="doc-nome-file" class="form-control form-control-sm" placeholder="Nome documento">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select name="tipologia" class="form-control form-control-sm" required>
|
||||
<select id="doc-tipologia" class="form-control form-control-sm">
|
||||
<option value="">Tipologia...</option>
|
||||
<option value="documento">Documento</option>
|
||||
<option value="statuto">Statuto</option>
|
||||
@@ -221,14 +216,14 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<input type="file" name="file" class="form-control form-control-sm" required>
|
||||
<input type="file" id="doc-file" class="form-control form-control-sm">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-xs btn-success"><i class="fas fa-upload mr-1"></i>Carica</button>
|
||||
<button type="button" class="btn btn-xs btn-success" onclick="submitUploadDocumentoGruppo()"><i class="fas fa-upload mr-1"></i>Carica</button>
|
||||
<button type="button" class="btn btn-xs btn-secondary" onclick="hideDocumentoForm()"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($gruppo->documenti && $gruppo->documenti->count() > 0)
|
||||
@@ -258,12 +253,9 @@
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
@endif
|
||||
<form action="/documenti/{{ $documento->id }}" method="POST" class="d-inline">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn btn-xs btn-danger" onclick="return confirm('Eliminare questo documento?')" title="Elimina">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
<button type="button" class="btn btn-xs btn-danger" onclick="deleteDocumentoFromEdit({{ $documento->id }})" title="Elimina">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@@ -473,5 +465,83 @@ function previewDocumento(id, mimeType) {
|
||||
document.getElementById('previewDownloadBtn').href = '/documenti/' + id + '/download';
|
||||
$('#previewModal').modal('show');
|
||||
}
|
||||
|
||||
function submitEditMembro(membroId) {
|
||||
var ruoloSelect = document.getElementById('edit-membro-ruoli-' + membroId);
|
||||
var dataInput = document.getElementById('edit-membro-data-' + membroId);
|
||||
var formData = new URLSearchParams();
|
||||
|
||||
var selectedRuoli = Array.from(ruoloSelect.selectedOptions).map(opt => opt.value).filter(v => v);
|
||||
selectedRuoli.forEach(function(ruoloId) {
|
||||
formData.append('ruolo_ids[]', ruoloId);
|
||||
});
|
||||
formData.append('data_adesione', dataInput ? dataInput.value : '');
|
||||
|
||||
fetch('/gruppi/{{ $gruppo->id }}/membri/' + membroId, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: formData.toString()
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) return response.text().then(text => { throw new Error(text); });
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) window.location.reload();
|
||||
else alert(data.error || 'Errore');
|
||||
})
|
||||
.catch(error => alert('Errore: ' + error.message));
|
||||
}
|
||||
|
||||
function submitUploadDocumentoGruppo() {
|
||||
var nomeFile = document.getElementById('doc-nome-file');
|
||||
var tipologia = document.getElementById('doc-tipologia');
|
||||
var fileInput = document.getElementById('doc-file');
|
||||
|
||||
if (!nomeFile.value || !tipologia.value || !fileInput.files[0]) {
|
||||
alert('Compila tutti i campi');
|
||||
return;
|
||||
}
|
||||
|
||||
var formData = new FormData();
|
||||
formData.append('_token', '{{ csrf_token() }}');
|
||||
formData.append('visibilita', 'gruppo');
|
||||
formData.append('visibilita_target_id', '{{ $gruppo->id }}');
|
||||
formData.append('visibilita_target_type', 'App\\Models\\Gruppo');
|
||||
formData.append('nome_file', nomeFile.value);
|
||||
formData.append('tipologia', tipologia.value);
|
||||
formData.append('file', fileInput.files[0]);
|
||||
|
||||
document.getElementById('documento-add-form').querySelector('.btn-success').disabled = true;
|
||||
|
||||
fetch('/documenti', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: { 'Accept': 'application/json' }
|
||||
})
|
||||
.then(function() { window.location.reload(); })
|
||||
.catch(function() { window.location.reload(); });
|
||||
}
|
||||
|
||||
function deleteDocumentoFromEdit(documentoId) {
|
||||
if (!confirm('Eliminare questo documento?')) return;
|
||||
|
||||
var formData = new FormData();
|
||||
formData.append('_token', '{{ csrf_token() }}');
|
||||
formData.append('_method', 'DELETE');
|
||||
formData.append('_redirect', '{{ url()->current() }}');
|
||||
|
||||
fetch('/documenti/' + documentoId, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: { 'Accept': 'application/json' }
|
||||
})
|
||||
.then(function() { window.location.reload(); })
|
||||
.catch(function() { window.location.reload(); });
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
@@ -0,0 +1,64 @@
|
||||
@extends('layouts.adminlte')
|
||||
@section('title', 'Importa Gruppi')
|
||||
@section('page_title', 'Importa Gruppi da CSV')
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-upload mr-2"></i>Carica file CSV</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ route('gruppi.import.store') }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<div class="form-group">
|
||||
<label for="file">Seleziona file CSV *</label>
|
||||
<input type="file" name="file" id="file" class="form-control" accept=".csv,.txt" required>
|
||||
<small class="form-text text-muted">
|
||||
Il file deve essere in formato CSV con separatore virgola.
|
||||
</small>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-upload mr-1"></i> Importa
|
||||
</button>
|
||||
<a href="{{ route('gruppi.index') }}" class="btn btn-secondary ml-2">
|
||||
Annulla
|
||||
</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header bg-info">
|
||||
<h3 class="card-title text-white"><i class="fas fa-download mr-2"></i>Template</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Scarica il template CSV per l'importazione:</p>
|
||||
<a href="{{ route('gruppi.template') }}" class="btn btn-success btn-block">
|
||||
<i class="fas fa-file-csv mr-1"></i> Scarica Template
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-info-circle mr-2"></i>Formato CSV</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6>Campi:</h6>
|
||||
<ul>
|
||||
<li><code>nome</code> (obbligatorio)</li>
|
||||
<li><code>descrizione</code></li>
|
||||
<li><code>parent_id</code> (ID del gruppo padre)</li>
|
||||
<li><code>diocesi_id</code> (ID della diocesi)</li>
|
||||
<li><code>indirizzo_incontro</code></li>
|
||||
<li><code>cap_incontro</code></li>
|
||||
<li><code>città_incontro</code></li>
|
||||
<li><code>sigla_provincia_incontro</code> (2 lettere)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -55,6 +55,9 @@ $visibleColumnsJson = json_encode($visibleColumns);
|
||||
<i class="fas fa-save mr-1"></i> Salva Vista
|
||||
</button>
|
||||
@if($canWriteGruppi)
|
||||
<a href="{{ route('gruppi.import') }}" class="btn btn-warning btn-sm mr-1">
|
||||
<i class="fas fa-file-import mr-1"></i> Importa
|
||||
</a>
|
||||
<a href="{{ route('gruppi.create') }}" class="btn btn-success btn-sm">
|
||||
<i class="fas fa-plus"></i> Nuovo Gruppo
|
||||
</a>
|
||||
@@ -174,6 +177,30 @@ $visibleColumnsJson = json_encode($visibleColumns);
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<form method="GET" class="form-inline" id="perPageForm">
|
||||
<label class="mr-2 small">Righe per pagina:</label>
|
||||
<select name="perPage" class="form-control form-control-sm" onchange="this.form.submit()" style="width: auto;">
|
||||
@foreach([10, 20, 25, 50, 100] as $p)
|
||||
<option value="{{ $p }}" {{ (int) request('perPage', 20) === $p ? 'selected' : '' }}>{{ $p }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@foreach(request()->except(['perPage', 'page']) as $key => $value)
|
||||
@if(is_array($value))
|
||||
@foreach($value as $v)
|
||||
<input type="hidden" name="{{ $key }}[]" value="{{ $v }}">
|
||||
@endforeach
|
||||
@else
|
||||
<input type="hidden" name="{{ $key }}" value="{{ $value }}">
|
||||
@endif
|
||||
@endforeach
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
{{ $gruppi->links() }}
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="card-body p-0">
|
||||
<div class="tree-view" style="padding: 15px;">
|
||||
|
||||
@@ -12,6 +12,7 @@ $canDeleteGruppi = Auth::user()->canDelete('gruppi');
|
||||
<li class="breadcrumb-item active">{{ $gruppo->nome }}</li>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success alert-dismissible">
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
@@ -24,8 +25,6 @@ $canDeleteGruppi = Auth::user()->canDelete('gruppi');
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="card card-success">
|
||||
|
||||
@@ -27,6 +27,11 @@
|
||||
<i class="fab fa-google-drive mr-2"></i> Google Drive
|
||||
</a>
|
||||
</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">
|
||||
<a class="nav-link" href="#help-tipologie" data-toggle="tab">
|
||||
<i class="fas fa-tags mr-2"></i> Tipologie e Dati
|
||||
@@ -194,6 +199,75 @@
|
||||
</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 --}}
|
||||
<div class="tab-pane" id="help-tipologie">
|
||||
<div class="card card-outline card-success">
|
||||
@@ -287,7 +361,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Backup e Migrazione --}}
|
||||
<div class="tab-pane" id="help-backup">
|
||||
@@ -471,9 +544,8 @@ sudo certbot --apache -d glastree.esempio.it</code></pre>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<h5>PHP Web Installer</h5>
|
||||
<p>L'applicazione include un <strong>wizard di installazione web</strong> nella cartella <code>installer/</code>. Questo wizard gestisce sia installazioni fresh che ripristino da backup.</p>
|
||||
<p>Vedi la sezione <strong>Installazione</strong> per la guida completa.</p>
|
||||
<h5>Comandi Artisan</h5>
|
||||
<p>Per installazione e ripristino usa i comandi Artisan da terminale. Vedi la sezione <strong>Installazione</strong> per la guida completa.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -777,7 +849,7 @@ echo 'Utente creato con successo!';
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5>Panoramica</h5>
|
||||
<p>l'applicazione include un <strong>wizard di installazione web</strong> che guida passo-passo nella configurazione su un nuovo server LAMP. Supporta due modalita:</p>
|
||||
<p>L'installazione avviene tramite l'interattivo <code>install.php</code> che guida passo-passo. Supporta due modalita:</p>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="callout callout-info">
|
||||
@@ -831,19 +903,37 @@ git clone <URL_REPOSITORY> glastree
|
||||
# Opzione B — ZIP (se non hai git)
|
||||
# unzip /percorso/del/glastree.zip -d glastree
|
||||
|
||||
cd glastree
|
||||
composer install --no-dev --optimize-autoloader
|
||||
npm ci && npm run build</code></pre>
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Se non hai accesso a npm, puoi saltare il build. L'interfaccia usera comunque AdminLTE via CDN.
|
||||
cd glastree</code></pre>
|
||||
|
||||
<h6>Passo 3: Eseguire l'installer</h6>
|
||||
<p>Lancerai l'installer interattivo che ti guidera attraverso tutte le fasi:</p>
|
||||
<pre><code>php install.php</code></pre>
|
||||
<p>L'installer ti chiedera:</p>
|
||||
<ol>
|
||||
<li><strong>Modalità</strong>: Fresh Install (Apache/Docker) o Restore da Backup</li>
|
||||
<li><strong>Parametri generali</strong>: nome sito, amministratore (nome, email, password), URL pubblico</li>
|
||||
<li><strong>Database</strong>: MySQL (host, porta, nome, utente, password) o SQLite</li>
|
||||
</ol>
|
||||
<p>L'installer esegue automaticamente:</p>
|
||||
<ul>
|
||||
<li>Generazione <code>.env</code> e <code>APP_KEY</code></li>
|
||||
<li>Creazione del database</li>
|
||||
<li><code>composer install</code> e registrazione pacchetti</li>
|
||||
<li>Migration e seed dei dati di base</li>
|
||||
<li>Creazione dell'utente amministratore</li>
|
||||
<li>Compilazione asset frontend (se npm disponibile)</li>
|
||||
<li>Impostazione permessi cartelle</li>
|
||||
</ul>
|
||||
<div class="callout callout-info">
|
||||
<h6><i class="fas fa-info-circle"></i> Docker</h6>
|
||||
<p>Se scegli la modalita Docker, l'installer builda l'immagine, avvia i container e crea l'admin all'interno del container.</p>
|
||||
</div>
|
||||
<div class="callout callout-success">
|
||||
<h6><i class="fas fa-upload"></i> Restore da Backup</h6>
|
||||
<p>Se scegli Restore, l'installer ti chiedera il percorso del file ZIP di backup, ripristinera il database, i file e la configurazione.</p>
|
||||
</div>
|
||||
|
||||
<h6>Passo 3: Permessi cartelle</h6>
|
||||
<pre><code>sudo chown -R www-data:www-data storage bootstrap/cache
|
||||
sudo chmod -R 775 storage bootstrap/cache</code></pre>
|
||||
|
||||
<h6>Passo 4: Configurare Apache</h6>
|
||||
<h6 class="mt-4">Passo 4: Configurare Apache (solo modalita Fresh Install)</h6>
|
||||
<pre><code>sudo tee /etc/apache2/sites-available/glastree.conf <<APACHE
|
||||
<VirtualHost *:80>
|
||||
ServerName glastree.esempio.it
|
||||
@@ -863,55 +953,29 @@ APACHE
|
||||
sudo a2dissite 000-default.conf
|
||||
sudo a2ensite glastree.conf
|
||||
sudo systemctl reload apache2</code></pre>
|
||||
|
||||
<h6>Passo 5: Avviare il wizard di installazione</h6>
|
||||
<p>Apri il browser e visita l'indirizzo del tuo server:</p>
|
||||
<pre><code>http://glastree.esempio.it/installer/</code></pre>
|
||||
|
||||
<p>Il wizard ti guidera attraverso 6 passi:</p>
|
||||
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead><tr><th>Passo</th><th>Descrizione</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>1. Benvenuto</td><td>Scegli Fresh Install o Restore da Backup</td></tr>
|
||||
<tr><td>2. Requisiti</td><td>Verifica PHP, estensioni e permessi</td></tr>
|
||||
<tr><td>3. Database</td><td>Configura MySQL (crea nuovo o usa esistente)</td></tr>
|
||||
<tr><td>4. Installazione</td><td>Fresh: migration e seed. Backup: upload ZIP</td></tr>
|
||||
<tr><td>5. Amministratore</td><td>Fresh: crea super-admin. Backup: verifica utenti</td></tr>
|
||||
<tr><td>6. Finalizzazione</td><td>Cache, storage link, auto-eliminazione installer</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<strong>Attenzione:</strong> Al termine dell'installazione, la cartella <code>installer/</code> viene <strong>automaticamente eliminata</strong>. Non puoi rieseguire l'installazione se non ricreando manualmente la cartella.
|
||||
<strong>Importante:</strong> Il restore da backup richiede che tu abbia un file ZIP generato dalla pagina <strong>Admin → Backup</strong> del server originale. Estrai manualmente il contenuto seguendo la struttura del progetto.
|
||||
</div>
|
||||
|
||||
<h5 class="mt-4">Modalita Fresh Install</h5>
|
||||
<p>Seleziona questa modalita quando installi l'applicazione per la prima volta su un server.</p>
|
||||
<p>Cosa succede durante l'installazione:</p>
|
||||
<h5 class="mt-4">Cosa succede durante l'installazione</h5>
|
||||
<p>I comandi <code>php artisan migrate --seed</code> eseguono:</p>
|
||||
<ol>
|
||||
<li>Genera <code>APP_KEY</code> per la crittografia</li>
|
||||
<li>Genera <code>APP_KEY</code> per la crittografia (gia eseguito al passo 5)</li>
|
||||
<li>Esegue tutte le migration per creare le tabelle del database</li>
|
||||
<li>Popola i dati di base (diocesi, comuni, ruoli, preset)</li>
|
||||
<li>Crea il collegamento <code>storage → public/storage</code></li>
|
||||
<li>Crea l'utente amministratore con i permessi completi</li>
|
||||
</ol>
|
||||
|
||||
<h5 class="mt-4">Modalita Restore da Backup</h5>
|
||||
<p>Seleziona questa modalita quando vuoi migrare un'installazione esistente su un nuovo server.</p>
|
||||
<p>Cosa serve:</p>
|
||||
<h5 class="mt-4">Restore da Backup</h5>
|
||||
<p>Il backup ZIP generato dalla pagina <strong>Admin → Backup</strong> contiene un dump SQL e i file dei documenti. Per ripristinare:</p>
|
||||
<ul>
|
||||
<li>Un file ZIP generato dalla pagina <strong>Admin → Backup</strong> del server originale</li>
|
||||
<li>Il wizard accetta upload diretto del file o un percorso sul server</li>
|
||||
<li>Estrai il file ZIP in una cartella temporanea</li>
|
||||
<li>Importa il database dal file SQL (<code>mysql -u root -p glastree < backup/database.sql</code>)</li>
|
||||
<li>Copia i file in <code>storage/app/</code> per ripristinare i documenti</li>
|
||||
<li>Copia il file <code>.env</code> dal backup (modifica le credenziali DB per il nuovo server)</li>
|
||||
<li>Esegui <code>php artisan key:generate</code> se la <code>APP_KEY</code> del backup non è utilizzabile</li>
|
||||
</ul>
|
||||
<p>Cosa succede durante il restore:</p>
|
||||
<ol>
|
||||
<li>Estrae il file ZIP in una cartella temporanea</li>
|
||||
<li>Importa <code>database.sql</code> nel database MySQL configurato</li>
|
||||
<li>Copia i file documenti in <code>storage/app/documenti/</code></li>
|
||||
<li>Ripristina il file <code>.env</code> dal backup (preservando le credenziali DB del nuovo server)</li>
|
||||
</ol>
|
||||
|
||||
<h5 class="mt-4">Dopo l'Installazione</h5>
|
||||
<ol>
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
<ol>
|
||||
<li><a href="#webdav">WebDAV / Nextcloud</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="#email">Email / SMTP</a></li>
|
||||
<li><a href="#installazione">Guida all'Installazione</a></li>
|
||||
@@ -182,8 +183,72 @@
|
||||
|
||||
<div class="page-break"></div>
|
||||
|
||||
{{-- 3. Tipologie e Dati --}}
|
||||
<h2 id="tipologie">3. Tipologie e Gestione Dati</h2>
|
||||
{{-- 3. Google Calendar --}}
|
||||
<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>
|
||||
<p>Le tipologie classificano i documenti (avatar, galleria, documento, statuto, altro).</p>
|
||||
@@ -210,8 +275,8 @@
|
||||
|
||||
<div class="page-break"></div>
|
||||
|
||||
{{-- 4. Email / SMTP --}}
|
||||
<h2 id="email">4. Email / SMTP</h2>
|
||||
{{-- 5. Email / SMTP --}}
|
||||
<h2 id="email">5. Email / SMTP</h2>
|
||||
|
||||
<h3>Configurazione Email</h3>
|
||||
<p>Vai in <strong>Impostazioni → Email</strong> per configurare la posta elettronica.</p>
|
||||
@@ -264,11 +329,11 @@
|
||||
|
||||
<div class="page-break"></div>
|
||||
|
||||
{{-- 5. Installazione --}}
|
||||
<h2 id="installazione">5. Guida all'Installazione</h2>
|
||||
{{-- 6. Installazione --}}
|
||||
<h2 id="installazione">6. Guida all'Installazione</h2>
|
||||
|
||||
<h3>Panoramica</h3>
|
||||
<p>L'applicazione include un <strong>wizard di installazione web</strong> che guida passo-passo nella configurazione su un nuovo server LAMP. Supporta due modalità:</p>
|
||||
<p>L'installazione avviene tramite l'interattivo <code>install.php</code> che guida passo-passo. Supporta due modalità:</p>
|
||||
|
||||
<table>
|
||||
<thead><tr><th>Modalità</th><th>Descrizione</th></tr></thead>
|
||||
@@ -313,16 +378,15 @@ git clone <URL_REPOSITORY> glastree
|
||||
# Opzione B — ZIP (se non hai git)
|
||||
# unzip /percorso/del/glastree.zip -d glastree
|
||||
|
||||
cd glastree
|
||||
composer install --no-dev --optimize-autoloader
|
||||
npm ci && npm run build</pre>
|
||||
<p>Se non hai accesso a npm, puoi saltare il build. L'interfaccia userà comunque AdminLTE via CDN.</p>
|
||||
cd glastree</pre>
|
||||
|
||||
<h4>Passo 3: Permessi cartelle</h4>
|
||||
<pre>sudo chown -R www-data:www-data storage bootstrap/cache
|
||||
sudo chmod -R 775 storage bootstrap/cache</pre>
|
||||
<h4>Passo 3: Eseguire l'installer</h4>
|
||||
<p>Lancia l'installer interattivo che guida attraverso tutte le fasi:</p>
|
||||
<pre>php install.php</pre>
|
||||
<p>L'installer chiede: modalità (Fresh Install Apache/Docker o Restore), nome sito, dati amministratore, URL pubblico, database (MySQL o SQLite).<br>
|
||||
Esegue automaticamente: <code>.env</code> + <code>APP_KEY</code>, composer, migration/seed, admin user, asset (se npm presente), permessi.</p>
|
||||
|
||||
<h4>Passo 4: Configurare Apache</h4>
|
||||
<h4>Passo 4: Configurare Apache (solo Fresh Install)</h4>
|
||||
<pre>sudo tee /etc/apache2/sites-available/glastree.conf <<APACHE
|
||||
<VirtualHost *:80>
|
||||
ServerName glastree.esempio.it
|
||||
@@ -343,51 +407,27 @@ sudo a2dissite 000-default.conf
|
||||
sudo a2ensite glastree.conf
|
||||
sudo systemctl reload apache2</pre>
|
||||
|
||||
<h4>Passo 5: Avviare il wizard di installazione</h4>
|
||||
<p>Apri il browser e visita: <code>http://glastree.esempio.it/installer/</code></p>
|
||||
<p>Configura SSL con Let's Encrypt dopo l'installazione.</p>
|
||||
php artisan tinker --execute="
|
||||
\$u = new \App\Models\User;
|
||||
\$u->name = 'Admin';
|
||||
\$u->email = 'admin@esempio.it';
|
||||
\$u->password = bcrypt('password_sicura');
|
||||
\$u->save();
|
||||
"</pre>
|
||||
|
||||
<p>Il wizard ti guiderà attraverso 6 passi:</p>
|
||||
<table>
|
||||
<thead><tr><th>Passo</th><th>Descrizione</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>1. Benvenuto</td><td>Scegli Fresh Install o Restore da Backup</td></tr>
|
||||
<tr><td>2. Requisiti</td><td>Verifica PHP, estensioni e permessi</td></tr>
|
||||
<tr><td>3. Database</td><td>Configura MySQL (crea nuovo o usa esistente)</td></tr>
|
||||
<tr><td>4. Installazione</td><td>Fresh: migration e seed. Backup: upload ZIP</td></tr>
|
||||
<tr><td>5. Amministratore</td><td>Fresh: crea super-admin. Backup: verifica utenti</td></tr>
|
||||
<tr><td>6. Finalizzazione</td><td>Cache, storage link, auto-eliminazione installer</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong>Restore da Backup:</strong></p>
|
||||
<pre># Importa il database
|
||||
# mysql -u root -p glastree < database.sql
|
||||
|
||||
<div class="callout callout-warning">
|
||||
<strong>Attenzione:</strong> Al termine dell'installazione, la cartella <code>installer/</code> viene <strong>automaticamente eliminata</strong>. Non puoi rieseguire l'installazione se non ricreando manualmente la cartella.
|
||||
</div>
|
||||
# Ripristina i file dei documenti
|
||||
# tar -xzf storage.tar.gz -C storage/
|
||||
|
||||
<h3>Modalità Fresh Install</h3>
|
||||
<p>Seleziona questa modalità quando installi l'applicazione per la prima volta su un server.</p>
|
||||
<p>Cosa succede durante l'installazione:</p>
|
||||
<ol>
|
||||
<li>Genera <code>APP_KEY</code> per la crittografia</li>
|
||||
<li>Esegue tutte le migration per creare le tabelle del database</li>
|
||||
<li>Popola i dati di base (diocesi, comuni, ruoli, preset)</li>
|
||||
<li>Crea il collegamento <code>storage → public/storage</code></li>
|
||||
<li>Crea l'utente amministratore con i permessi completi</li>
|
||||
</ol>
|
||||
# Rigenera APP_KEY
|
||||
php artisan key:generate
|
||||
|
||||
<h3>Modalità Restore da Backup</h3>
|
||||
<p>Seleziona questa modalità quando vuoi migrare un'installazione esistente su un nuovo server.</p>
|
||||
<p>Cosa serve:</p>
|
||||
<ul>
|
||||
<li>Un file ZIP generato dalla pagina <strong>Admin → Backup</strong> del server originale</li>
|
||||
<li>Il wizard accetta upload diretto del file o un percorso sul server</li>
|
||||
</ul>
|
||||
<p>Cosa succede durante il restore:</p>
|
||||
<ol>
|
||||
<li>Estrae il file ZIP in una cartella temporanea</li>
|
||||
<li>Importa <code>database.sql</code> nel database MySQL configurato</li>
|
||||
<li>Copia i file documenti in <code>storage/app/documenti/</code></li>
|
||||
<li>Ripristina il file <code>.env</code> dal backup (preservando le credenziali DB del nuovo server)</li>
|
||||
</ol>
|
||||
# Crea il collegamento storage
|
||||
php artisan storage:link</pre>
|
||||
|
||||
<h3>Dopo l'Installazione</h3>
|
||||
<ol>
|
||||
@@ -436,8 +476,8 @@ sudo systemctl reload apache2</pre>
|
||||
|
||||
<div class="page-break"></div>
|
||||
|
||||
{{-- 6. Backup e Migrazione --}}
|
||||
<h2 id="backup">6. Backup e Migrazione Server</h2>
|
||||
{{-- 7. Backup e Migrazione --}}
|
||||
<h2 id="backup">7. Backup e Migrazione Server</h2>
|
||||
|
||||
<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>
|
||||
@@ -611,14 +651,13 @@ sudo certbot --apache -d glastree.esempio.it</pre>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>PHP Web Installer</h3>
|
||||
<p>L'applicazione include un <strong>wizard di installazione web</strong> nella cartella <code>installer/</code>. Questo wizard gestisce sia installazioni fresh che ripristino da backup.</p>
|
||||
<p>Vedi la sezione <strong>Installazione</strong> per la guida completa.</p>
|
||||
<h3>Comandi Artisan</h3>
|
||||
<p>Per installazione e ripristino usa i comandi Artisan da terminale. Vedi la sezione <strong>Installazione</strong> per la guida completa.</p>
|
||||
|
||||
<div class="page-break"></div>
|
||||
|
||||
{{-- 7. Docker --}}
|
||||
<h2 id="docker">7. Docker</h2>
|
||||
{{-- 8. Docker --}}
|
||||
<h2 id="docker">8. Docker</h2>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -583,6 +583,7 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="hidden" name="is_active" value="0">
|
||||
<input type="checkbox" class="custom-control-input" id="is_active"
|
||||
name="is_active" value="1" {{ ($emailSettings->is_active ?? false) ? 'checked' : '' }}>
|
||||
<label class="custom-control-label" for="is_active">Account Email Attivo</label>
|
||||
@@ -599,11 +600,12 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" id="signature_enabled"
|
||||
name="signature_enabled" value="1" {{ ($emailSettings->signature_enabled ?? true) ? 'checked' : '' }}>
|
||||
<label class="custom-control-label" for="signature_enabled">Attiva firma automatica</label>
|
||||
</div>
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="hidden" name="signature_enabled" value="0">
|
||||
<input type="checkbox" class="custom-control-input" id="signature_enabled"
|
||||
name="signature_enabled" value="1" {{ ($emailSettings->signature_enabled ?? true) ? 'checked' : '' }}>
|
||||
<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>
|
||||
</div>
|
||||
<hr>
|
||||
@@ -709,7 +711,7 @@
|
||||
</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">
|
||||
<i class="fas fa-save mr-1"></i> Salva Impostazioni Email
|
||||
</button>
|
||||
@@ -724,25 +726,119 @@
|
||||
</a>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
{{-- === CALENDARIO === --}}
|
||||
<div class="tab-pane" id="calendario">
|
||||
<div class="card card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Calendari Remoti</h3>
|
||||
<h3 class="card-title"><i class="fas fa-calendar-alt mr-2"></i> Calendari Remoti</h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-sm btn-success" data-toggle="modal" data-target="#calendarioModal" onclick="resetCalendarioForm()">
|
||||
<i class="fas fa-plus mr-1"></i> Nuova Connessione
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">Configura la sincronizzazione con calendari esterni (Google Calendar, CalDAV, ecc.).</p>
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<strong>Prossimamente:</strong> Potrai collegare calendari Google, Infomaniak o altri provider CalDAV per sincronizzare gli eventi del calendario.
|
||||
<p class="text-muted">Configura la sincronizzazione con calendari esterni (Google Calendar, CalDAV come Infomaniak/NextCloud).</p>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success">{{ session('success') }}</div>
|
||||
@endif
|
||||
@if(session('error'))
|
||||
<div class="alert alert-danger">{{ session('error') }}</div>
|
||||
@endif
|
||||
|
||||
@php
|
||||
$calendarioConnessioni = \App\Models\CalendarioConnessione::orderBy('ordine')->get();
|
||||
@endphp
|
||||
|
||||
@if($calendarioConnessioni->count() > 0)
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40px;">#</th>
|
||||
<th>Nome</th>
|
||||
<th>Tipo</th>
|
||||
<th>Direzione Sync</th>
|
||||
<th>Stato</th>
|
||||
<th>Ultimo Sync</th>
|
||||
<th style="width:200px;">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="calendarioSortable">
|
||||
@foreach($calendarioConnessioni as $cal)
|
||||
<tr data-id="{{ $cal->id }}">
|
||||
<td class="text-center handle" style="cursor:grab;"><i class="fas fa-grip-vertical text-muted"></i></td>
|
||||
<td><strong>{{ $cal->nome }}</strong></td>
|
||||
<td>
|
||||
<span class="badge badge-{{ $cal->tipo === 'google_calendar' ? 'danger' : 'info' }}">
|
||||
<i class="fas {{ $cal->tipo === 'google_calendar' ? 'fa-google' : 'fa-calendar-alt' }} mr-1"></i>
|
||||
{{ \App\Models\CalendarioConnessione::etichettaTipo($cal->tipo) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
@switch($cal->sync_direction)
|
||||
@case('import') <span class="badge badge-primary"><i class="fas fa-download mr-1"></i> Solo Import</span> @break
|
||||
@case('export') <span class="badge badge-warning"><i class="fas fa-upload mr-1"></i> Solo Export</span> @break
|
||||
@default <span class="badge badge-success"><i class="fas fa-exchange-alt mr-1"></i> Bidirezionale</span>
|
||||
@endswitch
|
||||
</td>
|
||||
<td>
|
||||
@if($cal->stato === 'connesso')
|
||||
<span class="badge badge-success"><i class="fas fa-check-circle mr-1"></i> Connesso</span>
|
||||
@elseif($cal->stato === 'errore')
|
||||
<span class="badge badge-danger" title="{{ $cal->last_error_message ?? '' }}"><i class="fas fa-exclamation-circle mr-1"></i> Errore</span>
|
||||
@else
|
||||
<span class="badge badge-secondary"><i class="fas fa-circle mr-1"></i> {{ ucfirst($cal->stato) }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($cal->last_sync_at)
|
||||
<small class="text-muted">{{ $cal->last_sync_at->format('d/m/Y H:i') }}</small>
|
||||
@else
|
||||
<small class="text-muted">Mai</small>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-xs btn-info" onclick="editCalendario({{ $cal->id }})" title="Modifica">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-xs btn-success" onclick="testCalendario({{ $cal->id }}, this)" title="Test Connessione">
|
||||
<i class="fas fa-plug"></i>
|
||||
</button>
|
||||
<form action="{{ route('calendario-connessioni.sync', $cal->id) }}" method="POST" class="d-inline" onsubmit="return confirm('Avviare la sincronizzazione ora?')">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-xs btn-primary" title="Sincronizza Ora">
|
||||
<i class="fas fa-sync"></i>
|
||||
</button>
|
||||
</form>
|
||||
<button type="button" class="btn btn-xs btn-danger" onclick="deleteCalendario({{ $cal->id }}, '{{ addslashes($cal->nome) }}')" title="Elimina">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="text-center p-5 bg-light rounded">
|
||||
<i class="fas fa-calendar-plus fa-4x text-muted mb-3"></i>
|
||||
<p class="text-muted">Nessun calendario remoto configurato.</p>
|
||||
<p class="text-muted small">La funzionalità sarà disponibile in un prossimo aggiornamento.</p>
|
||||
@else
|
||||
<div class="text-center text-muted py-4">
|
||||
<i class="fas fa-calendar-plus fa-3x mb-3"></i>
|
||||
<p class="mb-0">Nessun calendario remoto configurato.</p>
|
||||
<p class="mb-0">Clicca "Nuova Connessione" per collegare Google Calendar o un server CalDAV (Infomaniak, NextCloud).</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1277,6 +1373,144 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ===== MODALE CONNESSIONE CALENDARIO ===== --}}
|
||||
<div class="modal fade" id="calendarioModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<form id="calendarioForm" method="POST" action="{{ route('calendario-connessioni.store') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="_method" id="calendarioMethod" value="POST">
|
||||
<input type="hidden" name="id" id="calendarioId" value="">
|
||||
<div class="modal-header bg-primary">
|
||||
<h5 class="modal-title" id="calendarioModalTitle"><i class="fas fa-calendar-plus mr-2"></i>Nuova Connessione Calendario</h5>
|
||||
<button type="button" class="close text-white" data-dismiss="modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="cal_nome">Nome *</label>
|
||||
<input type="text" name="nome" id="cal_nome" class="form-control" required placeholder="es. Calendario Parrocchia">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="cal_tipo">Tipo *</label>
|
||||
<select name="tipo" id="cal_tipo" class="form-control" required onchange="toggleCalendarioConfigFields()">
|
||||
<option value="caldav">CalDAV (Infomaniak/NextCloud)</option>
|
||||
<option value="google_calendar">Google Calendar</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="cal_sync_direction">Direzione Sync</label>
|
||||
<select name="sync_direction" id="cal_sync_direction" class="form-control">
|
||||
<option value="bidirectional">Bidirezionale</option>
|
||||
<option value="import">Solo Import (da remoto a locale)</option>
|
||||
<option value="export">Solo Export (da locale a remoto)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="cal_sync_interval">Intervallo Sync (minuti)</label>
|
||||
<select name="sync_interval_minutes" id="cal_sync_interval" class="form-control">
|
||||
<option value="5">5 minuti</option>
|
||||
<option value="15">15 minuti</option>
|
||||
<option value="30">30 minuti</option>
|
||||
<option value="60" selected>1 ora</option>
|
||||
<option value="360">6 ore</option>
|
||||
<option value="1440">24 ore</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" id="cal_is_active" name="is_active" value="1" checked>
|
||||
<label class="custom-control-label" for="cal_is_active">Connessione attiva</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<h6>Configurazione</h6>
|
||||
|
||||
{{-- CalDAV Fields --}}
|
||||
<div id="caldavFields">
|
||||
<div class="form-group">
|
||||
<label for="cal_caldav_url">URL Server CalDAV *</label>
|
||||
<input type="url" name="config[url]" id="cal_caldav_url" class="form-control" placeholder="es. https://nextcloud.parrocchia.it/remote.php/dav/calendars/utente/">
|
||||
<small class="text-muted">URL principale del server CalDAV (es. NextCloud, Infomaniak, Synology)</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cal_caldav_calendar_url">URL Calendario (opzionale)</label>
|
||||
<input type="url" name="config[calendar_url]" id="cal_caldav_calendar_url" class="form-control" placeholder="Lascia vuoto per usare l'URL principale">
|
||||
<small class="text-muted">URL specifico del calendario, se diverso dall'URL del server</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="cal_caldav_username">Username</label>
|
||||
<input type="text" name="config[username]" id="cal_caldav_username" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="cal_caldav_password">Password / App Password</label>
|
||||
<input type="password" name="config[password]" id="cal_caldav_password" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Google Calendar Fields --}}
|
||||
<div id="googleCalendarFields" style="display:none;">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="cal_gc_client_id">Client ID *</label>
|
||||
<input type="text" name="config[client_id]" id="cal_gc_client_id" class="form-control" placeholder="Google Client ID">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="cal_gc_client_secret">Client Secret *</label>
|
||||
<input type="password" name="config[client_secret]" id="cal_gc_client_secret" class="form-control" placeholder="Google Client Secret">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cal_gc_refresh_token">Refresh Token</label>
|
||||
<input type="text" name="config[refresh_token]" id="cal_gc_refresh_token" class="form-control" placeholder="Generato dopo l'autorizzazione OAuth">
|
||||
<small class="text-muted">Dopo aver configurato Client ID e Client Secret, salva la connessione e usa il test per verificare.</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cal_gc_calendar_id">Calendar ID</label>
|
||||
<input type="text" name="config[calendar_id]" id="cal_gc_calendar_id" class="form-control" value="primary" placeholder="primary">
|
||||
<small class="text-muted">ID del calendario Google (default: primary per il calendario principale)</small>
|
||||
</div>
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle mr-2"></i>
|
||||
<strong>Google Calendar:</strong> Crea il progetto in <a href="https://console.cloud.google.com/" target="_blank">Google Cloud Console</a>
|
||||
→ API & Services → Credentials → Crea OAuth Client ID (Web Application).
|
||||
Abilita <strong>Google Calendar API</strong> e configura l'URI di redirect.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annulla</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save mr-1"></i> Salva Connessione
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
@@ -1392,6 +1626,14 @@ $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||
if (target === '#tipologie-eventi') {
|
||||
sortableTipologieEventi.refresh();
|
||||
}
|
||||
if (target === '#calendario') {
|
||||
if (typeof sortableCalendario !== 'undefined') {
|
||||
sortableCalendario.refresh();
|
||||
}
|
||||
}
|
||||
if (target === '#email-firma') {
|
||||
setTimeout(initSignatureEditor, 100);
|
||||
}
|
||||
});
|
||||
|
||||
// ===== MITTENTI AGGIUNTIVI =====
|
||||
@@ -1539,9 +1781,11 @@ function testSmtp() {
|
||||
});
|
||||
}
|
||||
|
||||
var quillEditorInstance = null;
|
||||
|
||||
function createEditor() {
|
||||
var container = document.querySelector('#signature-editor');
|
||||
if (!container || container.querySelector('.ql-toolbar')) return;
|
||||
if (!container || quillEditorInstance) return;
|
||||
|
||||
var quill = new Quill('#signature-editor', {
|
||||
theme: 'snow',
|
||||
@@ -1590,10 +1834,12 @@ function createEditor() {
|
||||
signatureInput.value = quill.root.innerHTML;
|
||||
}
|
||||
});
|
||||
|
||||
quillEditorInstance = quill;
|
||||
}
|
||||
|
||||
function initSignatureEditor() {
|
||||
if (document.querySelector('#signature-editor .ql-toolbar')) return;
|
||||
if (quillEditorInstance) return;
|
||||
|
||||
if (typeof Quill === 'undefined') {
|
||||
var script = document.createElement('script');
|
||||
@@ -1771,7 +2017,122 @@ function testRepo(id, btn) {
|
||||
});
|
||||
}
|
||||
|
||||
// ===== CONNESSIONI CALENDARIO =====
|
||||
var calendarioConnessioni = @json($calendarioConnessioni ?? []);
|
||||
|
||||
function resetCalendarioForm() {
|
||||
document.getElementById('calendarioForm').action = '{{ route('calendario-connessioni.store') }}';
|
||||
document.getElementById('calendarioMethod').value = 'POST';
|
||||
document.getElementById('calendarioModalTitle').textContent = 'Nuova Connessione Calendario';
|
||||
document.getElementById('calendarioForm').reset();
|
||||
document.getElementById('calendarioId').value = '';
|
||||
document.getElementById('cal_is_active').checked = true;
|
||||
document.getElementById('cal_caldav_password').placeholder = '';
|
||||
document.getElementById('cal_gc_client_secret').placeholder = '';
|
||||
toggleCalendarioConfigFields();
|
||||
}
|
||||
|
||||
function toggleCalendarioConfigFields() {
|
||||
const tipo = document.getElementById('cal_tipo').value;
|
||||
document.getElementById('caldavFields').style.display = tipo === 'caldav' ? '' : 'none';
|
||||
document.getElementById('googleCalendarFields').style.display = tipo === 'google_calendar' ? '' : 'none';
|
||||
}
|
||||
|
||||
function editCalendario(id) {
|
||||
const cal = calendarioConnessioni.find(c => c.id === id);
|
||||
if (!cal) return;
|
||||
|
||||
document.getElementById('calendarioForm').action = '{{ route('calendario-connessioni.update', '__ID__') }}'.replace('__ID__', id);
|
||||
document.getElementById('calendarioMethod').value = 'PUT';
|
||||
document.getElementById('calendarioModalTitle').textContent = 'Modifica Connessione - ' + cal.nome;
|
||||
document.getElementById('calendarioId').value = cal.id;
|
||||
document.getElementById('cal_nome').value = cal.nome;
|
||||
document.getElementById('cal_tipo').value = cal.tipo;
|
||||
document.getElementById('cal_sync_direction').value = cal.sync_direction || 'bidirectional';
|
||||
document.getElementById('cal_sync_interval').value = cal.sync_interval_minutes || 60;
|
||||
document.getElementById('cal_is_active').checked = cal.is_active;
|
||||
|
||||
const cfg = cal.config || {};
|
||||
document.getElementById('cal_caldav_url').value = cfg.url || '';
|
||||
document.getElementById('cal_caldav_calendar_url').value = cfg.calendar_url || '';
|
||||
document.getElementById('cal_caldav_username').value = cfg.username || '';
|
||||
document.getElementById('cal_caldav_password').value = '';
|
||||
document.getElementById('cal_caldav_password').placeholder = cfg.password ? '......' : '';
|
||||
document.getElementById('cal_gc_client_id').value = cfg.client_id || '';
|
||||
document.getElementById('cal_gc_client_secret').value = '';
|
||||
document.getElementById('cal_gc_client_secret').placeholder = cfg.client_secret ? '......' : '';
|
||||
document.getElementById('cal_gc_refresh_token').value = cfg.refresh_token || '';
|
||||
document.getElementById('cal_gc_calendar_id').value = cfg.calendar_id || 'primary';
|
||||
|
||||
toggleCalendarioConfigFields();
|
||||
$('#calendarioModal').modal('show');
|
||||
}
|
||||
|
||||
function deleteCalendario(id, nome) {
|
||||
if (!confirm('Eliminare la connessione "' + nome + '"?')) return;
|
||||
var url = '{{ route('calendario-connessioni.destroy', '__ID__') }}'.replace('__ID__', id);
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'X-HTTP-Method-Override': 'DELETE'
|
||||
}
|
||||
}).then(function(r) { return r.json(); }).then(function(data) {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('❌ ' + (data.message || 'Errore'));
|
||||
}
|
||||
}).catch(function() {
|
||||
alert('❌ Errore di rete');
|
||||
});
|
||||
}
|
||||
|
||||
function testCalendario(id, btn) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Test...';
|
||||
|
||||
$.ajax({
|
||||
url: '{{ route('calendario-connessioni.test', '__ID__') }}'.replace('__ID__', id),
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
|
||||
timeout: 30000,
|
||||
success: function(response) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-plug"></i>';
|
||||
alert(response.success ? '✅ ' + response.message : '❌ ' + response.message);
|
||||
},
|
||||
error: function(xhr) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-plug"></i>';
|
||||
const msg = xhr.responseJSON?.message || 'Errore di connessione';
|
||||
alert('❌ ' + msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
if (document.getElementById('calendarioSortable')) {
|
||||
var sortableCalendario = Sortable.create(document.getElementById('calendarioSortable'), {
|
||||
handle: '.handle',
|
||||
animation: 150,
|
||||
onEnd: function(evt) {
|
||||
var order = [];
|
||||
$('#calendarioSortable tr').each(function() {
|
||||
order.push($(this).data('id'));
|
||||
});
|
||||
fetch('{{ route('calendario-connessioni.reorder') }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ order: order })
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (document.getElementById('repoSortable')) {
|
||||
var sortableRepo = Sortable.create(document.getElementById('repoSortable'), {
|
||||
handle: '.handle',
|
||||
|
||||
@@ -232,24 +232,20 @@
|
||||
<button type="button" class="btn btn-xs btn-warning" onclick="editGruppoInline({{ $gruppo->id }})" title="Modifica">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<form action="/individui/{{ $individuo->id }}/gruppi/{{ $gruppo->id }}" method="POST" class="mb-0">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn btn-xs btn-danger" onclick="return confirm('Rimuovere da questo gruppo?')" title="Rimuovi">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
<button type="button" class="btn btn-xs btn-danger" onclick="deleteGruppoFromEdit({{ $individuo->id }}, {{ $gruppo->id }})" title="Rimuovi">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="gruppo-edit-{{ $gruppo->id }}" style="display:none;">
|
||||
<td colspan="5">
|
||||
<form action="/individui/{{ $individuo->id }}/gruppi/{{ $gruppo->id }}" method="POST" class="mb-0">
|
||||
@csrf @method('PUT')
|
||||
<div class="mb-0">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<input type="text" class="form-control form-control-sm" value="{{ $gruppo->nome }}" disabled>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select name="ruolo_ids[]" class="form-control form-control-sm" multiple size="3">
|
||||
<select id="edit-ruoli-{{ $gruppo->id }}" class="form-control form-control-sm" multiple size="3">
|
||||
@php $selectedRuoliIds = $individuo->getRuoloIdsForGruppo($gruppo->id) @endphp
|
||||
@foreach(\App\Models\Ruolo::attive() as $ruolo)
|
||||
<option value="{{ $ruolo->id }}" {{ in_array($ruolo->id, $selectedRuoliIds) ? 'selected' : '' }}>{{ $ruolo->nome }}</option>
|
||||
@@ -257,14 +253,14 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<input type="date" name="data_adesione" class="form-control form-control-sm" value="{{ $gruppo->pivot->data_adesione }}">
|
||||
<input type="date" id="edit-data-{{ $gruppo->id }}" class="form-control form-control-sm" value="{{ $gruppo->pivot->data_adesione }}">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-xs btn-success"><i class="fas fa-check mr-1"></i>Salva</button>
|
||||
<button type="button" class="btn btn-xs btn-success" onclick="submitEditGruppo({{ $gruppo->id }})"><i class="fas fa-check mr-1"></i>Salva</button>
|
||||
<button type="button" class="btn btn-xs btn-secondary" onclick="cancelEditGruppo({{ $gruppo->id }})"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@@ -288,18 +284,13 @@
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div id="documento-add-form" style="display:none;" class="p-3 bg-light border-bottom">
|
||||
<form action="/documenti" method="POST" enctype="multipart/form-data" class="mb-0">
|
||||
@csrf
|
||||
<input type="hidden" name="_redirect" value="{{ url()->current() }}">
|
||||
<input type="hidden" name="visibilita" value="individuo">
|
||||
<input type="hidden" name="visibilita_target_id" value="{{ $individuo->id }}">
|
||||
<input type="hidden" name="visibilita_target_type" value="App\Models\Individuo">
|
||||
<div class="mb-0">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<input type="text" name="nome_file" class="form-control form-control-sm" placeholder="Nome documento" required>
|
||||
<input type="text" id="doc-nome-file" class="form-control form-control-sm" placeholder="Nome documento">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select name="tipologia" class="form-control form-control-sm" required>
|
||||
<select id="doc-tipologia" class="form-control form-control-sm">
|
||||
<option value="">Tipologia...</option>
|
||||
<option value="documento">Documento</option>
|
||||
<option value="avatar">Avatar</option>
|
||||
@@ -309,14 +300,14 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<input type="file" name="file" class="form-control form-control-sm" required>
|
||||
<input type="file" id="doc-file" class="form-control form-control-sm">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-xs btn-success"><i class="fas fa-upload mr-1"></i>Carica</button>
|
||||
<button type="button" class="btn btn-xs btn-success" onclick="submitUploadDocumentoIndividuo()"><i class="fas fa-upload mr-1"></i>Carica</button>
|
||||
<button type="button" class="btn btn-xs btn-secondary" onclick="hideDocumentoForm()"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@if($individuo->documenti->count() > 0)
|
||||
<table class="table table-bordered table-hover table-striped mb-0">
|
||||
@@ -347,12 +338,9 @@
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
@endif
|
||||
<form action="/documenti/{{ $documento->id }}" method="POST" class="d-inline">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn btn-xs btn-danger" onclick="return confirm('Eliminare questo documento?')" title="Elimina">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
<button type="button" class="btn btn-xs btn-danger" onclick="deleteDocumentoFromEdit({{ $documento->id }})" title="Elimina">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@@ -714,5 +702,105 @@ function deleteAvatar() {
|
||||
alert('Errore: ' + error);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteGruppoFromEdit(individuoId, gruppoId) {
|
||||
if (!confirm('Rimuovere da questo gruppo?')) return;
|
||||
|
||||
fetch('/individui/' + individuoId + '/gruppi/' + gruppoId, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) return response.text().then(text => { throw new Error(text); });
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) window.location.reload();
|
||||
else alert(data.error || 'Errore');
|
||||
})
|
||||
.catch(error => alert('Errore: ' + error.message));
|
||||
}
|
||||
|
||||
function submitEditGruppo(gruppoId) {
|
||||
var ruoloSelect = document.getElementById('edit-ruoli-' + gruppoId);
|
||||
var dataInput = document.getElementById('edit-data-' + gruppoId);
|
||||
var formData = new URLSearchParams();
|
||||
|
||||
var selectedRuoli = Array.from(ruoloSelect.selectedOptions).map(opt => opt.value).filter(v => v);
|
||||
selectedRuoli.forEach(function(ruoloId) {
|
||||
formData.append('ruolo_ids[]', ruoloId);
|
||||
});
|
||||
formData.append('data_adesione', dataInput ? dataInput.value : '');
|
||||
|
||||
fetch('/individui/{{ $individuo->id }}/gruppi/' + gruppoId, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: formData.toString()
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) return response.text().then(text => { throw new Error(text); });
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) window.location.reload();
|
||||
else alert(data.error || 'Errore');
|
||||
})
|
||||
.catch(error => alert('Errore: ' + error.message));
|
||||
}
|
||||
|
||||
function submitUploadDocumentoIndividuo() {
|
||||
var nomeFile = document.getElementById('doc-nome-file');
|
||||
var tipologia = document.getElementById('doc-tipologia');
|
||||
var fileInput = document.getElementById('doc-file');
|
||||
|
||||
if (!nomeFile.value || !tipologia.value || !fileInput.files[0]) {
|
||||
alert('Compila tutti i campi');
|
||||
return;
|
||||
}
|
||||
|
||||
var formData = new FormData();
|
||||
formData.append('_token', '{{ csrf_token() }}');
|
||||
formData.append('_redirect', '{{ url()->current() }}');
|
||||
formData.append('visibilita', 'individuo');
|
||||
formData.append('visibilita_target_id', '{{ $individuo->id }}');
|
||||
formData.append('visibilita_target_type', 'App\\Models\\Individuo');
|
||||
formData.append('nome_file', nomeFile.value);
|
||||
formData.append('tipologia', tipologia.value);
|
||||
formData.append('file', fileInput.files[0]);
|
||||
|
||||
document.getElementById('documento-add-form').querySelector('.btn-success').disabled = true;
|
||||
|
||||
fetch('/documenti', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: { 'Accept': 'application/json' }
|
||||
})
|
||||
.then(function() { window.location.reload(); })
|
||||
.catch(function() { window.location.reload(); });
|
||||
}
|
||||
|
||||
function deleteDocumentoFromEdit(documentoId) {
|
||||
if (!confirm('Eliminare questo documento?')) return;
|
||||
|
||||
var formData = new FormData();
|
||||
formData.append('_token', '{{ csrf_token() }}');
|
||||
formData.append('_method', 'DELETE');
|
||||
formData.append('_redirect', '{{ url()->current() }}');
|
||||
|
||||
fetch('/documenti/' + documentoId, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: { 'Accept': 'application/json' }
|
||||
})
|
||||
.then(function() { window.location.reload(); })
|
||||
.catch(function() { window.location.reload(); });
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
|
||||
@@ -220,8 +220,29 @@ $canDeleteIndividui = Auth::user()->canDelete('individui');
|
||||
@endif
|
||||
</div>
|
||||
@if($individui->count() > 0)
|
||||
<div class="card-footer">
|
||||
{{ $individui->links() }}
|
||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<form method="GET" class="form-inline" id="perPageForm">
|
||||
<label class="mr-2 small">Righe per pagina:</label>
|
||||
<select name="perPage" class="form-control form-control-sm" onchange="this.form.submit()" style="width: auto;">
|
||||
@foreach([10, 20, 25, 50, 100] as $p)
|
||||
<option value="{{ $p }}" {{ (int) request('perPage', 20) === $p ? 'selected' : '' }}>{{ $p }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@foreach(request()->except(['perPage', 'page']) as $key => $value)
|
||||
@if(is_array($value))
|
||||
@foreach($value as $v)
|
||||
<input type="hidden" name="{{ $key }}[]" value="{{ $v }}">
|
||||
@endforeach
|
||||
@else
|
||||
<input type="hidden" name="{{ $key }}" value="{{ $value }}">
|
||||
@endif
|
||||
@endforeach
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
{{ $individui->links() }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
+18
-1
@@ -23,6 +23,7 @@ use App\Http\Controllers\Admin\ActivityLogController;
|
||||
use App\Http\Controllers\Admin\EmailSettingsController;
|
||||
use App\Http\Controllers\EmailController;
|
||||
use App\Http\Controllers\StorageRepositoryController;
|
||||
use App\Http\Controllers\CalendarioConnessioneController;
|
||||
use App\Http\Middleware\AdminOnly;
|
||||
use App\Http\Middleware\CheckPermission;
|
||||
|
||||
@@ -86,6 +87,9 @@ Route::patch('gruppi/{gruppo}/membri/{individuo}', [GruppoMembroController::clas
|
||||
Route::delete('gruppi/{gruppo}/membri/{individuo}', [GruppoMembroController::class, 'destroy'])->middleware('auth');
|
||||
|
||||
// Gruppi specific routes (MUST be before resource route)
|
||||
Route::get('gruppi/import', [GruppoController::class, 'import'])->middleware('auth')->name('gruppi.import');
|
||||
Route::post('gruppi/import', [GruppoController::class, 'importStore'])->middleware('auth')->name('gruppi.import.store');
|
||||
Route::get('gruppi/template', [GruppoController::class, 'downloadTemplate'])->middleware('auth')->name('gruppi.template');
|
||||
Route::get('gruppi/all', [GruppoController::class, 'allGruppi'])->middleware('auth');
|
||||
Route::get('gruppi/{gruppo}/individui-email', [GruppoController::class, 'individuiEmail'])->middleware('auth');
|
||||
|
||||
@@ -96,6 +100,8 @@ Route::delete('gruppi/{gruppo}/avatar', [\App\Http\Controllers\GruppoAvatarContr
|
||||
Route::resource('gruppi', GruppoController::class)->middleware('auth');
|
||||
Route::get('eventi/calendar', [EventoController::class, 'calendar'])->name('eventi.calendar')->middleware('auth');
|
||||
Route::get('eventi/calendar/events', [EventoController::class, 'calendarEvents'])->name('eventi.calendar.events')->middleware('auth');
|
||||
Route::get('eventi/import', [EventoController::class, 'importIcs'])->middleware('auth')->name('eventi.import');
|
||||
Route::post('eventi/import', [EventoController::class, 'importIcsStore'])->middleware('auth')->name('eventi.import.store');
|
||||
Route::get('eventi/{evento}/export-ics', [EventoController::class, 'exportIcs'])->name('eventi.export-ics')->middleware('auth');
|
||||
Route::post('eventi/export-ics', [EventoController::class, 'exportSelectedIcs'])->name('eventi.export-selected-ics')->middleware('auth');
|
||||
Route::post('eventi/mass-elimina', [EventoController::class, 'massElimina'])->middleware('auth');
|
||||
@@ -183,13 +189,23 @@ Route::post('/impostazioni/migra-percorso', [ImpostazioniController::class, 'mig
|
||||
Route::get('/impostazioni/email', [EmailSettingsController::class, 'index'])->middleware('auth')->name('impostazioni.email.index');
|
||||
Route::post('/impostazioni/email', [EmailSettingsController::class, 'save'])->middleware('auth')->name('impostazioni.email.save');
|
||||
Route::post('/impostazioni/email/test', [EmailSettingsController::class, 'testConnection'])->middleware('auth')->name('impostazioni.email.test');
|
||||
Route::post('/impostazioni/email/test-smtp', [EmailSettingsController::class, 'testSmtp'])->middleware('auth')->name('impostazioni.email.testSmtp');
|
||||
Route::post('/impostazioni/email/test-smtp', [EmailSettingsController::class, 'testSmtp'])->middleware('auth')->name('impostazioni.email.testSmtp');
|
||||
Route::get('/impostazioni/email/sync', [EmailSettingsController::class, 'syncNow'])->middleware('auth')->name('impostazioni.email.sync');
|
||||
Route::delete('/impostazioni/email', [EmailSettingsController::class, 'destroy'])->middleware('auth')->name('impostazioni.email.destroy');
|
||||
Route::post('/impostazioni/sender', [EmailSettingsController::class, 'senderStore'])->middleware('auth')->name('impostazioni.sender.store');
|
||||
Route::put('/impostazioni/sender/{id}', [EmailSettingsController::class, 'senderUpdate'])->middleware('auth')->name('impostazioni.sender.update');
|
||||
Route::delete('/impostazioni/sender/{id}', [EmailSettingsController::class, 'senderDestroy'])->middleware('auth')->name('impostazioni.sender.destroy');
|
||||
Route::post('/impostazioni/sender/{id}/test', [EmailSettingsController::class, 'senderTestSmtp'])->middleware('auth')->name('impostazioni.sender.test');
|
||||
|
||||
// Calendar Connections (CalDAV / Google Calendar)
|
||||
Route::post('/calendario-connessioni', [App\Http\Controllers\CalendarioConnessioneController::class, 'store'])->middleware('auth')->name('calendario-connessioni.store');
|
||||
Route::put('/calendario-connessioni/{id}', [App\Http\Controllers\CalendarioConnessioneController::class, 'update'])->middleware('auth')->name('calendario-connessioni.update');
|
||||
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}/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');
|
||||
|
||||
// Storage Repositories (Opzione B)
|
||||
Route::post('/storage-repositories', [StorageRepositoryController::class, 'store'])->middleware('auth')->name('storage-repositories.store');
|
||||
Route::put('/storage-repositories/{storage_repository}', [StorageRepositoryController::class, 'update'])->middleware('auth')->name('storage-repositories.update');
|
||||
@@ -235,6 +251,7 @@ Route::prefix('admin')->middleware(['auth', AdminOnly::class])->group(function (
|
||||
Route::delete('/ruoli/{id}', [RuoloController::class, 'destroy'])->name('admin.ruoli.destroy');
|
||||
|
||||
Route::get('/activity-logs', [ActivityLogController::class, 'index'])->name('admin.activity-logs.index');
|
||||
Route::delete('/activity-logs', [ActivityLogController::class, 'destroy'])->name('admin.activity-logs.destroy');
|
||||
|
||||
// Backup
|
||||
Route::get('/backup', [\App\Http\Controllers\Admin\BackupController::class, 'index'])->name('admin.backup.index');
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -1,720 +0,0 @@
|
||||
<?php $__env->startSection('title', 'Modifica Individuo'); ?>
|
||||
<?php $__env->startSection('page_title', 'Modifica Individuo'); ?>
|
||||
|
||||
<?php $__env->startSection('content'); ?>
|
||||
<form action="/individui/<?php echo e($individuo->id); ?>" method="POST" id="main-form">
|
||||
<?php echo csrf_field(); ?>
|
||||
<?php echo method_field('PUT'); ?>
|
||||
<input type="hidden" name="individuo_id" value="<?php echo e($individuo->id); ?>">
|
||||
<input type="hidden" name="_avatar_token" value="<?php echo e(csrf_token()); ?>">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-user mr-2"></i>Dati Anagrafici</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label>Codice ID</label>
|
||||
<input type="text" class="form-control" value="<?php echo e($individuo->codice_id); ?>" disabled>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Cognome *</label>
|
||||
<input type="text" name="cognome" class="form-control" value="<?php echo e($individuo->cognome); ?>" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Nome *</label>
|
||||
<input type="text" name="nome" class="form-control" value="<?php echo e($individuo->nome); ?>" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Data di nascita</label>
|
||||
<input type="date" name="data_nascita" class="form-control" value="<?php echo e($individuo->data_nascita?->format('Y-m-d')); ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Genere</label>
|
||||
<select name="genere" class="form-control">
|
||||
<option value="">Seleziona...</option>
|
||||
<option value="M" <?php echo e($individuo->genere === 'M' ? 'selected' : ''); ?>>Maschio</option>
|
||||
<option value="F" <?php echo e($individuo->genere === 'F' ? 'selected' : ''); ?>>Femmina</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card card-secondary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-camera mr-2"></i>Avatar</h3>
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<?php if($individuo->avatar): ?>
|
||||
<img src="/individui/<?php echo e($individuo->id); ?>/avatar"
|
||||
alt="Avatar"
|
||||
class="img-thumbnail mb-2"
|
||||
style="max-width: 120px; max-height: 120px; object-fit: cover; border-radius: 50%;">
|
||||
<br>
|
||||
<button type="button" class="btn btn-danger btn-sm" onclick="deleteAvatar()">
|
||||
<i class="fas fa-trash mr-1"></i>Rimuovi
|
||||
</button>
|
||||
<?php else: ?>
|
||||
<div class="bg-secondary d-flex align-items-center justify-content-center mb-3"
|
||||
style="width: 120px; height: 120px; margin: 0 auto; border-radius: 50%;">
|
||||
<i class="fas fa-user fa-4x text-white"></i>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="custom-file mb-2">
|
||||
<input type="file" class="custom-file-input" id="avatar-input" name="avatar" accept="image/jpeg,image/png,image/gif">
|
||||
<label class="custom-file-label" for="avatar-input">Scegli immagine...</label>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="uploadAvatar()">
|
||||
<i class="fas fa-upload mr-1"></i>Carica Avatar
|
||||
</button>
|
||||
<div id="avatar-message" class="mt-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title"><i class="fas fa-map-marker-alt mr-2"></i>Residenza</h3></div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label>Indirizzo</label>
|
||||
<input type="text" name="indirizzo" class="form-control" value="<?php echo e($individuo->indirizzo); ?>">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-4">
|
||||
<label>CAP</label>
|
||||
<input type="text" name="cap" class="form-control" value="<?php echo e($individuo->cap); ?>">
|
||||
</div>
|
||||
<div class="form-group col-md-5">
|
||||
<label>Città</label>
|
||||
<input type="text" name="città" class="form-control" value="<?php echo e($individuo->città); ?>">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label>Provincia</label>
|
||||
<input type="text" name="sigla_provincia" class="form-control" maxlength="2" value="<?php echo e($individuo->sigla_provincia); ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title"><i class="fas fa-id-card mr-2"></i>Documento di identità</h3></div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label>Tipo documento</label>
|
||||
<select name="tipo_documento" class="form-control">
|
||||
<option value="">Seleziona...</option>
|
||||
<option value="carta_identita" <?php echo e($individuo->tipo_documento === 'carta_identita' ? 'selected' : ''); ?>>Carta d'Identità</option>
|
||||
<option value="patente" <?php echo e($individuo->tipo_documento === 'patente' ? 'selected' : ''); ?>>Patente</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Numero documento</label>
|
||||
<input type="text" name="numero_documento" class="form-control" value="<?php echo e($individuo->numero_documento); ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Scadenza documento</label>
|
||||
<input type="date" name="scadenza_documento" class="form-control" value="<?php echo e($individuo->scadenza_documento?->format('Y-m-d')); ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title"><i class="fas fa-sticky-note mr-2"></i>Note</h3></div>
|
||||
<div class="card-body">
|
||||
<div class="form-group mb-0">
|
||||
<textarea name="note" class="form-control" rows="4"><?php echo e($individuo->note); ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-address-book mr-2"></i>Contatti</h3>
|
||||
<button type="button" class="btn btn-xs btn-success float-right" onclick="aggiungiRigaContatto()">
|
||||
<i class="fas fa-plus mr-1"></i> Aggiungi
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-bordered mb-0" id="contatti-table">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th style="width: 20%;">Tipo</th>
|
||||
<th style="width: 30%;">Valore</th>
|
||||
<th style="width: 20%;">Etichetta</th>
|
||||
<th style="width: 10%;">Primario</th>
|
||||
<th style="width: 50px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="contatti-tbody">
|
||||
<?php $__currentLoopData = $individuo->contatti; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $contatto): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<tr>
|
||||
<td>
|
||||
<select name="contatti[<?php echo e($loop->index); ?>][tipo]" class="form-control">
|
||||
<option value="telefono" <?php echo e($contatto->tipo === 'telefono' ? 'selected' : ''); ?>>Telefono</option>
|
||||
<option value="cellulare" <?php echo e($contatto->tipo === 'cellulare' ? 'selected' : ''); ?>>Cellulare</option>
|
||||
<option value="email" <?php echo e($contatto->tipo === 'email' ? 'selected' : ''); ?>>Email</option>
|
||||
<option value="fax" <?php echo e($contatto->tipo === 'fax' ? 'selected' : ''); ?>>Fax</option>
|
||||
<option value="web" <?php echo e($contatto->tipo === 'web' ? 'selected' : ''); ?>>Web</option>
|
||||
<option value="telegram" <?php echo e($contatto->tipo === 'telegram' ? 'selected' : ''); ?>>Telegram</option>
|
||||
<option value="whatsapp" <?php echo e($contatto->tipo === 'whatsapp' ? 'selected' : ''); ?>>WhatsApp</option>
|
||||
<option value="altro" <?php echo e($contatto->tipo === 'altro' ? 'selected' : ''); ?>>Altro</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><input type="text" name="contatti[<?php echo e($loop->index); ?>][valore]" class="form-control" value="<?php echo e($contatto->valore); ?>"></td>
|
||||
<td><input type="text" name="contatti[<?php echo e($loop->index); ?>][etichetta]" class="form-control" value="<?php echo e($contatto->etichetta); ?>"></td>
|
||||
<td class="text-center"><input type="checkbox" name="contatti[<?php echo e($loop->index); ?>][is_primary]" value="1" <?php echo e($contatto->is_primary ? 'checked' : ''); ?>></td>
|
||||
<td><button type="button" class="btn btn-danger btn-xs" onclick="this.closest('tr').remove()"><i class="fas fa-trash"></i></button></td>
|
||||
</tr>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php if($individuo->contatti->isEmpty()): ?>
|
||||
<div id="no-contatti-msg" class="text-center text-muted py-4">
|
||||
<p class="mb-0">Nessun contatto. Clicca su "Aggiungi" per aggiungerne uno.</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-folder mr-2"></i>Gruppi</h3>
|
||||
<button type="button" class="btn btn-xs btn-success float-right" onclick="showGruppoForm()">
|
||||
<i class="fas fa-plus mr-1"></i> Aggiungi
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
|
||||
<?php if($individuo->gruppi->count() > 0): ?>
|
||||
<table class="table table-bordered table-hover table-striped mb-0">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>Nome</th>
|
||||
<th>Diocesi</th>
|
||||
<th style="width: 130px;">Ruolo</th>
|
||||
<th style="width: 110px;">Data Adesione</th>
|
||||
<th style="width: 80px;">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php $__currentLoopData = $individuo->gruppi; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $gruppo): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<tr id="gruppo-row-<?php echo e($gruppo->id); ?>">
|
||||
<td>
|
||||
<i class="fas fa-folder text-warning mr-1"></i>
|
||||
<a href="/gruppi/<?php echo e($gruppo->id); ?>"><?php echo e($gruppo->nome); ?></a>
|
||||
</td>
|
||||
<td><?php echo e($gruppo->diocesi?->nome ?: '-'); ?></td>
|
||||
<td>
|
||||
<?php $ruoliIndividuo = $individuo->getRuoliForGruppo($gruppo->id) ?>
|
||||
<?php if($ruoliIndividuo->count() > 0): ?>
|
||||
<?php $__currentLoopData = $ruoliIndividuo; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $ruolo): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<span class="badge badge-info mr-1"><?php echo e($ruolo->nome); ?></span>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?php echo e($gruppo->pivot->data_adesione ? \Carbon\Carbon::parse($gruppo->pivot->data_adesione)->format('d/m/Y') : '-'); ?></td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-xs btn-warning" onclick="editGruppoInline(<?php echo e($gruppo->id); ?>)" title="Modifica">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<form action="/individui/<?php echo e($individuo->id); ?>/gruppi/<?php echo e($gruppo->id); ?>" method="POST" class="mb-0">
|
||||
<?php echo csrf_field(); ?> <?php echo method_field('DELETE'); ?>
|
||||
<button type="submit" class="btn btn-xs btn-danger" onclick="return confirm('Rimuovere da questo gruppo?')" title="Rimuovi">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="gruppo-edit-<?php echo e($gruppo->id); ?>" style="display:none;">
|
||||
<td colspan="5">
|
||||
<form action="/individui/<?php echo e($individuo->id); ?>/gruppi/<?php echo e($gruppo->id); ?>" method="POST" class="mb-0">
|
||||
<?php echo csrf_field(); ?> <?php echo method_field('PUT'); ?>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<input type="text" class="form-control form-control-sm" value="<?php echo e($gruppo->nome); ?>" disabled>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select name="ruolo_ids[]" class="form-control form-control-sm" multiple size="3">
|
||||
<?php $selectedRuoliIds = $individuo->getRuoloIdsForGruppo($gruppo->id) ?>
|
||||
<?php $__currentLoopData = \App\Models\Ruolo::attive(); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $ruolo): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<option value="<?php echo e($ruolo->id); ?>" <?php echo e(in_array($ruolo->id, $selectedRuoliIds) ? 'selected' : ''); ?>><?php echo e($ruolo->nome); ?></option>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<input type="date" name="data_adesione" class="form-control form-control-sm" value="<?php echo e($gruppo->pivot->data_adesione); ?>">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-xs btn-success"><i class="fas fa-check mr-1"></i>Salva</button>
|
||||
<button type="button" class="btn btn-xs btn-secondary" onclick="cancelEditGruppo(<?php echo e($gruppo->id); ?>)"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php else: ?>
|
||||
<div class="text-center text-muted py-4">
|
||||
<i class="fas fa-users fa-2x mb-2"></i>
|
||||
<p class="mb-0">Nessun gruppo associato</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-file mr-2"></i>Documenti</h3>
|
||||
<button type="button" class="btn btn-xs btn-success float-right" onclick="showDocumentoForm()">
|
||||
<i class="fas fa-plus mr-1"></i> Carica
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div id="documento-add-form" style="display:none;" class="p-3 bg-light border-bottom">
|
||||
<form action="/documenti" method="POST" enctype="multipart/form-data" class="mb-0">
|
||||
<?php echo csrf_field(); ?>
|
||||
<input type="hidden" name="_redirect" value="<?php echo e(url()->current()); ?>">
|
||||
<input type="hidden" name="visibilita" value="individuo">
|
||||
<input type="hidden" name="visibilita_target_id" value="<?php echo e($individuo->id); ?>">
|
||||
<input type="hidden" name="visibilita_target_type" value="App\Models\Individuo">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<input type="text" name="nome_file" class="form-control form-control-sm" placeholder="Nome documento" required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select name="tipologia" class="form-control form-control-sm" required>
|
||||
<option value="">Tipologia...</option>
|
||||
<option value="documento">Documento</option>
|
||||
<option value="avatar">Avatar</option>
|
||||
<option value="galleria">Galleria</option>
|
||||
<option value="statuto">Statuto</option>
|
||||
<option value="altro">Altro</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<input type="file" name="file" class="form-control form-control-sm" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-xs btn-success"><i class="fas fa-upload mr-1"></i>Carica</button>
|
||||
<button type="button" class="btn btn-xs btn-secondary" onclick="hideDocumentoForm()"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<?php if($individuo->documenti->count() > 0): ?>
|
||||
<table class="table table-bordered table-hover table-striped mb-0">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>Nome</th>
|
||||
<th style="width: 120px;">Tipologia</th>
|
||||
<th style="width: 80px;">Dimensione</th>
|
||||
<th style="width: 130px;">Data Upload</th>
|
||||
<th style="width: 80px;">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php $__currentLoopData = $individuo->documenti; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $documento): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="#" onclick="event.preventDefault(); previewDocumento(<?php echo e($documento->id); ?>, '<?php echo e($documento->mime_type); ?>');">
|
||||
<i class="fas fa-file text-secondary mr-1"></i>
|
||||
<?php echo e($documento->nome_file); ?>
|
||||
|
||||
</a>
|
||||
</td>
|
||||
<td><?php echo e(ucfirst(str_replace('_', ' ', $documento->tipologia))); ?></td>
|
||||
<td><?php echo e(number_format($documento->dimensione / 1024, 1)); ?> KB</td>
|
||||
<td><?php echo e($documento->created_at->format('d/m/Y')); ?></td>
|
||||
<td>
|
||||
<?php if($documento->file_path): ?>
|
||||
<button type="button" class="btn btn-xs btn-primary" onclick="previewDocumento(<?php echo e($documento->id); ?>, '<?php echo e($documento->mime_type); ?>')" title="Anteprima">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<form action="/documenti/<?php echo e($documento->id); ?>" method="POST" class="d-inline">
|
||||
<?php echo csrf_field(); ?> <?php echo method_field('DELETE'); ?>
|
||||
<button type="submit" class="btn btn-xs btn-danger" onclick="return confirm('Eliminare questo documento?')" title="Elimina">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php else: ?>
|
||||
<div class="text-center text-muted py-4">
|
||||
<i class="fas fa-file fa-2x mb-2"></i>
|
||||
<p class="mb-0">Nessun documento</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 mb-4">
|
||||
<button type="button" class="btn btn-success btn-lg" id="save-all-btn" onclick="submitMainForm()">
|
||||
<i class="fas fa-save mr-2"></i> Salva Tutte le Modifiche
|
||||
</button>
|
||||
<a href="/individui/<?php echo e($individuo->id); ?>" class="btn btn-secondary btn-lg ml-2">
|
||||
<i class="fas fa-times mr-1"></i> Annulla
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="card mt-3" id="gruppo-add-card" style="display:none; background-color: #fff3cd; border-color: #ffc107;">
|
||||
<div class="card-header bg-warning">
|
||||
<h3 class="card-title"><i class="fas fa-folder-plus mr-2"></i>Aggiungi a Gruppo</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<select id="new-gruppo-id" class="form-control form-control-sm">
|
||||
<option value="">Seleziona gruppo...</option>
|
||||
<?php $__currentLoopData = \App\Models\Gruppo::orderBy('nome')->get(); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $g): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<?php if(!$individuo->gruppi->contains($g->id)): ?>
|
||||
<option value="<?php echo e($g->id); ?>"><?php echo e($g->full_path); ?></option>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select id="new-gruppo-ruolo" class="form-control form-control-sm" multiple size="3">
|
||||
<?php $__currentLoopData = \App\Models\Ruolo::attive(); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $ruolo): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<option value="<?php echo e($ruolo->id); ?>"><?php echo e($ruolo->nome); ?></option>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</select>
|
||||
<small class="text-muted">Ctrl+click per selezionare più ruoli</small>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<input type="date" id="new-gruppo-data" class="form-control form-control-sm">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="submitGruppoForm()">
|
||||
<i class="fas fa-check mr-1"></i> Associa
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm" onclick="hideGruppoForm()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="previewModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document" style="max-width: 90vw;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="fas fa-file mr-2"></i><span id="previewModalTitle">Anteprima</span></h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Chiudi">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body text-center p-0" style="min-height: 400px;">
|
||||
<iframe id="previewFrame" src="" style="width: 100%; height: 70vh; border: none;"></iframe>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a id="previewDownloadBtn" href="#" class="btn btn-primary" download>
|
||||
<i class="fas fa-download mr-1"></i> Scarica
|
||||
</a>
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Chiudi</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php $__env->stopSection(); ?>
|
||||
|
||||
<?php $__env->startSection('scripts'); ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('All form fields:');
|
||||
var form = document.getElementById('main-form');
|
||||
var fields = form.querySelectorAll('input, textarea, select');
|
||||
fields.forEach(function(f) {
|
||||
console.log(f.name + ' [' + f.type + ']: value="' + f.value.substring(0,30) + '" disabled=' + f.disabled);
|
||||
});
|
||||
|
||||
// Debug note field specifically
|
||||
var noteField = form.querySelector('[name="note"]');
|
||||
console.log('Note field found:', !!noteField);
|
||||
if (noteField) {
|
||||
console.log('Note value:', noteField.value);
|
||||
}
|
||||
|
||||
var saveBtn = document.getElementById('save-btn');
|
||||
if (saveBtn) {
|
||||
saveBtn.addEventListener('click', function() {
|
||||
console.log('=== SAVE CLICKED ===');
|
||||
var note = form.querySelector('[name="note"]');
|
||||
console.log('Note at save time:', note ? note.value : 'NOT FOUND');
|
||||
});
|
||||
}
|
||||
|
||||
form.addEventListener('submit', function(e) {
|
||||
console.log('=== FORM SUBMIT ===');
|
||||
var note = form.querySelector('[name="note"]');
|
||||
console.log('Note at submit:', note ? note.value : 'NOT FOUND');
|
||||
|
||||
var contattiFields = form.querySelectorAll('[name^="contatti"]');
|
||||
console.log('Contatti fields count:', contattiFields.length);
|
||||
contattiFields.forEach(function(f, i) {
|
||||
console.log('Contatto field ' + i + ':', f.name, '=', f.value);
|
||||
});
|
||||
|
||||
var formData = new FormData(form);
|
||||
console.log('FormData contatti:', formData.getAll('contatti'));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
function submitMainForm() {
|
||||
var form = document.getElementById('main-form');
|
||||
console.log('=== DEBUG SUBMIT ===');
|
||||
|
||||
var contattiFields = form.querySelectorAll('[name^="contatti"]');
|
||||
console.log('Contatti fields found:', contattiFields.length);
|
||||
contattiFields.forEach(function(f, i) {
|
||||
console.log('Field ' + i + ': name=' + f.name + ', value=' + f.value);
|
||||
});
|
||||
|
||||
form.action = '/individui/<?php echo e($individuo->id); ?>';
|
||||
var methodInput = form.querySelector('input[name="_method"]');
|
||||
if (!methodInput) {
|
||||
var m = document.createElement('input');
|
||||
m.type = 'hidden';
|
||||
m.name = '_method';
|
||||
m.value = 'PUT';
|
||||
form.appendChild(m);
|
||||
}
|
||||
var csrfInput = form.querySelector('input[name="_token"]');
|
||||
if (!csrfInput) {
|
||||
var c = document.createElement('input');
|
||||
c.type = 'hidden';
|
||||
c.name = '_token';
|
||||
c.value = '<?php echo e(csrf_token()); ?>';
|
||||
form.appendChild(c);
|
||||
}
|
||||
|
||||
var formData = new FormData(form);
|
||||
console.log('FormData contatti keys:');
|
||||
for (var pair of formData.entries()) {
|
||||
if (pair[0].startsWith('contatti')) {
|
||||
console.log(' ' + pair[0] + ' = ' + pair[1]);
|
||||
}
|
||||
}
|
||||
|
||||
form.submit();
|
||||
}
|
||||
|
||||
let contattoIndex = <?php echo e($individuo->contatti->count()); ?>;
|
||||
|
||||
function aggiungiRigaContatto() {
|
||||
const tbody = document.getElementById('contatti-tbody');
|
||||
const noMsg = document.getElementById('no-contatti-msg');
|
||||
if (noMsg) noMsg.style.display = 'none';
|
||||
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>
|
||||
<select name="contatti[${contattoIndex}][tipo]" class="form-control">
|
||||
<option value="">Seleziona...</option>
|
||||
<option value="telefono">Telefono</option>
|
||||
<option value="cellulare">Cellulare</option>
|
||||
<option value="email">Email</option>
|
||||
<option value="fax">Fax</option>
|
||||
<option value="web">Web</option>
|
||||
<option value="telegram">Telegram</option>
|
||||
<option value="whatsapp">WhatsApp</option>
|
||||
<option value="altro">Altro</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><input type="text" name="contatti[${contattoIndex}][valore]" class="form-control" placeholder="Valore"></td>
|
||||
<td><input type="text" name="contatti[${contattoIndex}][etichetta]" class="form-control" placeholder="Etichetta"></td>
|
||||
<td class="text-center"><input type="checkbox" name="contatti[${contattoIndex}][is_primary]" value="1"></td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-success btn-xs" title="Salva" onclick="submitMainForm()">
|
||||
<i class="fas fa-save"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger btn-xs" title="Elimina" onclick="this.closest('tr').remove()">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
contattoIndex++;
|
||||
}
|
||||
|
||||
function showGruppoForm() {
|
||||
var card = document.getElementById('gruppo-add-card');
|
||||
if (card) {
|
||||
card.style.display = 'block';
|
||||
card.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}
|
||||
|
||||
function hideGruppoForm() {
|
||||
var card = document.getElementById('gruppo-add-card');
|
||||
if (card) {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function submitGruppoForm() {
|
||||
var gruppoId = document.getElementById('new-gruppo-id').value;
|
||||
var ruoloSelect = document.getElementById('new-gruppo-ruolo');
|
||||
var dataAdesione = document.getElementById('new-gruppo-data').value;
|
||||
|
||||
if (!gruppoId) {
|
||||
alert('Seleziona un gruppo');
|
||||
return;
|
||||
}
|
||||
|
||||
var formData = new URLSearchParams();
|
||||
formData.append('gruppo_id', gruppoId);
|
||||
|
||||
var selectedRuoli = Array.from(ruoloSelect.selectedOptions).map(opt => opt.value).filter(v => v);
|
||||
selectedRuoli.forEach(function(ruoloId) {
|
||||
formData.append('ruolo_ids[]', ruoloId);
|
||||
});
|
||||
|
||||
formData.append('data_adesione', dataAdesione);
|
||||
|
||||
fetch('/individui/<?php echo e($individuo->id); ?>/gruppi', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '<?php echo e(csrf_token()); ?>',
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.text().then(text => { throw new Error(text); });
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
hideGruppoForm();
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(data.error || 'Errore');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Errore: ' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
function editGruppoInline(id) {
|
||||
document.getElementById('gruppo-row-' + id).style.display = 'none';
|
||||
document.getElementById('gruppo-edit-' + id).style.display = 'table-row';
|
||||
}
|
||||
|
||||
function cancelEditGruppo(id) {
|
||||
document.getElementById('gruppo-row-' + id).style.display = 'table-row';
|
||||
document.getElementById('gruppo-edit-' + id).style.display = 'none';
|
||||
}
|
||||
|
||||
function showDocumentoForm() {
|
||||
document.getElementById('documento-add-form').style.display = 'block';
|
||||
}
|
||||
|
||||
function hideDocumentoForm() {
|
||||
document.getElementById('documento-add-form').style.display = 'none';
|
||||
}
|
||||
|
||||
function previewDocumento(id, mimeType) {
|
||||
var previewUrl = '/documenti/' + id + '/preview';
|
||||
var downloadUrl = '/documenti/' + id + '/download';
|
||||
|
||||
document.getElementById('previewFrame').src = previewUrl;
|
||||
document.getElementById('previewDownloadBtn').href = downloadUrl;
|
||||
document.getElementById('previewModalTitle').textContent = 'Anteprima';
|
||||
|
||||
if (mimeType && mimeType.startsWith('image/')) {
|
||||
document.getElementById('previewDownloadBtn').style.display = 'inline-block';
|
||||
} else {
|
||||
document.getElementById('previewDownloadBtn').style.display = 'none';
|
||||
}
|
||||
|
||||
$('#previewModal').modal('show');
|
||||
}
|
||||
|
||||
document.getElementById('avatar-input').addEventListener('change', function(e) {
|
||||
var fileName = e.target.files[0]?.name || 'Scegli file...';
|
||||
e.target.nextElementSibling.textContent = fileName;
|
||||
});
|
||||
|
||||
function uploadAvatar() {
|
||||
var input = document.getElementById('avatar-input');
|
||||
if (!input.files[0]) {
|
||||
document.getElementById('avatar-message').innerHTML = '<span class="text-danger">Seleziona un\'immagine</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
var formData = new FormData();
|
||||
formData.append('avatar', input.files[0]);
|
||||
formData.append('_token', '<?php echo e(csrf_token()); ?>');
|
||||
|
||||
document.getElementById('avatar-message').innerHTML = '<span class="text-info"><i class="fas fa-spinner fa-spin"></i> Caricamento...</span>';
|
||||
|
||||
fetch('/individui/<?php echo e($individuo->id); ?>/avatar', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('avatar-message').innerHTML = '<span class="text-success">Avatar caricato!</span>';
|
||||
setTimeout(function() { window.location.reload(); }, 500);
|
||||
} else {
|
||||
document.getElementById('avatar-message').innerHTML = '<span class="text-danger">' + (data.message || 'Errore') + '</span>';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
document.getElementById('avatar-message').innerHTML = '<span class="text-danger">Errore: ' + error + '</span>';
|
||||
});
|
||||
}
|
||||
|
||||
function deleteAvatar() {
|
||||
if (!confirm('Rimuovere l\'avatar?')) return;
|
||||
|
||||
fetch('/individui/<?php echo e($individuo->id); ?>/avatar', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '<?php echo e(csrf_token()); ?>',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(data.message || 'Errore');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Errore: ' + error);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<?php $__env->stopSection(); ?>
|
||||
|
||||
<?php echo $__env->make('layouts.adminlte', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH /var/www/html/glastree/resources/views/individui/edit.blade.php ENDPATH**/ ?>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?php $__env->startSection('title', __('Not Found')); ?>
|
||||
<?php $__env->startSection('code', '404'); ?>
|
||||
<?php $__env->startSection('message', __('Not Found')); ?>
|
||||
|
||||
<?php echo $__env->make('errors::minimal', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH /var/www/html/glastree/vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/views/404.blade.php ENDPATH**/ ?>
|
||||
@@ -1,123 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?php echo $__env->yieldContent('title', 'Admin'); ?> - Glastree</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/admin-lte@3.2/dist/css/adminlte.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<?php echo $__env->yieldContent('styles'); ?>
|
||||
</head>
|
||||
<body class="hold-transition sidebar-mini layout-fixed">
|
||||
<div class="wrapper">
|
||||
<nav class="main-header navbar navbar-expand navbar-white navbar-light">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-widget="pushmenu" href="#"><i class="fas fa-bars"></i></a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav ml-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="<?php echo e(route('dashboard')); ?>">
|
||||
<i class="fas fa-home"></i> Torna al Sito
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link" data-toggle="dropdown" href="#">
|
||||
<i class="fas fa-user"></i> <?php echo e(Auth::user()->name); ?>
|
||||
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<form method="POST" action="<?php echo e(route('logout')); ?>">
|
||||
<?php echo csrf_field(); ?>
|
||||
<button type="submit" class="dropdown-item">
|
||||
<i class="fas fa-sign-out-alt mr-2"></i> Esci
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<aside class="main-sidebar sidebar-dark-primary elevation-4">
|
||||
<a href="<?php echo e(route('dashboard')); ?>" class="brand-link">
|
||||
<i class="fas fa-shield-alt brand-image ml-3 mr-2"></i>
|
||||
<span class="brand-text font-weight-light">Admin</span>
|
||||
</a>
|
||||
|
||||
<div class="sidebar">
|
||||
<nav class="mt-2">
|
||||
<ul class="nav nav-pills nav-sidebar flex-column">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/utenti" class="nav-link <?php echo e(request()->is('admin/utenti*') ? 'active' : ''); ?>">
|
||||
<i class="nav-icon fas fa-users"></i>
|
||||
<p>Utenti</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/ruoli" class="nav-link <?php echo e(request()->is('admin/ruoli*') ? 'active' : ''); ?>">
|
||||
<i class="nav-icon fas fa-user-tag"></i>
|
||||
<p>Ruoli</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/backup" class="nav-link <?php echo e(request()->is('admin/backup*') ? 'active' : ''); ?>">
|
||||
<i class="nav-icon fas fa-hdd"></i>
|
||||
<p>Backup</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/activity-logs" class="nav-link <?php echo e(request()->is('admin/activity-logs*') ? 'active' : ''); ?>">
|
||||
<i class="nav-icon fas fa-history"></i>
|
||||
<p>Log Attività</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-6">
|
||||
<h1 class="m-0"><?php echo $__env->yieldContent('page_title', 'Admin'); ?></h1>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<ol class="breadcrumb float-sm-right">
|
||||
<li class="breadcrumb-item"><a href="<?php echo e(route('dashboard')); ?>">Dashboard</a></li>
|
||||
<?php echo $__env->yieldContent('breadcrumbs'); ?>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="container-fluid">
|
||||
<?php if(session('success')): ?>
|
||||
<div class="alert alert-success"><?php echo e(session('success')); ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if(session('error')): ?>
|
||||
<div class="alert alert-danger"><?php echo e(session('error')); ?></div>
|
||||
<?php endif; ?>
|
||||
<?php echo $__env->yieldContent('content'); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="main-footer">
|
||||
<strong><?php echo e(\App\Models\AppSetting::getFooterText()); ?></strong>
|
||||
<?php if(\App\Models\AppSetting::shouldShowVersion() && \App\Models\AppSetting::getAppVersion()): ?>
|
||||
<span class="ml-2">v<?php echo e(\App\Models\AppSetting::getAppVersion()); ?></span>
|
||||
<?php endif; ?>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/admin-lte@3.2/dist/js/adminlte.min.js"></script>
|
||||
<?php echo $__env->yieldContent('scripts'); ?>
|
||||
</body>
|
||||
</html><?php /**PATH /var/www/html/glastree/resources/views/admin/layout.blade.php ENDPATH**/ ?>
|
||||
@@ -1,44 +0,0 @@
|
||||
<?php if($paginator->hasPages()): ?>
|
||||
<ul class="pagination justify-content-center" role="navigation">
|
||||
|
||||
<?php if($paginator->onFirstPage()): ?>
|
||||
<li class="page-item disabled" aria-disabled="true" aria-label="<?php echo app('translator')->get('pagination.previous'); ?>">
|
||||
<span class="page-link" aria-hidden="true">‹</span>
|
||||
</li>
|
||||
<?php else: ?>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="<?php echo e($paginator->previousPageUrl()); ?>" rel="prev" aria-label="<?php echo app('translator')->get('pagination.previous'); ?>">‹</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<?php $__currentLoopData = $elements; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $element): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
|
||||
<?php if(is_string($element)): ?>
|
||||
<li class="page-item disabled" aria-disabled="true"><span class="page-link"><?php echo e($element); ?></span></li>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<?php if(is_array($element)): ?>
|
||||
<?php $__currentLoopData = $element; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $page => $url): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<?php if($page == $paginator->currentPage()): ?>
|
||||
<li class="page-item active" aria-current="page"><span class="page-link"><?php echo e($page); ?></span></li>
|
||||
<?php elseif($page >= $paginator->currentPage() - 2 && $page <= $paginator->currentPage() + 2): ?>
|
||||
<li class="page-item"><a class="page-link" href="<?php echo e($url); ?>"><?php echo e($page); ?></a></li>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
|
||||
|
||||
<?php if($paginator->hasMorePages()): ?>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="<?php echo e($paginator->nextPageUrl()); ?>" rel="next" aria-label="<?php echo app('translator')->get('pagination.next'); ?>">›</a>
|
||||
</li>
|
||||
<?php else: ?>
|
||||
<li class="page-item disabled" aria-disabled="true" aria-label="<?php echo app('translator')->get('pagination.next'); ?>">
|
||||
<span class="page-link" aria-hidden="true">›</span>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
<?php endif; ?><?php /**PATH /var/www/html/glastree/resources/views/vendor/pagination/simple-bootstrap-4.blade.php ENDPATH**/ ?>
|
||||
@@ -1,248 +0,0 @@
|
||||
<?php $appName = \App\Models\AppSetting::getAppName() ?? 'Glastree'; ?>
|
||||
<?php $__env->startSection('title', 'Backup - ' . $appName); ?>
|
||||
<?php $__env->startSection('page_title', 'Backup e Migrazione'); ?>
|
||||
<?php $__env->startSection('breadcrumbs'); ?>
|
||||
<li class="breadcrumb-item"><a href="/">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="/admin/utenti">Admin</a></li>
|
||||
<li class="breadcrumb-item active">Backup</li>
|
||||
<?php $__env->stopSection(); ?>
|
||||
|
||||
<?php $__env->startSection('content'); ?>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
|
||||
<div class="card card-outline card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-hdd mr-2"></i>Backup Disponibili</h3>
|
||||
<div class="card-tools">
|
||||
<form method="POST" action="<?php echo e(route('admin.backup.run')); ?>" style="display:inline">
|
||||
<?php echo csrf_field(); ?>
|
||||
<button type="submit" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-play mr-1"></i> Esegui Backup Ora
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<?php if(count($backups) > 0): ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>File</th>
|
||||
<th>Data</th>
|
||||
<th>Dimensione</th>
|
||||
<th>Contenuto</th>
|
||||
<th class="text-center">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php $__currentLoopData = $backups; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $backup): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<tr>
|
||||
<td><code><?php echo e($backup['filename']); ?></code></td>
|
||||
<td><?php echo e(date('d/m/Y H:i', $backup['last_modified'])); ?></td>
|
||||
<td><?php echo e($backup['size_formatted']); ?></td>
|
||||
<td>
|
||||
<?php if($backup['manifest']): ?>
|
||||
<span class="badge badge-info" title="PHP <?php echo e($backup['manifest']['php_version'] ?? '?'); ?>, Laravel <?php echo e($backup['manifest']['laravel_version'] ?? '?'); ?>">
|
||||
<i class="fas fa-database mr-1"></i>DB
|
||||
</span>
|
||||
<?php if(isset($backup['manifest']['db_name'])): ?>
|
||||
<small class="text-muted"><?php echo e($backup['manifest']['db_name']); ?></small>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-secondary">N/A</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="<?php echo e(route('admin.backup.download', $backup['filename'])); ?>" class="btn btn-success btn-sm" title="Scarica">
|
||||
<i class="fas fa-download"></i>
|
||||
</a>
|
||||
<form method="POST" action="<?php echo e(route('admin.backup.destroy', $backup['filename'])); ?>" style="display:inline" onsubmit="return confirm('Eliminare definitivamente questo backup?')">
|
||||
<?php echo csrf_field(); ?>
|
||||
<?php echo method_field('DELETE'); ?>
|
||||
<button type="submit" class="btn btn-danger btn-sm" title="Elimina">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-hdd fa-3x text-muted mb-3"></i>
|
||||
<p class="text-muted mb-0">Nessun backup disponibile.</p>
|
||||
<p class="text-muted">Clicca "Esegui Backup Ora" per creare il primo backup.</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card card-outline card-info">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-info-circle mr-2"></i>Cosa Include il Backup</h3>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-bordered mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Componente</th>
|
||||
<th>Incluso</th>
|
||||
<th>Dettagli</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><i class="fas fa-database text-primary mr-2"></i> Database (MySQL)</td>
|
||||
<td><span class="badge badge-success">Sempre</span></td>
|
||||
<td>Struttura, dati, stored procedure, eventi, trigger</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i class="fas fa-file text-warning mr-2"></i> File caricati</td>
|
||||
<td>
|
||||
<?php if($config['backup_include_files']): ?>
|
||||
<span class="badge badge-success">Sì</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-secondary">No</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>Documenti, avatar, gallerie — dalla cartella storage</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i class="fas fa-cog text-secondary mr-2"></i> Configurazione (.env)</td>
|
||||
<td>
|
||||
<?php if($config['backup_include_env']): ?>
|
||||
<span class="badge badge-success">Sì</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-secondary">No</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>APP_KEY, DB credentials, SMTP, OAuth token</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i class="fas fa-tag text-info mr-2"></i> Metadati</td>
|
||||
<td><span class="badge badge-success">Sempre</span></td>
|
||||
<td>Manifest JSON con versione app, PHP, Laravel</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
|
||||
<div class="card card-outline card-secondary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-sliders-h mr-2"></i>Configurazione Backup</h3>
|
||||
</div>
|
||||
<form method="POST" action="<?php echo e(route('admin.backup.config')); ?>">
|
||||
<?php echo csrf_field(); ?>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="backup_path">Percorso backup</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><code>storage/app/</code></span>
|
||||
</div>
|
||||
<input type="text" name="backup_path" id="backup_path" class="form-control" value="<?php echo e($config['backup_path']); ?>">
|
||||
</div>
|
||||
<small class="text-muted">Cartella dove salvare i backup (relativa a storage/app/)</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="backup_retention_days">Ritenzione (giorni)</label>
|
||||
<input type="number" name="backup_retention_days" id="backup_retention_days" class="form-control" value="<?php echo e($config['backup_retention_days']); ?>" min="1" max="365">
|
||||
<small class="text-muted">I backup più vecchi vengono eliminati automaticamente</small>
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" id="backup_include_files" name="backup_include_files" value="1" <?php echo e($config['backup_include_files'] ? 'checked' : ''); ?>>
|
||||
<label class="custom-control-label" for="backup_include_files">Includi file caricati</label>
|
||||
</div>
|
||||
<div class="custom-control custom-switch mt-2">
|
||||
<input type="checkbox" class="custom-control-input" id="backup_include_env" name="backup_include_env" value="1" <?php echo e($config['backup_include_env'] ? 'checked' : ''); ?>>
|
||||
<label class="custom-control-label" for="backup_include_env">Includi .env (configurazione)</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<button type="submit" class="btn btn-secondary btn-block">
|
||||
<i class="fas fa-save mr-1"></i> Salva Configurazione
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card card-outline card-warning">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-clock mr-2"></i>Backup Automatico</h3>
|
||||
<div class="card-tools">
|
||||
<?php $autoEnabled = \App\Models\AppSetting::getSetting('backup_auto_enabled', false) ?>
|
||||
<form method="POST" action="<?php echo e(route('admin.backup.toggle-auto')); ?>" style="display:inline">
|
||||
<?php echo csrf_field(); ?>
|
||||
<button type="submit" class="btn btn-sm <?php echo e($autoEnabled ? 'btn-success' : 'btn-secondary'); ?>">
|
||||
<i class="fas <?php echo e($autoEnabled ? 'fa-check-circle' : 'fa-times-circle'); ?> mr-1"></i>
|
||||
<?php echo e($autoEnabled ? 'Attivo' : 'Disattivo'); ?>
|
||||
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" action="<?php echo e(route('admin.backup.save-auto')); ?>">
|
||||
<?php echo csrf_field(); ?>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="backup_auto_frequency">Frequenza</label>
|
||||
<select name="backup_auto_frequency" id="backup_auto_frequency" class="form-control">
|
||||
<option value="daily" <?php echo e(\App\Models\AppSetting::getSetting('backup_auto_frequency', 'daily') === 'daily' ? 'selected' : ''); ?>>Giornaliero</option>
|
||||
<option value="weekly" <?php echo e(\App\Models\AppSetting::getSetting('backup_auto_frequency', 'daily') === 'weekly' ? 'selected' : ''); ?>>Settimanale</option>
|
||||
<option value="monthly" <?php echo e(\App\Models\AppSetting::getSetting('backup_auto_frequency', 'daily') === 'monthly' ? 'selected' : ''); ?>>Mensile</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<label for="backup_auto_hour">Ora di esecuzione</label>
|
||||
<select name="backup_auto_hour" id="backup_auto_hour" class="form-control">
|
||||
<?php for($h = 0; $h <= 23; $h++): ?>
|
||||
<option value="<?php echo e($h); ?>" <?php echo e(\App\Models\AppSetting::getSetting('backup_auto_hour', 3) == $h ? 'selected' : ''); ?>><?php echo e(sprintf('%02d:00', $h)); ?></option>
|
||||
<?php endfor; ?>
|
||||
</select>
|
||||
<small class="text-muted">Ora del giorno (formato 24h). Consigliato: 03:00 (notte)</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<button type="submit" class="btn btn-warning btn-block">
|
||||
<i class="fas fa-save mr-1"></i> Salva Automatico
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card card-outline card-danger">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-truck mr-2"></i>Migrazione Server</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">Scarica il backup, poi esegui lo script di setup sul nuovo server per una migrazione completa.</p>
|
||||
<a href="<?php echo e(route('help')); ?>#help-backup" class="btn btn-danger btn-block" target="_blank">
|
||||
<i class="fas fa-book mr-1"></i> Guida alla Migrazione
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle mr-2"></i>
|
||||
<strong>Importante:</strong> Per il backup automatico, assicurati che il cron di Laravel sia configurato sul server:
|
||||
<code class="ml-2">* * * * * cd <?php echo e(base_path()); ?> && php artisan schedule:run >> /dev/null 2>&1</code>
|
||||
</div>
|
||||
<?php $__env->stopSection(); ?>
|
||||
|
||||
<?php echo $__env->make('admin.layout', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH /var/www/html/glastree/resources/views/admin/backup/index.blade.php ENDPATH**/ ?>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,72 +0,0 @@
|
||||
<?php $attributes ??= new \Illuminate\View\ComponentAttributeBag;
|
||||
|
||||
$__newAttributes = [];
|
||||
$__propNames = \Illuminate\View\ComponentAttributeBag::extractPropNames((['cartelle', 'currentFolderId' => null, 'level' => 0, 'canWrite' => false, 'canDelete' => false]));
|
||||
|
||||
foreach ($attributes->all() as $__key => $__value) {
|
||||
if (in_array($__key, $__propNames)) {
|
||||
$$__key = $$__key ?? $__value;
|
||||
} else {
|
||||
$__newAttributes[$__key] = $__value;
|
||||
}
|
||||
}
|
||||
|
||||
$attributes = new \Illuminate\View\ComponentAttributeBag($__newAttributes);
|
||||
|
||||
unset($__propNames);
|
||||
unset($__newAttributes);
|
||||
|
||||
foreach (array_filter((['cartelle', 'currentFolderId' => null, 'level' => 0, 'canWrite' => false, 'canDelete' => false]), 'is_string', ARRAY_FILTER_USE_KEY) as $__key => $__value) {
|
||||
$$__key = $$__key ?? $__value;
|
||||
}
|
||||
|
||||
$__defined_vars = get_defined_vars();
|
||||
|
||||
foreach ($attributes->all() as $__key => $__value) {
|
||||
if (array_key_exists($__key, $__defined_vars)) unset($$__key);
|
||||
}
|
||||
|
||||
unset($__defined_vars, $__key, $__value); ?>
|
||||
|
||||
<?php $__currentLoopData = $cartelle; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $cartella): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<?php
|
||||
$isActive = $currentFolderId && (int) $currentFolderId === (int) $cartella->id;
|
||||
$hasChildren = $cartella->children->count() > 0;
|
||||
?>
|
||||
<div class="list-group-item d-flex align-items-center py-1 px-2 <?php echo e($isActive ? 'active' : ''); ?>"
|
||||
style="padding-left: <?php echo e(20 + $level * 20); ?>px !important;">
|
||||
<a href="/documenti?folder_id=<?php echo e($cartella->id); ?>"
|
||||
class="d-flex align-items-center text-decoration-none flex-grow-1 <?php echo e($isActive ? 'text-white' : ''); ?>"
|
||||
style="min-width: 0;">
|
||||
<i class="fas fa-folder <?php echo e($isActive ? 'text-white' : 'text-warning'); ?> mr-2"></i>
|
||||
<span class="small text-truncate"><?php echo e($cartella->nome); ?></span>
|
||||
</a>
|
||||
<?php if($canWrite || $canDelete): ?>
|
||||
<div class="folder-actions ml-1 flex-shrink-0">
|
||||
<?php if($canWrite): ?>
|
||||
<button type="button" class="btn btn-xs btn-link p-0 mr-1" onclick='event.stopPropagation(); renameFolder(<?php echo e($cartella->id); ?>, <?php echo e(json_encode($cartella->nome)); ?>)' title="Rinomina">
|
||||
<i class="fas fa-pen <?php echo e($isActive ? 'text-white' : 'text-muted'); ?>"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-xs btn-link p-0 mr-1" onclick='event.stopPropagation(); moveFolder(<?php echo e($cartella->id); ?>, <?php echo e(json_encode($cartella->nome)); ?>)' title="Sposta">
|
||||
<i class="fas fa-folder-open <?php echo e($isActive ? 'text-white' : 'text-info'); ?>"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if($canDelete): ?>
|
||||
<button type="button" class="btn btn-xs btn-link p-0" onclick='event.stopPropagation(); deleteFolder(<?php echo e($cartella->id); ?>, <?php echo e(json_encode($cartella->nome)); ?>)' title="Elimina">
|
||||
<i class="fas fa-times <?php echo e($isActive ? 'text-white' : 'text-danger'); ?>"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if($hasChildren): ?>
|
||||
<?php echo $__env->make('documenti._folder_tree', [
|
||||
'cartelle' => $cartella->children,
|
||||
'currentFolderId' => $currentFolderId,
|
||||
'level' => $level + 1,
|
||||
'canWrite' => $canWrite,
|
||||
'canDelete' => $canDelete,
|
||||
], array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
<?php /**PATH /var/www/html/glastree/resources/views/documenti/_folder_tree.blade.php ENDPATH**/ ?>
|
||||
@@ -1,972 +0,0 @@
|
||||
<?php $appName = \App\Models\AppSetting::getAppName() ?? 'Glastree'; ?>
|
||||
<?php $__env->startSection('title', 'Guida - ' . $appName); ?>
|
||||
<?php $__env->startSection('page_title', 'Guida alla Configurazione'); ?>
|
||||
<?php $__env->startSection('breadcrumbs'); ?>
|
||||
<li class="breadcrumb-item"><a href="/">Home</a></li>
|
||||
<li class="breadcrumb-item active">Guida</li>
|
||||
<?php $__env->stopSection(); ?>
|
||||
|
||||
<?php $__env->startSection('content'); ?>
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="card card-outline card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Argomenti</h3>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<ul class="nav nav-pills flex-column" id="helpTabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="#help-webdav" data-toggle="tab">
|
||||
<i class="fas fa-cloud mr-2"></i> WebDAV / Nextcloud
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#help-googledrive" data-toggle="tab">
|
||||
<i class="fab fa-google-drive mr-2"></i> Google Drive
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#help-tipologie" data-toggle="tab">
|
||||
<i class="fas fa-tags mr-2"></i> Tipologie e Dati
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#help-email" data-toggle="tab">
|
||||
<i class="fas fa-envelope mr-2"></i> Email / SMTP
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#help-installazione" data-toggle="tab">
|
||||
<i class="fas fa-terminal mr-2"></i> Installazione
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#help-backup" data-toggle="tab">
|
||||
<i class="fas fa-hdd mr-2"></i> Backup e Migrazione
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#help-docker" data-toggle="tab">
|
||||
<i class="fab fa-docker mr-2"></i> Docker
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<a href="<?php echo e(route('help.pdf.download')); ?>" class="btn btn-primary btn-block">
|
||||
<i class="fas fa-file-pdf mr-2"></i> Scarica Guida PDF
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-9">
|
||||
<div class="tab-content">
|
||||
|
||||
<div class="tab-pane active" id="help-webdav">
|
||||
<div class="card card-outline card-info">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-cloud mr-2"></i> WebDAV / Nextcloud</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5>Cos'è</h5>
|
||||
<p>WebDAV è un protocollo che permette di accedere a file su server remoti. Nextcloud lo utilizza per la sincronizzazione dei file.</p>
|
||||
|
||||
<h5>Configurazione</h5>
|
||||
<p>Vai in <strong>Impostazioni → Repository</strong> e crea un nuovo repository di tipo WebDAV.</p>
|
||||
|
||||
<div class="callout callout-info">
|
||||
<h6>Campi obbligatori</h6>
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-sm-3">Nome</dt>
|
||||
<dd class="col-sm-9">Un nome descrittivo (es. "Nextcloud Lavoro")</dd>
|
||||
<dt class="col-sm-3">Base URI</dt>
|
||||
<dd class="col-sm-9">L'URL del tuo server WebDAV.
|
||||
<br><em>Nextcloud:</em> <code>https://tuodominio.it/remote.php/dav/files/utente/</code>
|
||||
<br><em>Altri:</em> <code>https://server.webdav.it/</code></dd>
|
||||
<dt class="col-sm-3">Root</dt>
|
||||
<dd class="col-sm-9">Percorso base all'interno del server (es. <code>/Documenti/</code>). Lascia <code>/</code> per usare la root.</dd>
|
||||
<dt class="col-sm-3">Username</dt>
|
||||
<dd class="col-sm-9">Il tuo nome utente</dd>
|
||||
<dt class="col-sm-3">Password</dt>
|
||||
<dd class="col-sm-9">La password dell'account</dd>
|
||||
<dt class="col-sm-3">Auth Type</dt>
|
||||
<dd class="col-sm-9"><code>Basic</code> per la maggior parte dei server (Nextcloud, ownCloud).</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<h5 class="mt-4">⚠️ Nome utente con @ (chiocciola)</h5>
|
||||
<p>Se il tuo nome utente contiene il carattere <code>@</code> (es. <code>mario.rossi@azienda.com</code>), segui queste regole:</p>
|
||||
<ul>
|
||||
<li><strong>Nel campo "Base URI":</strong> l'indirizzo email deve essere <strong>URL-encoded</strong>:
|
||||
<br><code>https://server.it/remote.php/dav/files/mario.rossi%40azienda.com/</code>
|
||||
<br><small>(il carattere <code>@</code> diventa <code>%40</code>)</small></li>
|
||||
<li><strong>Nel campo "Username":</strong> inserisci l'indirizzo email <strong>normale</strong> con la <code>@</code> vera.</li>
|
||||
</ul>
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Il sistema gestisce automaticamente la differenza tra l'URI (che richiede <code>%40</code>) e l'autenticazione (che usa la <code>@</code> reale).
|
||||
</div>
|
||||
|
||||
<h5 class="mt-4">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 dei repository per verificare che la connessione funzioni.</p>
|
||||
|
||||
<h5 class="mt-4">Importare file</h5>
|
||||
<p>Apri la sezione <strong>Documenti</strong>, clicca sul pulsante del repository nella toolbar per sfogliare i file remoti, poi clicca <span class="badge badge-success"><i class="fas fa-save"></i> Salva tra i documenti</span> su un file per importarlo come documento locale.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="tab-pane" id="help-googledrive">
|
||||
<div class="card card-outline card-danger">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fab fa-google-drive mr-2"></i> Google Drive</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5>Cos'è</h5>
|
||||
<p>Integrazione con Google Drive per sfogliare e importare file direttamente dal tuo spazio Google Drive.</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 Drive 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 Drive 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><?php echo e(url('/auth/google-drive/callback')); ?></code></li>
|
||||
<li>Completa la creazione e <strong>copia Client ID e Client Secret</strong></li>
|
||||
</ol>
|
||||
|
||||
<h5>Passo 2: Configurazione nell'app</h5>
|
||||
<ol>
|
||||
<li>Vai in <strong>Impostazioni → Repository</strong></li>
|
||||
<li>Clicca "<strong>Nuovo Repository</strong>" e seleziona tipo <strong>Google Drive</strong></li>
|
||||
<li>Inserisci un nome descrittivo (es. "Drive Lavoro")</li>
|
||||
<li>Incolla <strong>Client ID</strong> e <strong>Client Secret</strong></li>
|
||||
<li><strong>Folder ID (opzionale):</strong> se vuoi limitare l'accesso a una cartella specifica, inserisci il suo ID (lo trovi nell'URL quando apri la cartella in Google Drive: <code>https://drive.google.com/drive/folders/<strong>ID_QUI</strong></code>)</li>
|
||||
<li>Clicca "<strong>Autorizza Google Drive</strong>" — verrai reindirizzato a Google per concedere i permessi</li>
|
||||
<li>Dopo l'autorizzazione, il refresh token verrà salvato automaticamente</li>
|
||||
</ol>
|
||||
|
||||
<h5>⚠️ Google Drive API non abilitata</h5>
|
||||
<p>Se vedi l'errore <em>"Google Drive API non abilitata"</em> nel test connessione, significa che l'API Drive non è stata attivata nel tuo progetto Google Cloud.</p>
|
||||
<div class="callout callout-danger">
|
||||
<h6>Soluzione</h6>
|
||||
<p>Vai su <a href="https://console.developers.google.com/apis/api/drive.googleapis.com/overview" target="_blank">
|
||||
https://console.developers.google.com/apis/api/drive.googleapis.com/overview
|
||||
</a></p>
|
||||
<p>Seleziona il progetto corretto dall'header in alto, clicca <strong>"Enable"</strong> e attendi qualche minuto prima di riprovare.</p>
|
||||
</div>
|
||||
|
||||
<h5>Importare file</h5>
|
||||
<p>Stessa procedura di WebDAV: apri <strong>Documenti</strong>, clicca sul repository Drive, sfoglia i file e usa <span class="badge badge-success"><i class="fas fa-save"></i> Salva tra i documenti</span>.</p>
|
||||
|
||||
<h5 class="mt-4">⚠️ File Google (Documenti, Fogli, Presentazioni)</h5>
|
||||
<p>I file creati con Google Workspace (Documenti Google, Fogli Google, Presentazioni Google, Disegni Google) non possono essere scaricati direttamente perché non hanno un formato binario nativo.</p>
|
||||
<p>Il sistema li converte automaticamente in formato Office Open XML durante l'import:</p>
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead><tr><th>File Google</th><th>Formato convertito</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Documento Google</td><td><code>.docx</code> (Word)</td></tr>
|
||||
<tr><td>Foglio Google</td><td><code>.xlsx</code> (Excel)</td></tr>
|
||||
<tr><td>Presentazione Google</td><td><code>.pptx</code> (PowerPoint)</td></tr>
|
||||
<tr><td>Disegno Google</td><td><code>.png</code> (Immagine)</td></tr>
|
||||
<tr><td>Altri (Moduli, Script)</td><td><code>.pdf</code> (PDF)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Il nome del file viene automaticamente esteso con l'estensione corretta (es. "Relazione" → "Relazione.docx").
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="tab-pane" id="help-tipologie">
|
||||
<div class="card card-outline card-success">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-tags mr-2"></i> Tipologie e Gestione Dati</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5>Tipologie Documenti</h5>
|
||||
<p>Le tipologie classificano i documenti (avatar, galleria, documento, statuto, altro).</p>
|
||||
<p>Si gestiscono in <strong>Impostazioni → Tipologie Documenti</strong>. Puoi aggiungere, rinominare, riordinare ed eliminare tipologie (tranne quelle con documenti associati).</p>
|
||||
|
||||
<h5>Tipologie Individui</h5>
|
||||
<p>Classificano gli individui (es. socio, volontario, dipendente). Si gestiscono in <strong>Impostazioni → Tipologie</strong>.</p>
|
||||
|
||||
<h5>Tipologie Eventi</h5>
|
||||
<p>Classificano gli eventi (es. riunione, assemblea, formazione). Si gestiscono in <strong>Impostazioni → Tipologie Eventi</strong>.</p>
|
||||
|
||||
<h5>Ruoli Individui</h5>
|
||||
<p>Definiscono il ruolo o la carica di un individuo (es. presidente, segretario, tesoriere). Si gestiscono in <strong>Impostazioni → Ruoli</strong>.</p>
|
||||
|
||||
<h5>Visibilità Documenti</h5>
|
||||
<p>Ogni documento può avere diversi livelli di visibilità:</p>
|
||||
<ul>
|
||||
<li><strong>Pubblico</strong> — visibile a tutti gli utenti</li>
|
||||
<li><strong>Individuo</strong> — visibile solo a uno specifico individuo</li>
|
||||
<li><strong>Gruppo</strong> — visibile solo ai membri di un gruppo</li>
|
||||
<li><strong>Evento</strong> — visibile solo ai partecipanti di un evento</li>
|
||||
<li><strong>Mailing</strong> — visibile solo ai membri di una mailing list</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="tab-pane" id="help-email">
|
||||
<div class="card card-outline card-warning">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-envelope mr-2"></i> Email / SMTP</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5>Configurazione Email</h5>
|
||||
<p>Vai in <strong>Impostazioni → Email</strong> per configurare la posta elettronica.</p>
|
||||
|
||||
<h5>Server IMAP (Ricezione)</h5>
|
||||
<p>Usato per ricevere e leggere le email direttamente nell'app.</p>
|
||||
<div class="callout callout-info">
|
||||
<h6>Campi consigliati per provider comuni</h6>
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead><tr><th>Provider</th><th>Host</th><th>Porta</th><th>Crittografia</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Gmail</td><td><code>imap.gmail.com</code></td><td><code>993</code></td><td>SSL</td></tr>
|
||||
<tr><td>Outlook/Office 365</td><td><code>outlook.office365.com</code></td><td><code>993</code></td><td>SSL</td></tr>
|
||||
<tr><td>Libero</td><td><code>imapmail.libero.it</code></td><td><code>993</code></td><td>SSL</td></tr>
|
||||
<tr><td>Aruba</td><td><code>imap.aruba.it</code></td><td><code>993</code></td><td>SSL</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h5>Server SMTP (Invio)</h5>
|
||||
<p>Usato per inviare email dall'app. Supporta account multipli (mittenti).</p>
|
||||
<div class="callout callout-info">
|
||||
<h6>Campi consigliati per provider comuni</h6>
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead><tr><th>Provider</th><th>Host</th><th>Porta</th><th>Crittografia</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Gmail</td><td><code>smtp.gmail.com</code></td><td><code>587</code></td><td>TLS</td></tr>
|
||||
<tr><td>Outlook/Office 365</td><td><code>smtp.office365.com</code></td><td><code>587</code></td><td>TLS</td></tr>
|
||||
<tr><td>Libero</td><td><code>smtp.libero.it</code></td><td><code>465</code></td><td>SSL</td></tr>
|
||||
<tr><td>Aruba</td><td><code>smtp.aruba.it</code></td><td><code>465</code></td><td>SSL</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h5>Gmail / Google Workspace</h5>
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
Gmail richiede una <strong>Password per l'app</strong> (non la password normale) se usi la verifica in due passaggi. Puoi generarla su
|
||||
<a href="https://myaccount.google.com/apppasswords" target="_blank">https://myaccount.google.com/apppasswords</a>.
|
||||
</div>
|
||||
|
||||
<h5>Mailing</h5>
|
||||
<p>Le <strong>Mailing List</strong> permettono di inviare email a gruppi predefiniti di destinatari.</p>
|
||||
<ul>
|
||||
<li>Crea una mailing list in <strong>Mailing List → Nuova</strong></li>
|
||||
<li>Puoi popolare la lista da individui, gruppi o manualmente</li>
|
||||
<li>Invia email alla lista da <strong>Email → Nuova Email</strong>, selezionando la mailing list come destinatario</li>
|
||||
</ul>
|
||||
|
||||
<h5>Test connessione</h5>
|
||||
<p>Dopo aver configurato i server, usa il pulsante <span class="badge badge-success"><i class="fas fa-vial"></i> Test Connessione</span> per verificare che IMAP e SMTP funzionino correttamente.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="tab-pane" id="help-backup">
|
||||
<div class="card card-outline card-danger">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-hdd mr-2"></i> Backup e Migrazione Server</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5>Cos'è</h5>
|
||||
<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>
|
||||
|
||||
<h5>Cosa include il backup</h5>
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead><tr><th>Componente</th><th>Incluso</th><th>Dettagli</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Database (MySQL)</td><td><span class="badge badge-success">Sempre</span></td><td>Struttura, dati, stored procedure, eventi, trigger</td></tr>
|
||||
<tr><td>File caricati</td><td><span class="badge badge-success">Opzionale</span></td><td>Documenti, avatar, gallerie</td></tr>
|
||||
<tr><td>Configurazione .env</td><td><span class="badge badge-success">Opzionale</span></td><td>APP_KEY, credenziali DB, SMTP, token OAuth</td></tr>
|
||||
<tr><td>Metadati</td><td><span class="badge badge-success">Sempre</span></td><td>Versione app, PHP, Laravel</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h5 class="mt-4">Come eseguire un backup</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="callout callout-info">
|
||||
<h6>Via Interfaccia Web</h6>
|
||||
<ol class="mb-0">
|
||||
<li>Vai su <strong>Admin → Backup</strong></li>
|
||||
<li>Verifica le impostazioni (includi files/.env)</li>
|
||||
<li>Clicca <span class="badge badge-primary"><i class="fas fa-play"></i> Esegui Backup Ora</span></li>
|
||||
<li>Scarica il file ZIP generato</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="callout callout-success">
|
||||
<h6>Via Comando</h6>
|
||||
<pre class="mb-0"><code>php artisan backup:run</code></pre>
|
||||
<p class="mb-0 mt-2 small">Il file viene creato in <code>storage/app/backups/</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="mt-4">Backup automatico</h5>
|
||||
<p>Puoi abilitare il backup automatico dalla pagina <strong>Admin → Backup</strong>:</p>
|
||||
<ul>
|
||||
<li><strong>Frequenza:</strong> giornaliero, settimanale o mensile</li>
|
||||
<li><strong>Ora:</strong> consigliata 03:00 (notte)</li>
|
||||
<li><strong>Requisito:</strong> il cron di Laravel deve essere configurato sul server</li>
|
||||
</ul>
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-clock mr-2"></i>
|
||||
Per il backup automatico, aggiungi questo cron:
|
||||
<pre class="mt-2 mb-0"><code>* * * * * cd <?php echo e(base_path()); ?> && php artisan schedule:run >> /dev/null 2>&1</code></pre>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<h4><i class="fas fa-truck mr-2"></i> Procedura di Migrazione Completa</h4>
|
||||
<p>Segui questa procedura per migrare l'applicazione su un nuovo server LAMP (Linux, Apache, MySQL, PHP).</p>
|
||||
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-triangle mr-2"></i>
|
||||
<strong>ATTENZIONE: APP_KEY</strong>
|
||||
<p class="mb-0 mt-2">L'APP_KEY cripta i dati sensibili (password email SMTP, token OAuth Google Drive, ecc.).</p>
|
||||
<ul class="mt-2">
|
||||
<li><strong>Se hai il .env originale dal backup:</strong> NON rigenerare APP_KEY — mantieni i dati criptati accessibili.</li>
|
||||
<li><strong>Se hai perso il .env:</strong> DEVI rigenerare APP_KEY, ma perderai l'accesso ai dati criptati esistenti. Dovrai riconfigurare SMTP e OAuth.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h5 class="mt-4">Passo 1: Preparazione del server</h5>
|
||||
<pre><code>sudo apt update && sudo apt upgrade -y</code></pre>
|
||||
|
||||
<h5 class="mt-4">Passo 2: Installa LAMP + PHP 8.4</h5>
|
||||
<pre><code>sudo apt install -y apache2 mariadb-server php8.4 php8.4-cli \
|
||||
php8.4-mysql php8.4-xml php8.4-mbstring php8.4-curl \
|
||||
php8.4-zip php8.4-bcmath php8.4-gd php8.4-intl \
|
||||
composer git unzip
|
||||
|
||||
sudo a2enmod rewrite</code></pre>
|
||||
|
||||
<h5 class="mt-4">Passo 3: Ottieni l'applicazione</h5>
|
||||
<div class="callout callout-info py-2">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Puoi clonare il repository con git oppure estrarre il file ZIP ricevuto nella cartella <code>/var/www/glastree</code>.
|
||||
</div>
|
||||
<pre><code>cd /var/www
|
||||
|
||||
# Opzione A — Git clone
|
||||
git clone <URL_REPOSITORY> glastree
|
||||
|
||||
# Opzione B — ZIP (se non hai git)
|
||||
# unzip /percorso/del/glastree.zip -d glastree
|
||||
|
||||
cd glastree
|
||||
composer install --no-dev --optimize-autoloader
|
||||
npm ci && npm run build</code></pre>
|
||||
|
||||
<h5 class="mt-4">Passo 4: Carica il backup</h5>
|
||||
<p>Copia il file <code>backup.zip</code> sul nuovo server e estrailo:</p>
|
||||
<pre><code>mkdir -p /tmp/restore
|
||||
unzip backup.zip -d /tmp/restore/
|
||||
ls -la /tmp/restore/
|
||||
# Dovresti vedere: database.sql .env files/ manifest.json</code></pre>
|
||||
|
||||
<h5 class="mt-4">Passo 5: Crea database e importa</h5>
|
||||
<pre><code>sudo mysql -u root <<EOF
|
||||
CREATE DATABASE glastree CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
CREATE USER 'glastree'@'localhost' IDENTIFIED BY 'ScegliPasswordFortep4ss!';
|
||||
GRANT ALL ON glastree.* TO 'glastree'@'localhost';
|
||||
FLUSH PRIVILEGES;
|
||||
EOF
|
||||
|
||||
mysql -u glastree -p glastree < /tmp/restore/database.sql</code></pre>
|
||||
|
||||
<h5 class="mt-4">Passo 6: Configura .env</h5>
|
||||
<pre><code>cp /tmp/restore/.env .env
|
||||
# Modifica se necessario:
|
||||
# APP_URL=https://nuovo-dominio.it
|
||||
# DB_HOST=localhost
|
||||
# DB_DATABASE=glastree
|
||||
# DB_USERNAME=glastree
|
||||
# DB_PASSWORD=ScegliPasswordFortep4ss!
|
||||
|
||||
# DECIDI SULL'APP_KEY (leggi avviso sopra!)
|
||||
# php artisan key:generate ← SOLO se non hai il .env originale</code></pre>
|
||||
|
||||
<h5 class="mt-4">Passo 7: Ripristina file e permessi</h5>
|
||||
<pre><code>cp -r /tmp/restore/files/* storage/app/documenti/
|
||||
|
||||
sudo chown -R www-data:www-data storage bootstrap/cache
|
||||
sudo chmod -R 755 storage bootstrap/cache
|
||||
|
||||
php artisan storage:link</code></pre>
|
||||
|
||||
<h5 class="mt-4">Passo 8: Cache</h5>
|
||||
<pre><code>php artisan optimize:clear
|
||||
php artisan config:cache
|
||||
php artisan route:cache
|
||||
php artisan view:cache</code></pre>
|
||||
|
||||
<h5 class="mt-4">Passo 9: Configura Apache</h5>
|
||||
<pre><code>sudo tee /etc/apache2/sites-available/glastree.conf <<APACHE
|
||||
<VirtualHost *:80>
|
||||
ServerName glastree.esempio.it
|
||||
DocumentRoot /var/www/glastree/public
|
||||
|
||||
<Directory /var/www/glastree/public>
|
||||
Options -Indexes +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
ErrorLog \${APACHE_LOG_DIR}/glastree_error.log
|
||||
CustomLog \${APACHE_LOG_DIR}/glastree_access.log combined
|
||||
</VirtualHost>
|
||||
APACHE
|
||||
|
||||
sudo a2dissite 000-default.conf
|
||||
sudo a2ensite glastree.conf
|
||||
sudo systemctl reload apache2</code></pre>
|
||||
|
||||
<h5 class="mt-4">Passo 10: SSL Let's Encrypt</h5>
|
||||
<pre><code>sudo apt install -y certbot python3-certbot-apache
|
||||
sudo certbot --apache -d glastree.esempio.it</code></pre>
|
||||
|
||||
<h5 class="mt-4">Passo 11: Cron per backup automatico</h5>
|
||||
<pre><code>sudo crontab -u www-data -e
|
||||
# Inserisci:
|
||||
* * * * * cd /var/www/glastree && php artisan schedule:run >> /dev/null 2>&1</code></pre>
|
||||
|
||||
<h5 class="mt-4">Passo 12: Verifica finale</h5>
|
||||
<ol>
|
||||
<li>Apri https://glastree.esempio.it nel browser</li>
|
||||
<li>Fai login con le credenziali esistenti</li>
|
||||
<li>Verifica: Dashboard, Documenti, Impostazioni, Email</li>
|
||||
<li>Se hai rigenerato APP_KEY: riconfigura SMTP e OAuth Google Drive</li>
|
||||
</ol>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<h5>PHP Web Installer</h5>
|
||||
<p>L'applicazione include un <strong>wizard di installazione web</strong> nella cartella <code>installer/</code>. Questo wizard gestisce sia installazioni fresh che ripristino da backup.</p>
|
||||
<p>Vedi la sezione <strong>Installazione</strong> per la guida completa.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="tab-pane" id="help-docker">
|
||||
<div class="card card-outline card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fab fa-docker mr-2"></i> Docker</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5>Creare un'immagine Docker con dati di base</h5>
|
||||
<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>
|
||||
|
||||
<div class="callout callout-info">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<strong>Ottenere il codice:</strong> puoi clonare il repository con <code>git clone <URL> glastree</code> oppure, se non hai git, estrarre il file ZIP ricevuto con <code>unzip glastree.zip -d glastree</code> e posizionarti nella cartella <code>glastree</code> prima di procedere.
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="callout callout-info">
|
||||
<h6><i class="fas fa-database"></i> SQLite (Leggero)</h6>
|
||||
<p class="mb-0 small">Database embedded, nessun servizio aggiuntivo. Ideale per demo, test, sviluppo locale o ambienti con poca concorrenza.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="callout callout-success">
|
||||
<h6><i class="fas fa-server"></i> MySQL (Standard)</h6>
|
||||
<p class="mb-0 small">Database separato in un container MySQL. Configurazione standard, ideale per produzione, staging e ambienti con piu utenti.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="mt-4">1. Dockerfile (comune a entrambe le modalita)</h5>
|
||||
<p>Crea un file <code>Dockerfile</code> nella root del progetto:</p>
|
||||
<pre><code>FROM php:8.4-apache
|
||||
|
||||
# Abilita mod_rewrite
|
||||
RUN docker-php-ext-enable opcache \
|
||||
&& a2enmod rewrite
|
||||
|
||||
# Installa estensioni PHP (sia SQLite che MySQL)
|
||||
RUN docker-php-ext-install pdo_mysql pdo_sqlite bcmath gd zip
|
||||
|
||||
# Installa Composer
|
||||
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||
|
||||
# Configura DocumentRoot per Laravel
|
||||
ENV APACHE_DOCUMENT_ROOT=/var/www/html/public
|
||||
RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \
|
||||
&& sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf
|
||||
|
||||
# Copia l'applicazione
|
||||
COPY . /var/www/html/
|
||||
|
||||
# Installa dipendenze e compila asset
|
||||
RUN composer install --no-dev --optimize-autoloader \
|
||||
&& mkdir -p storage/app/public \
|
||||
&& mkdir -p storage/framework/cache/data \
|
||||
&& mkdir -p storage/framework/sessions \
|
||||
&& mkdir -p storage/framework/views \
|
||||
&& mkdir -p storage/logs \
|
||||
&& mkdir -p bootstrap/cache \
|
||||
&& chown -R www-data:www-data storage bootstrap/cache
|
||||
|
||||
# Entrypoint per inizializzazione
|
||||
COPY docker-entrypoint.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||
CMD ["apache2-foreground"]</code></pre>
|
||||
|
||||
<h5 class="mt-4">2. Entrypoint Script (comune)</h5>
|
||||
<p>Crea <code>docker-entrypoint.sh</code>. Lo script rileva automaticamente la configurazione in base alle variabili d'ambiente:</p>
|
||||
<pre><code>#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Se .env non esiste, lo genera dalle variabili d'ambiente
|
||||
if [ ! -f .env ]; then
|
||||
cp .env.example .env
|
||||
|
||||
if [ "$DB_CONNECTION" = "mysql" ]; then
|
||||
# Configura MySQL
|
||||
sed -i "s/DB_CONNECTION=mysql/DB_CONNECTION=mysql/" .env
|
||||
sed -i "s/DB_HOST=.*/DB_HOST=${DB_HOST:-mysql}/" .env
|
||||
sed -i "s/DB_PORT=.*/DB_PORT=${DB_PORT:-3306}/" .env
|
||||
sed -i "s/DB_DATABASE=.*/DB_DATABASE=${DB_DATABASE:-glastree}/" .env
|
||||
sed -i "s/DB_USERNAME=.*/DB_USERNAME=${DB_USERNAME:-glastree}/" .env
|
||||
sed -i "s/DB_PASSWORD=.*/DB_PASSWORD=${DB_PASSWORD:-secret}/" .env
|
||||
|
||||
# Attende che MySQL sia pronto
|
||||
echo "Attendo MySQL..."
|
||||
until php -r "new PDO('mysql:host=${DB_HOST:-mysql};dbname=${DB_DATABASE:-glastree}', '${DB_USERNAME:-glastree}', '${DB_PASSWORD:-secret}');" 2>/dev/null; do
|
||||
sleep 2
|
||||
done
|
||||
echo "MySQL pronto."
|
||||
else
|
||||
# Configura SQLite
|
||||
sed -i 's/DB_CONNECTION=mysql/DB_CONNECTION=sqlite/' .env
|
||||
sed -i 's/DB_HOST=.*//' .env
|
||||
sed -i 's/DB_PORT=.*//' .env
|
||||
sed -i 's/DB_DATABASE=.*/DB_DATABASE=\/var\/www\/html\/database\/database.sqlite/' .env
|
||||
sed -i 's/DB_USERNAME=.*//' .env
|
||||
sed -i 's/DB_PASSWORD=.*//' .env
|
||||
|
||||
touch database/database.sqlite
|
||||
chmod 664 database/database.sqlite
|
||||
fi
|
||||
|
||||
# Genera APP_KEY
|
||||
php artisan key:generate --force
|
||||
fi
|
||||
|
||||
# Esegui migration e seed se il database e vuoto
|
||||
if [ ! -f .db_initialized ]; then
|
||||
php artisan migrate --force
|
||||
php artisan db:seed --force
|
||||
touch .db_initialized
|
||||
fi
|
||||
|
||||
# Configura storage link
|
||||
php artisan storage:link --force 2>/dev/null || true
|
||||
|
||||
exec "$@"</code></pre>
|
||||
<pre><code>chmod +x docker-entrypoint.sh</code></pre>
|
||||
|
||||
<h5 class="mt-4">3. Seed Base Dati (comune)</h5>
|
||||
<p>Per includere tipologie documenti, tipologie eventi e ruoli, crea un seeder dedicato:</p>
|
||||
<pre><code>php artisan make:seeder DockerBaseDataSeeder</code></pre>
|
||||
<p>Contenuto di <code>database/seeders/DockerBaseDataSeeder.php</code>:</p>
|
||||
<pre><code><?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DockerBaseDataSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
// Tipologie Documenti
|
||||
$tipologieDoc = ['documento', 'avatar', 'galleria', 'statuto', 'programma', 'locandina', 'altro'];
|
||||
foreach ($tipologieDoc as $ord => $nome) {
|
||||
DB::table('tipologie_documenti')->updateOrInsert(
|
||||
['nome' => $nome],
|
||||
['descrizione' => ucfirst($nome), 'ordine' => $ord + 1, 'attiva' => true]
|
||||
);
|
||||
}
|
||||
|
||||
// Tipologie Eventi
|
||||
$tipologieEventi = [
|
||||
['nome' => 'catechesi', 'descrizione' => 'Catechesi'],
|
||||
['nome' => 'liturgia', 'descrizione' => 'Liturgia'],
|
||||
['nome' => 'animazione', 'descrizione' => 'Animazione'],
|
||||
['nome' => 'formazione', 'descrizione' => 'Formazione'],
|
||||
['nome' => 'incontro', 'descrizione' => 'Incontro'],
|
||||
['nome' => 'riunione', 'descrizione' => 'Riunione'],
|
||||
['nome' => 'festa', 'descrizione' => 'Festa'],
|
||||
['nome' => 'altro', 'descrizione' => 'Altro'],
|
||||
];
|
||||
foreach ($tipologieEventi as $ord => $t) {
|
||||
DB::table('tipologie_eventi')->updateOrInsert(
|
||||
['nome' => $t['nome']],
|
||||
['descrizione' => $t['descrizione'], 'ordine' => $ord + 1, 'attiva' => true]
|
||||
);
|
||||
}
|
||||
|
||||
// Ruoli
|
||||
$ruoli = ['presidente', 'vicepresidente', 'segretario', 'tesoriere', 'consigliere', 'volontario', 'socio', 'altro'];
|
||||
foreach ($ruoli as $ord => $nome) {
|
||||
DB::table('ruoli')->updateOrInsert(
|
||||
['nome' => $nome],
|
||||
['descrizione' => ucfirst($nome), 'ordine' => $ord + 1, 'attiva' => true]
|
||||
);
|
||||
}
|
||||
}
|
||||
}</code></pre>
|
||||
<p>Registralo in <code>database/seeders/DatabaseSeeder.php</code>:</p>
|
||||
<pre><code>$this->call([
|
||||
// ... altri seeder ...
|
||||
DockerBaseDataSeeder::class,
|
||||
]);</code></pre>
|
||||
|
||||
<h5 class="mt-4">4. Docker Compose</h5>
|
||||
|
||||
<h6 class="text-info"><i class="fas fa-database mr-1"></i> Opzione A — SQLite (leggero)</h6>
|
||||
<pre><code>services:
|
||||
glastree:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:80"
|
||||
volumes:
|
||||
- glastree_storage:/var/www/html/storage
|
||||
- glastree_db:/var/www/html/database
|
||||
environment:
|
||||
- APP_ENV=production
|
||||
- APP_DEBUG=false
|
||||
- APP_URL=http://localhost:8080
|
||||
- DB_CONNECTION=sqlite
|
||||
|
||||
volumes:
|
||||
glastree_storage:
|
||||
glastree_db:</code></pre>
|
||||
|
||||
<h6 class="mt-3 text-success"><i class="fas fa-server mr-1"></i> Opzione B — MySQL (standard)</h6>
|
||||
<pre><code>services:
|
||||
glastree:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:80"
|
||||
volumes:
|
||||
- glastree_storage:/var/www/html/storage
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- APP_ENV=production
|
||||
- APP_DEBUG=false
|
||||
- APP_URL=http://localhost:8080
|
||||
- DB_CONNECTION=mysql
|
||||
- DB_HOST=mysql
|
||||
- DB_PORT=3306
|
||||
- DB_DATABASE=glastree
|
||||
- DB_USERNAME=glastree
|
||||
- DB_PASSWORD=secret
|
||||
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
environment:
|
||||
- MYSQL_DATABASE=glastree
|
||||
- MYSQL_USER=glastree
|
||||
- MYSQL_PASSWORD=secret
|
||||
- MYSQL_ROOT_PASSWORD=root_secret
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
volumes:
|
||||
glastree_storage:
|
||||
mysql_data:</code></pre>
|
||||
|
||||
<h5 class="mt-4">5. Build e avvio</h5>
|
||||
<p>Assicurati di essere nella cartella del progetto (ottenuta via <code>git clone</code> o estraendo il ZIP), poi:</p>
|
||||
<pre><code># Costruisci l'immagine (comune)
|
||||
docker build -t glastree:latest .
|
||||
|
||||
# Avvia con SQLite
|
||||
docker compose -f docker-compose.yml up -d
|
||||
|
||||
# Oppure avvia con MySQL
|
||||
# docker compose -f docker-compose.mysql.yml up -d
|
||||
|
||||
# Apri nel browser
|
||||
open http://localhost:8080</code></pre>
|
||||
|
||||
<h5 class="mt-4">6. Primo accesso</h5>
|
||||
<p>Al primo avvio, l'entrypoint esegue automaticamente migration e seed. Troverai gia popolati:</p>
|
||||
<ul>
|
||||
<li><strong>Tipologie Documenti:</strong> documento, avatar, galleria, statuto, programma, locandina, altro</li>
|
||||
<li><strong>Tipologie Eventi:</strong> catechesi, liturgia, animazione, formazione, incontro, riunione, festa, altro</li>
|
||||
<li><strong>Ruoli:</strong> presidente, vicepresidente, segretario, tesoriere, consigliere, volontario, socio, altro</li>
|
||||
</ul>
|
||||
<p>Dovrai creare il primo utente amministratore via CLI:</p>
|
||||
<pre><code>docker exec -it glastree php artisan tinker --execute="
|
||||
\$u = new \App\Models\User;
|
||||
\$u->name = 'Amministratore';
|
||||
\$u->email = 'admin@esempio.it';
|
||||
\$u->password = bcrypt('password-forte');
|
||||
\$u->is_admin = true;
|
||||
\$u->permissions = ['individui' => 2, 'gruppi' => 2, 'eventi' => 2, 'documenti' => 2, 'mailing' => 2, 'viste' => 2, 'report' => 2, 'settings' => 2];
|
||||
\$u->save();
|
||||
echo 'Utente creato con successo!';
|
||||
"</code></pre>
|
||||
|
||||
<div class="callout callout-warning mt-4">
|
||||
<h6><i class="fas fa-exclamation-triangle"></i> Note importanti</h6>
|
||||
<ul class="mb-0">
|
||||
<li><strong>Persistenza:</strong> i volumi Docker preservano storage e database tra i riavvii</li>
|
||||
<li><strong>APP_KEY:</strong> viene generata al primo avvio — se elimini i volumi, la perdi e i dati criptati diventano inaccessibili</li>
|
||||
<li><strong>MySQL in produzione:</strong> imposta password sicure per <code>MYSQL_PASSWORD</code> e <code>MYSQL_ROOT_PASSWORD</code></li>
|
||||
<li><strong>File compose separati:</strong> puoi mantenere entrambi i file (<code>docker-compose.yml</code> per SQLite, <code>docker-compose.mysql.yml</code> per MySQL) e scegliere con <code>-f</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="tab-pane" id="help-installazione">
|
||||
<div class="card card-outline card-dark">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-terminal mr-2"></i> Guida all'Installazione</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5>Panoramica</h5>
|
||||
<p>l'applicazione include un <strong>wizard di installazione web</strong> che guida passo-passo nella configurazione su un nuovo server LAMP. Supporta due modalita:</p>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="callout callout-info">
|
||||
<h6><i class="fas fa-rocket"></i> Fresh Install</h6>
|
||||
<p class="mb-0">Installa l'applicazione da zero su un nuovo server: crea il database, esegue le migration, popola i dati di base e crea l'amministratore.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="callout callout-success">
|
||||
<h6><i class="fas fa-upload"></i> Restore da Backup</h6>
|
||||
<p class="mb-0">Ripristina un'installazione esistente da un file ZIP di backup: importa il database, ripristina i documenti e la configurazione.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="mt-4">Requisiti del Server</h5>
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead><tr><th>Componente</th><th>Versione Minima</th><th>Note</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Web Server</td><td>Apache 2.4+</td><td>Con mod_rewrite abilitato</td></tr>
|
||||
<tr><td>PHP</td><td>8.2+ (consigliato 8.4)</td><td>Estensioni: pdo_mysql, mbstring, xml, curl, zip, gd, fileinfo</td></tr>
|
||||
<tr><td>MySQL / MariaDB</td><td>MySQL 8.0+ / MariaDB 10.5+</td><td>Supporto utf8mb4</td></tr>
|
||||
<tr><td>Composer</td><td>2.x</td><td>Per gestione dipendenze PHP</td></tr>
|
||||
<tr><td>Node.js / npm</td><td>Node 20+ / npm 10+</td><td>Per compilazione asset frontend (opzionale se precompilati)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h5 class="mt-4">Installazione Passo-Passo</h5>
|
||||
|
||||
<h6>Passo 1: Preparare il server LAMP</h6>
|
||||
<pre><code>sudo apt update && sudo apt upgrade -y
|
||||
|
||||
sudo apt install -y apache2 mariadb-server php8.4 php8.4-cli \
|
||||
php8.4-mysql php8.4-xml php8.4-mbstring php8.4-curl \
|
||||
php8.4-zip php8.4-bcmath php8.4-gd php8.4-intl \
|
||||
composer git unzip
|
||||
|
||||
sudo a2enmod rewrite
|
||||
sudo systemctl restart apache2</code></pre>
|
||||
|
||||
<h6>Passo 2: Ottenere l'applicazione</h6>
|
||||
<div class="callout callout-info py-2">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Puoi clonare il repository con git oppure estrarre il file ZIP ricevuto nella cartella <code>/var/www/glastree</code>.
|
||||
</div>
|
||||
<pre><code>cd /var/www
|
||||
|
||||
# Opzione A — Git clone
|
||||
git clone <URL_REPOSITORY> glastree
|
||||
|
||||
# Opzione B — ZIP (se non hai git)
|
||||
# unzip /percorso/del/glastree.zip -d glastree
|
||||
|
||||
cd glastree
|
||||
composer install --no-dev --optimize-autoloader
|
||||
npm ci && npm run build</code></pre>
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Se non hai accesso a npm, puoi saltare il build. L'interfaccia usera comunque AdminLTE via CDN.
|
||||
</div>
|
||||
|
||||
<h6>Passo 3: Permessi cartelle</h6>
|
||||
<pre><code>sudo chown -R www-data:www-data storage bootstrap/cache
|
||||
sudo chmod -R 775 storage bootstrap/cache</code></pre>
|
||||
|
||||
<h6>Passo 4: Configurare Apache</h6>
|
||||
<pre><code>sudo tee /etc/apache2/sites-available/glastree.conf <<APACHE
|
||||
<VirtualHost *:80>
|
||||
ServerName glastree.esempio.it
|
||||
DocumentRoot /var/www/glastree/public
|
||||
|
||||
<Directory /var/www/glastree/public>
|
||||
Options -Indexes +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
ErrorLog \${APACHE_LOG_DIR}/glastree_error.log
|
||||
CustomLog \${APACHE_LOG_DIR}/glastree_access.log combined
|
||||
</VirtualHost>
|
||||
APACHE
|
||||
|
||||
sudo a2dissite 000-default.conf
|
||||
sudo a2ensite glastree.conf
|
||||
sudo systemctl reload apache2</code></pre>
|
||||
|
||||
<h6>Passo 5: Avviare il wizard di installazione</h6>
|
||||
<p>Apri il browser e visita l'indirizzo del tuo server:</p>
|
||||
<pre><code>http://glastree.esempio.it/installer/</code></pre>
|
||||
|
||||
<p>Il wizard ti guidera attraverso 6 passi:</p>
|
||||
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead><tr><th>Passo</th><th>Descrizione</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>1. Benvenuto</td><td>Scegli Fresh Install o Restore da Backup</td></tr>
|
||||
<tr><td>2. Requisiti</td><td>Verifica PHP, estensioni e permessi</td></tr>
|
||||
<tr><td>3. Database</td><td>Configura MySQL (crea nuovo o usa esistente)</td></tr>
|
||||
<tr><td>4. Installazione</td><td>Fresh: migration e seed. Backup: upload ZIP</td></tr>
|
||||
<tr><td>5. Amministratore</td><td>Fresh: crea super-admin. Backup: verifica utenti</td></tr>
|
||||
<tr><td>6. Finalizzazione</td><td>Cache, storage link, auto-eliminazione installer</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<strong>Attenzione:</strong> Al termine dell'installazione, la cartella <code>installer/</code> viene <strong>automaticamente eliminata</strong>. Non puoi rieseguire l'installazione se non ricreando manualmente la cartella.
|
||||
</div>
|
||||
|
||||
<h5 class="mt-4">Modalita Fresh Install</h5>
|
||||
<p>Seleziona questa modalita quando installi l'applicazione per la prima volta su un server.</p>
|
||||
<p>Cosa succede durante l'installazione:</p>
|
||||
<ol>
|
||||
<li>Genera <code>APP_KEY</code> per la crittografia</li>
|
||||
<li>Esegue tutte le migration per creare le tabelle del database</li>
|
||||
<li>Popola i dati di base (diocesi, comuni, ruoli, preset)</li>
|
||||
<li>Crea il collegamento <code>storage → public/storage</code></li>
|
||||
<li>Crea l'utente amministratore con i permessi completi</li>
|
||||
</ol>
|
||||
|
||||
<h5 class="mt-4">Modalita Restore da Backup</h5>
|
||||
<p>Seleziona questa modalita quando vuoi migrare un'installazione esistente su un nuovo server.</p>
|
||||
<p>Cosa serve:</p>
|
||||
<ul>
|
||||
<li>Un file ZIP generato dalla pagina <strong>Admin → Backup</strong> del server originale</li>
|
||||
<li>Il wizard accetta upload diretto del file o un percorso sul server</li>
|
||||
</ul>
|
||||
<p>Cosa succede durante il restore:</p>
|
||||
<ol>
|
||||
<li>Estrae il file ZIP in una cartella temporanea</li>
|
||||
<li>Importa <code>database.sql</code> nel database MySQL configurato</li>
|
||||
<li>Copia i file documenti in <code>storage/app/documenti/</code></li>
|
||||
<li>Ripristina il file <code>.env</code> dal backup (preservando le credenziali DB del nuovo server)</li>
|
||||
</ol>
|
||||
|
||||
<h5 class="mt-4">Dopo l'Installazione</h5>
|
||||
<ol>
|
||||
<li><strong>Configura SSL</strong> con Let's Encrypt:
|
||||
<pre><code>sudo apt install -y certbot python3-certbot-apache
|
||||
sudo certbot --apache -d glastree.esempio.it</code></pre>
|
||||
</li>
|
||||
<li><strong>Accedi</strong> con le credenziali amministratore create durante l'installazione</li>
|
||||
<li><strong>Configura l'applicazione</strong> in Impostazioni:
|
||||
<ul>
|
||||
<li>Nome applicazione e logo</li>
|
||||
<li>Email (SMTP per invio, IMAP per ricezione)</li>
|
||||
<li>Repository remoti (Google Drive, Nextcloud)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>Crea utenti</strong> dal pannello Admin → Utenti</li>
|
||||
<li><strong>Configura il cron</strong> per backup automatico e sync email:
|
||||
<pre><code>sudo crontab -u www-data -e
|
||||
# Inserisci:
|
||||
* * * * * cd /var/www/glastree && php artisan schedule:run >> /dev/null 2>&1</code></pre>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h5 class="mt-4">Risoluzione Problemi</h5>
|
||||
<div class="callout callout-danger">
|
||||
<h6>Errore "500 | Server Error" dopo l'installazione</h6>
|
||||
<p>Probabilmente mancano permessi sulle cartelle <code>storage</code> o <code>bootstrap/cache</code>:</p>
|
||||
<pre><code>sudo chown -R www-data:www-data storage bootstrap/cache
|
||||
sudo chmod -R 775 storage bootstrap/cache</code></pre>
|
||||
</div>
|
||||
<div class="callout callout-danger">
|
||||
<h6>Errore "No application encryption key"</h6>
|
||||
<p>APP_KEY non generata. Nella cartella dell'app:</p>
|
||||
<pre><code>php artisan key:generate</code></pre>
|
||||
</div>
|
||||
<div class="callout callout-warning">
|
||||
<h6>mod_rewrite non funziona</h6>
|
||||
<p>Se le rotte restituiscono 404, verifica che Apache abbia AllowOverride:</p>
|
||||
<pre><code>sudo a2enmod rewrite
|
||||
# Nel VirtualHost: AllowOverride All
|
||||
sudo systemctl reload apache2</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.nav-pills .nav-link { border-radius: 0; }
|
||||
.nav-pills .nav-link.active { background-color: #007bff; }
|
||||
.callout { border-left: 3px solid; padding: 12px 16px; margin: 16px 0; background: #f8f9fa; }
|
||||
.callout-info { border-color: #17a2b8; }
|
||||
.callout-danger { border-color: #dc3545; }
|
||||
.callout-warning { border-color: #ffc107; }
|
||||
</style>
|
||||
<?php $__env->stopSection(); ?>
|
||||
|
||||
<?php echo $__env->make('layouts.adminlte', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH /var/www/html/glastree/resources/views/help/index.blade.php ENDPATH**/ ?>
|
||||
@@ -1,165 +0,0 @@
|
||||
<?php $__env->startSection('title', 'Dashboard'); ?>
|
||||
<?php $__env->startSection('page_title', 'Dashboard'); ?>
|
||||
|
||||
<?php $__env->startSection('content'); ?>
|
||||
<div class="row">
|
||||
<div class="col-lg-3 col-6">
|
||||
<div class="small-box bg-info">
|
||||
<div class="inner">
|
||||
<h3><?php echo e($stats['individui']); ?></h3>
|
||||
<p>Individui</p>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<i class="fas fa-users"></i>
|
||||
</div>
|
||||
<a href="<?php echo e(route('individui.index')); ?>" class="small-box-footer">Visualizza <i class="fas fa-arrow-circle-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-6">
|
||||
<div class="small-box bg-success">
|
||||
<div class="inner">
|
||||
<h3><?php echo e($stats['gruppi']); ?></h3>
|
||||
<p>Gruppi</p>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<i class="fas fa-folder"></i>
|
||||
</div>
|
||||
<a href="<?php echo e(route('gruppi.index')); ?>" class="small-box-footer">Visualizza <i class="fas fa-arrow-circle-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-6">
|
||||
<div class="small-box bg-primary">
|
||||
<div class="inner">
|
||||
<h3><?php echo e($stats['documenti']); ?></h3>
|
||||
<p>Documenti</p>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<i class="fas fa-file"></i>
|
||||
</div>
|
||||
<a href="/documenti" class="small-box-footer">Visualizza <i class="fas fa-arrow-circle-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-6">
|
||||
<div class="small-box bg-danger">
|
||||
<div class="inner">
|
||||
<h3><?php echo e($stats['eventi']); ?></h3>
|
||||
<p>Eventi</p>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<i class="fas fa-calendar"></i>
|
||||
</div>
|
||||
<a href="/eventi" class="small-box-footer">Visualizza <i class="fas fa-arrow-circle-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-3 col-6">
|
||||
<div class="small-box bg-warning">
|
||||
<div class="inner">
|
||||
<h3><?php echo e($stats['notifiche']); ?></h3>
|
||||
<p>Notifiche</p>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<i class="fas fa-bell"></i>
|
||||
</div>
|
||||
<a href="#" class="small-box-footer">Visualizza <i class="fas fa-arrow-circle-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-6">
|
||||
<div class="small-box bg-secondary">
|
||||
<div class="inner">
|
||||
<h3><?php echo e($stats['email_nuove']); ?></h3>
|
||||
<p>Nuove Email</p>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</div>
|
||||
<a href="/email" class="small-box-footer">Visualizza <i class="fas fa-arrow-circle-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-6">
|
||||
<div class="small-box bg-dark">
|
||||
<div class="inner">
|
||||
<h3><?php echo e($stats['mailing_liste']); ?></h3>
|
||||
<p>Mailing List</p>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<i class="fas fa-list-alt"></i>
|
||||
</div>
|
||||
<a href="/mailing-liste" class="small-box-footer">Visualizza <i class="fas fa-arrow-circle-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<?php if(Auth::user()->canManage('settings')): ?>
|
||||
<div class="col-lg-3 col-6">
|
||||
<div class="small-box bg-navy">
|
||||
<div class="inner">
|
||||
<h3><?php echo e($stats['backups']); ?></h3>
|
||||
<p>Backup</p>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<i class="fas fa-hdd"></i>
|
||||
</div>
|
||||
<a href="<?php echo e(route('admin.backup.index')); ?>" class="small-box-footer">Visualizza <i class="fas fa-arrow-circle-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-bell"></i> Ultime Notifiche</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if($notifiche->count() > 0): ?>
|
||||
<ul class="todo-list" data-widget="todo-list">
|
||||
<?php $__currentLoopData = $notifiche; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $notifica): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<li>
|
||||
<span class="text"><?php echo e($notifica->titolo); ?></span>
|
||||
<small class="badge badge-info"><i class="far fa-clock"></i> <?php echo e($notifica->created_at->diffForHumans()); ?></small>
|
||||
</li>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</ul>
|
||||
<?php else: ?>
|
||||
<p class="text-muted">Nessuna notifica</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-info-circle"></i> Benvenuto</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php
|
||||
$appName = \App\Models\AppSetting::getAppName() ?? 'Glastree';
|
||||
$orgName = \App\Models\AppSetting::getOrgName();
|
||||
$welcome = \App\Models\AppSetting::getDashboardWelcome();
|
||||
?>
|
||||
<?php if($welcome): ?>
|
||||
<p><?php echo nl2br(e($welcome)); ?></p>
|
||||
<?php else: ?>
|
||||
<p>Benvenuto in <strong><?php echo e(e($appName)); ?></strong><?php echo e($orgName ? ', ' . e($orgName) : ''); ?>.</p>
|
||||
<?php endif; ?>
|
||||
<p>Dal menu laterale puoi navigare tra le sezioni:</p>
|
||||
<ul>
|
||||
<li><strong>Individui</strong>: Gestione delle persone registrate</li>
|
||||
<li><strong>Gruppi</strong>: Gestione della struttura organizzativa</li>
|
||||
<li><strong>Documenti</strong>: Archivio documentale</li>
|
||||
<li><strong>Eventi</strong>: Calendario eventi</li>
|
||||
<li><strong>Mailing</strong>: Comunicazioni email</li>
|
||||
</ul>
|
||||
<?php if(Auth::user()->is_admin): ?>
|
||||
<div class="alert alert-success mt-3">
|
||||
<i class="fas fa-crown"></i> <strong>Amministratore</strong>: Hai accesso completo al sistema.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php $__env->stopSection(); ?>
|
||||
<?php echo $__env->make('layouts.adminlte', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH /var/www/html/glastree/resources/views/home/dashboard.blade.php ENDPATH**/ ?>
|
||||
@@ -1,87 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Anteprima non disponibile</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
background-color: #f4f6f9;
|
||||
color: #333;
|
||||
}
|
||||
.fallback-container {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
max-width: 400px;
|
||||
}
|
||||
.fallback-icon {
|
||||
font-size: 64px;
|
||||
color: #6c757d;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.fallback-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.fallback-message {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.fallback-file-info {
|
||||
background: #f8f9fa;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 24px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.fallback-file-info strong {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.download-btn {
|
||||
display: inline-block;
|
||||
padding: 10px 24px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.download-btn:hover {
|
||||
background-color: #0056b3;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="fallback-container">
|
||||
<div class="fallback-icon">📄</div>
|
||||
<div class="fallback-title">Anteprima non disponibile</div>
|
||||
<div class="fallback-message">
|
||||
Questo tipo di file non può essere visualizzato nell'anteprima.
|
||||
Scarica il file per visualizzarlo con l'applicazione appropriata.
|
||||
</div>
|
||||
<div class="fallback-file-info">
|
||||
<strong><?php echo e($documento->nome_file); ?></strong>
|
||||
<?php echo e(strtoupper(pathinfo($documento->file_path, PATHINFO_EXTENSION))); ?> ·
|
||||
<?php echo e(number_format($documento->dimensione / 1024, 1)); ?> KB
|
||||
</div>
|
||||
<a href="/documenti/<?php echo e($documento->id); ?>/download" class="download-btn">
|
||||
⬇️ Scarica File
|
||||
</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<?php /**PATH /var/www/html/glastree/resources/views/documenti/preview-fallback.blade.php ENDPATH**/ ?>
|
||||
@@ -1,922 +0,0 @@
|
||||
<?php $__env->startSection('title', 'Individui'); ?>
|
||||
<?php $__env->startSection('page_title', 'Elenco Individui'); ?>
|
||||
|
||||
<?php
|
||||
$columnLabels = [
|
||||
'codice' => 'Codice',
|
||||
'cognome' => 'Cognome',
|
||||
'nome' => 'Nome',
|
||||
'email' => 'Email',
|
||||
'telefono' => 'Telefono',
|
||||
];
|
||||
$vistaDefaultJson = $vista ? $vista->toJson() : 'null';
|
||||
$allColumnsJson = json_encode($allColumns);
|
||||
$visibleColumnsJson = json_encode($visibleColumns);
|
||||
$canWriteIndividui = Auth::user()->canManage('individui');
|
||||
$canDeleteIndividui = Auth::user()->canDelete('individui');
|
||||
?>
|
||||
|
||||
<?php $__env->startSection('content'); ?>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-users mr-2"></i>Lista Individui</h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-info btn-sm mr-1" onclick="toggleSearchPanel()">
|
||||
<i class="fas fa-search mr-1"></i> Ricerca/Filtri
|
||||
</button>
|
||||
<div class="btn-group mr-1">
|
||||
<button type="button" class="btn btn-secondary btn-sm dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fas fa-cog mr-1"></i> Azioni
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#" onclick="exportCSV(); return false;">
|
||||
<i class="fas fa-file-csv mr-2"></i>Esporta Tutti CSV
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" onclick="exportSelectedCSV(); return false;">
|
||||
<i class="fas fa-file-csv mr-2"></i>Esporta Selezionati CSV
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="#" onclick="printSelected(); return false;">
|
||||
<i class="fas fa-print mr-2"></i>Stampa Selezionati
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" onclick="emailSelected(); return false;">
|
||||
<i class="fas fa-envelope mr-2"></i>Email Selezionati
|
||||
</a>
|
||||
<?php if($canWriteIndividui): ?>
|
||||
<a class="dropdown-item" href="#" onclick="showCreateMailingListModal(); return false;">
|
||||
<i class="fas fa-list mr-2"></i>Crea Mailing List
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger" href="#" onclick="deleteSelected(); return false;">
|
||||
<i class="fas fa-trash mr-2"></i>Elimina Selezionati
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="showSaveVistaModal()">
|
||||
<i class="fas fa-save mr-1"></i> Salva Vista
|
||||
</button>
|
||||
<?php if($canWriteIndividui): ?>
|
||||
<a href="<?php echo e(route('individui.import')); ?>" class="btn btn-warning btn-sm ml-2">
|
||||
<i class="fas fa-file-import mr-1"></i> Importa
|
||||
</a>
|
||||
<a href="/individui/create" class="btn btn-success btn-sm ml-2">
|
||||
<i class="fas fa-plus mr-1"></i> Nuovo
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="search-panel" class="p-3 bg-light border-bottom" style="display: none;">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="form-group mb-2">
|
||||
<label class="small font-weight-bold">Cerca</label>
|
||||
<input type="text" id="global-search" class="form-control form-control-sm" placeholder="Cerca in tutte le colonne..." oninput="applyGlobalSearch(this.value)">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group mb-2">
|
||||
<label class="small font-weight-bold">Colonna</label>
|
||||
<select id="filter-column" class="form-control form-control-sm">
|
||||
<option value="">Tutte le colonne</option>
|
||||
<?php $__currentLoopData = $allColumns; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $col): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<option value="<?php echo e($col['key']); ?>"><?php echo e($col['label']); ?></option>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="form-group mb-2">
|
||||
<label class="small font-weight-bold">Operatore</label>
|
||||
<select id="filter-operator" class="form-control form-control-sm">
|
||||
<option value="contains">Contiene</option>
|
||||
<option value="equals">Uguale a</option>
|
||||
<option value="starts">Inizia con</option>
|
||||
<option value="ends">Finisce con</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="form-group mb-2">
|
||||
<label class="small font-weight-bold">Valore</label>
|
||||
<input type="text" id="filter-value" class="form-control form-control-sm" placeholder="Valore filtro">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<label class="small"> </label>
|
||||
<div>
|
||||
<button type="button" class="btn btn-xs btn-primary" onclick="applyColumnFilter()">
|
||||
<i class="fas fa-filter"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-xs btn-secondary" onclick="clearFilters()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-info-circle mr-1"></i>
|
||||
Ordina cliccando sulle intestazioni delle colonne
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-0">
|
||||
<?php if($individui->count() > 0): ?>
|
||||
<table class="table table-bordered table-hover table-striped mb-0" id="individui-table">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th style="width: 40px;">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="select-all" onchange="toggleSelectAll(this)">
|
||||
<label class="custom-control-label" for="select-all"></label>
|
||||
</div>
|
||||
</th>
|
||||
<?php if(in_array('codice', $visibleColumns)): ?>
|
||||
<th style="width: 100px;" data-sortable="true" data-column="codice" onclick="sortTable('codice')">Codice <i class="fas fa-sort float-right"></i></th>
|
||||
<?php endif; ?>
|
||||
<?php if(in_array('cognome', $visibleColumns)): ?>
|
||||
<th data-sortable="true" data-column="cognome" onclick="sortTable('cognome')">Cognome <i class="fas fa-sort float-right"></i></th>
|
||||
<?php endif; ?>
|
||||
<?php if(in_array('nome', $visibleColumns)): ?>
|
||||
<th data-sortable="true" data-column="nome" onclick="sortTable('nome')">Nome <i class="fas fa-sort float-right"></i></th>
|
||||
<?php endif; ?>
|
||||
<?php if(in_array('email', $visibleColumns)): ?>
|
||||
<th data-sortable="true" data-column="email" onclick="sortTable('email')">Email <i class="fas fa-sort float-right"></i></th>
|
||||
<?php endif; ?>
|
||||
<?php if(in_array('telefono', $visibleColumns)): ?>
|
||||
<th data-sortable="true" data-column="telefono" onclick="sortTable('telefono')">Telefono <i class="fas fa-sort float-right"></i></th>
|
||||
<?php endif; ?>
|
||||
<th style="width: 120px;">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="table-body">
|
||||
<?php $__currentLoopData = $individui; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $ind): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<tr data-id="<?php echo e($ind->id); ?>">
|
||||
<td>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input row-checkbox" id="select-<?php echo e($ind->id); ?>" value="<?php echo e($ind->id); ?>">
|
||||
<label class="custom-control-label" for="select-<?php echo e($ind->id); ?>"></label>
|
||||
</div>
|
||||
</td>
|
||||
<?php if(in_array('codice', $visibleColumns)): ?>
|
||||
<td><span class="badge badge-secondary"><?php echo e($ind->codice_id); ?></span></td>
|
||||
<?php endif; ?>
|
||||
<?php if(in_array('cognome', $visibleColumns)): ?>
|
||||
<td class="font-weight-bold">
|
||||
<a href="<?php echo e(url('/individui/' . $ind->id . '/edit')); ?>"><?php echo e($ind->cognome); ?></a>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
<?php if(in_array('nome', $visibleColumns)): ?>
|
||||
<td>
|
||||
<a href="<?php echo e(url('/individui/' . $ind->id . '/edit')); ?>"><?php echo e($ind->nome); ?></a>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
<?php if(in_array('email', $visibleColumns)): ?>
|
||||
<td><?php echo e($ind->getEmailPrimariaAttribute() ?: '-'); ?></td>
|
||||
<?php endif; ?>
|
||||
<?php if(in_array('telefono', $visibleColumns)): ?>
|
||||
<td><?php echo e($ind->getTelefonoPrimarioAttribute() ?: '-'); ?></td>
|
||||
<?php endif; ?>
|
||||
<td>
|
||||
<?php if($canWriteIndividui): ?>
|
||||
<a href="<?php echo e(url('/individui/' . $ind->id . '/edit')); ?>" class="btn btn-xs btn-warning" title="Modifica">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if($canDeleteIndividui): ?>
|
||||
<button type="button" class="btn btn-xs btn-danger" onclick="showDeleteModal(<?php echo e($ind->id); ?>, '<?php echo e($ind->cognome); ?> <?php echo e($ind->nome); ?>')" title="Elimina">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<button type="button" class="btn btn-xs btn-secondary" onclick="printIndividuo(<?php echo e($ind->id); ?>)" title="Stampa">
|
||||
<i class="fas fa-print"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-xs btn-primary" onclick="exportIndividuoCSV(<?php echo e($ind->id); ?>)" title="Esporta CSV">
|
||||
<i class="fas fa-download"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-xs btn-info" onclick="sendEmail(<?php echo e($ind->id); ?>)" title="Invia Email">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-xs btn-dark" onclick="generateReport(<?php echo e($ind->id); ?>)" title="Genera Report">
|
||||
<i class="fas fa-chart-bar"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php else: ?>
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="fas fa-users fa-3x mb-3"></i>
|
||||
<p>Nessun individuo presente</p>
|
||||
<?php if($canWriteIndividui): ?>
|
||||
<a href="/individui/create" class="btn btn-success">Crea il primo individuo</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if($individui->count() > 0): ?>
|
||||
<div class="card-footer">
|
||||
<?php echo e($individui->links()); ?>
|
||||
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="saveVistaModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="fas fa-save mr-2"></i>Salva Vista</h5>
|
||||
<button type="button" class="close" data-dismiss="modal">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>Nome Vista *</label>
|
||||
<input type="text" id="vista-nome" class="form-control" placeholder="Es. Elenco soci attivi">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Colonne visibili</label>
|
||||
<div class="row" id="colonne-checkboxes">
|
||||
<?php $__currentLoopData = $allColumns; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $col): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<div class="col-md-6">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input col-visibile" id="col-vis-<?php echo e($col['key']); ?>" value="<?php echo e($col['key']); ?>" <?php echo e(in_array($col['key'], $visibleColumns) ? 'checked' : ''); ?>>
|
||||
<label class="custom-control-label" for="col-vis-<?php echo e($col['key']); ?>"><?php echo e($col['label']); ?></label>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="vista-default" value="true">
|
||||
<label class="custom-control-label" for="vista-default">Imposta come vista predefinita</label>
|
||||
</div>
|
||||
</div>
|
||||
</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="saveVista()">Salva</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="colonneModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="fas fa-columns mr-2"></i>Colonne Visibili</h5>
|
||||
<button type="button" class="close" data-dismiss="modal">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<?php $__currentLoopData = $allColumns; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $col): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<div class="col-md-6">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input col-toggle" id="col-toggle-<?php echo e($col['key']); ?>" value="<?php echo e($col['key']); ?>" <?php echo e(in_array($col['key'], $visibleColumns) ? 'checked' : ''); ?> onchange="toggleColumn('<?php echo e($col['key']); ?>', this.checked)">
|
||||
<label class="custom-control-label" for="col-toggle-<?php echo e($col['key']); ?>"><?php echo e($col['label']); ?></label>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Chiudi</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog">
|
||||
<?php if($canDeleteIndividui): ?>
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-danger">
|
||||
<h5 class="modal-title text-white"><i class="fas fa-trash-alt mr-2"></i>Conferma Eliminazione</h5>
|
||||
<button type="button" class="close text-white" data-dismiss="modal">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Stai per eliminare l'individuo: <strong id="delete-individuo-name"></strong></p>
|
||||
<p class="text-muted">Seleziona gli elementi collegati da eliminare:</p>
|
||||
<div class="ml-3">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="delete-contatti" checked>
|
||||
<label class="custom-control-label" for="delete-contatti">Contatti (<span id="count-contatti">0</span>)</label>
|
||||
</div>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="delete-gruppi">
|
||||
<label class="custom-control-label" for="delete-gruppi">Gruppi (<span id="count-gruppi">0</span>) - solo scollegamento</label>
|
||||
</div>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="delete-documenti" checked>
|
||||
<label class="custom-control-label" for="delete-documenti">Documenti (<span id="count-documenti">0</span>)</label>
|
||||
</div>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="delete-eventi">
|
||||
<label class="custom-control-label" for="delete-eventi">Eventi (<span id="count-eventi">0</span>) - solo scollegamento</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-warning mt-3 mb-0">
|
||||
<small><i class="fas fa-info-circle mr-1"></i>Se non selezionato, l'elemento verrà scollegato ma non eliminato.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annulla</button>
|
||||
<button type="button" class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete()">Elimina</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="createMailingListModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-info">
|
||||
<h5 class="modal-title text-white"><i class="fas fa-list mr-2"></i>Crea Mailing List</h5>
|
||||
<button type="button" class="close text-white" data-dismiss="modal">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>Nome Lista *</label>
|
||||
<input type="text" id="ml-nome" class="form-control" placeholder="Es. Newsletter Maggio 2026">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Descrizione</label>
|
||||
<textarea id="ml-descrizione" class="form-control" rows="2" placeholder="Descrizione opzionale"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Contatti inclusi (<span id="ml-count">0</span>)</label>
|
||||
<div class="table-responsive" style="max-height: 300px; overflow-y: auto;">
|
||||
<table class="table table-bordered table-sm" id="ml-contatti-table">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th style="width: 40px;">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="ml-select-all" onchange="toggleMlSelectAll(this)">
|
||||
<label class="custom-control-label" for="ml-select-all"></label>
|
||||
</div>
|
||||
</th>
|
||||
<th>Codice</th>
|
||||
<th>Cognome</th>
|
||||
<th>Nome</th>
|
||||
<th>Email</th>
|
||||
<th style="width: 50px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ml-contatti-tbody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annulla</button>
|
||||
<button type="button" class="btn btn-success" onclick="createMailingList()">
|
||||
<i class="fas fa-save mr-1"></i> Salva Lista
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php $__env->stopSection(); ?>
|
||||
|
||||
<?php $__env->startSection('scripts'); ?>
|
||||
<script>
|
||||
let currentSort = { column: null, direction: 'asc' };
|
||||
let columnVisibility = <?php echo json_encode(array_column($allColumns, 'key')); ?>;
|
||||
|
||||
function toggleSearchPanel() {
|
||||
const panel = document.getElementById('search-panel');
|
||||
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function applyGlobalSearch(value) {
|
||||
const rows = document.querySelectorAll('#table-body tr');
|
||||
const searchTerm = value.toLowerCase();
|
||||
rows.forEach(row => {
|
||||
const text = row.textContent.toLowerCase();
|
||||
row.style.display = text.includes(searchTerm) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function applyColumnFilter() {
|
||||
const col = document.getElementById('filter-column').value;
|
||||
const op = document.getElementById('filter-operator').value;
|
||||
const val = document.getElementById('filter-value').value.toLowerCase();
|
||||
|
||||
if (!col || !val) return;
|
||||
|
||||
const rows = document.querySelectorAll('#table-body tr');
|
||||
const colIndex = { codice: 1, cognome: 2, nome: 3, email: 4, telefono: 5, azioni: 6 }[col];
|
||||
|
||||
rows.forEach(row => {
|
||||
const cells = row.querySelectorAll('td');
|
||||
const cellText = cells[colIndex]?.textContent.toLowerCase() || '';
|
||||
let matches = false;
|
||||
|
||||
switch(op) {
|
||||
case 'contains': matches = cellText.includes(val); break;
|
||||
case 'equals': matches = cellText === val; break;
|
||||
case 'starts': matches = cellText.startsWith(val); break;
|
||||
case 'ends': matches = cellText.endsWith(val); break;
|
||||
}
|
||||
|
||||
row.style.display = matches ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
document.getElementById('global-search').value = '';
|
||||
document.getElementById('filter-column').value = '';
|
||||
document.getElementById('filter-value').value = '';
|
||||
document.querySelectorAll('#table-body tr').forEach(row => row.style.display = '');
|
||||
}
|
||||
|
||||
function sortTable(column) {
|
||||
if (currentSort.column === column) {
|
||||
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
currentSort = { column: column, direction: 'asc' };
|
||||
}
|
||||
|
||||
const colIndex = { codice: 0, cognome: 1, nome: 2, email: 3, telefono: 4, azioni: 5 }[column];
|
||||
const tbody = document.getElementById('table-body');
|
||||
const rows = Array.from(tbody.querySelectorAll('tr'));
|
||||
|
||||
rows.sort((a, b) => {
|
||||
const aVal = a.querySelectorAll('td')[colIndex]?.textContent.trim() || '';
|
||||
const bVal = b.querySelectorAll('td')[colIndex]?.textContent.trim() || '';
|
||||
const cmp = aVal.localeCompare(bVal, undefined, { numeric: true });
|
||||
return currentSort.direction === 'asc' ? cmp : -cmp;
|
||||
});
|
||||
|
||||
rows.forEach(row => tbody.appendChild(row));
|
||||
|
||||
document.querySelectorAll('th[data-sortable]').forEach(th => {
|
||||
const icon = th.querySelector('i');
|
||||
if (th.dataset.column === column) {
|
||||
icon.className = currentSort.direction === 'asc' ? 'fas fa-sort-up float-right' : 'fas fa-sort-down float-right';
|
||||
} else {
|
||||
icon.className = 'fas fa-sort float-right';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleColumn(column, visible) {
|
||||
const colIndex = { codice: 1, cognome: 2, nome: 3, email: 4, telefono: 5, azioni: 6 }[column];
|
||||
const ths = document.querySelectorAll('#individui-table thead th');
|
||||
const rows = document.querySelectorAll('#table-body tr');
|
||||
|
||||
if (visible) {
|
||||
ths[colIndex].style.display = '';
|
||||
rows.forEach(row => {
|
||||
row.querySelectorAll('td')[colIndex].style.display = '';
|
||||
});
|
||||
} else {
|
||||
ths[colIndex].style.display = 'none';
|
||||
rows.forEach(row => {
|
||||
row.querySelectorAll('td')[colIndex].style.display = 'none';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function saveVista() {
|
||||
let nome = document.getElementById('vista-nome').value.trim();
|
||||
if (!nome) {
|
||||
const now = new Date();
|
||||
nome = 'Vista ' + now.toLocaleDateString('it-IT') + ' ' + now.toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
const colonneVisibili = Array.from(document.querySelectorAll('#colonne-checkboxes input:checked')).map(i => i.value);
|
||||
const isDefault = document.getElementById('vista-default').checked;
|
||||
|
||||
const data = {
|
||||
nome: nome,
|
||||
tipo: 'individui',
|
||||
colonne_visibili: colonneVisibili,
|
||||
colonne_ordinamento: currentSort.column ? [[currentSort.column, currentSort.direction]] : [],
|
||||
ricerca: document.getElementById('global-search').value,
|
||||
is_default: isDefault
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/viste', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || ''
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Vista salvata!');
|
||||
$('#saveVistaModal').modal('hide');
|
||||
} else {
|
||||
alert('Errore salvataggio');
|
||||
}
|
||||
} catch(e) {
|
||||
alert('Errore: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSelectAll(source) {
|
||||
const checkboxes = document.querySelectorAll('.row-checkbox');
|
||||
checkboxes.forEach(cb => {
|
||||
cb.checked = source.checked;
|
||||
});
|
||||
}
|
||||
|
||||
function exportCSV() {
|
||||
window.location.href = '/individui/export';
|
||||
}
|
||||
|
||||
function exportSelectedCSV() {
|
||||
const selectedIds = getSelectedIds();
|
||||
if (selectedIds.length === 0) {
|
||||
alert('Seleziona almeno un individuo');
|
||||
return;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
selectedIds.forEach(function(id) {
|
||||
params.append('ids[]', id);
|
||||
});
|
||||
|
||||
window.location.href = '/individui/export?' + params.toString();
|
||||
}
|
||||
|
||||
let deleteIndividuoId = null;
|
||||
|
||||
async function showDeleteModal(id, nome) {
|
||||
deleteIndividuoId = id;
|
||||
document.getElementById('delete-individuo-name').textContent = nome;
|
||||
|
||||
document.getElementById('count-contatti').textContent = '...';
|
||||
document.getElementById('count-gruppi').textContent = '...';
|
||||
document.getElementById('count-documenti').textContent = '...';
|
||||
document.getElementById('count-eventi').textContent = '...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/individui/${id}/collegati`);
|
||||
const data = await response.json();
|
||||
document.getElementById('count-contatti').textContent = data.contatti;
|
||||
document.getElementById('count-gruppi').textContent = data.gruppi;
|
||||
document.getElementById('count-documenti').textContent = data.documenti;
|
||||
document.getElementById('count-eventi').textContent = data.eventi;
|
||||
} catch(e) {
|
||||
console.error('Errore recupero dati:', e);
|
||||
}
|
||||
|
||||
$('#deleteModal').modal('show');
|
||||
}
|
||||
|
||||
function confirmDelete() {
|
||||
const eliminaContatti = document.getElementById('delete-contatti').checked;
|
||||
const eliminaDocumenti = document.getElementById('delete-documenti').checked;
|
||||
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = `/individui/${deleteIndividuoId}/elimina`;
|
||||
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '<?php echo e(csrf_token()); ?>';
|
||||
form.innerHTML = `
|
||||
<input type="hidden" name="_token" value="${csrfToken}">
|
||||
<input type="hidden" name="elimina_contatti" value="${eliminaContatti}">
|
||||
<input type="hidden" name="elimina_documenti" value="${eliminaDocumenti}">
|
||||
`;
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
|
||||
function printIndividuo(id) {
|
||||
window.open('/individui/' + id + '/report', '_blank');
|
||||
}
|
||||
|
||||
function exportIndividuoCSV(id) {
|
||||
const row = document.querySelector(`tr[data-id="${id}"]`);
|
||||
if (!row) return;
|
||||
|
||||
const cells = Array.from(row.querySelectorAll('td'));
|
||||
const data = {
|
||||
codice: cells[1].textContent.trim(),
|
||||
cognome: cells[2].textContent.trim(),
|
||||
nome: cells[3].textContent.trim(),
|
||||
email: cells[4].textContent.trim(),
|
||||
telefono: cells[5].textContent.trim()
|
||||
};
|
||||
|
||||
const csv = `Codice,Cognome,Nome,Email,Telefono\n"${data.codice}","${data.cognome}","${data.nome}","${data.email}","${data.telefono}"`;
|
||||
const blob = new Blob([csv], { type: 'text/csv' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `individuo_${data.codice}.csv`;
|
||||
a.click();
|
||||
}
|
||||
|
||||
function sendEmail(id) {
|
||||
window.location.href = `/mailing/nuovo?individui=${id}`;
|
||||
}
|
||||
|
||||
function generateReport(id) {
|
||||
window.open('/individui/' + id + '/report', '_blank');
|
||||
}
|
||||
|
||||
function getSelectedIds() {
|
||||
return Array.from(document.querySelectorAll('.row-checkbox:checked')).map(cb => cb.value);
|
||||
}
|
||||
|
||||
function exportSelectedCSV() {
|
||||
const selectedIds = getSelectedIds();
|
||||
if (selectedIds.length === 0) {
|
||||
alert('Seleziona almeno un individuo');
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = [];
|
||||
rows.push('Codice,Cognome,Nome,Email,Telefono');
|
||||
|
||||
document.querySelectorAll('#table-body tr').forEach(row => {
|
||||
const checkbox = row.querySelector('.row-checkbox');
|
||||
if (checkbox && checkbox.checked) {
|
||||
const cells = Array.from(row.querySelectorAll('td'));
|
||||
const codice = cells[1].textContent.trim();
|
||||
const cognome = cells[2].textContent.trim();
|
||||
const nome = cells[3].textContent.trim();
|
||||
const email = cells[4].textContent.trim();
|
||||
const telefono = cells[5].textContent.trim();
|
||||
rows.push(`"${codice}","${cognome}","${nome}","${email}","${telefono}"`);
|
||||
}
|
||||
});
|
||||
|
||||
const blob = new Blob([rows.join('\n')], { type: 'text/csv' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `individui_selezionati_${new Date().toISOString().slice(0,10)}.csv`;
|
||||
a.click();
|
||||
}
|
||||
|
||||
function printSelected() {
|
||||
const selectedIds = getSelectedIds();
|
||||
if (selectedIds.length === 0) {
|
||||
alert('Seleziona almeno un individuo');
|
||||
return;
|
||||
}
|
||||
window.open(`/individui/report/stampa?ids=${selectedIds.join(',')}`, '_blank');
|
||||
}
|
||||
|
||||
function emailSelected() {
|
||||
const selectedIds = getSelectedIds();
|
||||
if (selectedIds.length === 0) {
|
||||
alert('Seleziona almeno un individuo');
|
||||
return;
|
||||
}
|
||||
window.location.href = `/mailing/nuovo?individui=${selectedIds.join(',')}`;
|
||||
}
|
||||
|
||||
function deleteSelected() {
|
||||
const selectedIds = getSelectedIds();
|
||||
if (selectedIds.length === 0) {
|
||||
alert('Seleziona almeno un individuo');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('delete-individuo-name').textContent = selectedIds.length + ' individui selezionati';
|
||||
document.getElementById('count-contatti').textContent = '...';
|
||||
document.getElementById('count-gruppi').textContent = '...';
|
||||
document.getElementById('count-documenti').textContent = '...';
|
||||
document.getElementById('count-eventi').textContent = '...';
|
||||
|
||||
document.getElementById('confirm-delete-btn').onclick = function() {
|
||||
const eliminaContatti = document.getElementById('delete-contatti').checked;
|
||||
const eliminaDocumenti = document.getElementById('delete-documenti').checked;
|
||||
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/individui/mass-elimina';
|
||||
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '<?php echo e(csrf_token()); ?>';
|
||||
|
||||
let html = `<input type="hidden" name="_token" value="${csrfToken}">`;
|
||||
html += `<input type="hidden" name="elimina_contatti" value="${eliminaContatti}">`;
|
||||
html += `<input type="hidden" name="elimina_documenti" value="${eliminaDocumenti}">`;
|
||||
|
||||
selectedIds.forEach(function(id) {
|
||||
html += `<input type="hidden" name="ids[]" value="${id}">`;
|
||||
});
|
||||
|
||||
form.innerHTML = html;
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
};
|
||||
|
||||
$('#deleteModal').modal('show');
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]');
|
||||
if (!csrfToken) {
|
||||
const meta = document.createElement('meta');
|
||||
meta.name = 'csrf-token';
|
||||
meta.content = '<?php echo e(csrf_token()); ?>';
|
||||
document.head.appendChild(meta);
|
||||
}
|
||||
|
||||
const visibleCols = <?php echo $visibleColumnsJson; ?>;
|
||||
const allCols = <?php echo $allColumnsJson; ?>;
|
||||
|
||||
allCols.forEach(function(col) {
|
||||
const visibile = visibleCols.includes(col.key);
|
||||
const saveCheckbox = document.querySelector(`#col-vis-${col.key}`);
|
||||
const toggleCheckbox = document.querySelector(`#col-toggle-${col.key}`);
|
||||
if (saveCheckbox) saveCheckbox.checked = visibile;
|
||||
if (toggleCheckbox) toggleCheckbox.checked = visibile;
|
||||
});
|
||||
|
||||
const vistaDefault = <?php echo $vistaDefaultJson; ?>;
|
||||
if (vistaDefault && vistaDefault.ricerca) {
|
||||
document.getElementById('global-search').value = vistaDefault.ricerca;
|
||||
applyGlobalSearch(vistaDefault.ricerca);
|
||||
}
|
||||
});
|
||||
|
||||
function showCreateMailingListModal() {
|
||||
document.getElementById('ml-nome').value = '';
|
||||
document.getElementById('ml-descrizione').value = '';
|
||||
document.getElementById('ml-contatti-tbody').innerHTML = '';
|
||||
document.getElementById('ml-count').textContent = '0';
|
||||
|
||||
const selectedIds = getSelectedIds();
|
||||
console.log('Selected IDs:', selectedIds);
|
||||
if (selectedIds.length > 0) {
|
||||
const url = '/individui/email-list?ids=' + selectedIds.join(',');
|
||||
console.log('Fetching URL:', url);
|
||||
fetch(url, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
console.log('Response status:', response.status);
|
||||
console.log('Response type:', response.headers.get('content-type'));
|
||||
if (!response.ok) {
|
||||
return response.text().then(text => {
|
||||
console.log('Full error response:', text.substring(0, 500));
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
const json = JSON.parse(text);
|
||||
throw new Error(json.message || 'Errore HTTP ' + response.status);
|
||||
} else {
|
||||
throw new Error('Server returned HTML (status ' + response.status + ') - probabilmente sessione scaduta. Ricarica la pagina.');
|
||||
}
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Data:', data);
|
||||
updateMlTable(data);
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error('Error:', err);
|
||||
alert('Errore caricamento contatti: ' + err.message + '\nURL: ' + url);
|
||||
});
|
||||
} else {
|
||||
alert('Seleziona almeno un individuo dalla tabella');
|
||||
}
|
||||
|
||||
$('#createMailingListModal').modal('show');
|
||||
}
|
||||
|
||||
function updateMlTable(data) {
|
||||
const tbody = document.getElementById('ml-contatti-tbody');
|
||||
const existingIds = new Set();
|
||||
|
||||
document.querySelectorAll('.ml-row-checkbox').forEach(function(cb) {
|
||||
existingIds.add(parseInt(cb.value));
|
||||
});
|
||||
|
||||
data.forEach(function(item) {
|
||||
if (existingIds.has(item.id)) return;
|
||||
|
||||
const tr = document.createElement('tr');
|
||||
const checked = item.email_count === 1 ? 'checked' : '';
|
||||
tr.innerHTML = `
|
||||
<td>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input ml-row-checkbox" id="ml-${item.id}" value="${item.id}" data-email="${item.email || ''}" ${checked}>
|
||||
<label class="custom-control-label" for="ml-${item.id}"></label>
|
||||
</div>
|
||||
</td>
|
||||
<td><span class="badge badge-secondary">${item.codice_id}</span></td>
|
||||
<td>${item.cognome}</td>
|
||||
<td>${item.nome}</td>
|
||||
<td>
|
||||
${item.email_count === 1 ? (item.email || '-') :
|
||||
'<select class="form-control form-control-sm" onchange="updateMlCheckboxEmail(' + item.id + ', this.value)">' +
|
||||
'<option value="">Seleziona email...</option>' +
|
||||
(item.emails || []).map(function(e) { return '<option value="' + e + '">' + e + '</option>'; }).join('') +
|
||||
'</select>'}
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-xs btn-danger" onclick="this.closest('tr').remove(); updateMlCount();">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
updateMlCount();
|
||||
}
|
||||
|
||||
function updateMlCount() {
|
||||
document.getElementById('ml-count').textContent = document.querySelectorAll('.ml-row-checkbox').length;
|
||||
}
|
||||
|
||||
function toggleMlSelectAll(source) {
|
||||
document.querySelectorAll('.ml-row-checkbox').forEach(function(cb) {
|
||||
cb.checked = source.checked;
|
||||
});
|
||||
}
|
||||
|
||||
function updateMlCheckboxEmail(id, email) {
|
||||
const checkbox = document.getElementById('ml-' + id);
|
||||
if (checkbox) {
|
||||
checkbox.dataset.email = email;
|
||||
checkbox.checked = email !== '';
|
||||
}
|
||||
}
|
||||
|
||||
async function createMailingList() {
|
||||
const nome = document.getElementById('ml-nome').value.trim();
|
||||
if (!nome) {
|
||||
alert('Inserisci un nome per la lista');
|
||||
return;
|
||||
}
|
||||
|
||||
const selected = [];
|
||||
document.querySelectorAll('.ml-row-checkbox:checked').forEach(function(cb) {
|
||||
if (cb.dataset.email) {
|
||||
selected.push({
|
||||
individuo_id: parseInt(cb.value),
|
||||
email: cb.dataset.email
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (selected.length === 0) {
|
||||
alert('Seleziona almeno un contatto con email');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
nome: nome,
|
||||
descrizione: document.getElementById('ml-descrizione').value.trim(),
|
||||
contatti: selected
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/mailing-liste/create-from-individui', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || ''
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Mailing list creata con successo!');
|
||||
$('#createMailingListModal').modal('hide');
|
||||
} else {
|
||||
alert('Errore nella creazione della lista');
|
||||
}
|
||||
} catch(e) {
|
||||
alert('Errore: ' + e.message);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<?php $__env->stopSection(); ?>
|
||||
<?php echo $__env->make('layouts.adminlte', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH /var/www/html/glastree/resources/views/individui/index.blade.php ENDPATH**/ ?>
|
||||
@@ -1,108 +0,0 @@
|
||||
<?php
|
||||
use App\Models\AppSetting;
|
||||
$appLogo = AppSetting::getLogoUrl();
|
||||
$appName = AppSetting::getAppName() ?? 'Glastree';
|
||||
$appOrgName = AppSetting::getOrgName() ?? '';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Accedi - <?php echo e(e($appName)); ?></title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/admin-lte@3.2/dist/css/adminlte.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
</head>
|
||||
<body class="hold-transition login-page">
|
||||
<div class="login-box">
|
||||
<div class="login-logo">
|
||||
<a href="/">
|
||||
<?php if($appLogo): ?>
|
||||
<img src="<?php echo e($appLogo); ?>" alt="Logo" style="max-height: 80px; max-width: 200px;">
|
||||
<?php else: ?>
|
||||
<i class="fas fa-tree text-success"></i> <b><?php echo e(e($appName)); ?></b>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php if($appOrgName): ?>
|
||||
<div class="text-center mb-3">
|
||||
<small class="text-muted"><?php echo e(e($appOrgName)); ?></small>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="card">
|
||||
<div class="card-body login-card-body">
|
||||
<p class="login-box-msg">Accedi per continuare</p>
|
||||
<form action="<?php echo e(route('login')); ?>" method="POST">
|
||||
<?php echo csrf_field(); ?>
|
||||
<div class="input-group mb-3">
|
||||
<input type="email" name="email" class="form-control <?php $__errorArgs = ['email'];
|
||||
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
|
||||
if ($__bag->has($__errorArgs[0])) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs[0]); ?> is-invalid <?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>" placeholder="Email" required>
|
||||
<div class="input-group-append">
|
||||
<div class="input-group-text">
|
||||
<span class="fas fa-envelope"></span>
|
||||
</div>
|
||||
</div>
|
||||
<?php $__errorArgs = ['email'];
|
||||
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
|
||||
if ($__bag->has($__errorArgs[0])) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs[0]); ?>
|
||||
<span class="invalid-feedback"><?php echo e($message); ?></span>
|
||||
<?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<input type="password" name="password" class="form-control <?php $__errorArgs = ['password'];
|
||||
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
|
||||
if ($__bag->has($__errorArgs[0])) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs[0]); ?> is-invalid <?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>" placeholder="Password" required>
|
||||
<div class="input-group-append">
|
||||
<div class="input-group-text">
|
||||
<span class="fas fa-lock"></span>
|
||||
</div>
|
||||
</div>
|
||||
<?php $__errorArgs = ['password'];
|
||||
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
|
||||
if ($__bag->has($__errorArgs[0])) :
|
||||
if (isset($message)) { $__messageOriginal = $message; }
|
||||
$message = $__bag->first($__errorArgs[0]); ?>
|
||||
<span class="invalid-feedback"><?php echo e($message); ?></span>
|
||||
<?php unset($message);
|
||||
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
|
||||
endif;
|
||||
unset($__errorArgs, $__bag); ?>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-8">
|
||||
<div class="icheck-primary">
|
||||
<input type="checkbox" id="remember">
|
||||
<label for="remember">Ricordami</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<button type="submit" class="btn btn-success btn-block">Accedi</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<p class="mb-0 text-center mt-3">
|
||||
<a href="<?php echo e(route('password.request')); ?>" class="text-center">Password dimenticata?</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html><?php /**PATH /var/www/html/glastree/resources/views/auth/login.blade.php ENDPATH**/ ?>
|
||||
@@ -1,301 +0,0 @@
|
||||
<?php
|
||||
$appLogoSmall = \App\Models\AppSetting::getLogoSmallUrl();
|
||||
$appName = \App\Models\AppSetting::getAppName() ?? 'Glastree';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="<?php echo e(csrf_token()); ?>">
|
||||
<title><?php echo $__env->yieldContent('title', e($appName)); ?></title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/admin-lte@3.2/dist/css/adminlte.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400;600;700&display=swap">
|
||||
<?php echo $__env->yieldContent('styles'); ?>
|
||||
</head>
|
||||
<body class="hold-transition sidebar-mini layout-fixed">
|
||||
<div class="wrapper">
|
||||
<nav class="main-header navbar navbar-expand navbar-white navbar-light">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-widget="pushmenu" href="#" role="button"><i class="fas fa-bars"></i></a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav ml-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="<?php echo e(route('help')); ?>" title="Guida">
|
||||
<i class="fas fa-question-circle"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link" data-toggle="dropdown" href="#">
|
||||
<i class="far fa-bell"></i>
|
||||
<?php $unread = Auth::user()->unreadNotificheCount() ?? 0 ?>
|
||||
<?php if($unread > 0): ?>
|
||||
<span class="badge badge-warning navbar-badge"><?php echo e($unread); ?></span>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<span class="dropdown-header">Notifiche (<?php echo e($unread); ?>)</span>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a href="#" class="dropdown-item">Visualizza tutte</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link" data-toggle="dropdown" href="#">
|
||||
<i class="fas fa-user"></i> <?php echo e(Auth::user()->name); ?>
|
||||
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a href="<?php echo e(route('profile')); ?>" class="dropdown-item">
|
||||
<i class="fas fa-user-cog mr-2"></i> Profilo
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<form method="POST" action="<?php echo e(route('logout')); ?>">
|
||||
<?php echo csrf_field(); ?>
|
||||
<button type="submit" class="dropdown-item">
|
||||
<i class="fas fa-sign-out-alt mr-2"></i> Esci
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<?php
|
||||
$appLogoSmall = \App\Models\AppSetting::getLogoSmallUrl();
|
||||
$appName = \App\Models\AppSetting::getAppName() ?? 'Glastree';
|
||||
?>
|
||||
<aside class="main-sidebar sidebar-dark-primary elevation-4">
|
||||
<a href="<?php echo e(route('dashboard')); ?>" class="brand-link">
|
||||
<?php if($appLogoSmall): ?>
|
||||
<img src="<?php echo e($appLogoSmall); ?>" alt="Logo" class="brand-image" style="max-height: 35px; max-width: 40px;">
|
||||
<?php else: ?>
|
||||
<i class="fas fa-tree brand-image ml-3 mr-2"></i>
|
||||
<?php endif; ?>
|
||||
<span class="brand-text font-weight-light"><?php echo e($appName); ?></span>
|
||||
</a>
|
||||
|
||||
<div class="sidebar">
|
||||
<nav class="mt-2">
|
||||
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu">
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('dashboard')); ?>" class="nav-link <?php echo e(request()->routeIs('dashboard') ? 'active' : ''); ?>">
|
||||
<i class="nav-icon fas fa-tachometer-alt"></i>
|
||||
<p>Dashboard</p>
|
||||
</a>
|
||||
</li>
|
||||
<?php if(Auth::user()->canAccess('individui')): ?>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('individui.index')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-users"></i>
|
||||
<p>Individui</p>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if(Auth::user()->canAccess('gruppi')): ?>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('gruppi.index')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-folder"></i>
|
||||
<p>Gruppi</p>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if(Auth::user()->canAccess('documenti')): ?>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('documenti.index')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-file"></i>
|
||||
<p>Documenti</p>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if(Auth::user()->canAccess('eventi')): ?>
|
||||
<li class="nav-item has-treeview">
|
||||
<a href="<?php echo e(route('eventi.calendar')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-calendar"></i>
|
||||
<p>Eventi <i class="right fas fa-angle-left" style="font-size: 0.8rem;"></i></p>
|
||||
</a>
|
||||
<ul class="nav nav-treeview">
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('eventi.calendar')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-calendar-alt"></i>
|
||||
<p>Calendario</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('eventi.index')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-list"></i>
|
||||
<p>Elenco</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('eventi.create')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-plus"></i>
|
||||
<p>Nuovo Evento</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if(Auth::user()->canAccess('email') || Auth::user()->canAccess('mailing')): ?>
|
||||
<li class="nav-item has-treeview">
|
||||
<a href="<?php echo e(route('email.index', 'inbox')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-envelope"></i>
|
||||
<p>Email <i class="right fas fa-angle-left" style="font-size: 0.8rem;"></i></p>
|
||||
</a>
|
||||
<ul class="nav nav-treeview">
|
||||
<?php if(Auth::user()->canAccess('email')): ?>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('email.compose')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-plus"></i>
|
||||
<p>Nuova Email</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('email.index', 'inbox')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-inbox"></i>
|
||||
<p>In arrivo</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('email.index', 'starred')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-star"></i>
|
||||
<p>Preferiti</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('email.index', 'sent')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-paper-plane"></i>
|
||||
<p>Inviate</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('email.index', 'drafts')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-edit"></i>
|
||||
<p>Bozze</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('email.index', 'archive')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-archive"></i>
|
||||
<p>Archivio</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('email.index', 'trash')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-trash"></i>
|
||||
<p>Cestino</p>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if(Auth::user()->canAccess('mailing')): ?>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('mailing-liste.index')); ?>" class="nav-link">
|
||||
<i class="nav-icon fas fa-list"></i>
|
||||
<p>Mailing List</p>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if(Auth::user()->canAccess('report')): ?>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('report.index')); ?>" class="nav-link <?php echo e(request()->routeIs('report.*') ? 'active' : ''); ?>">
|
||||
<i class="nav-icon fas fa-chart-bar"></i>
|
||||
<p>Report</p>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if(Auth::user()->canManage('settings')): ?>
|
||||
<li class="nav-item">
|
||||
<a href="/impostazioni" class="nav-link <?php echo e(request()->is('impostazioni*') ? 'active' : ''); ?>">
|
||||
<i class="nav-icon fas fa-cog"></i>
|
||||
<p>Impostazioni</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item has-treeview <?php echo e(request()->is('admin/*') ? 'menu-open' : ''); ?>">
|
||||
<a href="#" class="nav-link <?php echo e(request()->is('admin/*') ? 'active' : ''); ?>">
|
||||
<i class="nav-icon fas fa-users-cog"></i>
|
||||
<p>Admin <i class="right fas fa-angle-left" style="font-size: 0.8rem;"></i></p>
|
||||
</a>
|
||||
<ul class="nav nav-treeview">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/utenti" class="nav-link <?php echo e(request()->routeIs('admin.utenti.*') ? 'active' : ''); ?>">
|
||||
<i class="nav-icon fas fa-user-shield"></i>
|
||||
<p>Utenti</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('admin.ruoli.index')); ?>" class="nav-link <?php echo e(request()->routeIs('admin.ruoli.*') ? 'active' : ''); ?>">
|
||||
<i class="nav-icon fas fa-tag"></i>
|
||||
<p>Ruoli</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/admin/backup" class="nav-link <?php echo e(request()->routeIs('admin.backup.*') ? 'active' : ''); ?>">
|
||||
<i class="nav-icon fas fa-hdd"></i>
|
||||
<p>Backup</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('admin.activity-logs.index')); ?>" class="nav-link <?php echo e(request()->routeIs('admin.activity-logs.*') ? 'active' : ''); ?>">
|
||||
<i class="nav-icon fas fa-history"></i>
|
||||
<p>Log Attività</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo e(route('help')); ?>" class="nav-link <?php echo e(request()->routeIs('help') ? 'active' : ''); ?>">
|
||||
<i class="nav-icon fas fa-question-circle"></i>
|
||||
<p>Guida / Aiuto</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="content-wrapper" style="min-height: calc(100vh - 120px);">
|
||||
<div class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-6">
|
||||
<h1 class="m-0"><?php echo $__env->yieldContent('page_title', 'Dashboard'); ?></h1>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<ol class="breadcrumb float-sm-right">
|
||||
<?php echo $__env->yieldContent('breadcrumbs'); ?>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="container-fluid">
|
||||
<?php echo $slot ?? ''; ?>
|
||||
|
||||
<?php echo $__env->yieldContent('content'); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="main-footer">
|
||||
<strong><?php echo e(\App\Models\AppSetting::getFooterText()); ?></strong>
|
||||
<?php if(\App\Models\AppSetting::shouldShowVersion() && \App\Models\AppSetting::getAppVersion()): ?>
|
||||
<span class="ml-2">v<?php echo e(\App\Models\AppSetting::getAppVersion()); ?></span>
|
||||
<?php endif; ?>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/admin-lte@3.2/dist/js/adminlte.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<?php echo $__env->yieldContent('scripts'); ?>
|
||||
</body>
|
||||
</html><?php /**PATH /var/www/html/glastree/resources/views/layouts/adminlte.blade.php ENDPATH**/ ?>
|
||||
@@ -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');
|
||||
});
|
||||
Vendored
-122
@@ -1,122 +0,0 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../phpunit/phpunit/phpunit)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
$GLOBALS['__PHPUNIT_ISOLATION_EXCLUDE_LIST'] = $GLOBALS['__PHPUNIT_ISOLATION_BLACKLIST'] = array(realpath(__DIR__ . '/..'.'/phpunit/phpunit/phpunit'));
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = 'phpvfscomposer://'.$this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
$data = str_replace('__DIR__', var_export(dirname($this->realpath), true), $data);
|
||||
$data = str_replace('__FILE__', var_export($this->realpath, true), $data);
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
return include("phpvfscomposer://" . __DIR__ . '/..'.'/phpunit/phpunit/phpunit');
|
||||
}
|
||||
}
|
||||
|
||||
return include __DIR__ . '/..'.'/phpunit/phpunit/phpunit';
|
||||
Vendored
-119
@@ -1,119 +0,0 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../laravel/pint/builds/pint)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
return include("phpvfscomposer://" . __DIR__ . '/..'.'/laravel/pint/builds/pint');
|
||||
}
|
||||
}
|
||||
|
||||
return include __DIR__ . '/..'.'/laravel/pint/builds/pint';
|
||||
+15
-2092
File diff suppressed because it is too large
Load Diff
Vendored
+3
-10
@@ -11,11 +11,11 @@ return array(
|
||||
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
|
||||
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
|
||||
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
|
||||
'8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php',
|
||||
'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php',
|
||||
'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php',
|
||||
'383eaff206634a77a1be54e64e6459c7' => $vendorDir . '/sabre/uri/lib/functions.php',
|
||||
'8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php',
|
||||
'def43f6c87e4f8dfd0c9e1b1bab14fe8' => $vendorDir . '/symfony/polyfill-iconv/bootstrap.php',
|
||||
'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php',
|
||||
'667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
|
||||
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
|
||||
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
|
||||
@@ -27,15 +27,13 @@ return array(
|
||||
'2203a247e6fda86070a5e4e07aed533a' => $vendorDir . '/symfony/clock/Resources/now.php',
|
||||
'606a39d89246991a373564698c2d8383' => $vendorDir . '/symfony/polyfill-php85/bootstrap.php',
|
||||
'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php',
|
||||
'35a6ad97d21e794e7e22a17d806652e4' => $vendorDir . '/nunomaduro/termwind/src/Functions.php',
|
||||
'b33e3d135e5d9e47d845c576147bda89' => $vendorDir . '/php-di/php-di/src/functions.php',
|
||||
'1f87db08236948d07391152dccb70f04' => $vendorDir . '/google/apiclient-services/autoload.php',
|
||||
'ebdb698ed4152ae445614b69b5e4bb6a' => $vendorDir . '/sabre/http/lib/functions.php',
|
||||
'09f6b20656683369174dd6fa83b7e5fb' => $vendorDir . '/symfony/polyfill-uuid/bootstrap.php',
|
||||
'a8d3953fd9959404dd22d3dfcd0a79f0' => $vendorDir . '/google/apiclient/src/aliases.php',
|
||||
'23dd7ece5822da3d0100ef3deb0ef55f' => $vendorDir . '/laravel/agent-detector/src/functions.php',
|
||||
'47e1160838b5e5a10346ac4084b58c23' => $vendorDir . '/laravel/prompts/src/helpers.php',
|
||||
'6124b4c8570aa390c21fafd04a26c69f' => $vendorDir . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',
|
||||
'35a6ad97d21e794e7e22a17d806652e4' => $vendorDir . '/nunomaduro/termwind/src/Functions.php',
|
||||
'801c31d8ed748cfa537fa45402288c95' => $vendorDir . '/psy/psysh/src/functions.php',
|
||||
'e39a8b23c42d4e1452234d762b03835a' => $vendorDir . '/ramsey/uuid/src/functions.php',
|
||||
'9d2b9fc6db0f153a0a149fefb182415e' => $vendorDir . '/symfony/polyfill-php84/bootstrap.php',
|
||||
@@ -49,10 +47,5 @@ return array(
|
||||
'91892b814db86b8442ad76273bb7aec5' => $vendorDir . '/laravel/framework/src/Illuminate/Reflection/helpers.php',
|
||||
'493c6aea52f6009bab023b26c21a386a' => $vendorDir . '/laravel/framework/src/Illuminate/Support/functions.php',
|
||||
'58571171fd5812e6e447dce228f52f4d' => $vendorDir . '/laravel/framework/src/Illuminate/Support/helpers.php',
|
||||
'309cd39bb536ff667b25a3a76938cb83' => $vendorDir . '/laravel/pao/src/Autoload.php',
|
||||
'c72349b1fe8d0deeedd3a52e8aa814d8' => $vendorDir . '/mockery/mockery/library/helpers.php',
|
||||
'ce9671a430e4846b44e1c68c7611f9f5' => $vendorDir . '/mockery/mockery/library/Mockery.php',
|
||||
'a1cfe24d14977df6878b9bf804af2d1c' => $vendorDir . '/nunomaduro/collision/src/Adapters/Phpunit/Autoload.php',
|
||||
'ec07570ca5a812141189b1fa81503674' => $vendorDir . '/phpunit/phpunit/src/Framework/Assert/Functions.php',
|
||||
'377b22b161c09ed6e5152de788ca020a' => $vendorDir . '/spatie/laravel-permission/src/helpers.php',
|
||||
);
|
||||
|
||||
Vendored
+3
-12
@@ -10,10 +10,8 @@ return array(
|
||||
'ZBateson\\StreamDecorators\\' => array($vendorDir . '/zbateson/stream-decorators/src'),
|
||||
'ZBateson\\MbWrapper\\' => array($vendorDir . '/zbateson/mb-wrapper/src'),
|
||||
'ZBateson\\MailMimeParser\\' => array($vendorDir . '/zbateson/mail-mime-parser/src'),
|
||||
'Whoops\\' => array($vendorDir . '/filp/whoops/src/Whoops'),
|
||||
'Webklex\\PHPIMAP\\' => array($vendorDir . '/webklex/php-imap/src'),
|
||||
'TijsVerkoyen\\CssToInlineStyles\\' => array($vendorDir . '/tijsverkoyen/css-to-inline-styles/src'),
|
||||
'Tests\\' => array($baseDir . '/tests'),
|
||||
'Termwind\\' => array($vendorDir . '/nunomaduro/termwind/src'),
|
||||
'Symfony\\Polyfill\\Uuid\\' => array($vendorDir . '/symfony/polyfill-uuid'),
|
||||
'Symfony\\Polyfill\\Php86\\' => array($vendorDir . '/symfony/polyfill-php86'),
|
||||
@@ -66,10 +64,8 @@ return array(
|
||||
'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'),
|
||||
'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'),
|
||||
'PhpOption\\' => array($vendorDir . '/phpoption/phpoption/src/PhpOption'),
|
||||
'NunoMaduro\\Collision\\' => array($vendorDir . '/nunomaduro/collision/src'),
|
||||
'Nette\\' => array($vendorDir . '/nette/schema/src', $vendorDir . '/nette/utils/src'),
|
||||
'Monolog\\' => array($vendorDir . '/monolog/monolog/src/Monolog'),
|
||||
'Mockery\\' => array($vendorDir . '/mockery/mockery/library/Mockery'),
|
||||
'League\\Uri\\' => array($vendorDir . '/league/uri', $vendorDir . '/league/uri-interfaces'),
|
||||
'League\\MimeTypeDetection\\' => array($vendorDir . '/league/mime-type-detection/src'),
|
||||
'League\\Flysystem\\WebDAV\\' => array($vendorDir . '/league/flysystem-webdav'),
|
||||
@@ -80,9 +76,6 @@ return array(
|
||||
'Laravel\\Tinker\\' => array($vendorDir . '/laravel/tinker/src'),
|
||||
'Laravel\\SerializableClosure\\' => array($vendorDir . '/laravel/serializable-closure/src'),
|
||||
'Laravel\\Prompts\\' => array($vendorDir . '/laravel/prompts/src'),
|
||||
'Laravel\\Pao\\' => array($vendorDir . '/laravel/pao/src'),
|
||||
'Laravel\\Pail\\' => array($vendorDir . '/laravel/pail/src'),
|
||||
'Laravel\\AgentDetector\\' => array($vendorDir . '/laravel/agent-detector/src'),
|
||||
'Invoker\\' => array($vendorDir . '/php-di/invoker/src'),
|
||||
'Illuminate\\Support\\' => array($vendorDir . '/laravel/framework/src/Illuminate/Macroable', $vendorDir . '/laravel/framework/src/Illuminate/Collections', $vendorDir . '/laravel/framework/src/Illuminate/Conditionable', $vendorDir . '/laravel/framework/src/Illuminate/Reflection'),
|
||||
'Illuminate\\' => array($vendorDir . '/laravel/framework/src/Illuminate'),
|
||||
@@ -96,7 +89,6 @@ return array(
|
||||
'Google\\' => array($vendorDir . '/google/apiclient/src'),
|
||||
'Fruitcake\\Cors\\' => array($vendorDir . '/fruitcake/php-cors/src'),
|
||||
'Firebase\\JWT\\' => array($vendorDir . '/firebase/php-jwt/src'),
|
||||
'Faker\\' => array($vendorDir . '/fakerphp/faker/src/Faker'),
|
||||
'Egulias\\EmailValidator\\' => array($vendorDir . '/egulias/email-validator/src'),
|
||||
'Dotenv\\' => array($vendorDir . '/vlucas/phpdotenv/src'),
|
||||
'Doctrine\\Inflector\\' => array($vendorDir . '/doctrine/inflector/src'),
|
||||
@@ -104,9 +96,8 @@ return array(
|
||||
'DirectoryTree\\ImapEngine\\Laravel\\' => array($vendorDir . '/directorytree/imapengine-laravel/src'),
|
||||
'DirectoryTree\\ImapEngine\\' => array($vendorDir . '/directorytree/imapengine/src'),
|
||||
'Dflydev\\DotAccessData\\' => array($vendorDir . '/dflydev/dot-access-data/src'),
|
||||
'DeepCopy\\' => array($vendorDir . '/myclabs/deep-copy/src/DeepCopy'),
|
||||
'Database\\Seeders\\' => array($baseDir . '/database/seeders', $vendorDir . '/laravel/pint/database/seeders'),
|
||||
'Database\\Factories\\' => array($baseDir . '/database/factories', $vendorDir . '/laravel/pint/database/factories'),
|
||||
'Database\\Seeders\\' => array($baseDir . '/database/seeders'),
|
||||
'Database\\Factories\\' => array($baseDir . '/database/factories'),
|
||||
'DI\\' => array($vendorDir . '/php-di/php-di/src'),
|
||||
'Cron\\' => array($vendorDir . '/dragonmantank/cron-expression/src/Cron'),
|
||||
'Carbon\\Doctrine\\' => array($vendorDir . '/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine'),
|
||||
@@ -114,5 +105,5 @@ return array(
|
||||
'Brick\\Math\\' => array($vendorDir . '/brick/math/src'),
|
||||
'As247\\Flysystem\\GoogleDrive\\' => array($vendorDir . '/as247/flysystem-google-drive/src'),
|
||||
'As247\\CloudStorages\\' => array($vendorDir . '/as247/cloud-storages/src'),
|
||||
'App\\' => array($baseDir . '/app', $vendorDir . '/laravel/pint/app'),
|
||||
'App\\' => array($baseDir . '/app'),
|
||||
);
|
||||
|
||||
Vendored
+18
-2150
File diff suppressed because it is too large
Load Diff
Vendored
+8
-2411
File diff suppressed because it is too large
Load Diff
Vendored
+6
-321
@@ -3,11 +3,11 @@
|
||||
'name' => 'laravel/laravel',
|
||||
'pretty_version' => 'dev-main',
|
||||
'version' => 'dev-main',
|
||||
'reference' => '3471befb1af62975088d115a46aa180e13652a0d',
|
||||
'reference' => '90093a086b35dce8edd97fce09d32d2be42853e8',
|
||||
'type' => 'project',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'dev' => true,
|
||||
'dev' => false,
|
||||
),
|
||||
'versions' => array(
|
||||
'as247/cloud-storages' => array(
|
||||
@@ -46,18 +46,6 @@
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'cordoval/hamcrest-php' => array(
|
||||
'dev_requirement' => true,
|
||||
'replaced' => array(
|
||||
0 => '*',
|
||||
),
|
||||
),
|
||||
'davedevelopment/hamcrest-php' => array(
|
||||
'dev_requirement' => true,
|
||||
'replaced' => array(
|
||||
0 => '*',
|
||||
),
|
||||
),
|
||||
'dflydev/dot-access-data' => array(
|
||||
'pretty_version' => 'v3.0.3',
|
||||
'version' => '3.0.3.0',
|
||||
@@ -121,24 +109,6 @@
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'fakerphp/faker' => array(
|
||||
'pretty_version' => 'v1.24.1',
|
||||
'version' => '1.24.1.0',
|
||||
'reference' => 'e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../fakerphp/faker',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'filp/whoops' => array(
|
||||
'pretty_version' => '2.18.4',
|
||||
'version' => '2.18.4.0',
|
||||
'reference' => 'd2102955e48b9fd9ab24280a7ad12ed552752c4d',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../filp/whoops',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'firebase/php-jwt' => array(
|
||||
'pretty_version' => 'v7.0.5',
|
||||
'version' => '7.0.5.0',
|
||||
@@ -229,15 +199,6 @@
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'hamcrest/hamcrest-php' => array(
|
||||
'pretty_version' => 'v2.1.1',
|
||||
'version' => '2.1.1.0',
|
||||
'reference' => 'f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../hamcrest/hamcrest-php',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'illuminate/auth' => array(
|
||||
'dev_requirement' => false,
|
||||
'replaced' => array(
|
||||
@@ -454,21 +415,6 @@
|
||||
0 => 'v13.8.0',
|
||||
),
|
||||
),
|
||||
'kodova/hamcrest-php' => array(
|
||||
'dev_requirement' => true,
|
||||
'replaced' => array(
|
||||
0 => '*',
|
||||
),
|
||||
),
|
||||
'laravel/agent-detector' => array(
|
||||
'pretty_version' => 'v2.0.2',
|
||||
'version' => '2.0.2.0',
|
||||
'reference' => '90694b9256099591cf9e55d08c18ba7a00bf099f',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../laravel/agent-detector',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'laravel/framework' => array(
|
||||
'pretty_version' => 'v13.8.0',
|
||||
'version' => '13.8.0.0',
|
||||
@@ -481,39 +427,12 @@
|
||||
'laravel/laravel' => array(
|
||||
'pretty_version' => 'dev-main',
|
||||
'version' => 'dev-main',
|
||||
'reference' => '3471befb1af62975088d115a46aa180e13652a0d',
|
||||
'reference' => '90093a086b35dce8edd97fce09d32d2be42853e8',
|
||||
'type' => 'project',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'laravel/pail' => array(
|
||||
'pretty_version' => 'v1.2.6',
|
||||
'version' => '1.2.6.0',
|
||||
'reference' => 'aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../laravel/pail',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'laravel/pao' => array(
|
||||
'pretty_version' => 'v1.0.6',
|
||||
'version' => '1.0.6.0',
|
||||
'reference' => '02f62a64c2b60af44a418ee490fee193590d8269',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../laravel/pao',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'laravel/pint' => array(
|
||||
'pretty_version' => 'v1.29.1',
|
||||
'version' => '1.29.1.0',
|
||||
'reference' => '0770e9b7fafd50d4586881d456d6eb41c9247a80',
|
||||
'type' => 'project',
|
||||
'install_path' => __DIR__ . '/../laravel/pint',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'laravel/prompts' => array(
|
||||
'pretty_version' => 'v0.3.17',
|
||||
'version' => '0.3.17.0',
|
||||
@@ -613,15 +532,6 @@
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'mockery/mockery' => array(
|
||||
'pretty_version' => '1.6.12',
|
||||
'version' => '1.6.12.0',
|
||||
'reference' => '1f4efdd7d3beafe9807b08156dfcb176d18f1699',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../mockery/mockery',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'monolog/monolog' => array(
|
||||
'pretty_version' => '3.10.0',
|
||||
'version' => '3.10.0.0',
|
||||
@@ -637,15 +547,6 @@
|
||||
0 => '^1.0',
|
||||
),
|
||||
),
|
||||
'myclabs/deep-copy' => array(
|
||||
'pretty_version' => '1.13.4',
|
||||
'version' => '1.13.4.0',
|
||||
'reference' => '07d290f0c47959fd5eed98c95ee5602db07e0b6a',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../myclabs/deep-copy',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'nesbot/carbon' => array(
|
||||
'pretty_version' => '3.11.4',
|
||||
'version' => '3.11.4.0',
|
||||
@@ -682,15 +583,6 @@
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'nunomaduro/collision' => array(
|
||||
'pretty_version' => 'v8.9.4',
|
||||
'version' => '8.9.4.0',
|
||||
'reference' => '716af8f95a470e9094cfca09ed897b023be191a5',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../nunomaduro/collision',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'nunomaduro/termwind' => array(
|
||||
'pretty_version' => 'v2.4.0',
|
||||
'version' => '2.4.0.0',
|
||||
@@ -700,24 +592,6 @@
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'phar-io/manifest' => array(
|
||||
'pretty_version' => '2.0.4',
|
||||
'version' => '2.0.4.0',
|
||||
'reference' => '54750ef60c58e43759730615a392c31c80e23176',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../phar-io/manifest',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'phar-io/version' => array(
|
||||
'pretty_version' => '3.2.1',
|
||||
'version' => '3.2.1.0',
|
||||
'reference' => '4f7fd7836c6f332bb2933569e566a0d6c4cbed74',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../phar-io/version',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'php-di/invoker' => array(
|
||||
'pretty_version' => '2.3.7',
|
||||
'version' => '2.3.7.0',
|
||||
@@ -745,60 +619,6 @@
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'phpunit/php-code-coverage' => array(
|
||||
'pretty_version' => '12.5.6',
|
||||
'version' => '12.5.6.0',
|
||||
'reference' => '876099a072646c7745f673d7aeab5382c4439691',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../phpunit/php-code-coverage',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'phpunit/php-file-iterator' => array(
|
||||
'pretty_version' => '6.0.1',
|
||||
'version' => '6.0.1.0',
|
||||
'reference' => '3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../phpunit/php-file-iterator',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'phpunit/php-invoker' => array(
|
||||
'pretty_version' => '6.0.0',
|
||||
'version' => '6.0.0.0',
|
||||
'reference' => '12b54e689b07a25a9b41e57736dfab6ec9ae5406',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../phpunit/php-invoker',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'phpunit/php-text-template' => array(
|
||||
'pretty_version' => '5.0.0',
|
||||
'version' => '5.0.0.0',
|
||||
'reference' => 'e1367a453f0eda562eedb4f659e13aa900d66c53',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../phpunit/php-text-template',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'phpunit/php-timer' => array(
|
||||
'pretty_version' => '8.0.0',
|
||||
'version' => '8.0.0.0',
|
||||
'reference' => 'f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../phpunit/php-timer',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'phpunit/phpunit' => array(
|
||||
'pretty_version' => '12.5.24',
|
||||
'version' => '12.5.24.0',
|
||||
'reference' => 'd75dd30597caa80e72fad2ef7904601a30ef1046',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../phpunit/phpunit',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'psr/cache' => array(
|
||||
'pretty_version' => '3.0.0',
|
||||
'version' => '3.0.0.0',
|
||||
@@ -1010,9 +830,9 @@
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'sabre/vobject' => array(
|
||||
'pretty_version' => '4.5.8',
|
||||
'version' => '4.5.8.0',
|
||||
'reference' => 'd554eb24d64232922e1eab5896cc2f84b3b9ffb1',
|
||||
'pretty_version' => '4.6.0',
|
||||
'version' => '4.6.0.0',
|
||||
'reference' => '9432544fc369851fb8202c5d91159b2e669f0c88',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../sabre/vobject',
|
||||
'aliases' => array(),
|
||||
@@ -1027,123 +847,6 @@
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'sebastian/cli-parser' => array(
|
||||
'pretty_version' => '4.2.0',
|
||||
'version' => '4.2.0.0',
|
||||
'reference' => '90f41072d220e5c40df6e8635f5dafba2d9d4d04',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../sebastian/cli-parser',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'sebastian/comparator' => array(
|
||||
'pretty_version' => '7.1.6',
|
||||
'version' => '7.1.6.0',
|
||||
'reference' => 'c769009dee98f494e0edc3fd4f4087501688f11e',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../sebastian/comparator',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'sebastian/complexity' => array(
|
||||
'pretty_version' => '5.0.0',
|
||||
'version' => '5.0.0.0',
|
||||
'reference' => 'bad4316aba5303d0221f43f8cee37eb58d384bbb',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../sebastian/complexity',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'sebastian/diff' => array(
|
||||
'pretty_version' => '7.0.0',
|
||||
'version' => '7.0.0.0',
|
||||
'reference' => '7ab1ea946c012266ca32390913653d844ecd085f',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../sebastian/diff',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'sebastian/environment' => array(
|
||||
'pretty_version' => '8.1.0',
|
||||
'version' => '8.1.0.0',
|
||||
'reference' => 'b121608b28a13f721e76ffbbd386d08eff58f3f6',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../sebastian/environment',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'sebastian/exporter' => array(
|
||||
'pretty_version' => '7.0.2',
|
||||
'version' => '7.0.2.0',
|
||||
'reference' => '016951ae10980765e4e7aee491eb288c64e505b7',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../sebastian/exporter',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'sebastian/global-state' => array(
|
||||
'pretty_version' => '8.0.2',
|
||||
'version' => '8.0.2.0',
|
||||
'reference' => 'ef1377171613d09edd25b7816f05be8313f9115d',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../sebastian/global-state',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'sebastian/lines-of-code' => array(
|
||||
'pretty_version' => '4.0.0',
|
||||
'version' => '4.0.0.0',
|
||||
'reference' => '97ffee3bcfb5805568d6af7f0f893678fc076d2f',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../sebastian/lines-of-code',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'sebastian/object-enumerator' => array(
|
||||
'pretty_version' => '7.0.0',
|
||||
'version' => '7.0.0.0',
|
||||
'reference' => '1effe8e9b8e068e9ae228e542d5d11b5d16db894',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../sebastian/object-enumerator',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'sebastian/object-reflector' => array(
|
||||
'pretty_version' => '5.0.0',
|
||||
'version' => '5.0.0.0',
|
||||
'reference' => '4bfa827c969c98be1e527abd576533293c634f6a',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../sebastian/object-reflector',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'sebastian/recursion-context' => array(
|
||||
'pretty_version' => '7.0.1',
|
||||
'version' => '7.0.1.0',
|
||||
'reference' => '0b01998a7d5b1f122911a66bebcb8d46f0c82d8c',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../sebastian/recursion-context',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'sebastian/type' => array(
|
||||
'pretty_version' => '6.0.3',
|
||||
'version' => '6.0.3.0',
|
||||
'reference' => 'e549163b9760b8f71f191651d22acf32d56d6d4d',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../sebastian/type',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'sebastian/version' => array(
|
||||
'pretty_version' => '6.0.0',
|
||||
'version' => '6.0.0.0',
|
||||
'reference' => '3e6ccf7657d4f0a59200564b08cead899313b53c',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../sebastian/version',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'spatie/laravel-package-tools' => array(
|
||||
'pretty_version' => '1.93.0',
|
||||
'version' => '1.93.0.0',
|
||||
@@ -1168,15 +871,6 @@
|
||||
0 => '*',
|
||||
),
|
||||
),
|
||||
'staabm/side-effects-detector' => array(
|
||||
'pretty_version' => '1.0.5',
|
||||
'version' => '1.0.5.0',
|
||||
'reference' => 'd8334211a140ce329c13726d4a715adbddd0a163',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../staabm/side-effects-detector',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'symfony/clock' => array(
|
||||
'pretty_version' => 'v8.0.8',
|
||||
'version' => '8.0.8.0',
|
||||
@@ -1468,15 +1162,6 @@
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'theseer/tokenizer' => array(
|
||||
'pretty_version' => '2.0.1',
|
||||
'version' => '2.0.1.0',
|
||||
'reference' => '7989e43bf381af0eac72e4f0ca5bcbfa81658be4',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../theseer/tokenizer',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'tijsverkoyen/css-to-inline-styles' => array(
|
||||
'pretty_version' => 'v2.4.0',
|
||||
'version' => '2.4.0.0',
|
||||
|
||||
Vendored
-209
@@ -1,209 +0,0 @@
|
||||
# CHANGELOG
|
||||
|
||||
## [Unreleased](https://github.com/FakerPHP/Faker/compare/v1.24.0...1.24.1)
|
||||
|
||||
- Removed domain `gmail.com.au` from `Provider\en_AU\Internet` (#886)
|
||||
|
||||
## [2024-11-09, v1.24.0](https://github.com/FakerPHP/Faker/compare/v1.23.1..v1.24.0)
|
||||
|
||||
- Fix internal deprecations in Doctrine's populator by @gnutix in (#889)
|
||||
- Fix mobile phone number pattern for France by @ker0x in (#859)
|
||||
- PHP 8.4 Support by @Jubeki in (#904)
|
||||
|
||||
- Added support for PHP 8.4 (#904)
|
||||
|
||||
## [2023-09-29, v1.23.1](https://github.com/FakerPHP/Faker/compare/v1.23.0..v1.23.1)
|
||||
|
||||
- Fixed double `а` female lastName in `ru_RU/Person::name()` (#832)
|
||||
- Fixed polish license plates (#685)
|
||||
- Stopped using `static` in callables in `Provider\pt_BR\PhoneNumber` (#785)
|
||||
- Fixed incorrect female name (#794)
|
||||
- Stopped using the deprecated `MT_RAND_PHP` constant to seed the random generator on PHP 8.3 (#844)
|
||||
|
||||
## [2023-06-12, v1.23.0](https://github.com/FakerPHP/Faker/compare/v1.22.0..v1.23.0)
|
||||
|
||||
- Update `randomElements` to return random number of elements when no count is provided (#658)
|
||||
|
||||
## [2023-05-14, v1.22.0](https://github.com/FakerPHP/Faker/compare/v1.21.0..v1.22.0)
|
||||
|
||||
- Fixed `randomElements()` to accept empty iterator (#605)
|
||||
- Added support for passing an `Enum` to `randomElement()` and `randomElements()` (#620)
|
||||
- Started rejecting invalid arguments passed to `randomElement()` and `randomElements()` (#642)
|
||||
|
||||
## [2022-12-13, v1.21.0](https://github.com/FakerPHP/Faker/compare/v1.20.0..v1.21.0)
|
||||
|
||||
- Dropped support for PHP 7.1, 7.2, and 7.3 (#543)
|
||||
- Added support for PHP 8.2 (#528)
|
||||
|
||||
## [2022-07-20, v1.20.0](https://github.com/FakerPHP/Faker/compare/v1.19.0..v1.20.0)
|
||||
|
||||
- Fixed typo in French phone number (#452)
|
||||
- Fixed some Hungarian naming bugs (#451)
|
||||
- Fixed bug where the NL-BE VAT generation was incorrect (#455)
|
||||
- Improve Turkish phone numbers for E164 and added landline support (#460)
|
||||
- Add Microsoft Edge User Agent (#464)
|
||||
- Added option to set image formats on Faker\Provider\Image (#473)
|
||||
- Added support for French color translations (#466)
|
||||
- Support filtering timezones by country code (#480)
|
||||
- Fixed typo in some greek names (#490)
|
||||
- Marked the Faker\Provider\Image as deprecated
|
||||
|
||||
## [2022-02-02, v1.19.0](https://github.com/FakerPHP/Faker/compare/v1.18.0..v1.19.0)
|
||||
|
||||
- Added color extension to core (#442)
|
||||
- Added conflict with `doctrine/persistence` below version `1.4`
|
||||
- Fix for support on different Doctrine ORM versions (#414)
|
||||
- Fix usage of `Doctrine\Persistence` dependency
|
||||
- Fix CZ Person birthNumber docblock return type (#437)
|
||||
- Fix is_IS Person docbock types (#439)
|
||||
- Fix is_IS Address docbock type (#438)
|
||||
- Fix regexify escape backslash in character class (#434)
|
||||
- Removed UUID from Generator to be able to extend it (#441)
|
||||
|
||||
## [2022-01-23, v1.18.0](https://github.com/FakerPHP/Faker/compare/v1.17.0..v1.18.0)
|
||||
|
||||
- Deprecated UUID, use uuid3 to specify version (#427)
|
||||
- Reset formatters when adding a new provider (#366)
|
||||
- Helper methods to use our custom generators (#155)
|
||||
- Set allow-plugins for Composer 2.2 (#405)
|
||||
- Fix kk_KZ\Person::individualIdentificationNumber generation (#411)
|
||||
- Allow for -> syntax to be used in parsing (#423)
|
||||
- Person->name was missing string return type (#424)
|
||||
- Generate a valid BE TAX number (#415)
|
||||
- Added the UUID extension to Core (#427)
|
||||
|
||||
## [2021-12-05, v1.17.0](https://github.com/FakerPHP/Faker/compare/v1.16.0..v1.17.0)
|
||||
|
||||
- Partial PHP 8.1 compatibility (#373)
|
||||
- Add payment provider for `ne_NP` locale (#375)
|
||||
- Add Egyptian Arabic `ar_EG` locale (#377)
|
||||
- Updated list of South African TLDs (#383)
|
||||
- Fixed formatting of E.164 numbers (#380)
|
||||
- Allow `symfony/deprecation-contracts` `^3.0` (#397)
|
||||
|
||||
## [2021-09-06, v1.16.0](https://github.com/FakerPHP/Faker/compare/v1.15.0..v1.16.0)
|
||||
|
||||
- Add Company extension
|
||||
- Add Address extension
|
||||
- Add Person extension
|
||||
- Add PhoneNumber extension
|
||||
- Add VersionExtension (#350)
|
||||
- Stricter types in Extension\Container and Extension\GeneratorAwareExtension (#345)
|
||||
- Fix deprecated property access in `nl_NL` (#348)
|
||||
- Add support for `psr/container` >= 2.0 (#354)
|
||||
- Add missing union types in Faker\Generator (#352)
|
||||
|
||||
## [2021-07-06, v1.15.0](https://github.com/FakerPHP/Faker/compare/v1.14.1..v1.15.0)
|
||||
|
||||
- Updated the generator phpdoc to help identify magic methods (#307)
|
||||
- Prevent direct access and triggered deprecation warning for "word" (#302)
|
||||
- Updated length on all global e164 numbers (#301)
|
||||
- Updated last names from different source (#312)
|
||||
- Don't generate birth number of '000' for Swedish personal identity (#306)
|
||||
- Add job list for localization id_ID (#339)
|
||||
|
||||
## [2021-03-30, v1.14.1](https://github.com/FakerPHP/Faker/compare/v1.14.0..v1.14.1)
|
||||
|
||||
- Fix where randomNumber and randomFloat would return a 0 value (#291 / #292)
|
||||
|
||||
## [2021-03-29, v1.14.0](https://github.com/FakerPHP/Faker/compare/v1.13.0..v1.14.0)
|
||||
|
||||
- Fix for realText to ensure the text keeps closer to its boundaries (#152)
|
||||
- Fix where regexify produces a random character instead of a literal dot (#135
|
||||
- Deprecate zh_TW methods that only call base methods (#122)
|
||||
- Add used extensions to composer.json as suggestion (#120)
|
||||
- Moved TCNo and INN from calculator to localized providers (#108)
|
||||
- Fix regex dot/backslash issue where a dot is replaced with a backslash as escape character (#206)
|
||||
- Deprecate direct property access (#164)
|
||||
- Added test to assert unique() behaviour (#233)
|
||||
- Added RUC for the es_PE locale (#244)
|
||||
- Test IBAN formats for Latin America (AR/PE/VE) (#260)
|
||||
- Added VAT number for en_GB (#255)
|
||||
- Added new districts for the ne_NP locale (#258)
|
||||
- Fix for U.S. Area Code Generation (#261)
|
||||
- Fix in numerify where a better random numeric value is guaranteed (#256)
|
||||
- Fix e164PhoneNumber to only generate valid phone numbers with valid country codes (#264)
|
||||
- Extract fixtures into separate classes (#234)
|
||||
- Remove french domains that no longer exists (#277)
|
||||
- Fix error that occurs when getting a polish title (#279)
|
||||
- Use valid area codes for North America E164 phone numbers (#280)
|
||||
|
||||
- Adding support for extensions and PSR-11 (#154)
|
||||
- Adding trait for GeneratorAwareExtension (#165)
|
||||
- Added helper class for extension (#162)
|
||||
- Added blood extension to core (#232)
|
||||
- Added barcode extension to core (#252)
|
||||
- Added number extension (#257)
|
||||
|
||||
- Various code style updates
|
||||
- Added a note about our breaking change promise (#273)
|
||||
|
||||
## [2020-12-18, v1.13.0](https://github.com/FakerPHP/Faker/compare/v1.12.1..v1.13.0)
|
||||
|
||||
Several fixes and new additions in this release. A lot of cleanup has been done
|
||||
on the codebase on both tests and consistency.
|
||||
|
||||
- Feature/pl pl license plate (#62)
|
||||
- Fix greek phone numbers (#16)
|
||||
- Move AT payment provider logic to de_AT (#72)
|
||||
- Fix wiktionary links (#73)
|
||||
- Fix AT person links (#74)
|
||||
- Fix AT cities (#75)
|
||||
- Deprecate at_AT providers (#78)
|
||||
- Add Austrian `ssn()` to `Person` provider (#79)
|
||||
- Fix typos in id_ID Address (#83)
|
||||
- Austrian post codes (#86)
|
||||
- Updated Polish data (#70)
|
||||
- Improve Austrian social security number generation (#88)
|
||||
- Move US phone numbers with extension to own method (#91)
|
||||
- Add UK National Insurance number generator (#89)
|
||||
- Fix en_SG phone number generator (#100)
|
||||
- Remove usage of mt_rand (#87)
|
||||
- Remove whitespace from beginning of el_GR phone numbers (#105)
|
||||
- Building numbers can not be 0, 00, 000 (#107)
|
||||
- Add 172.16/12 local IPv4 block (#121)
|
||||
- Add JCB credit card type (#124)
|
||||
- Remove json_decode from emoji generation (#123)
|
||||
- Remove ro street address (#146)
|
||||
|
||||
## [2020-12-11, v1.12.1](https://github.com/FakerPHP/Faker/compare/v1.12.0..v1.12.1)
|
||||
|
||||
This is a security release that prevents a hacker to execute code on the server.
|
||||
|
||||
## [2020-11-23, v1.12.0](https://github.com/FakerPHP/Faker/compare/v1.11.0..v1.12.0)
|
||||
|
||||
- Fix ro_RO first and last day of year calculation offset (#65)
|
||||
- Fix en_NG locale test namespaces that did not match PSR-4 (#57)
|
||||
- Added Singapore NRIC/FIN provider (#56)
|
||||
- Added provider for Lithuanian municipalities (#58)
|
||||
- Added blood types provider (#61)
|
||||
|
||||
## [2020-11-15, v1.11.0](https://github.com/FakerPHP/Faker/compare/v1.10.1..v1.11.0)
|
||||
|
||||
- Added Provider for Swedish Municipalities
|
||||
- Updates to person names in pt_BR
|
||||
- Many code style changes
|
||||
|
||||
## [2020-10-28, v1.10.1](https://github.com/FakerPHP/Faker/compare/v1.10.0..v1.10.1)
|
||||
|
||||
- Updates the Danish addresses in dk_DK
|
||||
- Removed offense company names in nl_NL
|
||||
- Clarify changelog with original fork
|
||||
- Standin replacement for LoremPixel to Placeholder.com (#11)
|
||||
|
||||
## [2020-10-27, v1.10.0](https://github.com/FakerPHP/Faker/compare/v1.9.1..v1.10.0)
|
||||
|
||||
- Support PHP 7.1-8.0
|
||||
- Fix typo in de_DE Company Provider
|
||||
- Fix dateTimeThisYear method
|
||||
- Fix typo in de_DE jobTitleFormat
|
||||
- Fix IBAN generation for CR
|
||||
- Fix typos in greek first names
|
||||
- Fix US job title typo
|
||||
- Do not clear entity manager for doctrine orm populator
|
||||
- Remove persian rude words
|
||||
- Corrections to RU names
|
||||
|
||||
## 2020-10-27, v1.9.1
|
||||
|
||||
- Initial version. Same as `fzaninotto/Faker:v1.9.1`.
|
||||
Vendored
-22
@@ -1,22 +0,0 @@
|
||||
Copyright (c) 2011 François Zaninotto
|
||||
Portions Copyright (c) 2008 Caius Durling
|
||||
Portions Copyright (c) 2008 Adam Royle
|
||||
Portions Copyright (c) 2008 Fiona Burrows
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
Vendored
-114
@@ -1,114 +0,0 @@
|
||||
<p style="text-align: center"><img src="https://github.com/FakerPHP/Artwork/raw/main/src/socialcard.png" alt="Social card of FakerPHP"></p>
|
||||
|
||||
# Faker
|
||||
|
||||
[](https://packagist.org/packages/fakerphp/faker)
|
||||
[](https://github.com/FakerPHP/Faker/actions)
|
||||
[](https://shepherd.dev/github/FakerPHP/Faker)
|
||||
[](https://codecov.io/gh/FakerPHP/Faker)
|
||||
|
||||
Faker is a PHP library that generates fake data for you. Whether you need to bootstrap your database, create good-looking XML documents, fill-in your persistence to stress test it, or anonymize data taken from a production service, Faker is for you.
|
||||
|
||||
It's heavily inspired by Perl's [Data::Faker](https://metacpan.org/pod/Data::Faker), and by Ruby's [Faker](https://rubygems.org/gems/faker).
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Installation
|
||||
|
||||
Faker requires PHP >= 7.4.
|
||||
|
||||
```shell
|
||||
composer require fakerphp/faker
|
||||
```
|
||||
|
||||
### Documentation
|
||||
|
||||
Full documentation can be found over on [fakerphp.github.io](https://fakerphp.github.io).
|
||||
|
||||
### Basic Usage
|
||||
|
||||
Use `Faker\Factory::create()` to create and initialize a Faker generator, which can generate data by accessing methods named after the type of data you want.
|
||||
|
||||
```php
|
||||
<?php
|
||||
require_once 'vendor/autoload.php';
|
||||
|
||||
// use the factory to create a Faker\Generator instance
|
||||
$faker = Faker\Factory::create();
|
||||
// generate data by calling methods
|
||||
echo $faker->name();
|
||||
// 'Vince Sporer'
|
||||
echo $faker->email();
|
||||
// 'walter.sophia@hotmail.com'
|
||||
echo $faker->text();
|
||||
// 'Numquam ut mollitia at consequuntur inventore dolorem.'
|
||||
```
|
||||
|
||||
Each call to `$faker->name()` yields a different (random) result. This is because Faker uses `__call()` magic, and forwards `Faker\Generator->$method()` calls to `Faker\Generator->format($method, $attributes)`.
|
||||
|
||||
```php
|
||||
<?php
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
echo $faker->name() . "\n";
|
||||
}
|
||||
|
||||
// 'Cyrus Boyle'
|
||||
// 'Alena Cummerata'
|
||||
// 'Orlo Bergstrom'
|
||||
```
|
||||
|
||||
## Automated refactoring
|
||||
|
||||
If you already used this library with its properties, they are now deprecated and needs to be replaced by their equivalent methods.
|
||||
|
||||
You can use the provided [Rector](https://github.com/rectorphp/rector) config file to automate the work.
|
||||
|
||||
Run
|
||||
|
||||
```bash
|
||||
composer require --dev rector/rector
|
||||
```
|
||||
|
||||
to install `rector/rector`.
|
||||
|
||||
Run
|
||||
|
||||
```bash
|
||||
vendor/bin/rector process src/ --config vendor/fakerphp/faker/rector-migrate.php
|
||||
```
|
||||
|
||||
to run `rector/rector`.
|
||||
|
||||
*Note:* do not forget to replace `src/` with the path to your source directory.
|
||||
|
||||
Alternatively, import the configuration in your `rector.php` file:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Rector\Config;
|
||||
|
||||
return static function (Config\RectorConfig $rectorConfig): void {
|
||||
$rectorConfig->import('vendor/fakerphp/faker/rector-migrate.php');
|
||||
};
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Faker is released under the MIT License. See [`LICENSE`](LICENSE) for details.
|
||||
|
||||
## Backward compatibility promise
|
||||
|
||||
Faker is using [Semver](https://semver.org/). This means that versions are tagged
|
||||
with MAJOR.MINOR.PATCH. Only a new major version will be allowed to break backward
|
||||
compatibility (BC).
|
||||
|
||||
Classes marked as `@experimental` or `@internal` are not included in our backward compatibility promise.
|
||||
You are also not guaranteed that the value returned from a method is always the
|
||||
same. You are guaranteed that the data type will not change.
|
||||
|
||||
PHP 8 introduced [named arguments](https://wiki.php.net/rfc/named_params), which
|
||||
increased the cost and reduces flexibility for package maintainers. The names of the
|
||||
arguments for methods in Faker is not included in our BC promise.
|
||||
Vendored
-56
@@ -1,56 +0,0 @@
|
||||
{
|
||||
"name": "fakerphp/faker",
|
||||
"type": "library",
|
||||
"description": "Faker is a PHP library that generates fake data for you.",
|
||||
"keywords": [
|
||||
"faker",
|
||||
"fixtures",
|
||||
"data"
|
||||
],
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "François Zaninotto"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0",
|
||||
"psr/container": "^1.0 || ^2.0",
|
||||
"symfony/deprecation-contracts": "^2.2 || ^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-intl": "*",
|
||||
"bamarni/composer-bin-plugin": "^1.4.1",
|
||||
"doctrine/persistence": "^1.3 || ^2.0",
|
||||
"phpunit/phpunit": "^9.5.26",
|
||||
"symfony/phpunit-bridge": "^5.4.16"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Faker\\": "src/Faker/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Faker\\Test\\": "test/Faker/",
|
||||
"Faker\\Test\\Fixture\\": "test/Fixture/"
|
||||
}
|
||||
},
|
||||
"conflict": {
|
||||
"fzaninotto/faker": "*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-curl": "Required by Faker\\Provider\\Image to download images.",
|
||||
"ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.",
|
||||
"ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.",
|
||||
"ext-mbstring": "Required for multibyte Unicode string functionality.",
|
||||
"doctrine/orm": "Required to use Faker\\ORM\\Doctrine"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"bamarni/composer-bin-plugin": true,
|
||||
"composer/package-versions-deprecated": true
|
||||
},
|
||||
"sort-packages": true
|
||||
}
|
||||
}
|
||||
-161
@@ -1,161 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Faker\Generator;
|
||||
use Rector\Config;
|
||||
use Rector\Transform;
|
||||
|
||||
// This file configures rector/rector to replace all deprecated property usages with their equivalent functions.
|
||||
return static function (Config\RectorConfig $rectorConfig): void {
|
||||
$properties = [
|
||||
'address',
|
||||
'amPm',
|
||||
'asciify',
|
||||
'biasedNumberBetween',
|
||||
'boolean',
|
||||
'bothify',
|
||||
'buildingNumber',
|
||||
'century',
|
||||
'chrome',
|
||||
'city',
|
||||
'citySuffix',
|
||||
'colorName',
|
||||
'company',
|
||||
'companyEmail',
|
||||
'companySuffix',
|
||||
'country',
|
||||
'countryCode',
|
||||
'countryISOAlpha3',
|
||||
'creditCardDetails',
|
||||
'creditCardExpirationDate',
|
||||
'creditCardExpirationDateString',
|
||||
'creditCardNumber',
|
||||
'creditCardType',
|
||||
'currencyCode',
|
||||
'date',
|
||||
'dateTime',
|
||||
'dateTimeAD',
|
||||
'dateTimeBetween',
|
||||
'dateTimeInInterval',
|
||||
'dateTimeThisCentury',
|
||||
'dateTimeThisDecade',
|
||||
'dateTimeThisMonth',
|
||||
'dateTimeThisYear',
|
||||
'dayOfMonth',
|
||||
'dayOfWeek',
|
||||
'domainName',
|
||||
'domainWord',
|
||||
'e164PhoneNumber',
|
||||
'email',
|
||||
'emoji',
|
||||
'file',
|
||||
'firefox',
|
||||
'firstName',
|
||||
'firstNameFemale',
|
||||
'firstNameMale',
|
||||
'freeEmail',
|
||||
'freeEmailDomain',
|
||||
'getDefaultTimezone',
|
||||
'hexColor',
|
||||
'hslColor',
|
||||
'hslColorAsArray',
|
||||
'iban',
|
||||
'image',
|
||||
'imageUrl',
|
||||
'imei',
|
||||
'internetExplorer',
|
||||
'iosMobileToken',
|
||||
'ipv4',
|
||||
'ipv6',
|
||||
'iso8601',
|
||||
'jobTitle',
|
||||
'languageCode',
|
||||
'lastName',
|
||||
'latitude',
|
||||
'lexify',
|
||||
'linuxPlatformToken',
|
||||
'linuxProcessor',
|
||||
'localCoordinates',
|
||||
'localIpv4',
|
||||
'locale',
|
||||
'longitude',
|
||||
'macAddress',
|
||||
'macPlatformToken',
|
||||
'macProcessor',
|
||||
'md5',
|
||||
'month',
|
||||
'monthName',
|
||||
'msedge',
|
||||
'name',
|
||||
'numerify',
|
||||
'opera',
|
||||
'paragraph',
|
||||
'paragraphs',
|
||||
'passthrough',
|
||||
'password',
|
||||
'phoneNumber',
|
||||
'postcode',
|
||||
'randomAscii',
|
||||
'randomDigitNotNull',
|
||||
'randomElement',
|
||||
'randomElements',
|
||||
'randomHtml',
|
||||
'randomKey',
|
||||
'randomLetter',
|
||||
'realText',
|
||||
'realTextBetween',
|
||||
'regexify',
|
||||
'rgbColor',
|
||||
'rgbColorAsArray',
|
||||
'rgbCssColor',
|
||||
'rgbaCssColor',
|
||||
'safari',
|
||||
'safeColorName',
|
||||
'safeEmail',
|
||||
'safeEmailDomain',
|
||||
'safeHexColor',
|
||||
'sentence',
|
||||
'sentences',
|
||||
'setDefaultTimezone',
|
||||
'sha1',
|
||||
'sha256',
|
||||
'shuffle',
|
||||
'shuffleArray',
|
||||
'shuffleString',
|
||||
'slug',
|
||||
'streetAddress',
|
||||
'streetName',
|
||||
'streetSuffix',
|
||||
'swiftBicNumber',
|
||||
'text',
|
||||
'time',
|
||||
'timezone',
|
||||
'title',
|
||||
'titleFemale',
|
||||
'titleMale',
|
||||
'tld',
|
||||
'toLower',
|
||||
'toUpper',
|
||||
'unixTime',
|
||||
'url',
|
||||
'userAgent',
|
||||
'userName',
|
||||
'uuid',
|
||||
'windowsPlatformToken',
|
||||
'word',
|
||||
'words',
|
||||
'year',
|
||||
];
|
||||
|
||||
$rectorConfig->ruleWithConfiguration(
|
||||
Transform\Rector\Assign\PropertyFetchToMethodCallRector::class,
|
||||
array_map(static function (string $property): Transform\ValueObject\PropertyFetchToMethodCall {
|
||||
return new Transform\ValueObject\PropertyFetchToMethodCall(
|
||||
Generator::class,
|
||||
$property,
|
||||
$property,
|
||||
);
|
||||
}, $properties),
|
||||
);
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Faker\Calculator;
|
||||
|
||||
/**
|
||||
* Utility class for validating EAN-8 and EAN-13 numbers
|
||||
*/
|
||||
class Ean
|
||||
{
|
||||
/**
|
||||
* @var string EAN validation pattern
|
||||
*/
|
||||
public const PATTERN = '/^(?:\d{8}|\d{13})$/';
|
||||
|
||||
/**
|
||||
* Computes the checksum of an EAN number.
|
||||
*
|
||||
* @see https://en.wikipedia.org/wiki/International_Article_Number
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function checksum(string $digits)
|
||||
{
|
||||
$sequence = (strlen($digits) + 1) === 8 ? [3, 1] : [1, 3];
|
||||
$sums = 0;
|
||||
|
||||
foreach (str_split($digits) as $n => $digit) {
|
||||
$sums += ((int) $digit) * $sequence[$n % 2];
|
||||
}
|
||||
|
||||
return (10 - $sums % 10) % 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the provided number is an EAN compliant number and that
|
||||
* the checksum is correct.
|
||||
*
|
||||
* @param string $ean An EAN number
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isValid(string $ean)
|
||||
{
|
||||
if (!preg_match(self::PATTERN, $ean)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::checksum(substr($ean, 0, -1)) === (int) substr($ean, -1);
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Faker\Calculator;
|
||||
|
||||
class Iban
|
||||
{
|
||||
/**
|
||||
* Generates IBAN Checksum
|
||||
*
|
||||
* @return string Checksum (numeric string)
|
||||
*/
|
||||
public static function checksum(string $iban)
|
||||
{
|
||||
// Move first four digits to end and set checksum to '00'
|
||||
$checkString = substr($iban, 4) . substr($iban, 0, 2) . '00';
|
||||
|
||||
// Replace all letters with their number equivalents
|
||||
$checkString = preg_replace_callback(
|
||||
'/[A-Z]/',
|
||||
static function (array $matches): string {
|
||||
return (string) self::alphaToNumber($matches[0]);
|
||||
},
|
||||
$checkString,
|
||||
);
|
||||
|
||||
// Perform mod 97 and subtract from 98
|
||||
$checksum = 98 - self::mod97($checkString);
|
||||
|
||||
return str_pad($checksum, 2, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts letter to number
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function alphaToNumber(string $char)
|
||||
{
|
||||
return ord($char) - 55;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates mod97 on a numeric string
|
||||
*
|
||||
* @param string $number Numeric string
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function mod97(string $number)
|
||||
{
|
||||
$checksum = (int) $number[0];
|
||||
|
||||
for ($i = 1, $size = strlen($number); $i < $size; ++$i) {
|
||||
$checksum = (10 * $checksum + (int) $number[$i]) % 97;
|
||||
}
|
||||
|
||||
return $checksum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether an IBAN has a valid checksum
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isValid(string $iban)
|
||||
{
|
||||
return self::checksum($iban) === substr($iban, 2, 2);
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Faker\Calculator;
|
||||
|
||||
/**
|
||||
* @deprecated moved to ru_RU\Company, use {@link \Faker\Provider\ru_RU\Company}.
|
||||
* @see \Faker\Provider\ru_RU\Company
|
||||
*/
|
||||
class Inn
|
||||
{
|
||||
/**
|
||||
* Generates INN Checksum
|
||||
*
|
||||
* https://ru.wikipedia.org/wiki/%D0%98%D0%B4%D0%B5%D0%BD%D1%82%D0%B8%D1%84%D0%B8%D0%BA%D0%B0%D1%86%D0%B8%D0%BE%D0%BD%D0%BD%D1%8B%D0%B9_%D0%BD%D0%BE%D0%BC%D0%B5%D1%80_%D0%BD%D0%B0%D0%BB%D0%BE%D0%B3%D0%BE%D0%BF%D0%BB%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%D1%89%D0%B8%D0%BA%D0%B0
|
||||
*
|
||||
* @param string $inn
|
||||
*
|
||||
* @return string Checksum (one digit)
|
||||
*
|
||||
* @deprecated use {@link \Faker\Provider\ru_RU\Company::inn10Checksum()} instead
|
||||
* @see \Faker\Provider\ru_RU\Company::inn10Checksum()
|
||||
*/
|
||||
public static function checksum($inn)
|
||||
{
|
||||
return \Faker\Provider\ru_RU\Company::inn10Checksum($inn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether an INN has a valid checksum
|
||||
*
|
||||
* @param string $inn
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @deprecated use {@link \Faker\Provider\ru_RU\Company::inn10IsValid()} instead
|
||||
* @see \Faker\Provider\ru_RU\Company::inn10IsValid()
|
||||
*/
|
||||
public static function isValid($inn)
|
||||
{
|
||||
return \Faker\Provider\ru_RU\Company::inn10IsValid($inn);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Faker\Calculator;
|
||||
|
||||
/**
|
||||
* Utility class for validating ISBN-10
|
||||
*/
|
||||
class Isbn
|
||||
{
|
||||
/**
|
||||
* @var string ISBN-10 validation pattern
|
||||
*/
|
||||
public const PATTERN = '/^\d{9}[0-9X]$/';
|
||||
|
||||
/**
|
||||
* ISBN-10 check digit
|
||||
*
|
||||
* @see http://en.wikipedia.org/wiki/International_Standard_Book_Number#ISBN-10_check_digits
|
||||
*
|
||||
* @param string $input ISBN without check-digit
|
||||
*
|
||||
* @throws \LengthException When wrong input length passed
|
||||
*/
|
||||
public static function checksum(string $input): string
|
||||
{
|
||||
// We're calculating check digit for ISBN-10
|
||||
// so, the length of the input should be 9
|
||||
$length = 9;
|
||||
|
||||
if (strlen($input) !== $length) {
|
||||
throw new \LengthException(sprintf('Input length should be equal to %d', $length));
|
||||
}
|
||||
|
||||
$digits = str_split($input);
|
||||
array_walk(
|
||||
$digits,
|
||||
static function (&$digit, $position): void {
|
||||
$digit = (10 - $position) * $digit;
|
||||
},
|
||||
);
|
||||
$result = (11 - array_sum($digits) % 11) % 11;
|
||||
|
||||
// 10 is replaced by X
|
||||
return ($result < 10) ? (string) $result : 'X';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the provided number is a valid ISBN-10 number
|
||||
*
|
||||
* @param string $isbn ISBN to check
|
||||
*/
|
||||
public static function isValid(string $isbn): bool
|
||||
{
|
||||
if (!preg_match(self::PATTERN, $isbn)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::checksum(substr($isbn, 0, -1)) === substr($isbn, -1);
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Faker\Calculator;
|
||||
|
||||
/**
|
||||
* Utility class for generating and validating Luhn numbers.
|
||||
*
|
||||
* Luhn algorithm is used to validate credit card numbers, IMEI numbers, and
|
||||
* National Provider Identifier numbers.
|
||||
*
|
||||
* @see http://en.wikipedia.org/wiki/Luhn_algorithm
|
||||
*/
|
||||
class Luhn
|
||||
{
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
private static function checksum(string $number)
|
||||
{
|
||||
$number = (string) $number;
|
||||
$length = strlen($number);
|
||||
$sum = 0;
|
||||
|
||||
for ($i = $length - 1; $i >= 0; $i -= 2) {
|
||||
$sum += $number[$i];
|
||||
}
|
||||
|
||||
for ($i = $length - 2; $i >= 0; $i -= 2) {
|
||||
$sum += array_sum(str_split($number[$i] * 2));
|
||||
}
|
||||
|
||||
return $sum % 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public static function computeCheckDigit(string $partialNumber)
|
||||
{
|
||||
$checkDigit = self::checksum($partialNumber . '0');
|
||||
|
||||
if ($checkDigit === 0) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
return (string) (10 - $checkDigit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a number (partial number + check digit) is Luhn compliant
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isValid(string $number)
|
||||
{
|
||||
return self::checksum($number) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a Luhn compliant number.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function generateLuhnNumber(string $partialValue)
|
||||
{
|
||||
if (!preg_match('/^\d+$/', $partialValue)) {
|
||||
throw new \InvalidArgumentException('Argument should be an integer.');
|
||||
}
|
||||
|
||||
return $partialValue . Luhn::computeCheckDigit($partialValue);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Faker\Calculator;
|
||||
|
||||
/**
|
||||
* @deprecated moved to tr_TR\Person, use {@link \Faker\Provider\tr_TR\Person}.
|
||||
* @see \Faker\Provider\tr_TR\Person
|
||||
*/
|
||||
class TCNo
|
||||
{
|
||||
/**
|
||||
* Generates Turkish Identity Number Checksum
|
||||
* Gets first 9 digit as prefix and calculates checksum
|
||||
*
|
||||
* https://en.wikipedia.org/wiki/Turkish_Identification_Number
|
||||
*
|
||||
* @param string $identityPrefix
|
||||
*
|
||||
* @return string Checksum (two digit)
|
||||
*
|
||||
* @deprecated use {@link \Faker\Provider\tr_TR\Person::tcNoChecksum()} instead
|
||||
* @see \Faker\Provider\tr_TR\Person::tcNoChecksum()
|
||||
*/
|
||||
public static function checksum($identityPrefix)
|
||||
{
|
||||
return \Faker\Provider\tr_TR\Person::tcNoChecksum($identityPrefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a TCNo has a valid checksum
|
||||
*
|
||||
* @param string $tcNo
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @deprecated use {@link \Faker\Provider\tr_TR\Person::tcNoIsValid()} instead
|
||||
* @see \Faker\Provider\tr_TR\Person::tcNoIsValid()
|
||||
*/
|
||||
public static function isValid($tcNo)
|
||||
{
|
||||
return \Faker\Provider\tr_TR\Person::tcNoIsValid($tcNo);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Faker;
|
||||
|
||||
use Faker\Extension\Extension;
|
||||
|
||||
/**
|
||||
* This generator returns a default value for all called properties
|
||||
* and methods. It works with Faker\Generator::optional().
|
||||
*
|
||||
* @mixin Generator
|
||||
*/
|
||||
class ChanceGenerator
|
||||
{
|
||||
private $generator;
|
||||
private $weight;
|
||||
protected $default;
|
||||
|
||||
/**
|
||||
* @param Extension|Generator $generator
|
||||
*/
|
||||
public function __construct($generator, float $weight, $default = null)
|
||||
{
|
||||
$this->default = $default;
|
||||
$this->generator = $generator;
|
||||
$this->weight = $weight;
|
||||
}
|
||||
|
||||
public function ext(string $id)
|
||||
{
|
||||
return new self($this->generator->ext($id), $this->weight, $this->default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Catch and proxy all generator calls but return only valid values
|
||||
*
|
||||
* @param string $attribute
|
||||
*
|
||||
* @deprecated Use a method instead.
|
||||
*/
|
||||
public function __get($attribute)
|
||||
{
|
||||
trigger_deprecation('fakerphp/faker', '1.14', 'Accessing property "%s" is deprecated, use "%s()" instead.', $attribute, $attribute);
|
||||
|
||||
return $this->__call($attribute, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param array $arguments
|
||||
*/
|
||||
public function __call($name, $arguments)
|
||||
{
|
||||
if (mt_rand(1, 100) <= (100 * $this->weight)) {
|
||||
return call_user_func_array([$this->generator, $name], $arguments);
|
||||
}
|
||||
|
||||
return $this->default;
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Faker\Container;
|
||||
|
||||
use Faker\Extension\Extension;
|
||||
|
||||
/**
|
||||
* A simple implementation of a container.
|
||||
*
|
||||
* @experimental This class is experimental and does not fall under our BC promise
|
||||
*/
|
||||
final class Container implements ContainerInterface
|
||||
{
|
||||
/**
|
||||
* @var array<string, callable|object|string>
|
||||
*/
|
||||
private array $definitions;
|
||||
|
||||
private array $services = [];
|
||||
|
||||
/**
|
||||
* Create a container object with a set of definitions. The array value MUST
|
||||
* produce an object that implements Extension.
|
||||
*
|
||||
* @param array<string, callable|object|string> $definitions
|
||||
*/
|
||||
public function __construct(array $definitions)
|
||||
{
|
||||
$this->definitions = $definitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a definition from the container.
|
||||
*
|
||||
* @param string $id
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws \RuntimeException
|
||||
* @throws ContainerException
|
||||
* @throws NotInContainerException
|
||||
*/
|
||||
public function get($id): Extension
|
||||
{
|
||||
if (!is_string($id)) {
|
||||
throw new \InvalidArgumentException(sprintf(
|
||||
'First argument of %s::get() must be string',
|
||||
self::class,
|
||||
));
|
||||
}
|
||||
|
||||
if (array_key_exists($id, $this->services)) {
|
||||
return $this->services[$id];
|
||||
}
|
||||
|
||||
if (!$this->has($id)) {
|
||||
throw new NotInContainerException(sprintf(
|
||||
'There is not service with id "%s" in the container.',
|
||||
$id,
|
||||
));
|
||||
}
|
||||
|
||||
$definition = $this->definitions[$id];
|
||||
|
||||
$service = $this->getService($id, $definition);
|
||||
|
||||
if (!$service instanceof Extension) {
|
||||
throw new \RuntimeException(sprintf(
|
||||
'Service resolved for identifier "%s" does not implement the %s" interface.',
|
||||
$id,
|
||||
Extension::class,
|
||||
));
|
||||
}
|
||||
|
||||
$this->services[$id] = $service;
|
||||
|
||||
return $service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the service from a definition.
|
||||
*
|
||||
* @param callable|object|string $definition
|
||||
*/
|
||||
private function getService(string $id, $definition)
|
||||
{
|
||||
if (is_callable($definition)) {
|
||||
try {
|
||||
return $definition();
|
||||
} catch (\Throwable $e) {
|
||||
throw new ContainerException(
|
||||
sprintf('Error while invoking callable for "%s"', $id),
|
||||
0,
|
||||
$e,
|
||||
);
|
||||
}
|
||||
} elseif (is_object($definition)) {
|
||||
return $definition;
|
||||
} elseif (is_string($definition)) {
|
||||
if (class_exists($definition)) {
|
||||
try {
|
||||
return new $definition();
|
||||
} catch (\Throwable $e) {
|
||||
throw new ContainerException(sprintf('Could not instantiate class "%s"', $id), 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
throw new ContainerException(sprintf(
|
||||
'Could not instantiate class "%s". Class was not found.',
|
||||
$id,
|
||||
));
|
||||
} else {
|
||||
throw new ContainerException(sprintf(
|
||||
'Invalid type for definition with id "%s"',
|
||||
$id,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the container contains a given identifier.
|
||||
*
|
||||
* @param string $id
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function has($id): bool
|
||||
{
|
||||
if (!is_string($id)) {
|
||||
throw new \InvalidArgumentException(sprintf(
|
||||
'First argument of %s::get() must be string',
|
||||
self::class,
|
||||
));
|
||||
}
|
||||
|
||||
return array_key_exists($id, $this->definitions);
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Faker\Container;
|
||||
|
||||
use Faker\Core;
|
||||
use Faker\Extension;
|
||||
|
||||
/**
|
||||
* @experimental This class is experimental and does not fall under our BC promise
|
||||
*/
|
||||
final class ContainerBuilder
|
||||
{
|
||||
/**
|
||||
* @var array<string, callable|object|string>
|
||||
*/
|
||||
private array $definitions = [];
|
||||
|
||||
/**
|
||||
* @param callable|object|string $definition
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function add(string $id, $definition): self
|
||||
{
|
||||
if (!is_string($definition) && !is_callable($definition) && !is_object($definition)) {
|
||||
throw new \InvalidArgumentException(sprintf(
|
||||
'First argument to "%s::add()" must be a string, callable or object.',
|
||||
self::class,
|
||||
));
|
||||
}
|
||||
|
||||
$this->definitions[$id] = $definition;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function build(): ContainerInterface
|
||||
{
|
||||
return new Container($this->definitions);
|
||||
}
|
||||
|
||||
private static function defaultExtensions(): array
|
||||
{
|
||||
return [
|
||||
Extension\BarcodeExtension::class => Core\Barcode::class,
|
||||
Extension\BloodExtension::class => Core\Blood::class,
|
||||
Extension\ColorExtension::class => Core\Color::class,
|
||||
Extension\DateTimeExtension::class => Core\DateTime::class,
|
||||
Extension\FileExtension::class => Core\File::class,
|
||||
Extension\NumberExtension::class => Core\Number::class,
|
||||
Extension\UuidExtension::class => Core\Uuid::class,
|
||||
Extension\VersionExtension::class => Core\Version::class,
|
||||
];
|
||||
}
|
||||
|
||||
public static function withDefaultExtensions(): self
|
||||
{
|
||||
$instance = new self();
|
||||
|
||||
foreach (self::defaultExtensions() as $id => $definition) {
|
||||
$instance->add($id, $definition);
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Faker\Container;
|
||||
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
|
||||
/**
|
||||
* @experimental This class is experimental and does not fall under our BC promise
|
||||
*/
|
||||
final class ContainerException extends \RuntimeException implements ContainerExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Faker\Container;
|
||||
|
||||
use Psr\Container\ContainerInterface as BaseContainerInterface;
|
||||
|
||||
interface ContainerInterface extends BaseContainerInterface
|
||||
{
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Faker\Container;
|
||||
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
|
||||
/**
|
||||
* @experimental This class is experimental and does not fall under our BC promise
|
||||
*/
|
||||
final class NotInContainerException extends \RuntimeException implements NotFoundExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Faker\Core;
|
||||
|
||||
use Faker\Calculator;
|
||||
use Faker\Extension;
|
||||
|
||||
/**
|
||||
* @experimental This class is experimental and does not fall under our BC promise
|
||||
*/
|
||||
final class Barcode implements Extension\BarcodeExtension
|
||||
{
|
||||
private Extension\NumberExtension $numberExtension;
|
||||
|
||||
public function __construct(?Extension\NumberExtension $numberExtension = null)
|
||||
{
|
||||
$this->numberExtension = $numberExtension ?: new Number();
|
||||
}
|
||||
|
||||
private function ean(int $length = 13): string
|
||||
{
|
||||
$code = Extension\Helper::numerify(str_repeat('#', $length - 1));
|
||||
|
||||
return sprintf('%s%s', $code, Calculator\Ean::checksum($code));
|
||||
}
|
||||
|
||||
public function ean13(): string
|
||||
{
|
||||
return $this->ean();
|
||||
}
|
||||
|
||||
public function ean8(): string
|
||||
{
|
||||
return $this->ean(8);
|
||||
}
|
||||
|
||||
public function isbn10(): string
|
||||
{
|
||||
$code = Extension\Helper::numerify(str_repeat('#', 9));
|
||||
|
||||
return sprintf('%s%s', $code, Calculator\Isbn::checksum($code));
|
||||
}
|
||||
|
||||
public function isbn13(): string
|
||||
{
|
||||
$code = '97' . $this->numberExtension->numberBetween(8, 9) . Extension\Helper::numerify(str_repeat('#', 9));
|
||||
|
||||
return sprintf('%s%s', $code, Calculator\Ean::checksum($code));
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Faker\Core;
|
||||
|
||||
use Faker\Extension;
|
||||
|
||||
/**
|
||||
* @experimental This class is experimental and does not fall under our BC promise
|
||||
*/
|
||||
final class Blood implements Extension\BloodExtension
|
||||
{
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private array $bloodTypes = ['A', 'AB', 'B', 'O'];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private array $bloodRhFactors = ['+', '-'];
|
||||
|
||||
public function bloodType(): string
|
||||
{
|
||||
return Extension\Helper::randomElement($this->bloodTypes);
|
||||
}
|
||||
|
||||
public function bloodRh(): string
|
||||
{
|
||||
return Extension\Helper::randomElement($this->bloodRhFactors);
|
||||
}
|
||||
|
||||
public function bloodGroup(): string
|
||||
{
|
||||
return sprintf(
|
||||
'%s%s',
|
||||
$this->bloodType(),
|
||||
$this->bloodRh(),
|
||||
);
|
||||
}
|
||||
}
|
||||
-177
@@ -1,177 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Faker\Core;
|
||||
|
||||
use Faker\Extension;
|
||||
use Faker\Extension\Helper;
|
||||
|
||||
/**
|
||||
* @experimental This class is experimental and does not fall under our BC promise
|
||||
*/
|
||||
final class Color implements Extension\ColorExtension
|
||||
{
|
||||
private Extension\NumberExtension $numberExtension;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private array $safeColorNames = [
|
||||
'black', 'maroon', 'green', 'navy', 'olive',
|
||||
'purple', 'teal', 'lime', 'blue', 'silver',
|
||||
'gray', 'yellow', 'fuchsia', 'aqua', 'white',
|
||||
];
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private array $allColorNames = [
|
||||
'AliceBlue', 'AntiqueWhite', 'Aqua', 'Aquamarine',
|
||||
'Azure', 'Beige', 'Bisque', 'Black', 'BlanchedAlmond',
|
||||
'Blue', 'BlueViolet', 'Brown', 'BurlyWood', 'CadetBlue',
|
||||
'Chartreuse', 'Chocolate', 'Coral', 'CornflowerBlue',
|
||||
'Cornsilk', 'Crimson', 'Cyan', 'DarkBlue', 'DarkCyan',
|
||||
'DarkGoldenRod', 'DarkGray', 'DarkGreen', 'DarkKhaki',
|
||||
'DarkMagenta', 'DarkOliveGreen', 'Darkorange', 'DarkOrchid',
|
||||
'DarkRed', 'DarkSalmon', 'DarkSeaGreen', 'DarkSlateBlue',
|
||||
'DarkSlateGray', 'DarkTurquoise', 'DarkViolet', 'DeepPink',
|
||||
'DeepSkyBlue', 'DimGray', 'DimGrey', 'DodgerBlue', 'FireBrick',
|
||||
'FloralWhite', 'ForestGreen', 'Fuchsia', 'Gainsboro', 'GhostWhite',
|
||||
'Gold', 'GoldenRod', 'Gray', 'Green', 'GreenYellow', 'HoneyDew',
|
||||
'HotPink', 'IndianRed', 'Indigo', 'Ivory', 'Khaki', 'Lavender',
|
||||
'LavenderBlush', 'LawnGreen', 'LemonChiffon', 'LightBlue', 'LightCoral',
|
||||
'LightCyan', 'LightGoldenRodYellow', 'LightGray', 'LightGreen', 'LightPink',
|
||||
'LightSalmon', 'LightSeaGreen', 'LightSkyBlue', 'LightSlateGray', 'LightSteelBlue',
|
||||
'LightYellow', 'Lime', 'LimeGreen', 'Linen', 'Magenta', 'Maroon', 'MediumAquaMarine',
|
||||
'MediumBlue', 'MediumOrchid', 'MediumPurple', 'MediumSeaGreen', 'MediumSlateBlue',
|
||||
'MediumSpringGreen', 'MediumTurquoise', 'MediumVioletRed', 'MidnightBlue',
|
||||
'MintCream', 'MistyRose', 'Moccasin', 'NavajoWhite', 'Navy', 'OldLace', 'Olive',
|
||||
'OliveDrab', 'Orange', 'OrangeRed', 'Orchid', 'PaleGoldenRod', 'PaleGreen',
|
||||
'PaleTurquoise', 'PaleVioletRed', 'PapayaWhip', 'PeachPuff', 'Peru', 'Pink', 'Plum',
|
||||
'PowderBlue', 'Purple', 'Red', 'RosyBrown', 'RoyalBlue', 'SaddleBrown', 'Salmon',
|
||||
'SandyBrown', 'SeaGreen', 'SeaShell', 'Sienna', 'Silver', 'SkyBlue', 'SlateBlue',
|
||||
'SlateGray', 'Snow', 'SpringGreen', 'SteelBlue', 'Tan', 'Teal', 'Thistle', 'Tomato',
|
||||
'Turquoise', 'Violet', 'Wheat', 'White', 'WhiteSmoke', 'Yellow', 'YellowGreen',
|
||||
];
|
||||
|
||||
public function __construct(?Extension\NumberExtension $numberExtension = null)
|
||||
{
|
||||
$this->numberExtension = $numberExtension ?: new Number();
|
||||
}
|
||||
|
||||
/**
|
||||
* @example '#fa3cc2'
|
||||
*/
|
||||
public function hexColor(): string
|
||||
{
|
||||
return '#' . str_pad(dechex($this->numberExtension->numberBetween(1, 16777215)), 6, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @example '#ff0044'
|
||||
*/
|
||||
public function safeHexColor(): string
|
||||
{
|
||||
$color = str_pad(dechex($this->numberExtension->numberBetween(0, 255)), 3, '0', STR_PAD_LEFT);
|
||||
|
||||
return sprintf(
|
||||
'#%s%s%s%s%s%s',
|
||||
$color[0],
|
||||
$color[0],
|
||||
$color[1],
|
||||
$color[1],
|
||||
$color[2],
|
||||
$color[2],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @example 'array(0,255,122)'
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
public function rgbColorAsArray(): array
|
||||
{
|
||||
$color = $this->hexColor();
|
||||
|
||||
return [
|
||||
hexdec(substr($color, 1, 2)),
|
||||
hexdec(substr($color, 3, 2)),
|
||||
hexdec(substr($color, 5, 2)),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @example '0,255,122'
|
||||
*/
|
||||
public function rgbColor(): string
|
||||
{
|
||||
return implode(',', $this->rgbColorAsArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @example 'rgb(0,255,122)'
|
||||
*/
|
||||
public function rgbCssColor(): string
|
||||
{
|
||||
return sprintf(
|
||||
'rgb(%s)',
|
||||
$this->rgbColor(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @example 'rgba(0,255,122,0.8)'
|
||||
*/
|
||||
public function rgbaCssColor(): string
|
||||
{
|
||||
return sprintf(
|
||||
'rgba(%s,%s)',
|
||||
$this->rgbColor(),
|
||||
$this->numberExtension->randomFloat(1, 0, 1),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @example 'blue'
|
||||
*/
|
||||
public function safeColorName(): string
|
||||
{
|
||||
return Helper::randomElement($this->safeColorNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* @example 'NavajoWhite'
|
||||
*/
|
||||
public function colorName(): string
|
||||
{
|
||||
return Helper::randomElement($this->allColorNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* @example '340,50,20'
|
||||
*/
|
||||
public function hslColor(): string
|
||||
{
|
||||
return sprintf(
|
||||
'%s,%s,%s',
|
||||
$this->numberExtension->numberBetween(0, 360),
|
||||
$this->numberExtension->numberBetween(0, 100),
|
||||
$this->numberExtension->numberBetween(0, 100),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @example array(340, 50, 20)
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
public function hslColorAsArray(): array
|
||||
{
|
||||
return [
|
||||
$this->numberExtension->numberBetween(0, 360),
|
||||
$this->numberExtension->numberBetween(0, 100),
|
||||
$this->numberExtension->numberBetween(0, 100),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Faker\Core;
|
||||
|
||||
use Faker\Extension;
|
||||
|
||||
/**
|
||||
* @experimental This class is experimental and does not fall under our BC promise
|
||||
*/
|
||||
final class Coordinates implements Extension\Extension
|
||||
{
|
||||
private Extension\NumberExtension $numberExtension;
|
||||
|
||||
public function __construct(?Extension\NumberExtension $numberExtension = null)
|
||||
{
|
||||
$this->numberExtension = $numberExtension ?: new Number();
|
||||
}
|
||||
|
||||
/**
|
||||
* @example '77.147489'
|
||||
*
|
||||
* @return float Uses signed degrees format (returns a float number between -90 and 90)
|
||||
*/
|
||||
public function latitude(float $min = -90.0, float $max = 90.0): float
|
||||
{
|
||||
if ($min < -90 || $max < -90) {
|
||||
throw new \LogicException('Latitude cannot be less that -90.0');
|
||||
}
|
||||
|
||||
if ($min > 90 || $max > 90) {
|
||||
throw new \LogicException('Latitude cannot be greater that 90.0');
|
||||
}
|
||||
|
||||
return $this->randomFloat(6, $min, $max);
|
||||
}
|
||||
|
||||
/**
|
||||
* @example '86.211205'
|
||||
*
|
||||
* @return float Uses signed degrees format (returns a float number between -180 and 180)
|
||||
*/
|
||||
public function longitude(float $min = -180.0, float $max = 180.0): float
|
||||
{
|
||||
if ($min < -180 || $max < -180) {
|
||||
throw new \LogicException('Longitude cannot be less that -180.0');
|
||||
}
|
||||
|
||||
if ($min > 180 || $max > 180) {
|
||||
throw new \LogicException('Longitude cannot be greater that 180.0');
|
||||
}
|
||||
|
||||
return $this->randomFloat(6, $min, $max);
|
||||
}
|
||||
|
||||
/**
|
||||
* @example array('77.147489', '86.211205')
|
||||
*
|
||||
* @return array{latitude: float, longitude: float}
|
||||
*/
|
||||
public function localCoordinates(): array
|
||||
{
|
||||
return [
|
||||
'latitude' => $this->latitude(),
|
||||
'longitude' => $this->longitude(),
|
||||
];
|
||||
}
|
||||
|
||||
private function randomFloat(int $nbMaxDecimals, float $min, float $max): float
|
||||
{
|
||||
if ($min > $max) {
|
||||
throw new \LogicException('Invalid coordinates boundaries');
|
||||
}
|
||||
|
||||
return $this->numberExtension->randomFloat($nbMaxDecimals, $min, $max);
|
||||
}
|
||||
}
|
||||
-217
@@ -1,217 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Faker\Core;
|
||||
|
||||
use Faker\Extension\DateTimeExtension;
|
||||
use Faker\Extension\GeneratorAwareExtension;
|
||||
use Faker\Extension\GeneratorAwareExtensionTrait;
|
||||
use Faker\Extension\Helper;
|
||||
|
||||
/**
|
||||
* @experimental This class is experimental and does not fall under our BC promise
|
||||
*
|
||||
* @since 1.20.0
|
||||
*/
|
||||
final class DateTime implements DateTimeExtension, GeneratorAwareExtension
|
||||
{
|
||||
use GeneratorAwareExtensionTrait;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private array $centuries = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII', 'XIII', 'XIV', 'XV', 'XVI', 'XVII', 'XVIII', 'XIX', 'XX', 'XXI'];
|
||||
|
||||
private ?string $defaultTimezone = null;
|
||||
|
||||
/**
|
||||
* Get the POSIX-timestamp of a DateTime, int or string.
|
||||
*
|
||||
* @param \DateTime|float|int|string $until
|
||||
*
|
||||
* @return false|int
|
||||
*/
|
||||
private function getTimestamp($until = 'now')
|
||||
{
|
||||
if (is_numeric($until)) {
|
||||
return (int) $until;
|
||||
}
|
||||
|
||||
if ($until instanceof \DateTime) {
|
||||
return $until->getTimestamp();
|
||||
}
|
||||
|
||||
return strtotime(empty($until) ? 'now' : $until);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a DateTime created based on a POSIX-timestamp.
|
||||
*
|
||||
* @param int $timestamp the UNIX / POSIX-compatible timestamp
|
||||
*/
|
||||
private function getTimestampDateTime(int $timestamp): \DateTime
|
||||
{
|
||||
return new \DateTime('@' . $timestamp);
|
||||
}
|
||||
|
||||
private function resolveTimezone(?string $timezone): string
|
||||
{
|
||||
if ($timezone !== null) {
|
||||
return $timezone;
|
||||
}
|
||||
|
||||
return null === $this->defaultTimezone ? date_default_timezone_get() : $this->defaultTimezone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to set the timezone on a DateTime object.
|
||||
*/
|
||||
private function setTimezone(\DateTime $dateTime, ?string $timezone): \DateTime
|
||||
{
|
||||
$timezone = $this->resolveTimezone($timezone);
|
||||
|
||||
return $dateTime->setTimezone(new \DateTimeZone($timezone));
|
||||
}
|
||||
|
||||
public function dateTime($until = 'now', ?string $timezone = null): \DateTime
|
||||
{
|
||||
return $this->setTimezone(
|
||||
$this->getTimestampDateTime($this->unixTime($until)),
|
||||
$timezone,
|
||||
);
|
||||
}
|
||||
|
||||
public function dateTimeAD($until = 'now', ?string $timezone = null): \DateTime
|
||||
{
|
||||
$min = (PHP_INT_SIZE > 4) ? -62135597361 : -PHP_INT_MAX;
|
||||
|
||||
return $this->setTimezone(
|
||||
$this->getTimestampDateTime($this->generator->numberBetween($min, $this->getTimestamp($until))),
|
||||
$timezone,
|
||||
);
|
||||
}
|
||||
|
||||
public function dateTimeBetween($from = '-30 years', $until = 'now', ?string $timezone = null): \DateTime
|
||||
{
|
||||
$start = $this->getTimestamp($from);
|
||||
$end = $this->getTimestamp($until);
|
||||
|
||||
if ($start > $end) {
|
||||
throw new \InvalidArgumentException('"$from" must be anterior to "$until".');
|
||||
}
|
||||
|
||||
$timestamp = $this->generator->numberBetween($start, $end);
|
||||
|
||||
return $this->setTimezone(
|
||||
$this->getTimestampDateTime($timestamp),
|
||||
$timezone,
|
||||
);
|
||||
}
|
||||
|
||||
public function dateTimeInInterval($from = '-30 years', string $interval = '+5 days', ?string $timezone = null): \DateTime
|
||||
{
|
||||
$intervalObject = \DateInterval::createFromDateString($interval);
|
||||
$datetime = $from instanceof \DateTime ? $from : new \DateTime($from);
|
||||
|
||||
$other = (clone $datetime)->add($intervalObject);
|
||||
|
||||
$begin = min($datetime, $other);
|
||||
$end = $datetime === $begin ? $other : $datetime;
|
||||
|
||||
return $this->dateTimeBetween($begin, $end, $timezone);
|
||||
}
|
||||
|
||||
public function dateTimeThisWeek($until = 'sunday this week', ?string $timezone = null): \DateTime
|
||||
{
|
||||
return $this->dateTimeBetween('monday this week', $until, $timezone);
|
||||
}
|
||||
|
||||
public function dateTimeThisMonth($until = 'last day of this month', ?string $timezone = null): \DateTime
|
||||
{
|
||||
return $this->dateTimeBetween('first day of this month', $until, $timezone);
|
||||
}
|
||||
|
||||
public function dateTimeThisYear($until = 'last day of december', ?string $timezone = null): \DateTime
|
||||
{
|
||||
return $this->dateTimeBetween('first day of january', $until, $timezone);
|
||||
}
|
||||
|
||||
public function dateTimeThisDecade($until = 'now', ?string $timezone = null): \DateTime
|
||||
{
|
||||
$year = floor(date('Y') / 10) * 10;
|
||||
|
||||
return $this->dateTimeBetween("first day of january $year", $until, $timezone);
|
||||
}
|
||||
|
||||
public function dateTimeThisCentury($until = 'now', ?string $timezone = null): \DateTime
|
||||
{
|
||||
$year = floor(date('Y') / 100) * 100;
|
||||
|
||||
return $this->dateTimeBetween("first day of january $year", $until, $timezone);
|
||||
}
|
||||
|
||||
public function date(string $format = 'Y-m-d', $until = 'now'): string
|
||||
{
|
||||
return $this->dateTime($until)->format($format);
|
||||
}
|
||||
|
||||
public function time(string $format = 'H:i:s', $until = 'now'): string
|
||||
{
|
||||
return $this->date($format, $until);
|
||||
}
|
||||
|
||||
public function unixTime($until = 'now'): int
|
||||
{
|
||||
return $this->generator->numberBetween(0, $this->getTimestamp($until));
|
||||
}
|
||||
|
||||
public function iso8601($until = 'now'): string
|
||||
{
|
||||
return $this->date(\DateTime::ISO8601, $until);
|
||||
}
|
||||
|
||||
public function amPm($until = 'now'): string
|
||||
{
|
||||
return $this->date('a', $until);
|
||||
}
|
||||
|
||||
public function dayOfMonth($until = 'now'): string
|
||||
{
|
||||
return $this->date('d', $until);
|
||||
}
|
||||
|
||||
public function dayOfWeek($until = 'now'): string
|
||||
{
|
||||
return $this->date('l', $until);
|
||||
}
|
||||
|
||||
public function month($until = 'now'): string
|
||||
{
|
||||
return $this->date('m', $until);
|
||||
}
|
||||
|
||||
public function monthName($until = 'now'): string
|
||||
{
|
||||
return $this->date('F', $until);
|
||||
}
|
||||
|
||||
public function year($until = 'now'): string
|
||||
{
|
||||
return $this->date('Y', $until);
|
||||
}
|
||||
|
||||
public function century(): string
|
||||
{
|
||||
return Helper::randomElement($this->centuries);
|
||||
}
|
||||
|
||||
public function timezone(?string $countryCode = null): string
|
||||
{
|
||||
if ($countryCode) {
|
||||
$timezones = \DateTimeZone::listIdentifiers(\DateTimeZone::PER_COUNTRY, $countryCode);
|
||||
} else {
|
||||
$timezones = \DateTimeZone::listIdentifiers();
|
||||
}
|
||||
|
||||
return Helper::randomElement($timezones);
|
||||
}
|
||||
}
|
||||
-564
@@ -1,564 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Faker\Core;
|
||||
|
||||
use Faker\Extension;
|
||||
|
||||
/**
|
||||
* @experimental This class is experimental and does not fall under our BC promise
|
||||
*/
|
||||
final class File implements Extension\FileExtension
|
||||
{
|
||||
/**
|
||||
* MIME types from the apache.org file. Some types are truncated.
|
||||
*
|
||||
* @var array Map of MIME types => file extension(s)
|
||||
*
|
||||
* @see http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types
|
||||
*/
|
||||
private array $mimeTypes = [
|
||||
'application/atom+xml' => 'atom',
|
||||
'application/ecmascript' => 'ecma',
|
||||
'application/emma+xml' => 'emma',
|
||||
'application/epub+zip' => 'epub',
|
||||
'application/java-archive' => 'jar',
|
||||
'application/java-vm' => 'class',
|
||||
'application/javascript' => 'js',
|
||||
'application/json' => 'json',
|
||||
'application/jsonml+json' => 'jsonml',
|
||||
'application/lost+xml' => 'lostxml',
|
||||
'application/mathml+xml' => 'mathml',
|
||||
'application/mets+xml' => 'mets',
|
||||
'application/mods+xml' => 'mods',
|
||||
'application/mp4' => 'mp4s',
|
||||
'application/msword' => ['doc', 'dot'],
|
||||
'application/octet-stream' => [
|
||||
'bin',
|
||||
'dms',
|
||||
'lrf',
|
||||
'mar',
|
||||
'so',
|
||||
'dist',
|
||||
'distz',
|
||||
'pkg',
|
||||
'bpk',
|
||||
'dump',
|
||||
'elc',
|
||||
'deploy',
|
||||
],
|
||||
'application/ogg' => 'ogx',
|
||||
'application/omdoc+xml' => 'omdoc',
|
||||
'application/pdf' => 'pdf',
|
||||
'application/pgp-encrypted' => 'pgp',
|
||||
'application/pgp-signature' => ['asc', 'sig'],
|
||||
'application/pkix-pkipath' => 'pkipath',
|
||||
'application/pkixcmp' => 'pki',
|
||||
'application/pls+xml' => 'pls',
|
||||
'application/postscript' => ['ai', 'eps', 'ps'],
|
||||
'application/pskc+xml' => 'pskcxml',
|
||||
'application/rdf+xml' => 'rdf',
|
||||
'application/reginfo+xml' => 'rif',
|
||||
'application/rss+xml' => 'rss',
|
||||
'application/rtf' => 'rtf',
|
||||
'application/sbml+xml' => 'sbml',
|
||||
'application/vnd.adobe.air-application-installer-package+zip' => 'air',
|
||||
'application/vnd.adobe.xdp+xml' => 'xdp',
|
||||
'application/vnd.adobe.xfdf' => 'xfdf',
|
||||
'application/vnd.ahead.space' => 'ahead',
|
||||
'application/vnd.dart' => 'dart',
|
||||
'application/vnd.data-vision.rdz' => 'rdz',
|
||||
'application/vnd.dece.data' => ['uvf', 'uvvf', 'uvd', 'uvvd'],
|
||||
'application/vnd.dece.ttml+xml' => ['uvt', 'uvvt'],
|
||||
'application/vnd.dece.unspecified' => ['uvx', 'uvvx'],
|
||||
'application/vnd.dece.zip' => ['uvz', 'uvvz'],
|
||||
'application/vnd.denovo.fcselayout-link' => 'fe_launch',
|
||||
'application/vnd.dna' => 'dna',
|
||||
'application/vnd.dolby.mlp' => 'mlp',
|
||||
'application/vnd.dpgraph' => 'dpg',
|
||||
'application/vnd.dreamfactory' => 'dfac',
|
||||
'application/vnd.ds-keypoint' => 'kpxx',
|
||||
'application/vnd.dvb.ait' => 'ait',
|
||||
'application/vnd.dvb.service' => 'svc',
|
||||
'application/vnd.dynageo' => 'geo',
|
||||
'application/vnd.ecowin.chart' => 'mag',
|
||||
'application/vnd.enliven' => 'nml',
|
||||
'application/vnd.epson.esf' => 'esf',
|
||||
'application/vnd.epson.msf' => 'msf',
|
||||
'application/vnd.epson.quickanime' => 'qam',
|
||||
'application/vnd.epson.salt' => 'slt',
|
||||
'application/vnd.epson.ssf' => 'ssf',
|
||||
'application/vnd.ezpix-album' => 'ez2',
|
||||
'application/vnd.ezpix-package' => 'ez3',
|
||||
'application/vnd.fdf' => 'fdf',
|
||||
'application/vnd.fdsn.mseed' => 'mseed',
|
||||
'application/vnd.fdsn.seed' => ['seed', 'dataless'],
|
||||
'application/vnd.flographit' => 'gph',
|
||||
'application/vnd.fluxtime.clip' => 'ftc',
|
||||
'application/vnd.hal+xml' => 'hal',
|
||||
'application/vnd.hydrostatix.sof-data' => 'sfd-hdstx',
|
||||
'application/vnd.ibm.minipay' => 'mpy',
|
||||
'application/vnd.ibm.secure-container' => 'sc',
|
||||
'application/vnd.iccprofile' => ['icc', 'icm'],
|
||||
'application/vnd.igloader' => 'igl',
|
||||
'application/vnd.immervision-ivp' => 'ivp',
|
||||
'application/vnd.kde.karbon' => 'karbon',
|
||||
'application/vnd.kde.kchart' => 'chrt',
|
||||
'application/vnd.kde.kformula' => 'kfo',
|
||||
'application/vnd.kde.kivio' => 'flw',
|
||||
'application/vnd.kde.kontour' => 'kon',
|
||||
'application/vnd.kde.kpresenter' => ['kpr', 'kpt'],
|
||||
'application/vnd.kde.kspread' => 'ksp',
|
||||
'application/vnd.kde.kword' => ['kwd', 'kwt'],
|
||||
'application/vnd.kenameaapp' => 'htke',
|
||||
'application/vnd.kidspiration' => 'kia',
|
||||
'application/vnd.kinar' => ['kne', 'knp'],
|
||||
'application/vnd.koan' => ['skp', 'skd', 'skt', 'skm'],
|
||||
'application/vnd.kodak-descriptor' => 'sse',
|
||||
'application/vnd.las.las+xml' => 'lasxml',
|
||||
'application/vnd.llamagraphics.life-balance.desktop' => 'lbd',
|
||||
'application/vnd.llamagraphics.life-balance.exchange+xml' => 'lbe',
|
||||
'application/vnd.lotus-1-2-3' => '123',
|
||||
'application/vnd.lotus-approach' => 'apr',
|
||||
'application/vnd.lotus-freelance' => 'pre',
|
||||
'application/vnd.lotus-notes' => 'nsf',
|
||||
'application/vnd.lotus-organizer' => 'org',
|
||||
'application/vnd.lotus-screencam' => 'scm',
|
||||
'application/vnd.mozilla.xul+xml' => 'xul',
|
||||
'application/vnd.ms-artgalry' => 'cil',
|
||||
'application/vnd.ms-cab-compressed' => 'cab',
|
||||
'application/vnd.ms-excel' => [
|
||||
'xls',
|
||||
'xlm',
|
||||
'xla',
|
||||
'xlc',
|
||||
'xlt',
|
||||
'xlw',
|
||||
],
|
||||
'application/vnd.ms-excel.addin.macroenabled.12' => 'xlam',
|
||||
'application/vnd.ms-excel.sheet.binary.macroenabled.12' => 'xlsb',
|
||||
'application/vnd.ms-excel.sheet.macroenabled.12' => 'xlsm',
|
||||
'application/vnd.ms-excel.template.macroenabled.12' => 'xltm',
|
||||
'application/vnd.ms-fontobject' => 'eot',
|
||||
'application/vnd.ms-htmlhelp' => 'chm',
|
||||
'application/vnd.ms-ims' => 'ims',
|
||||
'application/vnd.ms-lrm' => 'lrm',
|
||||
'application/vnd.ms-officetheme' => 'thmx',
|
||||
'application/vnd.ms-pki.seccat' => 'cat',
|
||||
'application/vnd.ms-pki.stl' => 'stl',
|
||||
'application/vnd.ms-powerpoint' => ['ppt', 'pps', 'pot'],
|
||||
'application/vnd.ms-powerpoint.addin.macroenabled.12' => 'ppam',
|
||||
'application/vnd.ms-powerpoint.presentation.macroenabled.12' => 'pptm',
|
||||
'application/vnd.ms-powerpoint.slide.macroenabled.12' => 'sldm',
|
||||
'application/vnd.ms-powerpoint.slideshow.macroenabled.12' => 'ppsm',
|
||||
'application/vnd.ms-powerpoint.template.macroenabled.12' => 'potm',
|
||||
'application/vnd.ms-project' => ['mpp', 'mpt'],
|
||||
'application/vnd.ms-word.document.macroenabled.12' => 'docm',
|
||||
'application/vnd.ms-word.template.macroenabled.12' => 'dotm',
|
||||
'application/vnd.ms-works' => ['wps', 'wks', 'wcm', 'wdb'],
|
||||
'application/vnd.ms-wpl' => 'wpl',
|
||||
'application/vnd.ms-xpsdocument' => 'xps',
|
||||
'application/vnd.mseq' => 'mseq',
|
||||
'application/vnd.musician' => 'mus',
|
||||
'application/vnd.oasis.opendocument.chart' => 'odc',
|
||||
'application/vnd.oasis.opendocument.chart-template' => 'otc',
|
||||
'application/vnd.oasis.opendocument.database' => 'odb',
|
||||
'application/vnd.oasis.opendocument.formula' => 'odf',
|
||||
'application/vnd.oasis.opendocument.formula-template' => 'odft',
|
||||
'application/vnd.oasis.opendocument.graphics' => 'odg',
|
||||
'application/vnd.oasis.opendocument.graphics-template' => 'otg',
|
||||
'application/vnd.oasis.opendocument.image' => 'odi',
|
||||
'application/vnd.oasis.opendocument.image-template' => 'oti',
|
||||
'application/vnd.oasis.opendocument.presentation' => 'odp',
|
||||
'application/vnd.oasis.opendocument.presentation-template' => 'otp',
|
||||
'application/vnd.oasis.opendocument.spreadsheet' => 'ods',
|
||||
'application/vnd.oasis.opendocument.spreadsheet-template' => 'ots',
|
||||
'application/vnd.oasis.opendocument.text' => 'odt',
|
||||
'application/vnd.oasis.opendocument.text-master' => 'odm',
|
||||
'application/vnd.oasis.opendocument.text-template' => 'ott',
|
||||
'application/vnd.oasis.opendocument.text-web' => 'oth',
|
||||
'application/vnd.olpc-sugar' => 'xo',
|
||||
'application/vnd.oma.dd2+xml' => 'dd2',
|
||||
'application/vnd.openofficeorg.extension' => 'oxt',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.slide' => 'sldx',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.slideshow' => 'ppsx',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.template' => 'potx',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.template' => 'xltx',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.template' => 'dotx',
|
||||
'application/vnd.pvi.ptid1' => 'ptid',
|
||||
'application/vnd.quark.quarkxpress' => [
|
||||
'qxd',
|
||||
'qxt',
|
||||
'qwd',
|
||||
'qwt',
|
||||
'qxl',
|
||||
'qxb',
|
||||
],
|
||||
'application/vnd.realvnc.bed' => 'bed',
|
||||
'application/vnd.recordare.musicxml' => 'mxl',
|
||||
'application/vnd.recordare.musicxml+xml' => 'musicxml',
|
||||
'application/vnd.rig.cryptonote' => 'cryptonote',
|
||||
'application/vnd.rim.cod' => 'cod',
|
||||
'application/vnd.rn-realmedia' => 'rm',
|
||||
'application/vnd.rn-realmedia-vbr' => 'rmvb',
|
||||
'application/vnd.route66.link66+xml' => 'link66',
|
||||
'application/vnd.sailingtracker.track' => 'st',
|
||||
'application/vnd.seemail' => 'see',
|
||||
'application/vnd.sema' => 'sema',
|
||||
'application/vnd.semd' => 'semd',
|
||||
'application/vnd.semf' => 'semf',
|
||||
'application/vnd.shana.informed.formdata' => 'ifm',
|
||||
'application/vnd.shana.informed.formtemplate' => 'itp',
|
||||
'application/vnd.shana.informed.interchange' => 'iif',
|
||||
'application/vnd.shana.informed.package' => 'ipk',
|
||||
'application/vnd.simtech-mindmapper' => ['twd', 'twds'],
|
||||
'application/vnd.smaf' => 'mmf',
|
||||
'application/vnd.stepmania.stepchart' => 'sm',
|
||||
'application/vnd.sun.xml.calc' => 'sxc',
|
||||
'application/vnd.sun.xml.calc.template' => 'stc',
|
||||
'application/vnd.sun.xml.draw' => 'sxd',
|
||||
'application/vnd.sun.xml.draw.template' => 'std',
|
||||
'application/vnd.sun.xml.impress' => 'sxi',
|
||||
'application/vnd.sun.xml.impress.template' => 'sti',
|
||||
'application/vnd.sun.xml.math' => 'sxm',
|
||||
'application/vnd.sun.xml.writer' => 'sxw',
|
||||
'application/vnd.sun.xml.writer.global' => 'sxg',
|
||||
'application/vnd.sun.xml.writer.template' => 'stw',
|
||||
'application/vnd.sus-calendar' => ['sus', 'susp'],
|
||||
'application/vnd.svd' => 'svd',
|
||||
'application/vnd.symbian.install' => ['sis', 'sisx'],
|
||||
'application/vnd.syncml+xml' => 'xsm',
|
||||
'application/vnd.syncml.dm+wbxml' => 'bdm',
|
||||
'application/vnd.syncml.dm+xml' => 'xdm',
|
||||
'application/vnd.tao.intent-module-archive' => 'tao',
|
||||
'application/vnd.tcpdump.pcap' => ['pcap', 'cap', 'dmp'],
|
||||
'application/vnd.tmobile-livetv' => 'tmo',
|
||||
'application/vnd.trid.tpt' => 'tpt',
|
||||
'application/vnd.triscape.mxs' => 'mxs',
|
||||
'application/vnd.trueapp' => 'tra',
|
||||
'application/vnd.ufdl' => ['ufd', 'ufdl'],
|
||||
'application/vnd.uiq.theme' => 'utz',
|
||||
'application/vnd.umajin' => 'umj',
|
||||
'application/vnd.unity' => 'unityweb',
|
||||
'application/vnd.uoml+xml' => 'uoml',
|
||||
'application/vnd.vcx' => 'vcx',
|
||||
'application/vnd.visio' => ['vsd', 'vst', 'vss', 'vsw'],
|
||||
'application/vnd.visionary' => 'vis',
|
||||
'application/vnd.vsf' => 'vsf',
|
||||
'application/vnd.wap.wbxml' => 'wbxml',
|
||||
'application/vnd.wap.wmlc' => 'wmlc',
|
||||
'application/vnd.wap.wmlscriptc' => 'wmlsc',
|
||||
'application/vnd.webturbo' => 'wtb',
|
||||
'application/vnd.wolfram.player' => 'nbp',
|
||||
'application/vnd.wordperfect' => 'wpd',
|
||||
'application/vnd.wqd' => 'wqd',
|
||||
'application/vnd.wt.stf' => 'stf',
|
||||
'application/vnd.xara' => 'xar',
|
||||
'application/vnd.xfdl' => 'xfdl',
|
||||
'application/voicexml+xml' => 'vxml',
|
||||
'application/widget' => 'wgt',
|
||||
'application/winhlp' => 'hlp',
|
||||
'application/wsdl+xml' => 'wsdl',
|
||||
'application/wspolicy+xml' => 'wspolicy',
|
||||
'application/x-7z-compressed' => '7z',
|
||||
'application/x-bittorrent' => 'torrent',
|
||||
'application/x-blorb' => ['blb', 'blorb'],
|
||||
'application/x-bzip' => 'bz',
|
||||
'application/x-cdlink' => 'vcd',
|
||||
'application/x-cfs-compressed' => 'cfs',
|
||||
'application/x-chat' => 'chat',
|
||||
'application/x-chess-pgn' => 'pgn',
|
||||
'application/x-conference' => 'nsc',
|
||||
'application/x-cpio' => 'cpio',
|
||||
'application/x-csh' => 'csh',
|
||||
'application/x-debian-package' => ['deb', 'udeb'],
|
||||
'application/x-dgc-compressed' => 'dgc',
|
||||
'application/x-director' => [
|
||||
'dir',
|
||||
'dcr',
|
||||
'dxr',
|
||||
'cst',
|
||||
'cct',
|
||||
'cxt',
|
||||
'w3d',
|
||||
'fgd',
|
||||
'swa',
|
||||
],
|
||||
'application/x-font-ttf' => ['ttf', 'ttc'],
|
||||
'application/x-font-type1' => ['pfa', 'pfb', 'pfm', 'afm'],
|
||||
'application/x-font-woff' => 'woff',
|
||||
'application/x-freearc' => 'arc',
|
||||
'application/x-futuresplash' => 'spl',
|
||||
'application/x-gca-compressed' => 'gca',
|
||||
'application/x-glulx' => 'ulx',
|
||||
'application/x-gnumeric' => 'gnumeric',
|
||||
'application/x-gramps-xml' => 'gramps',
|
||||
'application/x-gtar' => 'gtar',
|
||||
'application/x-hdf' => 'hdf',
|
||||
'application/x-install-instructions' => 'install',
|
||||
'application/x-iso9660-image' => 'iso',
|
||||
'application/x-java-jnlp-file' => 'jnlp',
|
||||
'application/x-latex' => 'latex',
|
||||
'application/x-lzh-compressed' => ['lzh', 'lha'],
|
||||
'application/x-mie' => 'mie',
|
||||
'application/x-mobipocket-ebook' => ['prc', 'mobi'],
|
||||
'application/x-ms-application' => 'application',
|
||||
'application/x-ms-shortcut' => 'lnk',
|
||||
'application/x-ms-wmd' => 'wmd',
|
||||
'application/x-ms-wmz' => 'wmz',
|
||||
'application/x-ms-xbap' => 'xbap',
|
||||
'application/x-msaccess' => 'mdb',
|
||||
'application/x-msbinder' => 'obd',
|
||||
'application/x-mscardfile' => 'crd',
|
||||
'application/x-msclip' => 'clp',
|
||||
'application/x-msdownload' => ['exe', 'dll', 'com', 'bat', 'msi'],
|
||||
'application/x-msmediaview' => [
|
||||
'mvb',
|
||||
'm13',
|
||||
'm14',
|
||||
],
|
||||
'application/x-msmetafile' => ['wmf', 'wmz', 'emf', 'emz'],
|
||||
'application/x-rar-compressed' => 'rar',
|
||||
'application/x-research-info-systems' => 'ris',
|
||||
'application/x-sh' => 'sh',
|
||||
'application/x-shar' => 'shar',
|
||||
'application/x-shockwave-flash' => 'swf',
|
||||
'application/x-silverlight-app' => 'xap',
|
||||
'application/x-sql' => 'sql',
|
||||
'application/x-stuffit' => 'sit',
|
||||
'application/x-stuffitx' => 'sitx',
|
||||
'application/x-subrip' => 'srt',
|
||||
'application/x-sv4cpio' => 'sv4cpio',
|
||||
'application/x-sv4crc' => 'sv4crc',
|
||||
'application/x-t3vm-image' => 't3',
|
||||
'application/x-tads' => 'gam',
|
||||
'application/x-tar' => 'tar',
|
||||
'application/x-tcl' => 'tcl',
|
||||
'application/x-tex' => 'tex',
|
||||
'application/x-tex-tfm' => 'tfm',
|
||||
'application/x-texinfo' => ['texinfo', 'texi'],
|
||||
'application/x-tgif' => 'obj',
|
||||
'application/x-ustar' => 'ustar',
|
||||
'application/x-wais-source' => 'src',
|
||||
'application/x-x509-ca-cert' => ['der', 'crt'],
|
||||
'application/x-xfig' => 'fig',
|
||||
'application/x-xliff+xml' => 'xlf',
|
||||
'application/x-xpinstall' => 'xpi',
|
||||
'application/x-xz' => 'xz',
|
||||
'application/x-zmachine' => 'z1',
|
||||
'application/xaml+xml' => 'xaml',
|
||||
'application/xcap-diff+xml' => 'xdf',
|
||||
'application/xenc+xml' => 'xenc',
|
||||
'application/xhtml+xml' => ['xhtml', 'xht'],
|
||||
'application/xml' => ['xml', 'xsl'],
|
||||
'application/xml-dtd' => 'dtd',
|
||||
'application/xop+xml' => 'xop',
|
||||
'application/xproc+xml' => 'xpl',
|
||||
'application/xslt+xml' => 'xslt',
|
||||
'application/xspf+xml' => 'xspf',
|
||||
'application/xv+xml' => ['mxml', 'xhvml', 'xvml', 'xvm'],
|
||||
'application/yang' => 'yang',
|
||||
'application/yin+xml' => 'yin',
|
||||
'application/zip' => 'zip',
|
||||
'audio/adpcm' => 'adp',
|
||||
'audio/basic' => ['au', 'snd'],
|
||||
'audio/midi' => ['mid', 'midi', 'kar', 'rmi'],
|
||||
'audio/mp4' => 'mp4a',
|
||||
'audio/mpeg' => [
|
||||
'mpga',
|
||||
'mp2',
|
||||
'mp2a',
|
||||
'mp3',
|
||||
'm2a',
|
||||
'm3a',
|
||||
],
|
||||
'audio/ogg' => ['oga', 'ogg', 'spx'],
|
||||
'audio/vnd.dece.audio' => ['uva', 'uvva'],
|
||||
'audio/vnd.rip' => 'rip',
|
||||
'audio/webm' => 'weba',
|
||||
'audio/x-aac' => 'aac',
|
||||
'audio/x-aiff' => ['aif', 'aiff', 'aifc'],
|
||||
'audio/x-caf' => 'caf',
|
||||
'audio/x-flac' => 'flac',
|
||||
'audio/x-matroska' => 'mka',
|
||||
'audio/x-mpegurl' => 'm3u',
|
||||
'audio/x-ms-wax' => 'wax',
|
||||
'audio/x-ms-wma' => 'wma',
|
||||
'audio/x-pn-realaudio' => ['ram', 'ra'],
|
||||
'audio/x-pn-realaudio-plugin' => 'rmp',
|
||||
'audio/x-wav' => 'wav',
|
||||
'audio/xm' => 'xm',
|
||||
'image/bmp' => 'bmp',
|
||||
'image/cgm' => 'cgm',
|
||||
'image/g3fax' => 'g3',
|
||||
'image/gif' => 'gif',
|
||||
'image/ief' => 'ief',
|
||||
'image/jpeg' => ['jpeg', 'jpg', 'jpe'],
|
||||
'image/ktx' => 'ktx',
|
||||
'image/png' => 'png',
|
||||
'image/prs.btif' => 'btif',
|
||||
'image/sgi' => 'sgi',
|
||||
'image/svg+xml' => ['svg', 'svgz'],
|
||||
'image/tiff' => ['tiff', 'tif'],
|
||||
'image/vnd.adobe.photoshop' => 'psd',
|
||||
'image/vnd.dece.graphic' => ['uvi', 'uvvi', 'uvg', 'uvvg'],
|
||||
'image/vnd.dvb.subtitle' => 'sub',
|
||||
'image/vnd.djvu' => ['djvu', 'djv'],
|
||||
'image/vnd.dwg' => 'dwg',
|
||||
'image/vnd.dxf' => 'dxf',
|
||||
'image/vnd.fastbidsheet' => 'fbs',
|
||||
'image/vnd.fpx' => 'fpx',
|
||||
'image/vnd.fst' => 'fst',
|
||||
'image/vnd.fujixerox.edmics-mmr' => 'mmr',
|
||||
'image/vnd.fujixerox.edmics-rlc' => 'rlc',
|
||||
'image/vnd.ms-modi' => 'mdi',
|
||||
'image/vnd.ms-photo' => 'wdp',
|
||||
'image/vnd.net-fpx' => 'npx',
|
||||
'image/vnd.wap.wbmp' => 'wbmp',
|
||||
'image/vnd.xiff' => 'xif',
|
||||
'image/webp' => 'webp',
|
||||
'image/x-3ds' => '3ds',
|
||||
'image/x-cmu-raster' => 'ras',
|
||||
'image/x-cmx' => 'cmx',
|
||||
'image/x-freehand' => ['fh', 'fhc', 'fh4', 'fh5', 'fh7'],
|
||||
'image/x-icon' => 'ico',
|
||||
'image/x-mrsid-image' => 'sid',
|
||||
'image/x-pcx' => 'pcx',
|
||||
'image/x-pict' => ['pic', 'pct'],
|
||||
'image/x-portable-anymap' => 'pnm',
|
||||
'image/x-portable-bitmap' => 'pbm',
|
||||
'image/x-portable-graymap' => 'pgm',
|
||||
'image/x-portable-pixmap' => 'ppm',
|
||||
'image/x-rgb' => 'rgb',
|
||||
'image/x-tga' => 'tga',
|
||||
'image/x-xbitmap' => 'xbm',
|
||||
'image/x-xpixmap' => 'xpm',
|
||||
'image/x-xwindowdump' => 'xwd',
|
||||
'message/rfc822' => ['eml', 'mime'],
|
||||
'model/iges' => ['igs', 'iges'],
|
||||
'model/mesh' => ['msh', 'mesh', 'silo'],
|
||||
'model/vnd.collada+xml' => 'dae',
|
||||
'model/vnd.dwf' => 'dwf',
|
||||
'model/vnd.gdl' => 'gdl',
|
||||
'model/vnd.gtw' => 'gtw',
|
||||
'model/vnd.mts' => 'mts',
|
||||
'model/vnd.vtu' => 'vtu',
|
||||
'model/vrml' => ['wrl', 'vrml'],
|
||||
'model/x3d+binary' => 'x3db',
|
||||
'model/x3d+vrml' => 'x3dv',
|
||||
'model/x3d+xml' => 'x3d',
|
||||
'text/cache-manifest' => 'appcache',
|
||||
'text/calendar' => ['ics', 'ifb'],
|
||||
'text/css' => 'css',
|
||||
'text/csv' => 'csv',
|
||||
'text/html' => ['html', 'htm'],
|
||||
'text/n3' => 'n3',
|
||||
'text/plain' => [
|
||||
'txt',
|
||||
'text',
|
||||
'conf',
|
||||
'def',
|
||||
'list',
|
||||
'log',
|
||||
'in',
|
||||
],
|
||||
'text/prs.lines.tag' => 'dsc',
|
||||
'text/richtext' => 'rtx',
|
||||
'text/sgml' => ['sgml', 'sgm'],
|
||||
'text/tab-separated-values' => 'tsv',
|
||||
'text/troff' => [
|
||||
't',
|
||||
'tr',
|
||||
'roff',
|
||||
'man',
|
||||
'me',
|
||||
'ms',
|
||||
],
|
||||
'text/turtle' => 'ttl',
|
||||
'text/uri-list' => ['uri', 'uris', 'urls'],
|
||||
'text/vcard' => 'vcard',
|
||||
'text/vnd.curl' => 'curl',
|
||||
'text/vnd.curl.dcurl' => 'dcurl',
|
||||
'text/vnd.curl.scurl' => 'scurl',
|
||||
'text/vnd.curl.mcurl' => 'mcurl',
|
||||
'text/vnd.dvb.subtitle' => 'sub',
|
||||
'text/vnd.fly' => 'fly',
|
||||
'text/vnd.fmi.flexstor' => 'flx',
|
||||
'text/vnd.graphviz' => 'gv',
|
||||
'text/vnd.in3d.3dml' => '3dml',
|
||||
'text/vnd.in3d.spot' => 'spot',
|
||||
'text/vnd.sun.j2me.app-descriptor' => 'jad',
|
||||
'text/vnd.wap.wml' => 'wml',
|
||||
'text/vnd.wap.wmlscript' => 'wmls',
|
||||
'text/x-asm' => ['s', 'asm'],
|
||||
'text/x-fortran' => ['f', 'for', 'f77', 'f90'],
|
||||
'text/x-java-source' => 'java',
|
||||
'text/x-opml' => 'opml',
|
||||
'text/x-pascal' => ['p', 'pas'],
|
||||
'text/x-nfo' => 'nfo',
|
||||
'text/x-setext' => 'etx',
|
||||
'text/x-sfv' => 'sfv',
|
||||
'text/x-uuencode' => 'uu',
|
||||
'text/x-vcalendar' => 'vcs',
|
||||
'text/x-vcard' => 'vcf',
|
||||
'video/3gpp' => '3gp',
|
||||
'video/3gpp2' => '3g2',
|
||||
'video/h261' => 'h261',
|
||||
'video/h263' => 'h263',
|
||||
'video/h264' => 'h264',
|
||||
'video/jpeg' => 'jpgv',
|
||||
'video/jpm' => ['jpm', 'jpgm'],
|
||||
'video/mj2' => 'mj2',
|
||||
'video/mp4' => 'mp4',
|
||||
'video/mpeg' => ['mpeg', 'mpg', 'mpe', 'm1v', 'm2v'],
|
||||
'video/ogg' => 'ogv',
|
||||
'video/quicktime' => ['qt', 'mov'],
|
||||
'video/vnd.dece.hd' => ['uvh', 'uvvh'],
|
||||
'video/vnd.dece.mobile' => ['uvm', 'uvvm'],
|
||||
'video/vnd.dece.pd' => ['uvp', 'uvvp'],
|
||||
'video/vnd.dece.sd' => ['uvs', 'uvvs'],
|
||||
'video/vnd.dece.video' => ['uvv', 'uvvv'],
|
||||
'video/vnd.dvb.file' => 'dvb',
|
||||
'video/vnd.fvt' => 'fvt',
|
||||
'video/vnd.mpegurl' => ['mxu', 'm4u'],
|
||||
'video/vnd.ms-playready.media.pyv' => 'pyv',
|
||||
'video/vnd.uvvu.mp4' => ['uvu', 'uvvu'],
|
||||
'video/vnd.vivo' => 'viv',
|
||||
'video/webm' => 'webm',
|
||||
'video/x-f4v' => 'f4v',
|
||||
'video/x-fli' => 'fli',
|
||||
'video/x-flv' => 'flv',
|
||||
'video/x-m4v' => 'm4v',
|
||||
'video/x-matroska' => ['mkv', 'mk3d', 'mks'],
|
||||
'video/x-mng' => 'mng',
|
||||
'video/x-ms-asf' => ['asf', 'asx'],
|
||||
'video/x-ms-vob' => 'vob',
|
||||
'video/x-ms-wm' => 'wm',
|
||||
'video/x-ms-wmv' => 'wmv',
|
||||
'video/x-ms-wmx' => 'wmx',
|
||||
'video/x-ms-wvx' => 'wvx',
|
||||
'video/x-msvideo' => 'avi',
|
||||
'video/x-sgi-movie' => 'movie',
|
||||
];
|
||||
|
||||
public function mimeType(): string
|
||||
{
|
||||
return array_rand($this->mimeTypes, 1);
|
||||
}
|
||||
|
||||
public function extension(): string
|
||||
{
|
||||
$extension = $this->mimeTypes[array_rand($this->mimeTypes, 1)];
|
||||
|
||||
return is_array($extension) ? $extension[array_rand($extension, 1)] : $extension;
|
||||
}
|
||||
|
||||
public function filePath(): string
|
||||
{
|
||||
return tempnam(sys_get_temp_dir(), 'faker');
|
||||
}
|
||||
}
|
||||
-83
@@ -1,83 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Faker\Core;
|
||||
|
||||
use Faker\Extension;
|
||||
|
||||
/**
|
||||
* @experimental This class is experimental and does not fall under our BC promise
|
||||
*/
|
||||
final class Number implements Extension\NumberExtension
|
||||
{
|
||||
public function numberBetween(int $min = 0, int $max = 2147483647): int
|
||||
{
|
||||
$int1 = min($min, $max);
|
||||
$int2 = max($min, $max);
|
||||
|
||||
return mt_rand($int1, $int2);
|
||||
}
|
||||
|
||||
public function randomDigit(): int
|
||||
{
|
||||
return $this->numberBetween(0, 9);
|
||||
}
|
||||
|
||||
public function randomDigitNot(int $except): int
|
||||
{
|
||||
$result = $this->numberBetween(0, 8);
|
||||
|
||||
if ($result >= $except) {
|
||||
++$result;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function randomDigitNotZero(): int
|
||||
{
|
||||
return $this->numberBetween(1, 9);
|
||||
}
|
||||
|
||||
public function randomFloat(?int $nbMaxDecimals = null, float $min = 0, ?float $max = null): float
|
||||
{
|
||||
if (null === $nbMaxDecimals) {
|
||||
$nbMaxDecimals = $this->randomDigit();
|
||||
}
|
||||
|
||||
if (null === $max) {
|
||||
$max = $this->randomNumber();
|
||||
|
||||
if ($min > $max) {
|
||||
$max = $min;
|
||||
}
|
||||
}
|
||||
|
||||
if ($min > $max) {
|
||||
$tmp = $min;
|
||||
$min = $max;
|
||||
$max = $tmp;
|
||||
}
|
||||
|
||||
return round($min + $this->numberBetween() / mt_getrandmax() * ($max - $min), $nbMaxDecimals);
|
||||
}
|
||||
|
||||
public function randomNumber(?int $nbDigits = null, bool $strict = false): int
|
||||
{
|
||||
if (null === $nbDigits) {
|
||||
$nbDigits = $this->randomDigitNotZero();
|
||||
}
|
||||
$max = 10 ** $nbDigits - 1;
|
||||
|
||||
if ($max > mt_getrandmax()) {
|
||||
throw new \InvalidArgumentException('randomNumber() can only generate numbers up to mt_getrandmax()');
|
||||
}
|
||||
|
||||
if ($strict) {
|
||||
return $this->numberBetween(10 ** ($nbDigits - 1), $max);
|
||||
}
|
||||
|
||||
return $this->numberBetween(0, $max);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user