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

1391 lines
68 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\Fornitore;
use App\Models\FornitoreDipendente;
use App\Models\Stabile;
use App\Models\Ticket;
use App\Models\User;
use App\Support\StabileContext;
use BackedEnum;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Repeater;
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\Facades\Hash;
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 ?string $lastGeneratedPassword = null;
/** @var array<int,string> */
public array $collaboratorePbxExtension = [];
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),
]),
Section::make('Centralino studio')
->columns(3)
->schema([
TextInput::make('impostazioni.centralino.numero_principale')
->label('Numero principale centralino')
->maxLength(30),
TextInput::make('impostazioni.centralino.numero_backup')
->label('Numero secondario/backup')
->maxLength(30),
TextInput::make('impostazioni.centralino.numero_emergenza')
->label('Numero emergenza')
->maxLength(30),
TextInput::make('impostazioni.centralino.interno_centralino')
->label('Interno centralino')
->maxLength(20)
->placeholder('Es. 201'),
TextInput::make('impostazioni.centralino.interno_operatore')
->label('Interno operatore studio')
->maxLength(20)
->placeholder('Es. 205'),
TextInput::make('impostazioni.centralino.interno_amministratore')
->label('Interno amministratore')
->maxLength(20)
->placeholder('Es. 206'),
TextInput::make('impostazioni.centralino.interno_gruppo_giorno')
->label('Interno gruppo giorno')
->maxLength(20)
->placeholder('Es. 601'),
TextInput::make('impostazioni.centralino.interno_gruppo_notte')
->label('Interno gruppo notte')
->maxLength(20)
->placeholder('Es. 603'),
TextInput::make('impostazioni.centralino.note_instradamento')
->label('Note instradamento')
->maxLength(255)
->columnSpanFull(),
]),
]),
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'),
Toggle::make('impostazioni.google.mail_enabled')
->label('Abilita lettura Gmail')
->default(false),
TextInput::make('impostazioni.google.gmail_label')
->label('Label Gmail da leggere')
->maxLength(255)
->helperText('Es: NetGescon/Assicurazioni'),
TextInput::make('impostazioni.google.gmail_query')
->label('Query Gmail')
->maxLength(255)
->helperText('Es: from:assicurazione@example.it has:attachment newer_than:30d'),
Toggle::make('impostazioni.google.archive_after_import')
->label('Archivia dopo l\'acquisizione')
->default(false),
Toggle::make('impostazioni.google.import_attachments')
->label('Importa allegati')
->default(true),
TextInput::make('impostazioni.google.allowed_senders')
->label('Mittenti ammessi')
->maxLength(500)
->helperText('Email separate da virgola; utile per assicurazioni o inoltri FE.'),
]),
Section::make('Caselle studio / PEC da collegare')
->description('Caselle dello studio amministratore. Per le caselle specifiche del condominio usa la nuova voce Posta stabile nella scheda Stabile.')
->schema([
Repeater::make('impostazioni.posta.caselle')
->label('Caselle collegate')
->default([])
->schema([
Toggle::make('enabled')
->label('Attiva')
->default(true),
Select::make('tipo')
->label('Tipo')
->options([
'gmail' => 'Gmail / Google Workspace',
'imap' => 'IMAP ordinaria',
'pec' => 'PEC via IMAP',
])
->default('imap')
->required(),
TextInput::make('label')
->label('Etichetta')
->required()
->maxLength(120),
TextInput::make('email')
->label('Email casella')
->email()
->maxLength(255),
TextInput::make('host')
->label('Host IMAP')
->maxLength(255),
TextInput::make('port')
->label('Porta')
->numeric(),
Select::make('encryption')
->label('Cifratura')
->options([
'ssl' => 'SSL',
'tls' => 'TLS',
'none' => 'Nessuna',
])
->default('ssl'),
TextInput::make('username')
->label('Username')
->maxLength(255),
TextInput::make('password')
->label('Password')
->password()
->revealable()
->maxLength(255),
TextInput::make('folder')
->label('Cartella da leggere')
->maxLength(120)
->default('INBOX'),
])
->columns(2)
->columnSpanFull(),
]),
Section::make('Acquisizione messaggi e allegati')
->columns(2)
->schema([
Toggle::make('impostazioni.posta.routing.salva_documenti')
->label('Archivia allegati nei documenti')
->default(true),
Toggle::make('impostazioni.posta.routing.salva_contabilita')
->label('Rendi disponibili gli allegati in contabilità')
->default(true),
TextInput::make('impostazioni.posta.routing.mittenti_assicurazione')
->label('Mittenti assicurazione')
->maxLength(500)
->helperText('Email separate da virgola per filtrare i messaggi dellassicuratore.'),
TextInput::make('impostazioni.posta.routing.descrizione_default')
->label('Descrizione default allegato')
->maxLength(255),
]),
Section::make('Operativita posta / Google')
->schema([
Placeholder::make('mail_ops_panel')
->hiddenLabel()
->content(fn() => view('filament.pages.impostazioni.partials.scheda-amministratore-posta-operativita')),
]),
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),
]),
Section::make('Azioni operative')
->schema([
Placeholder::make('ops_distribution_panel')
->hiddenLabel()
->content(fn() => view('filament.pages.impostazioni.partials.scheda-amministratore-distribuzione-backup')),
]),
]),
Tab::make('Operativita / Accessi')
->schema([
Section::make('Azioni operative e gestione accessi')
->schema([
Placeholder::make('ops_access_panel')
->hiddenLabel()
->content(fn() => view('filament.pages.impostazioni.partials.scheda-amministratore-operativita-accessi')),
]),
]),
]),
]);
}
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 runGoogleOAuthReadinessCheck(): void
{
$this->runOpsProcess(
['bash', 'scripts/ops/netgescon-google-check.sh'],
'Controllo Google OAuth completato',
'Controllo Google OAuth fallito',
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();
}
/**
* @return array<int,array<string,mixed>>
*/
public function getAccessoFornitoriRowsProperty(): array
{
return FornitoreDipendente::query()
->with(['user.roles', 'fornitore'])
->whereHas('fornitore', function ($q): void {
$q->where('amministratore_id', (int) $this->amministratore->id);
})
->orderByDesc('attivo')
->orderBy('cognome')
->orderBy('nome')
->limit(120)
->get()
->map(function (FornitoreDipendente $dipendente): array {
$user = $dipendente->user;
return [
'dipendente_id' => (int) $dipendente->id,
'fornitore' => (string) ($dipendente->fornitore?->ragione_sociale ?: $dipendente->fornitore?->nome ?: 'Fornitore'),
'nome' => $dipendente->nome_completo,
'email' => (string) ($dipendente->email ?? ''),
'attivo' => (bool) $dipendente->attivo,
'user_id' => $user ? (int) $user->id : null,
'ruoli' => $user ? $user->roles->pluck('name')->values()->all() : [],
];
})
->all();
}
/**
* @return array<int,array<string,mixed>>
*/
public function getFornitoriDaTicketRowsProperty(): array
{
$fornitoreIds = Ticket::query()
->whereNotNull('assegnato_a_fornitore_id')
->whereHas('stabile', function ($q): void {
$q->where('amministratore_id', (int) $this->amministratore->id);
})
->distinct()
->pluck('assegnato_a_fornitore_id')
->filter(fn($v) => is_numeric($v))
->map(fn($v) => (int) $v)
->values()
->all();
if ($fornitoreIds === []) {
return [];
}
return Fornitore::query()
->whereIn('id', $fornitoreIds)
->orderByRaw("COALESCE(ragione_sociale, nome, '')")
->get()
->map(function (Fornitore $fornitore): array {
$linkedDipendente = FornitoreDipendente::query()
->where('fornitore_id', (int) $fornitore->id)
->whereNotNull('user_id')
->first();
return [
'fornitore_id' => (int) $fornitore->id,
'fornitore_nome' => (string) ($fornitore->ragione_sociale ?: trim(($fornitore->nome ?? '') . ' ' . ($fornitore->cognome ?? '')) ?: ('Fornitore #' . $fornitore->id)),
'email' => (string) ($fornitore->email ?? ''),
'telefono' => (string) ($fornitore->telefono ?: $fornitore->cellulare ?: ''),
'linked_user_id' => $linkedDipendente ? (int) $linkedDipendente->user_id : null,
];
})
->values()
->all();
}
public function abilitaAccessoFornitoreDaTicket(int $fornitoreId): void
{
$fornitore = Fornitore::query()->find($fornitoreId);
if (! $fornitore || (int) ($fornitore->amministratore_id ?? 0) !== (int) $this->amministratore->id) {
Notification::make()->title('Fornitore non valido')->danger()->send();
return;
}
$email = trim((string) ($fornitore->email ?? ''));
if ($email === '') {
Notification::make()
->title('Email fornitore mancante')
->warning()
->body('Inserisci email del fornitore per creare credenziali di accesso.')
->send();
return;
}
$dipendente = FornitoreDipendente::query()
->where('fornitore_id', (int) $fornitore->id)
->whereRaw('LOWER(email) = ?', [mb_strtolower($email)])
->first();
if (! $dipendente) {
$dipendente = FornitoreDipendente::query()->create([
'fornitore_id' => (int) $fornitore->id,
'nome' => (string) ($fornitore->ragione_sociale ?: ($fornitore->nome ?: 'Referente fornitore')),
'cognome' => $fornitore->ragione_sociale ? null : (($fornitore->cognome ?? null) ?: null),
'email' => $email,
'telefono' => $fornitore->telefono ?: $fornitore->cellulare,
'attivo' => true,
'created_by_user_id' => Auth::id(),
'updated_by_user_id' => Auth::id(),
]);
}
$this->abilitaAccessoFornitore((int) $dipendente->id);
}
/**
* @return array<int,array<string,string>>
*/
public function getPendingUsersReviewRowsProperty(): array
{
$output = (string) ($this->opsLastOutput ?? '');
if ($output === '' || ! str_contains($output, 'PENDING_USERS=')) {
return [];
}
$rows = [];
$lines = preg_split('/\r\n|\r|\n/', $output) ?: [];
foreach ($lines as $line) {
$line = trim((string) $line);
if (! str_starts_with($line, 'id=')) {
continue;
}
preg_match('/id=(\d+)/', $line, $idMatch);
preg_match('/email=([^|]+)/', $line, $emailMatch);
preg_match('/name=([^|]+)/', $line, $nameMatch);
preg_match('/roles=([^|]+)/', $line, $rolesMatch);
preg_match('/flags=([^|]+)/', $line, $flagsMatch);
preg_match('/created=(.+)$/', $line, $createdMatch);
$rows[] = [
'id' => isset($idMatch[1]) ? trim($idMatch[1]) : '-',
'email' => isset($emailMatch[1]) ? trim($emailMatch[1]) : '-',
'name' => isset($nameMatch[1]) ? trim($nameMatch[1]) : '-',
'roles' => isset($rolesMatch[1]) ? trim($rolesMatch[1]) : '-',
'flags' => isset($flagsMatch[1]) ? trim($flagsMatch[1]) : '-',
'created' => isset($createdMatch[1]) ? trim($createdMatch[1]) : '-',
];
}
return $rows;
}
/**
* @return array<int,array<string,mixed>>
*/
public function getAccessoAmministratoreRowsProperty(): array
{
$query = User::query()
->with('roles')
->whereHas('roles', function ($q): void {
$q->whereIn('name', ['super-admin', 'admin', 'amministratore']);
})
->where(function ($q): void {
$q->whereKey((int) ($this->amministratore->user_id ?? 0))
->orWhereHas('amministratore', function ($qa): void {
$qa->whereKey((int) $this->amministratore->id);
});
})
->orderBy('name')
->limit(40);
return $query
->get()
->map(fn(User $user): array=> [
'user_id' => (int) $user->id,
'name' => (string) $user->name,
'email' => (string) $user->email,
'ruoli' => $user->roles->pluck('name')->values()->all(),
])
->all();
}
/**
* @return array<int,array<string,mixed>>
*/
public function getCollaboratoriCentralinoRowsProperty(): array
{
$query = User::query()
->with('roles')
->whereHas('roles', function ($q): void {
$q->where('name', 'collaboratore');
})
->whereHas('stabiliAssegnati', function ($q): void {
$q->where('amministratore_id', (int) $this->amministratore->id);
})
->orderBy('name')
->limit(200);
return $query
->get()
->map(function (User $user): array {
$this->collaboratorePbxExtension[(int) $user->id] = (string) ($user->pbx_extension ?? '');
return [
'user_id' => (int) $user->id,
'name' => (string) $user->name,
'email' => (string) $user->email,
'ruoli' => $user->roles->pluck('name')->values()->all(),
'pbx_extension' => (string) ($user->pbx_extension ?? ''),
'pbx_popup_enabled' => (bool) ($user->pbx_popup_enabled ?? false),
'pbx_click_to_call_enabled' => (bool) ($user->pbx_click_to_call_enabled ?? false),
];
})
->all();
}
public function salvaInternoCollaboratore(int $userId): void
{
$user = User::query()->find($userId);
if (! $user) {
Notification::make()->title('Utente non trovato')->danger()->send();
return;
}
$isCollaboratoreDelTenant = $user->hasRole('collaboratore')
&& $user->stabiliAssegnati()->where('amministratore_id', (int) $this->amministratore->id)->exists();
if (! $isCollaboratoreDelTenant) {
Notification::make()->title('Collaboratore non valido')->danger()->send();
return;
}
$extension = trim((string) ($this->collaboratorePbxExtension[$userId] ?? ''));
$user->pbx_extension = $extension !== '' ? $extension : null;
$user->save();
Notification::make()
->title('Interno collaboratore aggiornato')
->success()
->send();
}
public function abilitaAccessoFornitore(int $dipendenteId): void
{
$dipendente = FornitoreDipendente::query()
->with('fornitore')
->whereKey($dipendenteId)
->first();
if (! $dipendente || (int) ($dipendente->fornitore?->amministratore_id ?? 0) !== (int) $this->amministratore->id) {
Notification::make()->title('Dipendente non valido')->danger()->send();
return;
}
$email = trim((string) ($dipendente->email ?? ''));
if ($email === '') {
Notification::make()
->title('Email mancante')
->warning()
->body('Imposta prima un\'email per il dipendente fornitore.')
->send();
return;
}
$user = User::query()->whereRaw('LOWER(email) = ?', [mb_strtolower($email)])->first();
$created = false;
if (! $user) {
$createdPassword = $this->generateTemporaryPassword();
$user = User::query()->create([
'name' => trim((string) ($dipendente->nome_completo ?: 'Utente Fornitore')),
'email' => $email,
'password' => Hash::make($createdPassword),
'email_verified_at' => now(),
'is_active' => true,
]);
$this->lastGeneratedPassword = $createdPassword;
$created = true;
}
$user->assignRole('fornitore');
$dipendente->user_id = (int) $user->id;
$dipendente->save();
$body = $created
? 'Accesso creato. Password temporanea: ' . ($this->lastGeneratedPassword ?? '(non disponibile)')
: 'Accesso attivo/aggiornato. Se necessario, usa il reset password.';
Notification::make()
->title('Accesso fornitore abilitato')
->success()
->body($body)
->send();
}
public function resetPasswordUtente(int $userId): void
{
$user = User::query()->find($userId);
if (! $user) {
Notification::make()->title('Utente non trovato')->danger()->send();
return;
}
$isAllowedAdminUser = (int) $user->id === (int) ($this->amministratore->user_id ?? 0);
$isAllowedSupplierUser = FornitoreDipendente::query()
->where('user_id', (int) $user->id)
->whereHas('fornitore', function ($q): void {
$q->where('amministratore_id', (int) $this->amministratore->id);
})
->exists();
if (! $isAllowedAdminUser && ! $isAllowedSupplierUser) {
Notification::make()
->title('Operazione non consentita')
->danger()
->body('Puoi resettare solo utenti collegati al tuo gruppo amministratore/fornitore.')
->send();
return;
}
$newPassword = $this->generateTemporaryPassword();
$user->password = Hash::make($newPassword);
$user->save();
$this->lastGeneratedPassword = $newPassword;
Notification::make()
->title('Password resettata')
->success()
->body('Nuova password temporanea: ' . $newPassword)
->send();
}
private function generateTemporaryPassword(): string
{
return Str::random(12);
}
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();
}
}