feat: implement discount and transport features in the quote system
- Added maximum discount settings configurable by the administrator. - Implemented discount calculations in the quote confirmation process. - Introduced transport options with associated costs, including handling for exceeding maximum piece limits. - Updated frontend to display discounts and transport information in the quote summary. - Created new database migrations for storing discount and transport settings. - Developed a new Filament page for managing global discount settings. - Enhanced email templates to include discount and transport details. - Added tests to ensure proper functionality of discounts and transport calculations.
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use App\Models\Impostazione;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Page;
|
||||
|
||||
/**
|
||||
* Regola base di scontistica: sconti massimi che ogni cliente può applicare al preventivo,
|
||||
* salvo massimi personalizzati sulla sua scheda.
|
||||
*
|
||||
* @property Form $form
|
||||
*/
|
||||
class Scontistica extends Page implements HasForms
|
||||
{
|
||||
use InteractsWithForms;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-receipt-percent';
|
||||
|
||||
protected static ?string $navigationLabel = 'Scontistica';
|
||||
|
||||
protected static ?string $title = 'Scontistica globale';
|
||||
|
||||
protected static ?int $navigationSort = 3;
|
||||
|
||||
protected static string $view = 'filament.pages.scontistica';
|
||||
|
||||
/** @var array<string, mixed> */
|
||||
public ?array $data = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->form->fill(Impostazione::scontiBase());
|
||||
}
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->statePath('data')
|
||||
->schema([
|
||||
Forms\Components\Section::make('Regola base')
|
||||
->description('Sconti massimi che i clienti possono applicare al preventivo prima dell\'invio: il 1° sull\'imponibile, lo sconto extra sul totale già scontato. Vale per tutti i clienti, salvo quelli con massimi personalizzati o sconti disabilitati nella loro scheda.')
|
||||
->columns(2)
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('max_1')
|
||||
->label('1° sconto massimo')
|
||||
->numeric()
|
||||
->required()
|
||||
->minValue(0)
|
||||
->maxValue(100)
|
||||
->step(0.01)
|
||||
->suffix('%'),
|
||||
Forms\Components\TextInput::make('max_2')
|
||||
->label('Sconto extra massimo')
|
||||
->numeric()
|
||||
->required()
|
||||
->minValue(0)
|
||||
->maxValue(100)
|
||||
->step(0.01)
|
||||
->suffix('%'),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$dati = $this->form->getState();
|
||||
|
||||
Impostazione::imposta('sconti.max_1', (float) $dati['max_1']);
|
||||
Impostazione::imposta('sconti.max_2', (float) $dati['max_2']);
|
||||
|
||||
Notification::make()->title('Scontistica salvata')->success()->send();
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\ClienteResource\Pages;
|
||||
use App\Models\Cliente;
|
||||
use App\Models\Impostazione;
|
||||
use App\Support\ClienteCredenziali;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
@@ -63,6 +64,36 @@ class ClienteResource extends Resource
|
||||
? '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('Scontistica')
|
||||
->description(fn () => sprintf(
|
||||
'Sconti che il cliente può applicare al preventivo prima dell\'invio. Lascia vuoto per la regola base (1° sconto fino al %s%%, sconto extra fino al %s%% sul totale già scontato).',
|
||||
static::percentuale(Impostazione::scontiBase()['max_1']),
|
||||
static::percentuale(Impostazione::scontiBase()['max_2']),
|
||||
))
|
||||
->columns(2)
|
||||
->schema([
|
||||
Forms\Components\Toggle::make('sconti_abilitati')
|
||||
->label('Può applicare sconti')
|
||||
->default(true)
|
||||
->live()
|
||||
->columnSpanFull(),
|
||||
Forms\Components\TextInput::make('sconto_max_1')
|
||||
->label('1° sconto massimo')
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->maxValue(100)
|
||||
->suffix('%')
|
||||
->placeholder(fn () => static::percentuale(Impostazione::scontiBase()['max_1']).' (regola base)')
|
||||
->visible(fn (Forms\Get $get) => (bool) $get('sconti_abilitati')),
|
||||
Forms\Components\TextInput::make('sconto_max_2')
|
||||
->label('Sconto extra massimo')
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->maxValue(100)
|
||||
->suffix('%')
|
||||
->placeholder(fn () => static::percentuale(Impostazione::scontiBase()['max_2']).' (regola base)')
|
||||
->visible(fn (Forms\Get $get) => (bool) $get('sconti_abilitati')),
|
||||
]),
|
||||
Forms\Components\Section::make('Anagrafica')
|
||||
->description('Campi facoltativi.')
|
||||
->columns(2)
|
||||
@@ -124,6 +155,19 @@ class ClienteResource extends Resource
|
||||
->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\TextEntry::make('scontistica')
|
||||
->label('Scontistica')
|
||||
->state(function (Cliente $record): string {
|
||||
if (! $record->sconti_abilitati) {
|
||||
return 'Disabilitata';
|
||||
}
|
||||
$massimi = $record->scontiMassimi();
|
||||
$base = $record->sconto_max_1 === null && $record->sconto_max_2 === null ? ' (regola base)' : '';
|
||||
|
||||
return sprintf('Fino al %s%% + extra fino al %s%%%s',
|
||||
static::percentuale($massimi['max_1']), static::percentuale($massimi['max_2']), $base);
|
||||
})
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
Infolists\Components\Section::make('Anagrafica')
|
||||
->columns(3)
|
||||
@@ -140,6 +184,12 @@ class ClienteResource extends Resource
|
||||
]);
|
||||
}
|
||||
|
||||
/** 50.00 -> "50", 12.50 -> "12,5" */
|
||||
private static function percentuale(mixed $valore): string
|
||||
{
|
||||
return rtrim(rtrim(number_format((float) $valore, 2, ',', ''), '0'), ',');
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -145,7 +145,28 @@ class PreventivoResource extends Resource
|
||||
->label('Stato')
|
||||
->badge()
|
||||
->formatStateUsing(fn (string $state) => Preventivo::STATI[$state] ?? $state),
|
||||
Infolists\Components\TextEntry::make('totale')->label('Totale')->money('EUR'),
|
||||
Infolists\Components\TextEntry::make('subtotale')
|
||||
->label('Totale parziale')
|
||||
->money('EUR')
|
||||
->visible(fn (Preventivo $record) => $record->haSconti()),
|
||||
Infolists\Components\TextEntry::make('sconti')
|
||||
->label('Sconti applicati')
|
||||
->visible(fn (Preventivo $record) => $record->haSconti())
|
||||
->state(function (Preventivo $record): string {
|
||||
$importi = $record->importiSconto();
|
||||
$euro = fn (float $v) => '€ '.number_format($v, 2, ',', '.');
|
||||
|
||||
return sprintf('1° sconto %s%% (−%s) · extra %s%% (−%s)',
|
||||
(float) $record->sconto_1, $euro($importi['sconto_1']),
|
||||
(float) $record->sconto_2, $euro($importi['sconto_2']));
|
||||
}),
|
||||
Infolists\Components\TextEntry::make('trasporto_etichetta')
|
||||
->label('Trasporto')
|
||||
->visible(fn (Preventivo $record) => $record->trasporto !== null)
|
||||
->formatStateUsing(fn (string $state, Preventivo $record) => $record->trasportoDaQuantificare()
|
||||
? "{$state} — da quantificare"
|
||||
: $state.' — € '.number_format($record->trasporto_importo, 2, ',', '.')),
|
||||
Infolists\Components\TextEntry::make('totale')->label('Totale (IVA esclusa)')->money('EUR'),
|
||||
Infolists\Components\TextEntry::make('confirmed_at')->label('Confermato il')->dateTime('d/m/Y H:i')->placeholder('—'),
|
||||
Infolists\Components\TextEntry::make('created_at')->label('Creato il')->dateTime('d/m/Y H:i'),
|
||||
]),
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Filament\Resources\PreventivoResource\Pages;
|
||||
use App\Filament\Resources\PreventivoResource;
|
||||
use App\Support\PreventivoPdf;
|
||||
use Filament\Actions;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
@@ -38,6 +39,21 @@ class ViewPreventivo extends ViewRecord
|
||||
'preventivo-'.$this->record->numero.'.pdf'
|
||||
);
|
||||
}),
|
||||
// Il PDF è salvato alla conferma e poi riservito così com'è: dopo modifiche al
|
||||
// template o ai dati va rigenerato.
|
||||
Actions\Action::make('rigeneraPdf')
|
||||
->label('Rigenera PDF')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->color('gray')
|
||||
->visible(fn () => $this->record->stato !== 'bozza')
|
||||
->requiresConfirmation()
|
||||
->modalDescription('Il PDF verrà ricreato con il modello e i dati attuali, sostituendo quello salvato.')
|
||||
->action(function () {
|
||||
$this->record->pdf_path = PreventivoPdf::genera($this->record);
|
||||
$this->record->save();
|
||||
|
||||
Notification::make()->title('PDF rigenerato')->success()->send();
|
||||
}),
|
||||
Actions\EditAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user