22 Commits

Author SHA1 Message Date
inext 557bf3fcdf fix doc 2026-06-23 11:12:28 +02:00
inext f2ea5e160b fix vari - doc - viste 2026-06-23 08:48:11 +02:00
inext def8d22544 fix mailing an email 2026-06-23 08:05:51 +02:00
inext c5127da750 diagnose 2026-06-19 09:09:52 +02:00
inext baea181159 individui e gruppi 2026-06-18 19:41:27 +02:00
Mariano d2084b9d07 inizio sistemazione tag mailing list 2026-06-18 07:43:33 +02:00
Mariano 3b2ad1a0d5 altre migrations sistemate 2026-06-17 19:35:20 +02:00
Mariano 92ba47e99f modifica migrations 2026-06-17 19:18:48 +02:00
inext 9d649180a6 pèassaggio su rete esterna da sistemare tag mailinglist 2026-06-17 18:15:13 +02:00
inext 6909e21b67 pre version 2 2026-06-17 13:50:41 +02:00
inext c333bbce4e cancellazione modifiche dopo step before oauth 2026-06-16 21:43:24 +02:00
inext 6001c2e3b8 Revert OAuth2.0 2026-06-16 21:35:10 +02:00
inext 69f4d6a602 add OAUth2.0 2026-06-15 11:14:35 +02:00
inext 7973a94498 before OAuth2.0 implementation 2026-06-15 09:18:27 +02:00
inext 43a2c81312 setup viste 2026-06-10 14:03:43 +02:00
inext 0dd567cf7a email documenti 2026-06-10 06:42:49 +02:00
inext d9421c9159 ver 2.0.1.1 2026-06-09 12:04:03 +02:00
inext 7999c663fe ver 2.0.1 2026-06-09 11:44:40 +02:00
inext 06387568cb finale 2.0.0 2026-06-09 11:09:00 +02:00
inext d82b123bfd versione 2.0.0 2026-06-09 11:02:01 +02:00
inext 6f106e3130 vers 1.2.10 2026-06-08 21:04:06 +02:00
inext ce48481bc8 fix 1.2.9 2026-06-08 21:02:12 +02:00
5924 changed files with 688050 additions and 1777 deletions
+15 -5
View File
@@ -2,10 +2,18 @@ APP_NAME=Glastree
APP_ENV=local APP_ENV=local
APP_KEY=base64:aWz7908H9c7s+it9uMTwb6pyUrpddyMclcuN9Kzv7Ao= APP_KEY=base64:aWz7908H9c7s+it9uMTwb6pyUrpddyMclcuN9Kzv7Ao=
APP_DEBUG=true APP_DEBUG=true
APP_URL= APP_URL=http://glastree.soon.it
APP_PROTOCOL=https APP_PROTOCOL=http
FORCE_HTTPS=true FORCE_HTTPS=false
TRUSTED_PROXIES=* TRUSTED_PROXIES="10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1"
#GOOGLE_CLIENT_ID=980774223097-o5h7a6kepvg69te34fof2otn7ibi9uha.apps.googleusercontent.com
#GOOGLE_CLIENT_SECRET=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
GOOGLE_CLIENT_ID=416164569291-mss5favrg1qdmbk343plaj1898g3200f.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=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
APP_LOCALE=it APP_LOCALE=it
APP_FALLBACK_LOCALE=it APP_FALLBACK_LOCALE=it
@@ -30,9 +38,11 @@ DB_DATABASE=glastree
DB_USERNAME=glastree DB_USERNAME=glastree
DB_PASSWORD=glastree DB_PASSWORD=glastree
SESSION_DRIVER=file SESSION_DRIVER=database
SESSION_LIFETIME=120 SESSION_LIFETIME=120
SESSION_ENCRYPT=false SESSION_ENCRYPT=false
SESSION_SECURE_COOKIE=false
SESSION_SAME_SITE=lax
SESSION_PATH=/ SESSION_PATH=/
SESSION_DOMAIN=null SESSION_DOMAIN=null
+9 -3
View File
@@ -1,8 +1,12 @@
APP_NAME="Glastree" APP_NAME="Glastree"
APP_ENV=local APP_ENV=production
APP_KEY= APP_KEY=
APP_DEBUG=true APP_DEBUG=false
APP_URL=http://localhost APP_URL=http://localhost
APP_PROTOCOL=https
APP_SUBFOLDER=
FORCE_HTTPS=false
TRUSTED_PROXIES="10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1"
APP_LOCALE=it APP_LOCALE=it
APP_FALLBACK_LOCALE=en APP_FALLBACK_LOCALE=en
@@ -24,9 +28,11 @@ DB_DATABASE=glastree
DB_USERNAME=glastree DB_USERNAME=glastree
DB_PASSWORD= DB_PASSWORD=
SESSION_DRIVER=file SESSION_DRIVER=database
SESSION_LIFETIME=120 SESSION_LIFETIME=120
SESSION_ENCRYPT=false SESSION_ENCRYPT=false
SESSION_SECURE_COOKIE=false
SESSION_SAME_SITE=lax
SESSION_PATH=/ SESSION_PATH=/
SESSION_DOMAIN=null SESSION_DOMAIN=null
+1
View File
@@ -0,0 +1 @@
{"version":2,"defects":{"Tests\\Feature\\DocumentoLinkTest::test_admin_can_create_link_document":7,"Tests\\Feature\\DocumentoLinkTest::test_create_link_requires_url":8,"Tests\\Feature\\DocumentoLinkTest::test_create_link_validates_url_format":8,"Tests\\Feature\\DocumentoLinkTest::test_create_link_does_not_require_file":7,"Tests\\Feature\\DocumentoLinkTest::test_create_link_sets_tipo_to_link":7,"Tests\\Feature\\DocumentoLinkTest::test_create_link_with_visibility_individuo":7,"Tests\\Feature\\DocumentoLinkTest::test_create_link_with_visibility_gruppo":7,"Tests\\Feature\\DocumentoLinkTest::test_download_link_redirects_to_url":8,"Tests\\Feature\\DocumentoLinkTest::test_preview_link_returns_fallback_view":8,"Tests\\Feature\\DocumentoLinkTest::test_update_link_url":8,"Tests\\Feature\\DocumentoLinkTest::test_update_link_preserves_url_when_not_sent":8,"Tests\\Feature\\DocumentoLinkTest::test_update_link_validates_url_format":8,"Tests\\Feature\\DocumentoLinkTest::test_destroy_link_does_not_call_storage":8,"Tests\\Feature\\DocumentoLinkTest::test_mass_download_skips_links":7},"times":{"Tests\\Unit\\DocumentoLinkTest::test_is_link_returns_true_when_tipo_is_link":0.001,"Tests\\Unit\\DocumentoLinkTest::test_is_link_returns_false_when_tipo_is_upload":0.001,"Tests\\Unit\\DocumentoLinkTest::test_is_link_returns_false_when_tipo_is_allegato":0.001,"Tests\\Unit\\DocumentoLinkTest::test_is_link_returns_false_when_tipo_is_null":0.002,"Tests\\Unit\\DocumentoLinkTest::test_url_is_fillable":0.001,"Tests\\Unit\\DocumentoLinkTest::test_link_doc_has_no_file_fields":0.001,"Tests\\Feature\\DocumentoLinkTest::test_admin_can_create_link_document":0.064,"Tests\\Feature\\DocumentoLinkTest::test_create_link_requires_url":0.008,"Tests\\Feature\\DocumentoLinkTest::test_create_link_validates_url_format":0.007,"Tests\\Feature\\DocumentoLinkTest::test_create_link_does_not_require_file":0.006,"Tests\\Feature\\DocumentoLinkTest::test_create_link_sets_tipo_to_link":0.014,"Tests\\Feature\\DocumentoLinkTest::test_create_link_with_visibility_individuo":0.008,"Tests\\Feature\\DocumentoLinkTest::test_create_link_with_visibility_gruppo":0.008,"Tests\\Feature\\DocumentoLinkTest::test_download_link_redirects_to_url":0.005,"Tests\\Feature\\DocumentoLinkTest::test_preview_link_returns_fallback_view":0.009,"Tests\\Feature\\DocumentoLinkTest::test_update_link_url":0.008,"Tests\\Feature\\DocumentoLinkTest::test_update_link_preserves_url_when_not_sent":0.006,"Tests\\Feature\\DocumentoLinkTest::test_update_link_validates_url_format":0.006,"Tests\\Feature\\DocumentoLinkTest::test_destroy_link_does_not_call_storage":0.004,"Tests\\Feature\\DocumentoLinkTest::test_mass_download_skips_links":0.004}}
+27
View File
@@ -0,0 +1,27 @@
# Fix: tabella `google_o_auth_connections` mancante
## Problema
Migration `2026_06_15_000001_create_google_oauth_connections_table.php` segnata come "Ran" (batch 39) ma tabella fisica inesistente sul database MySQL/MariaDB. Causa 500 su GET `/impostazioni`.
## Comandi (eseguire sul server `192.168.222.177`)
```bash
# 1. Creare la tabella mancante con la stessa definizione della migration
php artisan tinker --execute="
Schema::create('google_o_auth_connections', function (\Illuminate\Database\Schema\Blueprint \$table) {
\$table->id();
\$table->string('service', 20)->unique();
\$table->string('client_id', 255);
\$table->text('client_secret');
\$table->text('refresh_token');
\$table->timestamps();
});
"
# 2. Pulire la cache delle viste (stale compiled view)
php artisan view:clear
# 3. Verificare che la tabella esista
php artisan tinker --execute="echo Schema::hasTable('google_o_auth_connections') ? 'OK' : 'ERRORE';"
```
+1012 -1
View File
File diff suppressed because it is too large Load Diff
Executable
+59
View File
@@ -0,0 +1,59 @@
# Proxy Configuration — Glastree
## Nginx Proxy Manager (Custom Nginx Configuration)
Nella scheda **Advanced** del tuo proxy host:
```nginx
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
```
---
## `.env` (App Laravel)
```env
APP_URL=https://glastree.soon.it
APP_PROTOCOL=https
FORCE_HTTPS=true
TRUSTED_PROXIES=*
```
Dopo qualsiasi modifica:
```bash
php artisan config:clear
```
---
## Google OAuth — Authorized Redirect URIs
```
https://glastree.soon.it/auth/google/callback
```
---
## `bootstrap/app.php`
```php
$middleware->trustProxies(at: '*');
```
---
## Schema richieste
```
Cliente (HTTPS)
Nginx Proxy Manager (termina TLS, inoltra HTTP + X-Forwarded-Proto: https)
App Laravel (vede richieste come HTTPS grazie a trustProxies(at:"*"))
```
+11 -3
View File
@@ -28,16 +28,17 @@ class BackupRunCommand extends Command
{ {
$this->info('Avvio backup...'); $this->info('Avvio backup...');
$options = [];
if ($this->option('no-files')) { if ($this->option('no-files')) {
$this->backupService->saveConfig(['backup_include_files' => false]); $options['include_files'] = false;
$this->warn('Files esclusi dal backup.'); $this->warn('Files esclusi dal backup.');
} }
if ($this->option('no-env')) { if ($this->option('no-env')) {
$this->backupService->saveConfig(['backup_include_env' => false]); $options['include_env'] = false;
$this->warn('.env escluso dal backup.'); $this->warn('.env escluso dal backup.');
} }
$result = $this->backupService->run(); $result = $this->backupService->run($options);
if ($result['success']) { if ($result['success']) {
$this->info(' Backup completato con successo!'); $this->info(' Backup completato con successo!');
@@ -45,6 +46,13 @@ class BackupRunCommand extends Command
$this->line('Dimensione: ' . $result['size_formatted']); $this->line('Dimensione: ' . $result['size_formatted']);
$this->line('Percorso: ' . storage_path('app/backups/' . $result['filename'])); $this->line('Percorso: ' . storage_path('app/backups/' . $result['filename']));
if (!empty($result['steps'])) {
$this->line('Contenuto:');
foreach ($result['steps'] as $step) {
$this->line(' - ' . $step);
}
}
return Command::SUCCESS; return Command::SUCCESS;
} }
+48
View File
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Enums;
enum GoogleService: string
{
case Email = 'email';
case Drive = 'drive';
case Calendar = 'calendar';
public function scopes(): array
{
return match ($this) {
self::Email => ['https://mail.google.com/'],
self::Drive => ['https://www.googleapis.com/auth/drive'],
self::Calendar => ['https://www.googleapis.com/auth/calendar'],
};
}
public function label(): string
{
return match ($this) {
self::Email => 'Gmail',
self::Drive => 'Google Drive',
self::Calendar => 'Google Calendar',
};
}
public function icon(): string
{
return match ($this) {
self::Email => 'fas fa-envelope',
self::Drive => 'fab fa-google-drive',
self::Calendar => 'fas fa-calendar-alt',
};
}
public function description(): string
{
return match ($this) {
self::Email => 'Invia e ricevi email tramite Gmail API (alternativa a SMTP/IMAP con password)',
self::Drive => 'Accedi ai file su Google Drive per documenti e allegati',
self::Calendar => 'Sincronizza eventi con Google Calendar',
};
}
}
+4
View File
@@ -132,6 +132,7 @@ class ReportColumnRegistry
'contatti.telefono' => ['label' => 'Telefono (da contatti)', 'type' => 'relation'], 'contatti.telefono' => ['label' => 'Telefono (da contatti)', 'type' => 'relation'],
'contatti.cellulare' => ['label' => 'Cellulare (da contatti)', 'type' => 'relation'], 'contatti.cellulare' => ['label' => 'Cellulare (da contatti)', 'type' => 'relation'],
'gruppi.nome' => ['label' => 'Gruppi', 'type' => 'relation'], 'gruppi.nome' => ['label' => 'Gruppi', 'type' => 'relation'],
'tags.nome' => ['label' => 'Tag', 'type' => 'relation'],
], ],
'gruppi' => [ 'gruppi' => [
@@ -151,6 +152,7 @@ class ReportColumnRegistry
'parent.nome' => ['label' => 'Gruppo Padre', 'type' => 'relation'], 'parent.nome' => ['label' => 'Gruppo Padre', 'type' => 'relation'],
'diocesi.nome' => ['label' => 'Diocesi', 'type' => 'relation'], 'diocesi.nome' => ['label' => 'Diocesi', 'type' => 'relation'],
'individui_count' => ['label' => 'Numero Membri', 'type' => 'relation'], 'individui_count' => ['label' => 'Numero Membri', 'type' => 'relation'],
'tags.nome' => ['label' => 'Tag', 'type' => 'relation'],
], ],
'eventi' => [ 'eventi' => [
@@ -171,6 +173,7 @@ class ReportColumnRegistry
'info_ricorrenza' => ['label' => 'Info Ricorrenza', 'type' => 'accessor'], 'info_ricorrenza' => ['label' => 'Info Ricorrenza', 'type' => 'accessor'],
'gruppi.nome' => ['label' => 'Gruppi', 'type' => 'relation'], 'gruppi.nome' => ['label' => 'Gruppi', 'type' => 'relation'],
'responsabili.nome_completo' => ['label' => 'Responsabili', 'type' => 'relation'], 'responsabili.nome_completo' => ['label' => 'Responsabili', 'type' => 'relation'],
'tags.nome' => ['label' => 'Tag', 'type' => 'relation'],
], ],
'documenti' => [ 'documenti' => [
@@ -183,6 +186,7 @@ class ReportColumnRegistry
'note' => ['label' => 'Note', 'type' => 'direct'], 'note' => ['label' => 'Note', 'type' => 'direct'],
'created_at' => ['label' => 'Data Caricamento', 'type' => 'direct'], 'created_at' => ['label' => 'Data Caricamento', 'type' => 'direct'],
'user.name' => ['label' => 'Caricato Da', 'type' => 'relation'], 'user.name' => ['label' => 'Caricato Da', 'type' => 'relation'],
'tags.nome' => ['label' => 'Tag', 'type' => 'relation'],
], ],
'contatti' => [ 'contatti' => [
@@ -34,8 +34,9 @@ class BackupController extends Controller
$result = $this->backupService->run(); $result = $this->backupService->run();
if ($result['success']) { if ($result['success']) {
$steps = !empty($result['steps']) ? ' — ' . implode(', ', $result['steps']) : '';
return redirect()->route('admin.backup.index') return redirect()->route('admin.backup.index')
->with('success', 'Backup completato: ' . ($result['filename'] ?? '') . ' (' . ($result['size_formatted'] ?? '') . ')'); ->with('success', 'Backup completato: ' . ($result['filename'] ?? '') . ' (' . ($result['size_formatted'] ?? '') . ')' . $steps);
} }
return redirect()->route('admin.backup.index') return redirect()->route('admin.backup.index')
@@ -2,15 +2,20 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Enums\GoogleService;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\EmailAttachment; use App\Models\EmailAttachment;
use App\Models\EmailFolder; use App\Models\EmailFolder;
use App\Models\EmailSetting; use App\Models\EmailSetting;
use App\Models\Firma;
use App\Models\SenderAccount; use App\Models\SenderAccount;
use App\Services\GoogleOAuthService;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
class EmailSettingsController extends Controller class EmailSettingsController extends Controller
{ {
@@ -19,7 +24,9 @@ class EmailSettingsController extends Controller
$this->authorizeWrite('settings'); $this->authorizeWrite('settings');
$settings = EmailSetting::first() ?? new EmailSetting(); $settings = EmailSetting::first() ?? new EmailSetting();
$senderAccounts = SenderAccount::orderBy('email_address')->get(); $senderAccounts = SenderAccount::orderBy('email_address')->get();
return view('admin.email-settings.index', compact('settings', 'senderAccounts')); $googleStatus = app(GoogleOAuthService::class)->getConnectionStatus();
$firme = $settings->exists ? $settings->firme : collect();
return view('admin.email-settings.index', compact('settings', 'senderAccounts', 'googleStatus', 'firme'));
} }
public function save(Request $request) public function save(Request $request)
@@ -38,11 +45,15 @@ class EmailSettingsController extends Controller
'smtp_password' => 'nullable|string', 'smtp_password' => 'nullable|string',
'email_address' => 'required|email', 'email_address' => 'required|email',
'email_name' => 'nullable|string|max:255', 'email_name' => 'nullable|string|max:255',
'from_email' => 'nullable|email|max:255',
'from_name' => 'nullable|string|max:255',
'reply_to' => 'nullable|email', 'reply_to' => 'nullable|email',
'sync_interval_minutes' => 'nullable|integer|min:1|max:60', 'sync_interval_minutes' => 'nullable|integer|min:1|max:60',
'is_active' => 'boolean', 'is_active' => 'boolean',
'signature' => 'nullable|string', 'signature' => 'nullable|string',
'signature_enabled' => 'boolean', 'signature_enabled' => 'boolean',
'auth_method' => 'nullable|in:password,oauth',
'google_oauth_connection_id' => 'nullable|integer|exists:google_oauth_connections,id',
]); ]);
if (!empty($validated['imap_password'])) { if (!empty($validated['imap_password'])) {
@@ -61,29 +72,17 @@ class EmailSettingsController extends Controller
$validated $validated
); );
// Verifica che i dati siano stati salvati
$verify = EmailSetting::find($saved->id); $verify = EmailSetting::find($saved->id);
if ($verify &&
$allGood = $verify &&
$verify->imap_host === $validated['imap_host'] && $verify->imap_host === $validated['imap_host'] &&
$verify->email_address === $validated['email_address']) { $verify->email_address === $validated['email_address'];
$signatureMatch = !array_key_exists('signature', $validated) || $verify->signature === $validated['signature']; $allGood = $allGood &&
$signatureEnabledMatch = !array_key_exists('signature_enabled', $validated) || (bool)$verify->signature_enabled === (bool)$validated['signature_enabled']; (!array_key_exists('signature', $validated) || $verify->signature === $validated['signature']) &&
(!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,
]);
}
if ($allGood) {
return back()->with('success', 'Impostazioni email salvate e verificate nel database.'); return back()->with('success', 'Impostazioni email salvate e verificate nel database.');
} else { } else {
return back()->with('error', 'Errore nel salvataggio: i dati non sono stati salvati correttamente.'); return back()->with('error', 'Errore nel salvataggio: i dati non sono stati salvati correttamente.');
@@ -99,7 +98,6 @@ class EmailSettingsController extends Controller
return response()->json(['success' => false, 'message' => 'Configurazione non trovata.']); return response()->json(['success' => false, 'message' => 'Configurazione non trovata.']);
} }
// Verifica che i dati siano salvati
if (empty($settings->imap_host) || empty($settings->email_address)) { if (empty($settings->imap_host) || empty($settings->email_address)) {
return response()->json(['success' => false, 'message' => 'Prima salva le impostazioni.']); return response()->json(['success' => false, 'message' => 'Prima salva le impostazioni.']);
} }
@@ -129,60 +127,27 @@ class EmailSettingsController extends Controller
return response()->json(['success' => false, 'message' => 'Configurazione non trovata.']); return response()->json(['success' => false, 'message' => 'Configurazione non trovata.']);
} }
if (empty($settings->smtp_host)) {
$settings->smtp_host = 'smtp.gmail.com';
$settings->smtp_port = 587;
$settings->smtp_encryption = 'tls';
}
try { try {
$smtpUsername = $settings->smtp_username; $mailer = $settings->getSmtpMailer();
$smtpPassword = $settings->smtp_password;
if (empty($smtpUsername)) { if ($mailer === null) {
$smtpUsername = $settings->email_address; return response()->json(['success' => false, 'message' => 'Impossibile creare il mailer SMTP. Verifica la configurazione.']);
}
if (empty($smtpPassword)) {
$smtpPassword = $settings->getDecryptedPassword();
} else {
$smtpPassword = Crypt::decryptString($smtpPassword);
} }
$encryption = $settings->smtp_encryption ?? 'tls'; $smtpUser = $settings->smtp_username ?: $settings->email_address;
$scheme = ($encryption === 'ssl') ? 'smtps' : 'smtp'; $fromName = $settings->getEffectiveFromName();
$replyToEmail = $settings->from_email ?: $smtpUser;
$dsn = sprintf( $email = (new Email())
'%s://%s:%s@%s:%d?encryption=%s', ->from(Address::create($smtpUser, $fromName))
$scheme,
rawurlencode($smtpUsername),
rawurlencode($smtpPassword),
$settings->smtp_host,
$settings->smtp_port ?? 587,
$encryption
);
$transport = \Symfony\Component\Mailer\Transport::fromDsn($dsn);
$mailer = new \Symfony\Component\Mailer\Mailer($transport);
$fromName = $settings->email_name ?? '';
if (filter_var($fromName, FILTER_VALIDATE_EMAIL)) {
$fromName = 'Glastree';
} else {
$fromName = preg_replace('/[^\p{L}\p{N}\s]/u', '', $fromName);
$fromName = trim($fromName);
if (empty($fromName) || strlen($fromName) > 50) {
$fromName = 'Glastree';
}
}
$fromAddress = \Symfony\Component\Mime\Address::create($settings->email_address, $fromName);
$email = (new \Symfony\Component\Mime\Email())
->from($fromAddress)
->to($request->email) ->to($request->email)
->subject('Test Configurazione SMTP - Glastree') ->subject('Test Configurazione SMTP - Glastree')
->text('Test email da Glastree - Configurazione SMTP funziona!'); ->text('Test email da Glastree - Configurazione SMTP funziona!');
if ($replyToEmail !== $smtpUser) {
$email->replyTo(Address::create($replyToEmail, $fromName));
}
$mailer->send($email); $mailer->send($email);
\Illuminate\Support\Facades\Log::info('Test SMTP ok', ['to' => $request->email, 'from' => $settings->email_address]); \Illuminate\Support\Facades\Log::info('Test SMTP ok', ['to' => $request->email, 'from' => $settings->email_address]);
@@ -227,6 +192,8 @@ class EmailSettingsController extends Controller
'verify_email' => 'nullable|email|max:255', 'verify_email' => 'nullable|email|max:255',
'note' => 'nullable|string|max:500', 'note' => 'nullable|string|max:500',
'is_active' => 'boolean', 'is_active' => 'boolean',
'auth_method' => 'nullable|in:password,oauth',
'google_oauth_connection_id' => 'nullable|integer|exists:google_oauth_connections,id',
]); ]);
if (!empty($data['smtp_password'])) { if (!empty($data['smtp_password'])) {
@@ -257,6 +224,8 @@ class EmailSettingsController extends Controller
'verify_email' => 'nullable|email|max:255', 'verify_email' => 'nullable|email|max:255',
'note' => 'nullable|string|max:500', 'note' => 'nullable|string|max:500',
'is_active' => 'boolean', 'is_active' => 'boolean',
'auth_method' => 'nullable|in:password,oauth',
'google_oauth_connection_id' => 'nullable|integer|exists:google_oauth_connections,id',
]); ]);
if (!empty($data['smtp_password'])) { if (!empty($data['smtp_password'])) {
@@ -326,4 +295,66 @@ class EmailSettingsController extends Controller
return response()->json(['success' => false, 'message' => 'Errore invio: ' . $e->getMessage()]); return response()->json(['success' => false, 'message' => 'Errore invio: ' . $e->getMessage()]);
} }
} }
public function firmaStore(Request $request)
{
$this->authorizeWrite('settings');
$data = $request->validate([
'nome' => 'required|string|max:255',
'contenuto_html' => 'nullable|string',
'is_default' => 'boolean',
]);
$settings = EmailSetting::getActive();
if (!$settings) {
return redirect('/impostazioni/email')->with('error', 'Account email non configurato.');
}
$firma = $settings->firme()->create($data);
if ($data['is_default'] ?? false) {
$settings->firme()->where('id', '!=', $firma->id)->update(['is_default' => false]);
}
return redirect('/impostazioni/email#firme')->with('success', 'Firma "' . $firma->nome . '" creata.');
}
public function firmaUpdate(Request $request, int $id)
{
$this->authorizeWrite('settings');
$data = $request->validate([
'nome' => 'required|string|max:255',
'contenuto_html' => 'nullable|string',
'is_default' => 'boolean',
]);
$firma = Firma::findOrFail($id);
$firma->update($data);
if ($data['is_default'] ?? false) {
Firma::where('email_setting_id', $firma->email_setting_id)
->where('id', '!=', $firma->id)
->update(['is_default' => false]);
}
return redirect('/impostazioni/email#firme')->with('success', 'Firma "' . $firma->nome . '" aggiornata.');
}
public function firmaSetDefault(int $id)
{
$this->authorizeWrite('settings');
$firma = Firma::findOrFail($id);
Firma::where('email_setting_id', $firma->email_setting_id)->update(['is_default' => false]);
$firma->update(['is_default' => true]);
return redirect('/impostazioni/email#firme')->with('success', 'Firma "' . $firma->nome . '" impostata come predefinita.');
}
public function firmaDestroy(int $id)
{
$this->authorizeWrite('settings');
$firma = Firma::findOrFail($id);
$nome = $firma->nome;
$firma->delete();
return redirect('/impostazioni/email#firme')->with('success', 'Firma "' . $nome . '" eliminata.');
}
} }
@@ -64,7 +64,7 @@ class CalendarioConnessioneController extends Controller
$connessione->is_active = $request->boolean('is_active', true); $connessione->is_active = $request->boolean('is_active', true);
if (!empty($data['config'])) { if (!empty($data['config'])) {
$connessione->encryptAndSetConfig($data['config']); $connessione->encryptAndSetConfig($data['config'], $connessione->config ?? []);
} }
$connessione->save(); $connessione->save();
+207 -21
View File
@@ -11,6 +11,7 @@ use App\Models\Individuo;
use App\Models\Gruppo; use App\Models\Gruppo;
use App\Models\Evento; use App\Models\Evento;
use App\Models\StorageRepository; use App\Models\StorageRepository;
use App\Models\Tag;
use App\Models\TipologiaDocumento; use App\Models\TipologiaDocumento;
use App\Services\StorageRepositoryService; use App\Services\StorageRepositoryService;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
@@ -42,18 +43,24 @@ class DocumentoController extends Controller
$folderId = $request->get('folder_id'); $folderId = $request->get('folder_id');
$repoId = $request->get('repository_id'); $repoId = $request->get('repository_id');
$query = Documento::with(['user', 'target', 'cartella', 'repository']) $query = Documento::with(['user', 'target', 'cartella', 'repository', 'tags'])
->orderByDesc('created_at'); ->orderByDesc('created_at');
if ($request->filled('tag')) {
$tagSlugs = (array) $request->input('tag');
$query->withAnyTags($tagSlugs);
}
if ($repoId) { if ($repoId) {
$query->where('repository_id', $repoId); $query->where('repository_id', $repoId);
} elseif ($folderId) { } elseif ($folderId) {
$query->where('cartella_id', $folderId); $query->where('cartella_id', $folderId);
} else { } else {
$query->whereNull('repository_id'); $query->whereNull('repository_id')->whereNull('cartella_id');
} }
$documenti = $query->paginate(20); $documenti = $query->paginate(20);
$allTags = Tag::orderBy('name')->get();
$cartelle = DocumentoCartella::with('children') $cartelle = DocumentoCartella::with('children')
->whereNull('parent_id') ->whereNull('parent_id')
->orderBy('nome') ->orderBy('nome')
@@ -85,11 +92,45 @@ class DocumentoController extends Controller
'children' => $f->children->map(fn($c) => ['id' => $c->id, 'nome' => $c->nome]), 'children' => $f->children->map(fn($c) => ['id' => $c->id, 'nome' => $c->nome]),
]); ]);
$vista = \App\Models\VistaReport::where('user_id', auth()->id())
->where('tipo', 'documenti')
->where('is_default', true)
->first();
$userVistas = \App\Models\VistaReport::where('user_id', auth()->id())
->where('tipo', 'documenti')
->orderBy('is_default', 'desc')
->orderBy('nome')
->get();
$allColumnDefs = [
['key' => 'nome_file', 'label' => 'Nome'],
['key' => 'tipologia', 'label' => 'Tipologia'],
['key' => 'dimensione', 'label' => 'Dimensione'],
['key' => 'visibilita', 'label' => 'Contesto'],
['key' => 'tag', 'label' => 'Tag'],
['key' => 'created_at', 'label' => 'Data'],
];
$defaultVisible = ['nome_file', 'tipologia', 'dimensione', 'visibilita', 'tag', 'created_at'];
$visibleColumns = $vista && !empty($vista->colonne_visibili)
? $vista->colonne_visibili
: $defaultVisible;
$tableColumns = array_map(fn($col) => [
'key' => $col['key'],
'label' => $col['label'],
'visible' => in_array($col['key'], $visibleColumns),
], $allColumnDefs);
$entityType = 'documenti';
$columnWidths = $vista && $vista->colonne_larghezze ? $vista->colonne_larghezze : [];
return view('documenti.index', compact( return view('documenti.index', compact(
'documenti', 'cartelle', 'currentFolder', 'breadcrumb', 'sottoCartelle', 'documenti', 'cartelle', 'currentFolder', 'breadcrumb', 'sottoCartelle',
'currentRepo', 'repositories', 'currentRepo', 'repositories',
'individui', 'gruppi', 'eventi', 'mailingLists', 'tipologie', 'individui', 'gruppi', 'eventi', 'mailingLists', 'tipologie',
'cartelleMoveOptions' 'cartelleMoveOptions', 'allTags',
'vista', 'visibleColumns', 'tableColumns', 'entityType', 'columnWidths',
'userVistas'
)); ));
} }
@@ -106,13 +147,15 @@ class DocumentoController extends Controller
public function edit($documento) public function edit($documento)
{ {
$this->authorizeWrite('documenti'); $this->authorizeWrite('documenti');
$documento = Documento::with('target')->findOrFail($documento); $documento = Documento::with(['target', 'tags'])->findOrFail($documento);
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get(); $individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
$gruppi = Gruppo::orderBy('nome')->get(); $gruppi = Gruppo::orderBy('nome')->get();
$eventi = Evento::orderBy('nome_evento')->get(); $eventi = Evento::orderBy('nome_evento')->get();
$mailingLists = \App\Models\MailingList::orderBy('nome')->get(); $mailingLists = \App\Models\MailingList::orderBy('nome')->get();
$tipologie = TipologiaDocumento::attive(); $tipologie = TipologiaDocumento::attive();
return view('documenti.edit', compact('documento', 'individui', 'gruppi', 'eventi', 'mailingLists', 'tipologie')); $tags = Tag::orderBy('name')->get();
$selectedTags = $documento->tags->pluck('id')->toArray();
return view('documenti.edit', compact('documento', 'individui', 'gruppi', 'eventi', 'mailingLists', 'tipologie', 'tags', 'selectedTags'));
} }
public function options(Request $request) public function options(Request $request)
@@ -158,50 +201,115 @@ class DocumentoController extends Controller
$tipologieValidi = TipologiaDocumento::opzioni(); $tipologieValidi = TipologiaDocumento::opzioni();
$tipologieRule = 'in:' . implode(',', $tipologieValidi); $tipologieRule = 'in:' . implode(',', $tipologieValidi);
$data = $request->validate([ $rules = [
'nome_file' => 'required|string|max:255', 'nome_file' => 'required|string|max:255',
'tipologia' => 'required|' . $tipologieRule, 'tipologia' => 'required|' . $tipologieRule,
'visibilita' => 'nullable|string', 'contesto_tipo' => 'nullable|in:individuo,gruppo,evento,mailing',
'visibilita_target_id' => 'nullable|integer', 'visibilita_target_id' => 'nullable|integer',
'visibilita_target_type' => 'nullable|string',
'note' => 'nullable|string', 'note' => 'nullable|string',
]); 'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
];
$data['visibilita'] = $request->contesto_tipo ?: 'pubblico'; if ($documento->isLink()) {
$rules['url'] = 'nullable|url|max:2048';
}
if ($request->contesto_tipo) { $data = $request->validate($rules);
$contestoTipo = $data['contesto_tipo'] ?? null;
$data['visibilita'] = $contestoTipo ?: 'pubblico';
if ($contestoTipo) {
$typeMap = [ $typeMap = [
'individuo' => Individuo::class, 'individuo' => Individuo::class,
'gruppo' => Gruppo::class, 'gruppo' => Gruppo::class,
'evento' => Evento::class, 'evento' => Evento::class,
'mailing' => \App\Models\MailingList::class, 'mailing' => \App\Models\MailingList::class,
]; ];
$data['visibilita_target_type'] = $typeMap[$request->contesto_tipo] ?? null; $data['visibilita_target_type'] = $typeMap[$contestoTipo] ?? null;
} else { } else {
$data['visibilita'] = 'pubblico'; $data['visibilita'] = 'pubblico';
$data['visibilita_target_id'] = null; $data['visibilita_target_id'] = null;
$data['visibilita_target_type'] = null; $data['visibilita_target_type'] = null;
} }
if ($documento->isLink()) {
$data['url'] = $data['url'] ?? $documento->url;
}
$documento->update($data); $documento->update($data);
return redirect('/documenti')->with('success', 'Documento aggiornato.'); if ($request->has('tags')) {
$documento->tags()->sync($request->tags);
} else {
$documento->tags()->detach();
}
$redirect = $request->input('folder_id') ? '/documenti?folder_id=' . $request->input('folder_id') : '/documenti';
return redirect($redirect)->with('success', 'Documento aggiornato.');
} }
public function store(Request $request) public function store(Request $request)
{ {
$this->authorizeWrite('documenti'); $this->authorizeWrite('documenti');
$data = $request->validate([ $tipologieValidi = TipologiaDocumento::opzioni();
$tipologieRule = 'in:' . implode(',', $tipologieValidi);
$isLink = $request->input('tipo_caricamento') === 'link';
$rules = [
'nome_file' => 'required|string|max:255', 'nome_file' => 'required|string|max:255',
'tipologia' => 'required|in:avatar,galleria,documento,statuto,altro', 'tipologia' => 'required|' . $tipologieRule,
'file' => 'required|file|max:10240',
'visibilita' => 'required|in:pubblico,individuo,gruppo', 'visibilita' => 'required|in:pubblico,individuo,gruppo',
'visibilita_target_id' => 'nullable|integer', 'visibilita_target_id' => 'nullable|integer',
'visibilita_target_type' => 'nullable|string', 'visibilita_target_type' => 'nullable|string',
'cartella_id' => 'nullable|integer|exists:documenti_cartelle,id', 'cartella_id' => 'nullable|integer|exists:documenti_cartelle,id',
'repository_id' => 'nullable|integer|exists:storage_repositories,id', 'repository_id' => 'nullable|integer|exists:storage_repositories,id',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
'tipo_caricamento' => 'required|in:file,link',
'url' => 'required_if:tipo_caricamento,link|nullable|url|max:2048',
];
if (!$isLink) {
$rules['file'] = 'required|file|max:10240';
}
$data = $request->validate($rules);
if ($isLink) {
$linkVisibilitaTargetType = null;
if ($data['visibilita'] === 'individuo' && $data['visibilita_target_id']) {
$linkVisibilitaTargetType = Individuo::class;
} elseif ($data['visibilita'] === 'gruppo' && $data['visibilita_target_id']) {
$linkVisibilitaTargetType = Gruppo::class;
}
$doc = Documento::create([
'nome_file' => $data['nome_file'],
'url' => $data['url'],
'tipo' => 'link',
'tipologia' => $data['tipologia'],
'visibilita' => $data['visibilita'],
'visibilita_target_id' => $data['visibilita_target_id'] ?? null,
'visibilita_target_type' => $linkVisibilitaTargetType,
'user_id' => auth()->id(),
'cartella_id' => $data['cartella_id'] ?? null,
'repository_id' => null,
]); ]);
if ($doc && $request->has('tags')) {
$doc->tags()->sync($request->tags);
}
$redirect = $request->_redirect ?? url()->previous();
if ($request->filled('folder_id')) {
$redirect = '/documenti?folder_id=' . $request->input('folder_id');
}
return redirect($redirect)->with('success', 'Documento (link) creato.');
}
$repositoryId = $data['repository_id'] ?? null; $repositoryId = $data['repository_id'] ?? null;
if ($repositoryId) { if ($repositoryId) {
@@ -233,7 +341,7 @@ class DocumentoController extends Controller
$visibilitaTargetType = Gruppo::class; $visibilitaTargetType = Gruppo::class;
} }
Documento::create([ $doc = Documento::create([
'nome_file' => $data['nome_file'], 'nome_file' => $data['nome_file'],
'file_path' => $filePath, 'file_path' => $filePath,
'storage_disk' => $disk ?? null, 'storage_disk' => $disk ?? null,
@@ -249,7 +357,13 @@ class DocumentoController extends Controller
'repository_id' => $repositoryId, 'repository_id' => $repositoryId,
]); ]);
if ($doc && $request->has('tags')) {
$doc->tags()->sync($request->tags);
}
$redirect = $request->_redirect ?? url()->previous(); $redirect = $request->_redirect ?? url()->previous();
if ($request->filled('folder_id')) {
$redirect = '/documenti?folder_id=' . $request->input('folder_id');
}
return redirect($redirect)->with('success', 'Documento caricato.'); return redirect($redirect)->with('success', 'Documento caricato.');
} }
@@ -258,6 +372,10 @@ class DocumentoController extends Controller
$this->authorizeRead('documenti'); $this->authorizeRead('documenti');
$documento = Documento::findOrFail($documento); $documento = Documento::findOrFail($documento);
if ($documento->isLink()) {
return redirect()->away($documento->url);
}
$extension = pathinfo($documento->file_path, PATHINFO_EXTENSION); $extension = pathinfo($documento->file_path, PATHINFO_EXTENSION);
$filename = $extension ? $documento->nome_file . '.' . $extension : $documento->nome_file; $filename = $extension ? $documento->nome_file . '.' . $extension : $documento->nome_file;
@@ -303,6 +421,12 @@ class DocumentoController extends Controller
$this->authorizeRead('documenti'); $this->authorizeRead('documenti');
$documento = Documento::findOrFail($documento); $documento = Documento::findOrFail($documento);
if ($documento->isLink()) {
return response()->view('documenti.preview-fallback', [
'documento' => $documento,
])->header('X-Preview-Fallback', 'true');
}
$previewableMimeTypes = [ $previewableMimeTypes = [
'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml', 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
'application/pdf', 'application/pdf',
@@ -353,6 +477,7 @@ class DocumentoController extends Controller
$this->authorizeDelete('documenti'); $this->authorizeDelete('documenti');
$documento = Documento::findOrFail($documento); $documento = Documento::findOrFail($documento);
if (!$documento->isLink()) {
if ($documento->repository_id) { if ($documento->repository_id) {
$repo = StorageRepository::find($documento->repository_id); $repo = StorageRepository::find($documento->repository_id);
if ($repo) { if ($repo) {
@@ -367,6 +492,7 @@ class DocumentoController extends Controller
Storage::disk($disk)->delete($documento->file_path); Storage::disk($disk)->delete($documento->file_path);
} }
} }
}
$documento->delete(); $documento->delete();
@@ -443,9 +569,10 @@ class DocumentoController extends Controller
{ {
$idsInput = $request->input('ids', ''); $idsInput = $request->input('ids', '');
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []); $ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
$ids = array_values(array_filter($ids, fn($v) => is_numeric($v)));
if (empty($ids)) { if (empty($ids)) {
return response()->json(['linked_count' => 0, 'links' => []]); return back()->with('error', 'Nessun documento selezionato.');
} }
$documenti = Documento::whereIn('id', $ids)->get(); $documenti = Documento::whereIn('id', $ids)->get();
@@ -521,6 +648,7 @@ class DocumentoController extends Controller
$this->authorizeRead('documenti'); $this->authorizeRead('documenti');
$idsInput = $request->input('ids', ''); $idsInput = $request->input('ids', '');
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []); $ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
$ids = array_values(array_filter($ids, fn($v) => is_numeric($v)));
if (empty($ids)) { if (empty($ids)) {
return back()->with('error', 'Nessun documento selezionato.'); return back()->with('error', 'Nessun documento selezionato.');
@@ -539,8 +667,14 @@ class DocumentoController extends Controller
return back()->with('error', 'Impossibile creare l\'archivio ZIP.'); return back()->with('error', 'Impossibile creare l\'archivio ZIP.');
} }
$skippedLinks = 0;
$addedCount = 0; $addedCount = 0;
foreach ($documenti as $documento) { foreach ($documenti as $documento) {
if ($documento->isLink()) {
$skippedLinks++;
continue;
}
$extension = pathinfo($documento->file_path, PATHINFO_EXTENSION); $extension = pathinfo($documento->file_path, PATHINFO_EXTENSION);
$filename = $extension ? $documento->nome_file . '.' . $extension : $documento->nome_file; $filename = $extension ? $documento->nome_file . '.' . $extension : $documento->nome_file;
@@ -573,11 +707,23 @@ class DocumentoController extends Controller
$zip->close(); $zip->close();
if ($addedCount === 0) { if ($addedCount === 0) {
unlink($zipPath); if (file_exists($zipPath)) {
return back()->with('error', 'Nessun file disponibile per il download.'); @unlink($zipPath);
}
$message = 'Nessun file disponibile per il download.';
if ($skippedLinks > 0) {
$message .= " ({$skippedLinks} link esterni saltati)";
}
return back()->with('error', $message);
} }
return response()->download($zipPath, 'documenti.zip')->deleteFileAfterSend(true); $message = $addedCount . ' file scaricati.';
if ($skippedLinks > 0) {
$message .= " {$skippedLinks} link esterni saltati.";
}
return response()->download($zipPath, 'documenti.zip')->deleteFileAfterSend(true)
->with('success', $message);
} }
public function massDestroy(Request $request) public function massDestroy(Request $request)
@@ -585,6 +731,7 @@ class DocumentoController extends Controller
$this->authorizeDelete('documenti'); $this->authorizeDelete('documenti');
$idsInput = $request->input('ids', ''); $idsInput = $request->input('ids', '');
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []); $ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
$ids = array_values(array_filter($ids, fn($v) => is_numeric($v)));
if (empty($ids)) { if (empty($ids)) {
return back()->with('error', 'Nessun documento selezionato.'); return back()->with('error', 'Nessun documento selezionato.');
@@ -593,6 +740,7 @@ class DocumentoController extends Controller
$documenti = Documento::whereIn('id', $ids)->get(); $documenti = Documento::whereIn('id', $ids)->get();
foreach ($documenti as $documento) { foreach ($documenti as $documento) {
if (!$documento->isLink()) {
if ($documento->repository_id) { if ($documento->repository_id) {
$repo = StorageRepository::find($documento->repository_id); $repo = StorageRepository::find($documento->repository_id);
if ($repo) { if ($repo) {
@@ -607,6 +755,7 @@ class DocumentoController extends Controller
Storage::disk($disk)->delete($documento->file_path); Storage::disk($disk)->delete($documento->file_path);
} }
} }
}
$documento->delete(); $documento->delete();
} }
@@ -680,6 +829,7 @@ class DocumentoController extends Controller
$this->authorizeWrite('documenti'); $this->authorizeWrite('documenti');
$idsInput = $request->input('ids', ''); $idsInput = $request->input('ids', '');
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []); $ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
$ids = array_values(array_filter($ids, fn($v) => is_numeric($v)));
if (empty($ids)) { if (empty($ids)) {
return back()->with('error', 'Nessun documento selezionato.'); return back()->with('error', 'Nessun documento selezionato.');
@@ -706,6 +856,42 @@ class DocumentoController extends Controller
return back()->with('success', count($ids) . ' documenti associati.'); return back()->with('success', count($ids) . ' documenti associati.');
} }
public function massTag(Request $request)
{
$this->authorizeWrite('documenti');
$idsInput = $request->input('ids', '');
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
$ids = array_values(array_filter($ids, fn($v) => is_numeric($v)));
if (empty($ids)) {
return back()->with('error', 'Nessun documento selezionato.');
}
$data = $request->validate([
'tags' => 'required|array',
'tags.*' => 'exists:tags,id',
'mode' => 'required|in:assign,remove',
]);
$tagIds = $data['tags'];
$mode = $data['mode'];
$count = 0;
Documento::whereIn('id', $ids)->chunk(100, function ($documenti) use ($tagIds, $mode, &$count) {
foreach ($documenti as $documento) {
if ($mode === 'assign') {
$documento->tags()->syncWithoutDetaching($tagIds);
} else {
$documento->tags()->detach($tagIds);
}
$count++;
}
});
$actionLabel = $mode === 'assign' ? 'assegnati' : 'rimossi';
return back()->with('success', "Tag $actionLabel per $count documenti.");
}
public function massMove(Request $request): RedirectResponse public function massMove(Request $request): RedirectResponse
{ {
$this->authorizeWrite('documenti'); $this->authorizeWrite('documenti');
+102 -44
View File
@@ -11,6 +11,7 @@ use App\Models\MailingList;
use App\Models\Gruppo; use App\Models\Gruppo;
use App\Models\Individuo; use App\Models\Individuo;
use App\Models\Documento; use App\Models\Documento;
use App\Models\Firma;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
@@ -94,6 +95,11 @@ class EmailController extends Controller
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get(); $individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
$senderAccounts = SenderAccount::active()->get(); $senderAccounts = SenderAccount::active()->get();
$documenti = Documento::orderBy('nome_file')->get();
$settings = EmailSetting::getActive();
$firme = $settings?->firme ?? collect();
$prefill = []; $prefill = [];
if ($request->reply_to) { if ($request->reply_to) {
$original = EmailMessage::find($request->reply_to); $original = EmailMessage::find($request->reply_to);
@@ -111,7 +117,7 @@ class EmailController extends Controller
} }
} }
return view('email.compose', compact('folders', 'mailingLists', 'gruppi', 'individui', 'prefill', 'senderAccounts')); return view('email.compose', compact('folders', 'mailingLists', 'gruppi', 'individui', 'documenti', 'prefill', 'senderAccounts', 'firme'));
} }
public function send(Request $request) public function send(Request $request)
@@ -126,6 +132,7 @@ class EmailController extends Controller
$rules['to'] = 'required'; $rules['to'] = 'required';
} }
$rules['firma_id'] = 'nullable|integer|exists:firme,id';
$validated = $request->validate($rules); $validated = $request->validate($rules);
$recipients = $this->resolveRecipients($request); $recipients = $this->resolveRecipients($request);
@@ -155,12 +162,18 @@ class EmailController extends Controller
$imapSuccess = false; $imapSuccess = false;
$imapError = null; $imapError = null;
$attachments = $this->processAttachments($request); $attachmentResult = $this->processAttachments($request);
$attachmentPaths = $attachmentResult['paths'];
$linkDocs = $attachmentResult['links'];
$firmaId = $request->firma_id;
$body = $this->appendLinksToBody($validated['body'], $linkDocs);
foreach ($recipients as $recipient) { foreach ($recipients as $recipient) {
try { try {
$bodyWithSig = $this->appendSignature($validated['body'], $settings->getSignature(), $settings->signature_enabled ?? false); $bodyWithSig = $this->appendSignature($body, $firmaId, $settings);
$this->sendViaImap($settings, $recipient, $validated['subject'], $bodyWithSig, $request->cc, $attachments); $this->sendViaImap($settings, $recipient, $validated['subject'], $bodyWithSig, $request->cc, $attachmentPaths);
$imapSuccess = true; $imapSuccess = true;
} catch (\Exception $e) { } catch (\Exception $e) {
$imapError = $e->getMessage(); $imapError = $e->getMessage();
@@ -169,7 +182,7 @@ class EmailController extends Controller
} }
try { try {
$bodyWithSig = $this->appendSignature($validated['body'], $settings->getSignature(), $settings->signature_enabled ?? false); $bodyWithSig = $this->appendSignature($body, $firmaId, $settings);
$this->storeSentMessage($settings, $recipients, $validated['subject'], $bodyWithSig, $request->cc); $this->storeSentMessage($settings, $recipients, $validated['subject'], $bodyWithSig, $request->cc);
} catch (\Exception $e) { } catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Store sent message error', ['error' => $e->getMessage()]); \Illuminate\Support\Facades\Log::error('Store sent message error', ['error' => $e->getMessage()]);
@@ -186,13 +199,20 @@ class EmailController extends Controller
private function sendWithSender(Request $request, array $validated, array $recipients) private function sendWithSender(Request $request, array $validated, array $recipients)
{ {
$sender = SenderAccount::findOrFail($request->mittente_id); $sender = SenderAccount::findOrFail($request->mittente_id);
$attachmentPaths = $this->resolveAttachmentPaths($request); $attachmentResult = $this->resolveAttachmentPaths($request);
$attachmentPaths = $attachmentResult['paths'];
$linkDocs = $attachmentResult['links'];
$settings = EmailSetting::getActive();
$body = $this->appendLinksToBody($validated['body'], $linkDocs);
$body = $this->appendSignature($body, $request->firma_id, $settings);
$ok = 0; $ok = 0;
$errors = []; $errors = [];
foreach ($recipients as $recipient) { foreach ($recipients as $recipient) {
try { try {
$sender->sendEmail($recipient, $validated['subject'], $validated['body'], $attachmentPaths); $sender->sendEmail($recipient, $validated['subject'], $body, $attachmentPaths);
$ok++; $ok++;
} catch (\Exception $e) { } catch (\Exception $e) {
$errors[] = $recipient . ': ' . $e->getMessage(); $errors[] = $recipient . ': ' . $e->getMessage();
@@ -235,8 +255,8 @@ class EmailController extends Controller
'body_text' => $request->body, 'body_text' => $request->body,
'to_email' => $request->to, 'to_email' => $request->to,
'is_draft' => true, 'is_draft' => true,
'from_email' => $settings->email_address ?? '', 'from_email' => $settings->getEffectiveFromAddress(),
'from_name' => $settings->email_name ?? '', 'from_name' => $settings->getEffectiveFromName(),
]); ]);
return response()->json(['success' => true, 'draft_id' => $message->id]); return response()->json(['success' => true, 'draft_id' => $message->id]);
@@ -341,7 +361,7 @@ class EmailController extends Controller
$message->delete(); $message->delete();
} }
return back()->with('success', 'Email spostata nel cestino.'); return redirect()->route('email.index', ['folder' => 'inbox'])->with('success', 'Email spostata nel cestino.');
} }
public function downloadAttachment($id) public function downloadAttachment($id)
@@ -494,12 +514,20 @@ class EmailController extends Controller
return array_filter(array_unique($recipients)); return array_filter(array_unique($recipients));
} }
private function appendSignature(string $body, ?string $signature, bool $enabled): string private function appendSignature(string $body, int|string|null $firmaId = null, ?EmailSetting $settings = null): string
{ {
if (empty($signature) || !$enabled) { if ($firmaId) {
return $body; $firma = Firma::find($firmaId);
if ($firma && $firma->contenuto_html) {
return $body . "\n\n---\n" . $firma->contenuto_html;
} }
return $body . "\n\n---\n" . $signature; }
if ($settings && $settings->signature && ($settings->signature_enabled ?? false)) {
return $body . "\n\n---\n" . $settings->signature;
}
return $body;
} }
private function sendViaImap($settings, $to, $subject, $body, $cc = null, array $attachmentPaths = []) private function sendViaImap($settings, $to, $subject, $body, $cc = null, array $attachmentPaths = [])
@@ -525,18 +553,10 @@ class EmailController extends Controller
$transport = \Symfony\Component\Mailer\Transport::fromDsn($dsn); $transport = \Symfony\Component\Mailer\Transport::fromDsn($dsn);
$mailer = new \Symfony\Component\Mailer\Mailer($transport); $mailer = new \Symfony\Component\Mailer\Mailer($transport);
$fromName = $settings->email_name ?? ''; $fromAddress = \Symfony\Component\Mime\Address::create(
if (filter_var($fromName, FILTER_VALIDATE_EMAIL)) { $settings->getEffectiveFromAddress(),
$fromName = 'Glastree'; $settings->getEffectiveFromName()
} else { );
$fromName = preg_replace('/[^\p{L}\p{N}\s]/u', '', $fromName);
$fromName = trim($fromName);
if (empty($fromName)) {
$fromName = 'Glastree';
}
}
$fromAddress = \Symfony\Component\Mime\Address::create($settings->email_address, $fromName);
$email = (new \Symfony\Component\Mime\Email()) $email = (new \Symfony\Component\Mime\Email())
->from($fromAddress) ->from($fromAddress)
@@ -544,6 +564,11 @@ class EmailController extends Controller
->subject($subject) ->subject($subject)
->text($body); ->text($body);
$replyTo = $settings->getEffectiveReplyTo();
if ($replyTo) {
$email->replyTo($replyTo);
}
if ($cc) { if ($cc) {
$email->cc($cc); $email->cc($cc);
} }
@@ -565,19 +590,7 @@ class EmailController extends Controller
private function processAttachments(Request $request): array private function processAttachments(Request $request): array
{ {
$paths = []; $paths = [];
if ($request->hasFile('allegati')) { $links = [];
foreach ($request->file('allegati') as $file) {
if ($file->isValid()) {
$paths[] = $file->getRealPath();
}
}
}
return $paths;
}
private function resolveAttachmentPaths(Request $request): array
{
$paths = [];
if ($request->hasFile('allegati')) { if ($request->hasFile('allegati')) {
foreach ($request->file('allegati') as $file) { foreach ($request->file('allegati') as $file) {
@@ -592,26 +605,71 @@ class EmailController extends Controller
$ids = is_array($documentIds) ? $documentIds : explode(',', $documentIds); $ids = is_array($documentIds) ? $documentIds : explode(',', $documentIds);
$docs = Documento::whereIn('id', $ids)->get(); $docs = Documento::whereIn('id', $ids)->get();
foreach ($docs as $doc) { foreach ($docs as $doc) {
if ($doc->file_path && \Illuminate\Support\Facades\Storage::exists($doc->file_path)) { if ($doc->isLink()) {
$links[] = ['nome' => $doc->nome_file, 'url' => $doc->url];
} elseif ($doc->file_path && Storage::exists($doc->file_path)) {
$paths[] = Storage::path($doc->file_path);
}
}
}
return ['paths' => $paths, 'links' => $links];
}
private function resolveAttachmentPaths(Request $request): array
{
$paths = [];
$links = [];
if ($request->hasFile('allegati')) {
foreach ($request->file('allegati') as $file) {
if ($file->isValid()) {
$paths[] = $file->getRealPath();
}
}
}
$documentIds = $request->input('documenti_selezionati');
if (!empty($documentIds)) {
$ids = is_array($documentIds) ? $documentIds : explode(',', $documentIds);
$docs = Documento::whereIn('id', $ids)->get();
foreach ($docs as $doc) {
if ($doc->isLink()) {
$links[] = ['nome' => $doc->nome_file, 'url' => $doc->url];
} elseif ($doc->file_path && \Illuminate\Support\Facades\Storage::exists($doc->file_path)) {
$paths[] = \Illuminate\Support\Facades\Storage::path($doc->file_path); $paths[] = \Illuminate\Support\Facades\Storage::path($doc->file_path);
} }
} }
} }
return $paths; return ['paths' => $paths, 'links' => $links];
}
private function appendLinksToBody(string $body, array $linkDocs): string
{
if (empty($linkDocs)) {
return $body;
}
$linksText = "\n\n---\nDocumenti collegati:\n";
foreach ($linkDocs as $link) {
$linksText .= "- {$link['nome']}: {$link['url']}\n";
}
return $body . $linksText;
} }
private function storeSentMessage($settings, $recipients, $subject, $body, $cc = null) private function storeSentMessage($settings, $recipients, $subject, $body, $cc = null)
{ {
$sentFolder = EmailFolder::where('type', 'sent')->first(); $sentFolder = EmailFolder::where('type', 'sent')->first();
$bodyWithSignature = $this->appendSignature($body, $settings->getSignature(), $settings->signature_enabled ?? false); $bodyWithSignature = $body;
$message = EmailMessage::create([ $message = EmailMessage::create([
'email_folder_id' => $sentFolder->id, 'email_folder_id' => $sentFolder->id,
'subject' => $subject, 'subject' => $subject,
'body_text' => $bodyWithSignature, 'body_text' => $bodyWithSignature,
'from_email' => $settings->email_address, 'from_email' => $settings->getEffectiveFromAddress(),
'from_name' => $settings->email_name, 'from_name' => $settings->getEffectiveFromName(),
'to_email' => implode(', ', $recipients), 'to_email' => implode(', ', $recipients),
'cc' => $cc, 'cc' => $cc,
'is_sent' => true, 'is_sent' => true,
+97 -6
View File
@@ -1,10 +1,13 @@
<?php <?php
declare(strict_types=1);
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Evento; use App\Models\Evento;
use App\Models\Gruppo; use App\Models\Gruppo;
use App\Models\Individuo; use App\Models\Individuo;
use App\Models\Tag;
use App\Models\TipologiaEvento; use App\Models\TipologiaEvento;
use App\Services\IcsExportService; use App\Services\IcsExportService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -16,7 +19,12 @@ class EventoController extends Controller
public function index(Request $request) public function index(Request $request)
{ {
$this->authorizeRead('eventi'); $this->authorizeRead('eventi');
$query = Evento::with(['gruppi', 'responsabili']); $query = Evento::with(['gruppi', 'responsabili', 'tags']);
if ($request->filled('tag')) {
$tagSlugs = (array) $request->input('tag');
$query->withAnyTags($tagSlugs);
}
if ($request->filled('search')) { if ($request->filled('search')) {
$query->where('nome_evento', 'like', '%' . $request->search . '%'); $query->where('nome_evento', 'like', '%' . $request->search . '%');
@@ -47,8 +55,44 @@ class EventoController extends Controller
$perPage = in_array((int) $request->perPage, [10, 20, 25, 50, 100]) ? (int) $request->perPage : 20; $perPage = in_array((int) $request->perPage, [10, 20, 25, 50, 100]) ? (int) $request->perPage : 20;
$eventi = $query->paginate($perPage); $eventi = $query->paginate($perPage);
$gruppi = Gruppo::orderBy('nome')->get(); $gruppi = Gruppo::orderBy('nome')->get();
$allTags = Tag::orderBy('name')->get();
return view('eventi.index', compact('eventi', 'gruppi')); $vista = \App\Models\VistaReport::where('user_id', auth()->id())
->where('tipo', 'eventi')
->where('is_default', true)
->first();
$userVistas = \App\Models\VistaReport::where('user_id', auth()->id())
->where('tipo', 'eventi')
->orderBy('is_default', 'desc')
->orderBy('nome')
->get();
$allColumnDefs = [
['key' => 'nome_evento', 'label' => 'Nome Evento'],
['key' => 'descrizione_evento', 'label' => 'Descrizione'],
['key' => 'tipo_evento', 'label' => 'Tipologia'],
['key' => 'tipo_recorrenza', 'label' => 'Ricorrenza'],
['key' => 'gruppi', 'label' => 'Gruppi'],
['key' => 'responsabili', 'label' => 'Responsabili'],
['key' => 'tag', 'label' => 'Tag'],
['key' => 'data_specifica', 'label' => 'Data'],
['key' => 'created_at', 'label' => 'Creato'],
];
$defaultVisible = ['nome_evento', 'descrizione_evento', 'tipo_evento', 'tipo_recorrenza', 'gruppi', 'responsabili', 'tag', 'created_at'];
$visibleColumns = $vista && !empty($vista->colonne_visibili)
? $vista->colonne_visibili
: $defaultVisible;
$tableColumns = array_map(fn($col) => [
'key' => $col['key'],
'label' => $col['label'],
'visible' => in_array($col['key'], $visibleColumns),
], $allColumnDefs);
$entityType = 'eventi';
$columnWidths = $vista && $vista->colonne_larghezze ? $vista->colonne_larghezze : [];
return view('eventi.index', compact('eventi', 'gruppi', 'allTags', 'vista', 'visibleColumns', 'tableColumns', 'entityType', 'columnWidths', 'userVistas'));
} }
public function create(Request $request) public function create(Request $request)
@@ -58,8 +102,9 @@ class EventoController extends Controller
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get(); $individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
$selectedGruppo = $request->query('gruppo_id') ? Gruppo::find($request->query('gruppo_id')) : null; $selectedGruppo = $request->query('gruppo_id') ? Gruppo::find($request->query('gruppo_id')) : null;
$tipologieEventi = TipologiaEvento::attive(); $tipologieEventi = TipologiaEvento::attive();
$tags = Tag::orderBy('name')->get();
return view('eventi.create', compact('gruppi', 'individui', 'selectedGruppo', 'tipologieEventi')); return view('eventi.create', compact('gruppi', 'individui', 'selectedGruppo', 'tipologieEventi', 'tags'));
} }
public function store(Request $request) public function store(Request $request)
@@ -88,6 +133,8 @@ class EventoController extends Controller
'responsabili.*' => 'exists:individui,id', 'responsabili.*' => 'exists:individui,id',
'luogo_indirizzo' => 'nullable|string|max:500', 'luogo_indirizzo' => 'nullable|string|max:500',
'luogo_url_maps' => 'nullable|string', 'luogo_url_maps' => 'nullable|string',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]); ]);
$evento = Evento::create([ $evento = Evento::create([
@@ -110,6 +157,10 @@ class EventoController extends Controller
'luogo_url_maps' => $data['luogo_url_maps'] ?? null, 'luogo_url_maps' => $data['luogo_url_maps'] ?? null,
]); ]);
if (!empty($data['tags'])) {
$evento->tags()->attach($data['tags']);
}
if (!empty($data['gruppi'])) { if (!empty($data['gruppi'])) {
$evento->gruppi()->attach($data['gruppi']); $evento->gruppi()->attach($data['gruppi']);
} }
@@ -124,19 +175,21 @@ class EventoController extends Controller
public function show($evento) public function show($evento)
{ {
$this->authorizeRead('eventi'); $this->authorizeRead('eventi');
$evento = Evento::with(['gruppi', 'responsabili.contatti', 'documenti'])->findOrFail($evento); $evento = Evento::with(['gruppi', 'responsabili.contatti', 'documenti', 'tags'])->findOrFail($evento);
return view('eventi.show', compact('evento')); return view('eventi.show', compact('evento'));
} }
public function edit($evento) public function edit($evento)
{ {
$this->authorizeWrite('eventi'); $this->authorizeWrite('eventi');
$evento = Evento::with(['gruppi', 'responsabili', 'documenti'])->findOrFail($evento); $evento = Evento::with(['gruppi', 'responsabili', 'documenti', 'tags'])->findOrFail($evento);
$gruppi = Gruppo::orderBy('nome')->get(); $gruppi = Gruppo::orderBy('nome')->get();
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get(); $individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
$tipologieEventi = TipologiaEvento::attive(); $tipologieEventi = TipologiaEvento::attive();
$tags = Tag::orderBy('name')->get();
$selectedTags = $evento->tags->pluck('id')->toArray();
return view('eventi.edit', compact('evento', 'gruppi', 'individui', 'tipologieEventi')); return view('eventi.edit', compact('evento', 'gruppi', 'individui', 'tipologieEventi', 'tags', 'selectedTags'));
} }
public function update(Request $request, $evento) public function update(Request $request, $evento)
@@ -167,6 +220,8 @@ class EventoController extends Controller
'responsabili.*' => 'exists:individui,id', 'responsabili.*' => 'exists:individui,id',
'luogo_indirizzo' => 'nullable|string|max:500', 'luogo_indirizzo' => 'nullable|string|max:500',
'luogo_url_maps' => 'nullable|string', 'luogo_url_maps' => 'nullable|string',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]); ]);
$evento->update([ $evento->update([
@@ -189,6 +244,7 @@ class EventoController extends Controller
'luogo_url_maps' => $data['luogo_url_maps'] ?? null, 'luogo_url_maps' => $data['luogo_url_maps'] ?? null,
]); ]);
$evento->tags()->sync($data['tags'] ?? []);
$evento->gruppi()->sync($data['gruppi'] ?? []); $evento->gruppi()->sync($data['gruppi'] ?? []);
$evento->responsabili()->sync($data['responsabili'] ?? []); $evento->responsabili()->sync($data['responsabili'] ?? []);
@@ -576,4 +632,39 @@ class EventoController extends Controller
return redirect('/eventi')->with('success', $message); return redirect('/eventi')->with('success', $message);
} }
public function massTag(Request $request)
{
$this->authorizeWrite('eventi');
$idsInput = $request->input('ids', '');
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
if (empty($ids)) {
return back()->with('error', 'Nessun evento selezionato.');
}
$data = $request->validate([
'tags' => 'required|array',
'tags.*' => 'exists:tags,id',
'mode' => 'required|in:assign,remove',
]);
$tagIds = $data['tags'];
$mode = $data['mode'];
$count = 0;
Evento::whereIn('id', $ids)->chunk(100, function ($eventi) use ($tagIds, $mode, &$count) {
foreach ($eventi as $evento) {
if ($mode === 'assign') {
$evento->tags()->syncWithoutDetaching($tagIds);
} else {
$evento->tags()->detach($tagIds);
}
$count++;
}
});
$actionLabel = $mode === 'assign' ? 'assegnati' : 'rimossi';
return back()->with('success', "Tag $actionLabel per $count eventi.");
}
} }
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Evento; use App\Models\Evento;
@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Enums\GoogleService;
use App\Models\GoogleOAuthConnection;
use App\Services\GoogleOAuthService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
class GoogleOAuthController extends Controller
{
public function __construct(
private readonly GoogleOAuthService $googleOAuthService,
) {}
public function redirect(string $service): RedirectResponse
{
try {
$serviceEnum = GoogleService::from($service);
} catch (\ValueError $e) {
return redirect()->back()->withErrors(['error' => 'Servizio Google non valido: ' . $service]);
}
$authUrl = $this->googleOAuthService->getAuthUrl($service);
return redirect()->away($authUrl);
}
public function callback(Request $request): RedirectResponse
{
$error = $request->input('error');
if ($error) {
$message = match ($error) {
'access_denied' => 'Accesso negato. Autorizzazione non concessa.',
default => 'Errore Google: ' . $error,
};
return redirect()->route('impostazioni.index', ['tab' => 'google'])
->withErrors(['error' => $message]);
}
$code = $request->input('code');
if (!$code) {
return redirect()->route('impostazioni.index', ['tab' => 'google'])
->withErrors(['error' => 'Nessun codice di autorizzazione ricevuto']);
}
$state = $request->input('state', 'email');
try {
$service = GoogleService::from($state);
} catch (\ValueError $e) {
$service = GoogleService::Email;
}
try {
$connection = $this->googleOAuthService->handleCallback($code, $service);
$message = match ($service) {
GoogleService::Email => 'Account email Gmail connesso con successo!',
GoogleService::Drive => 'Account Google Drive connesso con successo!',
GoogleService::Calendar => 'Account Google Calendar connesso con successo!',
};
return redirect()->route('impostazioni.index', ['tab' => 'google'])
->with('success', $message);
} catch (\Exception $e) {
Log::error('Google OAuth callback error', [
'error' => $e->getMessage(),
'service' => $service->value,
]);
return redirect()->route('impostazioni.index', ['tab' => 'google'])
->withErrors(['error' => 'Errore durante la connessione: ' . $e->getMessage()]);
}
}
public function revoke(GoogleOAuthConnection $connection): RedirectResponse
{
if ($connection->user_id !== auth()->id()) {
abort(403);
}
try {
$service = GoogleService::from($connection->service);
$message = match ($service) {
GoogleService::Email => 'Account email Gmail disconnesso.',
GoogleService::Drive => 'Account Google Drive disconnesso.',
GoogleService::Calendar => 'Account Google Calendar disconnesso.',
};
$this->googleOAuthService->revoke($connection);
return redirect()->route('impostazioni.index', ['tab' => 'google'])
->with('success', $message);
} catch (\Exception $e) {
Log::error('Google OAuth revoke error', [
'error' => $e->getMessage(),
'connection_id' => $connection->id,
]);
return redirect()->route('impostazioni.index', ['tab' => 'google'])
->withErrors(['error' => 'Errore durante la disconnessione: ' . $e->getMessage()]);
}
}
public function status(): \Illuminate\Http\JsonResponse
{
$status = $this->googleOAuthService->getConnectionStatus();
return response()->json($status);
}
}
+190 -19
View File
@@ -7,9 +7,11 @@ namespace App\Http\Controllers;
use App\Models\Gruppo; use App\Models\Gruppo;
use App\Models\Individuo; use App\Models\Individuo;
use App\Models\Diocesi; use App\Models\Diocesi;
use App\Models\Tag;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\Paginator; use Illuminate\Pagination\Paginator;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
class GruppoController extends Controller class GruppoController extends Controller
@@ -27,8 +29,29 @@ class GruppoController extends Controller
->where('is_default', true) ->where('is_default', true)
->first(); ->first();
$allColumns = ['nome', 'descrizione', 'diocesi', 'livello', 'padre', 'membri', 'responsabili', 'indirizzo', 'citta', 'telefono', 'email']; $userVistas = \App\Models\VistaReport::where('user_id', $user->id)
$defaultVisible = ['nome', 'livello', 'padre', 'membri', 'responsabili']; ->where('tipo', 'gruppi')
->orderBy('is_default', 'desc')
->orderBy('nome')
->get();
$allColumnDefs = [
['key' => 'nome', 'label' => 'Nome'],
['key' => 'anno_fondazione', 'label' => 'Anno Fondazione'],
['key' => 'descrizione', 'label' => 'Descrizione'],
['key' => 'diocesi', 'label' => 'Diocesi'],
['key' => 'livello', 'label' => 'Livello'],
['key' => 'padre', 'label' => 'Gruppo Padre'],
['key' => 'membri', 'label' => 'Membri'],
['key' => 'responsabili', 'label' => 'Responsabili'],
['key' => 'indirizzo', 'label' => 'Indirizzo'],
['key' => 'citta', 'label' => 'Città'],
['key' => 'telefono', 'label' => 'Telefono'],
['key' => 'email', 'label' => 'Email'],
['key' => 'tag', 'label' => 'Tag'],
];
$allColumns = array_column($allColumnDefs, 'key');
$defaultVisible = ['nome', 'livello', 'padre', 'membri', 'responsabili', 'tag'];
if ($vista && !empty($vista->colonne_visibili)) { if ($vista && !empty($vista->colonne_visibili)) {
$visibleColumns = $vista->colonne_visibili; $visibleColumns = $vista->colonne_visibili;
@@ -36,9 +59,22 @@ class GruppoController extends Controller
$visibleColumns = $defaultVisible; $visibleColumns = $defaultVisible;
} }
$allGruppi = Gruppo::with(['diocesi', 'parent', 'children', 'individui', 'avatar']) $tableColumns = array_map(fn($col) => [
->orderBy('nome') 'key' => $col['key'],
->get() 'label' => $col['label'],
'visible' => in_array($col['key'], $visibleColumns),
], $allColumnDefs);
$entityType = 'gruppi';
$columnWidths = $vista && $vista->colonne_larghezze ? $vista->colonne_larghezze : [];
$gruppiQuery = Gruppo::with(['diocesi', 'gruppoContatti', 'parent', 'children', 'individui', 'avatar', 'tags']);
if (request()->filled('tag')) {
$tagSlugs = (array) request()->input('tag');
$gruppiQuery->withAnyTags($tagSlugs);
}
$allGruppi = $gruppiQuery->orderBy('nome')->get()
->map(function ($gruppo) { ->map(function ($gruppo) {
$depth = 0; $depth = 0;
$parent = $gruppo->parent; $parent = $gruppo->parent;
@@ -65,12 +101,19 @@ class GruppoController extends Controller
$gruppi = $allGruppi; $gruppi = $allGruppi;
} }
$allTags = Tag::orderBy('name')->get();
return view('gruppi.index', compact( return view('gruppi.index', compact(
'gruppi', 'gruppi',
'allColumns', 'allColumns',
'visibleColumns', 'visibleColumns',
'viewMode', 'viewMode',
'vista' 'vista',
'allTags',
'tableColumns',
'entityType',
'columnWidths',
'userVistas'
)); ));
} }
@@ -100,11 +143,12 @@ class GruppoController extends Controller
public function create(Request $request) public function create(Request $request)
{ {
$this->authorizeWrite('gruppi'); $this->authorizeWrite('gruppi');
$diocesi = Diocesi::orderBy('nome')->get(); $diocesi = Diocesi::orderBy('id')->get();
$allGruppi = Gruppo::orderBy('nome')->get(); $allGruppi = Gruppo::orderBy('nome')->get();
$selectedParent = $request->query('parent_id') ? Gruppo::find($request->query('parent_id')) : null; $selectedParent = $request->query('parent_id') ? Gruppo::find($request->query('parent_id')) : null;
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get(); $individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
return view('gruppi.create', compact('diocesi', 'allGruppi', 'selectedParent', 'individui')); $tags = Tag::orderBy('name')->get();
return view('gruppi.create', compact('diocesi', 'allGruppi', 'selectedParent', 'individui', 'tags'));
} }
public function store(Request $request) public function store(Request $request)
@@ -112,9 +156,11 @@ class GruppoController extends Controller
$this->authorizeWrite('gruppi'); $this->authorizeWrite('gruppi');
$data = $request->validate([ $data = $request->validate([
'nome' => 'required|string|max:255', 'nome' => 'required|string|max:255',
'anno_fondazione' => 'nullable|integer|min:1800|max:' . (date('Y') + 1),
'descrizione' => 'nullable|string', 'descrizione' => 'nullable|string',
'parent_id' => 'nullable|exists:gruppi,id', 'parent_id' => 'nullable|exists:gruppi,id',
'diocesi_id' => 'nullable|exists:diocesi,id', 'diocesi_ids' => 'nullable|array',
'diocesi_ids.*' => 'exists:diocesi,id',
'responsabile_ids' => 'nullable|array', 'responsabile_ids' => 'nullable|array',
'responsabile_ids.*' => 'exists:individui,id', 'responsabile_ids.*' => 'exists:individui,id',
'indirizzo_incontro' => 'nullable|string|max:500', 'indirizzo_incontro' => 'nullable|string|max:500',
@@ -122,6 +168,13 @@ class GruppoController extends Controller
'città_incontro' => 'nullable|string|max:255', 'città_incontro' => 'nullable|string|max:255',
'sigla_provincia_incontro' => 'nullable|string|max:2', 'sigla_provincia_incontro' => 'nullable|string|max:2',
'mappa_posizione' => 'nullable|string', 'mappa_posizione' => 'nullable|string',
'contatti' => 'nullable|array',
'contatti.*.tipo' => 'required|string|in:email,telefono,cellulare',
'contatti.*.valore' => 'required|string|max:255',
'contatti.*.etichetta' => 'nullable|string|max:255',
'contatti.*.is_primary' => 'nullable|boolean',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]); ]);
if (!empty($data['responsabile_ids'])) { if (!empty($data['responsabile_ids'])) {
@@ -130,6 +183,25 @@ class GruppoController extends Controller
$gruppo = Gruppo::create($data); $gruppo = Gruppo::create($data);
if (!empty($data['diocesi_ids'])) {
$gruppo->diocesi()->sync($data['diocesi_ids']);
}
if (!empty($data['contatti'])) {
foreach ($data['contatti'] as $contattoData) {
$gruppo->gruppoContatti()->create([
'tipo' => $contattoData['tipo'],
'valore' => $contattoData['valore'],
'etichetta' => $contattoData['etichetta'] ?? null,
'is_primary' => $contattoData['is_primary'] ?? false,
]);
}
}
if ($request->has('tags')) {
$gruppo->tags()->sync($request->tags);
}
if ($request->has('individui')) { if ($request->has('individui')) {
foreach ($request->individui as $individuoData) { foreach ($request->individui as $individuoData) {
if (!empty($individuoData['individuo_id'])) { if (!empty($individuoData['individuo_id'])) {
@@ -147,9 +219,9 @@ class GruppoController extends Controller
public function show($id) public function show($id)
{ {
$this->authorizeRead('gruppi'); $this->authorizeRead('gruppi');
$gruppo = Gruppo::with(['parent', 'children', 'diocesi', 'individui.contatti', 'individui.documenti', 'individui' => function ($q) { $gruppo = Gruppo::with(['parent', 'children', 'diocesi', 'gruppoContatti', 'individui.contatti', 'individui.documenti', 'individui' => function ($q) {
$q->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione'); $q->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione');
}, 'avatar', 'eventi' => function ($q) { }, 'avatar', 'tags', 'eventi' => function ($q) {
$q->where('is_incontro_gruppo', true); $q->where('is_incontro_gruppo', true);
}, 'eventi.responsabili.contatti'])->findOrFail($id); }, 'eventi.responsabili.contatti'])->findOrFail($id);
return view('gruppi.show', compact('gruppo')); return view('gruppi.show', compact('gruppo'));
@@ -158,11 +230,14 @@ class GruppoController extends Controller
public function edit($id) public function edit($id)
{ {
$this->authorizeWrite('gruppi'); $this->authorizeWrite('gruppi');
$gruppo = Gruppo::with(['individui.contatti', 'individui.documenti', 'avatar'])->findOrFail($id); $gruppo = Gruppo::with(['individui.contatti', 'individui.documenti', 'gruppoContatti', 'avatar', 'tags'])->findOrFail($id);
$diocesi = Diocesi::orderBy('nome')->get(); $diocesi = Diocesi::orderBy('id')->get();
$gruppi = Gruppo::where('id', '!=', $gruppo->id)->orderBy('nome')->get(); $gruppi = Gruppo::where('id', '!=', $gruppo->id)->orderBy('nome')->get();
$membri = $gruppo->individui()->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione')->orderBy('cognome')->orderBy('nome')->get(); $membri = $gruppo->individui()->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione')->orderBy('cognome')->orderBy('nome')->get();
return view('gruppi.edit', compact('gruppo', 'diocesi', 'gruppi', 'membri')); $tags = Tag::orderBy('name')->get();
$selectedTags = $gruppo->tags->pluck('id')->toArray();
$selectedDiocesiIds = $gruppo->diocesi()->pluck('diocesi.id')->toArray();
return view('gruppi.edit', compact('gruppo', 'diocesi', 'gruppi', 'membri', 'tags', 'selectedTags', 'selectedDiocesiIds'));
} }
public function update(Request $request, $id) public function update(Request $request, $id)
@@ -171,9 +246,11 @@ class GruppoController extends Controller
$gruppo = Gruppo::findOrFail($id); $gruppo = Gruppo::findOrFail($id);
$data = $request->validate([ $data = $request->validate([
'nome' => 'required|string|max:255', 'nome' => 'required|string|max:255',
'anno_fondazione' => 'nullable|integer|min:1800|max:' . (date('Y') + 1),
'descrizione' => 'nullable|string', 'descrizione' => 'nullable|string',
'parent_id' => 'nullable|exists:gruppi,id', 'parent_id' => 'nullable|exists:gruppi,id',
'diocesi_id' => 'nullable|exists:diocesi,id', 'diocesi_ids' => 'nullable|array',
'diocesi_ids.*' => 'exists:diocesi,id',
'responsabile_ids' => 'nullable|array', 'responsabile_ids' => 'nullable|array',
'responsabile_ids.*' => 'exists:individui,id', 'responsabile_ids.*' => 'exists:individui,id',
'indirizzo_incontro' => 'nullable|string|max:500', 'indirizzo_incontro' => 'nullable|string|max:500',
@@ -181,6 +258,13 @@ class GruppoController extends Controller
'città_incontro' => 'nullable|string|max:255', 'città_incontro' => 'nullable|string|max:255',
'sigla_provincia_incontro' => 'nullable|string|max:2', 'sigla_provincia_incontro' => 'nullable|string|max:2',
'mappa_posizione' => 'nullable|string', 'mappa_posizione' => 'nullable|string',
'contatti' => 'nullable|array',
'contatti.*.tipo' => 'required|string|in:email,telefono,cellulare',
'contatti.*.valore' => 'required|string|max:255',
'contatti.*.etichetta' => 'nullable|string|max:255',
'contatti.*.is_primary' => 'nullable|boolean',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]); ]);
if (!empty($data['responsabile_ids'])) { if (!empty($data['responsabile_ids'])) {
@@ -191,6 +275,30 @@ class GruppoController extends Controller
$gruppo->update($data); $gruppo->update($data);
if ($request->has('diocesi_ids')) {
$gruppo->diocesi()->sync($data['diocesi_ids'] ?? []);
} else {
$gruppo->diocesi()->detach();
}
if ($request->has('contatti')) {
$gruppo->gruppoContatti()->delete();
foreach ($data['contatti'] as $contattoData) {
$gruppo->gruppoContatti()->create([
'tipo' => $contattoData['tipo'],
'valore' => $contattoData['valore'],
'etichetta' => $contattoData['etichetta'] ?? null,
'is_primary' => $contattoData['is_primary'] ?? false,
]);
}
}
if ($request->has('tags')) {
$gruppo->tags()->sync($request->tags);
} else {
$gruppo->tags()->detach();
}
if ($request->has('individui')) { if ($request->has('individui')) {
$gruppo->individui()->detach(); $gruppo->individui()->detach();
foreach ($request->individui as $individuoData) { foreach ($request->individui as $individuoData) {
@@ -327,9 +435,12 @@ class GruppoController extends Controller
'file' => 'required|file|mimes:csv,txt', 'file' => 'required|file|mimes:csv,txt',
]); ]);
set_time_limit(0);
session_write_close();
$file = $request->file('file'); $file = $request->file('file');
$handle = fopen($file->path(), 'r'); $handle = fopen($file->path(), 'r');
$header = fgetcsv($handle, 1000, ','); $header = fgetcsv($handle, 0, ',');
if ($header === false) { if ($header === false) {
fclose($handle); fclose($handle);
@@ -342,7 +453,9 @@ class GruppoController extends Controller
$errors = []; $errors = [];
$rowNumber = 1; $rowNumber = 1;
while (($row = fgetcsv($handle, 1000, ',')) !== false) { DB::beginTransaction();
while (($row = fgetcsv($handle, 0, ',')) !== false) {
$rowNumber++; $rowNumber++;
if (count($row) < count($header)) { if (count($row) < count($header)) {
@@ -368,6 +481,7 @@ class GruppoController extends Controller
try { try {
Gruppo::create([ Gruppo::create([
'nome' => $nome, 'nome' => $nome,
'anno_fondazione' => !empty($data['anno_fondazione']) ? (int) $data['anno_fondazione'] : null,
'descrizione' => $data['descrizione'] ?? null, 'descrizione' => $data['descrizione'] ?? null,
'parent_id' => !empty($data['parent_id']) ? (int) $data['parent_id'] : null, 'parent_id' => !empty($data['parent_id']) ? (int) $data['parent_id'] : null,
'diocesi_id' => !empty($data['diocesi_id']) ? (int) $data['diocesi_id'] : null, 'diocesi_id' => !empty($data['diocesi_id']) ? (int) $data['diocesi_id'] : null,
@@ -386,6 +500,8 @@ class GruppoController extends Controller
fclose($handle); fclose($handle);
DB::commit();
$message = 'Importati ' . $imported . ' gruppi.'; $message = 'Importati ' . $imported . ' gruppi.';
if ($skipped > 0) { if ($skipped > 0) {
$message .= ' Saltate ' . $skipped . ' righe (vuote o incomplete).'; $message .= ' Saltate ' . $skipped . ' righe (vuote o incomplete).';
@@ -400,8 +516,8 @@ class GruppoController extends Controller
public function downloadTemplate() public function downloadTemplate()
{ {
$this->authorizeRead('gruppi'); $this->authorizeRead('gruppi');
$headers = ['nome', 'descrizione', 'parent_id', 'diocesi_id', 'indirizzo_incontro', 'cap_incontro', 'città_incontro', 'sigla_provincia_incontro']; $headers = ['nome', 'anno_fondazione', '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']; $example = ['Gruppo Giovani', '2020', 'Giovani dai 18 ai 30 anni', '', '', 'Via Roma 10', '00100', 'Roma', 'RM'];
$csv = implode(',', $headers) . "\n" . implode(',', $example); $csv = implode(',', $headers) . "\n" . implode(',', $example);
@@ -410,4 +526,59 @@ class GruppoController extends Controller
'Content-Disposition' => 'attachment; filename="template_gruppi.csv"', 'Content-Disposition' => 'attachment; filename="template_gruppi.csv"',
]); ]);
} }
public function massTag(Request $request)
{
$this->authorizeWrite('gruppi');
$idsInput = $request->input('ids', '');
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
if (empty($ids)) {
return redirect()->route('gruppi.index')->with('error', 'Nessun gruppo selezionato.');
}
$data = $request->validate([
'tags' => 'required|array',
'tags.*' => 'exists:tags,id',
'mode' => 'required|in:assign,remove',
]);
$tagIds = $data['tags'];
$mode = $data['mode'];
$count = 0;
Gruppo::whereIn('id', $ids)->chunk(100, function ($gruppi) use ($tagIds, $mode, &$count) {
foreach ($gruppi as $gruppo) {
if ($mode === 'assign') {
$gruppo->tags()->syncWithoutDetaching($tagIds);
} else {
$gruppo->tags()->detach($tagIds);
}
$count++;
}
});
$actionLabel = $mode === 'assign' ? 'assegnati' : 'rimossi';
return redirect()->route('gruppi.index')->with('success', "Tag {$actionLabel} per {$count} gruppi.");
}
public function massElimina(Request $request)
{
$this->authorizeDelete('gruppi');
$ids = $request->input('ids', []);
if (!is_array($ids) || empty($ids)) {
return redirect()->route('gruppi.index')->with('error', 'Nessun gruppo selezionato.');
}
$gruppi = Gruppo::whereIn('id', $ids)->get();
$count = 0;
foreach ($gruppi as $gruppo) {
$gruppo->individui()->detach();
$gruppo->delete();
$count++;
}
return redirect()->route('gruppi.index')->with('success', "{$count} gruppi eliminati con successo.");
}
} }
@@ -9,6 +9,8 @@ use App\Models\EmailSetting;
use App\Models\Ruolo; use App\Models\Ruolo;
use App\Models\SenderAccount; use App\Models\SenderAccount;
use App\Models\StorageRepository; use App\Models\StorageRepository;
use App\Models\Tag;
use App\Services\GoogleOAuthService;
use App\Models\TipologiaDocumento; use App\Models\TipologiaDocumento;
use App\Models\TipologiaEvento; use App\Models\TipologiaEvento;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
@@ -26,14 +28,16 @@ class ImpostazioniController extends Controller
$tipologie = TipologiaDocumento::orderBy('ordine')->get(); $tipologie = TipologiaDocumento::orderBy('ordine')->get();
$tipologieEventi = TipologiaEvento::orderBy('ordine')->get(); $tipologieEventi = TipologiaEvento::orderBy('ordine')->get();
$ruoli = Ruolo::orderBy('ordine')->get(); $ruoli = Ruolo::orderBy('ordine')->get();
$tags = Tag::orderBy('order_column')->get();
$appSettings = AppSetting::first(); $appSettings = AppSetting::first();
$emailSettings = EmailSetting::first() ?? new EmailSetting(); $emailSettings = EmailSetting::first() ?? new EmailSetting();
$senderAccounts = SenderAccount::orderBy('email_address')->get(); $senderAccounts = SenderAccount::orderBy('email_address')->get();
$repositories = StorageRepository::orderBy('ordine')->get(); $repositories = StorageRepository::orderBy('ordine')->get();
$googleDriveNewToken = session('google_drive_new_token'); $googleDriveNewToken = session('google_drive_new_token');
$calendarioConnessioni = CalendarioConnessione::orderBy('ordine')->get(); $calendarioConnessioni = CalendarioConnessione::orderBy('ordine')->get();
$googleStatus = app(GoogleOAuthService::class)->getConnectionStatus();
return view('impostazioni.index', compact('tipologie', 'tipologieEventi', 'ruoli', 'appSettings', 'emailSettings', 'senderAccounts', 'repositories', 'googleDriveNewToken', 'calendarioConnessioni')); return view('impostazioni.index', compact('tipologie', 'tipologieEventi', 'ruoli', 'tags', 'appSettings', 'emailSettings', 'senderAccounts', 'repositories', 'googleDriveNewToken', 'calendarioConnessioni', 'googleStatus'));
} }
public function saveAppSettings(Request $request) public function saveAppSettings(Request $request)
@@ -371,6 +375,70 @@ class ImpostazioniController extends Controller
return response()->json(['success' => true]); return response()->json(['success' => true]);
} }
public function tagStore(Request $request)
{
$this->authorizeWrite('settings');
$validated = $request->validate([
'name' => 'required|string|max:100|unique:tags,name',
'color' => 'nullable|string|max:7',
]);
$maxOrdine = Tag::max('order_column') ?? 0;
Tag::create([
'name' => $validated['name'],
'slug' => str()->slug($validated['name']),
'color' => $validated['color'] ?? null,
'order_column' => $maxOrdine + 1,
]);
return redirect('/impostazioni#tag')->with('success', 'Tag aggiunto.');
}
public function tagUpdate(Request $request, $id)
{
$this->authorizeWrite('settings');
$tag = Tag::findOrFail($id);
$validated = $request->validate([
'name' => 'required|string|max:100|unique:tags,name,' . $id,
'color' => 'nullable|string|max:7',
]);
$tag->update([
'name' => $validated['name'],
'slug' => str()->slug($validated['name']),
'color' => $validated['color'] ?? null,
]);
return redirect('/impostazioni#tag')->with('success', 'Tag aggiornato.');
}
public function tagDestroy($id)
{
$this->authorizeDelete('settings');
$tag = Tag::findOrFail($id);
$tag->delete();
return redirect('/impostazioni#tag')->with('success', 'Tag eliminato.');
}
public function tagReorder(Request $request)
{
$this->authorizeWrite('settings');
$order = $request->input('order', []);
foreach ($order as $index => $id) {
Tag::where('id', $id)->update(['order_column' => $index]);
}
return response()->json(['success' => true]);
}
public function uploadLogo(Request $request) public function uploadLogo(Request $request)
{ {
$this->authorizeWrite('settings'); $this->authorizeWrite('settings');
+153 -10
View File
@@ -1,13 +1,20 @@
<?php <?php
declare(strict_types=1);
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Individuo; use App\Models\Individuo;
use App\Models\Contatto; use App\Models\Contatto;
use App\Models\Documento; use App\Models\Documento;
use App\Models\Gruppo;
use App\Models\Comune; use App\Models\Comune;
use App\Models\Diocesi; use App\Models\Diocesi;
use App\Models\Tag;
use App\Models\VistaReport;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpFoundation\StreamedResponse;
@@ -18,7 +25,16 @@ class IndividuoController extends Controller
{ {
$this->authorizeRead('individui'); $this->authorizeRead('individui');
$perPage = in_array((int) $request->perPage, [10, 20, 25, 50, 100]) ? (int) $request->perPage : 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);
$query = Individuo::with('contatti', 'tags');
if ($request->filled('tag')) {
$tagSlugs = (array) $request->input('tag');
$query->withAnyTags($tagSlugs);
}
$individui = $query->orderBy('cognome')->orderBy('nome')->paginate($perPage);
$allTags = Tag::orderBy('name')->get();
$vista = null; $vista = null;
@@ -39,16 +55,43 @@ class IndividuoController extends Controller
['key' => 'codice', 'label' => 'Codice'], ['key' => 'codice', 'label' => 'Codice'],
['key' => 'cognome', 'label' => 'Cognome'], ['key' => 'cognome', 'label' => 'Cognome'],
['key' => 'nome', 'label' => 'Nome'], ['key' => 'nome', 'label' => 'Nome'],
['key' => 'data_nascita', 'label' => 'Data Nascita'],
['key' => 'genere', 'label' => 'Genere'],
['key' => 'indirizzo', 'label' => 'Indirizzo'],
['key' => 'cap', 'label' => 'CAP'],
['key' => 'citta', 'label' => 'Città'],
['key' => 'provincia', 'label' => 'Provincia'],
['key' => 'tipo_documento', 'label' => 'Tipo Documento'],
['key' => 'numero_documento', 'label' => 'Numero Documento'],
['key' => 'scadenza_documento', 'label' => 'Scadenza Documento'],
['key' => 'email', 'label' => 'Email'], ['key' => 'email', 'label' => 'Email'],
['key' => 'telefono', 'label' => 'Telefono'], ['key' => 'telefono', 'label' => 'Telefono'],
['key' => 'note', 'label' => 'Note'],
['key' => 'tag', 'label' => 'Tag'],
['key' => 'created_at', 'label' => 'Creato il'],
]; ];
$defaultVisible = ['codice', 'cognome', 'nome', 'email', 'telefono']; $defaultVisible = ['codice', 'cognome', 'nome', 'email', 'telefono', 'tag'];
$visibleColumns = $vista && !empty($vista->colonne_visibili) $visibleColumns = $vista && !empty($vista->colonne_visibili)
? $vista->colonne_visibili ? $vista->colonne_visibili
: $defaultVisible; : $defaultVisible;
return view('individui.index', compact('individui', 'vista', 'allColumns', 'visibleColumns')); $entityType = 'individui';
$tableColumns = array_map(fn($col) => [
'key' => $col['key'],
'label' => $col['label'],
'visible' => in_array($col['key'], $visibleColumns),
], $allColumns);
$columnWidths = $vista && $vista->colonne_larghezze ? $vista->colonne_larghezze : [];
$userVistas = VistaReport::where('user_id', auth()->id())
->where('tipo', 'individui')
->orderBy('is_default', 'desc')
->orderBy('nome')
->get();
$gruppi = Gruppo::orderBy('nome')->get(['id', 'nome']);
return view('individui.index', compact('individui', 'vista', 'allColumns', 'visibleColumns', 'allTags', 'gruppi', 'entityType', 'tableColumns', 'columnWidths', 'userVistas'));
} }
public function exportCSV(Request $request) public function exportCSV(Request $request)
@@ -109,8 +152,9 @@ public function create()
{ {
$this->authorizeWrite('individui'); $this->authorizeWrite('individui');
$comuni = Comune::orderBy('nome')->get(); $comuni = Comune::orderBy('nome')->get();
$diocesi = Diocesi::orderBy('nome')->get(); $diocesi = Diocesi::orderBy('id')->get();
return view('individui.create', compact('comuni', 'diocesi')); $tags = Tag::orderBy('name')->get();
return view('individui.create', compact('comuni', 'diocesi', 'tags'));
} }
public function store(Request $request) public function store(Request $request)
@@ -129,10 +173,16 @@ public function create()
'numero_documento' => 'nullable|string|max:50', 'numero_documento' => 'nullable|string|max:50',
'scadenza_documento' => 'nullable|date', 'scadenza_documento' => 'nullable|date',
'note' => 'nullable|string', 'note' => 'nullable|string',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]); ]);
$individuo = Individuo::create($data); $individuo = Individuo::create($data);
if ($request->has('tags')) {
$individuo->tags()->sync($request->tags);
}
if ($request->has('contatti')) { if ($request->has('contatti')) {
foreach ($request->contatti as $contatto) { foreach ($request->contatti as $contatto) {
if (!empty($contatto['tipo']) && !empty($contatto['valore'])) { if (!empty($contatto['tipo']) && !empty($contatto['valore'])) {
@@ -154,7 +204,7 @@ public function create()
$this->authorizeRead('individui'); $this->authorizeRead('individui');
$individuo = Individuo::with(['contatti', 'gruppi' => function ($q) { $individuo = Individuo::with(['contatti', 'gruppi' => function ($q) {
$q->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione'); $q->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione');
}, 'avatar'])->findOrFail($individuo); }, 'avatar', 'tags'])->findOrFail($individuo);
return view('individui.show', compact('individuo')); return view('individui.show', compact('individuo'));
} }
@@ -180,8 +230,10 @@ public function create()
public function edit($individuo) public function edit($individuo)
{ {
$this->authorizeWrite('individui'); $this->authorizeWrite('individui');
$individuo = Individuo::with(['contatti', 'gruppi', 'avatar'])->findOrFail($individuo); $individuo = Individuo::with(['contatti', 'gruppi', 'avatar', 'tags'])->findOrFail($individuo);
return view('individui.edit', compact('individuo')); $tags = Tag::orderBy('name')->get();
$selectedTags = $individuo->tags->pluck('id')->toArray();
return view('individui.edit', compact('individuo', 'tags', 'selectedTags'));
} }
public function update(Request $request, $individuo) public function update(Request $request, $individuo)
@@ -205,10 +257,18 @@ public function create()
'numero_documento' => 'nullable|string|max:50', 'numero_documento' => 'nullable|string|max:50',
'scadenza_documento' => 'nullable|date', 'scadenza_documento' => 'nullable|date',
'note' => 'nullable|string', 'note' => 'nullable|string',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]); ]);
$individuo->update($data); $individuo->update($data);
if ($request->has('tags')) {
$individuo->tags()->sync($request->tags);
} else {
$individuo->tags()->detach();
}
\Log::info('Individuo dati salvati', ['id' => $individuo->id]); \Log::info('Individuo dati salvati', ['id' => $individuo->id]);
if ($request->has('contatti')) { if ($request->has('contatti')) {
@@ -308,6 +368,75 @@ public function create()
return redirect()->route('individui.index')->with('success', count($individui) . ' individui eliminati con successo.'); return redirect()->route('individui.index')->with('success', count($individui) . ' individui eliminati con successo.');
} }
public function massGruppo(Request $request)
{
$this->authorizeWrite('individui');
$idsInput = $request->input('ids', '');
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
if (empty($ids)) {
return redirect()->route('individui.index')->with('error', 'Nessun individuo selezionato.');
}
$data = $request->validate([
'gruppi' => 'required|array',
'gruppi.*' => 'exists:gruppi,id',
'mode' => 'required|in:assign,remove',
]);
$gruppiIds = $data['gruppi'];
$mode = $data['mode'];
$count = 0;
foreach ($ids as $id) {
$individuo = Individuo::find($id);
if (!$individuo) continue;
if ($mode === 'assign') {
$individuo->gruppi()->syncWithoutDetaching($gruppiIds);
} else {
$individuo->gruppi()->detach($gruppiIds);
}
$count++;
}
$actionLabel = $mode === 'assign' ? 'assegnati' : 'rimossi';
return redirect()->route('individui.index')->with('success', "Gruppi {$actionLabel} per {$count} individui.");
}
public function massTag(Request $request)
{
$this->authorizeWrite('individui');
$idsInput = $request->input('ids', '');
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
if (empty($ids)) {
return redirect()->route('individui.index')->with('error', 'Nessun individuo selezionato.');
}
$data = $request->validate([
'tags' => 'required|array',
'tags.*' => 'exists:tags,id',
'mode' => 'required|in:assign,remove',
]);
$tagIds = $data['tags'];
$mode = $data['mode'];
$count = 0;
Individuo::whereIn('id', $ids)->chunk(100, function ($individui) use ($tagIds, $mode, &$count) {
foreach ($individui as $individuo) {
if ($mode === 'assign') {
$individuo->tags()->syncWithoutDetaching($tagIds);
} else {
$individuo->tags()->detach($tagIds);
}
$count++;
}
});
$actionLabel = $mode === 'assign' ? 'assegnati' : 'rimossi';
return redirect()->route('individui.index')->with('success', "Tag {$actionLabel} per {$count} individui.");
}
public function elementiCollegati($individuo) public function elementiCollegati($individuo)
{ {
$individuo = Individuo::findOrFail($individuo); $individuo = Individuo::findOrFail($individuo);
@@ -372,16 +501,27 @@ public function emailList(Request $request)
'file' => 'required|file|mimes:csv,txt', 'file' => 'required|file|mimes:csv,txt',
]); ]);
set_time_limit(0);
session_write_close();
$file = $request->file('file'); $file = $request->file('file');
$handle = fopen($file->path(), 'r'); $handle = fopen($file->path(), 'r');
$header = fgetcsv($handle, 1000, ','); $header = fgetcsv($handle, 0, ',');
if ($header === false) {
fclose($handle);
return back()->with('error', 'Il file CSV non è valido.');
}
$header = array_map('trim', $header);
$imported = 0; $imported = 0;
$skipped = 0; $skipped = 0;
$errors = []; $errors = [];
$rowNumber = 1; $rowNumber = 1;
while (($row = fgetcsv($handle, 1000, ',')) !== false) { DB::beginTransaction();
while (($row = fgetcsv($handle, 0, ',')) !== false) {
$rowNumber++; $rowNumber++;
if (count($row) < count($header)) { if (count($row) < count($header)) {
@@ -440,12 +580,15 @@ public function emailList(Request $request)
$imported++; $imported++;
} catch (\Exception $e) { } catch (\Exception $e) {
Log::warning('Errore import individuo riga ' . $rowNumber . ': ' . $e->getMessage());
$errors[] = 'Errore riga ' . $rowNumber . ': ' . $e->getMessage(); $errors[] = 'Errore riga ' . $rowNumber . ': ' . $e->getMessage();
} }
} }
fclose($handle); fclose($handle);
DB::commit();
$message = 'Importati ' . $imported . ' individui.'; $message = 'Importati ' . $imported . ' individui.';
if ($skipped > 0) { if ($skipped > 0) {
$message .= ' Saltate ' . $skipped . ' righe (vuote o incomplete).'; $message .= ' Saltate ' . $skipped . ' righe (vuote o incomplete).';
+108 -28
View File
@@ -10,6 +10,8 @@ use App\Models\MailingMessaggio;
use App\Models\SenderAccount; use App\Models\SenderAccount;
use App\Models\Individuo; use App\Models\Individuo;
use App\Models\Documento; use App\Models\Documento;
use App\Models\EmailSetting;
use App\Models\Firma;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
@@ -30,6 +32,8 @@ class MailingController extends Controller
$liste = MailingList::all(); $liste = MailingList::all();
$documenti = Documento::orderBy('nome_file')->get(); $documenti = Documento::orderBy('nome_file')->get();
$senderAccounts = SenderAccount::active()->get(); $senderAccounts = SenderAccount::active()->get();
$settings = EmailSetting::getActive();
$firme = $settings?->firme ?? collect();
$documentiSelezionati = collect(); $documentiSelezionati = collect();
if ($request->has('documenti_selezionati')) { if ($request->has('documenti_selezionati')) {
@@ -37,7 +41,7 @@ class MailingController extends Controller
$documentiSelezionati = Documento::whereIn('id', $docIds)->get(); $documentiSelezionati = Documento::whereIn('id', $docIds)->get();
} }
return view('mailing.nuovo', compact('individui', 'liste', 'documenti', 'documentiSelezionati', 'senderAccounts')); return view('mailing.nuovo', compact('individui', 'liste', 'documenti', 'documentiSelezionati', 'senderAccounts', 'firme'));
} }
public function invia(Request $request) public function invia(Request $request)
@@ -46,16 +50,22 @@ class MailingController extends Controller
$data = $request->validate([ $data = $request->validate([
'oggetto' => 'required|string|max:255', 'oggetto' => 'required|string|max:255',
'corpo' => 'required', 'corpo' => 'required',
'lista_id' => 'nullable|exists:mailing_liste,id', 'lista_id' => 'nullable|exists:mailing_lists,id',
'destinatari_ids' => 'nullable|string', 'destinatari_ids' => 'nullable|string',
'documenti_selezionati' => 'nullable|string', 'documenti_selezionati' => 'nullable|string',
'allegato' => 'nullable|file|max:10240', 'allegato' => 'nullable|file|max:10240',
'mittente_id' => 'nullable|integer|exists:sender_accounts,id', 'mittente_id' => 'nullable|integer|exists:sender_accounts,id',
'firma_id' => 'nullable|integer|exists:firme,id',
]); ]);
$sender = null; $sender = null;
if (!empty($data['mittente_id'])) { if (!empty($data['mittente_id'])) {
$sender = SenderAccount::findOrFail($data['mittente_id']); $sender = SenderAccount::findOrFail($data['mittente_id']);
} elseif (!empty($data['lista_id'])) {
$lista = MailingList::with('senderAccount')->find($data['lista_id']);
if ($lista && $lista->senderAccount) {
$sender = $lista->senderAccount;
}
} }
$documentiAllegati = []; $documentiAllegati = [];
@@ -95,14 +105,38 @@ class MailingController extends Controller
} }
} }
$emails = $destinatari->flatMap(fn($ind) => $ind->contatti->where('tipo', 'email')->pluck('valore')) $totaleDestinatari = $destinatari->count();
$emails = $destinatari->map(fn($ind) => $ind->contatti->where('tipo', 'email')->first()?->valore)
->filter()->unique()->values(); ->filter()->unique()->values();
$skipped = $totaleDestinatari - $emails->count();
if ($skipped > 0) {
Log::info("Mailing invia: {$skipped} contatti saltati perché senza email", [
'lista_id' => $data['lista_id'] ?? null,
]);
}
if ($emails->isEmpty()) { if ($emails->isEmpty()) {
return back()->with('error', 'Nessun destinatario email valido trovato.')->withInput(); return back()->with('error', 'Nessun destinatario email valido trovato.')->withInput();
} }
$attachmentPaths = $this->resolveMailingAttachmentPaths($documentiAllegati); $attachmentResult = $this->resolveMailingAttachmentPaths($documentiAllegati);
$attachmentPaths = $attachmentResult['paths'];
$linkDocs = $attachmentResult['links'];
$firmaId = $data['firma_id'] ?? null;
if (!$firmaId && !empty($data['lista_id'])) {
$lista = MailingList::with('firma')->find($data['lista_id']);
$firmaId = $lista?->firma?->id;
}
$corpoConFirma = $this->appendMailingLinksToBody($data['corpo'], $linkDocs);
if ($firmaId) {
$firma = Firma::find($firmaId);
if ($firma && $firma->contenuto_html) {
$corpoConFirma .= "\n\n---\n" . $firma->contenuto_html;
}
}
$messaggio = MailingMessaggio::create([ $messaggio = MailingMessaggio::create([
'mailing_list_id' => $data['lista_id'] ?? null, 'mailing_list_id' => $data['lista_id'] ?? null,
@@ -113,6 +147,7 @@ class MailingController extends Controller
'totale_destinatari' => $emails->count(), 'totale_destinatari' => $emails->count(),
'mittente_nome' => $sender?->email_name, 'mittente_nome' => $sender?->email_name,
'mittente_email' => $sender?->email_address, 'mittente_email' => $sender?->email_address,
'firma_id' => $firmaId,
]); ]);
$ok = 0; $ok = 0;
@@ -122,9 +157,9 @@ class MailingController extends Controller
foreach ($emails as $email) { foreach ($emails as $email) {
try { try {
if ($sender) { if ($sender) {
$sender->sendEmail($email, $data['oggetto'], $data['corpo'], $attachmentPaths); $sender->sendEmail($email, $data['oggetto'], $corpoConFirma, $attachmentPaths);
} else { } else {
$this->sendViaSystem($email, $data['oggetto'], $data['corpo'], $attachmentPaths); $this->sendViaSystem($email, $data['oggetto'], $corpoConFirma, $attachmentPaths);
} }
$ok++; $ok++;
} catch (\Throwable $e) { } catch (\Throwable $e) {
@@ -173,11 +208,13 @@ class MailingController extends Controller
public function invio(Request $request) public function invio(Request $request)
{ {
$this->authorizeWrite('mailing'); $this->authorizeWrite('mailing');
$liste = MailingList::where('attiva', true)->orderBy('nome')->get(); $liste = MailingList::with('firma')->where('attiva', true)->orderBy('nome')->get();
$documenti = Documento::orderBy('nome_file')->get(); $documenti = Documento::orderBy('nome_file')->get();
$senderAccounts = SenderAccount::active()->get(); $senderAccounts = SenderAccount::active()->get();
$settings = EmailSetting::getActive();
$firme = $settings?->firme ?? collect();
return view('mailing.invio', compact('liste', 'documenti', 'senderAccounts')); return view('mailing.invio', compact('liste', 'documenti', 'senderAccounts', 'firme'));
} }
public function invioElabora(Request $request) public function invioElabora(Request $request)
@@ -185,18 +222,23 @@ class MailingController extends Controller
$this->authorizeWrite('mailing'); $this->authorizeWrite('mailing');
$data = $request->validate([ $data = $request->validate([
'liste' => 'required|array', 'liste' => 'required|array',
'liste.*' => 'exists:mailing_liste,id', 'liste.*' => 'exists:mailing_lists,id',
'oggetto' => 'required|string|max:255', 'oggetto' => 'required|string|max:255',
'corpo' => 'required', 'corpo' => 'required',
'documenti_selezionati' => 'nullable|array', 'documenti_selezionati' => 'nullable|array',
'documenti_selezionati.*' => 'exists:documenti,id', 'documenti_selezionati.*' => 'exists:documenti,id',
'allegato' => 'nullable|file|max:10240', 'allegato' => 'nullable|file|max:10240',
'mittente_id' => 'nullable|integer|exists:sender_accounts,id', 'mittente_id' => 'nullable|integer|exists:sender_accounts,id',
'firma_id' => 'nullable|integer|exists:firme,id',
]); ]);
$listeModels = MailingList::with('firma', 'senderAccount')->whereIn('id', $data['liste'])->get();
$sender = null; $sender = null;
if (!empty($data['mittente_id'])) { if (!empty($data['mittente_id'])) {
$sender = SenderAccount::findOrFail($data['mittente_id']); $sender = SenderAccount::findOrFail($data['mittente_id']);
} elseif ($listeModels->count() === 1) {
$sender = $listeModels->first()->senderAccount;
} }
$documentiAllegati = []; $documentiAllegati = [];
@@ -229,16 +271,39 @@ class MailingController extends Controller
->with('individuo.contatti') ->with('individuo.contatti')
->get(); ->get();
$totaleContatti = $contatti->count();
$emails = $contatti->map(fn($c) => $c->individuo?->contatti->where('tipo', 'email')->first()?->valore) $emails = $contatti->map(fn($c) => $c->individuo?->contatti->where('tipo', 'email')->first()?->valore)
->filter()->unique()->values(); ->filter()->unique()->values();
$skipped = $totaleContatti - $emails->count();
if ($skipped > 0) {
Log::info("Mailing invio elabora: {$skipped} contatti saltati perché senza email", [
'liste_ids' => $data['liste'],
]);
}
if ($emails->isEmpty()) { if ($emails->isEmpty()) {
return back()->with('error', 'Nessun destinatario email valido trovato nelle liste selezionate.'); return back()->with('error', 'Nessun destinatario email valido trovato nelle liste selezionate.');
} }
$attachmentPaths = $this->resolveMailingAttachmentPaths($documentiAllegati); $attachmentResult = $this->resolveMailingAttachmentPaths($documentiAllegati);
$attachmentPaths = $attachmentResult['paths'];
$linkDocs = $attachmentResult['links'];
$listeNomi = MailingList::whereIn('id', $data['liste'])->pluck('nome')->implode(', '); $firmaId = $data['firma_id'] ?? null;
if (!$firmaId && $listeModels->count() === 1) {
$firmaId = $listeModels->first()->firma_id;
}
$corpoConFirma = $this->appendMailingLinksToBody($data['corpo'], $linkDocs);
if ($firmaId) {
$firma = Firma::find($firmaId);
if ($firma && $firma->contenuto_html) {
$corpoConFirma .= "\n\n---\n" . $firma->contenuto_html;
}
}
$listeNomi = $listeModels->pluck('nome')->implode(', ');
$messaggio = MailingMessaggio::create([ $messaggio = MailingMessaggio::create([
'mailing_list_id' => null, 'mailing_list_id' => null,
@@ -249,6 +314,7 @@ class MailingController extends Controller
'totale_destinatari' => $emails->count(), 'totale_destinatari' => $emails->count(),
'mittente_nome' => $sender?->email_name, 'mittente_nome' => $sender?->email_name,
'mittente_email' => $sender?->email_address, 'mittente_email' => $sender?->email_address,
'firma_id' => $firmaId,
]); ]);
$ok = 0; $ok = 0;
@@ -258,9 +324,9 @@ class MailingController extends Controller
foreach ($emails as $email) { foreach ($emails as $email) {
try { try {
if ($sender) { if ($sender) {
$sender->sendEmail($email, $data['oggetto'], $data['corpo'], $attachmentPaths); $sender->sendEmail($email, $data['oggetto'], $corpoConFirma, $attachmentPaths);
} else { } else {
$this->sendViaSystem($email, $data['oggetto'], $data['corpo'], $attachmentPaths); $this->sendViaSystem($email, $data['oggetto'], $corpoConFirma, $attachmentPaths);
} }
$ok++; $ok++;
} catch (\Throwable $e) { } catch (\Throwable $e) {
@@ -331,18 +397,10 @@ class MailingController extends Controller
$transport = \Symfony\Component\Mailer\Transport::fromDsn($dsn); $transport = \Symfony\Component\Mailer\Transport::fromDsn($dsn);
$mailer = new \Symfony\Component\Mailer\Mailer($transport); $mailer = new \Symfony\Component\Mailer\Mailer($transport);
$fromName = $settings->email_name ?? ''; $fromAddress = \Symfony\Component\Mime\Address::create(
if (filter_var($fromName, FILTER_VALIDATE_EMAIL)) { $settings->getEffectiveFromAddress(),
$fromName = 'Glastree'; $settings->getEffectiveFromName()
} else { );
$fromName = preg_replace('/[^\p{L}\p{N}\s]/u', '', $fromName);
$fromName = trim($fromName);
if (empty($fromName) || strlen($fromName) > 50) {
$fromName = 'Glastree';
}
}
$fromAddress = \Symfony\Component\Mime\Address::create($settings->email_address, $fromName);
$email = (new \Symfony\Component\Mime\Email()) $email = (new \Symfony\Component\Mime\Email())
->from($fromAddress) ->from($fromAddress)
@@ -350,6 +408,11 @@ class MailingController extends Controller
->subject($subject) ->subject($subject)
->text($body); ->text($body);
$replyTo = $settings->getEffectiveReplyTo();
if ($replyTo) {
$email->replyTo($replyTo);
}
foreach ($attachmentPaths as $path) { foreach ($attachmentPaths as $path) {
$email->attachFromPath($path); $email->attachFromPath($path);
} }
@@ -360,17 +423,34 @@ class MailingController extends Controller
private function resolveMailingAttachmentPaths(array $documentIds): array private function resolveMailingAttachmentPaths(array $documentIds): array
{ {
$paths = []; $paths = [];
$links = [];
if (empty($documentIds)) { if (empty($documentIds)) {
return $paths; return ['paths' => $paths, 'links' => $links];
} }
$docs = Documento::whereIn('id', $documentIds)->get(); $docs = Documento::whereIn('id', $documentIds)->get();
foreach ($docs as $doc) { foreach ($docs as $doc) {
if ($doc->file_path && Storage::exists($doc->file_path)) { if ($doc->isLink()) {
$links[] = ['nome' => $doc->nome_file, 'url' => $doc->url];
} elseif ($doc->file_path && Storage::exists($doc->file_path)) {
$paths[] = Storage::path($doc->file_path); $paths[] = Storage::path($doc->file_path);
} }
} }
return $paths; return ['paths' => $paths, 'links' => $links];
}
private function appendMailingLinksToBody(string $body, array $linkDocs): string
{
if (empty($linkDocs)) {
return $body;
}
$linksText = "\n\n---\nDocumenti collegati:\n";
foreach ($linkDocs as $link) {
$linksText .= "- {$link['nome']}: {$link['url']}\n";
}
return $body . $linksText;
} }
} }
+116 -9
View File
@@ -3,23 +3,68 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\MailingList; use App\Models\MailingList;
use App\Models\SenderAccount;
use App\Models\Tag;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class MailingListController extends Controller class MailingListController extends Controller
{ {
public function index() public function index(Request $request)
{ {
$this->authorizeRead('mailing'); $this->authorizeRead('mailing');
$mailingLists = MailingList::with(['user', 'contatti']) $query = MailingList::with(['user', 'contatti', 'tags']);
if ($request->filled('tag')) {
$tagSlugs = (array) $request->input('tag');
$query->withAnyTags($tagSlugs);
}
$mailingLists = $query->orderBy('nome')->get();
$allTags = Tag::orderBy('name')->get();
$vista = \App\Models\VistaReport::where('user_id', auth()->id())
->where('tipo', 'mailing-liste')
->where('is_default', true)
->first();
$userVistas = \App\Models\VistaReport::where('user_id', auth()->id())
->where('tipo', 'mailing-liste')
->orderBy('is_default', 'desc')
->orderBy('nome') ->orderBy('nome')
->get(); ->get();
return view('mailing-liste.index', compact('mailingLists'));
$allColumnDefs = [
['key' => 'nome', 'label' => 'Nome'],
['key' => 'descrizione', 'label' => 'Descrizione'],
['key' => 'contatti_count', 'label' => 'Contatti'],
['key' => 'tag', 'label' => 'Tag'],
['key' => 'attiva', 'label' => 'Stato'],
['key' => 'created_at', 'label' => 'Creata il'],
];
$defaultVisible = ['nome', 'descrizione', 'contatti_count', 'tag', 'attiva', 'created_at'];
$visibleColumns = $vista && !empty($vista->colonne_visibili)
? $vista->colonne_visibili
: $defaultVisible;
$tableColumns = array_map(fn($col) => [
'key' => $col['key'],
'label' => $col['label'],
'visible' => in_array($col['key'], $visibleColumns),
], $allColumnDefs);
$entityType = 'mailing-liste';
$columnWidths = $vista && $vista->colonne_larghezze ? $vista->colonne_larghezze : [];
return view('mailing-liste.index', compact('mailingLists', 'allTags', 'vista', 'visibleColumns', 'tableColumns', 'entityType', 'columnWidths', 'userVistas'));
} }
public function create() public function create()
{ {
$this->authorizeWrite('mailing'); $this->authorizeWrite('mailing');
return view('mailing-liste.create'); $tags = Tag::orderBy('name')->get();
$settings = \App\Models\EmailSetting::getActive();
$firme = $settings?->firme ?? collect();
$senderAccounts = SenderAccount::active()->get();
return view('mailing-liste.create', compact('tags', 'firme', 'senderAccounts'));
} }
public function store(Request $request) public function store(Request $request)
@@ -30,6 +75,10 @@ class MailingListController extends Controller
'descrizione' => 'nullable|string', 'descrizione' => 'nullable|string',
'attiva' => 'boolean', 'attiva' => 'boolean',
'contatti_json' => 'nullable|string', 'contatti_json' => 'nullable|string',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
'firma_id' => 'nullable|integer|exists:firme,id',
'sender_account_id' => 'nullable|integer|exists:sender_accounts,id',
]); ]);
$data['user_id'] = auth()->id(); $data['user_id'] = auth()->id();
@@ -40,13 +89,19 @@ class MailingListController extends Controller
'descrizione' => $data['descrizione'] ?? null, 'descrizione' => $data['descrizione'] ?? null,
'attiva' => $data['attiva'], 'attiva' => $data['attiva'],
'user_id' => $data['user_id'], 'user_id' => $data['user_id'],
'firma_id' => $data['firma_id'] ?? null,
'sender_account_id' => $data['sender_account_id'] ?? null,
]); ]);
if ($request->has('tags')) {
$lista->tags()->sync($request->tags);
}
if (!empty($data['contatti_json'])) { if (!empty($data['contatti_json'])) {
$contatti = json_decode($data['contatti_json'], true); $contatti = json_decode($data['contatti_json'], true);
if (is_array($contatti)) { if (is_array($contatti)) {
foreach ($contatti as $contatto) { foreach ($contatti as $contatto) {
if (!empty($contatto['individuo_id']) && !empty($contatto['email'])) { if (!empty($contatto['individuo_id'])) {
$lista->contatti()->create([ $lista->contatti()->create([
'individuo_id' => $contatto['individuo_id'], 'individuo_id' => $contatto['individuo_id'],
'opt_in' => true, 'opt_in' => true,
@@ -63,15 +118,20 @@ class MailingListController extends Controller
public function show($mailing_liste) public function show($mailing_liste)
{ {
$this->authorizeRead('mailing'); $this->authorizeRead('mailing');
$mailingList = MailingList::with(['contatti.individuo.contatti'])->findOrFail($mailing_liste); $mailingList = MailingList::with(['contatti.individuo.contatti', 'tags'])->findOrFail($mailing_liste);
return view('mailing-liste.show', compact('mailingList')); return view('mailing-liste.show', compact('mailingList'));
} }
public function edit($mailingList) public function edit($mailingList)
{ {
$this->authorizeWrite('mailing'); $this->authorizeWrite('mailing');
$mailingList = MailingList::with('contatti.individuo.contatti')->findOrFail($mailingList); $mailingList = MailingList::with(['contatti.individuo.contatti', 'tags', 'firma', 'senderAccount'])->findOrFail($mailingList);
return view('mailing-liste.edit', compact('mailingList')); $tags = Tag::orderBy('name')->get();
$selectedTags = $mailingList->tags->pluck('id')->toArray();
$settings = \App\Models\EmailSetting::getActive();
$firme = $settings?->firme ?? collect();
$senderAccounts = SenderAccount::active()->get();
return view('mailing-liste.edit', compact('mailingList', 'tags', 'selectedTags', 'firme', 'senderAccounts'));
} }
public function update(Request $request, $mailingList) public function update(Request $request, $mailingList)
@@ -84,6 +144,10 @@ class MailingListController extends Controller
'descrizione' => 'nullable|string', 'descrizione' => 'nullable|string',
'attiva' => 'boolean', 'attiva' => 'boolean',
'contatti_json' => 'nullable|string', 'contatti_json' => 'nullable|string',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
'firma_id' => 'nullable|integer|exists:firme,id',
'sender_account_id' => 'nullable|integer|exists:sender_accounts,id',
]); ]);
$data['attiva'] = $data['attiva'] ?? false; $data['attiva'] = $data['attiva'] ?? false;
@@ -92,8 +156,16 @@ class MailingListController extends Controller
'nome' => $data['nome'], 'nome' => $data['nome'],
'descrizione' => $data['descrizione'] ?? null, 'descrizione' => $data['descrizione'] ?? null,
'attiva' => $data['attiva'], 'attiva' => $data['attiva'],
'firma_id' => $data['firma_id'] ?? null,
'sender_account_id' => $data['sender_account_id'] ?? null,
]); ]);
if ($request->has('tags')) {
$mailingList->tags()->sync($request->tags);
} else {
$mailingList->tags()->detach();
}
if (!empty($data['contatti_json'])) { if (!empty($data['contatti_json'])) {
$newContatti = json_decode($data['contatti_json'], true); $newContatti = json_decode($data['contatti_json'], true);
if (is_array($newContatti)) { if (is_array($newContatti)) {
@@ -107,7 +179,7 @@ class MailingListController extends Controller
$toAdd = array_diff($newIds, $existingIds); $toAdd = array_diff($newIds, $existingIds);
foreach ($newContatti as $contatto) { foreach ($newContatti as $contatto) {
if (in_array($contatto['individuo_id'], $toAdd) && !empty($contatto['email'])) { if (in_array($contatto['individuo_id'], $toAdd)) {
$mailingList->contatti()->create([ $mailingList->contatti()->create([
'individuo_id' => $contatto['individuo_id'], 'individuo_id' => $contatto['individuo_id'],
'opt_in' => true, 'opt_in' => true,
@@ -129,6 +201,41 @@ class MailingListController extends Controller
return redirect()->route('mailing-liste.index')->with('success', 'Lista eliminata.'); return redirect()->route('mailing-liste.index')->with('success', 'Lista eliminata.');
} }
public function massTag(Request $request)
{
$this->authorizeWrite('mailing');
$idsInput = $request->input('ids', '');
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
if (empty($ids)) {
return back()->with('error', 'Nessuna mailing list selezionata.');
}
$data = $request->validate([
'tags' => 'required|array',
'tags.*' => 'exists:tags,id',
'mode' => 'required|in:assign,remove',
]);
$tagIds = $data['tags'];
$mode = $data['mode'];
$count = 0;
MailingList::whereIn('id', $ids)->chunk(100, function ($lists) use ($tagIds, $mode, &$count) {
foreach ($lists as $list) {
if ($mode === 'assign') {
$list->tags()->syncWithoutDetaching($tagIds);
} else {
$list->tags()->detach($tagIds);
}
$count++;
}
});
$actionLabel = $mode === 'assign' ? 'assegnati' : 'rimossi';
return back()->with('success', "Tag $actionLabel per $count mailing list.");
}
public function massElimina(Request $request) public function massElimina(Request $request)
{ {
$this->authorizeDelete('mailing'); $this->authorizeDelete('mailing');
+161 -5
View File
@@ -11,6 +11,7 @@ use App\Models\Evento;
use App\Models\Documento; use App\Models\Documento;
use App\Models\MailingList; use App\Models\MailingList;
use App\Models\Contatto; use App\Models\Contatto;
use App\Models\Tag;
use App\Models\TipologiaDocumento; use App\Models\TipologiaDocumento;
use App\Models\ReportCustom; use App\Models\ReportCustom;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -47,12 +48,15 @@ class ReportController extends Controller
$columnOptions[$type] = ReportColumnRegistry::getColumns($type); $columnOptions[$type] = ReportColumnRegistry::getColumns($type);
} }
$allTags = Tag::orderBy('name')->get();
return view('report.index', compact( return view('report.index', compact(
'stats', 'stats',
'prebuiltReports', 'prebuiltReports',
'customReports', 'customReports',
'tipologieDocumento', 'tipologieDocumento',
'columnOptions' 'columnOptions',
'allTags'
)); ));
} }
@@ -136,6 +140,14 @@ class ReportController extends Controller
$config['search_columns'] = $request->input('search_columns'); $config['search_columns'] = $request->input('search_columns');
} }
if ($request->has('tag_filter')) {
$config['tag_filter'] = $request->input('tag_filter');
}
if ($request->filled('tag_filter_mode')) {
$config['tag_filter_mode'] = $request->input('tag_filter_mode');
}
ReportCustom::create([ ReportCustom::create([
'user_id' => auth()->id(), 'user_id' => auth()->id(),
'nome' => $data['nome'], 'nome' => $data['nome'],
@@ -252,6 +264,9 @@ class ReportController extends Controller
'documenti_completo' => $this->reportDocumentiCompleto($tipologiaFilter), 'documenti_completo' => $this->reportDocumentiCompleto($tipologiaFilter),
'contatti_completo' => $this->reportContattiCompleto(), 'contatti_completo' => $this->reportContattiCompleto(),
'scadenze_documenti' => $this->reportScadenzeDocumenti($tipologiaFilter), 'scadenze_documenti' => $this->reportScadenzeDocumenti($tipologiaFilter),
'tag_statistiche' => $this->reportTagStatistiche(),
'individui_per_tag' => $this->reportIndividuiPerTag(),
'documenti_per_tag' => $this->reportDocumentiPerTag(),
default => null, default => null,
}; };
} }
@@ -403,6 +418,30 @@ class ReportController extends Controller
'colore' => 'teal', 'colore' => 'teal',
'has_tipologia_filter' => false, 'has_tipologia_filter' => false,
], ],
[
'id' => 'tag_statistiche',
'nome' => 'Statistiche Tag',
'descrizione' => 'Distribuzione degli elementi per tag (individui, gruppi, eventi, documenti, mailing list)',
'icona' => 'fa-tags',
'colore' => 'indigo',
'has_tipologia_filter' => false,
],
[
'id' => 'individui_per_tag',
'nome' => 'Individui per Tag',
'descrizione' => 'Elenco completo degli individui raggruppati per tag assegnato',
'icona' => 'fa-user-tag',
'colore' => 'info',
'has_tipologia_filter' => false,
],
[
'id' => 'documenti_per_tag',
'nome' => 'Documenti per Tag',
'descrizione' => 'Elenco completo dei documenti raggruppati per tag assegnato',
'icona' => 'fa-tags',
'colore' => 'secondary',
'has_tipologia_filter' => false,
],
]; ];
} }
@@ -476,7 +515,7 @@ class ReportController extends Controller
$rows[] = [ $rows[] = [
'livello' => 0, 'livello' => 0,
'nome' => $gruppo->nome, 'nome' => $gruppo->nome,
'diocesi' => $gruppo->diocesi?->nome ?? '-', 'diocesi' => $gruppo->diocesi->count() > 0 ? $gruppo->diocesi->pluck('nome')->implode(', ') : '-',
'membri' => $gruppo->individui()->count(), 'membri' => $gruppo->individui()->count(),
'padre' => '-', 'padre' => '-',
]; ];
@@ -499,7 +538,7 @@ class ReportController extends Controller
$rows[] = [ $rows[] = [
'livello' => $level, 'livello' => $level,
'nome' => str_repeat(' ', $level) . $child->nome, 'nome' => str_repeat(' ', $level) . $child->nome,
'diocesi' => $child->diocesi?->nome ?? '-', 'diocesi' => $child->diocesi->count() > 0 ? $child->diocesi->pluck('nome')->implode(', ') : '-',
'membri' => $child->individui()->count(), 'membri' => $child->individui()->count(),
'padre' => $parent?->nome ?? '-', 'padre' => $parent?->nome ?? '-',
]; ];
@@ -515,7 +554,7 @@ class ReportController extends Controller
$parent = $g->parent; $parent = $g->parent;
return [ return [
'nome' => $g->full_path, 'nome' => $g->full_path,
'diocesi' => $g->diocesi?->nome ?? '-', 'diocesi' => $g->diocesi->count() > 0 ? $g->diocesi->pluck('nome')->implode(', ') : '-',
'membri' => $g->individui_count, 'membri' => $g->individui_count,
'padre' => $parent?->nome ?? '-', 'padre' => $parent?->nome ?? '-',
'responsabili' => $g->getResponsabili()->pluck('cognome')->implode(', ') ?: '-', 'responsabili' => $g->getResponsabili()->pluck('cognome')->implode(', ') ?: '-',
@@ -856,7 +895,7 @@ class ReportController extends Controller
return [ return [
'nome' => $g->nome, 'nome' => $g->nome,
'descrizione' => $g->descrizione ?? '-', 'descrizione' => $g->descrizione ?? '-',
'diocesi' => $g->diocesi?->nome ?? '-', 'diocesi' => $g->diocesi->count() > 0 ? $g->diocesi->pluck('nome')->implode(', ') : '-',
'livello' => $g->parent_id ? (count($g->getAncestors()) + 1) : 0, 'livello' => $g->parent_id ? (count($g->getAncestors()) + 1) : 0,
'membri' => $g->individui_count, 'membri' => $g->individui_count,
'padre' => $parent?->nome ?? '-', 'padre' => $parent?->nome ?? '-',
@@ -927,6 +966,102 @@ class ReportController extends Controller
]; ];
} }
protected function reportTagStatistiche(): array
{
$tags = Tag::orderBy('name')->get();
$rows = $tags->map(function ($tag) {
return [
'name' => $tag->name,
'color' => $tag->color ?? '#6c757d',
'individui' => $tag->individui()->count(),
'gruppi' => $tag->gruppi()->count(),
'eventi' => $tag->eventi()->count(),
'documenti' => $tag->documenti()->count(),
'mailing' => $tag->mailingLists()->count(),
'totale' => $tag->count,
];
})->toArray();
$totalIndividui = array_sum(array_column($rows, 'individui'));
$totalGruppi = array_sum(array_column($rows, 'gruppi'));
$totalEventi = array_sum(array_column($rows, 'eventi'));
$totalDocumenti = array_sum(array_column($rows, 'documenti'));
$totalMailing = array_sum(array_column($rows, 'mailing'));
return [
'title' => 'Statistiche Tag',
'headers' => ['Tag', 'Individui', 'Gruppi', 'Eventi', 'Documenti', 'Mailing List', 'Totale Elementi'],
'rows' => array_map(fn($r) => [$r['name'], $r['individui'], $r['gruppi'], $r['eventi'], $r['documenti'], $r['mailing'], $r['totale']], $rows),
'summary' => 'Tag: ' . count($tags) . ' | Individui: ' . $totalIndividui . ' | Gruppi: ' . $totalGruppi . ' | Eventi: ' . $totalEventi . ' | Documenti: ' . $totalDocumenti . ' | Mailing List: ' . $totalMailing,
'detail_rows' => $rows,
];
}
protected function reportIndividuiPerTag(): array
{
$individui = Individuo::with('tags')
->whereHas('tags')
->orderBy('cognome')
->orderBy('nome')
->get();
$rows = $individui->flatMap(function ($ind) {
if ($ind->tags->isEmpty()) {
return collect();
}
return $ind->tags->map(function ($tag) use ($ind) {
return [
'tag' => $tag->name,
'tag_color' => $tag->color ?? '#6c757d',
'codice' => $ind->codice_id,
'cognome' => $ind->cognome,
'nome' => $ind->nome,
'email' => $ind->email_primaria ?? '-',
];
});
})->toArray();
return [
'title' => 'Individui per Tag',
'headers' => ['Tag', 'Codice', 'Cognome', 'Nome', 'Email'],
'rows' => array_map(fn($r) => [$r['tag'], $r['codice'], $r['cognome'], $r['nome'], $r['email']], $rows),
'summary' => 'Totale righe (individuo × tag): ' . count($rows),
'detail_rows' => $rows,
];
}
protected function reportDocumentiPerTag(): array
{
$documenti = Documento::with('tags')
->whereHas('tags')
->orderBy('nome_file')
->get();
$rows = $documenti->flatMap(function ($doc) {
if ($doc->tags->isEmpty()) {
return collect();
}
return $doc->tags->map(function ($tag) use ($doc) {
return [
'tag' => $tag->name,
'tag_color' => $tag->color ?? '#6c757d',
'nome_file' => $doc->nome_file,
'tipologia' => ucfirst((string) $doc->tipologia),
'data' => $doc->created_at->format('d/m/Y'),
];
});
})->toArray();
return [
'title' => 'Documenti per Tag',
'headers' => ['Tag', 'Nome File', 'Tipologia', 'Data Caricamento'],
'rows' => array_map(fn($r) => [$r['tag'], $r['nome_file'], $r['tipologia'], $r['data']], $rows),
'summary' => 'Totale righe (documento × tag): ' . count($rows),
'detail_rows' => $rows,
];
}
protected function runCustomReport(ReportCustom $report): array protected function runCustomReport(ReportCustom $report): array
{ {
$tipo = $report->tipo_report; $tipo = $report->tipo_report;
@@ -954,13 +1089,34 @@ class ReportController extends Controller
$columns = $config['columns'] ?? []; $columns = $config['columns'] ?? [];
$hasRelations = false; $hasRelations = false;
// Always load tags if tags column or tag filter is present
$needsTags = in_array('tags.nome', $columns) || !empty($config['tag_filter']);
if ($needsTags) {
$query->with('tags');
}
foreach ($columns as $col) { foreach ($columns as $col) {
if (str_contains($col, '.')) { if (str_contains($col, '.')) {
$hasRelations = true; $hasRelations = true;
$relation = explode('.', $col)[0]; $relation = explode('.', $col)[0];
if ($relation !== 'tags') {
$query->with($relation); $query->with($relation);
} }
} }
}
// Apply tag filter
if (!empty($config['tag_filter'])) {
$tagSlugs = Tag::whereIn('id', (array) $config['tag_filter'])->pluck('slug')->toArray();
if (!empty($tagSlugs)) {
$mode = $config['tag_filter_mode'] ?? 'any';
if ($mode === 'all') {
$query->withAllTags($tagSlugs);
} else {
$query->withAnyTags($tagSlugs);
}
}
}
if (!empty($config['search'])) { if (!empty($config['search'])) {
$search = $config['search']; $search = $config['search'];
@@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers;
use App\Models\Tag;
use App\Models\Individuo;
use App\Models\Gruppo;
use App\Models\Evento;
use App\Models\Documento;
use App\Models\MailingList;
use Illuminate\Http\Request;
class RicercaController extends Controller
{
public function index(Request $request)
{
$this->authorizeRead('individui');
$allTags = Tag::orderBy('name')->get();
$tagSlugs = (array) $request->input('tag', []);
if (empty($tagSlugs)) {
return view('ricerca.index', compact('allTags'));
}
$tagSlugs = array_filter($tagSlugs);
$selectedTags = Tag::whereIn('slug', $tagSlugs)->get();
$results = [];
$totals = [];
if (count($tagSlugs) === 1) {
$tag = $selectedTags->first();
$results['individui'] = $tag->individui()->orderBy('cognome')->orderBy('nome')->get();
$results['gruppi'] = $tag->gruppi()->orderBy('nome')->get();
$results['eventi'] = $tag->eventi()->orderBy('data_specifica')->orderBy('nome_evento')->get();
$results['documenti'] = $tag->documenti()->orderBy('nome_file')->get();
$results['mailingLists'] = $tag->mailingLists()->orderBy('nome')->get();
} else {
$results['individui'] = Individuo::withAllTags($tagSlugs)->orderBy('cognome')->orderBy('nome')->get();
$results['gruppi'] = Gruppo::withAllTags($tagSlugs)->orderBy('nome')->get();
$results['eventi'] = Evento::withAllTags($tagSlugs)->orderBy('data_specifica')->orderBy('nome_evento')->get();
$results['documenti'] = Documento::withAllTags($tagSlugs)->orderBy('nome_file')->get();
$results['mailingLists'] = MailingList::withAllTags($tagSlugs)->orderBy('nome')->get();
}
$totals = [
'individui' => $results['individui']->count(),
'gruppi' => $results['gruppi']->count(),
'eventi' => $results['eventi']->count(),
'documenti' => $results['documenti']->count(),
'mailingLists' => $results['mailingLists']->count(),
];
return view('ricerca.index', compact('selectedTags', 'tagSlugs', 'results', 'totals', 'allTags'));
}
}
+11 -1
View File
@@ -33,9 +33,11 @@ class VistaReportController extends Controller
$this->authorizeWrite('viste'); $this->authorizeWrite('viste');
$data = $request->validate([ $data = $request->validate([
'nome' => 'required|string|max:255', 'nome' => 'required|string|max:255',
'tipo' => 'required|in:individui,gruppi,documenti,eventi', 'tipo' => 'required|in:individui,gruppi,documenti,eventi,mailing-liste',
'colonne_visibili' => 'nullable|array', 'colonne_visibili' => 'nullable|array',
'colonne_ordinamento' => 'nullable|array', 'colonne_ordinamento' => 'nullable|array',
'colonne_larghezze' => 'nullable|array',
'colonne_ordine' => 'nullable|array',
'filtri' => 'nullable|array', 'filtri' => 'nullable|array',
'ricerca' => 'nullable|string', 'ricerca' => 'nullable|string',
'is_default' => 'nullable|boolean', 'is_default' => 'nullable|boolean',
@@ -55,6 +57,8 @@ class VistaReportController extends Controller
'tipo' => $data['tipo'], 'tipo' => $data['tipo'],
'colonne_visibili' => $data['colonne_visibili'] ?? null, 'colonne_visibili' => $data['colonne_visibili'] ?? null,
'colonne_ordinamento' => $data['colonne_ordinamento'] ?? null, 'colonne_ordinamento' => $data['colonne_ordinamento'] ?? null,
'colonne_larghezze' => $data['colonne_larghezze'] ?? null,
'colonne_ordine' => $data['colonne_ordine'] ?? null,
'filtri' => $data['filtri'] ?? null, 'filtri' => $data['filtri'] ?? null,
'ricerca' => $data['ricerca'] ?? null, 'ricerca' => $data['ricerca'] ?? null,
'is_default' => $isDefault, 'is_default' => $isDefault,
@@ -78,6 +82,8 @@ class VistaReportController extends Controller
'nome' => 'required|string|max:255', 'nome' => 'required|string|max:255',
'colonne_visibili' => 'nullable|array', 'colonne_visibili' => 'nullable|array',
'colonne_ordinamento' => 'nullable|array', 'colonne_ordinamento' => 'nullable|array',
'colonne_larghezze' => 'nullable|array',
'colonne_ordine' => 'nullable|array',
'filtri' => 'nullable|array', 'filtri' => 'nullable|array',
'ricerca' => 'nullable|string', 'ricerca' => 'nullable|string',
'is_default' => 'nullable|boolean', 'is_default' => 'nullable|boolean',
@@ -92,6 +98,10 @@ class VistaReportController extends Controller
$vistaReport->update($data); $vistaReport->update($data);
if ($request->expectsJson()) {
return response()->json(['success' => true, 'message' => 'Vista aggiornata.']);
}
return back()->with('success', 'Vista aggiornata.'); return back()->with('success', 'Vista aggiornata.');
} }
+4 -2
View File
@@ -53,11 +53,13 @@ class CalendarioConnessione extends Model
return $config; return $config;
} }
public function encryptAndSetConfig(array $config): void public function encryptAndSetConfig(array $config, array $existingConfig = []): void
{ {
foreach (['username', 'password', 'client_secret', 'refresh_token', 'api_key'] as $field) { foreach (['username', 'password', 'client_secret', 'refresh_token', 'api_key'] as $field) {
if (isset($config[$field]) && $config[$field] !== '' && !str_starts_with($config[$field], 'eyJpdiI')) { if (!empty($config[$field]) && !str_starts_with($config[$field], 'eyJpdiI')) {
$config[$field] = Crypt::encryptString($config[$field]); $config[$field] = Crypt::encryptString($config[$field]);
} elseif (empty($config[$field]) && !empty($existingConfig[$field])) {
$config[$field] = $existingConfig[$field];
} }
} }
$this->config = $config; $this->config = $config;
+3 -3
View File
@@ -3,15 +3,15 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Diocesi extends Model class Diocesi extends Model
{ {
protected $table = 'diocesi'; protected $table = 'diocesi';
protected $fillable = ['nome', 'regione']; protected $fillable = ['nome', 'regione'];
public function gruppi(): HasMany public function gruppi(): BelongsToMany
{ {
return $this->hasMany(Gruppo::class); return $this->belongsToMany(Gruppo::class, 'diocesi_gruppo');
} }
} }
+10 -1
View File
@@ -1,7 +1,10 @@
<?php <?php
declare(strict_types=1);
namespace App\Models; namespace App\Models;
use App\Traits\HasTagsLight;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Relations\MorphTo;
@@ -9,14 +12,20 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Documento extends Model class Documento extends Model
{ {
use HasTagsLight;
protected $table = 'documenti'; protected $table = 'documenti';
protected $fillable = [ protected $fillable = [
'tenant_id', 'user_id', 'cartella_id', 'repository_id', 'storage_disk', 'tenant_id', 'user_id', 'cartella_id', 'repository_id', 'storage_disk',
'nome_file', 'file_path', 'tipo', 'tipologia', 'nome_file', 'file_path', 'tipo', 'tipologia',
'visibilita', 'visibilita_target_id', 'visibilita_target_type', 'visibilita', 'visibilita_target_id', 'visibilita_target_type',
'mime_type', 'dimensione', 'note' 'mime_type', 'dimensione', 'note', 'url'
]; ];
public function isLink(): bool
{
return $this->tipo === 'link';
}
public function tenant(): BelongsTo public function tenant(): BelongsTo
{ {
return $this->belongsTo(Tenant::class); return $this->belongsTo(Tenant::class);
+152 -4
View File
@@ -2,7 +2,9 @@
namespace App\Models; namespace App\Models;
use App\Services\GoogleOAuthService;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Crypt;
@@ -13,14 +15,17 @@ class EmailSetting extends Model
protected $fillable = [ protected $fillable = [
'imap_host', 'imap_port', 'imap_encryption', 'imap_username', 'imap_password', 'imap_host', 'imap_port', 'imap_encryption', 'imap_username', 'imap_password',
'smtp_host', 'smtp_port', 'smtp_encryption', 'smtp_username', 'smtp_password', 'smtp_host', 'smtp_port', 'smtp_encryption', 'smtp_username', 'smtp_password',
'email_address', 'email_name', 'reply_to', 'sync_interval_minutes', 'email_address', 'email_name', 'from_email', 'from_name', 'reply_to',
'last_sync_at', 'is_active', 'signature', 'signature_enabled' 'sync_interval_minutes',
'last_sync_at', 'is_active', 'signature', 'signature_enabled',
'auth_method', 'google_oauth_connection_id',
]; ];
protected $casts = [ protected $casts = [
'is_active' => 'boolean', 'is_active' => 'boolean',
'signature_enabled' => 'boolean', 'signature_enabled' => 'boolean',
'last_sync_at' => 'datetime', 'last_sync_at' => 'datetime',
'google_oauth_connection_id' => 'integer',
]; ];
public function folders(): HasMany public function folders(): HasMany
@@ -28,8 +33,25 @@ class EmailSetting extends Model
return $this->hasMany(EmailFolder::class)->orderBy('sort_order'); return $this->hasMany(EmailFolder::class)->orderBy('sort_order');
} }
public function googleOAuthConnection(): BelongsTo
{
return $this->belongsTo(GoogleOAuthConnection::class);
}
public function getImapConfig(): array public function getImapConfig(): array
{ {
if ($this->auth_method === 'oauth') {
$token = $this->resolveOAuthToken();
return [
'host' => $this->imap_host,
'port' => $this->imap_port,
'encryption' => $this->imap_encryption,
'username' => $this->imap_username,
'authentication' => 'oauth',
'password' => $token,
];
}
return [ return [
'host' => $this->imap_host, 'host' => $this->imap_host,
'port' => $this->imap_port, 'port' => $this->imap_port,
@@ -41,6 +63,10 @@ class EmailSetting extends Model
public function getDecryptedPassword(): string public function getDecryptedPassword(): string
{ {
if ($this->auth_method === 'oauth') {
return $this->resolveOAuthToken();
}
if (empty($this->imap_password)) { if (empty($this->imap_password)) {
return ''; return '';
} }
@@ -53,6 +79,10 @@ class EmailSetting extends Model
public function getDecryptedSmtpPassword(): string public function getDecryptedSmtpPassword(): string
{ {
if ($this->auth_method === 'oauth') {
return $this->resolveOAuthToken();
}
if (empty($this->smtp_password)) { if (empty($this->smtp_password)) {
return $this->getDecryptedPassword(); return $this->getDecryptedPassword();
} }
@@ -63,6 +93,22 @@ class EmailSetting extends Model
} }
} }
protected function resolveOAuthToken(): string
{
if (!$this->relationLoaded('googleOAuthConnection')) {
$this->load('googleOAuthConnection');
}
$connection = $this->googleOAuthConnection;
if (!$connection) {
return '';
}
try {
return app(GoogleOAuthService::class)->getAccessToken($connection);
} catch (\Exception $e) {
return '';
}
}
public function getSmtpConfig(): array public function getSmtpConfig(): array
{ {
return [ return [
@@ -87,19 +133,87 @@ class EmailSetting extends Model
default => 'none', default => 'none',
}; };
return new \DirectoryTree\ImapEngine\Mailbox([ $config = [
'host' => $this->imap_host, 'host' => $this->imap_host,
'port' => $this->imap_port ?? 993, 'port' => $this->imap_port ?? 993,
'encryption' => $encryption, 'encryption' => $encryption,
'validate_cert' => false, 'validate_cert' => false,
'username' => $this->imap_username, 'username' => $this->imap_username,
'password' => $this->getDecryptedPassword(), 'password' => $this->getDecryptedPassword(),
]); ];
if ($this->auth_method === 'oauth') {
$config['authentication'] = 'oauth';
}
return new \DirectoryTree\ImapEngine\Mailbox($config);
} catch (\Exception $e) { } catch (\Exception $e) {
return null; return null;
} }
} }
public function getSmtpMailer(): ?\Symfony\Component\Mailer\Mailer
{
if ($this->auth_method === 'oauth') {
return $this->buildOAuthSmtpMailer();
}
$dsn = $this->buildSmtpDsn();
if (!$dsn) {
return null;
}
try {
$transport = \Symfony\Component\Mailer\Transport::fromDsn($dsn);
return new \Symfony\Component\Mailer\Mailer($transport);
} catch (\Exception $e) {
return null;
}
}
protected function buildOAuthSmtpMailer(): ?\Symfony\Component\Mailer\Mailer
{
$token = $this->resolveOAuthToken();
if (empty($token)) {
return null;
}
$username = $this->smtp_username ?? $this->email_address;
$host = $this->smtp_host ?? 'smtp.gmail.com';
$port = $this->smtp_port ?? 587;
$transport = new \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport($host, $port, true);
$transport->setUsername($username);
$transport->setPassword($token);
$transport->setAuthenticators([
new \Symfony\Component\Mailer\Transport\Smtp\Auth\XOAuth2Authenticator(),
]);
return new \Symfony\Component\Mailer\Mailer($transport);
}
public function buildSmtpDsn(): ?string
{
$password = $this->getDecryptedSmtpPassword();
if (empty($password)) {
return null;
}
$username = $this->smtp_username ?? $this->email_address;
$encryption = $this->smtp_encryption ?? 'tls';
$scheme = ($encryption === 'ssl') ? 'smtps' : 'smtp';
return sprintf(
'%s://%s:%s@%s:%d?encryption=%s',
$scheme,
rawurlencode($username),
rawurlencode($password),
$this->smtp_host ?? 'smtp.gmail.com',
$this->smtp_port ?? 587,
$encryption
);
}
public function testConnection(): array public function testConnection(): array
{ {
try { try {
@@ -134,4 +248,38 @@ class EmailSetting extends Model
{ {
return $this->signature; return $this->signature;
} }
public function getEffectiveFromAddress(): string
{
return $this->from_email ?: $this->email_address;
}
public function getEffectiveFromName(): string
{
$name = $this->from_name ?: $this->email_name;
if (filter_var($name, FILTER_VALIDATE_EMAIL)) {
return 'Glastree';
}
$name = preg_replace('/[^\p{L}\p{N}\s]/u', '', $name);
$name = trim($name);
return empty($name) ? 'Glastree' : $name;
}
public function getEffectiveReplyTo(): ?string
{
if ($this->from_email) {
return $this->from_email;
}
return $this->reply_to ?: null;
}
public function firme(): HasMany
{
return $this->hasMany(Firma::class);
}
public function firmaPredefinita(): ?Firma
{
return $this->firme()->where('is_default', true)->first() ?? $this->firme()->first();
}
} }
+4
View File
@@ -1,7 +1,10 @@
<?php <?php
declare(strict_types=1);
namespace App\Models; namespace App\Models;
use App\Traits\HasTagsLight;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@@ -9,6 +12,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
class Evento extends Model class Evento extends Model
{ {
use HasTagsLight;
protected $table = 'eventi'; protected $table = 'eventi';
protected $fillable = [ protected $fillable = [
+29
View File
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Firma extends Model
{
protected $table = 'firme';
protected $fillable = [
'email_setting_id',
'nome',
'contenuto_html',
'is_default',
];
protected $casts = [
'is_default' => 'boolean',
];
public function emailSetting(): BelongsTo
{
return $this->belongsTo(EmailSetting::class);
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Services\GoogleOAuthService;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class GoogleOAuthConnection extends Model
{
protected $table = 'google_oauth_connections';
protected $fillable = [
'user_id',
'service',
'email',
'access_token',
'refresh_token',
'expires_at',
];
protected $casts = [
'expires_at' => 'datetime',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function isExpired(): bool
{
return $this->expires_at && $this->expires_at->isPast();
}
public function refresh(): void
{
if (!$this->refresh_token) {
throw new \RuntimeException('Nessun refresh token disponibile per ' . $this->email);
}
app(GoogleOAuthService::class)->refreshToken($this);
}
public function getValidAccessToken(): string
{
if ($this->isExpired() && $this->refresh_token) {
$this->refresh();
}
return $this->access_token;
}
}
+25 -3
View File
@@ -2,6 +2,7 @@
namespace App\Models; namespace App\Models;
use App\Traits\HasTagsLight;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -10,18 +11,22 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Gruppo extends Model class Gruppo extends Model
{ {
use HasTagsLight;
protected $table = 'gruppi'; protected $table = 'gruppi';
protected $fillable = [ protected $fillable = [
'tenant_id', 'parent_id', 'diocesi_id', 'responsabile_ids', 'tenant_id', 'parent_id', 'diocesi_id', 'responsabile_ids',
'nome', 'descrizione', 'indirizzo_incontro', 'cap_incontro', 'nome', 'anno_fondazione', 'descrizione', 'indirizzo_incontro', 'cap_incontro',
'città_incontro', 'sigla_provincia_incontro', 'mappa_posizione', 'metadata' 'città_incontro', 'sigla_provincia_incontro', 'mappa_posizione', 'metadata'
]; ];
protected $casts = [ protected $casts = [
'metadata' => 'array', 'metadata' => 'array',
'responsabile_ids' => 'array', 'responsabile_ids' => 'array',
'anno_fondazione' => 'integer',
]; ];
protected $appends = ['email_primaria', 'telefono_primario'];
public function tenant(): BelongsTo public function tenant(): BelongsTo
{ {
return $this->belongsTo(Tenant::class); return $this->belongsTo(Tenant::class);
@@ -37,9 +42,14 @@ class Gruppo extends Model
return $this->hasMany(Gruppo::class, 'parent_id'); return $this->hasMany(Gruppo::class, 'parent_id');
} }
public function diocesi(): BelongsTo public function diocesi(): BelongsToMany
{ {
return $this->belongsTo(Diocesi::class); return $this->belongsToMany(Diocesi::class, 'diocesi_gruppo');
}
public function gruppoContatti(): HasMany
{
return $this->hasMany(GruppoContatto::class, 'gruppo_id');
} }
public function individui(): BelongsToMany public function individui(): BelongsToMany
@@ -64,6 +74,18 @@ class Gruppo extends Model
return null; return null;
} }
public function getEmailPrimariaAttribute(): ?string
{
return $this->gruppoContatti()->where('tipo', 'email')->where('is_primary', true)->first()?->valore
?? $this->gruppoContatti()->where('tipo', 'email')->first()?->valore;
}
public function getTelefonoPrimarioAttribute(): ?string
{
return $this->gruppoContatti()->whereIn('tipo', ['telefono', 'cellulare'])->where('is_primary', true)->first()?->valore
?? $this->gruppoContatti()->whereIn('tipo', ['telefono', 'cellulare'])->first()?->valore;
}
public function getResponsabili() public function getResponsabili()
{ {
if (empty($this->responsabile_ids)) { if (empty($this->responsabile_ids)) {
+23
View File
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class GruppoContatto extends Model
{
protected $table = 'gruppo_contatti';
protected $fillable = ['gruppo_id', 'tipo', 'valore', 'etichetta', 'is_primary'];
protected $casts = [
'is_primary' => 'boolean',
];
public function gruppo(): BelongsTo
{
return $this->belongsTo(Gruppo::class);
}
}
+2
View File
@@ -2,6 +2,7 @@
namespace App\Models; namespace App\Models;
use App\Traits\HasTagsLight;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -10,6 +11,7 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
class Individuo extends Model class Individuo extends Model
{ {
use HasTagsLight;
protected $table = 'individui'; protected $table = 'individui';
protected $fillable = [ protected $fillable = [
'tenant_id', 'codice_id', 'cognome', 'nome', 'data_nascita', 'tenant_id', 'codice_id', 'cognome', 'nome', 'data_nascita',
+14 -1
View File
@@ -5,11 +5,14 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use App\Traits\HasTagsLight;
class MailingList extends Model class MailingList extends Model
{ {
use HasTagsLight;
protected $table = 'mailing_lists'; protected $table = 'mailing_lists';
protected $fillable = ['tenant_id', 'user_id', 'nome', 'descrizione', 'attiva']; protected $fillable = ['tenant_id', 'user_id', 'nome', 'descrizione', 'attiva', 'firma_id', 'sender_account_id'];
protected $casts = ['attiva' => 'boolean']; protected $casts = ['attiva' => 'boolean'];
@@ -33,6 +36,16 @@ class MailingList extends Model
return $this->hasMany(MailingMessaggio::class); return $this->hasMany(MailingMessaggio::class);
} }
public function firma(): BelongsTo
{
return $this->belongsTo(Firma::class);
}
public function senderAccount(): BelongsTo
{
return $this->belongsTo(SenderAccount::class);
}
public function getIndividui() public function getIndividui()
{ {
return Individuo::whereHas('mailingContacts', function ($q) { return Individuo::whereHas('mailingContacts', function ($q) {
+1 -1
View File
@@ -11,7 +11,7 @@ class MailingMessaggio extends Model
protected $fillable = [ protected $fillable = [
'tenant_id', 'mailing_list_id', 'user_id', 'oggetto', 'corpo', 'tenant_id', 'mailing_list_id', 'user_id', 'oggetto', 'corpo',
'canale', 'stato', 'inviato_al', 'totale_destinatari', 'invii_ok', 'invii_falliti', 'canale', 'stato', 'inviato_al', 'totale_destinatari', 'invii_ok', 'invii_falliti',
'log_falliti', 'mittente_nome', 'mittente_email', 'log_falliti', 'mittente_nome', 'mittente_email', 'firma_id',
]; ];
protected $casts = [ protected $casts = [
+52
View File
@@ -4,8 +4,10 @@ declare(strict_types=1);
namespace App\Models; namespace App\Models;
use App\Services\GoogleOAuthService;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Crypt;
class SenderAccount extends Model class SenderAccount extends Model
@@ -24,11 +26,14 @@ class SenderAccount extends Model
'verify_email', 'verify_email',
'note', 'note',
'is_active', 'is_active',
'auth_method',
'google_oauth_connection_id',
]; ];
protected $casts = [ protected $casts = [
'is_active' => 'boolean', 'is_active' => 'boolean',
'smtp_port' => 'integer', 'smtp_port' => 'integer',
'google_oauth_connection_id' => 'integer',
]; ];
public function scopeActive(Builder $query): Builder public function scopeActive(Builder $query): Builder
@@ -36,8 +41,33 @@ class SenderAccount extends Model
return $query->where('is_active', true); return $query->where('is_active', true);
} }
public function googleOAuthConnection(): BelongsTo
{
return $this->belongsTo(GoogleOAuthConnection::class);
}
protected function resolveOAuthToken(): string
{
if (!$this->relationLoaded('googleOAuthConnection')) {
$this->load('googleOAuthConnection');
}
$connection = $this->googleOAuthConnection;
if (!$connection) {
return '';
}
try {
return app(GoogleOAuthService::class)->getAccessToken($connection);
} catch (\Exception $e) {
return '';
}
}
public function getDecryptedPassword(): string public function getDecryptedPassword(): string
{ {
if ($this->auth_method === 'oauth') {
return $this->resolveOAuthToken();
}
if (empty($this->smtp_password)) { if (empty($this->smtp_password)) {
return ''; return '';
} }
@@ -121,11 +151,33 @@ class SenderAccount extends Model
$email->attachFromPath($path); $email->attachFromPath($path);
} }
if ($this->auth_method === 'oauth') {
$mailer = $this->buildOAuthMailer();
} else {
$transport = \Symfony\Component\Mailer\Transport::fromDsn($this->buildDsn()); $transport = \Symfony\Component\Mailer\Transport::fromDsn($this->buildDsn());
$mailer = new \Symfony\Component\Mailer\Mailer($transport); $mailer = new \Symfony\Component\Mailer\Mailer($transport);
}
$mailer->send($email); $mailer->send($email);
} }
protected function buildOAuthMailer(): \Symfony\Component\Mailer\Mailer
{
$token = $this->resolveOAuthToken();
$username = $this->smtp_username ?? $this->email_address;
$host = $this->smtp_host ?? 'smtp.gmail.com';
$port = $this->smtp_port ?? 587;
$transport = new \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport($host, $port, true);
$transport->setUsername($username);
$transport->setPassword($token);
$transport->setAuthenticators([
new \Symfony\Component\Mailer\Transport\Smtp\Auth\XOAuth2Authenticator(),
]);
return new \Symfony\Component\Mailer\Mailer($transport);
}
public function sendReport(string $subject, string $body): bool public function sendReport(string $subject, string $body): bool
{ {
if (empty($this->verify_email)) { if (empty($this->verify_email)) {
+62
View File
@@ -0,0 +1,62 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
class Tag extends Model
{
protected $table = 'tags';
protected $fillable = ['name', 'slug', 'color', 'order_column'];
protected $casts = [
'order_column' => 'integer',
];
protected static function boot(): void
{
parent::boot();
static::creating(function (self $tag) {
if (empty($tag->slug)) {
$tag->slug = str()->slug($tag->name);
}
});
}
public function individui(): MorphToMany
{
return $this->morphedByMany(Individuo::class, 'taggable');
}
public function gruppi(): MorphToMany
{
return $this->morphedByMany(Gruppo::class, 'taggable');
}
public function eventi(): MorphToMany
{
return $this->morphedByMany(Evento::class, 'taggable');
}
public function documenti(): MorphToMany
{
return $this->morphedByMany(Documento::class, 'taggable');
}
public function mailingLists(): MorphToMany
{
return $this->morphedByMany(MailingList::class, 'taggable');
}
public function getCountAttribute(): int
{
return $this->individui()->count()
+ $this->gruppi()->count()
+ $this->eventi()->count()
+ $this->documenti()->count()
+ $this->mailingLists()->count();
}
}
+2
View File
@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
+5 -4
View File
@@ -197,12 +197,13 @@ class User extends Authenticatable implements CanResetPasswordContract
$resetUrl = url('/password/reset/' . $token); $resetUrl = url('/password/reset/' . $token);
$fromName = $settings->email_name ?? config('app.name'); $fromAddress = new Address(
$fromName = preg_replace('/[^\p{L}\p{N}\s]/u', '', $fromName); $settings->getEffectiveFromAddress(),
$fromName = trim($fromName) ?: config('app.name'); $settings->getEffectiveFromName()
);
$email = (new Email()) $email = (new Email())
->from(new Address($settings->email_address, $fromName)) ->from($fromAddress)
->to($this->email) ->to($this->email)
->subject('Reset della password - ' . config('app.name')) ->subject('Reset della password - ' . config('app.name'))
->html(view('auth.emails.reset-password', [ ->html(view('auth.emails.reset-password', [
+4 -1
View File
@@ -10,13 +10,16 @@ class VistaReport extends Model
protected $table = 'viste_report'; protected $table = 'viste_report';
protected $fillable = [ protected $fillable = [
'user_id', 'nome', 'tipo', 'colonne_visibili', 'user_id', 'nome', 'tipo', 'colonne_visibili',
'colonne_ordinamento', 'filtri', 'ricerca', 'is_default' 'colonne_ordinamento', 'filtri', 'ricerca', 'is_default',
'colonne_larghezze', 'colonne_ordine',
]; ];
protected $casts = [ protected $casts = [
'colonne_visibili' => 'array', 'colonne_visibili' => 'array',
'colonne_ordinamento' => 'array', 'colonne_ordinamento' => 'array',
'filtri' => 'array', 'filtri' => 'array',
'colonne_larghezze' => 'array',
'colonne_ordine' => 'array',
]; ];
public function user(): BelongsTo public function user(): BelongsTo
+29 -10
View File
@@ -60,7 +60,7 @@ class BackupService
return $backups; return $backups;
} }
public function run(): array public function run(array $options = []): array
{ {
$result = ['success' => true, 'message' => '', 'filename' => '']; $result = ['success' => true, 'message' => '', 'filename' => ''];
@@ -74,12 +74,28 @@ class BackupService
$filename = $appName . '_' . $timestamp . '.zip'; $filename = $appName . '_' . $timestamp . '.zip';
$filepath = $this->backupDir . '/' . $filename; $filepath = $this->backupDir . '/' . $filename;
$includeFiles = $options['include_files'] ?? (bool) AppSetting::getSetting('backup_include_files', true);
$includeEnv = $options['include_env'] ?? (bool) AppSetting::getSetting('backup_include_env', true);
$steps = []; $steps = [];
$includedComponents = [];
$steps[] = $this->backupDatabase(); $steps[] = $this->backupDatabase();
$steps[] = $this->backupEnv(); $includedComponents[] = 'database';
$steps[] = $this->backupFiles();
$this->writeManifest($timestamp); $stepEnv = $this->backupEnv($includeEnv);
$steps[] = $stepEnv;
if ($includeEnv) {
$includedComponents[] = 'env';
}
$stepFiles = $this->backupFiles($includeFiles);
$steps[] = $stepFiles;
if ($includeFiles) {
$includedComponents[] = 'files';
}
$this->writeManifest($timestamp, $includedComponents);
$zipResult = $this->createZip($filepath); $zipResult = $this->createZip($filepath);
if (!$zipResult) { if (!$zipResult) {
@@ -90,7 +106,7 @@ class BackupService
$size = file_exists($filepath) ? filesize($filepath) : 0; $size = file_exists($filepath) ? filesize($filepath) : 0;
Log::info("Backup completato: {$filename} (" . $this->formatBytes($size) . ")"); Log::info("Backup completato: {$filename} (" . $this->formatBytes($size) . ')');
$this->cleanupOldBackups(); $this->cleanupOldBackups();
@@ -100,6 +116,7 @@ class BackupService
'filename' => $filename, 'filename' => $filename,
'size' => $size, 'size' => $size,
'size_formatted' => $this->formatBytes($size), 'size_formatted' => $this->formatBytes($size),
'steps' => $steps,
]; ];
} catch (\Exception $e) { } catch (\Exception $e) {
$this->cleanTemp(); $this->cleanTemp();
@@ -109,6 +126,7 @@ class BackupService
'success' => false, 'success' => false,
'message' => 'Backup fallito: ' . $e->getMessage(), 'message' => 'Backup fallito: ' . $e->getMessage(),
'filename' => '', 'filename' => '',
'steps' => [],
]; ];
} }
} }
@@ -181,9 +199,9 @@ class BackupService
return 'database.sql (' . $this->formatBytes(filesize($sqlFile)) . ')'; return 'database.sql (' . $this->formatBytes(filesize($sqlFile)) . ')';
} }
private function backupEnv(): string private function backupEnv(bool $includeEnv = true): string
{ {
if (!AppSetting::getSetting('backup_include_env', true)) { if (!$includeEnv) {
return '.env (escluso)'; return '.env (escluso)';
} }
@@ -200,9 +218,9 @@ class BackupService
return '.env copiato'; return '.env copiato';
} }
private function backupFiles(): string private function backupFiles(bool $includeFiles = true): string
{ {
if (!AppSetting::getSetting('backup_include_files', true)) { if (!$includeFiles) {
return 'files (esclusi)'; return 'files (esclusi)';
} }
@@ -220,7 +238,7 @@ class BackupService
return 'files copiati'; return 'files copiati';
} }
private function writeManifest(string $timestamp): void private function writeManifest(string $timestamp, array $includedComponents = ['database']): void
{ {
$manifest = [ $manifest = [
'created_at' => $timestamp, 'created_at' => $timestamp,
@@ -230,6 +248,7 @@ class BackupService
'db_name' => config('database.connections.mysql.database'), 'db_name' => config('database.connections.mysql.database'),
'php_version' => PHP_VERSION, 'php_version' => PHP_VERSION,
'laravel_version' => app()->version(), 'laravel_version' => app()->version(),
'included_components' => $includedComponents,
]; ];
File::put($this->tempDir . '/manifest.json', json_encode($manifest, JSON_PRETTY_PRINT)); File::put($this->tempDir . '/manifest.json', json_encode($manifest, JSON_PRETTY_PRINT));
+178
View File
@@ -0,0 +1,178 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\GoogleOAuthConnection;
use App\Enums\GoogleService;
use Google_Client;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class GoogleOAuthService
{
private const TOKEN_CACHE_TTL = 300;
private const MAX_RETRIES = 2;
public function buildClient(): Google_Client
{
$client = new Google_Client();
$client->setClientId(config('services.google.client_id'));
$client->setClientSecret(config('services.google.client_secret'));
$client->setRedirectUri(route('google-oauth.callback'));
$client->setAccessType('offline');
$client->setPrompt('consent');
$client->setIncludeGrantedScopes(true);
return $client;
}
public function getAuthUrl(string $service): string
{
$serviceEnum = GoogleService::from($service);
$client = $this->buildClient();
$client->setScopes($serviceEnum->scopes());
$client->setState($service);
return $client->createAuthUrl();
}
public function handleCallback(string $authorizationCode, GoogleService $service): GoogleOAuthConnection
{
$client = $this->buildClient();
$client->setScopes($service->scopes());
$client->fetchAccessTokenWithAuthCode($authorizationCode);
$tokenData = $client->getAccessToken();
if (isset($tokenData['error'])) {
throw new \RuntimeException('Google OAuth error: ' . ($tokenData['error_description'] ?? $tokenData['error']));
}
$token = $tokenData['access_token'];
$refreshToken = $tokenData['refresh_token'] ?? null;
$expiresAt = now()->addSeconds($tokenData['expires_in']);
$email = $this->getEmailFromToken($token);
$connection = GoogleOAuthConnection::updateOrCreate([
'user_id' => auth()->id(),
'service' => $service->value,
'email' => $email,
], [
'access_token' => $token,
'refresh_token' => $refreshToken,
'expires_at' => $expiresAt,
]);
return $connection;
}
public function getAccessToken(GoogleOAuthConnection $connection): string
{
$cacheKey = "google_oauth_token_{$connection->id}";
return Cache::remember($cacheKey, self::TOKEN_CACHE_TTL, function () use ($connection) {
if ($connection->isExpired()) {
$this->refreshToken($connection);
}
return $connection->access_token;
});
}
public function refreshToken(GoogleOAuthConnection $connection): void
{
if (!$connection->refresh_token) {
throw new \RuntimeException('Impossibile rinnovare: nessun refresh token');
}
$client = $this->buildClient();
$client->fetchAccessTokenWithRefreshToken($connection->refresh_token);
$tokenData = $client->getAccessToken();
if (isset($tokenData['error'])) {
$connection->delete();
Cache::forget("google_oauth_token_{$connection->id}");
throw new \RuntimeException(
'Refresh token scaduto o revocato per ' . $connection->email
);
}
$connection->access_token = $tokenData['access_token'];
$connection->expires_at = now()->addSeconds($tokenData['expires_in']);
if (isset($tokenData['refresh_token'])) {
$connection->refresh_token = $tokenData['refresh_token'];
}
$connection->save();
Cache::forget("google_oauth_token_{$connection->id}");
}
public function revoke(GoogleOAuthConnection $connection): void
{
$client = $this->buildClient();
$client->revokeToken($connection->access_token);
$connection->delete();
Cache::forget("google_oauth_token_{$connection->id}");
}
public function createXoAuth2SmtpTransport(
string $email,
string $accessToken
): \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport {
$transport = new \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport(
'smtp.gmail.com',
587,
true,
);
$transport->setUsername($email);
$transport->setPassword($accessToken);
$transport->setAuthenticators([
new \Symfony\Component\Mailer\Transport\Smtp\Auth\XOAuth2Authenticator(),
]);
return $transport;
}
private function getEmailFromToken(string $accessToken): string
{
$client = $this->buildClient();
$payload = $client->verifyIdToken($accessToken);
if ($payload && isset($payload['email'])) {
return $payload['email'];
}
$http = new \GuzzleHttp\Client();
$response = $http->get('https://www.googleapis.com/oauth2/v1/userinfo?alt=json', [
'headers' => ['Authorization' => "Bearer $accessToken"],
]);
$data = json_decode((string) $response->getBody(), true);
return $data['email'];
}
public function getConnectionStatus(): array
{
$connections = GoogleOAuthConnection::where('user_id', auth()->id())->get();
$status = [];
foreach (GoogleService::cases() as $service) {
$connection = $connections->firstWhere('service', $service->value);
$status[$service->value] = [
'connected' => $connection !== null,
'connection' => $connection,
'email' => $connection?->email,
'expired' => $connection?->isExpired() ?? false,
];
}
return $status;
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Traits;
use App\Models\Tag;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
trait HasTagsLight
{
public function tags(): MorphToMany
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function scopeWithAllTags($query, array $tags)
{
foreach ($tags as $tag) {
$query->whereHas('tags', fn ($q) => $q->where('slug', $tag));
}
return $query;
}
public function scopeWithAnyTags($query, array $tags)
{
$query->whereHas('tags', fn ($q) => $q->whereIn('slug', $tags));
return $query;
}
}
+3 -6
View File
@@ -19,12 +19,9 @@ return Application::configure(basePath: dirname(__DIR__))
'permission' => CheckPermission::class, 'permission' => CheckPermission::class,
]); ]);
$middleware->trustProxies(at: '*'); $middleware->trustProxies(at: '*');
$middleware->web(append: [ // Web middleware defaults are already applied by the framework.
\Illuminate\Cookie\Middleware\EncryptCookies::class, // Do NOT append EncryptCookies, StartSession, etc. here — they
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, // would run twice and corrupt session cookies, causing 419 errors.
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
]);
}) })
->withExceptions(function (Exceptions $exceptions): void { ->withExceptions(function (Exceptions $exceptions): void {
// //
+227 -21
View File
@@ -20,6 +20,223 @@ echo "==> Pulizia cache..."
rm -rf bootstrap/cache/*.php storage/framework/cache/data/* storage/framework/sessions/* storage/framework/views/* storage/logs/* 2>/dev/null || true rm -rf bootstrap/cache/*.php storage/framework/cache/data/* storage/framework/sessions/* storage/framework/views/* storage/logs/* 2>/dev/null || true
echo " OK" echo " OK"
echo "==> Generazione post-deploy.sh..."
cat > post-deploy.sh << 'POSTDEPLOY'
#!/bin/bash
# post-deploy.sh — Generated by build-dist.sh
# Usage: bash post-deploy.sh {check|upgrade|install} [--yes]
set -u
APP_DIR="$(cd "$(dirname "$0")" && pwd)"
ARTISAN="php ${APP_DIR}/artisan"
DIAGNOSE="php ${APP_DIR}/diagnose.php"
RED='\033[31m'; GREEN='\033[32m'; YELLOW='\033[33m'
CYAN='\033[36m'; BOLD='\033[1m'; NC='\033[0m'
MODE="${1:-check}"
BATCH=false; [ "${2:-}" = "--yes" ] && BATCH=true
echo -e "${CYAN}${BOLD}============================================${NC}"
echo -e "${CYAN}${BOLD} Glastree Post-Deploy — ${MODE}${NC}"
echo -e "${CYAN}${BOLD}============================================${NC}"
echo ""
IS_FRESH=false; [ ! -f "${APP_DIR}/.env" ] && IS_FRESH=true
log() { echo -e " ${BOLD}$1${NC}"; }
ok() { echo -e " ${GREEN}✅ $1${NC}"; }
warn() { echo -e " ${YELLOW}⚠ $1${NC}"; }
fail() { echo -e " ${RED}❌ $1${NC}"; }
check_artisan() {
if [ ! -f "${APP_DIR}/artisan" ]; then
fail "artisan non trovato. Esegui da root dell'applicazione."
exit 1
fi
}
run_step() {
local step="$1" desc="$2"; shift 2
log "${step} ${desc}"
if "$@" 2>&1 | sed 's/^/ /'; then
ok "${desc}"
return 0
else
local rc=$?
fail "${desc} — exit code ${rc}"
return ${rc}
fi
}
run_diagnose() {
if ${DIAGNOSE} 2>&1 | tail -5; then
ok "Diagnostica OK"
else
warn "Diagnostica ha rilevato problemi (non bloccante)"
fi
}
run_check() {
echo -e "${YELLOW}═══════════ CONTROLLO ═══════════${NC}" ; echo ""
echo -e "${BOLD}[0] Diagnostica sistema${NC}"
run_diagnose ; echo ""
echo -e "${BOLD}[1] Ambiente${NC}"
if $IS_FRESH; then warn ".env non trovato — fresh install necessario"
else ok ".env presente"
fi ; echo ""
echo -e "${BOLD}[2] Migration pendenti${NC}"
if [ -f "${APP_DIR}/artisan" ]; then
$ARTISAN migrate:status 2>&1 | grep -E '(Pending|Ran)' | head -5 || true
PENDING=$(php -r "echo trim(shell_exec('${ARTISAN} migrate:status 2>&1 | grep -c Pending || true'));" 2>/dev/null || echo "0")
if [ "${PENDING:-0}" -gt 0 ] 2>/dev/null; then
warn "${PENDING} migration da eseguire"
else
ok "Nessuna migration pendente"
fi
else
warn "artisan non disponibile — skip migration check"
fi ; echo ""
echo -e "${BOLD}[3] Storage & Permessi${NC}"
for d in storage/framework/cache/data storage/framework/sessions \
storage/framework/views storage/logs bootstrap/cache \
storage/app/public storage/app/backups; do
p="${APP_DIR}/${d}"
if [ -d "$p" ]; then
[ -w "$p" ] && ok "${d}" || warn "${d} — non scrivibile"
else
warn "${d} — mancante"
fi
done ; echo ""
echo -e "${BOLD}[4] Symlink storage${NC}"
if [ -L "${APP_DIR}/public/storage" ]; then
ok "public/storage → $(readlink "${APP_DIR}/public/storage")"
else
warn "public/storage non presente"
fi ; echo ""
echo -e "${BOLD}[5] Cache${NC}"
for d in bootstrap/cache storage/framework/cache/data \
storage/framework/sessions storage/framework/views; do
c=$(find "${APP_DIR}/${d}" -type f 2>/dev/null | wc -l)
[ "$c" -gt 0 ] && warn "${d}: ${c} file" || ok "${d}: vuoto"
done
echo "" ; echo -e "${YELLOW}═══════════ FINE CONTROLLO ═══════════${NC}"
}
run_upgrade() {
echo -e "${YELLOW}═══════════ UPGRADE ═══════════${NC}" ; check_artisan
run_step "[1/9]" "Directory storage" mkdir -p \
"${APP_DIR}/storage/framework/cache/data" \
"${APP_DIR}/storage/framework/sessions" \
"${APP_DIR}/storage/framework/views" \
"${APP_DIR}/storage/logs" "${APP_DIR}/bootstrap/cache" \
"${APP_DIR}/storage/app/public" \
"${APP_DIR}/storage/app/public/logos" \
"${APP_DIR}/storage/app/public/documenti/eventi" \
"${APP_DIR}/storage/app/backups" \
"${APP_DIR}/storage/app/documenti" \
"${APP_DIR}/storage/app/private/avatars/gruppi" || true
run_step "[2/9]" ".gitignore storage" bash -c \
"echo '*'>'${APP_DIR}/storage/app/public/.gitignore'; echo '!.gitignore'>>'${APP_DIR}/storage/app/public/.gitignore'" || true
run_step "[3/9]" "Permessi" chmod -R 775 \
"${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true
run_step "[4/9]" "Symlink storage" bash -c \
"rm -f '${APP_DIR}/public/storage'; ln -sf '../storage/app/public' '${APP_DIR}/public/storage'" || true
run_step "[5/10]" "Migrazioni DB" $ARTISAN migrate --force || true
run_step "[6/10]" "Seed diocesi" $ARTISAN db:seed --class=DiocesiSeeder --force || true
run_step "[7/10]" "Dump autoload" composer dump-autoload --no-dev --optimize || true
run_step "[8/10]" "Key generate" $ARTISAN key:generate --force || true
run_step "[9/10]" "Pulizia cache" bash -c \
"$ARTISAN config:clear && $ARTISAN route:clear && $ARTISAN view:clear" || true
run_step "[10/10]" "Verifica finale" $DIAGNOSE || true
echo "" ; echo -e "${GREEN}${BOLD}✅ Upgrade completato.${NC}"
}
run_install() {
echo -e "${YELLOW}═══════════ INSTALL (fresh) ═══════════${NC}" ; check_artisan
run_step "[1/10]" "Directory storage" mkdir -p \
"${APP_DIR}/storage/framework/cache/data" \
"${APP_DIR}/storage/framework/sessions" \
"${APP_DIR}/storage/framework/views" \
"${APP_DIR}/storage/logs" "${APP_DIR}/bootstrap/cache" \
"${APP_DIR}/storage/app/public" \
"${APP_DIR}/storage/app/public/logos" \
"${APP_DIR}/storage/app/public/documenti/eventi" \
"${APP_DIR}/storage/app/backups" \
"${APP_DIR}/storage/app/documenti" \
"${APP_DIR}/storage/app/private/avatars/gruppi" || true
run_step "[2/10]" ".gitignore storage" bash -c \
"echo '*'>'${APP_DIR}/storage/app/public/.gitignore'; echo '!.gitignore'>>'${APP_DIR}/storage/app/public/.gitignore'" || true
run_step "[3/10]" "Permessi" chmod -R 775 \
"${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true
run_step "[4/10]" "File .env" cp "${APP_DIR}/.env.example" "${APP_DIR}/.env" || true
warn "MODIFICA .env con i tuoi dati DB/URL prima di proseguire"
if ! $BATCH; then read -p " Fatto? (Invio per continuare) " _; fi
run_step "[5/10]" "Symlink storage" bash -c \
"rm -f '${APP_DIR}/public/storage'; ln -sf '../storage/app/public' '${APP_DIR}/public/storage'" || true
run_step "[6/10]" "Dump autoload" composer dump-autoload --no-dev --optimize || true
run_step "[7/10]" "Key generate" $ARTISAN key:generate --force || true
run_step "[8/11]" "Migrazioni DB" $ARTISAN migrate --force || true
run_step "[9/11]" "Seed diocesi" $ARTISAN db:seed --class=DiocesiSeeder --force || true
run_step "[10/11]" "Pulizia cache" bash -c \
"$ARTISAN config:clear && $ARTISAN route:clear && $ARTISAN view:clear" || true
run_step "[11/11]" "Verifica finale" $DIAGNOSE || true
echo "" ; echo -e "${GREEN}${BOLD}✅ Installazione completata.${NC}"
}
case "$MODE" in
check) run_check ;;
upgrade)
run_check ; echo ""
if $BATCH; then run_upgrade
else
read -p "Procedere con l'upgrade? (s/N) " c
[ "$c" = "s" ] || [ "$c" = "S" ] && run_upgrade || echo "Annullato."
fi
;;
install) run_install ;;
*)
echo "Uso: bash post-deploy.sh {check|upgrade|install} [--yes]"
echo " check — diagnostica (nessuna modifica)"
echo " upgrade — aggiorna installazione esistente"
echo " install — fresh install"
echo " --yes — batch (nessuna conferma)"
exit 1
;;
esac
POSTDEPLOY
chmod +x post-deploy.sh
echo " OK"
echo "==> Creazione archivio: ${ARCHIVE}" echo "==> Creazione archivio: ${ARCHIVE}"
tar czf "${ARCHIVE}" \ tar czf "${ARCHIVE}" \
@@ -60,6 +277,8 @@ tar czf "${ARCHIVE}" \
--warning=no-file-changed \ --warning=no-file-changed \
. .
rm -f post-deploy.sh
echo "" echo ""
echo "============================================" echo "============================================"
echo " Archivio creato: ${ARCHIVE}" echo " Archivio creato: ${ARCHIVE}"
@@ -70,27 +289,14 @@ echo "Per estrarre sul server di destinazione:"
echo " tar xzf ${ARCHIVE}" echo " tar xzf ${ARCHIVE}"
echo "" echo ""
echo "Poi esegui:" echo "Poi esegui:"
echo " # 1. Crea directory necessarie (se non esistono già)" echo " # Upgrade di un'installazione esistente:"
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 " bash post-deploy.sh upgrade"
echo "" echo ""
echo " # 2. Crea file .gitignore in storage/app/public (serve a Laravel)" echo " # Fresh install:"
echo ' echo "*" > storage/app/public/.gitignore' echo " bash post-deploy.sh install"
echo ' echo "!.gitignore" >> storage/app/public/.gitignore'
echo "" echo ""
echo " # 3. Permessi (web server = www-data)" echo " # Solo controllo (nessuna modifica):"
echo ' chmod -R 775 storage bootstrap/cache' echo " bash post-deploy.sh check"
echo ' chown -R www-data:www-data storage bootstrap/cache'
echo "" echo ""
echo " # 4. Configura ambiente" echo " # Batch mode (nessuna conferma):"
echo " cp .env.example .env # modifica DB, APP_URL, etc." echo " bash post-deploy.sh upgrade --yes"
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"
+6
View File
@@ -35,4 +35,10 @@ return [
], ],
], ],
'google' => [
'client_id' => env('GOOGLE_CLIENT_ID'),
'client_secret' => env('GOOGLE_CLIENT_SECRET'),
'redirect' => env('GOOGLE_REDIRECT_URI', config('app.url') . '/auth/google/callback'),
],
]; ];
@@ -8,6 +8,10 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (Schema::hasTable('tenants')) {
return;
}
Schema::create('tenants', function (Blueprint $table) { Schema::create('tenants', function (Blueprint $table) {
$table->id(); $table->id();
$table->string('nome'); $table->string('nome');
@@ -8,13 +8,16 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasTable('diocesi')) {
Schema::create('diocesi', function (Blueprint $table) { Schema::create('diocesi', function (Blueprint $table) {
$table->id(); $table->id();
$table->string('nome'); $table->string('nome');
$table->string('regione')->nullable(); $table->string('regione')->nullable();
$table->timestamps(); $table->timestamps();
}); });
}
if (!Schema::hasTable('comuni')) {
Schema::create('comuni', function (Blueprint $table) { Schema::create('comuni', function (Blueprint $table) {
$table->id(); $table->id();
$table->string('nome'); $table->string('nome');
@@ -29,6 +32,7 @@ return new class extends Migration
$table->index('sigla_provincia'); $table->index('sigla_provincia');
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -8,6 +8,7 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasTable('individui')) {
Schema::create('individui', function (Blueprint $table) { Schema::create('individui', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null'); $table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
@@ -30,6 +31,7 @@ return new class extends Migration
$table->index('cognome'); $table->index('cognome');
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -8,6 +8,7 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasTable('contatti')) {
Schema::create('contatti', function (Blueprint $table) { Schema::create('contatti', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('individuo_id')->constrained('individui')->onDelete('cascade'); $table->foreignId('individuo_id')->constrained('individui')->onDelete('cascade');
@@ -19,6 +20,7 @@ return new class extends Migration
$table->index('individuo_id'); $table->index('individuo_id');
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -8,6 +8,7 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasTable('gruppi')) {
Schema::create('gruppi', function (Blueprint $table) { Schema::create('gruppi', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null'); $table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
@@ -27,6 +28,7 @@ return new class extends Migration
$table->index('parent_id'); $table->index('parent_id');
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -8,6 +8,7 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasTable('gruppo_individuo')) {
Schema::create('gruppo_individuo', function (Blueprint $table) { Schema::create('gruppo_individuo', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('gruppo_id')->constrained('gruppi')->onDelete('cascade'); $table->foreignId('gruppo_id')->constrained('gruppi')->onDelete('cascade');
@@ -18,6 +19,7 @@ return new class extends Migration
$table->unique(['gruppo_id', 'individuo_id']); $table->unique(['gruppo_id', 'individuo_id']);
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -8,6 +8,7 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasTable('eventi')) {
Schema::create('eventi', function (Blueprint $table) { Schema::create('eventi', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null'); $table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
@@ -23,14 +24,18 @@ return new class extends Migration
$table->timestamps(); $table->timestamps();
$table->index('tenant_id'); $table->index('tenant_id');
}); });
}
if (!Schema::hasTable('eventi_gruppi')) {
Schema::create('eventi_gruppi', function (Blueprint $table) { Schema::create('eventi_gruppi', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('evento_id')->constrained('eventi')->onDelete('cascade'); $table->foreignId('evento_id')->constrained('eventi')->onDelete('cascade');
$table->foreignId('gruppo_id')->constrained('gruppi')->onDelete('cascade'); $table->foreignId('gruppo_id')->constrained('gruppi')->onDelete('cascade');
$table->timestamps(); $table->timestamps();
}); });
}
if (!Schema::hasTable('eventi_responsabili')) {
Schema::create('eventi_responsabili', function (Blueprint $table) { Schema::create('eventi_responsabili', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('evento_id')->constrained('eventi')->onDelete('cascade'); $table->foreignId('evento_id')->constrained('eventi')->onDelete('cascade');
@@ -38,6 +43,7 @@ return new class extends Migration
$table->timestamps(); $table->timestamps();
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -8,14 +8,15 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasTable('documenti')) {
Schema::create('documenti', function (Blueprint $table) { Schema::create('documenti', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null'); $table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
$table->string('nome_file'); $table->string('nome_file');
$table->string('file_path'); $table->string('file_path')->nullable();
$table->string('tipo')->nullable(); $table->string('tipo')->nullable();
$table->enum('tipologia', ['avatar', 'galleria', 'documento', 'statuto', 'altro'])->default('documento'); $table->string('tipologia', 50)->default('documento');
$table->enum('visibilita', ['pubblico', 'gruppo', 'individuo', 'associazione', 'federazione'])->default('gruppo'); $table->string('visibilita', 50)->default('gruppo');
$table->unsignedBigInteger('visibilita_target_id')->nullable(); $table->unsignedBigInteger('visibilita_target_id')->nullable();
$table->string('visibilita_target_type')->nullable(); $table->string('visibilita_target_type')->nullable();
$table->string('mime_type')->nullable(); $table->string('mime_type')->nullable();
@@ -25,6 +26,7 @@ return new class extends Migration
$table->index('tenant_id'); $table->index('tenant_id');
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -8,6 +8,7 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasTable('mailing_lists')) {
Schema::create('mailing_lists', function (Blueprint $table) { Schema::create('mailing_lists', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null'); $table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
@@ -17,7 +18,9 @@ return new class extends Migration
$table->timestamps(); $table->timestamps();
$table->index('tenant_id'); $table->index('tenant_id');
}); });
}
if (!Schema::hasTable('mailing_contacts')) {
Schema::create('mailing_contacts', function (Blueprint $table) { Schema::create('mailing_contacts', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('mailing_list_id')->constrained('mailing_lists')->onDelete('cascade'); $table->foreignId('mailing_list_id')->constrained('mailing_lists')->onDelete('cascade');
@@ -28,7 +31,9 @@ return new class extends Migration
$table->timestamps(); $table->timestamps();
$table->unique(['mailing_list_id', 'individuo_id']); $table->unique(['mailing_list_id', 'individuo_id']);
}); });
}
if (!Schema::hasTable('mailing_messaggi')) {
Schema::create('mailing_messaggi', function (Blueprint $table) { Schema::create('mailing_messaggi', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null'); $table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
@@ -46,6 +51,7 @@ return new class extends Migration
$table->index('tenant_id'); $table->index('tenant_id');
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -8,6 +8,7 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasTable('notifiche')) {
Schema::create('notifiche', function (Blueprint $table) { Schema::create('notifiche', function (Blueprint $table) {
$table->id(); $table->id();
$table->unsignedBigInteger('user_id')->nullable(); $table->unsignedBigInteger('user_id')->nullable();
@@ -21,6 +22,7 @@ return new class extends Migration
$table->index('user_id'); $table->index('user_id');
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -8,6 +8,7 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasTable('users')) {
Schema::create('users', function (Blueprint $table) { Schema::create('users', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null'); $table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
@@ -21,13 +22,17 @@ return new class extends Migration
$table->index('tenant_id'); $table->index('tenant_id');
$table->index('email'); $table->index('email');
}); });
}
if (!Schema::hasTable('password_reset_tokens')) {
Schema::create('password_reset_tokens', function (Blueprint $table) { Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary(); $table->string('email')->primary();
$table->string('token'); $table->string('token');
$table->timestamp('created_at')->nullable(); $table->timestamp('created_at')->nullable();
}); });
}
if (!Schema::hasTable('sessions')) {
Schema::create('sessions', function (Blueprint $table) { Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary(); $table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index(); $table->foreignId('user_id')->nullable()->index();
@@ -37,6 +42,7 @@ return new class extends Migration
$table->integer('last_activity')->index(); $table->integer('last_activity')->index();
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -8,18 +8,23 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasTable('cache')) {
Schema::create('cache', function (Blueprint $table) { Schema::create('cache', function (Blueprint $table) {
$table->string('key')->primary(); $table->string('key')->primary();
$table->mediumText('value'); $table->mediumText('value');
$table->integer('expiration'); $table->integer('expiration');
}); });
}
if (!Schema::hasTable('cache_locks')) {
Schema::create('cache_locks', function (Blueprint $table) { Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary(); $table->string('key')->primary();
$table->string('owner'); $table->string('owner');
$table->integer('expiration'); $table->integer('expiration');
}); });
}
if (!Schema::hasTable('jobs')) {
Schema::create('jobs', function (Blueprint $table) { Schema::create('jobs', function (Blueprint $table) {
$table->id(); $table->id();
$table->string('queue')->index(); $table->string('queue')->index();
@@ -29,7 +34,9 @@ return new class extends Migration
$table->unsignedInteger('available_at'); $table->unsignedInteger('available_at');
$table->unsignedInteger('created_at'); $table->unsignedInteger('created_at');
}); });
}
if (!Schema::hasTable('job_batches')) {
Schema::create('job_batches', function (Blueprint $table) { Schema::create('job_batches', function (Blueprint $table) {
$table->string('id')->primary(); $table->string('id')->primary();
$table->string('name'); $table->string('name');
@@ -42,7 +49,9 @@ return new class extends Migration
$table->integer('created_at'); $table->integer('created_at');
$table->integer('finished_at')->nullable(); $table->integer('finished_at')->nullable();
}); });
}
if (!Schema::hasTable('failed_jobs')) {
Schema::create('failed_jobs', function (Blueprint $table) { Schema::create('failed_jobs', function (Blueprint $table) {
$table->id(); $table->id();
$table->string('uuid')->unique(); $table->string('uuid')->unique();
@@ -53,6 +62,7 @@ return new class extends Migration
$table->timestamp('failed_at'); $table->timestamp('failed_at');
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -8,10 +8,12 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasColumn('individui', 'numero_documento')) {
Schema::table('individui', function (Blueprint $table) { Schema::table('individui', function (Blueprint $table) {
$table->string('numero_documento', 50)->nullable()->after('tipo_documento'); $table->string('numero_documento', 50)->nullable()->after('tipo_documento');
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -8,12 +8,14 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasColumn('eventi', 'giorno_mese')) {
Schema::table('eventi', function (Blueprint $table) { Schema::table('eventi', function (Blueprint $table) {
$table->string('giorno_mese')->nullable()->after('giorno_settimana'); $table->string('giorno_mese')->nullable()->after('giorno_settimana');
$table->string('mesi_recorrenza')->nullable()->after('giorno_mese'); $table->string('mesi_recorrenza')->nullable()->after('giorno_mese');
$table->string('mese_annuale')->nullable()->after('mesi_recorrenza'); $table->string('mese_annuale')->nullable()->after('mesi_recorrenza');
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -8,10 +8,12 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasColumn('eventi', 'occorrenza_mese')) {
Schema::table('eventi', function (Blueprint $table) { Schema::table('eventi', function (Blueprint $table) {
$table->unsignedTinyInteger('occorrenza_mese')->nullable()->after('giorno_mese'); $table->unsignedTinyInteger('occorrenza_mese')->nullable()->after('giorno_mese');
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -8,6 +8,7 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasTable('eventi_documenti')) {
Schema::create('eventi_documenti', function (Blueprint $table) { Schema::create('eventi_documenti', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('evento_id')->constrained('eventi')->onDelete('cascade'); $table->foreignId('evento_id')->constrained('eventi')->onDelete('cascade');
@@ -15,6 +16,7 @@ return new class extends Migration
$table->timestamps(); $table->timestamps();
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -8,10 +8,12 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasColumn('documenti', 'user_id')) {
Schema::table('documenti', function (Blueprint $table) { Schema::table('documenti', function (Blueprint $table) {
$table->foreignId('user_id')->nullable()->constrained('users')->onDelete('set null')->after('tenant_id'); $table->foreignId('user_id')->nullable()->constrained('users')->onDelete('set null')->after('tenant_id');
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -8,6 +8,7 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasTable('viste_report')) {
Schema::create('viste_report', function (Blueprint $table) { Schema::create('viste_report', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('user_id')->constrained('users')->onDelete('cascade'); $table->foreignId('user_id')->constrained('users')->onDelete('cascade');
@@ -22,6 +23,7 @@ return new class extends Migration
$table->index('user_id'); $table->index('user_id');
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -8,11 +8,13 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasColumn('viste_report', 'is_default')) {
Schema::table('viste_report', function (Blueprint $table) { Schema::table('viste_report', function (Blueprint $table) {
$table->boolean('is_default')->default(false)->after('nome'); $table->boolean('is_default')->default(false)->after('nome');
$table->index('is_default'); $table->index('is_default');
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -8,11 +8,13 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasColumn('mailing_lists', 'user_id')) {
Schema::table('mailing_lists', function (Blueprint $table) { Schema::table('mailing_lists', function (Blueprint $table) {
$table->foreignId('user_id')->nullable()->constrained('users')->onDelete('set null')->after('tenant_id'); $table->foreignId('user_id')->nullable()->constrained('users')->onDelete('set null')->after('tenant_id');
$table->index('user_id'); $table->index('user_id');
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -8,6 +8,7 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasTable('tipologie_documenti')) {
Schema::create('tipologie_documenti', function (Blueprint $table) { Schema::create('tipologie_documenti', function (Blueprint $table) {
$table->id(); $table->id();
$table->string('nome', 50)->unique(); $table->string('nome', 50)->unique();
@@ -29,6 +30,7 @@ return new class extends Migration
]); ]);
} }
} }
}
public function down(): void public function down(): void
{ {
@@ -8,10 +8,13 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasColumn('users', 'permissions')) {
Schema::table('users', function (Blueprint $table) { Schema::table('users', function (Blueprint $table) {
$table->json('permissions')->nullable()->after('is_admin'); $table->json('permissions')->nullable()->after('is_admin');
}); });
}
if (!Schema::hasTable('activity_logs')) {
Schema::create('activity_logs', function (Blueprint $table) { Schema::create('activity_logs', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade'); $table->foreignId('user_id')->constrained()->onDelete('cascade');
@@ -26,7 +29,9 @@ return new class extends Migration
$table->index(['created_at']); $table->index(['created_at']);
$table->index(['action']); $table->index(['action']);
}); });
}
if (!Schema::hasTable('role_presets')) {
Schema::create('role_presets', function (Blueprint $table) { Schema::create('role_presets', function (Blueprint $table) {
$table->id(); $table->id();
$table->string('name', 50)->unique(); $table->string('name', 50)->unique();
@@ -35,6 +40,7 @@ return new class extends Migration
$table->timestamps(); $table->timestamps();
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -23,6 +23,7 @@ return new class extends Migration
/** /**
* See `docs/prerequisites.md` for suggested lengths on 'name' and 'guard_name' if "1071 Specified key was too long" errors are encountered. * See `docs/prerequisites.md` for suggested lengths on 'name' and 'guard_name' if "1071 Specified key was too long" errors are encountered.
*/ */
if (!Schema::hasTable($tableNames['permissions'])) {
Schema::create($tableNames['permissions'], static function (Blueprint $table) { Schema::create($tableNames['permissions'], static function (Blueprint $table) {
$table->id(); // permission id $table->id(); // permission id
$table->string('name'); $table->string('name');
@@ -31,10 +32,12 @@ return new class extends Migration
$table->unique(['name', 'guard_name']); $table->unique(['name', 'guard_name']);
}); });
}
/** /**
* See `docs/prerequisites.md` for suggested lengths on 'name' and 'guard_name' if "1071 Specified key was too long" errors are encountered. * See `docs/prerequisites.md` for suggested lengths on 'name' and 'guard_name' if "1071 Specified key was too long" errors are encountered.
*/ */
if (!Schema::hasTable($tableNames['roles'])) {
Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) { Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
$table->id(); // role id $table->id(); // role id
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
@@ -50,7 +53,9 @@ return new class extends Migration
$table->unique(['name', 'guard_name']); $table->unique(['name', 'guard_name']);
} }
}); });
}
if (!Schema::hasTable($tableNames['model_has_permissions'])) {
Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) { Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
$table->unsignedBigInteger($pivotPermission); $table->unsignedBigInteger($pivotPermission);
@@ -73,7 +78,9 @@ return new class extends Migration
'model_has_permissions_permission_model_type_primary'); 'model_has_permissions_permission_model_type_primary');
} }
}); });
}
if (!Schema::hasTable($tableNames['model_has_roles'])) {
Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) { Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
$table->unsignedBigInteger($pivotRole); $table->unsignedBigInteger($pivotRole);
@@ -96,7 +103,9 @@ return new class extends Migration
'model_has_roles_role_model_type_primary'); 'model_has_roles_role_model_type_primary');
} }
}); });
}
if (!Schema::hasTable($tableNames['role_has_permissions'])) {
Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) { Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
$table->unsignedBigInteger($pivotPermission); $table->unsignedBigInteger($pivotPermission);
$table->unsignedBigInteger($pivotRole); $table->unsignedBigInteger($pivotRole);
@@ -113,6 +122,7 @@ return new class extends Migration
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary'); $table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
}); });
}
app('cache') app('cache')
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null) ->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
@@ -8,6 +8,7 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasTable('email_settings')) {
Schema::create('email_settings', function (Blueprint $table) { Schema::create('email_settings', function (Blueprint $table) {
$table->id(); $table->id();
$table->string('imap_host')->nullable(); $table->string('imap_host')->nullable();
@@ -29,6 +30,7 @@ return new class extends Migration
$table->timestamps(); $table->timestamps();
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -8,6 +8,7 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasTable('email_folders')) {
Schema::create('email_folders', function (Blueprint $table) { Schema::create('email_folders', function (Blueprint $table) {
$table->id(); $table->id();
$table->string('name'); $table->string('name');
@@ -20,6 +21,7 @@ return new class extends Migration
$table->timestamps(); $table->timestamps();
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -8,6 +8,7 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasTable('email_messages')) {
Schema::create('email_messages', function (Blueprint $table) { Schema::create('email_messages', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('email_folder_id')->constrained('email_folders')->onDelete('cascade'); $table->foreignId('email_folder_id')->constrained('email_folders')->onDelete('cascade');
@@ -38,6 +39,7 @@ return new class extends Migration
$table->index('received_at'); $table->index('received_at');
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -1,22 +1,19 @@
<?php <?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
Schema::table('documenti', function (Blueprint $table) { // Column type changed to string in 2024_01_01_000008_create_documenti_table.php.
$table->enum('tipologia', ['avatar', 'galleria', 'documento', 'statuto', 'altro', 'email_attachment'])->default('documento')->change(); // Application-level validation handles allowed values.
});
} }
public function down(): void public function down(): void
{ {
Schema::table('documenti', function (Blueprint $table) { // No-op: column is string, not enum.
$table->enum('tipologia', ['avatar', 'galleria', 'documento', 'statuto', 'altro'])->default('documento')->change();
});
} }
}; };
@@ -7,14 +7,15 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
DB::table('tipologie_documenti')->insert([ DB::table('tipologie_documenti')->updateOrInsert(
'nome' => 'email_attachment', ['nome' => 'email_attachment'],
[
'descrizione' => 'Allegato email', 'descrizione' => 'Allegato email',
'ordine' => 100, 'ordine' => 100,
'attiva' => true, 'attiva' => true,
'created_at' => now(),
'updated_at' => now(), 'updated_at' => now(),
]); ]
);
} }
public function down(): void public function down(): void
@@ -8,6 +8,7 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasTable('email_attachments')) {
Schema::create('email_attachments', function (Blueprint $table) { Schema::create('email_attachments', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('email_message_id')->constrained('email_messages')->onDelete('cascade'); $table->foreignId('email_message_id')->constrained('email_messages')->onDelete('cascade');
@@ -20,6 +21,7 @@ return new class extends Migration
$table->timestamps(); $table->timestamps();
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -8,6 +8,7 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasTable('ruoli')) {
Schema::create('ruoli', function (Blueprint $table) { Schema::create('ruoli', function (Blueprint $table) {
$table->id(); $table->id();
$table->string('nome', 50)->unique(); $table->string('nome', 50)->unique();
@@ -37,7 +38,9 @@ return new class extends Migration
'updated_at' => now(), 'updated_at' => now(),
]); ]);
} }
}
if (!Schema::hasColumn('gruppo_individuo', 'ruolo_id')) {
Schema::table('gruppo_individuo', function (Blueprint $table) { Schema::table('gruppo_individuo', function (Blueprint $table) {
$table->unsignedBigInteger('ruolo_id')->nullable()->after('ruolo_nel_gruppo'); $table->unsignedBigInteger('ruolo_id')->nullable()->after('ruolo_nel_gruppo');
$table->foreign('ruolo_id')->references('id')->on('ruoli')->onDelete('set null'); $table->foreign('ruolo_id')->references('id')->on('ruoli')->onDelete('set null');
@@ -60,6 +63,7 @@ return new class extends Migration
} }
} }
} }
}
public function down(): void public function down(): void
{ {
@@ -8,15 +8,20 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (Schema::hasColumn('gruppo_individuo', 'ruolo_id')) {
Schema::table('gruppo_individuo', function (Blueprint $table) { Schema::table('gruppo_individuo', function (Blueprint $table) {
$table->dropIndex(['ruolo_id']);
$table->dropForeign(['ruolo_id']); $table->dropForeign(['ruolo_id']);
$table->dropColumn('ruolo_id'); $table->dropColumn('ruolo_id');
}); });
}
if (!Schema::hasColumn('gruppo_individuo', 'ruolo_ids')) {
Schema::table('gruppo_individuo', function (Blueprint $table) { Schema::table('gruppo_individuo', function (Blueprint $table) {
$table->json('ruolo_ids')->nullable()->after('individuo_id'); $table->json('ruolo_ids')->nullable()->after('individuo_id');
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -24,9 +29,11 @@ return new class extends Migration
$table->dropColumn('ruolo_ids'); $table->dropColumn('ruolo_ids');
}); });
if (!Schema::hasColumn('gruppo_individuo', 'ruolo_id')) {
Schema::table('gruppo_individuo', function (Blueprint $table) { Schema::table('gruppo_individuo', function (Blueprint $table) {
$table->unsignedBigInteger('ruolo_id')->nullable()->after('individuo_id'); $table->unsignedBigInteger('ruolo_id')->nullable()->after('individuo_id');
$table->foreign('ruolo_id')->references('id')->on('ruoli')->onDelete('set null'); $table->foreign('ruolo_id')->references('id')->on('ruoli')->onDelete('set null');
}); });
} }
}
}; };
@@ -13,10 +13,12 @@ return new class extends Migration
$table->dropColumn('responsabile_id'); $table->dropColumn('responsabile_id');
}); });
if (!Schema::hasColumn('gruppi', 'responsabile_ids')) {
Schema::table('gruppi', function (Blueprint $table) { Schema::table('gruppi', function (Blueprint $table) {
$table->json('responsabile_ids')->nullable()->after('diocesi_id'); $table->json('responsabile_ids')->nullable()->after('diocesi_id');
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -24,9 +26,11 @@ return new class extends Migration
$table->dropColumn('responsabile_ids'); $table->dropColumn('responsabile_ids');
}); });
if (!Schema::hasColumn('gruppi', 'responsabile_id')) {
Schema::table('gruppi', function (Blueprint $table) { Schema::table('gruppi', function (Blueprint $table) {
$table->unsignedBigInteger('responsabile_id')->nullable()->after('diocesi_id'); $table->unsignedBigInteger('responsabile_id')->nullable()->after('diocesi_id');
$table->foreign('responsabile_id')->references('id')->on('individui')->onDelete('set null'); $table->foreign('responsabile_id')->references('id')->on('individui')->onDelete('set null');
}); });
} }
}
}; };
@@ -8,11 +8,13 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasColumn('eventi', 'luogo_indirizzo')) {
Schema::table('eventi', function (Blueprint $table) { Schema::table('eventi', function (Blueprint $table) {
$table->string('luogo_indirizzo', 500)->nullable()->after('note'); $table->string('luogo_indirizzo', 500)->nullable()->after('note');
$table->text('luogo_url_maps')->nullable()->after('luogo_indirizzo'); $table->text('luogo_url_maps')->nullable()->after('luogo_indirizzo');
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -1,17 +1,19 @@
<?php <?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
DB::statement("ALTER TABLE documenti MODIFY COLUMN tipologia ENUM('avatar', 'galleria', 'documento', 'statuto', 'altro', 'programma', 'locandina') DEFAULT 'documento'"); // Column type changed to string in 2024_01_01_000008_create_documenti_table.php.
// Application-level validation handles allowed values.
} }
public function down(): void public function down(): void
{ {
DB::statement("ALTER TABLE documenti MODIFY COLUMN tipologia ENUM('avatar', 'galleria', 'documento', 'statuto', 'altro') DEFAULT 'documento'"); // No-op: column is string, not enum.
} }
}; };
@@ -8,10 +8,12 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasColumn('email_settings', 'signature')) {
Schema::table('email_settings', function (Blueprint $table) { Schema::table('email_settings', function (Blueprint $table) {
$table->longText('signature')->nullable()->after('reply_to'); $table->longText('signature')->nullable()->after('reply_to');
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -8,10 +8,12 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasColumn('email_settings', 'signature_enabled')) {
Schema::table('email_settings', function (Blueprint $table) { Schema::table('email_settings', function (Blueprint $table) {
$table->boolean('signature_enabled')->default(true)->after('signature'); $table->boolean('signature_enabled')->default(true)->after('signature');
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -8,6 +8,7 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasTable('report_custom')) {
Schema::create('report_custom', function (Blueprint $table) { Schema::create('report_custom', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('user_id')->constrained('users')->onDelete('cascade'); $table->foreignId('user_id')->constrained('users')->onDelete('cascade');
@@ -18,6 +19,7 @@ return new class extends Migration
$table->timestamps(); $table->timestamps();
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -10,6 +10,7 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasTable('tipologie_eventi')) {
Schema::create('tipologie_eventi', function (Blueprint $table) { Schema::create('tipologie_eventi', function (Blueprint $table) {
$table->id(); $table->id();
$table->string('nome', 50)->unique(); $table->string('nome', 50)->unique();
@@ -30,6 +31,7 @@ return new class extends Migration
['nome' => 'altro', 'descrizione' => 'Altro', 'ordine' => 7, 'attiva' => true], ['nome' => 'altro', 'descrizione' => 'Altro', 'ordine' => 7, 'attiva' => true],
]); ]);
} }
}
public function down(): void public function down(): void
{ {
@@ -10,6 +10,7 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasTable('sender_accounts')) {
Schema::create('sender_accounts', function (Blueprint $table) { Schema::create('sender_accounts', function (Blueprint $table) {
$table->id(); $table->id();
$table->string('email_address')->unique(); $table->string('email_address')->unique();
@@ -26,6 +27,7 @@ return new class extends Migration
$table->timestamps(); $table->timestamps();
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -10,12 +10,14 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasColumn('mailing_messaggi', 'log_falliti')) {
Schema::table('mailing_messaggi', function (Blueprint $table) { Schema::table('mailing_messaggi', function (Blueprint $table) {
$table->json('log_falliti')->nullable()->after('invii_falliti'); $table->json('log_falliti')->nullable()->after('invii_falliti');
$table->string('mittente_nome')->nullable()->after('log_falliti'); $table->string('mittente_nome')->nullable()->after('log_falliti');
$table->string('mittente_email')->nullable()->after('mittente_nome'); $table->string('mittente_email')->nullable()->after('mittente_nome');
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -8,6 +8,7 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasTable('documenti_cartelle')) {
Schema::create('documenti_cartelle', function (Blueprint $table) { Schema::create('documenti_cartelle', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('parent_id')->nullable()->constrained('documenti_cartelle')->onDelete('cascade'); $table->foreignId('parent_id')->nullable()->constrained('documenti_cartelle')->onDelete('cascade');
@@ -15,6 +16,7 @@ return new class extends Migration
$table->timestamps(); $table->timestamps();
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -8,10 +8,12 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasColumn('documenti', 'cartella_id')) {
Schema::table('documenti', function (Blueprint $table) { Schema::table('documenti', function (Blueprint $table) {
$table->foreignId('cartella_id')->nullable()->constrained('documenti_cartelle')->onDelete('set null'); $table->foreignId('cartella_id')->nullable()->constrained('documenti_cartelle')->onDelete('set null');
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -8,6 +8,7 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasTable('storage_repositories')) {
Schema::create('storage_repositories', function (Blueprint $table) { Schema::create('storage_repositories', function (Blueprint $table) {
$table->id(); $table->id();
$table->string('nome'); $table->string('nome');
@@ -18,6 +19,7 @@ return new class extends Migration
$table->timestamps(); $table->timestamps();
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -8,6 +8,7 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasColumn('documenti', 'repository_id')) {
Schema::table('documenti', function (Blueprint $table) { Schema::table('documenti', function (Blueprint $table) {
$table->foreignId('repository_id') $table->foreignId('repository_id')
->nullable() ->nullable()
@@ -16,6 +17,7 @@ return new class extends Migration
->after('cartella_id'); ->after('cartella_id');
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -10,10 +10,12 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasColumn('documenti', 'storage_disk')) {
Schema::table('documenti', function (Blueprint $table) { Schema::table('documenti', function (Blueprint $table) {
$table->string('storage_disk', 50)->nullable()->after('repository_id'); $table->string('storage_disk', 50)->nullable()->after('repository_id');
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -10,6 +10,7 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasTable('calendario_connessioni')) {
Schema::create('calendario_connessioni', function (Blueprint $table) { Schema::create('calendario_connessioni', function (Blueprint $table) {
$table->id(); $table->id();
$table->string('nome'); $table->string('nome');
@@ -26,6 +27,7 @@ return new class extends Migration
$table->timestamps(); $table->timestamps();
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -10,11 +10,13 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasColumn('eventi', 'uid_esterno')) {
Schema::table('eventi', function (Blueprint $table) { Schema::table('eventi', function (Blueprint $table) {
$table->string('uid_esterno', 500)->nullable()->after('luogo_url_maps'); $table->string('uid_esterno', 500)->nullable()->after('luogo_url_maps');
$table->index('uid_esterno'); $table->index('uid_esterno');
}); });
} }
}
public function down(): void public function down(): void
{ {
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (!Schema::hasColumn('gruppo_individuo', 'ruolo_nel_gruppo')) {
Schema::table('gruppo_individuo', function (Blueprint $table) {
$table->string('ruolo_nel_gruppo')->nullable()->after('ruolo_ids');
});
}
}
public function down(): void
{
}
};
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (!Schema::hasTable('tags')) {
Schema::create('tags', function (Blueprint $table) {
$table->id();
$table->string('name', 100);
$table->string('slug', 100)->unique();
$table->string('color', 7)->nullable();
$table->integer('order_column')->default(0);
$table->timestamps();
});
}
if (!Schema::hasTable('taggables')) {
Schema::create('taggables', function (Blueprint $table) {
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
$table->morphs('taggable');
$table->primary(['tag_id', 'taggable_id', 'taggable_type']);
});
}
}
public function down(): void
{
Schema::dropIfExists('taggables');
Schema::dropIfExists('tags');
}
};
@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('viste_report', function (Blueprint $table) {
if (!Schema::hasColumn('viste_report', 'colonne_larghezze')) {
$table->json('colonne_larghezze')->nullable()->after('colonne_visibili');
}
if (!Schema::hasColumn('viste_report', 'colonne_ordine')) {
$table->json('colonne_ordine')->nullable()->after('colonne_larghezze');
}
});
}
public function down(): void
{
Schema::table('viste_report', function (Blueprint $table) {
if (Schema::hasColumn('viste_report', 'colonne_larghezze')) {
$table->dropColumn('colonne_larghezze');
}
if (Schema::hasColumn('viste_report', 'colonne_ordine')) {
$table->dropColumn('colonne_ordine');
}
});
}
};
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (Schema::hasTable('google_oauth_connections')) {
return;
}
Schema::create('google_oauth_connections', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('service'); // email, drive, calendar
$table->string('email');
$table->text('access_token');
$table->text('refresh_token')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamps();
$table->unique(['user_id', 'service', 'email']);
});
}
public function down(): void
{
Schema::dropIfExists('google_oauth_connections');
}
};
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (!Schema::hasColumn('email_settings', 'auth_method')) {
Schema::table('email_settings', function (Blueprint $table) {
$table->string('auth_method', 20)->default('password');
$table->foreignId('google_oauth_connection_id')
->nullable()
->constrained('google_oauth_connections')
->nullOnDelete();
});
}
if (!Schema::hasColumn('sender_accounts', 'auth_method')) {
Schema::table('sender_accounts', function (Blueprint $table) {
$table->string('auth_method', 20)->default('password');
$table->foreignId('google_oauth_connection_id')
->nullable()
->constrained('google_oauth_connections')
->nullOnDelete();
});
}
}
public function down(): void
{
Schema::table('email_settings', function (Blueprint $table) {
if (Schema::hasColumn('email_settings', 'google_oauth_connection_id')) {
$table->dropForeign(['google_oauth_connection_id']);
}
if (Schema::hasColumn('email_settings', 'auth_method')) {
$table->dropColumn(['auth_method', 'google_oauth_connection_id']);
}
});
Schema::table('sender_accounts', function (Blueprint $table) {
if (Schema::hasColumn('sender_accounts', 'google_oauth_connection_id')) {
$table->dropForeign(['google_oauth_connection_id']);
}
if (Schema::hasColumn('sender_accounts', 'auth_method')) {
$table->dropColumn(['auth_method', 'google_oauth_connection_id']);
}
});
}
};

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