diff --git a/app/Filament/Resources/Manage/FeedPurchases/FeedPurchaseResource.php b/app/Filament/Resources/Manage/FeedPurchases/FeedPurchaseResource.php new file mode 100644 index 0000000..3c38d32 --- /dev/null +++ b/app/Filament/Resources/Manage/FeedPurchases/FeedPurchaseResource.php @@ -0,0 +1,50 @@ + ListFeedPurchases::route('/'), + 'create' => CreateFeedPurchase::route('/create'), + ]; + } +} diff --git a/app/Filament/Resources/Manage/FeedPurchases/Pages/CreateFeedPurchase.php b/app/Filament/Resources/Manage/FeedPurchases/Pages/CreateFeedPurchase.php new file mode 100644 index 0000000..bdc4d92 --- /dev/null +++ b/app/Filament/Resources/Manage/FeedPurchases/Pages/CreateFeedPurchase.php @@ -0,0 +1,66 @@ +syncTotalPriceFromCart(); + } + + protected function syncTotalPriceFromCart(): void + { + $total = (int) FeedPurchaseItem::whereNull('feed_purchase_id')->sum('subtotal'); + + $this->form->fill([ + ...$this->form->getState(), + 'total_price' => $total, + ]); + } + + protected function getHeaderActions(): array + { + return [ + BackAction::make() + ->url(ListFeedPurchases::getUrl()), + ]; + } + + protected function getRedirectUrl(): string + { + return $this->getResource()::getUrl('index'); + } + + protected function mutateFormDataBeforeCreate(array $data): array + { + $total = FeedPurchaseItem::whereNull('feed_purchase_id')->sum('subtotal'); + $data['total_price'] = $total; + + return $data; + } + + protected function afterCreate(): void + { + FeedPurchaseItem::whereNull('feed_purchase_id') + ->update(['feed_purchase_id' => $this->record->id]); + + $this->record->updateFeedStock(); + } +} diff --git a/app/Filament/Resources/Manage/FeedPurchases/Pages/ListFeedPurchases.php b/app/Filament/Resources/Manage/FeedPurchases/Pages/ListFeedPurchases.php new file mode 100644 index 0000000..728ce72 --- /dev/null +++ b/app/Filament/Resources/Manage/FeedPurchases/Pages/ListFeedPurchases.php @@ -0,0 +1,22 @@ +label('Tambah'), + ]; + } +} diff --git a/app/Filament/Resources/Manage/FeedPurchases/Schemas/FeedPurchaseForm.php b/app/Filament/Resources/Manage/FeedPurchases/Schemas/FeedPurchaseForm.php new file mode 100644 index 0000000..75e44d5 --- /dev/null +++ b/app/Filament/Resources/Manage/FeedPurchases/Schemas/FeedPurchaseForm.php @@ -0,0 +1,111 @@ +latest() + ->get() + ->map(function ($feed) { + $feed->image = $feed->getFirstMediaUrl('feeds'); + + return $feed; + }); + + $cartItems = FeedPurchaseItem::with('feed') + ->whereNull('feed_purchase_id') + ->latest() + ->get() + ->map(function ($cartItem) { + $cartItem->subtotal = $cartItem->unit_price * $cartItem->quantity; + $cartItem->feedImage = $cartItem->feed?->getFirstMediaUrl('feeds'); + + return $cartItem; + }); + + return $schema + ->components([ + Section::make('Informasi Belanja') + ->schema([ + Grid::make(3) + ->schema([ + TextInput::make('reference_number') + ->label('Nomor Referensi') + ->placeholder('...') + ->nullable() + ->autocomplete(false) + ->autofocus() + ->helperText('Silakan masukan nomor belanja, invoice atau lainnya.'), + + DatePicker::make('purchase_date') + ->label('Tanggal Belanja') + ->default(now()) + ->required() + ->native(false) + ->displayFormat('l, d F Y'), + + TextInput::make('total_price') + ->label('Total Harga') + ->placeholder('0') + ->readOnly() + ->live() + ->currencyMask(thousandSeparator: '.', decimalSeparator: ',', precision: 0) + ->prefix('Rp') + ->required() + ->dehydrateStateUsing(fn ($state) => (int) str()->replace(',', '', (string) ($state ?? 0))) + ->default(fn () => (int) FeedPurchaseItem::whereNull('feed_purchase_id')->sum('subtotal')), + + Textarea::make('notes') + ->label('Catatan') + ->placeholder('...') + ->columnSpanFull(), + ]), + ]) + ->collapsible(), + + Grid::make([ + 'default' => 1, + 'lg' => 3, + ]) + ->schema([ + Section::make('Daftar Pakan') + ->schema([ + View::make('filament.resources.manage.feed-purchases.feed-items-grid') + ->viewData([ + 'feeds' => $feeds, + ]), + ]) + ->columnSpan([ + 'default' => 1, + 'lg' => 2, + ]), + + Section::make('Keranjang') + ->schema([ + View::make('filament.resources.manage.feed-purchases.cart') + ->viewData([ + 'cartItems' => $cartItems, + ]), + ]) + ->columnSpan([ + 'default' => 1, + 'lg' => 1, + ]), + ]), + ]) + ->columns(1); + } +} diff --git a/app/Filament/Resources/Manage/FeedPurchases/Tables/FeedPurchasesTable.php b/app/Filament/Resources/Manage/FeedPurchases/Tables/FeedPurchasesTable.php new file mode 100644 index 0000000..e9a07d7 --- /dev/null +++ b/app/Filament/Resources/Manage/FeedPurchases/Tables/FeedPurchasesTable.php @@ -0,0 +1,99 @@ +columns([ + TextColumn::make('reference_number') + ->label('No. Referensi') + ->searchable() + ->sortable(), + + TextColumn::make('purchase_date') + ->label('Tanggal Belanja') + ->date('l, d F Y') + ->sortable(), + + TextColumn::make('items_summary') + ->label('Item Pakan') + ->getStateUsing(function ($record): array { + return $record->items + ->map(function ($item) { + $feedName = $item->feed?->name ?? '-'; + $feedUnit = $item->feed?->unit?->name ?? '-'; + $quantity = number_format($item->quantity, 0, ',', '.'); + $subtotal = $item->formatted_subtotal; + + return sprintf( + '
+
+ + %s + + + x%s %s + +
+ +
+ Rp %s +
+
', + e($feedName), + $quantity, + $feedUnit, + $subtotal, + ); + }) + ->toArray(); + }) + ->listWithLineBreaks() + ->html() + ->toggleable(), + + TextColumn::make('total_price') + ->label('Total Harga') + ->money('IDR', decimalPlaces: 0) + ->sortable(), + + TextColumn::make('created_at') + ->label('Dibuat') + ->dateTime('d/m/Y H:i') + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + TrashedFilter::make() + ->native(false), + ]) + ->recordActions([ + DeleteAction::make(), + + ForceDeleteAction::make(), + + RestoreAction::make(), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + ...DefaultBulkActions::make('Belanja Pakan'), + ]), + ]) + ->emptyStateIcon(Heroicon::Banknotes) + ->emptyStateDescription('Setelah Anda membuat data pertama, maka akan muncul disini.') + ->defaultSort('created_at', 'desc') + ->deferFilters(false); + } +} diff --git a/app/Filament/Resources/Manage/FeedPurchases/Traits/HasCart.php b/app/Filament/Resources/Manage/FeedPurchases/Traits/HasCart.php new file mode 100644 index 0000000..98f98d4 --- /dev/null +++ b/app/Filament/Resources/Manage/FeedPurchases/Traits/HasCart.php @@ -0,0 +1,74 @@ +cart = FeedPurchaseItem::whereNull('feed_purchase_id') + ->get() + ->pluck('quantity', 'feed_id') + ->map(fn ($qty) => ['qty' => (int) $qty]) + ->toArray(); + } + + public function addFeed(int $feedId): void + { + $feed = Feed::find($feedId); + if (! $feed) { + return; + } + + $item = FeedPurchaseItem::whereNull('feed_purchase_id') + ->where('feed_id', $feedId) + ->first(); + + if ($item) { + $item->increment('quantity', 1); + $item->update(['subtotal' => $item->quantity * $item->unit_price]); + } else { + FeedPurchaseItem::create([ + 'feed_purchase_id' => null, + 'feed_id' => $feedId, + 'quantity' => 1, + 'unit_price' => $feed->price, + 'subtotal' => $feed->price, + ]); + } + + $this->loadCartFromDb(); + $this->dispatch('cart-updated'); + } + + public function decreaseQty(int $feedId): void + { + $item = FeedPurchaseItem::whereNull('feed_purchase_id') + ->where('feed_id', $feedId) + ->first(); + + if ($item) { + if ($item->quantity > 1) { + $item->decrement('quantity', 1); + $item->update(['subtotal' => $item->quantity * $item->unit_price]); + } else { + $item->delete(); + } + + $this->loadCartFromDb(); + $this->dispatch('cart-updated'); + } + } + + public function removeCartItem(int $itemId): void + { + FeedPurchaseItem::where('id', $itemId)->whereNull('feed_purchase_id')->delete(); + $this->loadCartFromDb(); + $this->dispatch('cart-updated'); + } +} diff --git a/app/Models/Feed.php b/app/Models/Feed.php index 3de9499..ad62571 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Enums\FeedStatus; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -25,6 +26,13 @@ protected function casts(): array ]; } + protected function formattedPrice(): Attribute + { + return Attribute::make( + get: fn () => number_format($this->price, 0, ',', '.'), + ); + } + public function unit(): BelongsTo { return $this->belongsTo(Unit::class); diff --git a/app/Models/FeedPurchase.php b/app/Models/FeedPurchase.php new file mode 100644 index 0000000..e8795c0 --- /dev/null +++ b/app/Models/FeedPurchase.php @@ -0,0 +1,61 @@ + 'date', + 'total_price' => 'integer', + ]; + + protected static function boot() + { + parent::boot(); + + static::deleted(function ($feedPurchase) { + $feedPurchase->revertFeedStock(); + }); + + static::forceDeleted(function ($feedPurchase) { + $feedPurchase->revertFeedStock(); + }); + + static::restored(function ($feedPurchase) { + $feedPurchase->updateFeedStock(); + }); + } + + public function items(): HasMany + { + return $this->hasMany(FeedPurchaseItem::class); + } + + public function updateFeedStock(): void + { + foreach ($this->items as $item) { + $feed = $item->feed; + if ($feed) { + $feed->increment('stock', $item->quantity); + } + } + } + + public function revertFeedStock(): void + { + foreach ($this->items as $item) { + $feed = $item->feed; + if ($feed) { + $feed->decrement('stock', $item->quantity); + } + } + } +} diff --git a/app/Models/FeedPurchaseItem.php b/app/Models/FeedPurchaseItem.php new file mode 100644 index 0000000..18f457c --- /dev/null +++ b/app/Models/FeedPurchaseItem.php @@ -0,0 +1,45 @@ + 'integer', + 'unit_price' => 'integer', + 'subtotal' => 'integer', + ]; + + protected function formattedUnitPrice(): Attribute + { + return Attribute::make( + get: fn () => number_format($this->unit_price, 0, ',', '.'), + ); + } + + protected function formattedSubtotal(): Attribute + { + return Attribute::make( + get: fn () => number_format($this->subtotal, 0, ',', '.'), + ); + } + + public function purchase(): BelongsTo + { + return $this->belongsTo(FeedPurchase::class, 'feed_purchase_id'); + } + + public function feed(): BelongsTo + { + return $this->belongsTo(Feed::class); + } +} diff --git a/database/migrations/2026_02_09_033312_create_feed_purchases_table.php b/database/migrations/2026_02_09_033312_create_feed_purchases_table.php new file mode 100644 index 0000000..d1c5563 --- /dev/null +++ b/database/migrations/2026_02_09_033312_create_feed_purchases_table.php @@ -0,0 +1,27 @@ +id(); + $table->string('reference_number', 50)->nullable(); + $table->date('purchase_date'); + $table->unsignedInteger('total_price'); + $table->text('notes')->nullable(); + $table->timestamp('created_at')->useCurrent(); + $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); + $table->softDeletes(); + }); + } + + public function down(): void + { + Schema::dropIfExists('feed_purchases'); + } +}; diff --git a/database/migrations/2026_02_09_033313_create_feed_purchase_items_table.php b/database/migrations/2026_02_09_033313_create_feed_purchase_items_table.php new file mode 100644 index 0000000..877ee4a --- /dev/null +++ b/database/migrations/2026_02_09_033313_create_feed_purchase_items_table.php @@ -0,0 +1,28 @@ +id(); + $table->foreignId('feed_purchase_id')->nullable()->constrained()->cascadeOnDelete(); + $table->foreignId('feed_id')->constrained()->cascadeOnDelete(); + $table->unsignedInteger('quantity'); + $table->unsignedInteger('unit_price'); + $table->unsignedInteger('subtotal'); + $table->timestamp('created_at')->useCurrent(); + $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); + $table->softDeletes(); + }); + } + + public function down(): void + { + Schema::dropIfExists('feed_purchase_items'); + } +}; diff --git a/resources/views/filament/resources/manage/feed-purchases/cart.blade.php b/resources/views/filament/resources/manage/feed-purchases/cart.blade.php new file mode 100644 index 0000000..c0cdbc3 --- /dev/null +++ b/resources/views/filament/resources/manage/feed-purchases/cart.blade.php @@ -0,0 +1,69 @@ +
+ + +
+ @forelse ($cartItems as $cartItem) +
+ +
+
+ +
+ {{ $cartItem->feed?->name }} +
+ +
+

+ {{ $cartItem->feed?->name }} +

+
+ + Rp {{ $cartItem->formatted_unit_price }} + +
+
+
+ + +
+ + +
+
+ {{ $cartItem->quantity }} + {{ $cartItem->feed?->unit?->alias ?? 'Unit' }} +
+ +
+ Rp {{ $cartItem->formatted_subtotal }} +
+
+
+ @empty +
+
+ + + +
+

Keranjang Kosong

+

Pilih pakan di sebelah kiri untuk + menambahkan ke keranjang.

+
+ @endforelse +
+
diff --git a/resources/views/filament/resources/manage/feed-purchases/feed-items-grid.blade.php b/resources/views/filament/resources/manage/feed-purchases/feed-items-grid.blade.php new file mode 100644 index 0000000..826dd00 --- /dev/null +++ b/resources/views/filament/resources/manage/feed-purchases/feed-items-grid.blade.php @@ -0,0 +1,44 @@ +
+ + @foreach ($feeds as $feed) +
+ + + +
+ +
+ {{ $feed->unit?->name ?? '-' }} +
+ +
+ {{ $feed->name }} +
+ +
+ Rp {{ $feed->formatted_price }} +
+ +
+ + + - + + + + {{ $this->cart[$feed->id]['qty'] ?? 0 }} + + + + + + + +
+ +
+
+ @endforeach + +