refactor: replace remaining material with sampel and hasil cutting diluar sampel in cutting requests and models, update related validation and frontend components

This commit is contained in:
Yoga Pangestu 2026-06-27 23:50:41 +07:00
parent 1f27acc63a
commit 9dba5e1491
22 changed files with 217 additions and 250 deletions

View File

@ -25,7 +25,6 @@ public function rules(): array
Rule::exists('raw_material_prices', 'id')->whereNull('deleted_at'), Rule::exists('raw_material_prices', 'id')->whereNull('deleted_at'),
], ],
'material_usage' => ['required', 'numeric', 'decimal:0,4', 'gt:0'], 'material_usage' => ['required', 'numeric', 'decimal:0,4', 'gt:0'],
'remaining_material' => ['required', 'numeric', 'decimal:0,4', 'gte:0'],
]; ];
} }
} }

View File

@ -25,8 +25,8 @@ public function rules(): array
Rule::exists('product_variants', 'id')->whereNull('deleted_at'), Rule::exists('product_variants', 'id')->whereNull('deleted_at'),
], ],
'cutting_result' => ['required', 'integer', 'min:1'], 'cutting_result' => ['required', 'integer', 'min:1'],
'warehouse_stock' => ['required', 'integer', 'min:0'], 'sampel' => ['required', 'integer', 'min:0'],
'cutting_reject' => ['required', 'integer', 'min:0'], 'hasil_cutting_diluar_sampel' => ['required', 'integer', 'min:0'],
]; ];
} }
} }

View File

@ -40,7 +40,6 @@ public function rules(): array
Rule::exists('raw_material_prices', 'id')->whereNull('deleted_at'), Rule::exists('raw_material_prices', 'id')->whereNull('deleted_at'),
]; ];
$rules['materials.*.material_usage'] = ['required', 'numeric', 'decimal:0,4', 'gt:0']; $rules['materials.*.material_usage'] = ['required', 'numeric', 'decimal:0,4', 'gt:0'];
$rules['materials.*.remaining_material'] = ['required', 'numeric', 'decimal:0,4', 'gte:0'];
$rules['results'] = ['required', 'array', 'min:1']; $rules['results'] = ['required', 'array', 'min:1'];
$rules['results.*.product_variant_id'] = [ $rules['results.*.product_variant_id'] = [
@ -50,8 +49,8 @@ public function rules(): array
Rule::exists('product_variants', 'id')->whereNull('deleted_at'), Rule::exists('product_variants', 'id')->whereNull('deleted_at'),
]; ];
$rules['results.*.cutting_result'] = ['required', 'integer', 'min:1']; $rules['results.*.cutting_result'] = ['required', 'integer', 'min:1'];
$rules['results.*.warehouse_stock'] = ['required', 'integer', 'min:0']; $rules['results.*.sampel'] = ['required', 'integer', 'min:0'];
$rules['results.*.cutting_reject'] = ['required', 'integer', 'min:0']; $rules['results.*.hasil_cutting_diluar_sampel'] = ['required', 'integer', 'min:0'];
} }
return $rules; return $rules;
@ -67,12 +66,11 @@ public function attributes(): array
'materials' => 'bahan baku', 'materials' => 'bahan baku',
'materials.*.raw_material_price_id' => 'bahan baku', 'materials.*.raw_material_price_id' => 'bahan baku',
'materials.*.material_usage' => 'pemakaian', 'materials.*.material_usage' => 'pemakaian',
'materials.*.remaining_material' => 'sisa',
'results' => 'hasil produk', 'results' => 'hasil produk',
'results.*.product_variant_id' => 'varian produk', 'results.*.product_variant_id' => 'varian produk',
'results.*.cutting_result' => 'hasil', 'results.*.cutting_result' => 'hasil',
'results.*.warehouse_stock' => 'bagus', 'results.*.sampel' => 'sampel',
'results.*.cutting_reject' => 'reject', 'results.*.hasil_cutting_diluar_sampel' => 'hasil cutting diluar sampel',
'sewing_cost' => 'jasa jahit', 'sewing_cost' => 'jasa jahit',
'other_cost' => 'biaya lainnya', 'other_cost' => 'biaya lainnya',
]; ];

View File

@ -46,8 +46,8 @@ public function rules(): array
'verification_note' => ['nullable', 'string', 'max:500'], 'verification_note' => ['nullable', 'string', 'max:500'],
'results' => ['nullable', 'array'], 'results' => ['nullable', 'array'],
'results.*.product_variant_id' => ['required_with:results', 'integer', 'exists:product_variants,id'], 'results.*.product_variant_id' => ['required_with:results', 'integer', 'exists:product_variants,id'],
'results.*.warehouse_stock' => ['required_with:results', 'integer', 'min:0'], 'results.*.sampel' => ['required_with:results', 'integer', 'min:0'],
'results.*.cutting_reject' => ['required_with:results', 'integer', 'min:0'], 'results.*.hasil_cutting_diluar_sampel' => ['required_with:results', 'integer', 'min:0'],
'result_prices' => ['nullable', 'array'], 'result_prices' => ['nullable', 'array'],
'result_prices.*.product_variant_id' => ['required_with:result_prices', 'integer', 'exists:product_variants,id'], 'result_prices.*.product_variant_id' => ['required_with:result_prices', 'integer', 'exists:product_variants,id'],
'result_prices.*.prices' => ['required_with:result_prices', 'array', 'min:1'], 'result_prices.*.prices' => ['required_with:result_prices', 'array', 'min:1'],
@ -92,8 +92,8 @@ public function withValidator(Validator $validator): void
$cuttingResults = $cutting->results->keyBy('product_variant_id'); $cuttingResults = $cutting->results->keyBy('product_variant_id');
foreach ($this->input('results', []) as $index => $item) { foreach ($this->input('results', []) as $index => $item) {
$variantId = $item['product_variant_id'] ?? 0; $variantId = $item['product_variant_id'] ?? 0;
$warehouseStock = (int) ($item['warehouse_stock'] ?? 0); $sampel = (int) ($item['sampel'] ?? 0);
$cuttingReject = (int) ($item['cutting_reject'] ?? 0); $hasilCuttingDiluarSampel = (int) ($item['hasil_cutting_diluar_sampel'] ?? 0);
$originalResult = $cuttingResults->get($variantId); $originalResult = $cuttingResults->get($variantId);
if ($originalResult === null) { if ($originalResult === null) {
@ -102,8 +102,8 @@ public function withValidator(Validator $validator): void
continue; continue;
} }
if (($warehouseStock + $cuttingReject) !== (int) $originalResult->cutting_result) { if (($sampel + $hasilCuttingDiluarSampel) !== (int) $originalResult->cutting_result) {
$validator->errors()->add("results.{$index}.warehouse_stock", "Total jumlah (diterima + reject) harus sama dengan hasil cutting asli ({$originalResult->cutting_result} pcs)."); $validator->errors()->add("results.{$index}.sampel", "Total jumlah (sampel + hasil cutting diluar sampel) harus sama dengan hasil cutting asli ({$originalResult->cutting_result} pcs).");
} }
} }
} }

View File

