1763 lines
89 KiB
PHP
1763 lines
89 KiB
PHP
<?php
|
||
namespace App\Filament\Pages\Impostazioni;
|
||
|
||
use App\Models\Amministratore;
|
||
use App\Models\CommunicationMessage;
|
||
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 as FilamentSchema;
|
||
use Illuminate\Support\Arr;
|
||
use Illuminate\Support\Facades\Auth;
|
||
use Illuminate\Support\Facades\Hash;
|
||
use Illuminate\Support\Facades\Schema;
|
||
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(FilamentSchema $schema): FilamentSchema
|
||
{
|
||
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 dell’assicuratore.'),
|
||
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('PBX / TAPI')
|
||
->schema([
|
||
Section::make('Profilo centralino e bridge')
|
||
->columns(3)
|
||
->schema([
|
||
Select::make('impostazioni.pbx.provider')
|
||
->label('Tipo centralino')
|
||
->options([
|
||
'panasonic_ns1000' => 'Panasonic NS1000',
|
||
'panasonic_generic' => 'Panasonic generico',
|
||
'wildix' => 'Wildix',
|
||
'3cx' => '3CX',
|
||
'custom' => 'Altro / custom',
|
||
])
|
||
->default('panasonic_ns1000'),
|
||
Select::make('impostazioni.pbx.channel')
|
||
->label('Canale CRM')
|
||
->options([
|
||
'panasonic_csta' => 'Panasonic CSTA / CTI',
|
||
'tapi' => 'Windows TAPI',
|
||
'hybrid' => 'Ibrido TAPI + CSTA',
|
||
])
|
||
->default('hybrid'),
|
||
Select::make('impostazioni.pbx.bridge_mode')
|
||
->label('Modalita bridge')
|
||
->options([
|
||
'group-first' => 'Group-first',
|
||
'extension-first' => 'Extension-first',
|
||
'none' => 'Solo monitoraggio',
|
||
])
|
||
->default('group-first'),
|
||
TextInput::make('impostazioni.pbx.tapi_host')
|
||
->label('Host Windows watcher')
|
||
->maxLength(255)
|
||
->placeholder('Es. ws-panasonic-01'),
|
||
TextInput::make('impostazioni.pbx.tapi_provider')
|
||
->label('Provider TAPI / TSP')
|
||
->maxLength(255)
|
||
->default('CTSP0000.tsp'),
|
||
TextInput::make('impostazioni.pbx.watch_extensions')
|
||
->label('Interni monitorati')
|
||
->maxLength(255)
|
||
->placeholder('201,206,601,603,0003')
|
||
->helperText('Lista CSV degli interni/gruppi che il watcher Windows deve osservare.'),
|
||
Toggle::make('impostazioni.pbx.popup_enabled')
|
||
->label('Popup CRM attivi')
|
||
->default(true),
|
||
Toggle::make('impostazioni.pbx.click_to_call_enabled')
|
||
->label('Click-to-call attivo')
|
||
->default(true),
|
||
Textarea::make('impostazioni.pbx.routing_notes')
|
||
->label('Note routing PBX')
|
||
->rows(3)
|
||
->columnSpanFull(),
|
||
]),
|
||
|
||
Section::make('Gruppi di risposta')
|
||
->schema([
|
||
Repeater::make('impostazioni.pbx.response_groups')
|
||
->label('Gruppi di risposta Panasonic')
|
||
->defaultItems(0)
|
||
->schema([
|
||
TextInput::make('extension')
|
||
->label('Interno gruppo')
|
||
->maxLength(20)
|
||
->placeholder('601'),
|
||
TextInput::make('label')
|
||
->label('Descrizione')
|
||
->maxLength(120)
|
||
->placeholder('Gruppo giorno'),
|
||
TextInput::make('mode')
|
||
->label('Modalita')
|
||
->maxLength(60)
|
||
->placeholder('giorno / notte / backup'),
|
||
TextInput::make('target_extension')
|
||
->label('Interno di fallback')
|
||
->maxLength(20)
|
||
->placeholder('201 oppure 206'),
|
||
])
|
||
->columns(4)
|
||
->columnSpanFull(),
|
||
]),
|
||
|
||
Section::make('Linee entranti e DID')
|
||
->schema([
|
||
Repeater::make('impostazioni.pbx.incoming_lines')
|
||
->label('Linee in entrata')
|
||
->defaultItems(0)
|
||
->schema([
|
||
TextInput::make('number')
|
||
->label('Numero / DID')
|
||
->maxLength(40)
|
||
->placeholder('0003'),
|
||
TextInput::make('label')
|
||
->label('Descrizione linea')
|
||
->maxLength(120)
|
||
->placeholder('Linea amministratore'),
|
||
TextInput::make('route_group')
|
||
->label('Gruppo target')
|
||
->maxLength(20)
|
||
->placeholder('601 o 603'),
|
||
TextInput::make('target_extension')
|
||
->label('Interno target')
|
||
->maxLength(20)
|
||
->placeholder('interno amministratore'),
|
||
Textarea::make('notes')
|
||
->label('Note')
|
||
->rows(2)
|
||
->columnSpanFull(),
|
||
])
|
||
->columns(4)
|
||
->columnSpanFull(),
|
||
]),
|
||
|
||
Section::make('Cruscotto chiamate Panasonic / TAPI')
|
||
->schema([
|
||
Placeholder::make('pbx_tapi_panel')
|
||
->hiddenLabel()
|
||
->content(fn() => view('filament.pages.impostazioni.partials.scheda-amministratore-pbx-tapi')),
|
||
]),
|
||
]),
|
||
|
||
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();
|
||
}
|
||
|
||
/**
|
||
* @return array{status:string,label:string,provider:string,channel:string,bridge_mode:string,watched_extensions:array<int,string>,response_groups:array<int,array<string,mixed>>,incoming_lines:array<int,array<string,mixed>>,last_event_at:?string,last_event_type:?string,total_messages:int,last_24h:int,missed_calls:int,assessment:string}
|
||
*/
|
||
public function getPbxSummaryProperty(): array
|
||
{
|
||
$settings = (array) Arr::get($this->amministratore->impostazioni ?? [], 'pbx', []);
|
||
$provider = (string) ($settings['provider'] ?? 'panasonic_ns1000');
|
||
$channel = (string) ($settings['channel'] ?? 'hybrid');
|
||
$bridgeMode = (string) ($settings['bridge_mode'] ?? 'group-first');
|
||
$watchedExtensions = $this->resolveWatchedPbxExtensions($settings);
|
||
$responseGroups = array_values(array_filter((array) ($settings['response_groups'] ?? []), fn($row): bool => is_array($row)));
|
||
$incomingLines = array_values(array_filter((array) ($settings['incoming_lines'] ?? []), fn($row): bool => is_array($row)));
|
||
|
||
if (! Schema::hasTable('communication_messages')) {
|
||
return [
|
||
'status' => ($watchedExtensions !== [] || $responseGroups !== [] || $incomingLines !== []) ? 'configured' : 'to-configure',
|
||
'label' => ($watchedExtensions !== [] || $responseGroups !== [] || $incomingLines !== []) ? 'Configurato, in attesa eventi' : 'Da configurare',
|
||
'provider' => $provider,
|
||
'channel' => $channel,
|
||
'bridge_mode' => $bridgeMode,
|
||
'watched_extensions' => $watchedExtensions,
|
||
'response_groups' => $responseGroups,
|
||
'incoming_lines' => $incomingLines,
|
||
'last_event_at' => null,
|
||
'last_event_type' => null,
|
||
'total_messages' => 0,
|
||
'last_24h' => 0,
|
||
'missed_calls' => 0,
|
||
'assessment' => 'La tabella communication_messages non e disponibile in questo ambiente, quindi il cruscotto mostra solo la configurazione PBX.',
|
||
];
|
||
}
|
||
|
||
$query = $this->pbxMessagesQuery();
|
||
$rawQuery = $this->pbxRawMessagesQuery();
|
||
$latest = (clone $query)->orderByDesc('received_at')->orderByDesc('id')->first();
|
||
$total = (clone $query)->count();
|
||
$last24h = (clone $query)->where('received_at', '>=', now()->subDay())->count();
|
||
$missedCalls = (clone $rawQuery)->where(function ($q): void {
|
||
$q->where('message_text', 'like', '%persa%')
|
||
->orWhere('metadata->outcome', 'like', '%miss%')
|
||
->orWhere('metadata->outcome', 'like', '%no_answer%');
|
||
})->count();
|
||
|
||
$status = 'to-configure';
|
||
$label = 'Da configurare';
|
||
if ($total > 0) {
|
||
$status = 'active';
|
||
$label = 'CRM collegato';
|
||
} elseif ($watchedExtensions !== [] || $responseGroups !== [] || $incomingLines !== []) {
|
||
$status = 'configured';
|
||
$label = 'Configurato, in attesa eventi';
|
||
}
|
||
|
||
$assessment = $bridgeMode === 'group-first'
|
||
? 'Con modalita group-first il watcher Windows puo vedere eventi sugli interni singoli ma il bridge usa come sorgente autorevole i gruppi di risposta. Questo e coerente con i log Panasonic gia acquisiti.'
|
||
: 'La configurazione e predisposta per usare gli eventi PBX direttamente nel CRM. Verifica i log del watcher Windows per confermare il flusso sul canale selezionato.';
|
||
|
||
return [
|
||
'status' => $status,
|
||
'label' => $label,
|
||
'provider' => $provider,
|
||
'channel' => $channel,
|
||
'bridge_mode' => $bridgeMode,
|
||
'watched_extensions' => $watchedExtensions,
|
||
'response_groups' => $responseGroups,
|
||
'incoming_lines' => $incomingLines,
|
||
'last_event_at' => $latest?->received_at?->format('d/m/Y H:i:s'),
|
||
'last_event_type' => $latest ? (string) data_get($latest->metadata, 'event_type', $latest->status) : null,
|
||
'total_messages' => $total,
|
||
'last_24h' => $last24h,
|
||
'missed_calls' => $missedCalls,
|
||
'assessment' => $assessment,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* @return array<int,array<string,mixed>>
|
||
*/
|
||
public function getPbxRecentMessagesProperty(): array
|
||
{
|
||
if (! Schema::hasTable('communication_messages')) {
|
||
return [];
|
||
}
|
||
|
||
return $this->pbxMessagesQuery()
|
||
->with('assignedUser:id,name')
|
||
->orderByDesc('received_at')
|
||
->orderByDesc('id')
|
||
->limit(12)
|
||
->get(['id', 'phone_number', 'target_extension', 'assigned_user_id', 'direction', 'status', 'received_at', 'metadata'])
|
||
->map(fn(CommunicationMessage $message): array=> [
|
||
'id' => (int) $message->id,
|
||
'received_at' => $message->received_at?->format('d/m/Y H:i:s') ?? '-',
|
||
'phone_number' => (string) ($message->phone_number ?? '-'),
|
||
'target_extension' => (string) ($message->target_extension ?? data_get($message->metadata, 'called_extension', '-')),
|
||
'direction' => (string) ($message->direction ?? '-'),
|
||
'status' => (string) ($message->status ?? '-'),
|
||
'event_type' => (string) data_get($message->metadata, 'event_type', '-'),
|
||
'provider' => (string) data_get($message->metadata, 'provider', $message->channel),
|
||
'user' => (string) ($message->assignedUser?->name ?? '-'),
|
||
'studio_role' => (string) data_get($message->metadata, 'studio_role', '-'),
|
||
'studio_mode' => (string) data_get($message->metadata, 'studio_mode', '-'),
|
||
])
|
||
->all();
|
||
}
|
||
|
||
/**
|
||
* @return array<int,array<string,string>>
|
||
*/
|
||
public function getPbxRoutingRowsProperty(): array
|
||
{
|
||
$settings = (array) Arr::get($this->amministratore->impostazioni ?? [], 'pbx', []);
|
||
$centralino = (array) Arr::get($this->amministratore->impostazioni ?? [], 'centralino', []);
|
||
$groupRows = array_values(array_filter((array) ($settings['response_groups'] ?? []), fn($row): bool => is_array($row)));
|
||
$incoming = array_values(array_filter((array) ($settings['incoming_lines'] ?? []), fn($row): bool => is_array($row)));
|
||
$routingRows = [
|
||
[
|
||
'type' => 'Studio',
|
||
'label' => 'Interno centralino',
|
||
'extension' => (string) Arr::get($centralino, 'interno_centralino', '-'),
|
||
'target' => 'Smistamento studio',
|
||
'note' => (string) Arr::get($centralino, 'numero_principale', '-'),
|
||
],
|
||
[
|
||
'type' => 'Studio',
|
||
'label' => 'Operatore',
|
||
'extension' => (string) Arr::get($centralino, 'interno_operatore', '-'),
|
||
'target' => 'Utente operatore',
|
||
'note' => 'Instradamento diretto',
|
||
],
|
||
[
|
||
'type' => 'Studio',
|
||
'label' => 'Amministratore',
|
||
'extension' => (string) Arr::get($centralino, 'interno_amministratore', '-'),
|
||
'target' => 'Utente amministratore',
|
||
'note' => 'Instradamento diretto',
|
||
],
|
||
[
|
||
'type' => 'Gruppo',
|
||
'label' => 'Gruppo giorno',
|
||
'extension' => (string) Arr::get($centralino, 'interno_gruppo_giorno', '-'),
|
||
'target' => 'Modalita giorno',
|
||
'note' => 'Routing shared',
|
||
],
|
||
[
|
||
'type' => 'Gruppo',
|
||
'label' => 'Gruppo notte',
|
||
'extension' => (string) Arr::get($centralino, 'interno_gruppo_notte', '-'),
|
||
'target' => 'Modalita notte',
|
||
'note' => 'Routing shared',
|
||
],
|
||
];
|
||
|
||
foreach ($groupRows as $row) {
|
||
$routingRows[] = [
|
||
'type' => 'Gruppo',
|
||
'label' => (string) ($row['label'] ?? 'Gruppo risposta'),
|
||
'extension' => (string) ($row['extension'] ?? '-'),
|
||
'target' => (string) ($row['target_extension'] ?? '-'),
|
||
'note' => (string) ($row['mode'] ?? '-'),
|
||
];
|
||
}
|
||
|
||
foreach ($incoming as $row) {
|
||
$routingRows[] = [
|
||
'type' => 'Linea',
|
||
'label' => (string) ($row['label'] ?? 'Linea entrante'),
|
||
'extension' => (string) ($row['number'] ?? '-'),
|
||
'target' => trim((string) (($row['route_group'] ?? '') !== '' ? $row['route_group'] : ($row['target_extension'] ?? '-'))),
|
||
'note' => (string) ($row['notes'] ?? '-'),
|
||
];
|
||
}
|
||
|
||
return $routingRows;
|
||
}
|
||
|
||
private function pbxRawMessagesQuery()
|
||
{
|
||
$settings = (array) Arr::get($this->amministratore->impostazioni ?? [], 'pbx', []);
|
||
$channels = $this->resolvePbxChannels($settings);
|
||
$extensions = $this->resolveWatchedPbxExtensions((array) Arr::get($this->amministratore->impostazioni ?? [], 'pbx', []));
|
||
|
||
return CommunicationMessage::query()
|
||
->whereIn('channel', $channels)
|
||
->whereIn('direction', ['inbound', 'outbound'])
|
||
->where(function ($q) use ($extensions): void {
|
||
$q->where('metadata->amministratore_id', (int) $this->amministratore->id);
|
||
|
||
if ($extensions !== [] && Schema::hasColumn('communication_messages', 'target_extension')) {
|
||
$q->orWhereIn('target_extension', $extensions);
|
||
}
|
||
});
|
||
}
|
||
|
||
private function pbxMessagesQuery()
|
||
{
|
||
return $this->pbxRawMessagesQuery()
|
||
->where(function ($q): void {
|
||
$q->whereNull('metadata->outcome')
|
||
->orWhere(function ($inner): void {
|
||
$inner->where('metadata->outcome', 'not like', '%miss%')
|
||
->where('metadata->outcome', 'not like', '%no_answer%');
|
||
});
|
||
})
|
||
->where(function ($q): void {
|
||
$q->whereNull('message_text')
|
||
->orWhere('message_text', 'not like', '%persa%');
|
||
});
|
||
}
|
||
|
||
/**
|
||
* @param array<string,mixed> $settings
|
||
* @return array<int,string>
|
||
*/
|
||
private function resolvePbxChannels(array $settings): array
|
||
{
|
||
$channel = mb_strtolower(trim((string) ($settings['channel'] ?? 'hybrid')));
|
||
|
||
return match ($channel) {
|
||
'panasonic', 'panasonic_csta', 'csta' => ['panasonic_csta'],
|
||
'smdr' => ['smdr'],
|
||
default => ['panasonic_csta', 'smdr'],
|
||
};
|
||
}
|
||
|
||
/**
|
||
* @return array<int,string>
|
||
*/
|
||
private function resolveWatchedPbxExtensions(array $settings): array
|
||
{
|
||
$centralinoExtensions = [
|
||
(string) Arr::get($this->amministratore->impostazioni ?? [], 'centralino.interno_centralino', ''),
|
||
(string) Arr::get($this->amministratore->impostazioni ?? [], 'centralino.interno_operatore', ''),
|
||
(string) Arr::get($this->amministratore->impostazioni ?? [], 'centralino.interno_amministratore', ''),
|
||
(string) Arr::get($this->amministratore->impostazioni ?? [], 'centralino.interno_gruppo_giorno', ''),
|
||
(string) Arr::get($this->amministratore->impostazioni ?? [], 'centralino.interno_gruppo_notte', ''),
|
||
];
|
||
|
||
$csvExtensions = preg_split('/\s*,\s*/', (string) ($settings['watch_extensions'] ?? ''), -1, PREG_SPLIT_NO_EMPTY) ?: [];
|
||
$groupExtensions = array_map(fn($row): string => (string) ($row['extension'] ?? ''), array_filter((array) ($settings['response_groups'] ?? []), fn($row): bool => is_array($row)));
|
||
$lineTargets = array_map(fn($row): string => (string) (($row['target_extension'] ?? '') !== '' ? $row['target_extension'] : ($row['route_group'] ?? '')), array_filter((array) ($settings['incoming_lines'] ?? []), fn($row): bool => is_array($row)));
|
||
|
||
return array_values(array_unique(array_filter(array_map(
|
||
fn($value): string => preg_replace('/\D+/', '', (string) $value) ?: '',
|
||
array_merge($centralinoExtensions, $csvExtensions, $groupExtensions, $lineTargets)
|
||
))));
|
||
}
|
||
|
||
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();
|
||
}
|
||
}
|