diff --git a/app/Filament/Resources/DelayedGoods/DelayedGoodResource.php b/app/Filament/Resources/DelayedGoods/DelayedGoodResource.php new file mode 100644 index 0000000..9c39989 --- /dev/null +++ b/app/Filament/Resources/DelayedGoods/DelayedGoodResource.php @@ -0,0 +1,58 @@ + ListDelayedGoods::route('/'), + ]; + } + + public static function getRecordRouteBindingEloquentQuery(): Builder + { + return parent::getRecordRouteBindingEloquentQuery() + ->withoutGlobalScopes([ + SoftDeletingScope::class, + ]); + } +} diff --git a/app/Filament/Resources/DelayedGoods/Pages/ListDelayedGoods.php b/app/Filament/Resources/DelayedGoods/Pages/ListDelayedGoods.php new file mode 100644 index 0000000..7bd0e2e --- /dev/null +++ b/app/Filament/Resources/DelayedGoods/Pages/ListDelayedGoods.php @@ -0,0 +1,22 @@ +label('Tambah'), + ]; + } +} diff --git a/app/Filament/Resources/DelayedGoods/Schemas/DelayedGoodForm.php b/app/Filament/Resources/DelayedGoods/Schemas/DelayedGoodForm.php new file mode 100644 index 0000000..c5bee1e --- /dev/null +++ b/app/Filament/Resources/DelayedGoods/Schemas/DelayedGoodForm.php @@ -0,0 +1,217 @@ +replace('.', '', (string) ($get('quantity') ?? 0)); + $unitPrice = (int) str()->replace('.', '', (string) ($get('unit_price') ?? 0)); + $totalAmount = $quantity * $unitPrice; + $set('total_amount', number_format($totalAmount, 0, ',', '.')); + }; + + return $schema->components([ + Hidden::make('unit_id') + ->required(), + + Grid::make(2) + ->schema([ + Select::make('egg_price_id') + ->label('Harga Telur') + ->options(EggPrice::query()->with('unit')->get()->mapWithKeys(function ($item) { + return [$item->id => $item->name.' ('.($item->unit?->alias ?? '-').')']; + })) + ->searchable() + ->preload() + ->native(false) + ->live() + ->afterStateHydrated(function ($state, callable $set, callable $get, $record): void { + if ($record && ! $state) { + $eggPrice = EggPrice::where('unit_id', $record->unit_id) + ->where('price', $record->unit_price) + ->first(); + + if ($eggPrice) { + $set('egg_price_id', $eggPrice->id); + } + } + }) + ->afterStateUpdated(function ($state, callable $set, callable $get) use ($recalculateTotal): void { + if (! $state) { + $set('unit_price', 0); + $set('unit_id', null); + $recalculateTotal($get, $set); + + return; + } + + $price = EggPrice::with('unit')->find($state); + + $set('unit_price', number_format($price?->price ?? 0, 0, ',', '.')); + $set('unit_id', $price?->unit_id); + + $recalculateTotal($get, $set); + }) + ->dehydrated(false) + ->required(), + + Select::make('customer_id') + ->label('Pelanggan') + ->relationship('customer', 'name') + ->searchable() + ->preload() + ->native(false) + ->required(), + + DatePicker::make('stored_date') + ->label('Tanggal Penyimpanan') + ->default(now()) + ->required() + ->native(false) + ->displayFormat('l, d F Y'), + + TextInput::make('quantity') + ->label('Jumlah Barang') + ->placeholder('0') + ->autocomplete(false) + ->required() + ->live() + ->afterStateUpdated(function ($state, callable $get, callable $set) use ($recalculateTotal): void { + $recalculateTotal($get, $set); + }) + ->currencyMask(thousandSeparator: '.', decimalSeparator: ',', precision: 0) + ->dehydrateStateUsing(fn ($state) => (int) str()->replace('.', '', (string) ($state ?? 0))), + ]), + + Grid::make(2) + ->schema([ + TextInput::make('unit_price') + ->label('Harga per Satuan') + ->placeholder('0') + ->readOnly() + ->prefix('Rp') + ->required() + ->formatStateUsing( + fn ($state) => number_format((int) ($state ?? 0), 0, ',', '.') + ) + ->dehydrateStateUsing(fn ($state) => (int) str()->replace('.', '', (string) ($state ?? 0))) + ->afterStateUpdated(function ($state, callable $get, callable $set) use ($recalculateTotal): void { + $recalculateTotal($get, $set); + }), + + TextInput::make('total_amount') + ->label('Total') + ->placeholder('0') + ->readOnly() + ->prefix('Rp') + ->required() + ->live() + ->formatStateUsing( + fn ($state) => number_format((int) ($state ?? 0), 0, ',', '.') + ) + ->dehydrateStateUsing(fn ($state) => (int) str()->replace('.', '', (string) ($state ?? 0))), + ]), + + Grid::make(3) + ->schema([ + TextInput::make('paid_amount') + ->label('Jumlah yang Sudah Dibayar (DP)') + ->placeholder('0') + ->default(0) + ->autocomplete(false) + ->required() + ->live() + ->prefix('Rp') + ->readOnly(fn (callable $get) => $get('is_paid')) + ->afterStateUpdated(function ($state, callable $get, callable $set): void { + $paidAmount = (int) str()->replace('.', '', (string) ($state ?? 0)); + $totalAmount = (int) str()->replace('.', '', (string) ($get('total_amount') ?? 0)); + + // Update is_paid based on paid_amount (hanya jika tidak dari toggle) + if (! $get('is_paid')) { + $set('is_paid', $paidAmount >= $totalAmount && $totalAmount > 0); + } + + if ($paidAmount >= $totalAmount && $totalAmount > 0 && ! $get('payment_date')) { + $set('payment_date', now()); + } + + if ($paidAmount < $totalAmount) { + $set('payment_date', null); + } + }) + ->currencyMask(thousandSeparator: '.', decimalSeparator: ',', precision: 0) + ->dehydrateStateUsing(fn ($state) => (int) str()->replace('.', '', (string) ($state ?? 0))) + ->formatStateUsing( + fn ($state) => number_format((int) ($state ?? 0), 0, ',', '.') + ), + + TextInput::make('remaining_amount') + ->label('Sisa Pembayaran') + ->placeholder('0') + ->readOnly() + ->prefix('Rp') + ->default(0) + ->formatStateUsing(function ($state, callable $get) { + $paidAmount = (int) str()->replace('.', '', (string) ($get('paid_amount') ?? 0)); + $totalAmount = (int) str()->replace('.', '', (string) ($get('total_amount') ?? 0)); + $remaining = max(0, $totalAmount - $paidAmount); + + return number_format($remaining, 0, ',', '.'); + }) + ->dehydrated(false) + ->live(), + + Toggle::make('is_paid') + ->label('Lunas') + ->default(false) + ->live() + ->afterStateUpdated(function ($state, callable $get, callable $set): void { + $totalAmount = (int) str()->replace('.', '', (string) ($get('total_amount') ?? 0)); + + if ($state) { + $set('paid_amount', $totalAmount); + + if (! $get('payment_date')) { + $set('payment_date', now()); + } + } else { + $set('paid_amount', 0); + $set('payment_date', null); + } + }), + + DatePicker::make('payment_date') + ->label('Tanggal Pembayaran Lunas') + ->native(false) + ->displayFormat('l, d F Y') + ->visible(function (callable $get) { + $paidAmount = (int) str()->replace('.', '', (string) ($get('paid_amount') ?? 0)); + $totalAmount = (int) str()->replace('.', '', (string) ($get('total_amount') ?? 0)); + + return $paidAmount >= $totalAmount && $totalAmount > 0; + }), + ]), + + Textarea::make('notes') + ->label('Catatan') + ->placeholder('...') + ->autocomplete(false) + ->columnSpanFull(), + ]) + ->columns(1); + } +} diff --git a/app/Filament/Resources/DelayedGoods/Tables/DelayedGoodsTable.php b/app/Filament/Resources/DelayedGoods/Tables/DelayedGoodsTable.php new file mode 100644 index 0000000..4881d0d --- /dev/null +++ b/app/Filament/Resources/DelayedGoods/Tables/DelayedGoodsTable.php @@ -0,0 +1,111 @@ +columns([ + TextColumn::make('stored_date') + ->label('Tanggal Penyimpanan') + ->date('l, d F Y') + ->sortable(), + + TextColumn::make('customer.name') + ->label('Pelanggan') + ->searchable() + ->sortable(), + + TextColumn::make('quantity') + ->label('Jumlah') + ->suffix(fn ($record) => ' '.$record->unit?->alias) + ->numeric() + ->sortable(), + + TextColumn::make('unit_price') + ->label('Harga per Satuan') + ->money('IDR', decimalPlaces: 0) + ->sortable(), + + TextColumn::make('total_amount') + ->label('Total') + ->money('IDR', decimalPlaces: 0) + ->sortable(), + + TextColumn::make('paid_amount') + ->label('Sudah Dibayar') + ->money('IDR', decimalPlaces: 0) + ->sortable() + ->color(fn ($record) => $record->paid_amount >= $record->total_amount ? 'success' : 'warning'), + + TextColumn::make('remaining_amount') + ->label('Sisa') + ->getStateUsing(fn ($record) => max(0, $record->total_amount - $record->paid_amount)) + ->money('IDR', decimalPlaces: 0) + ->sortable() + ->color(fn ($record) => $record->paid_amount >= $record->total_amount ? 'success' : 'danger'), + + IconColumn::make('is_paid') + ->label('Status') + ->boolean() + ->trueIcon(Heroicon::CheckCircle) + ->falseIcon(Heroicon::XCircle) + ->trueColor('success') + ->falseColor('warning') + ->sortable(), + + TextColumn::make('notes') + ->label('Catatan') + ->limit(30) + ->toggleable(isToggledHiddenByDefault: true), + + TextColumn::make('payment_date') + ->label('Tanggal Lunas') + ->date('l, d F Y') + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + + TextColumn::make('created_at') + ->label('Dibuat') + ->dateTime('d/m/Y H:i') + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + TrashedFilter::make() + ->native(false), + ]) + ->recordActions([ + EditAction::make() + ->label('Ubah'), + + DeleteAction::make(), + + ForceDeleteAction::make(), + + RestoreAction::make(), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + ...DefaultBulkActions::make('Barang Tertunda'), + ]), + ]) + ->emptyStateIcon(Heroicon::Clock) + ->emptyStateDescription('Setelah Anda membuat data pertama, maka akan muncul disini.') + ->defaultSort('created_at', 'desc') + ->deferFilters(false); + } +} diff --git a/app/Models/DelayedGood.php b/app/Models/DelayedGood.php new file mode 100644 index 0000000..e61e508 --- /dev/null +++ b/app/Models/DelayedGood.php @@ -0,0 +1,38 @@ + 'date', + 'payment_date' => 'date', + 'quantity' => 'integer', + 'unit_price' => 'integer', + 'total_amount' => 'integer', + 'paid_amount' => 'integer', + 'is_paid' => 'boolean', + ]; + + public function customer(): BelongsTo + { + return $this->belongsTo(Customer::class); + } + + public function unit(): BelongsTo + { + return $this->belongsTo(Unit::class); + } +} diff --git a/app/Observers/DelayedGoodObserver.php b/app/Observers/DelayedGoodObserver.php new file mode 100644 index 0000000..5f3a6bb --- /dev/null +++ b/app/Observers/DelayedGoodObserver.php @@ -0,0 +1,24 @@ +is_paid = $delayedGood->paid_amount >= $delayedGood->total_amount; + + if ($delayedGood->is_paid && ! $delayedGood->payment_date) { + $delayedGood->payment_date = now(); + } + + if (! $delayedGood->is_paid) { + $delayedGood->payment_date = null; + } + } +} diff --git a/database/migrations/2026_02_19_021653_create_delayed_goods_table.php b/database/migrations/2026_02_19_021653_create_delayed_goods_table.php new file mode 100644 index 0000000..040fdbd --- /dev/null +++ b/database/migrations/2026_02_19_021653_create_delayed_goods_table.php @@ -0,0 +1,39 @@ +id(); + $table->foreignId('customer_id')->nullable()->constrained()->cascadeOnDelete(); + $table->foreignId('unit_id')->constrained()->cascadeOnDelete(); + $table->unsignedInteger('quantity'); + $table->unsignedInteger('unit_price'); + $table->unsignedInteger('total_amount'); + $table->unsignedInteger('paid_amount')->default(0); + $table->date('stored_date'); + $table->boolean('is_paid')->default(false); + $table->date('payment_date')->nullable(); + $table->text('notes')->nullable(); + $table->timestamp('created_at')->useCurrent(); + $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('delayed_goods'); + } +};