@ -26,8 +26,8 @@ public function rules(): array
'verification_note' => ['nullable', 'string', 'max:500'], 'verification_note' => ['nullable', 'string', 'max:500'],
'results' => ['nullable', 'array'], 'results' => ['nullable', 'array'],
'results.*.product_variant_id' => ['required_with:results', 'integer', 'exists:product_variants,id'], 'results.*.product_variant_id' => ['required_with:results', 'integer', 'exists:product_variants,id'],
'results.*.warehouse_stock' => ['required_with:results', 'integer', 'min:0'], 'results.*.sampel' => ['required_with:results', 'integer', 'min:0'],
'results.*.cutting_reject' => ['required_with:results', 'integer', 'min:0'], 'results.*.hasil_cutting_diluar_sampel' => ['required_with:results', 'integer', 'min:0'],
'result_prices' => ['nullable', 'array'], 'result_prices' => ['nullable', 'array'],
'result_prices.*.product_variant_id' => ['required_with:result_prices', 'integer', 'exists:product_variants,id'], 'result_prices.*.product_variant_id' => ['required_with:result_prices', 'integer', 'exists:product_variants,id'],
'result_prices.*.prices' => ['required_with:result_prices', 'array', 'min:1'], 'result_prices.*.prices' => ['required_with:result_prices', 'array', 'min:1'],
@ -50,8 +50,8 @@ public function attributes(): array
'result_prices.*.prices.*.price' => 'harga jual', 'result_prices.*.prices.*.price' => 'harga jual',
'results' => 'hasil cutting', 'results' => 'hasil cutting',
'results.*.product_variant_id' => 'varian produk', 'results.*.product_variant_id' => 'varian produk',
'results.*.warehouse_stock' => 'stok bagus', 'results.*.sampel' => 'sampel',
'results.*.cutting_reject' => 'stok reject', 'results.*.hasil_cutting_diluar_sampel' => 'hasil cutting diluar sampel',
]; ];
} }
@ -71,8 +71,8 @@ public function withValidator(Validator $validator): void
$cuttingResults = $cutting->results->keyBy('product_variant_id'); $cuttingResults = $cutting->results->keyBy('product_variant_id');
foreach ($this->input('results', []) as $index => $item) { foreach ($this->input('results', []) as $index => $item) {
$variantId = $item['product_variant_id'] ?? 0; $variantId = $item['product_variant_id'] ?? 0;
$warehouseStock = (int) ($item['warehouse_stock'] ?? 0); $sampel = (int) ($item['sampel'] ?? 0);
$cuttingReject = (int) ($item['cutting_reject'] ?? 0); $hasilCuttingDiluarSampel = (int) ($item['hasil_cutting_diluar_sampel'] ?? 0);
$originalResult = $cuttingResults->get($variantId); $originalResult = $cuttingResults->get($variantId);
if ($originalResult === null) { if ($originalResult === null) {
@ -81,8 +81,8 @@ public function withValidator(Validator $validator): void
continue; continue;
} }
if (($warehouseStock + $cuttingReject) !== (int) $originalResult->cutting_result) { if (($sampel + $hasilCuttingDiluarSampel) !== (int) $originalResult->cutting_result) {
$validator->errors()->add("results.{$index}.warehouse_stock", "Total jumlah (diterima + reject) harus sama dengan hasil cutting asli ({$originalResult->cutting_result} pcs)."); $validator->errors()->add("results.{$index}.sampel", "Total jumlah (sampel + hasil cutting diluar sampel) harus sama dengan hasil cutting asli ({$originalResult->cutting_result} pcs).");
} }
} }
} }

View File

@ -15,8 +15,6 @@
#[Appends([ #[Appends([
'material_usage_formatted', 'material_usage_formatted',
'material_usage_input', 'material_usage_input',
'remaining_material_formatted',
'remaining_material_input',
'unit_abbreviation', 'unit_abbreviation',
])] ])]
class CuttingMaterial extends Model class CuttingMaterial extends Model
@ -27,7 +25,6 @@ protected function casts(): array
{ {
return [ return [
'material_usage' => 'decimal:4', 'material_usage' => 'decimal:4',
'remaining_material' => 'decimal:4',
]; ];
} }
@ -60,20 +57,6 @@ public function materialUsageInput(): Attribute
); );
} }
public function remainingMaterialFormatted(): Attribute
{
return Attribute::make(
get: fn () => $this->formatQuantity($this->remaining_material),
);
}
public function remainingMaterialInput(): Attribute
{
return Attribute::make(
get: fn () => $this->formatQuantityInput($this->remaining_material),
);
}
public function unitAbbreviation(): Attribute public function unitAbbreviation(): Attribute
{ {
return Attribute::make( return Attribute::make(
@ -89,7 +72,7 @@ public function materialCost(): int
return 0; return 0;
} }
return (int) round(((float) $this->material_usage - (float) $this->remaining_material) * (int) $price->price); return (int) round((float) $this->material_usage * (int) $price->price);
} }
private function formatQuantity(float|string|null $value): string private function formatQuantity(float|string|null $value): string

View File

@ -18,8 +18,8 @@ protected function casts(): array
{ {
return [ return [
'cutting_result' => 'integer', 'cutting_result' => 'integer',
'warehouse_stock' => 'integer', 'sampel' => 'integer',
'cutting_reject' => 'integer', 'hasil_cutting_diluar_sampel' => 'integer',
]; ];
} }

View File

