select(['id', 'supplier_id', 'created_by_id', 'subtotal', 'discount', 'shipping_cost', 'total', 'notes', 'created_at']) ->with([ 'supplier:id,name', 'createdBy:id', 'createdBy.userProfile:id,user_id,full_name', ]) ->selectRaw("{$itemCountQuery} as variants_count") ->selectRaw("{$totalQtyQuery} as total_qty") ->selectRaw("{$materialNameQuery} as material_name") ->selectRaw("{$unitQuery} as unit") ->when($highlight, fn ($q) => $q->where('id', $highlight)) ->when($search, function ($q) use ($search) { $q->whereHas('supplier', fn ($sq) => $sq->where('name', 'like', "%{$search}%")) ->orWhere('notes', 'like', "%{$search}%"); }) ->when($filters['supplier_id'] ?? null, fn ($q, $supplierId) => $q->where('supplier_id', $supplierId)) ->orderBy($sort, $direction) ->paginate($perPage); $paginator->getCollection()->each(function (Purchase $purchase) { $purchaseMedia = $purchase->getMedia('photos'); $purchase->photo_urls = $purchaseMedia->map( fn ($media) => $this->s3Service->getTemporaryUrl($media->getPath()) )->toArray(); $purchase->photo_conversion_urls = $purchaseMedia->map( fn ($media) => $this->s3Service->getTemporaryUrl($media->getPath('thumb')) )->toArray(); }); return $paginator; } public function getItems(Purchase $purchase): Collection { return $purchase->purchaseItems() ->select(['id', 'purchase_id', 'raw_material_price_id', 'quantity', 'unit_price', 'subtotal']) ->with(['rawMaterialPrice:id,raw_material_id,variant,price,stock', 'rawMaterialPrice.rawMaterial:id,name,unit']) ->orderByRaw('(SELECT variant FROM raw_material_prices WHERE raw_material_prices.id = purchase_items.raw_material_price_id)') ->get() ->each(function (PurchaseItem $item) { if (! $item->rawMaterialPrice) { return; } $media = $item->rawMaterialPrice->getFirstMedia('images'); $item->rawMaterialPrice->photo_url = $media ? $this->s3Service->getTemporaryUrl($media->getPath()) : null; $item->rawMaterialPrice->photo_conversion_url = $media ? $this->s3Service->getTemporaryUrl($media->getPath('thumb')) : null; }); } public function getForCreate(): array { return [ 'suppliers' => Supplier::select(['id', 'name'])->latest()->get(), 'rawMaterials' => RawMaterial::query() ->select(['id', 'name', 'unit', 'is_active']) ->with([ 'rawMaterialPrices:id,raw_material_id,variant,price,stock', ]) ->orderBy('name') ->get(), ]; } public function getForEdit(Purchase $purchase): array { $purchase->load([ 'purchaseItems' => fn ($q) => $q ->orderByRaw('(SELECT variant FROM raw_material_prices WHERE raw_material_prices.id = purchase_items.raw_material_price_id)'), 'purchaseItems.rawMaterialPrice.rawMaterial', 'supplier', ]); $rawMaterial = $purchase->purchaseItems->first()?->rawMaterialPrice?->rawMaterial; $variants = $purchase->purchaseItems->map(function (PurchaseItem $item) { if (! $item->rawMaterialPrice) { return null; } $media = $item->rawMaterialPrice->getFirstMedia('images'); return [ 'id' => $item->rawMaterialPrice->id, 'variant' => $item->rawMaterialPrice->variant, 'price' => $item->unit_price, 'stock' => $item->quantity, 'photo_key' => $media?->getCustomProperty('s3_key') ?? $media?->file_name, 'photo_url' => $media ? $this->s3Service->getTemporaryUrl($media->getPath()) : null, ]; })->filter()->values(); $items = $purchase->purchaseItems; $items = $items->filter(fn (PurchaseItem $item) => $item->rawMaterialPrice !== null); $materials = $items ->map(fn (PurchaseItem $item) => $item->rawMaterialPrice->rawMaterial) ->filter() ->unique(fn (RawMaterial $material) => $material->id); $singleMaterial = $materials->count() === 1; $sharedWithOther = PurchaseItem::where('purchase_id', '!=', $purchase->id) ->whereIn('raw_material_price_id', $items->pluck('raw_material_price_id')) ->exists(); $purchaseMedia = $purchase->getMedia('photos'); $purchasePhotoKeys = $purchaseMedia->map( fn ($media) => $media->getCustomProperty('s3_key') ?? $media->file_name )->toArray(); $purchasePhotoUrls = $purchaseMedia->map( fn ($media) => $this->s3Service->getTemporaryUrl($media->getPath()) )->toArray(); return [ 'id' => $purchase->id, 'name' => $rawMaterial?->name ?? '', 'unit' => $rawMaterial?->unit->value ?? 'kg', 'supplier_id' => $purchase->supplier_id, 'discount' => $purchase->discount, 'shipping_cost' => $purchase->shipping_cost, 'notes' => $purchase->notes, 'photo_keys' => $purchasePhotoKeys, 'photo_urls' => $purchasePhotoUrls, 'variants' => $variants, 'default_mode' => $singleMaterial && ! $sharedWithOther ? 'new' : 'existing', 'existing_material_name' => $singleMaterial ? $materials->first()->name : null, 'existing_quantities' => $singleMaterial ? $items->mapWithKeys(fn (PurchaseItem $item) => [ (int) $item->raw_material_price_id => (int) $item->quantity, ])->all() : [], ]; } public function store(array $data): Purchase { if (($data['mode'] ?? 'new') === 'existing') { return $this->storeFromExisting($data); } return $this->storeNew($data); } private function storeFromExisting(array $data): Purchase { return DB::transaction(function () use ($data) { $now = now(); $subtotal = 0; $itemRows = collect($data['existing_items'])->map(function ($item) use ($now, &$subtotal) { $itemSubtotal = (int) ($item['unit_price'] * $item['quantity']); $subtotal += $itemSubtotal; return [ 'purchase_id' => null, 'raw_material_price_id' => $item['raw_material_price_id'], 'user_id' => auth()->id(), 'quantity' => $item['quantity'], 'unit_price' => $item['unit_price'], 'subtotal' => $itemSubtotal, 'created_at' => $now, 'updated_at' => $now, ]; })->toArray(); $discount = $data['discount'] ?? 0; $shippingCost = $data['shipping_cost'] ?? 0; $total = $subtotal - $discount + $shippingCost; $purchase = Purchase::create([ 'supplier_id' => $data['supplier_id'], 'created_by_id' => auth()->id(), 'subtotal' => $subtotal, 'discount' => $discount, 'shipping_cost' => $shippingCost, 'total' => $total, 'notes' => $data['notes'] ?? null, ]); foreach ($itemRows as &$row) { $row['purchase_id'] = $purchase->id; } DB::table('purchase_items')->insert($itemRows); $stockIncrementMap = collect($data['existing_items']) ->groupBy('raw_material_price_id') ->map(fn ($group) => $group->sum('quantity')); foreach ($stockIncrementMap as $priceId => $totalQty) { RawMaterialPrice::where('id', $priceId)->increment('stock', $totalQty); } if (! empty($data['photo_keys']) && is_array($data['photo_keys'])) { $this->registerPhotos( model: $purchase, photoKeys: $data['photo_keys'], collectionName: 'photos', ); } NotificationService::notify( roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU], title: 'Belanja Baru', body: 'Belanja bahan baku sebesar Rp '.number_format($total, 0, ',', '.').' berhasil dicatat oleh '.auth()->user()->full_name.'.', url: route('admin.manage.purchases.index', ['highlight' => $purchase->id]), ); return $purchase; }); } private function storeNew(array $data): Purchase { return DB::transaction(function () use ($data) { $rawMaterial = RawMaterial::create([ 'name' => $data['name'], 'unit' => $data['unit'], 'is_active' => true, ]); $subtotal = 0; $now = now(); $priceRows = collect($data['variants'])->map(function ($v) use ($rawMaterial, $now, &$subtotal) { $itemSubtotal = (int) ($v['price'] * $v['stock']); $subtotal += $itemSubtotal; return [ 'raw_material_id' => $rawMaterial->id, 'variant' => $v['variant'], 'price' => $v['price'], 'stock' => $v['stock'], 'created_at' => $now, 'updated_at' => $now, ]; })->toArray(); DB::table('raw_material_prices')->insert($priceRows); $priceIdMap = DB::table('raw_material_prices') ->where('raw_material_id', $rawMaterial->id) ->pluck('id', 'variant') ->toArray(); foreach ($data['variants'] as $variantData) { if (! empty($variantData['photo_key'])) { $priceId = $priceIdMap[$variantData['variant']] ?? null; if ($priceId) { $priceModel = RawMaterialPrice::find($priceId); $this->registerMedia( model: $priceModel, s3Key: $variantData['photo_key'], collectionName: 'images', orderColumn: 1, ); } } } $discount = $data['discount'] ?? 0; $shippingCost = $data['shipping_cost'] ?? 0; $total = $subtotal - $discount + $shippingCost; $purchase = Purchase::create([ 'supplier_id' => $data['supplier_id'], 'created_by_id' => auth()->id(), 'subtotal' => $subtotal, 'discount' => $discount, 'shipping_cost' => $shippingCost, 'total' => $total, 'notes' => $data['notes'] ?? null, ]); $purchaseItems = collect($data['variants'])->map(function ($v) use ($purchase, $priceIdMap, $now) { $priceId = $priceIdMap[$v['variant']] ?? null; return [ 'purchase_id' => $purchase->id, 'raw_material_price_id' => $priceId, 'user_id' => auth()->id(), 'quantity' => $v['stock'], 'unit_price' => $v['price'], 'subtotal' => (int) ($v['price'] * $v['stock']), 'created_at' => $now, 'updated_at' => $now, ]; })->toArray(); DB::table('purchase_items')->insert($purchaseItems); if (! empty($data['photo_keys']) && is_array($data['photo_keys'])) { $this->registerPhotos( model: $purchase, photoKeys: $data['photo_keys'], collectionName: 'photos', ); } NotificationService::notify( roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU], title: 'Belanja Baru', body: 'Belanja bahan baku sebesar Rp '.number_format($total, 0, ',', '.').' berhasil dicatat oleh '.auth()->user()->full_name.'.', url: route('admin.manage.purchases.index', ['highlight' => $purchase->id]), ); return $purchase; }); } public function update(Purchase $purchase, array $data): Purchase { $purchase = DB::transaction(function () use ($purchase, $data) { $oldStockMap = $purchase->purchaseItems() ->select('raw_material_price_id', 'quantity') ->get() ->groupBy('raw_material_price_id') ->map(fn ($group) => $group->sum('quantity')); foreach ($oldStockMap as $priceId => $totalQty) { RawMaterialPrice::where('id', $priceId)->decrement('stock', $totalQty); } $oldMaterial = $purchase->purchaseItems() ->join('raw_material_prices', 'raw_material_prices.id', '=', 'purchase_items.raw_material_price_id') ->join('raw_materials', 'raw_materials.id', '=', 'raw_material_prices.raw_material_id') ->select('raw_materials.*') ->first(); $purchase->purchaseItems()->delete(); $now = now(); $subtotal = 0; if (($data['mode'] ?? 'new') === 'existing') { $itemRows = collect($data['existing_items'])->map(function ($item) use ($now, &$subtotal) { $itemSubtotal = (int) ($item['unit_price'] * $item['quantity']); $subtotal += $itemSubtotal; return [ 'purchase_id' => null, 'raw_material_price_id' => $item['raw_material_price_id'], 'user_id' => auth()->id(), 'quantity' => $item['quantity'], 'unit_price' => $item['unit_price'], 'subtotal' => $itemSubtotal, 'created_at' => $now, 'updated_at' => $now, ]; })->toArray(); $stockIncrementMap = collect($data['existing_items']) ->groupBy('raw_material_price_id') ->map(fn ($group) => $group->sum('quantity')); foreach ($stockIncrementMap as $priceId => $totalQty) { RawMaterialPrice::where('id', $priceId)->increment('stock', $totalQty); } } else { if ($oldMaterial) { $rawMaterial = $oldMaterial; $rawMaterial->update([ 'name' => $data['name'], 'is_active' => true, ]); } else { $rawMaterial = RawMaterial::create([ 'name' => $data['name'], 'unit' => $data['unit'] ?? 'kg', 'is_active' => true, ]); } $itemRows = collect($data['variants'])->map(function ($v) use ($purchase, $rawMaterial, $now, &$subtotal) { $price = null; if (! empty($v['id'])) { $price = RawMaterialPrice::withTrashed()->find($v['id']); } if (! $price) { $price = $rawMaterial->rawMaterialPrices() ->where('variant', $v['variant']) ->first(); } if ($price) { $price->increment('stock', (int) $v['stock']); $price->update(['price' => $v['price']]); } else { $price = $rawMaterial->rawMaterialPrices()->create([ 'variant' => $v['variant'], 'price' => $v['price'], 'stock' => $v['stock'], ]); } if (! empty($v['photo_key']) && $price->getFirstMedia('images')?->getCustomProperty('s3_key') !== $v['photo_key']) { $price->clearMediaCollection('images'); $this->registerMedia( model: $price, s3Key: $v['photo_key'], collectionName: 'images', orderColumn: 1, ); } $itemSubtotal = (int) ($v['price'] * $v['stock']); $subtotal += $itemSubtotal; return [ 'purchase_id' => $purchase->id, 'raw_material_price_id' => $price->id, 'user_id' => auth()->id(), 'quantity' => $v['stock'], 'unit_price' => $v['price'], 'subtotal' => $itemSubtotal, 'created_at' => $now, 'updated_at' => $now, ]; })->toArray(); } $discount = $data['discount'] ?? 0; $shippingCost = $data['shipping_cost'] ?? 0; $total = $subtotal - $discount + $shippingCost; $purchase->update([ 'supplier_id' => $data['supplier_id'], 'subtotal' => $subtotal, 'discount' => $discount, 'shipping_cost' => $shippingCost, 'total' => $total, 'notes' => $data['notes'] ?? null, ]); foreach ($itemRows as &$row) { $row['purchase_id'] = $purchase->id; } DB::table('purchase_items')->insert($itemRows); $this->syncPhotos($purchase, $data['photo_keys'] ?? [], 'photos'); return $purchase; }); NotificationService::notify( roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU], title: 'Belanja Diperbarui', body: 'Belanja bahan baku sebesar '.$purchase->formatted_total.' berhasil diperbarui oleh '.auth()->user()->full_name.'.', url: route('admin.manage.purchases.index', ['highlight' => $purchase->id]), ); return $purchase; } public function destroy(Purchase $purchase): bool { $result = DB::transaction(function () use ($purchase) { $stockDecrementMap = $purchase->purchaseItems() ->select('raw_material_price_id', 'quantity') ->get() ->groupBy('raw_material_price_id') ->map(fn ($group) => $group->sum('quantity')); foreach ($stockDecrementMap as $priceId => $totalQty) { RawMaterialPrice::where('id', $priceId)->decrement('stock', $totalQty); } $purchase->purchaseItems()->delete(); $purchase->delete(); return true; }); NotificationService::notify( roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU], title: 'Belanja Dihapus', body: 'Belanja bahan baku berhasil dihapus oleh '.auth()->user()->full_name.'.', url: route('admin.manage.purchases.index'), ); return $result; } }