with([ 'supplier:id,name', 'createdBy.profile', 'items.rawMaterialPrice.rawMaterial:id,name,unit', 'media', ]) ->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void { $search = $tableQuery['search']; $query->where(function (Builder $query) use ($search): void { $query->where('notes', 'like', "%{$search}%") ->orWhereHas('supplier', fn (Builder $query) => $query->where('name', 'like', "%{$search}%")) ->orWhereHas('items.rawMaterialPrice', function (Builder $query) use ($search): void { $query->where('variant', 'like', "%{$search}%") ->orWhereHas('rawMaterial', fn (Builder $query) => $query->where('name', 'like', "%{$search}%")); }); }); }); $this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']); return $query ->paginate(10) ->withQueryString() ->through(function (Purchase $purchase) { $purchase->setAttribute( 'photos', MediaPresenter::first($purchase, 'photos'), ); return $purchase; }); } /** * @return list */ public function supplierOptions(): array { return Supplier::query() ->orderBy('name') ->get(['id', 'name']) ->map(fn (Supplier $supplier) => [ 'value' => $supplier->id, 'label' => $supplier->name, ]) ->all(); } /** * @return Collection */ public function catalogItems(?Purchase $purchase = null, ?User $user = null): Collection { $purchasePriceIds = $purchase ? $purchase->items()->pluck('raw_material_price_id')->all() : ($user ? $this->draftItemsQuery($user)->pluck('raw_material_price_id')->all() : []); return RawMaterial::query() ->with([ 'prices' => fn ($query) => $query ->orderBy('created_at') ->with(['rawMaterial:id,unit', 'media']), ]) ->where(function (Builder $query) use ($purchasePriceIds): void { $query->active(); if ($purchasePriceIds !== []) { $query->orWhereHas( 'prices', fn (Builder $query) => $query->whereIn('id', $purchasePriceIds), ); } }) ->orderBy('name') ->get() ->each(function (RawMaterial $rawMaterial): void { $rawMaterial->prices->each(function (RawMaterialPrice $price): void { $price->setAttribute( 'images', MediaPresenter::collection($price, 'images'), ); }); }); } public function findForEdit(Purchase $purchase): Purchase { $purchase->load([ 'items.rawMaterialPrice.rawMaterial:id,name,unit', 'media', ]); $purchase->setAttribute( 'photos', MediaPresenter::first($purchase, 'photos'), ); $purchase->items->each(function (PurchaseItem $item): void { $price = $item->rawMaterialPrice; if ($price) { $price->setAttribute( 'images', MediaPresenter::collection($price, 'images'), ); } }); return $purchase; } /** * @return list> */ public function draftItemsForUser(User $user): array { return $this->draftItemsQuery($user) ->with([ 'rawMaterialPrice.rawMaterial:id,name,unit', 'rawMaterialPrice.media', ]) ->get() ->map(fn (PurchaseItem $item) => $this->presentDraftItem($item)) ->values() ->all(); } /** * @param array $validated * @return array */ public function syncDraftItem(array $validated, User $user): array { $price = RawMaterialPrice::query() ->with('rawMaterial:id,name,unit') ->findOrFail($validated['raw_material_price_id']); $quantity = (float) $validated['quantity']; $unitPrice = (int) $price->price; $subtotal = (int) round($quantity * $unitPrice); $item = PurchaseItem::query()->updateOrCreate( [ 'user_id' => $user->id, 'raw_material_price_id' => $price->id, 'purchase_id' => null, ], [ 'quantity' => $quantity, 'unit_price' => $unitPrice, 'subtotal' => $subtotal, ], ); $item->load([ 'rawMaterialPrice.rawMaterial:id,name,unit', 'rawMaterialPrice.media', ]); return $this->presentDraftItem($item); } public function removeDraftItem(User $user, RawMaterialPrice $rawMaterialPrice): void { PurchaseItem::query() ->whereNull('purchase_id') ->where('user_id', $user->id) ->where('raw_material_price_id', $rawMaterialPrice->id) ->delete(); } /** * @param array $validated */ public function create(array $validated, User $user): Purchase { $purchase = DB::transaction(function () use ($validated, $user): Purchase { /** @var EloquentCollection $draftItems */ $draftItems = $this->draftItemsQuery($user) ->lockForUpdate() ->get(); if ($draftItems->isEmpty()) { throw ValidationException::withMessages([ 'items' => 'Tambahkan minimal satu bahan baku ke keranjang.', ]); } $subtotal = $draftItems->sum('subtotal'); $discount = (int) ($validated['discount'] ?? 0); $shippingCost = (int) ($validated['shipping_cost'] ?? 0); $total = max($subtotal - $discount + $shippingCost, 0); $purchase = Purchase::create([ 'supplier_id' => $validated['supplier_id'], 'created_by_id' => $user->id, 'subtotal' => $subtotal, 'discount' => $discount, 'shipping_cost' => $shippingCost, 'total' => $total, 'notes' => $validated['notes'] ?? null, ]); foreach ($draftItems as $item) { $item->update([ 'purchase_id' => $purchase->id, ]); $this->incrementStock($item); } $this->syncPhotos($purchase, $validated); return $purchase; }); $purchase->load('supplier'); $this->pushNotificationService->sendToRoles( '🛒 Belanja Baru', "Pembelian dari supplier {$purchase->supplier->name} senilai {$purchase->total_formatted} telah ditambahkan oleh {$user->profile?->full_name}.", ['owner', 'developer'], route('admin.manage.purchases.index'), ); return $purchase; } /** * @param array $validated */ public function update(Purchase $purchase, array $validated): void { DB::transaction(function () use ($purchase, $validated): void { $purchase->load('items'); foreach ($purchase->items as $item) { $this->decrementStock($item); } $purchase->items()->delete(); $lineItems = $this->buildLineItems($validated['items']); $subtotal = array_sum(array_column($lineItems, 'subtotal')); $discount = (int) ($validated['discount'] ?? 0); $shippingCost = (int) ($validated['shipping_cost'] ?? 0); $total = max($subtotal - $discount + $shippingCost, 0); $purchase->update([ 'supplier_id' => $validated['supplier_id'], 'subtotal' => $subtotal, 'discount' => $discount, 'shipping_cost' => $shippingCost, 'total' => $total, 'notes' => $validated['notes'] ?? null, ]); foreach ($lineItems as $itemData) { $purchaseItem = $purchase->items()->create($itemData); $this->incrementStock($purchaseItem); } $this->syncPhotos($purchase, $validated); }); $purchase->load('supplier'); $this->pushNotificationService->sendToRoles( '✏️ Belanja Diperbarui', "Pembelian dari supplier {$purchase->supplier->name} senilai {$purchase->total_formatted} telah diperbarui.", ['owner', 'developer'], route('admin.manage.purchases.index'), ); } public function delete(Purchase $purchase): void { $supplierName = $purchase->supplier->name; DB::transaction(function () use ($purchase): void { $purchase->load('items'); foreach ($purchase->items as $item) { $this->decrementStock($item); } $purchase->clearMediaCollection('photos'); $purchase->items()->delete(); $purchase->delete(); }); $this->pushNotificationService->sendToRoles( '🗑️ Belanja Dihapus', "Pembelian dari supplier {$supplierName} senilai {$purchase->total_formatted} telah dihapus.", ['owner', 'developer'], route('admin.manage.purchases.index'), ); } /** * @param list $items * @return list */ private function buildLineItems(array $items): array { return collect($items) ->map(function (array $itemData, int $index) { $price = RawMaterialPrice::query()->find($itemData['raw_material_price_id']); if ($price === null) { throw ValidationException::withMessages([ "items.{$index}.raw_material_price_id" => 'Bahan baku tidak ditemukan.', ]); } $quantity = (float) $itemData['quantity']; $unitPrice = (int) $price->price; $subtotal = (int) round($quantity * $unitPrice); return [ 'raw_material_price_id' => $price->id, 'quantity' => $quantity, 'unit_price' => $unitPrice, 'subtotal' => $subtotal, ]; }) ->all(); } /** * @param array $validated */ private function syncPhotos(Purchase $purchase, array $validated): void { $this->mediaService->syncCollection( $purchase, 'photos', $validated['photos'] ?? null, $validated['remove_media_ids'] ?? null, self::MAX_PHOTOS, required: false, errorKey: 'photos', ); } /** * @return Builder */ private function draftItemsQuery(User $user): Builder { return PurchaseItem::query() ->whereNull('purchase_id') ->where('user_id', $user->id); } /** * @return array */ private function presentDraftItem(PurchaseItem $item): array { $price = $item->rawMaterialPrice; $rawMaterial = $price?->rawMaterial; return [ 'raw_material_price_id' => $item->raw_material_price_id, 'raw_material_name' => $rawMaterial?->name ?? '', 'variant' => $price?->variant ?? '', 'unit_abbreviation' => $rawMaterial?->unit?->abbreviation() ?? '', 'quantity' => $item->quantity_input, 'unit_price' => $item->unit_price, 'images' => $price ? MediaPresenter::collection($price, 'images') : [], ]; } private function incrementStock(PurchaseItem $item): void { RawMaterialPrice::query() ->whereKey($item->raw_material_price_id) ->increment('stock', $item->quantity); } private function decrementStock(PurchaseItem $item): void { RawMaterialPrice::query() ->whereKey($item->raw_material_price_id) ->decrement('stock', $item->quantity); } private function applySorting(Builder $query, string $sort, string $direction): void { if (in_array($sort, ['created_at', 'total', 'discount', 'subtotal'], true)) { $query->orderBy($sort, $direction); return; } $query->latest(); } }