@ -307,7 +307,6 @@ public function syncDraftMaterial(array $validated, User $user): array
->findOrFail($validated['raw_material_price_id']); ->findOrFail($validated['raw_material_price_id']);
$materialUsage = round((float) $validated['material_usage'], 2); $materialUsage = round((float) $validated['material_usage'], 2);
$remainingMaterial = round((float) $validated['remaining_material'], 2);
if ($materialUsage <= 0) { if ($materialUsage <= 0) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
@ -315,12 +314,6 @@ public function syncDraftMaterial(array $validated, User $user): array
]); ]);
} }
if ($remainingMaterial < 0) {
throw ValidationException::withMessages([
'remaining_material' => 'Sisa bahan tidak boleh negatif.',
]);
}
$item = CuttingMaterial::query()->updateOrCreate( $item = CuttingMaterial::query()->updateOrCreate(
[ [
'user_id' => $user->id, 'user_id' => $user->id,
@ -329,7 +322,6 @@ public function syncDraftMaterial(array $validated, User $user): array
], ],
[ [
'material_usage' => $materialUsage, 'material_usage' => $materialUsage,
'remaining_material' => $remainingMaterial,
], ],
); );
@ -352,8 +344,8 @@ public function syncDraftResult(array $validated, User $user): array
->findOrFail($validated['product_variant_id']); ->findOrFail($validated['product_variant_id']);
$cuttingResult = (int) $validated['cutting_result']; $cuttingResult = (int) $validated['cutting_result'];
$warehouseStock = (int) $validated['warehouse_stock']; $sampel = (int) $validated['sampel'];
$cuttingReject = (int) ($validated['cutting_reject'] ?? 0); $hasilCuttingDiluarSampel = (int) ($validated['hasil_cutting_diluar_sampel'] ?? 0);
if ($cuttingResult < 1) { if ($cuttingResult < 1) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
@ -361,21 +353,21 @@ public function syncDraftResult(array $validated, User $user): array
]); ]);
} }
if ($warehouseStock < 0) { if ($sampel < 0) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'warehouse_stock' => 'Stok gudang tidak boleh negatif.', 'sampel' => 'Sampel tidak boleh negatif.',
]); ]);
} }
if ($cuttingReject < 0) { if ($hasilCuttingDiluarSampel < 0) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'cutting_reject' => 'Reject tidak boleh negatif.', 'hasil_cutting_diluar_sampel' => 'Hasil cutting diluar sampel tidak boleh negatif.',
]); ]);
} }
if (($warehouseStock + $cuttingReject) !== $cuttingResult) { if (($sampel + $hasilCuttingDiluarSampel) !== $cuttingResult) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'cutting_result' => 'Hasil cutting harus sama dengan stok gudang ditambah reject.', 'cutting_result' => 'Hasil cutting harus sama dengan sampel ditambah hasil cutting diluar sampel.',
]); ]);
} }
@ -387,8 +379,8 @@ public function syncDraftResult(array $validated, User $user): array
], ],
[ [
'cutting_result' => $cuttingResult, 'cutting_result' => $cuttingResult,
'warehouse_stock' => $warehouseStock, 'sampel' => $sampel,
'cutting_reject' => $cuttingReject, 'hasil_cutting_diluar_sampel' => $hasilCuttingDiluarSampel,
], ],
); );
@ -647,8 +639,8 @@ public function transitionStatus(
$cutting->results() $cutting->results()
->where('product_variant_id', $item['product_variant_id']) ->where('product_variant_id', $item['product_variant_id'])
->update([ ->update([
'warehouse_stock' => $item['warehouse_stock'], 'sampel' => $item['sampel'],
'cutting_reject' => $item['cutting_reject'], 'hasil_cutting_diluar_sampel' => $item['hasil_cutting_diluar_sampel'],
]); ]);
} }
$cutting->load('results'); $cutting->load('results');
@ -709,8 +701,8 @@ public function transitionStatus(
} }
/** /**
* @param list<array{raw_material_price_id: int, material_usage: float|int|string, remaining_material: float|int|string}> $materials * @param list<array{raw_material_price_id: int, material_usage: float|int|string}> $materials
* @return list<array{raw_material_price_id: int, material_usage: float, remaining_material: float}> * @return list<array{raw_material_price_id: int, material_usage: float}>
*/ */
private function buildMaterials(array $materials): array private function buildMaterials(array $materials): array
{ {
@ -727,7 +719,6 @@ private function buildMaterials(array $materials): array
} }
$materialUsage = round((float) $itemData['material_usage'], 2); $materialUsage = round((float) $itemData['material_usage'], 2);
$remainingMaterial = round((float) $itemData['remaining_material'], 2);
if ($materialUsage <= 0) { if ($materialUsage <= 0) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
@ -735,24 +726,17 @@ private function buildMaterials(array $materials): array
]); ]);
} }
if ($remainingMaterial < 0) {
throw ValidationException::withMessages([
"materials.{$index}.remaining_material" => 'Sisa bahan tidak boleh negatif.',
]);
}
return [ return [
'raw_material_price_id' => $price->id, 'raw_material_price_id' => $price->id,
'material_usage' => $materialUsage, 'material_usage' => $materialUsage,
'remaining_material' => $remainingMaterial,
]; ];
}) })
->all(); ->all();
} }
/** /**
* @param list<array{product_variant_id: int, cutting_result: int|string, warehouse_stock: int|string, cutting_reject?: int|string}> $results * @param list<array{product_variant_id: int, cutting_result: int|string, sampel: int|string, hasil_cutting_diluar_sampel?: int|string}> $results
* @return list<array{product_variant_id: int, cutting_result: int, warehouse_stock: int, cutting_reject: int}> * @return list<array{product_variant_id: int, cutting_result: int, sampel: int, hasil_cutting_diluar_sampel: int}>
*/ */
private function buildResults(array $results): array private function buildResults(array $results): array
{ {
@ -767,8 +751,8 @@ private function buildResults(array $results): array
} }
$cuttingResult = (int) $itemData['cutting_result']; $cuttingResult = (int) $itemData['cutting_result'];
$warehouseStock = (int) $itemData['warehouse_stock']; $sampel = (int) $itemData['sampel'];
$cuttingReject = (int) ($itemData['cutting_reject'] ?? 0); $hasilCuttingDiluarSampel = (int) ($itemData['hasil_cutting_diluar_sampel'] ?? 0);
if ($cuttingResult < 1) { if ($cuttingResult < 1) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
@ -776,29 +760,29 @@ private function buildResults(array $results): array
]); ]);
} }
if ($warehouseStock < 0) { if ($sampel < 0) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
"results.{$index}.warehouse_stock" => 'Stok gudang tidak boleh negatif.', "results.{$index}.sampel" => 'Sampel tidak boleh negatif.',
]); ]);
} }
if ($cuttingReject < 0) { if ($hasilCuttingDiluarSampel < 0) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
"results.{$index}.cutting_reject" => 'Reject tidak boleh negatif.', "results.{$index}.hasil_cutting_diluar_sampel" => 'Hasil cutting diluar sampel tidak boleh negatif.',
]); ]);
} }
if (($warehouseStock + $cuttingReject) !== $cuttingResult) { if (($sampel + $hasilCuttingDiluarSampel) !== $cuttingResult) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
"results.{$index}.cutting_result" => 'Hasil cutting harus sama dengan stok gudang ditambah reject.', "results.{$index}.cutting_result" => 'Hasil cutting harus sama dengan sampel ditambah hasil cutting diluar sampel.',
]); ]);
} }
return [ return [
'product_variant_id' => $variant->id, 'product_variant_id' => $variant->id,
'cutting_result' => $cuttingResult, 'cutting_result' => $cuttingResult,
'warehouse_stock' => $warehouseStock, 'sampel' => $sampel,
'cutting_reject' => $cuttingReject, 'hasil_cutting_diluar_sampel' => $hasilCuttingDiluarSampel,
]; ];
}) })
->all(); ->all();
@ -807,7 +791,7 @@ private function buildResults(array $results): array
private function deductMaterialStock(Cutting $cutting): void private function deductMaterialStock(Cutting $cutting): void
{ {
foreach ($cutting->materials as $material) { foreach ($cutting->materials as $material) {
$totalTaken = (float) $material->material_usage - (float) $material->remaining_material; $totalTaken = (float) $material->material_usage;
$price = RawMaterialPrice::query()->lockForUpdate()->find($material->raw_material_price_id); $price = RawMaterialPrice::query()->lockForUpdate()->find($material->raw_material_price_id);
if ($price === null) { if ($price === null) {
@ -829,36 +813,34 @@ private function deductMaterialStock(Cutting $cutting): void
private function reverseTotalMaterialStock(Cutting $cutting): void private function reverseTotalMaterialStock(Cutting $cutting): void
{ {
foreach ($cutting->materials as $material) { foreach ($cutting->materials as $material) {
$totalTaken = (float) $material->material_usage - (float) $material->remaining_material;
RawMaterialPrice::query() RawMaterialPrice::query()
->whereKey($material->raw_material_price_id) ->whereKey($material->raw_material_price_id)
->increment('stock', $totalTaken); ->increment('stock', (float) $material->material_usage);
} }
} }
private function reverseMaterialStock(Cutting $cutting): void private function reverseMaterialStock(Cutting $cutting): void
{ {
foreach ($cutting->materials as $material) { foreach ($cutting->materials as $material) {
$totalTaken = (float) $material->material_usage - (float) $material->remaining_material;
RawMaterialPrice::query() RawMaterialPrice::query()
->whereKey($material->raw_material_price_id) ->whereKey($material->raw_material_price_id)
->increment('stock', $totalTaken); ->increment('stock', (float) $material->material_usage);
} }
} }
private function applyProductStockOnVerify(Cutting $cutting): void private function applyProductStockOnVerify(Cutting $cutting): void
{ {
foreach ($cutting->results as $result) { foreach ($cutting->results as $result) {
if ($result->warehouse_stock > 0) { if ($result->sampel > 0) {
ProductVariant::query() ProductVariant::query()
->whereKey($result->product_variant_id) ->whereKey($result->product_variant_id)
->increment('stock', $result->warehouse_stock); ->increment('stock', $result->sampel);
} }
if ($result->cutting_reject > 0) { if ($result->hasil_cutting_diluar_sampel > 0) {
ProductVariant::query() ProductVariant::query()
->whereKey($result->product_variant_id) ->whereKey($result->product_variant_id)
->increment('reject_stock', $result->cutting_reject); ->increment('reject_stock', $result->hasil_cutting_diluar_sampel);
} }
} }
} }
@ -866,16 +848,16 @@ private function applyProductStockOnVerify(Cutting $cutting): void
private function reverseProductStock(Cutting $cutting): void private function reverseProductStock(Cutting $cutting): void
{ {
foreach ($cutting->results as $result) { foreach ($cutting->results as $result) {
if ($result->warehouse_stock > 0) { if ($result->sampel > 0) {
ProductVariant::query() ProductVariant::query()
->whereKey($result->product_variant_id) ->whereKey($result->product_variant_id)
->decrement('stock', $result->warehouse_stock); ->decrement('stock', $result->sampel);
} }
if ($result->cutting_reject > 0) { if ($result->hasil_cutting_diluar_sampel > 0) {
ProductVariant::query() ProductVariant::query()
->whereKey($result->product_variant_id) ->whereKey($result->product_variant_id)
->decrement('reject_stock', $result->cutting_reject); ->decrement('reject_stock', $result->hasil_cutting_diluar_sampel);
} }
} }
} }
@ -943,7 +925,6 @@ private function presentDraftMaterial(CuttingMaterial $item): array
'unit_abbreviation' => $rawMaterial?->unit?->abbreviation() ?? '', 'unit_abbreviation' => $rawMaterial?->unit?->abbreviation() ?? '',
'stock_input' => $price?->stock_input ?? '', 'stock_input' => $price?->stock_input ?? '',
'material_usage' => $this->formatQuantityInput((float) $item->material_usage), 'material_usage' => $this->formatQuantityInput((float) $item->material_usage),
'remaining_material' => $this->formatQuantityInput((float) $item->remaining_material),
'images' => $price ? MediaPresenter::collection($price, 'images') : [], 'images' => $price ? MediaPresenter::collection($price, 'images') : [],
]; ];
} }
@ -961,8 +942,8 @@ private function presentDraftResult(CuttingResult $item): array
'variant_name' => $variant?->name ?? '', 'variant_name' => $variant?->name ?? '',
'stock' => $variant?->stock ?? 0, 'stock' => $variant?->stock ?? 0,
'cutting_result' => (string) $item->cutting_result, 'cutting_result' => (string) $item->cutting_result,
'warehouse_stock' => (string) $item->warehouse_stock, 'sampel' => (string) $item->sampel,
'cutting_reject' => (string) $item->cutting_reject, 'hasil_cutting_diluar_sampel' => (string) $item->hasil_cutting_diluar_sampel,
'images' => $variant ? MediaPresenter::collection($variant, 'images') : [], 'images' => $variant ? MediaPresenter::collection($variant, 'images') : [],
]; ];
} }
@ -975,7 +956,7 @@ private function formatQuantityInput(float $value): string
private function appendCostPreview(Cutting $cutting): void private function appendCostPreview(Cutting $cutting): void
{ {
$cutting->setAttribute('total_result_pieces', (int) $cutting->results->sum('cutting_result')); $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)); $cutting->setAttribute('total_material_usage', (float) $cutting->materials->sum(fn (CuttingMaterial $material) => (float) $material->material_usage));
$totalMaterialCost = $cutting->total_material_cost ?? $this->calculateTotalMaterialCost($cutting); $totalMaterialCost = $cutting->total_material_cost ?? $this->calculateTotalMaterialCost($cutting);
$sewingCost = (int) ($cutting->sewing_cost ?? 0); $sewingCost = (int) ($cutting->sewing_cost ?? 0);

