feat: Implement client authentication and pre-fill functionality

- Added AuthController for client login and token management.
- Introduced AuthGate for handling authentication state and login UI.
- Updated API to support fetching client data for pre-filling forms.
- Modified App.vue to pre-fill client information if logged in.
- Enhanced client model to include username and password fields.
- Created migration for adding username and password to clients table.
- Added utility for generating secure usernames and passwords.
- Updated frontend to handle client tokens for pre-filling checkout forms.
- Removed old CSS file and updated HTML references to new assets.
- Added StatsOverview widget for displaying client and quote statistics.
- Improved error handling and user feedback in the login process.
This commit is contained in:
2026-08-24 17:24:46 +02:00
parent 1c40ef6601
commit d6a9978167
23 changed files with 666 additions and 54 deletions
@@ -4,8 +4,11 @@ namespace App\Filament\Resources;
use App\Filament\Resources\ClienteResource\Pages;
use App\Models\Cliente;
use App\Support\ClienteCredenziali;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Infolists;
use Filament\Infolists\Infolist;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
@@ -27,16 +30,54 @@ class ClienteResource extends Resource
public static function form(Form $form): Form
{
return $form->schema([
Forms\Components\TextInput::make('nome')->label('Nome')->required(),
Forms\Components\TextInput::make('cognome')->label('Cognome'),
Forms\Components\TextInput::make('ragione_sociale')->label('Ragione sociale'),
Forms\Components\TextInput::make('email')->label('Email')->email()->required(),
Forms\Components\TextInput::make('telefono')->label('Telefono')->tel(),
Forms\Components\TextInput::make('indirizzo')->label('Indirizzo'),
Forms\Components\TextInput::make('cap')->label('CAP')->maxLength(10),
Forms\Components\TextInput::make('citta')->label('Città'),
Forms\Components\TextInput::make('provincia')->label('Provincia')->maxLength(5),
Forms\Components\Textarea::make('note')->label('Note')->columnSpanFull(),
Forms\Components\Section::make('Accesso')
->description('Credenziali per l\'accesso del cliente. Generate automaticamente, ma modificabili.')
->columns(2)
->schema([
Forms\Components\TextInput::make('username')
->label('Nome utente')
->required()
->maxLength(50)
->unique(ignoreRecord: true)
->default(fn (string $operation) => $operation === 'create' ? ClienteCredenziali::generaUsername() : null)
->suffixAction(
Forms\Components\Actions\Action::make('rigeneraUsername')
->icon('heroicon-m-arrow-path')
->tooltip('Genera un nuovo nome utente')
->action(fn (Forms\Set $set) => $set('username', ClienteCredenziali::generaUsername()))
),
Forms\Components\TextInput::make('password')
->label('Password')
->password()
->revealable()
->required(fn (string $operation) => $operation === 'create')
->dehydrated(fn (?string $state) => filled($state))
->default(fn (string $operation) => $operation === 'create' ? ClienteCredenziali::generaPassword() : null)
->suffixAction(
Forms\Components\Actions\Action::make('rigeneraPassword')
->icon('heroicon-m-arrow-path')
->tooltip('Genera una nuova password')
->action(fn (Forms\Set $set) => $set('password', ClienteCredenziali::generaPassword()))
)
->helperText(fn (string $operation) => $operation === 'create'
? 'Generata automaticamente. Ricordati di copiarla dopo il salvataggio: non sarà più leggibile.'
: 'Lascia vuoto per mantenere la password attuale, oppure genera/inserisci una nuova password.'),
]),
Forms\Components\Section::make('Anagrafica')
->description('Campi facoltativi.')
->columns(2)
->schema([
Forms\Components\TextInput::make('nome')->label('Nome'),
Forms\Components\TextInput::make('cognome')->label('Cognome'),
Forms\Components\TextInput::make('ragione_sociale')->label('Ragione sociale'),
Forms\Components\TextInput::make('email')->label('Email')->email(),
Forms\Components\TextInput::make('telefono')->label('Telefono')->tel(),
Forms\Components\TextInput::make('indirizzo')->label('Indirizzo'),
Forms\Components\TextInput::make('cap')->label('CAP')->maxLength(10),
Forms\Components\TextInput::make('citta')->label('Città'),
Forms\Components\TextInput::make('provincia')->label('Provincia')->maxLength(5),
Forms\Components\Textarea::make('note')->label('Note')->columnSpanFull(),
]),
]);
}
@@ -46,13 +87,15 @@ class ClienteResource extends Resource
->defaultSort('created_at', 'desc')
->columns([
Tables\Columns\TextColumn::make('nominativo')->label('Nominativo')->searchable(['nome', 'cognome', 'ragione_sociale']),
Tables\Columns\TextColumn::make('email')->label('Email')->searchable()->copyable(),
Tables\Columns\TextColumn::make('username')->label('Nome utente')->searchable()->placeholder('—'),
Tables\Columns\TextColumn::make('email')->label('Email')->searchable()->copyable()->placeholder('—'),
Tables\Columns\TextColumn::make('telefono')->label('Telefono')->placeholder('—'),
Tables\Columns\TextColumn::make('citta')->label('Città')->placeholder('—'),
Tables\Columns\TextColumn::make('preventivi_count')->label('Preventivi')->counts('preventivi'),
Tables\Columns\TextColumn::make('created_at')->label('Registrato')->dateTime('d/m/Y')->sortable(),
])
->actions([
Tables\Actions\ViewAction::make(),
Tables\Actions\EditAction::make(),
])
->bulkActions([
@@ -62,10 +105,42 @@ class ClienteResource extends Resource
]);
}
public static function infolist(Infolist $infolist): Infolist
{
return $infolist->schema([
Infolists\Components\View::make('filament.infolists.cliente-password-reveal')
->visible(fn () => filled(session('cliente_password_plain')))
->viewData(fn () => [
'username' => session('cliente_username_plain'),
'password' => session('cliente_password_plain'),
]),
Infolists\Components\Section::make('Accesso')
->columns(2)
->schema([
Infolists\Components\TextEntry::make('username')->label('Nome utente')->copyable()->placeholder('—'),
Infolists\Components\TextEntry::make('created_at')->label('Cliente dal')->dateTime('d/m/Y H:i'),
]),
Infolists\Components\Section::make('Anagrafica')
->columns(3)
->schema([
Infolists\Components\TextEntry::make('nominativo')->label('Nominativo')->placeholder('—'),
Infolists\Components\TextEntry::make('email')->label('Email')->copyable()->placeholder('—'),
Infolists\Components\TextEntry::make('telefono')->label('Telefono')->placeholder('—'),
Infolists\Components\TextEntry::make('indirizzo')->label('Indirizzo')->placeholder('—'),
Infolists\Components\TextEntry::make('cap')->label('CAP')->placeholder('—'),
Infolists\Components\TextEntry::make('citta')->label('Città')->placeholder('—'),
Infolists\Components\TextEntry::make('provincia')->label('Provincia')->placeholder('—'),
Infolists\Components\TextEntry::make('note')->label('Note')->columnSpanFull()->placeholder('—'),
]),
]);
}
public static function getPages(): array
{
return [
'index' => Pages\ListClientes::route('/'),
'create' => Pages\CreateCliente::route('/create'),
'view' => Pages\ViewCliente::route('/{record}'),
'edit' => Pages\EditCliente::route('/{record}/edit'),
];
}