873 lines
44 KiB
PHP
873 lines
44 KiB
PHP
<?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();
|
||
}
|
||
}
|