View File

@ -71,7 +71,7 @@ public function getPendingApprovalCuttings(User $user): Collection
/** /**
* Submit verification for a completed cutting - saves data but does NOT add stock yet. * Submit verification for a completed cutting - saves data but does NOT add stock yet.
* *
* @param list<array{product_variant_id: int, warehouse_stock: int, cutting_reject: int}>|null $results * @param list<array{product_variant_id: int, sampel: int, hasil_cutting_diluar_sampel: int}>|null $results
* @param list<array{product_variant_id: int, prices: list<array{type: string, price: int}>}>|null $resultPrices * @param list<array{product_variant_id: int, prices: list<array{type: string, price: int}>}>|null $resultPrices
*/ */
public function submitVerification( public function submitVerification(
@ -96,8 +96,8 @@ public function submitVerification(
$cutting->results() $cutting->results()
->where('product_variant_id', $item['product_variant_id']) ->where('product_variant_id', $item['product_variant_id'])
->update([ ->update([
'warehouse_stock' => $item['warehouse_stock'], 'sampel' => $item['sampel'],
'cutting_reject' => $item['cutting_reject'], 'hasil_cutting_diluar_sampel' => $item['hasil_cutting_diluar_sampel'],
]); ]);
} }
$cutting->load('results'); $cutting->load('results');
@ -248,16 +248,16 @@ public function rejectVerification(
private function applyProductStockOnVerify(Cutting $cutting): void private function applyProductStockOnVerify(Cutting $cutting): void
{ {
foreach ($cutting->results as $result) { foreach ($cutting->results as $result) {
if ($result->warehouse_stock > 0) { if ($result->sampel > 0) {
ProductVariant::query() ProductVariant::query()
->whereKey($result->product_variant_id) ->whereKey($result->product_variant_id)
->increment('stock', $result->warehouse_stock); ->increment('stock', $result->sampel);
} }
if ($result->cutting_reject > 0) { if ($result->hasil_cutting_diluar_sampel > 0) {
ProductVariant::query() ProductVariant::query()
->whereKey($result->product_variant_id) ->whereKey($result->product_variant_id)
->increment('reject_stock', $result->cutting_reject); ->increment('reject_stock', $result->hasil_cutting_diluar_sampel);
} }
} }
} }

View File

@ -14,14 +14,10 @@ class CuttingMaterialFactory extends Factory
{ {
public function definition(): array public function definition(): array
{ {
$materialUsage = fake()->randomFloat(4, 1, 25);
$remainingMaterial = fake()->randomFloat(4, 0, 10);
return [ return [
'cutting_id' => Cutting::factory(), 'cutting_id' => Cutting::factory(),
'raw_material_price_id' => RawMaterialPrice::factory(), 'raw_material_price_id' => RawMaterialPrice::factory(),
'material_usage' => $materialUsage, 'material_usage' => fake()->randomFloat(4, 1, 25),
'remaining_material' => $remainingMaterial,
]; ];
} }
} }

View File

@ -15,14 +15,14 @@ class CuttingResultFactory extends Factory
public function definition(): array public function definition(): array
{ {
$cuttingResult = fake()->numberBetween(10, 200); $cuttingResult = fake()->numberBetween(10, 200);
$cuttingReject = fake()->numberBetween(0, (int) ($cuttingResult * 0.1)); $hasilCuttingDiluarSampel = fake()->numberBetween(0, (int) ($cuttingResult * 0.1));
return [ return [
'cutting_id' => Cutting::factory(), 'cutting_id' => Cutting::factory(),
'product_variant_id' => ProductVariant::factory(), 'product_variant_id' => ProductVariant::factory(),
'cutting_result' => $cuttingResult, 'cutting_result' => $cuttingResult,
'warehouse_stock' => $cuttingResult - $cuttingReject, 'sampel' => $cuttingResult - $hasilCuttingDiluarSampel,
'cutting_reject' => $cuttingReject, 'hasil_cutting_diluar_sampel' => $hasilCuttingDiluarSampel,
]; ];
} }
} }

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
// 1. Drop remaining_material from cutting_materials
Schema::table('cutting_materials', function (Blueprint $table): void {
$table->dropColumn('remaining_material');
});
// 2. Rename warehouse_stock -> sampel, cutting_reject -> hasil_cutting_diluar_sampel in cutting_results
Schema::table('cutting_results', function (Blueprint $table): void {
$table->renameColumn('warehouse_stock', 'sampel');
$table->renameColumn('cutting_reject', 'hasil_cutting_diluar_sampel');
});
}
public function down(): void
{
// 1. Add back remaining_material to cutting_materials
Schema::table('cutting_materials', function (Blueprint $table): void {
$table->decimal('remaining_material', 18, 2)->after('material_usage');
});
// 2. Rename back to original names
Schema::table('cutting_results', function (Blueprint $table): void {
$table->renameColumn('sampel', 'warehouse_stock');
$table->renameColumn('hasil_cutting_diluar_sampel', 'cutting_reject');
});
}
};

View File

@ -30,7 +30,6 @@ const initialData = computed(() => ({
item.raw_material_price?.raw_material?.unit_abbreviation ?? '', item.raw_material_price?.raw_material?.unit_abbreviation ?? '',
stock_input: item.raw_material_price?.stock_input ?? '', stock_input: item.raw_material_price?.stock_input ?? '',
material_usage: item.material_usage_input, material_usage: item.material_usage_input,
remaining_material: item.remaining_material_input,
images: item.raw_material_price?.images ?? [], images: item.raw_material_price?.images ?? [],
})), })),
results: props.cutting.results.map((item) => ({ results: props.cutting.results.map((item) => ({
@ -39,8 +38,8 @@ const initialData = computed(() => ({
variant_name: item.product_variant?.name ?? '', variant_name: item.product_variant?.name ?? '',
stock: item.product_variant?.stock ?? 0, stock: item.product_variant?.stock ?? 0,
cutting_result: String(item.cutting_result), cutting_result: String(item.cutting_result),
warehouse_stock: String(item.warehouse_stock), sampel: String(item.sampel),
cutting_reject: String(item.cutting_reject), hasil_cutting_diluar_sampel: String(item.hasil_cutting_diluar_sampel),
images: item.product_variant?.images ?? [], images: item.product_variant?.images ?? [],
})), })),
})); }));

View File

