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): Collection { $purchasePriceIds = $purchase ? $purchase->items()->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; } /** * @param array $validated */ public function create(array $validated, User $user): Purchase { return DB::transaction(function () use ($validated, $user): Purchase { $lineItems = $this->buildLineItems($validated['items']); $subtotal = array_sum(array_column($lineItems, 'subtotal')); $discount = (int) ($validated['discount'] ?? 0); $total = max($subtotal - $discount, 0); $purchase = Purchase::create([ 'supplier_id' => $validated['supplier_id'], 'created_by_id' => $user->id, 'subtotal' => $subtotal, 'discount' => $discount, 'total' => $total, 'notes' => $validated['notes'] ?? null, ]); foreach ($lineItems as $itemData) { $purchaseItem = $purchase->items()->create($itemData); $this->incrementStock($purchaseItem); } $this->syncPhotos($purchase, $validated); 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); $total = max($subtotal - $discount, 0); $purchase->supplier_id = $validated['supplier_id']; $purchase->subtotal = $subtotal; $purchase->discount = $discount; $purchase->total = $total; $purchase->notes = $validated['notes'] ?? null; $purchase->save(); foreach ($lineItems as $itemData) { $purchaseItem = $purchase->items()->create($itemData); $this->incrementStock($purchaseItem); } $this->syncPhotos($purchase, $validated); }); } public function delete(Purchase $purchase): void { 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(); }); } /** * @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', ); } 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(); } }