From 48b182cd4b0123b9d11d6051ade6981fc2032e21 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Sat, 7 Mar 2026 12:04:05 +0700 Subject: [PATCH] feat: Implement decimal precision for quantities and monetary values across feed purchases, delayed goods, and related calculations. --- .../Resources/Manage/Orders/OrderResource.php | 104 ++++++++++++------ app/Models/Order.php | 13 ++- app/Observers/OrderObserver.php | 65 +++++++++++ .../2026_02_18_000004_create_orders_table.php | 7 +- 4 files changed, 152 insertions(+), 37 deletions(-) create mode 100644 app/Observers/OrderObserver.php diff --git a/app/Filament/Resources/Manage/Orders/OrderResource.php b/app/Filament/Resources/Manage/Orders/OrderResource.php index fd8df32..2daa650 100644 --- a/app/Filament/Resources/Manage/Orders/OrderResource.php +++ b/app/Filament/Resources/Manage/Orders/OrderResource.php @@ -11,6 +11,7 @@ use App\Filament\Columns\TimestampColumns; use App\Filament\Resources\Manage\Orders\Pages\ManageOrders; use App\Models\Order; +use App\Models\Warehouse; use BackedEnum; use Filament\Actions\BulkActionGroup; use Filament\Forms\Components\DatePicker; @@ -46,17 +47,39 @@ class OrderResource extends Resource public static function form(Schema $schema): Schema { - $recalculateTotal = function (callable $get, callable $set): void { - $qty = $get('egg_quantity') ?? 0; - $quantity = str_contains((string) $qty, ',') - ? (float) str_replace(',', '.', str_replace('.', '', (string) $qty)) - : (float) $qty; + $unmaskQty = function ($state) { + if (blank($state)) { + return 0; + } - $price = $get('unit_price') ?? 0; - $unitPrice = (int) str_replace('.', '', (string) $price); + if (is_int($state) || is_float($state)) { + return $state; + } - $discountRaw = $get('discount') ?? 0; - $discount = (int) str_replace('.', '', (string) $discountRaw); + return (float) str($state) + ->replace(',', '.') + ->toString(); + }; + + $unmaskMoney = function ($state) { + if (blank($state)) { + return 0; + } + + if (is_int($state) || is_float($state)) { + return $state; + } + + return (float) str($state) + ->replace('.', '') + ->replace(',', '.') + ->toString(); + }; + + $recalculateTotal = function (callable $get, callable $set) use ($unmaskMoney, $unmaskQty): void { + $quantity = $unmaskQty($get('egg_quantity')); + $unitPrice = $unmaskMoney($get('unit_price')); + $discount = $unmaskMoney($get('discount')); $subtotal = $quantity * $unitPrice; $totalAmount = max(0, $subtotal - $discount); @@ -66,19 +89,32 @@ public static function form(Schema $schema): Schema return $schema ->components([ - DatePicker::make('order_date') - ->label('Tanggal Order') - ->default(now()) - ->required() - ->native(false) - ->displayFormat('l, d F Y'), + Grid::make(3) + ->schema([ + DatePicker::make('order_date') + ->label('Tanggal Order') + ->default(now()) + ->required() + ->native(false) + ->displayFormat('l, d F Y'), - Select::make('customer_id') - ->label('Pelanggan') - ->relationship('customer', 'name') - ->searchable() - ->preload() - ->native(false), + Select::make('warehouse_id') + ->label('Ambil Dari Gudang') + ->relationship('warehouse', 'name') + ->searchable() + ->preload() + ->native(false) + ->required() + ->default(fn () => Warehouse::first()?->id), + + Select::make('customer_id') + ->label('Pelanggan') + ->relationship('customer', 'name') + ->searchable() + ->preload() + ->native(false), + ]) + ->columnSpanFull(), Grid::make(3) ->schema([ @@ -93,9 +129,8 @@ public static function form(Schema $schema): Schema $recalculateTotal($get, $set); }) ->currencyMask(thousandSeparator: '.', decimalSeparator: ',', precision: 2) - ->dehydrateStateUsing(fn ($state) => str_contains((string) $state, ',') - ? (float) str_replace(',', '.', str_replace('.', '', (string) $state)) - : (float) $state), + ->formatStateUsing(fn ($state) => $state === null ? null : (float) $state) + ->dehydrateStateUsing($unmaskQty), TextInput::make('unit_price') ->label('Harga per Satuan') @@ -105,7 +140,7 @@ public static function form(Schema $schema): Schema ->required() ->live() ->currencyMask(thousandSeparator: '.', decimalSeparator: ',', precision: 0) - ->dehydrateStateUsing(fn ($state) => (int) str()->replace('.', '', (string) ($state ?? 0))) + ->dehydrateStateUsing($unmaskMoney) ->afterStateUpdated(function ($state, callable $get, callable $set) use ($recalculateTotal): void { $recalculateTotal($get, $set); }), @@ -123,6 +158,7 @@ public static function form(Schema $schema): Schema TextInput::make('discount') ->label('Diskon') ->placeholder('0') + ->autocomplete(false) ->default(0) ->prefix('Rp') ->live() @@ -130,7 +166,7 @@ public static function form(Schema $schema): Schema $recalculateTotal($get, $set); }) ->currencyMask(thousandSeparator: '.', decimalSeparator: ',', precision: 0) - ->dehydrateStateUsing(fn ($state) => (int) str()->replace('.', '', (string) ($state ?? 0))), + ->dehydrateStateUsing($unmaskMoney), TextInput::make('total_amount') ->label('Total') @@ -138,10 +174,8 @@ public static function form(Schema $schema): Schema ->readOnly() ->prefix('Rp') ->required() - ->formatStateUsing( - fn ($state) => number_format((float) ($state ?? 0), 0, ',', '.') - ) - ->dehydrateStateUsing(fn ($state) => (float) str_replace('.', '', (string) ($state ?? 0))), + ->formatStateUsing(fn ($state) => number_format(($state ?? 0), 0, ',', '.')) + ->dehydrateStateUsing($unmaskMoney), Textarea::make('notes') ->label('Catatan') @@ -166,10 +200,16 @@ public static function table(Table $table): Table ->searchable() ->sortable(), + TextColumn::make('warehouse.name') + ->label('Gudang') + ->searchable() + ->sortable(), + TextColumn::make('egg_quantity') ->label('Jumlah') - ->suffix(fn ($record) => ' '.$record->unit?->alias) - ->numeric(decimalPlaces: 2, decimalSeparator: ',', thousandsSeparator: '.') + ->getStateUsing(function ($record) { + return str_replace('.', ',', (float) $record->egg_quantity)." {$record->unit?->alias}"; + }) ->sortable(), TextColumn::make('unit_price') diff --git a/app/Models/Order.php b/app/Models/Order.php index 20e7c19..34294c8 100644 --- a/app/Models/Order.php +++ b/app/Models/Order.php @@ -2,11 +2,15 @@ namespace App\Models; +use App\Observers\OrderObserver; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\SoftDeletes; +#[ObservedBy(OrderObserver::class)] + class Order extends Model { use HasFactory, SoftDeletes; @@ -19,8 +23,8 @@ protected function casts(): array 'order_date' => 'date', 'egg_quantity' => 'decimal:2', 'unit_price' => 'integer', - 'total_amount' => 'decimal:2', - 'discount' => 'decimal:2', + 'total_amount' => 'integer', + 'discount' => 'integer', ]; } @@ -33,4 +37,9 @@ public function unit(): BelongsTo { return $this->belongsTo(Unit::class); } + + public function warehouse(): BelongsTo + { + return $this->belongsTo(Warehouse::class); + } } diff --git a/app/Observers/OrderObserver.php b/app/Observers/OrderObserver.php new file mode 100644 index 0000000..1f4323a --- /dev/null +++ b/app/Observers/OrderObserver.php @@ -0,0 +1,65 @@ +warehouse_id) { + WarehouseStock::adjustStock( + $order->warehouse_id, + $order->unit_id, + false, // Assume sold eggs are not broken + -$order->egg_quantity + ); + } + } + + /** + * Handle the Order "updated" event. + */ + public function updated(Order $order): void + { + // Revert old stock if warehouse_id, unit_id, egg_quantity, or deleted_at changed + if ($order->isDirty(['warehouse_id', 'unit_id', 'egg_quantity'])) { + $oldWarehouseId = $order->getOriginal('warehouse_id'); + $oldUnitId = $order->getOriginal('unit_id'); + $oldQuantity = $order->getOriginal('egg_quantity'); + + if ($oldWarehouseId) { + WarehouseStock::adjustStock($oldWarehouseId, $oldUnitId, false, $oldQuantity); + } + + if ($order->warehouse_id) { + WarehouseStock::adjustStock($order->warehouse_id, $order->unit_id, false, -$order->egg_quantity); + } + } + } + + /** + * Handle the Order "deleted" event. + */ + public function deleted(Order $order): void + { + if (! $order->isForceDeleting() && $order->warehouse_id) { + WarehouseStock::adjustStock($order->warehouse_id, $order->unit_id, false, $order->egg_quantity); + } + } + + /** + * Handle the Order "restored" event. + */ + public function restored(Order $order): void + { + if ($order->warehouse_id) { + WarehouseStock::adjustStock($order->warehouse_id, $order->unit_id, false, -$order->egg_quantity); + } + } +} diff --git a/database/migrations/2026_02_18_000004_create_orders_table.php b/database/migrations/2026_02_18_000004_create_orders_table.php index 6f53823..aaaab0d 100644 --- a/database/migrations/2026_02_18_000004_create_orders_table.php +++ b/database/migrations/2026_02_18_000004_create_orders_table.php @@ -11,13 +11,14 @@ public function up(): void Schema::create('orders', function (Blueprint $table) { $table->id(); $table->foreignId('customer_id')->nullable()->constrained()->cascadeOnDelete(); + $table->foreignId('warehouse_id')->nullable()->constrained()->nullOnDelete(); $table->foreignId('unit_id')->constrained()->cascadeOnDelete(); - $table->decimal('egg_quantity', 10, 2); + $table->decimal('egg_quantity', 12, 2); $table->unsignedInteger('unit_price'); $table->date('order_date'); $table->string('status', 20)->default('pending'); - $table->decimal('total_amount', 15, 2); - $table->decimal('discount', 15, 2)->default(0); + $table->unsignedBigInteger('total_amount'); + $table->unsignedBigInteger('discount')->default(0); $table->text('notes')->nullable(); $table->timestamp('created_at')->useCurrent(); $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();