value] ?? [], true); } public function transitionPermission(CuttingStatus $status): Permission { $permissions = config('cutting-status.permissions', []); return $permissions[$status->value] ?? throw new \InvalidArgumentException('Status tidak mendukung transisi.'); } /** * @return list */ public function availableActions(CuttingStatus $status): array { return config('cutting-status.actions.'.$status->value, []); } /** * @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery */ public function paginateForIndex(array $tableQuery, User $user): LengthAwarePaginator { $query = Cutting::query() ->with([ 'createdBy.profile', 'rejection.rejectedBy.profile', 'materials.rawMaterialPrice.rawMaterial:id,name,unit', 'results.productVariant.product:id,name', 'results.productVariant:id,product_id,name', ]) ->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void { $search = $tableQuery['search']; $query->where(function (Builder $query) use ($search): void { $query->where('description', 'like', "%{$search}%") ->orWhereHas('materials.rawMaterialPrice', function (Builder $query) use ($search): void { $query->where('variant', 'like', "%{$search}%") ->orWhereHas('rawMaterial', fn (Builder $query) => $query->where('name', 'like', "%{$search}%")); }) ->orWhereHas('results.productVariant', function (Builder $query) use ($search): void { $query->where('name', 'like', "%{$search}%") ->orWhereHas('product', fn (Builder $query) => $query->where('name', 'like', "%{$search}%")); }); }); }); $this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']); return $query ->paginate(10) ->withQueryString() ->through(function (Cutting $cutting) use ($user) { $actions = collect($this->availableActions($cutting->status)) ->filter(fn (array $action) => $user->can($action['permission'])) ->values() ->all(); $cutting->setAttribute('available_actions', $actions); $cutting->setAttribute('is_editable', $this->isEditable($cutting->status)); $this->appendCostPreview($cutting); return $cutting; }); } /** * @return Collection */ public function getInProgressCuttings(User $user): Collection { return Cutting::query() ->with([ 'createdBy.profile', 'rejection.rejectedBy.profile', 'materials.rawMaterialPrice.rawMaterial:id,name,unit', 'results.productVariant.product:id,name', 'results.productVariant:id,product_id,name', ]) ->inProgress() ->latest() ->get() ->each(function (Cutting $cutting) use ($user): void { $actions = collect($this->availableActions($cutting->status)) ->filter(fn (array $action) => $user->can($action['permission'])) ->values() ->all(); $cutting->setAttribute('available_actions', $actions); $cutting->setAttribute('is_editable', $this->isEditable($cutting->status)); $this->appendCostPreview($cutting); }); } /** * @return Collection */ public function getCompletedCuttings(User $user): Collection { return Cutting::query() ->with([ 'createdBy.profile', 'rejection.rejectedBy.profile', 'materials.rawMaterialPrice.rawMaterial:id,name,unit', 'results.productVariant.product:id,name', 'results.productVariant:id,product_id,name', ]) ->completed() ->latest() ->get() ->each(function (Cutting $cutting) use ($user): void { $actions = collect($this->availableActions($cutting->status)) ->filter(fn (array $action) => $user->can($action['permission'])) ->values() ->all(); $cutting->setAttribute('available_actions', $actions); $cutting->setAttribute('is_editable', $this->isEditable($cutting->status)); $this->appendCostPreview($cutting); }); } /** * @return Collection */ public function rawMaterialCatalog(?Cutting $cutting = null, ?User $user = null): Collection { $selectedPriceIds = $cutting ? $cutting->materials()->pluck('raw_material_price_id')->all() : ($user ? $this->draftMaterialsQuery($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 ($selectedPriceIds): void { $query->active(); if ($selectedPriceIds !== []) { $query->orWhereHas( 'prices', fn (Builder $query) => $query->whereIn('id', $selectedPriceIds), ); } }) ->orderBy('name') ->get() ->each(function (RawMaterial $rawMaterial): void { $rawMaterial->prices->each(function (RawMaterialPrice $price): void { $price->setAttribute( 'images', MediaPresenter::collection($price, 'images'), ); }); }); } /** * @return Collection */ public function productCatalog(?Cutting $cutting = null, ?User $user = null): Collection { $selectedVariantIds = $cutting ? $cutting->results()->pluck('product_variant_id')->all() : ($user ? $this->draftResultsQuery($user)->pluck('product_variant_id')->all() : []); return Product::query() ->with([ 'variants' => fn ($query) => $query ->with('media') ->orderBy('created_at'), ]) ->where(function (Builder $query) use ($selectedVariantIds): void { $query->active(); if ($selectedVariantIds !== []) { $query->orWhereHas( 'variants', fn (Builder $query) => $query->whereIn('id', $selectedVariantIds), ); } }) ->orderBy('name') ->get() ->each(function (Product $product): void { $product->variants->each(function (ProductVariant $variant): void { $variant->setAttribute( 'images', MediaPresenter::collection($variant, 'images'), ); }); }); } public function findForEdit(Cutting $cutting): Cutting { $cutting->load([ 'materials.rawMaterialPrice.rawMaterial:id,name,unit', 'materials.rawMaterialPrice.media', 'results.productVariant.product:id,name', 'results.productVariant.media', 'rejection.rejectedBy.profile', ]); $cutting->materials->each(function (CuttingMaterial $material): void { $price = $material->rawMaterialPrice; if ($price) { $price->setAttribute( 'images', MediaPresenter::collection($price, 'images'), ); } }); $cutting->results->each(function (CuttingResult $result): void { $variant = $result->productVariant; if ($variant) { $variant->setAttribute( 'images', MediaPresenter::collection($variant, 'images'), ); } }); return $cutting; } /** * @return list> */ public function draftMaterialsForUser(User $user): array { return $this->draftMaterialsQuery($user) ->with([ 'rawMaterialPrice.rawMaterial:id,name,unit', 'rawMaterialPrice.media', ]) ->get() ->map(fn (CuttingMaterial $item) => $this->presentDraftMaterial($item)) ->values() ->all(); } /** * @return list> */ public function draftResultsForUser(User $user): array { return $this->draftResultsQuery($user) ->with([ 'productVariant.product:id,name', 'productVariant.media', ]) ->get() ->map(fn (CuttingResult $item) => $this->presentDraftResult($item)) ->values() ->all(); } /** * @param array $validated * @return array */ public function syncDraftMaterial(array $validated, User $user): array { $price = RawMaterialPrice::query() ->with('rawMaterial') ->findOrFail($validated['raw_material_price_id']); $materialUsage = round((float) $validated['material_usage'], 2); $remainingMaterial = round((float) $validated['remaining_material'], 2); if ($materialUsage <= 0) { throw ValidationException::withMessages([ 'material_usage' => 'Pemakaian bahan harus lebih dari 0.', ]); } if ($remainingMaterial < 0) { throw ValidationException::withMessages([ 'remaining_material' => 'Sisa bahan tidak boleh negatif.', ]); } $item = CuttingMaterial::query()->updateOrCreate( [ 'user_id' => $user->id, 'raw_material_price_id' => $price->id, 'cutting_id' => null, ], [ 'material_usage' => $materialUsage, 'remaining_material' => $remainingMaterial, ], ); $item->load([ 'rawMaterialPrice.rawMaterial:id,name,unit', 'rawMaterialPrice.media', ]); return $this->presentDraftMaterial($item); } /** * @param array $validated * @return array */ public function syncDraftResult(array $validated, User $user): array { $variant = ProductVariant::query() ->with('product:id,name') ->findOrFail($validated['product_variant_id']); $cuttingResult = (int) $validated['cutting_result']; $warehouseStock = (int) $validated['warehouse_stock']; $cuttingReject = (int) ($validated['cutting_reject'] ?? 0); if ($cuttingResult < 1) { throw ValidationException::withMessages([ 'cutting_result' => 'Hasil cutting minimal 1 pcs.', ]); } if ($warehouseStock < 0) { throw ValidationException::withMessages([ 'warehouse_stock' => 'Stok gudang tidak boleh negatif.', ]); } if ($cuttingReject < 0) { throw ValidationException::withMessages([ 'cutting_reject' => 'Reject tidak boleh negatif.', ]); } if (($warehouseStock + $cuttingReject) !== $cuttingResult) { throw ValidationException::withMessages([ 'cutting_result' => 'Hasil cutting harus sama dengan stok gudang ditambah reject.', ]); } $item = CuttingResult::query()->updateOrCreate( [ 'user_id' => $user->id, 'product_variant_id' => $variant->id, 'cutting_id' => null, ], [ 'cutting_result' => $cuttingResult, 'warehouse_stock' => $warehouseStock, 'cutting_reject' => $cuttingReject, ], ); $item->load([ 'productVariant.product:id,name', 'productVariant.media', ]); return $this->presentDraftResult($item); } public function removeDraftMaterial(User $user, RawMaterialPrice $rawMaterialPrice): void { CuttingMaterial::query() ->whereNull('cutting_id') ->where('user_id', $user->id) ->where('raw_material_price_id', $rawMaterialPrice->id) ->delete(); } public function removeDraftResult(User $user, ProductVariant $productVariant): void { CuttingResult::query() ->whereNull('cutting_id') ->where('user_id', $user->id) ->where('product_variant_id', $productVariant->id) ->delete(); } /** * @param array $validated */ public function create(array $validated, User $user): Cutting { $cutting = DB::transaction(function () use ($validated, $user): Cutting { /** @var EloquentCollection $draftMaterials */ $draftMaterials = $this->draftMaterialsQuery($user) ->with('rawMaterialPrice.rawMaterial') ->lockForUpdate() ->get(); /** @var EloquentCollection $draftResults */ $draftResults = $this->draftResultsQuery($user) ->lockForUpdate() ->get(); if ($draftMaterials->isEmpty()) { throw ValidationException::withMessages([ 'materials' => 'Tambahkan minimal satu bahan baku.', ]); } if ($draftResults->isEmpty()) { throw ValidationException::withMessages([ 'results' => 'Tambahkan minimal satu hasil produk.', ]); } $cutting = Cutting::create([ 'status' => CuttingStatus::IN_PROGRESS, 'description' => $validated['description'] ?? null, 'sewing_cost' => (int) ($validated['sewing_cost'] ?? 0), 'other_cost' => (int) ($validated['other_cost'] ?? 0), 'created_by_id' => $user->id, ]); foreach ($draftMaterials as $material) { $material->cutting_id = $cutting->id; $material->user_id = null; $material->save(); } foreach ($draftResults as $result) { $result->cutting_id = $cutting->id; $result->user_id = null; $result->save(); } $cutting->load('materials.rawMaterialPrice.rawMaterial'); $this->deductMaterialStock($cutting); return $cutting; }); $this->pushNotificationService->sendToRoles( '✂️ Proses Cutting Baru', "Proses cutting dengan deskripsi ({$cutting->description}) telah dimulai oleh {$user->profile?->full_name}.", ['owner', 'developer'], '/admin/manage/cuttings', ); return $cutting; } /** * @param array $validated */ public function update(Cutting $cutting, array $validated): void { if (! $this->isEditable($cutting->status)) { throw ValidationException::withMessages([ 'status' => 'Proses cutting tidak dapat diubah.', ]); } DB::transaction(function () use ($cutting, $validated): void { $cutting->load(['materials.rawMaterialPrice.rawMaterial', 'results']); if ($cutting->status === CuttingStatus::IN_PROGRESS) { $this->reverseTotalMaterialStock($cutting); } elseif ($cutting->status === CuttingStatus::REJECTED) { $this->reverseMaterialStock($cutting); } $cutting->materials()->delete(); $cutting->results()->delete(); $materials = $this->buildMaterials($validated['materials']); $results = $this->buildResults($validated['results']); $cutting->description = $validated['description'] ?? null; $cutting->sewing_cost = (int) ($validated['sewing_cost'] ?? 0); $cutting->other_cost = (int) ($validated['other_cost'] ?? 0); if ($cutting->status === CuttingStatus::REJECTED) { $cutting->status = CuttingStatus::IN_PROGRESS; $cutting->rejection()?->delete(); } $cutting->save(); foreach ($materials as $materialData) { $cutting->materials()->create($materialData); } foreach ($results as $resultData) { $cutting->results()->create($resultData); } if ($cutting->status === CuttingStatus::IN_PROGRESS) { $cutting->load('materials.rawMaterialPrice.rawMaterial'); $this->deductMaterialStock($cutting); } }); $description = $cutting->description ?? '-'; $this->pushNotificationService->sendToRoles( '✏️ Proses Cutting Diperbarui', "Proses cutting dengan deskripsi '{$description}' telah diperbarui.", ['owner', 'developer'], '/admin/manage/cuttings', ); } public function delete(Cutting $cutting): void { if (! in_array($cutting->status, [CuttingStatus::IN_PROGRESS, CuttingStatus::REJECTED], true)) { throw ValidationException::withMessages([ 'status' => 'Proses cutting hanya dapat dihapus saat masih proses atau ditolak.', ]); } $description = $cutting->description ?? '-'; DB::transaction(function () use ($cutting): void { $cutting->load(['materials.rawMaterialPrice.rawMaterial']); if ($cutting->status === CuttingStatus::IN_PROGRESS) { $this->reverseTotalMaterialStock($cutting); } elseif ($cutting->status === CuttingStatus::REJECTED) { $this->reverseMaterialStock($cutting); } $cutting->materials()->delete(); $cutting->results()->delete(); $cutting->delete(); }); $this->pushNotificationService->sendToRoles( '🗑️ Proses Cutting Dihapus', "Proses cutting dengan deskripsi {$description} telah dihapus.", ['owner', 'developer'], '/admin/manage/cuttings', ); } public function transitionStatus( Cutting $cutting, CuttingStatus $status, User $user, ?string $reason = null, ?string $verificationNote = null, ?array $results = null, ?array $resultPrices = null, ): void { if (! $this->canTransitionTo($cutting->status, $status)) { throw ValidationException::withMessages([ 'status' => 'Status proses cutting tidak dapat diubah.', ]); } DB::transaction(function () use ($cutting, $status, $verificationNote, $results, $resultPrices, $user, $reason): void { $cutting->load(['materials.rawMaterialPrice', 'results']); if ($status === CuttingStatus::COMPLETED) { $cutting->total_material_cost = $this->calculateTotalMaterialCost($cutting); $cutting->cost_per_unit = $this->calculateCostPerUnit($cutting); } if ($status === CuttingStatus::IN_PROGRESS) { $cutting->rejection()?->delete(); } if ($status === CuttingStatus::REJECTED) { $this->storeRejection($cutting, $reason, $user); } if ($status === CuttingStatus::VERIFIED) { if ($results !== null) { foreach ($results as $item) { $cutting->results() ->where('product_variant_id', $item['product_variant_id']) ->update([ 'warehouse_stock' => $item['warehouse_stock'], 'cutting_reject' => $item['cutting_reject'], ]); } $cutting->load('results'); } $this->applyProductStockOnVerify($cutting); $this->storeResultPrices($cutting, $resultPrices ?? []); if ($verificationNote !== null && trim($verificationNote) !== '') { $cutting->rejection()->create([ 'reason' => trim($verificationNote), 'rejected_by_id' => $user->id, ]); } } $cutting->status = $status; $cutting->save(); }); $description = $cutting->description ?? '-'; $message = match ($status) { CuttingStatus::COMPLETED => "Proses cutting dengan deskripsi '{$description}' telah selesai dan menunggu verifikasi.", CuttingStatus::VERIFIED => "Proses cutting dengan deskripsi '{$description}' telah diverifikasi.", CuttingStatus::REJECTED => "Proses cutting dengan deskripsi '{$description}' ditolak".($reason ? " dengan alasan: '{$reason}'" : '').'.', CuttingStatus::IN_PROGRESS => "Proses cutting dengan deskripsi '{$description}' dikembalikan ke proses.", default => "Status proses cutting dengan deskripsi '{$description}' telah diperbarui ke: {$status->label()}.", }; $title = match ($status) { CuttingStatus::COMPLETED => '✂️ Proses Cutting Selesai', CuttingStatus::VERIFIED => '✂️ Proses Cutting Terverifikasi', CuttingStatus::REJECTED => '✂️ Proses Cutting Ditolak', CuttingStatus::IN_PROGRESS => '✂️ Proses Cutting Dikembalikan', default => '✂️ Proses Cutting Diperbarui', }; $roles = $status === CuttingStatus::COMPLETED ? ['owner', 'developer', 'admin-toko'] : ['owner', 'developer']; $this->pushNotificationService->sendToRoles( $title, $message, $roles, '/admin/manage/cuttings', ); } /** * @param list $materials * @return list */ private function buildMaterials(array $materials): array { return collect($materials) ->map(function (array $itemData, int $index) { $price = RawMaterialPrice::query() ->with('rawMaterial') ->find($itemData['raw_material_price_id']); if ($price === null) { throw ValidationException::withMessages([ "materials.{$index}.raw_material_price_id" => 'Bahan baku tidak ditemukan.', ]); } $materialUsage = round((float) $itemData['material_usage'], 2); $remainingMaterial = round((float) $itemData['remaining_material'], 2); if ($materialUsage <= 0) { throw ValidationException::withMessages([ "materials.{$index}.material_usage" => 'Pemakaian bahan harus lebih dari 0.', ]); } if ($remainingMaterial < 0) { throw ValidationException::withMessages([ "materials.{$index}.remaining_material" => 'Sisa bahan tidak boleh negatif.', ]); } return [ 'raw_material_price_id' => $price->id, 'material_usage' => $materialUsage, 'remaining_material' => $remainingMaterial, ]; }) ->all(); } /** * @param list $results * @return list */ private function buildResults(array $results): array { return collect($results) ->map(function (array $itemData, int $index) { $variant = ProductVariant::query()->find($itemData['product_variant_id']); if ($variant === null) { throw ValidationException::withMessages([ "results.{$index}.product_variant_id" => 'Varian produk tidak ditemukan.', ]); } $cuttingResult = (int) $itemData['cutting_result']; $warehouseStock = (int) $itemData['warehouse_stock']; $cuttingReject = (int) ($itemData['cutting_reject'] ?? 0); if ($cuttingResult < 1) { throw ValidationException::withMessages([ "results.{$index}.cutting_result" => 'Hasil cutting minimal 1 pcs.', ]); } if ($warehouseStock < 0) { throw ValidationException::withMessages([ "results.{$index}.warehouse_stock" => 'Stok gudang tidak boleh negatif.', ]); } if ($cuttingReject < 0) { throw ValidationException::withMessages([ "results.{$index}.cutting_reject" => 'Reject tidak boleh negatif.', ]); } if (($warehouseStock + $cuttingReject) !== $cuttingResult) { throw ValidationException::withMessages([ "results.{$index}.cutting_result" => 'Hasil cutting harus sama dengan stok gudang ditambah reject.', ]); } return [ 'product_variant_id' => $variant->id, 'cutting_result' => $cuttingResult, 'warehouse_stock' => $warehouseStock, 'cutting_reject' => $cuttingReject, ]; }) ->all(); } private function deductMaterialStock(Cutting $cutting): void { foreach ($cutting->materials as $material) { $totalTaken = (float) $material->material_usage - (float) $material->remaining_material; $price = RawMaterialPrice::query()->lockForUpdate()->find($material->raw_material_price_id); if ($price === null) { throw ValidationException::withMessages([ 'materials' => 'Bahan baku tidak ditemukan.', ]); } if ((float) $price->stock < $totalTaken) { throw ValidationException::withMessages([ 'materials' => "Stok {$price->rawMaterial?->name} ({$price->variant}) tidak mencukupi.", ]); } $price->decrement('stock', $totalTaken); } } private function reverseTotalMaterialStock(Cutting $cutting): void { foreach ($cutting->materials as $material) { $totalTaken = (float) $material->material_usage - (float) $material->remaining_material; RawMaterialPrice::query() ->whereKey($material->raw_material_price_id) ->increment('stock', $totalTaken); } } private function reverseMaterialStock(Cutting $cutting): void { foreach ($cutting->materials as $material) { $totalTaken = (float) $material->material_usage - (float) $material->remaining_material; RawMaterialPrice::query() ->whereKey($material->raw_material_price_id) ->increment('stock', $totalTaken); } } private function applyProductStockOnVerify(Cutting $cutting): void { foreach ($cutting->results as $result) { if ($result->warehouse_stock > 0) { ProductVariant::query() ->whereKey($result->product_variant_id) ->increment('stock', $result->warehouse_stock); } if ($result->cutting_reject > 0) { ProductVariant::query() ->whereKey($result->product_variant_id) ->increment('reject_stock', $result->cutting_reject); } } } private function reverseProductStock(Cutting $cutting): void { foreach ($cutting->results as $result) { if ($result->warehouse_stock > 0) { ProductVariant::query() ->whereKey($result->product_variant_id) ->decrement('stock', $result->warehouse_stock); } if ($result->cutting_reject > 0) { ProductVariant::query() ->whereKey($result->product_variant_id) ->decrement('reject_stock', $result->cutting_reject); } } } private function storeRejection(Cutting $cutting, ?string $reason, User $user): void { if ($reason === null || trim($reason) === '') { throw ValidationException::withMessages([ 'reason' => 'Alasan penolakan wajib diisi.', ]); } $cutting->rejection()?->delete(); $cutting->rejection()->create([ 'reason' => trim($reason), 'rejected_by_id' => $user->id, ]); } private function applySorting(Builder $query, string $sort, string $direction): void { if (in_array($sort, ['created_at', 'status'], true)) { $query->orderBy($sort, $direction); return; } $query->latest(); } /** * @return Builder */ private function draftMaterialsQuery(User $user): Builder { return CuttingMaterial::query() ->whereNull('cutting_id') ->where('user_id', $user->id); } /** * @return Builder */ private function draftResultsQuery(User $user): Builder { return CuttingResult::query() ->whereNull('cutting_id') ->where('user_id', $user->id); } /** * @return array */ private function presentDraftMaterial(CuttingMaterial $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' => $rawMaterial?->unit?->value ?? '', 'unit_abbreviation' => $rawMaterial?->unit?->abbreviation() ?? '', 'stock_input' => $price?->stock_input ?? '', 'material_usage' => $this->formatQuantityInput((float) $item->material_usage), 'remaining_material' => $this->formatQuantityInput((float) $item->remaining_material), 'images' => $price ? MediaPresenter::collection($price, 'images') : [], ]; } /** * @return array */ private function presentDraftResult(CuttingResult $item): array { $variant = $item->productVariant; return [ 'product_variant_id' => $item->product_variant_id, 'product_name' => $variant?->product?->name ?? '', 'variant_name' => $variant?->name ?? '', 'stock' => $variant?->stock ?? 0, 'cutting_result' => (string) $item->cutting_result, 'warehouse_stock' => (string) $item->warehouse_stock, 'cutting_reject' => (string) $item->cutting_reject, 'images' => $variant ? MediaPresenter::collection($variant, 'images') : [], ]; } private function formatQuantityInput(float $value): string { return rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.'); } private function appendCostPreview(Cutting $cutting): void { $cutting->setAttribute('total_result_pieces', (int) $cutting->results->sum('cutting_result')); $cutting->setAttribute('total_material_usage', (float) $cutting->materials->sum(fn (CuttingMaterial $material) => (float) $material->material_usage - (float) $material->remaining_material)); $totalMaterialCost = $cutting->total_material_cost ?? $this->calculateTotalMaterialCost($cutting); $sewingCost = (int) ($cutting->sewing_cost ?? 0); $otherCost = (int) ($cutting->other_cost ?? 0); $totalProductionCost = $totalMaterialCost + $sewingCost + $otherCost; $costPerUnit = $cutting->cost_per_unit ?? $this->calculateCostPerUnit($cutting); $cutting->setAttribute('total_material_cost', $totalMaterialCost); $cutting->setAttribute('total_material_cost_formatted', 'Rp '.number_format($totalMaterialCost, 0, ',', '.')); $cutting->setAttribute('sewing_cost', $sewingCost); $cutting->setAttribute('sewing_cost_formatted', 'Rp '.number_format($sewingCost, 0, ',', '.')); $cutting->setAttribute('other_cost', $otherCost); $cutting->setAttribute('other_cost_formatted', 'Rp '.number_format($otherCost, 0, ',', '.')); $cutting->setAttribute('total_production_cost', $totalProductionCost); $cutting->setAttribute('total_production_cost_formatted', 'Rp '.number_format($totalProductionCost, 0, ',', '.')); $cutting->setAttribute('estimated_cost_per_unit', $costPerUnit); $cutting->setAttribute('estimated_cost_per_unit_formatted', 'Rp '.number_format($costPerUnit, 0, ',', '.')); } public function calculateTotalMaterialCost(Cutting $cutting): int { return (int) $cutting->materials->sum(fn (CuttingMaterial $material) => $material->materialCost()); } public function calculateTotalProductionCost(Cutting $cutting): int { return $this->calculateTotalMaterialCost($cutting) + (int) ($cutting->sewing_cost ?? 0) + (int) ($cutting->other_cost ?? 0); } public function calculateCostPerUnit(Cutting $cutting): int { $totalPieces = (int) $cutting->results->sum('cutting_result'); if ($totalPieces <= 0) { return 0; } return (int) round($this->calculateTotalProductionCost($cutting) / $totalPieces); } /** * @param list}> $resultPrices */ private function storeResultPrices(Cutting $cutting, array $resultPrices): void { $costPerUnit = (int) ($cutting->cost_per_unit ?? $this->calculateCostPerUnit($cutting)); foreach ($resultPrices as $resultData) { foreach ($resultData['prices'] as $priceData) { CuttingResultPrice::query()->create([ 'cutting_id' => $cutting->id, 'product_variant_id' => $resultData['product_variant_id'], 'price_type' => $priceData['type'], 'price' => (int) $priceData['price'], 'cost_per_unit' => $costPerUnit, ]); } } } }