netgescon-day0/app/Filament/Pages/Impostazioni/SchedaAmministratore.php

873 lines
44 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Filament\Pages\Impostazioni;
use App\Models\Amministratore;
use App\Models\Stabile;
use App\Models\User;
use App\Support\StabileContext;
use BackedEnum;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Schema;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use Symfony\Component\Process\Process;
use UnitEnum;
class SchedaAmministratore extends Page implements HasForms
{
use InteractsWithForms;
protected static ?string $navigationLabel = 'Scheda amministratore';
protected static ?string $title = 'Scheda amministratore';
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-building-office-2';
protected static UnitEnum|string|null $navigationGroup = 'Impostazioni';
protected static ?int $navigationSort = 10;
protected static ?string $slug = 'impostazioni/scheda-amministratore';
protected string $view = 'filament.pages.impostazioni.scheda-amministratore';
public ?array $data = [];
public ?string $opsLastOutput = null;
public ?string $lastUpdatePackagePath = null;
public ?string $lastUpdatePackageName = null;
public Amministratore $amministratore;
public static function canAccess(): bool
{
$user = Auth::user();
return $user instanceof User
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore']);
}
public function mount(): void
{
$user = Auth::user();
if (! $user instanceof User) {
abort(403);
}
if (! $user->hasAnyRole(['super-admin', 'admin', 'amministratore'])) {
abort(403);
}
$amministratore = $user->amministratore;
if (! $amministratore instanceof Amministratore) {
// Per admin/super-admin, se l'utente non è "amministratore" diretto,
// usiamo l'amministratore dello stabile attivo (così si configurano i servizi per lo stabile selezionato).
if ($user->hasAnyRole(['super-admin', 'admin'])) {
$stabile = StabileContext::getActiveStabile($user);
if ($stabile instanceof Stabile && $stabile->amministratore) {
$amministratore = $stabile->amministratore;
}
}
}
if (! $amministratore instanceof Amministratore) {
abort(403);
}
$this->amministratore = $amministratore;
$isSuperAdmin = $user->hasAnyRole(['super-admin', 'admin']);
$this->getSchema('form')?->fill([
...$this->amministratore->only([
'nome',
'cognome',
'denominazione_studio',
'partita_iva',
'codice_fiscale_studio',
'indirizzo_studio',
'cap_studio',
'citta_studio',
'provincia_studio',
'telefono_studio',
'cellulare',
'email_studio',
'pec_studio',
'cartella_dati',
'database_attivo',
'fe_cassetto_enabled',
'fe_cassetto_base_url',
]),
// Per sicurezza non precompiliamo mai i segreti nel form.
// La configurazione completa è gestita dal Super Admin.
'fe_cassetto_password_script' => $isSuperAdmin ? null : null,
'fe_cassetto_username' => $isSuperAdmin ? null : null,
'fe_cassetto_password' => $isSuperAdmin ? null : null,
'fe_cassetto_pin' => $isSuperAdmin ? null : null,
'impostazioni' => $this->amministratore->impostazioni ?? [],
]);
}
public function form(Schema $schema): Schema
{
return $schema
->statePath('data')
->components([
Tabs::make('scheda_amministratore')
->tabs([
Tab::make('Anagrafica')
->schema([
Section::make('Dati studio')
->columns(2)
->schema([
TextInput::make('denominazione_studio')
->label('Denominazione studio')
->required()
->maxLength(255),
TextInput::make('partita_iva')
->label('Partita IVA')
->maxLength(32),
TextInput::make('codice_fiscale_studio')
->label('Codice fiscale studio')
->maxLength(32),
TextInput::make('nome')
->label('Nome')
->maxLength(100),
TextInput::make('cognome')
->label('Cognome')
->maxLength(100),
]),
Section::make('Recapiti')
->columns(2)
->schema([
TextInput::make('indirizzo_studio')
->label('Indirizzo')
->maxLength(255),
TextInput::make('cap_studio')
->label('CAP')
->maxLength(10),
TextInput::make('citta_studio')
->label('Città')
->maxLength(120),
TextInput::make('provincia_studio')
->label('Provincia')
->maxLength(10),
TextInput::make('telefono_studio')
->label('Telefono')
->maxLength(30),
TextInput::make('cellulare')
->label('Cellulare')
->maxLength(30),
TextInput::make('email_studio')
->label('Email')
->email()
->maxLength(255),
TextInput::make('pec_studio')
->label('PEC')
->email()
->maxLength(255),
]),
]),
Tab::make('Documento')
->schema([
Section::make('Intestazione e loghi')
->columns(2)
->schema([
FileUpload::make('impostazioni.documento.logo_1')
->label('Logo 1')
->disk('public')
->directory('amministratori/loghi')
->image()
->imageEditor(),
FileUpload::make('impostazioni.documento.logo_2')
->label('Logo 2 (opzionale)')
->disk('public')
->directory('amministratori/loghi')
->image()
->imageEditor(),
Textarea::make('impostazioni.documento.cappello')
->label('Cappello (testo intestazione)')
->rows(4)
->columnSpanFull()
->helperText('Se vuoto, usa automaticamente denominazione/indirizzo/contatti dello studio.'),
]),
Section::make('Dati fiscali e note documento')
->schema([
Textarea::make('impostazioni.documento.dati_fiscali')
->label('Dati amministrativi/fiscali (testo libero)')
->rows(4)
->helperText('Esempio: P.IVA, CF, REA, SDI, PEC, estremi professionali, ecc.'),
Textarea::make('impostazioni.documento.piede')
->label('Piè di pagina (testo libero)')
->rows(3)
->helperText('Testo che apparirà in basso in tutte le stampe dello studio.'),
Textarea::make('impostazioni.documento.firma')
->label('Firma (testo libero)')
->rows(3)
->helperText('Sarà riutilizzata nelle stampe dove prevista.'),
]),
Section::make('Compensi (stampa)')
->columns(3)
->schema([
TextInput::make('impostazioni.compensi.base')
->label('Compenso base')
->numeric()
->inputMode('decimal'),
TextInput::make('impostazioni.compensi.cassa_percento')
->label('Cassa (%)')
->numeric()
->inputMode('decimal'),
TextInput::make('impostazioni.compensi.iva_percento')
->label('IVA (%)')
->numeric()
->inputMode('decimal'),
]),
]),
Tab::make('Banche')
->schema([
Section::make('Conto corrente studio')
->columns(2)
->schema([
TextInput::make('impostazioni.banche.iban')
->label('IBAN')
->maxLength(64),
TextInput::make('impostazioni.banche.banca')
->label('Banca')
->maxLength(255),
TextInput::make('impostazioni.banche.intestatario')
->label('Intestatario')
->maxLength(255)
->columnSpanFull(),
]),
]),
Tab::make('Posta / API')
->schema([
Section::make('Posta (SMTP)')
->columns(2)
->schema([
TextInput::make('impostazioni.posta.smtp_host')
->label('SMTP host')
->maxLength(255),
TextInput::make('impostazioni.posta.smtp_port')
->label('SMTP porta')
->numeric(),
TextInput::make('impostazioni.posta.username')
->label('Username')
->maxLength(255),
TextInput::make('impostazioni.posta.password')
->label('Password')
->password()
->revealable()
->maxLength(255),
]),
Section::make('Google Workspace (Gmail / Calendar / Rubrica)')
->columns(2)
->schema([
Toggle::make('impostazioni.google.enabled')
->label('Abilita integrazione Google')
->default(false)
->columnSpanFull(),
TextInput::make('impostazioni.google.project_id')
->label('Google Project ID')
->maxLength(255),
TextInput::make('impostazioni.google.client_id')
->label('OAuth Client ID')
->maxLength(255),
TextInput::make('impostazioni.google.client_secret')
->label('OAuth Client Secret')
->password()
->revealable()
->maxLength(255),
TextInput::make('impostazioni.google.redirect_uri')
->label('Redirect URI')
->maxLength(255)
->helperText('Es: https://tuo-dominio/oauth/google/callback'),
TextInput::make('impostazioni.google.workspace_email')
->label('Email Google da collegare')
->email()
->maxLength(255),
TextInput::make('impostazioni.google.calendar_id')
->label('Calendar ID')
->maxLength(255)
->helperText('Es: primary o id calendario condiviso'),
TextInput::make('impostazioni.google.contacts_group')
->label('Gruppo rubrica Google')
->maxLength(255)
->helperText('Es: NetGesCon / Condomini'),
]),
Section::make('WhatsApp Cloud API (Meta)')
->columns(2)
->schema([
Toggle::make('impostazioni.whatsapp.enabled')
->label('Abilita integrazione WhatsApp')
->default(false)
->columnSpanFull(),
TextInput::make('impostazioni.whatsapp.business_account_id')
->label('WhatsApp Business Account ID')
->maxLength(255),
TextInput::make('impostazioni.whatsapp.phone_number_id')
->label('Phone Number ID')
->maxLength(255),
TextInput::make('impostazioni.whatsapp.access_token')
->label('Access Token')
->password()
->revealable()
->maxLength(255),
TextInput::make('impostazioni.whatsapp.webhook_verify_token')
->label('Webhook Verify Token')
->maxLength(255),
TextInput::make('impostazioni.whatsapp.webhook_url')
->label('Webhook URL (ricezione)')
->maxLength(255)
->helperText('Endpoint pubblico HTTPS per ricevere messaggi/eventi da Meta'),
TextInput::make('impostazioni.whatsapp.default_country_prefix')
->label('Prefisso default numeri')
->maxLength(8)
->helperText('Es: +39'),
TextInput::make('impostazioni.whatsapp.template_namespace')
->label('Namespace template (se richiesto)')
->maxLength(255),
]),
Section::make('Servizi esterni / API')
->columns(2)
->schema([
TextInput::make('impostazioni.api.endpoint')
->label('Endpoint')
->maxLength(255),
TextInput::make('impostazioni.api.token')
->label('Token/API key')
->password()
->revealable()
->maxLength(255),
Textarea::make('impostazioni.api.note')
->label('Note')
->rows(3)
->columnSpanFull(),
]),
]),
Tab::make('Stampe')
->schema([
Section::make('Stampanti')
->columns(2)
->schema([
TextInput::make('impostazioni.stampe.stampante_etichette')
->label('Stampante etichette (nome/descrizione)')
->maxLength(255)
->helperText('Campo informativo: su web non possiamo selezionare la stampante in modo affidabile; serve per ricordare quale usare.'),
Textarea::make('impostazioni.stampe.note')
->label('Note stampa')
->rows(3)
->columnSpanFull(),
]),
Section::make('Calibrazione DYMO 11354 (57×32)')
->columns(4)
->schema([
Toggle::make('impostazioni.stampe.dymo.11354.fix')
->label('Attiva workaround driver')
->default(false)
->columnSpanFull(),
Select::make('impostazioni.stampe.dymo.11354.rot')
->label('Rotazione')
->options([
-90 => '-90°',
90 => '90°',
])
->default(-90)
->required(),
TextInput::make('impostazioni.stampe.dymo.11354.dx')
->label('dx (mm)')
->numeric()
->inputMode('decimal')
->default(0),
TextInput::make('impostazioni.stampe.dymo.11354.dy')
->label('dy (mm)')
->numeric()
->inputMode('decimal')
->default(0),
]),
Section::make('Calibrazione DYMO 99014 (101×54)')
->columns(3)
->schema([
TextInput::make('impostazioni.stampe.dymo.99014.dx')
->label('dx (mm)')
->numeric()
->inputMode('decimal')
->default(0),
TextInput::make('impostazioni.stampe.dymo.99014.dy')
->label('dy (mm)')
->numeric()
->inputMode('decimal')
->default(0),
Select::make('impostazioni.stampe.dymo.99014.rot')
->label('Rotazione')
->options([
0 => '0°',
-90 => '-90°',
90 => '90°',
])
->default(0)
->required(),
]),
]),
Tab::make('Archivi')
->schema([
Section::make('Archivi (operatività)')
->columns(2)
->schema([
TextInput::make('cartella_dati')
->label('Cartella dati')
->disabled()
->dehydrated(false),
TextInput::make('database_attivo')
->label('Database attivo')
->disabled()
->dehydrated(false),
TextInput::make('impostazioni.archivi.documenti_path')
->label('Percorso documenti (override)')
->helperText('Opzionale: se impostato, verrà preferito come base per documenti/stampe.')
->columnSpanFull(),
]),
]),
Tab::make('Distribuzione / Backup / Ripristino')
->schema([
Section::make('Aggiornamento produzione')
->columns(2)
->schema([
TextInput::make('impostazioni.ops.release_tag')
->label('Tag release')
->placeholder('v1.3.0')
->helperText('Tag git da pubblicare in produzione.'),
TextInput::make('impostazioni.ops.production_app_dir')
->label('Percorso app produzione')
->default('/var/www/netgescon')
->maxLength(255),
TextInput::make('impostazioni.ops.production_backup_dir')
->label('Percorso backup produzione')
->default('/var/backups/netgescon')
->maxLength(255),
Toggle::make('impostazioni.ops.skip_migrations')
->label('Salta migrazioni DB (solo emergenza)')
->default(false),
]),
Section::make('Backup / Restore dati (file)')
->columns(2)
->schema([
TextInput::make('impostazioni.ops.dev_data_dir')
->label('Sorgente dati sviluppo')
->default('/home/michele/netgescon/netgescon-laravel/storage/app/amministratori')
->maxLength(255),
TextInput::make('impostazioni.ops.prod_data_dir')
->label('Destinazione dati produzione')
->default('/var/www/netgescon/storage/app/amministratori')
->maxLength(255)
->helperText('Restore non distruttivo: aggiunge/aggiorna, non elimina file destinazione.'),
Toggle::make('impostazioni.ops.restore_dry_run')
->label('Restore in dry-run (consigliato prima esecuzione)')
->default(true),
TextInput::make('impostazioni.ops.backup_remote')
->label('Remote backup (rclone)')
->default('gdrive:NetGesconBackups/prod')
->maxLength(255),
]),
Section::make('Sync aggiornamenti e archivi')
->columns(2)
->schema([
TextInput::make('impostazioni.ops.sync_source_dir')
->label('Sorgente codice (sviluppo)')
->default('/home/michele/netgescon/netgescon-laravel')
->maxLength(255),
TextInput::make('impostazioni.ops.sync_target_dir')
->label('Destinazione codice (produzione)')
->default('/var/www/netgescon')
->maxLength(255),
TextInput::make('impostazioni.ops.sync_archive_source')
->label('Sorgente archivi')
->default('/home/michele/backup-completo')
->maxLength(255),
TextInput::make('impostazioni.ops.sync_archive_target')
->label('Destinazione archivi')
->default('/mnt/nas-backup-produzione')
->maxLength(255),
Toggle::make('impostazioni.ops.sync_dry_run')
->label('Sync in dry-run')
->default(true),
]),
Section::make('Pacchetto aggiornamento scaricabile (solo differenze codice)')
->columns(2)
->schema([
TextInput::make('impostazioni.ops.package_source_dir')
->label('Sorgente codice sviluppo')
->default('/home/michele/netgescon/netgescon-laravel')
->maxLength(255),
TextInput::make('impostazioni.ops.package_compare_dir')
->label('Confronta con destinazione')
->default('/var/www/netgescon')
->helperText('Il pacchetto contiene solo file nuovi/modificati rispetto alla destinazione.')
->maxLength(255),
TextInput::make('impostazioni.ops.package_output_dir')
->label('Cartella output pacchetti')
->default('/var/www/netgescon/storage/app/public/ops-packages')
->maxLength(255)
->columnSpanFull(),
]),
Section::make('Backup incrementale dati su Google Drive')
->columns(2)
->schema([
TextInput::make('impostazioni.ops.data_backup_source')
->label('Sorgente dati (file)')
->default('/var/www/netgescon/storage/app/amministratori')
->helperText('Invia su Drive solo file nuovi/modificati; non cancella sul remoto.')
->maxLength(255),
TextInput::make('impostazioni.ops.data_backup_remote')
->label('Remote dati (rclone)')
->default('gdrive:NetGesconBackups/prod/data-incremental')
->maxLength(255),
]),
]),
]),
]);
}
public function runProductionUpgrade(): void
{
$tag = trim((string) Arr::get($this->data, 'impostazioni.ops.release_tag', ''));
if ($tag === '') {
Notification::make()
->title('Tag release mancante')
->warning()
->body('Imposta un tag release (es. v1.3.0) prima di aggiornare la produzione.')
->send();
return;
}
$appDir = trim((string) Arr::get($this->data, 'impostazioni.ops.production_app_dir', '/var/www/netgescon'));
$backupDir = trim((string) Arr::get($this->data, 'impostazioni.ops.production_backup_dir', '/var/backups/netgescon'));
$skipMigrations = (bool) Arr::get($this->data, 'impostazioni.ops.skip_migrations', false);
$cmd = [
'bash',
'scripts/ops/netgescon-upgrade-apply.sh',
'--tag',
$tag,
'--app-dir',
$appDir,
'--backup-dir',
$backupDir,
];
if ($skipMigrations) {
$cmd[] = '--skip-migrations';
}
$this->runOpsProcess($cmd, 'Aggiornamento produzione completato', 'Aggiornamento produzione fallito', 3600);
}
public function reviewUsersToEnable(): void
{
$this->runOpsProcess(
['bash', 'scripts/ops/netgescon-users-review.sh', '--limit', '200'],
'Controllo utenti completato',
'Controllo utenti fallito',
300,
);
}
public function runBackupProductionData(): void
{
$appDir = trim((string) Arr::get($this->data, 'impostazioni.ops.production_app_dir', '/var/www/netgescon'));
$remote = trim((string) Arr::get($this->data, 'impostazioni.ops.backup_remote', 'gdrive:NetGesconBackups/prod'));
$this->runOpsProcess(
['bash', 'scripts/ops/netgescon-backup-gdrive.sh', '--app-dir', $appDir, '--remote', $remote],
'Backup produzione completato',
'Backup produzione fallito',
3600,
);
}
public function runRestoreDataNonDistruttivo(): void
{
$from = trim((string) Arr::get($this->data, 'impostazioni.ops.dev_data_dir', '/home/michele/netgescon/netgescon-laravel/storage/app/amministratori'));
$to = trim((string) Arr::get($this->data, 'impostazioni.ops.prod_data_dir', '/var/www/netgescon/storage/app/amministratori'));
$dryRun = (bool) Arr::get($this->data, 'impostazioni.ops.restore_dry_run', true);
$cmd = ['bash', 'scripts/ops/netgescon-data-sync-nondestructive.sh', '--from', $from, '--to', $to];
if ($dryRun) {
$cmd[] = '--dry-run';
}
$this->runOpsProcess(
$cmd,
$dryRun ? 'Restore dry-run completato' : 'Restore non distruttivo completato',
'Restore non distruttivo fallito',
3600,
);
}
public function runSyncProdUpdate(): void
{
$src = trim((string) Arr::get($this->data, 'impostazioni.ops.sync_source_dir', '/home/michele/netgescon/netgescon-laravel'));
$dst = trim((string) Arr::get($this->data, 'impostazioni.ops.sync_target_dir', '/var/www/netgescon'));
$archiveSrc = trim((string) Arr::get($this->data, 'impostazioni.ops.sync_archive_source', '/home/michele/backup-completo'));
$archiveDst = trim((string) Arr::get($this->data, 'impostazioni.ops.sync_archive_target', '/mnt/nas-backup-produzione'));
$dryRun = (bool) Arr::get($this->data, 'impostazioni.ops.sync_dry_run', true);
$cmd = [
'bash',
'scripts/ops/netgescon-sync-prod-update.sh',
'--src', $src,
'--dst', $dst,
'--archive-src', $archiveSrc,
'--archive-dst', $archiveDst,
];
if ($dryRun) {
$cmd[] = '--dry-run';
} else {
$cmd[] = '--post-deploy';
}
$this->runOpsProcess(
$cmd,
$dryRun ? 'Sync produzione dry-run completato' : 'Sync produzione completato',
'Sync produzione fallito',
7200,
);
}
public function runBuildProdUpdatePackage(): void
{
$src = trim((string) Arr::get($this->data, 'impostazioni.ops.package_source_dir', '/home/michele/netgescon/netgescon-laravel'));
$compare = trim((string) Arr::get($this->data, 'impostazioni.ops.package_compare_dir', '/var/www/netgescon'));
$outDir = trim((string) Arr::get($this->data, 'impostazioni.ops.package_output_dir', '/var/www/netgescon/storage/app/public/ops-packages'));
$process = new Process([
'bash',
'scripts/ops/netgescon-build-update-package.sh',
'--src', $src,
'--compare-dir', $compare,
'--out-dir', $outDir,
], base_path());
$process->setTimeout(3600);
$process->run();
$output = trim((string) $process->getOutput());
$error = trim((string) $process->getErrorOutput());
$merged = trim($output . PHP_EOL . $error);
$this->opsLastOutput = $merged !== '' ? $merged : '(nessun output)';
if (! $process->isSuccessful()) {
Notification::make()
->title('Creazione pacchetto fallita')
->danger()
->body($error !== '' ? mb_substr($error, 0, 400) : 'Controlla output operazioni nella pagina.')
->send();
return;
}
if (preg_match('/^PACKAGE_PATH=(.+)$/m', $output, $matches) === 1) {
$path = trim($matches[1]);
if ($path !== '') {
$this->lastUpdatePackagePath = $path;
$this->lastUpdatePackageName = basename($path);
}
}
Notification::make()
->title('Pacchetto aggiornamento creato')
->success()
->send();
}
public function downloadLastUpdatePackage()
{
$path = trim((string) ($this->lastUpdatePackagePath ?? ''));
if ($path === '' || ! is_file($path)) {
Notification::make()
->title('Nessun pacchetto disponibile')
->warning()
->body('Genera prima il pacchetto aggiornamento.')
->send();
return null;
}
return response()->download($path, $this->lastUpdatePackageName ?: basename($path));
}
public function runBackupModifiedDataToGdrive(): void
{
$source = trim((string) Arr::get($this->data, 'impostazioni.ops.data_backup_source', '/var/www/netgescon/storage/app/amministratori'));
$remote = trim((string) Arr::get($this->data, 'impostazioni.ops.data_backup_remote', 'gdrive:NetGesconBackups/prod/data-incremental'));
$this->runOpsProcess(
['bash', 'scripts/ops/netgescon-data-backup-incremental-gdrive.sh', '--source', $source, '--remote', $remote],
'Backup incrementale dati su Google Drive completato',
'Backup incrementale dati su Google Drive fallito',
3600,
);
}
public function runGoogleDriveCheck(): void
{
$remote = trim((string) Arr::get($this->data, 'impostazioni.ops.data_backup_remote', 'gdrive:NetGesconBackups/prod/data-incremental'));
$remoteName = Str::before($remote, ':');
$this->runOpsProcess(
['bash', 'scripts/ops/netgescon-gdrive-check.sh', '--remote', $remoteName !== '' ? $remoteName : 'gdrive'],
'Connessione Google Drive verificata',
'Connessione Google Drive non valida',
120,
);
}
public function connectGoogle(): void
{
$google = Arr::get($this->amministratore->impostazioni ?? [], 'google', []);
$clientId = trim((string) ($google['client_id'] ?? config('services.google.client_id')));
$clientSecret = trim((string) ($google['client_secret'] ?? config('services.google.client_secret')));
$redirectUri = trim((string) ($google['redirect_uri'] ?? config('services.google.redirect')));
if ($clientId === '' || $clientSecret === '' || $redirectUri === '') {
Notification::make()
->title('Configura prima Google OAuth')
->warning()
->body('Compila Client ID, Client Secret e Redirect URI nella tab Posta / API, poi salva la scheda.')
->send();
return;
}
// In azioni Livewire/Filament usiamo redirectRoute per evitare mismatch di tipo.
$this->redirectRoute('oauth.google.redirect');
}
public function disconnectGoogle(): void
{
$impostazioni = $this->amministratore->impostazioni ?? [];
$google = Arr::get($impostazioni, 'google', []);
$google['oauth'] = [
'connected' => false,
'disconnected_at' => now()->toDateTimeString(),
];
$impostazioni['google'] = $google;
$this->amministratore->impostazioni = $impostazioni;
$this->amministratore->save();
Notification::make()
->title('Google scollegato')
->success()
->send();
}
private function runOpsProcess(array $command, string $successTitle, string $errorTitle, int $timeoutSeconds): void
{
$process = new Process($command, base_path());
$process->setTimeout($timeoutSeconds);
$process->run();
$output = trim((string) $process->getOutput());
$error = trim((string) $process->getErrorOutput());
$merged = trim($output . PHP_EOL . $error);
$this->opsLastOutput = $merged !== '' ? $merged : '(nessun output)';
if ($process->isSuccessful()) {
Notification::make()
->title($successTitle)
->success()
->send();
return;
}
Notification::make()
->title($errorTitle)
->danger()
->body($error !== '' ? mb_substr($error, 0, 400) : 'Controlla output operazioni nella pagina.')
->send();
}
public function submit(): void
{
$state = $this->getSchema('form')?->getState() ?? [];
// In alcune configurazioni Filament, con statePath('data') getState() può restituire ['data' => ...].
// Normalizziamo per essere robusti.
if (is_array($state['data'] ?? null)) {
$state = $state['data'];
} elseif (is_array($this->data ?? null) && ! empty($this->data)) {
$state = $this->data;
}
$fillable = [
'nome',
'cognome',
'denominazione_studio',
'partita_iva',
'codice_fiscale_studio',
'indirizzo_studio',
'cap_studio',
'citta_studio',
'provincia_studio',
'telefono_studio',
'cellulare',
'email_studio',
'pec_studio',
'fe_cassetto_enabled',
'fe_cassetto_base_url',
];
$update = Arr::only($state, $fillable);
// Segreti: sovrascrivi solo se compilati.
foreach (['fe_cassetto_password_script', 'fe_cassetto_username', 'fe_cassetto_password', 'fe_cassetto_pin'] as $k) {
if (isset($state[$k]) && filled($state[$k])) {
$update[$k] = (string) $state[$k];
}
}
$this->amministratore->fill($update);
$this->amministratore->impostazioni = is_array($state['impostazioni'] ?? null)
? $state['impostazioni']
: [];
$this->amministratore->save();
Notification::make()
->title('Scheda salvata')
->success()
->send();
}
}