select(['id', 'created_by_id', 'status', 'description', 'total_material_cost', 'cost_per_unit', 'created_at']) ->with([ 'createdBy:id', 'createdBy.userProfile:id,user_id,full_name', 'cuttingResults:id,cutting_id,product_name,cutting_result', 'cuttingMaterials:id,cutting_id,raw_material_price_id,material_usage,material_result,combination_id', 'cuttingMaterials.rawMaterialPrice:id,raw_material_id,variant', 'cuttingMaterials.rawMaterialPrice.rawMaterial:id,name,unit', 'cuttingMaterialCombinations:id,cutting_id,material_result', ]) ->when($search, function ($q) use ($search) { $q->whereHas('cuttingResults', fn ($rq) => $rq->where('product_name', 'like', "%{$search}%")) ->orWhere('description', 'like', "%{$search}%"); }) ->when($filters['product_name'] ?? null, fn ($q, $productName) => $q->whereHas('cuttingResults', fn ($rq) => $rq->where('product_name', $productName))) ->when($filters['status'] ?? null, fn ($q, $status) => $q->where('status', $status)) ->orderBy($sort, $direction) ->paginate($perPage); $paginator->getCollection()->each(function (Cutting $cutting) { $cuttingMedia = $cutting->getFirstMedia('images'); $cutting->photo_url = $cuttingMedia ? $this->s3Service->getTemporaryUrl($cuttingMedia->getPath()) : null; $cutting->photo_conversion_url = $cuttingMedia ? $this->s3Service->getTemporaryUrl($cuttingMedia->getPath('thumb')) : null; $cutting->cuttingMaterials->each(function (CuttingMaterial $material) { $media = $material->rawMaterialPrice?->getFirstMedia('images'); if ($material->rawMaterialPrice) { $material->rawMaterialPrice->photo_url = $media ? $this->s3Service->getTemporaryUrl($media->getPath()) : null; $material->rawMaterialPrice->photo_conversion_url = $media ? $this->s3Service->getTemporaryUrl($media->getPath('thumb')) : null; } }); }); return $paginator; } public function getProductNames(): Collection { return CuttingResult::query() ->select('product_name') ->whereNotNull('product_name') ->where('product_name', '!=', '') ->distinct() ->orderBy('product_name') ->get(); } public function getForShow(Cutting $cutting): array { $cutting->load([ 'createdBy:id', 'createdBy.userProfile:id,user_id,full_name', 'cuttingResults:id,cutting_id,product_name,cutting_result,sample,original_outside_sample', 'cuttingMaterials:id,cutting_id,raw_material_price_id,material_usage,material_result,combination_id', 'cuttingMaterials.rawMaterialPrice:id,raw_material_id,variant', 'cuttingMaterials.rawMaterialPrice.rawMaterial:id,name,unit', 'cuttingMaterialCombinations:id,cutting_id,material_result', ]); $cuttingMedia = $cutting->getFirstMedia('images'); $cutting->photo_url = $cuttingMedia ? $this->s3Service->getTemporaryUrl($cuttingMedia->getPath()) : null; $cutting->photo_conversion_url = $cuttingMedia ? $this->s3Service->getTemporaryUrl($cuttingMedia->getPath('thumb')) : null; $cutting->cuttingMaterials->each(function (CuttingMaterial $material) { $media = $material->rawMaterialPrice?->getFirstMedia('images'); if ($material->rawMaterialPrice) { $material->rawMaterialPrice->photo_url = $media ? $this->s3Service->getTemporaryUrl($media->getPath()) : null; $material->rawMaterialPrice->photo_conversion_url = $media ? $this->s3Service->getTemporaryUrl($media->getPath('thumb')) : null; } }); return [ 'id' => $cutting->id, 'status' => $cutting->status->value, 'status_label' => $cutting->status_label, 'description' => $cutting->description, 'total_material_cost' => $cutting->total_material_cost, 'formatted_total_material_cost' => $cutting->formatted_total_material_cost, 'cost_per_unit' => $cutting->cost_per_unit, 'formatted_cost_per_unit' => $cutting->formatted_cost_per_unit, 'created_at' => $cutting->created_at, 'formatted_created_at' => $cutting->formatted_created_at, 'photo_url' => $cutting->photo_url, 'photo_conversion_url' => $cutting->photo_conversion_url, 'created_by' => [ 'id' => $cutting->createdBy->id, 'user_profile' => [ 'full_name' => $cutting->createdBy->userProfile->full_name ?? '-', ], ], 'cutting_results' => $cutting->cuttingResults->map(fn ($r) => [ 'id' => $r->id, 'product_name' => $r->product_name, 'cutting_result' => $r->cutting_result, 'sample' => $r->sample, 'original_outside_sample' => $r->original_outside_sample, ]), 'cutting_materials' => $cutting->cuttingMaterials->map(fn ($m) => [ 'id' => $m->id, 'material_usage' => $m->material_usage, 'material_result' => $m->material_result, 'combination_id' => $m->combination_id, 'raw_material_price' => [ 'id' => $m->rawMaterialPrice->id, 'variant' => $m->rawMaterialPrice->variant, 'price' => $m->rawMaterialPrice->price, 'photo_url' => $m->rawMaterialPrice->photo_url, 'photo_conversion_url' => $m->rawMaterialPrice->photo_conversion_url, 'raw_material' => [ 'id' => $m->rawMaterialPrice->rawMaterial->id, 'name' => $m->rawMaterialPrice->rawMaterial->name, 'unit' => $m->rawMaterialPrice->rawMaterial->unit, ], ], ]), 'cutting_material_combinations' => $cutting->cuttingMaterialCombinations->map(fn ($c) => [ 'id' => $c->id, 'material_result' => $c->material_result, ]), ]; } public function getForEdit(Cutting $cutting): array { $cutting->load([ 'cuttingResults', 'cuttingMaterials.rawMaterialPrice.rawMaterial', 'cuttingMaterialCombinations', ]); $result = $cutting->cuttingResults->first(); $materials = $cutting->cuttingMaterials->map(function (CuttingMaterial $material) { $media = $material->rawMaterialPrice?->getFirstMedia('images'); return [ 'id' => $material->id, 'raw_material_price_id' => $material->raw_material_price_id, 'material_usage' => $material->material_usage, 'material_result' => $material->material_result, 'combination_id' => $material->combination_id, 'variant' => $material->rawMaterialPrice?->variant, 'photo_url' => $media ? $this->s3Service->getTemporaryUrl($media->getPath()) : null, ]; }); $combinations = $cutting->cuttingMaterialCombinations->map(function (CuttingMaterialCombination $combination) use ($materials) { $materialIndices = $materials ->filter(fn ($m) => $m['combination_id'] === $combination->id) ->keys() ->values(); return [ 'id' => $combination->id, 'material_result' => $combination->material_result, 'material_indices' => $materialIndices, ]; }); $cuttingMedia = $cutting->getFirstMedia('images'); $photoKey = $cuttingMedia?->getCustomProperty('s3_key') ?? $cuttingMedia?->file_name; $photoUrl = $cuttingMedia ? $this->s3Service->getTemporaryUrl($cuttingMedia->getPath()) : null; return [ 'id' => $cutting->id, 'status' => $cutting->status->value, 'description' => $cutting->description, 'product_name' => $result?->product_name ?? '', 'sample' => $result?->sample ?? 0, 'original_outside_sample' => $result?->original_outside_sample ?? 0, 'cutting_result' => $result?->cutting_result ?? 0, 'materials' => $materials, 'combinations' => $combinations, 'photo_key' => $photoKey, 'photo_url' => $photoUrl, ]; } public function store(array $data): Cutting { return DB::transaction(function () use ($data) { foreach ($data['materials'] as $materialData) { $usage = (int) ($materialData['material_usage'] ?? 0); if ($usage <= 0) { continue; } $price = RawMaterialPrice::find($materialData['raw_material_price_id']); if (! $price) { continue; } if ($usage > $price->stock) { throw ValidationException::withMessages([ 'materials' => "Stok {$price->variant} tidak mencukupi. Tersedia: {$price->stock}, dibutuhkan: {$usage}.", ]); } } $cutting = Cutting::create([ 'created_by_id' => auth()->id(), 'status' => 'in_progress', 'description' => $data['description'] ?? null, ]); $totalMaterialCost = 0; $now = now(); $combinations = $data['combinations'] ?? []; $combinationMap = []; foreach ($combinations as $index => $combo) { $combination = CuttingMaterialCombination::create([ 'cutting_id' => $cutting->id, 'user_id' => auth()->id(), 'material_result' => $combo['material_result'] ?? null, 'created_at' => $now, 'updated_at' => $now, ]); $combinationMap[$index] = $combination->id; } foreach ($data['materials'] as $materialData) { $price = RawMaterialPrice::find($materialData['raw_material_price_id']); $materialCost = $price ? $price->price * ($materialData['material_usage'] ?? 0) : 0; $totalMaterialCost += $materialCost; $combinationId = null; if (isset($materialData['combination_index']) && isset($combinationMap[$materialData['combination_index']])) { $combinationId = $combinationMap[$materialData['combination_index']]; } CuttingMaterial::create([ 'cutting_id' => $cutting->id, 'user_id' => auth()->id(), 'raw_material_price_id' => $materialData['raw_material_price_id'], 'material_usage' => $materialData['material_usage'] ?? 0, 'material_result' => $materialData['material_result'] ?? null, 'combination_id' => $combinationId, 'created_at' => $now, 'updated_at' => $now, ]); if ($price && ($materialData['material_usage'] ?? 0) > 0) { $price->decrement('stock', (int) $materialData['material_usage']); } } $costPerUnit = 0; if (($data['cutting_result'] ?? 0) > 0) { $costPerUnit = (int) ($totalMaterialCost / $data['cutting_result']); } $cutting->update([ 'total_material_cost' => $totalMaterialCost, 'cost_per_unit' => $costPerUnit, ]); CuttingResult::create([ 'cutting_id' => $cutting->id, 'user_id' => auth()->id(), 'product_name' => $data['product_name'] ?? null, 'sample' => $data['sample'] ?? null, 'original_outside_sample' => $data['original_outside_sample'] ?? null, 'cutting_result' => $data['cutting_result'] ?? null, 'created_at' => $now, 'updated_at' => $now, ]); if (! empty($data['photo_key'])) { $this->registerMedia( model: $cutting, s3Key: $data['photo_key'], collectionName: 'images', orderColumn: 1, ); } return $cutting; }); } public function update(Cutting $cutting, array $data): Cutting { return DB::transaction(function () use ($cutting, $data) { $cutting->load(['cuttingMaterials.rawMaterialPrice']); foreach ($cutting->cuttingMaterials as $oldMaterial) { if ($oldMaterial->material_usage > 0 && $oldMaterial->rawMaterialPrice) { $oldMaterial->rawMaterialPrice->increment('stock', (int) $oldMaterial->material_usage); } } foreach ($data['materials'] as $materialData) { $usage = (int) ($materialData['material_usage'] ?? 0); if ($usage <= 0) { continue; } $price = RawMaterialPrice::find($materialData['raw_material_price_id']); if (! $price) { continue; } if ($usage > $price->stock) { throw ValidationException::withMessages([ 'materials' => "Stok {$price->variant} tidak mencukupi. Tersedia: {$price->stock}, dibutuhkan: {$usage}.", ]); } } $cutting->load(['cuttingMaterials', 'cuttingMaterialCombinations', 'cuttingResults']); $cutting->cuttingResults()->delete(); $cutting->cuttingMaterials()->delete(); $cutting->cuttingMaterialCombinations()->delete(); $totalMaterialCost = 0; $now = now(); $combinations = $data['combinations'] ?? []; $combinationMap = []; foreach ($combinations as $index => $combo) { $combination = CuttingMaterialCombination::create([ 'cutting_id' => $cutting->id, 'user_id' => auth()->id(), 'material_result' => $combo['material_result'] ?? null, 'created_at' => $now, 'updated_at' => $now, ]); $combinationMap[$index] = $combination->id; } foreach ($data['materials'] as $materialData) { $price = RawMaterialPrice::find($materialData['raw_material_price_id']); $materialCost = $price ? $price->price * ($materialData['material_usage'] ?? 0) : 0; $totalMaterialCost += $materialCost; $combinationId = null; if (isset($materialData['combination_index']) && isset($combinationMap[$materialData['combination_index']])) { $combinationId = $combinationMap[$materialData['combination_index']]; } CuttingMaterial::create([ 'cutting_id' => $cutting->id, 'user_id' => auth()->id(), 'raw_material_price_id' => $materialData['raw_material_price_id'], 'material_usage' => $materialData['material_usage'] ?? 0, 'material_result' => $materialData['material_result'] ?? null, 'combination_id' => $combinationId, 'created_at' => $now, 'updated_at' => $now, ]); if ($price && ($materialData['material_usage'] ?? 0) > 0) { $price->decrement('stock', (int) $materialData['material_usage']); } } $costPerUnit = 0; if (($data['cutting_result'] ?? 0) > 0) { $costPerUnit = (int) ($totalMaterialCost / $data['cutting_result']); } $cutting->update([ 'description' => $data['description'] ?? null, 'total_material_cost' => $totalMaterialCost, 'cost_per_unit' => $costPerUnit, ]); CuttingResult::create([ 'cutting_id' => $cutting->id, 'user_id' => auth()->id(), 'product_name' => $data['product_name'] ?? null, 'sample' => $data['sample'] ?? null, 'original_outside_sample' => $data['original_outside_sample'] ?? null, 'cutting_result' => $data['cutting_result'] ?? null, 'created_at' => $now, 'updated_at' => $now, ]); $this->syncPhoto($cutting, $data); return $cutting; }); } public function destroy(Cutting $cutting): bool { return DB::transaction(function () use ($cutting) { $cutting->load('cuttingMaterials.rawMaterialPrice'); foreach ($cutting->cuttingMaterials as $material) { if ($material->material_usage > 0 && $material->rawMaterialPrice) { $material->rawMaterialPrice->increment('stock', (int) $material->material_usage); } } $cutting->cuttingResults()->delete(); $cutting->cuttingMaterials()->delete(); $cutting->cuttingMaterialCombinations()->delete(); $cutting->delete(); return true; }); } }