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 @@
+
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
+
+