@ -235,27 +235,26 @@ function upsertResultCartItem(item: CuttingResultCartItem) {
resultCart.value[index] = { ...item }; resultCart.value[index] = { ...item };
} }
async function syncDraftMaterial(rawMaterial: CuttingRawMaterialCatalogItem, price: CatalogPrice, materialUsage: string, remainingMaterial: string) { async function syncDraftMaterial(rawMaterial: CuttingRawMaterialCatalogItem, price: CatalogPrice, materialUsage: string) {
const { item } = await apiFetch<{ item: CuttingMaterialCartItem }>(draft_materials.store.url(), { const { item } = await apiFetch<{ item: CuttingMaterialCartItem }>(draft_materials.store.url(), {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
raw_material_price_id: price.id, raw_material_price_id: price.id,
material_usage: materialUsage, material_usage: materialUsage,
remaining_material: remainingMaterial,
}), }),
}); });
upsertMaterialCartItem(item); upsertMaterialCartItem(item);
} }
async function syncDraftResult(product: CuttingProductCatalogItem, variant: CuttingProductCatalogItem['variants'][number], cuttingResult: string, warehouseStock: string, cuttingReject: string) { async function syncDraftResult(product: CuttingProductCatalogItem, variant: CuttingProductCatalogItem['variants'][number], cuttingResult: string, sampel: string, hasilCuttingDiluarSampel: string) {
const { item } = await apiFetch<{ item: CuttingResultCartItem }>(draft_results.store.url(), { const { item } = await apiFetch<{ item: CuttingResultCartItem }>(draft_results.store.url(), {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
product_variant_id: variant.id, product_variant_id: variant.id,
cutting_result: cuttingResult, cutting_result: cuttingResult,
warehouse_stock: warehouseStock, sampel: sampel,
cutting_reject: cuttingReject, hasil_cutting_diluar_sampel: hasilCuttingDiluarSampel,
}), }),
}); });
@ -300,7 +299,6 @@ async function decreaseMaterialQty(priceId: number) {
{ id: 0, name: '', unit: '', unit_label: '', unit_abbreviation: '', prices: [] } as CuttingRawMaterialCatalogItem, { id: 0, name: '', unit: '', unit_label: '', unit_abbreviation: '', prices: [] } as CuttingRawMaterialCatalogItem,
{ id: priceId, variant: '', price: 0, price_formatted: '', stock_input: '', stock_formatted: '', images: [] } as CatalogPrice, { id: priceId, variant: '', price: 0, price_formatted: '', stock_input: '', stock_formatted: '', images: [] } as CatalogPrice,
String(nextQty), String(nextQty),
item.remaining_material,
); );
} catch (error) { } catch (error) {
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui jumlah item.'); toast.error(error instanceof Error ? error.message : 'Gagal memperbarui jumlah item.');
@ -325,11 +323,10 @@ async function addMaterial(
const nextUsage = existing const nextUsage = existing
? String((Number(existing.material_usage) || 0) + increment) ? String((Number(existing.material_usage) || 0) + increment)
: defaultUsage; : defaultUsage;
const remaining = existing?.remaining_material ?? '0';
if (isCreateMode.value) { if (isCreateMode.value) {
try { try {
await syncDraftMaterial(rawMaterial, price, nextUsage, remaining); await syncDraftMaterial(rawMaterial, price, nextUsage);
} catch (error) { } catch (error) {
toast.error(error instanceof Error ? error.message : 'Gagal menyimpan item ke keranjang.'); toast.error(error instanceof Error ? error.message : 'Gagal menyimpan item ke keranjang.');
} }
@ -351,7 +348,6 @@ async function addMaterial(
unit_abbreviation: rawMaterial.unit_abbreviation, unit_abbreviation: rawMaterial.unit_abbreviation,
stock_input: price.stock_input, stock_input: price.stock_input,
material_usage: defaultUsage, material_usage: defaultUsage,
remaining_material: '0',
images: price.images ?? [], images: price.images ?? [],
}); });
} }
@ -406,8 +402,8 @@ async function decreaseResultQty(variantId: number) {
return; return;
} }
const nextWarehouse = Math.max(0, (Number(item.warehouse_stock) || 0) - 1); const nextSampel = Math.max(0, (Number(item.sampel) || 0) - 1);
const nextReject = Math.max(nextQty - nextWarehouse, 0); const nextHasilCuttingDiluarSampel = Math.max(nextQty - nextSampel, 0);
if (isCreateMode.value) { if (isCreateMode.value) {
try { try {
@ -415,8 +411,8 @@ async function decreaseResultQty(variantId: number) {
{ id: 0, name: '', variants: [] } as CuttingProductCatalogItem, { id: 0, name: '', variants: [] } as CuttingProductCatalogItem,
{ id: variantId, name: '', stock: 0, images: [] } as CuttingProductCatalogItem['variants'][number], { id: variantId, name: '', stock: 0, images: [] } as CuttingProductCatalogItem['variants'][number],
String(nextQty), String(nextQty),
String(nextWarehouse), String(nextSampel),
String(nextReject), String(nextHasilCuttingDiluarSampel),
); );
} catch (error) { } catch (error) {
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui jumlah item.'); toast.error(error instanceof Error ? error.message : 'Gagal memperbarui jumlah item.');
@ -426,7 +422,7 @@ async function decreaseResultQty(variantId: number) {
} }
item.cutting_result = String(nextQty); item.cutting_result = String(nextQty);
item.warehouse_stock = String(nextWarehouse); item.sampel = String(nextSampel);
syncResultTotals(item); syncResultTotals(item);
} }
@ -439,12 +435,12 @@ async function addResult(
); );
const nextQty = existing ? (Number(existing.cutting_result) || 0) + 1 : 1; const nextQty = existing ? (Number(existing.cutting_result) || 0) + 1 : 1;
const nextWarehouse = existing ? (Number(existing.warehouse_stock) || 0) + 1 : 1; const nextSampel = existing ? (Number(existing.sampel) || 0) + 1 : 1;
const nextReject = Math.max(nextQty - nextWarehouse, 0); const nextHasilCuttingDiluarSampel = Math.max(nextQty - nextSampel, 0);
if (isCreateMode.value) { if (isCreateMode.value) {
try { try {
await syncDraftResult(product, variant, String(nextQty), String(nextWarehouse), String(nextReject)); await syncDraftResult(product, variant, String(nextQty), String(nextSampel), String(nextHasilCuttingDiluarSampel));
} catch (error) { } catch (error) {
toast.error(error instanceof Error ? error.message : 'Gagal menyimpan item ke keranjang.'); toast.error(error instanceof Error ? error.message : 'Gagal menyimpan item ke keranjang.');
} }
@ -454,7 +450,7 @@ async function addResult(
if (existing) { if (existing) {
existing.cutting_result = String(nextQty); existing.cutting_result = String(nextQty);
existing.warehouse_stock = String(nextWarehouse); existing.sampel = String(nextSampel);
syncResultTotals(existing); syncResultTotals(existing);
return; return;
@ -466,8 +462,8 @@ async function addResult(
variant_name: variant.name, variant_name: variant.name,
stock: variant.stock, stock: variant.stock,
cutting_result: '1', cutting_result: '1',
warehouse_stock: '1', sampel: '1',
cutting_reject: '0', hasil_cutting_diluar_sampel: '0',
images: variant.images ?? [], images: variant.images ?? [],
}); });
} }
@ -493,10 +489,10 @@ async function removeResult(index: number) {
function syncResultTotals(item: CuttingResultCartItem) { function syncResultTotals(item: CuttingResultCartItem) {
const total = Number(item.cutting_result) || 0; const total = Number(item.cutting_result) || 0;
const warehouse = Number(item.warehouse_stock) || 0; const sampel = Number(item.sampel) || 0;
const reject = Math.max(total - warehouse, 0); const hasilCuttingDiluarSampel = Math.max(total - sampel, 0);
item.cutting_reject = String(reject); item.hasil_cutting_diluar_sampel = String(hasilCuttingDiluarSampel);
} }
async function syncMaterialField(index: number) { async function syncMaterialField(index: number) {
@ -512,7 +508,6 @@ async function syncMaterialField(index: number) {
body: JSON.stringify({ body: JSON.stringify({
raw_material_price_id: item.raw_material_price_id, raw_material_price_id: item.raw_material_price_id,
material_usage: item.material_usage, material_usage: item.material_usage,
remaining_material: item.remaining_material,
}), }),
}); });
} catch (error) { } catch (error) {
@ -533,8 +528,8 @@ async function syncResultField(index: number) {
body: JSON.stringify({ body: JSON.stringify({
product_variant_id: item.product_variant_id, product_variant_id: item.product_variant_id,
cutting_result: item.cutting_result, cutting_result: item.cutting_result,
warehouse_stock: item.warehouse_stock, sampel: item.sampel,
cutting_reject: item.cutting_reject, hasil_cutting_diluar_sampel: item.hasil_cutting_diluar_sampel,
}), }),
}); });
} catch (error) { } catch (error) {
@ -553,13 +548,12 @@ function buildPayload() {
payload.materials = materialCart.value.map((item) => ({ payload.materials = materialCart.value.map((item) => ({
raw_material_price_id: item.raw_material_price_id, raw_material_price_id: item.raw_material_price_id,
material_usage: item.material_usage, material_usage: item.material_usage,
remaining_material: item.remaining_material,
})); }));
payload.results = resultCart.value.map((item) => ({ payload.results = resultCart.value.map((item) => ({
product_variant_id: item.product_variant_id, product_variant_id: item.product_variant_id,
cutting_result: item.cutting_result, cutting_result: item.cutting_result,
warehouse_stock: item.warehouse_stock, sampel: item.sampel,
cutting_reject: item.cutting_reject, hasil_cutting_diluar_sampel: item.hasil_cutting_diluar_sampel,
})); }));
} }
@ -838,7 +832,7 @@ function submit() {
</Button> </Button>
</div> </div>
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-1 gap-2">
<Field> <Field>
<FieldLabel class="text-xs"> <FieldLabel class="text-xs">
Pemakaian ({{ item.unit_abbreviation }}) Pemakaian ({{ item.unit_abbreviation }})
@ -850,17 +844,6 @@ function submit() {
) )
" /> " />
</Field> </Field>
<Field>
<FieldLabel class="text-xs">
Sisa ({{ item.unit_abbreviation }})
</FieldLabel>
<DecimalInput v-model="item.remaining_material
" class="h-8" @change="
syncMaterialField(
index,
)
" />
</Field>
</div> </div>
</div> </div>
</div> </div>
@ -912,16 +895,16 @@ function submit() {
" /> " />
</Field> </Field>
<Field> <Field>
<FieldLabel class="text-xs">Bagus</FieldLabel> <FieldLabel class="text-xs">Sampel</FieldLabel>
<NumberInput v-model="item.warehouse_stock <NumberInput v-model="item.sampel
" class="h-8" @change=" " class="h-8" @change="
syncResultTotals(item); syncResultTotals(item);
syncResultField(index) syncResultField(index)
" /> " />
</Field> </Field>
<Field> <Field>
<FieldLabel class="text-xs">Reject</FieldLabel> <FieldLabel class="text-xs">Diluar Sampel</FieldLabel>
<NumberInput v-model="item.cutting_reject <NumberInput v-model="item.hasil_cutting_diluar_sampel
" class="h-8" /> " class="h-8" />
</Field> </Field>
</div> </div>
@ -1023,7 +1006,6 @@ function submit() {
</span> </span>
</div> </div>
<div class="mt-1.5 flex items-center justify-between gap-2 text-xs text-muted-foreground"> <div class="mt-1.5 flex items-center justify-between gap-2 text-xs text-muted-foreground">
<span>Sisa: {{ item.remaining_material }} {{ item.unit_abbreviation }}</span>
<span class="font-medium text-foreground">{{ formatRupiah(materialLineCost(item)) <span class="font-medium text-foreground">{{ formatRupiah(materialLineCost(item))
}}</span> }}</span>
</div> </div>
@ -1046,9 +1028,9 @@ function submit() {
<div class="mt-1.5 flex items-center gap-3 text-xs tabular-nums"> <div class="mt-1.5 flex items-center gap-3 text-xs tabular-nums">
<span>Hasil: <strong>{{ item.cutting_result }}</strong></span> <span>Hasil: <strong>{{ item.cutting_result }}</strong></span>
<span class="text-muted-foreground">·</span> <span class="text-muted-foreground">·</span>
<span>Bagus: <strong>{{ item.warehouse_stock }}</strong></span> <span>Sampel: <strong>{{ item.sampel }}</strong></span>
<span class="text-muted-foreground">·</span> <span class="text-muted-foreground">·</span>
<span>Reject: <strong>{{ item.cutting_reject }}</strong></span> <span>Diluar Sampel: <strong>{{ item.hasil_cutting_diluar_sampel }}</strong></span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -30,7 +30,7 @@ function getGroupedMaterialUsage(materials: any[]): MaterialUsageGroup[] {
materials.forEach((mat) => { materials.forEach((mat) => {
const unit = mat.raw_material_price?.raw_material?.unit_abbreviation ?? ''; const unit = mat.raw_material_price?.raw_material?.unit_abbreviation ?? '';
const usage = Number(mat.material_usage ?? 0) - Number(mat.remaining_material ?? 0); const usage = Number(mat.material_usage ?? 0);
groups[unit] = (groups[unit] ?? 0) + usage; groups[unit] = (groups[unit] ?? 0) + usage;
}); });

View File

@ -143,7 +143,7 @@ function getGroupedMaterialUsage(materials: any[]): MaterialUsageGroup[] {
materials.forEach((mat) => { materials.forEach((mat) => {
const unit = mat.raw_material_price?.raw_material?.unit_abbreviation ?? ''; const unit = mat.raw_material_price?.raw_material?.unit_abbreviation ?? '';
const usage = Number(mat.material_usage ?? 0) - Number(mat.remaining_material ?? 0); const usage = Number(mat.material_usage ?? 0);
groups[unit] = (groups[unit] ?? 0) + usage; groups[unit] = (groups[unit] ?? 0) + usage;
}); });
@ -209,19 +209,18 @@ function getGroupedMaterialUsage(materials: any[]): MaterialUsageGroup[] {
<TableRow> <TableRow>
<TableHead>Varian</TableHead> <TableHead>Varian</TableHead>
<TableHead>Pemakaian</TableHead> <TableHead>Pemakaian</TableHead>
<TableHead>Sisa</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
<TableRow v-if="!cutting.materials.length" :key="`${cutting.id}-material-empty`"> <TableRow v-if="!cutting.materials.length" :key="`${cutting.id}-material-empty`">
<TableCell colspan="3" class="text-muted-foreground"> <TableCell colspan="2" class="text-muted-foreground">
Belum ada bahan baku Belum ada bahan baku
</TableCell> </TableCell>
</TableRow> </TableRow>
<template v-else v-for="group in getGroupedMaterials(cutting.materials)" <template v-else v-for="group in getGroupedMaterials(cutting.materials)"
:key="group.rawMaterialId"> :key="group.rawMaterialId">
<TableRow class="bg-muted/20 hover:bg-muted/20"> <TableRow class="bg-muted/20 hover:bg-muted/20">
<TableCell colspan="3" class="font-semibold text-foreground"> <TableCell colspan="2" class="font-semibold text-foreground">
{{ group.rawMaterialName }} {{ group.rawMaterialName }}
<Badge v-if="group.unitLabel" variant="secondary" <Badge v-if="group.unitLabel" variant="secondary"
class="ml-2 font-normal"> class="ml-2 font-normal">
@ -236,9 +235,6 @@ function getGroupedMaterialUsage(materials: any[]): MaterialUsageGroup[] {
<TableCell class="tabular-nums"> <TableCell class="tabular-nums">
{{ material.material_usage_formatted }} {{ material.material_usage_formatted }}
</TableCell> </TableCell>
<TableCell class="tabular-nums">
{{ material.remaining_material_formatted }}
</TableCell>
</TableRow> </TableRow>
</template> </template>
</TableBody> </TableBody>
@ -254,8 +250,8 @@ function getGroupedMaterialUsage(materials: any[]): MaterialUsageGroup[] {
<TableRow> <TableRow>
<TableHead>Varian</TableHead> <TableHead>Varian</TableHead>
<TableHead>Hasil</TableHead> <TableHead>Hasil</TableHead>
<TableHead>Bagus</TableHead> <TableHead>Sampel</TableHead>
<TableHead>Reject</TableHead> <TableHead>Diluar Sampel</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
@ -279,10 +275,10 @@ function getGroupedMaterialUsage(materials: any[]): MaterialUsageGroup[] {
{{ result.cutting_result }} pcs {{ result.cutting_result }} pcs
</TableCell> </TableCell>
<TableCell class="tabular-nums"> <TableCell class="tabular-nums">
{{ result.warehouse_stock }} pcs {{ result.sampel }} pcs
</TableCell> </TableCell>
<TableCell class="tabular-nums"> <TableCell class="tabular-nums">
{{ result.cutting_reject }} pcs {{ result.hasil_cutting_diluar_sampel }} pcs
</TableCell> </TableCell>
</TableRow> </TableRow>
</template> </template>

View File

@ -21,7 +21,7 @@ function getGroupedMaterialUsage(materials: any[]): MaterialUsageGroup[] {
materials.forEach((mat) => { materials.forEach((mat) => {
const unit = mat.raw_material_price?.raw_material?.unit_abbreviation ?? ''; const unit = mat.raw_material_price?.raw_material?.unit_abbreviation ?? '';
const usage = Number(mat.material_usage ?? 0) - Number(mat.remaining_material ?? 0); const usage = Number(mat.material_usage ?? 0);
groups[unit] = (groups[unit] ?? 0) + usage; groups[unit] = (groups[unit] ?? 0) + usage;
}); });

View File

@ -67,10 +67,10 @@ const verifyForm = useForm({
product_variant_id: res.product_variant?.id || 0, product_variant_id: res.product_variant?.id || 0,
name: `${res.product_variant?.product?.name || ''} (${res.product_variant?.name || ''})`, name: `${res.product_variant?.product?.name || ''} (${res.product_variant?.name || ''})`,
cutting_result: res.cutting_result, cutting_result: res.cutting_result,
warehouse_stock: res.warehouse_stock, sampel: res.sampel,
cutting_reject: res.cutting_reject, hasil_cutting_diluar_sampel: res.hasil_cutting_diluar_sampel,
original_warehouse_stock: res.warehouse_stock, original_sampel: res.sampel,
original_cutting_reject: res.cutting_reject, original_hasil_cutting_diluar_sampel: res.hasil_cutting_diluar_sampel,
prices: buildEmptyPrices(), prices: buildEmptyPrices(),
})), })),
result_prices: [] as Array<{ result_prices: [] as Array<{
@ -87,10 +87,10 @@ watch(verifyDialogOpen, (isOpen) => {
product_variant_id: res.product_variant?.id || 0, product_variant_id: res.product_variant?.id || 0,
name: `${res.product_variant?.product?.name || ''} (${res.product_variant?.name || ''})`, name: `${res.product_variant?.product?.name || ''} (${res.product_variant?.name || ''})`,
cutting_result: res.cutting_result, cutting_result: res.cutting_result,
warehouse_stock: res.warehouse_stock, sampel: res.sampel,
cutting_reject: res.cutting_reject, hasil_cutting_diluar_sampel: res.hasil_cutting_diluar_sampel,
original_warehouse_stock: res.warehouse_stock, original_sampel: res.sampel,
original_cutting_reject: res.cutting_reject, original_hasil_cutting_diluar_sampel: res.hasil_cutting_diluar_sampel,
prices: buildEmptyPrices(), prices: buildEmptyPrices(),
})); }));
verifyForm.result_prices = []; verifyForm.result_prices = [];
@ -105,8 +105,8 @@ watch(allMatches, (matches) => {
verifyForm.results = verifyForm.results.map((result) => ({ verifyForm.results = verifyForm.results.map((result) => ({
...result, ...result,
warehouse_stock: result.original_warehouse_stock, sampel: result.original_sampel,
cutting_reject: result.original_cutting_reject, hasil_cutting_diluar_sampel: result.original_hasil_cutting_diluar_sampel,
})); }));
}); });
@ -116,8 +116,8 @@ function setAllMatches(value: boolean) {
if (value) { if (value) {
verifyForm.results = verifyForm.results.map((result) => ({ verifyForm.results = verifyForm.results.map((result) => ({
...result, ...result,
warehouse_stock: result.original_warehouse_stock, sampel: result.original_sampel,
cutting_reject: result.original_cutting_reject, hasil_cutting_diluar_sampel: result.original_hasil_cutting_diluar_sampel,
})); }));
} }
} }
@ -159,11 +159,11 @@ function canPerformAction(action: CuttingStatusAction): boolean {
function statusConfirmDescription(action: CuttingStatusAction): string { function statusConfirmDescription(action: CuttingStatusAction): string {
if (action.status === CuttingStatus.COMPLETED) { if (action.status === CuttingStatus.COMPLETED) {
return 'Cutting akan ditandai selesai. Stok bahan baku akan dipotong dan sisa dikembalikan. Menunggu verifikasi admin toko.'; return 'Cutting akan ditandai selesai. Stok bahan baku akan dipotong sesuai pemakaian. Menunggu verifikasi admin toko.';
} }
if (action.status === CuttingStatus.VERIFIED) { if (action.status === CuttingStatus.VERIFIED) {
return 'Hasil cutting akan diverifikasi. Stok bagus dan reject akan ditambahkan ke produk.'; return 'Hasil cutting akan diverifikasi. Stok sampel dan hasil cutting diluar sampel akan ditambahkan ke produk.';
} }
if (action.status === CuttingStatus.IN_PROGRESS) { if (action.status === CuttingStatus.IN_PROGRESS) {
@ -197,10 +197,10 @@ function submitVerify() {
.transform((data) => ({ .transform((data) => ({
status: data.status, status: data.status,
verification_note: data.verification_note, verification_note: data.verification_note,
results: data.results.map(({ product_variant_id, warehouse_stock, cutting_reject }) => ({ results: data.results.map(({ product_variant_id, sampel, hasil_cutting_diluar_sampel }) => ({
product_variant_id, product_variant_id,
warehouse_stock, sampel,
cutting_reject, hasil_cutting_diluar_sampel,
})), })),
result_prices: buildResultPricesPayload(), result_prices: buildResultPricesPayload(),
})) }))
@ -379,34 +379,34 @@ function actionIcon(status: string) {
<div> <div>
<span class="text-muted-foreground block mb-0.5">Data cutting:</span> <span class="text-muted-foreground block mb-0.5">Data cutting:</span>
<span class="font-medium tabular-nums"> <span class="font-medium tabular-nums">
{{ result.original_warehouse_stock }} bagus · {{ result.original_sampel }} sampel ·
{{ result.original_cutting_reject }} reject {{ result.original_hasil_cutting_diluar_sampel }} diluar sampel
</span> </span>
</div> </div>
<div> <div>
<span class="text-muted-foreground block">Stok Reject:</span> <span class="text-muted-foreground block">Diluar Sampel:</span>
<Badge variant="secondary" class="font-semibold"> <Badge variant="secondary" class="font-semibold">
{{ result.cutting_reject }} pcs {{ result.hasil_cutting_diluar_sampel }} pcs
</Badge> </Badge>
</div> </div>
</div> </div>
<div class="grid grid-cols-2 gap-2 items-end text-xs"> <div class="grid grid-cols-2 gap-2 items-end text-xs">
<div> <div>
<label :for="`good-${index}`" class="text-muted-foreground block mb-0.5">Stok Bagus <label :for="`sampel-${index}`" class="text-muted-foreground block mb-0.5">Sampel
(diterima):</label> (diterima):</label>
<Input :id="`good-${index}`" type="number" v-model.number="result.warehouse_stock" <Input :id="`sampel-${index}`" type="number" v-model.number="result.sampel"
min="0" :max="result.cutting_result" class="h-8 w-full px-2 text-xs" min="0" :max="result.cutting_result" class="h-8 w-full px-2 text-xs"
:disabled="allMatches" :disabled="allMatches"
@input="result.cutting_reject = result.cutting_result - result.warehouse_stock" /> @input="result.hasil_cutting_diluar_sampel = result.cutting_result - result.sampel" />
</div> </div>
<div> <div>
<label :for="`reject-${index}`" class="text-muted-foreground block mb-0.5">Stok <label :for="`diluar-sampel-${index}`" class="text-muted-foreground block mb-0.5">Diluar
Reject Sampel
(diterima):</label> (diterima):</label>
<Input :id="`reject-${index}`" type="number" v-model.number="result.cutting_reject" <Input :id="`diluar-sampel-${index}`" type="number" v-model.number="result.hasil_cutting_diluar_sampel"
min="0" :max="result.cutting_result" class="h-8 w-full px-2 text-xs" min="0" :max="result.cutting_result" class="h-8 w-full px-2 text-xs"
:disabled="allMatches" :disabled="allMatches"
@input="result.warehouse_stock = result.cutting_result - result.cutting_reject" /> @input="result.sampel = result.cutting_result - result.hasil_cutting_diluar_sampel" />
</div> </div>
</div> </div>

View File

@ -35,7 +35,7 @@ defineProps<{
{{ res.product_variant?.product?.name }} ({{ res.product_variant?.name }}) - {{ res.product_variant?.product?.name }} ({{ res.product_variant?.name }}) -
{{ res.cutting_result }} pcs {{ res.cutting_result }} pcs
<span class="text-[10px]"> <span class="text-[10px]">
({{ res.warehouse_stock }} bagus, {{ res.cutting_reject }} reject) ({{ res.sampel }} sampel, {{ res.hasil_cutting_diluar_sampel }} diluar sampel)
</span> </span>
</li> </li>
<li v-if="!cutting.results.length">Belum ada hasil produk</li> <li v-if="!cutting.results.length">Belum ada hasil produk</li>

View File

@ -100,8 +100,8 @@ const showingCount = computed(() => props.cuttings.length);
<TableRow> <TableRow>
<TableHead>Varian</TableHead> <TableHead>Varian</TableHead>
<TableHead>Hasil</TableHead> <TableHead>Hasil</TableHead>
<TableHead>Bagus</TableHead> <TableHead>Sampel</TableHead>
<TableHead>Reject</TableHead> <TableHead>Diluar Sampel</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
@ -125,10 +125,10 @@ const showingCount = computed(() => props.cuttings.length);
{{ result.cutting_result }} pcs {{ result.cutting_result }} pcs
</TableCell> </TableCell>
<TableCell class="tabular-nums"> <TableCell class="tabular-nums">
{{ result.warehouse_stock }} pcs {{ result.sampel }} pcs
</TableCell> </TableCell>
<TableCell class="tabular-nums"> <TableCell class="tabular-nums">
{{ result.cutting_reject }} pcs {{ result.hasil_cutting_diluar_sampel }} pcs
</TableCell> </TableCell>
</TableRow> </TableRow>
</template> </template>

View File

@ -56,10 +56,10 @@ const verifyForm = useForm({
product_variant_id: res.product_variant?.id || 0, product_variant_id: res.product_variant?.id || 0,
name: `${res.product_variant?.product?.name || ''} (${res.product_variant?.name || ''})`, name: `${res.product_variant?.product?.name || ''} (${res.product_variant?.name || ''})`,
cutting_result: res.cutting_result, cutting_result: res.cutting_result,
warehouse_stock: res.warehouse_stock, sampel: res.sampel,
cutting_reject: res.cutting_reject, hasil_cutting_diluar_sampel: res.hasil_cutting_diluar_sampel,
original_warehouse_stock: res.warehouse_stock, original_sampel: res.sampel,
original_cutting_reject: res.cutting_reject, original_hasil_cutting_diluar_sampel: res.hasil_cutting_diluar_sampel,
})), })),
variant_prices: [] as VariantPriceRow[], variant_prices: [] as VariantPriceRow[],
shared_prices: buildEmptyPrices(), shared_prices: buildEmptyPrices(),
@ -73,10 +73,10 @@ watch(verifyDialogOpen, (isOpen) => {
product_variant_id: res.product_variant?.id || 0, product_variant_id: res.product_variant?.id || 0,
name: `${res.product_variant?.product?.name || ''} (${res.product_variant?.name || ''})`, name: `${res.product_variant?.product?.name || ''} (${res.product_variant?.name || ''})`,
cutting_result: res.cutting_result, cutting_result: res.cutting_result,
warehouse_stock: res.warehouse_stock, sampel: res.sampel,
cutting_reject: res.cutting_reject, hasil_cutting_diluar_sampel: res.hasil_cutting_diluar_sampel,
original_warehouse_stock: res.warehouse_stock, original_sampel: res.sampel,
original_cutting_reject: res.cutting_reject, original_hasil_cutting_diluar_sampel: res.hasil_cutting_diluar_sampel,
})); }));
verifyForm.variant_prices = props.cutting.results.map(res => ({ verifyForm.variant_prices = props.cutting.results.map(res => ({
product_variant_id: res.product_variant?.id || 0, product_variant_id: res.product_variant?.id || 0,
@ -95,8 +95,8 @@ watch(allMatches, (matches) => {
verifyForm.results = verifyForm.results.map((result) => ({ verifyForm.results = verifyForm.results.map((result) => ({
...result, ...result,
warehouse_stock: result.original_warehouse_stock, sampel: result.original_sampel,
cutting_reject: result.original_cutting_reject, hasil_cutting_diluar_sampel: result.original_hasil_cutting_diluar_sampel,
})); }));
}); });
@ -106,8 +106,8 @@ function setAllMatches(value: boolean) {
if (value) { if (value) {
verifyForm.results = verifyForm.results.map((result) => ({ verifyForm.results = verifyForm.results.map((result) => ({
...result, ...result,
warehouse_stock: result.original_warehouse_stock, sampel: result.original_sampel,
cutting_reject: result.original_cutting_reject, hasil_cutting_diluar_sampel: result.original_hasil_cutting_diluar_sampel,
})); }));
} }
} }
@ -134,10 +134,10 @@ function submitVerify() {
verifyForm verifyForm
.transform((data) => ({ .transform((data) => ({
verification_note: data.verification_note, verification_note: data.verification_note,
results: data.results.map(({ product_variant_id, warehouse_stock, cutting_reject }) => ({ results: data.results.map(({ product_variant_id, sampel, hasil_cutting_diluar_sampel }) => ({
product_variant_id, product_variant_id,
warehouse_stock, sampel,
cutting_reject, hasil_cutting_diluar_sampel,
})), })),
result_prices: buildResultPricesPayload(), result_prices: buildResultPricesPayload(),
})) }))
@ -186,26 +186,26 @@ function submitVerify() {
<p class="font-medium text-sm truncate">{{ result.name }}</p> <p class="font-medium text-sm truncate">{{ result.name }}</p>
<p class="text-xs text-muted-foreground"> <p class="text-xs text-muted-foreground">
{{ result.cutting_result }} pcs {{ result.cutting_result }} pcs
<span class="text-[10px]">({{ result.original_warehouse_stock }} bagus, {{ <span class="text-[10px]">({{ result.original_sampel }} sampel, {{
result.original_cutting_reject }} reject)</span> result.original_hasil_cutting_diluar_sampel }} diluar sampel)</span>
</p> </p>
</div> </div>
<div class="flex items-center gap-2 shrink-0"> <div class="flex items-center gap-2 shrink-0">
<div class="text-center"> <div class="text-center">
<label :for="`stock-good-${index}`" <label :for="`stock-sampel-${index}`"
class="text-[10px] text-muted-foreground block">Bagus</label> class="text-[10px] text-muted-foreground block">Sampel</label>
<Input :id="`stock-good-${index}`" type="number" <Input :id="`stock-sampel-${index}`" type="number"
v-model.number="result.warehouse_stock" min="0" :max="result.cutting_result" v-model.number="result.sampel" min="0" :max="result.cutting_result"
class="h-7 w-16 px-1.5 text-xs text-center" :disabled="allMatches" class="h-7 w-16 px-1.5 text-xs text-center" :disabled="allMatches"
@input="result.cutting_reject = result.cutting_result - result.warehouse_stock" /> @input="result.hasil_cutting_diluar_sampel = result.cutting_result - result.sampel" />
</div> </div>
<div class="text-center"> <div class="text-center">
<label :for="`stock-reject-${index}`" <label :for="`stock-diluar-sampel-${index}`"
class="text-[10px] text-muted-foreground block">Reject</label> class="text-[10px] text-muted-foreground block">Diluar Sampel</label>
<Input :id="`stock-reject-${index}`" type="number" <Input :id="`stock-diluar-sampel-${index}`" type="number"
v-model.number="result.cutting_reject" min="0" :max="result.cutting_result" v-model.number="result.hasil_cutting_diluar_sampel" min="0" :max="result.cutting_result"
class="h-7 w-16 px-1.5 text-xs text-center" :disabled="allMatches" class="h-7 w-16 px-1.5 text-xs text-center" :disabled="allMatches"
@input="result.warehouse_stock = result.cutting_result - result.cutting_reject" /> @input="result.sampel = result.cutting_result - result.hasil_cutting_diluar_sampel" />
</div> </div>
</div> </div>
</div> </div>

View File

@ -13,7 +13,6 @@ export type CuttingStatusAction = {
export type CuttingMaterialListItem = { export type CuttingMaterialListItem = {
id: number; id: number;
material_usage_formatted: string; material_usage_formatted: string;
remaining_material_formatted: string;
raw_material_price?: { raw_material_price?: {
id: number; id: number;
variant: string; variant: string;
@ -41,8 +40,8 @@ export type CuttingResultPriceItem = {
export type CuttingResultListItem = { export type CuttingResultListItem = {
id: number; id: number;
cutting_result: number; cutting_result: number;
warehouse_stock: number; sampel: number;
cutting_reject: number; hasil_cutting_diluar_sampel: number;
product_variant?: { product_variant?: {
id: number; id: number;
name: string; name: string;
@ -100,7 +99,6 @@ export type CuttingMaterialCartItem = {
unit_abbreviation: string; unit_abbreviation: string;
stock_input: string; stock_input: string;
material_usage: string; material_usage: string;
remaining_material: string;
images?: MediaItem[]; images?: MediaItem[];
}; };
@ -110,8 +108,8 @@ export type CuttingResultCartItem = {
variant_name: string; variant_name: string;
stock: number; stock: number;
cutting_result: string; cutting_result: string;
warehouse_stock: string; sampel: string;
cutting_reject: string; hasil_cutting_diluar_sampel: string;
images?: MediaItem[]; images?: MediaItem[];
}; };
@ -124,7 +122,6 @@ export type CuttingEditItem = {
materials: Array<{ materials: Array<{
raw_material_price_id: number; raw_material_price_id: number;
material_usage_input: string; material_usage_input: string;
remaining_material_input: string;
raw_material_price?: { raw_material_price?: {
variant: string; variant: string;
stock_input?: string; stock_input?: string;
@ -139,8 +136,8 @@ export type CuttingEditItem = {
results: Array<{ results: Array<{
product_variant_id: number; product_variant_id: number;
cutting_result: number; cutting_result: number;
warehouse_stock: number; sampel: number;
cutting_reject: number; hasil_cutting_diluar_sampel: number;
product_variant?: { product_variant?: {
name: string; name: string;
stock: number; stock: number;