refactor: update material usage and remaining material precision to four decimal places across cutting management components for improved accuracy

This commit is contained in:
Yoga Pangestu 2026-06-21 11:02:54 +07:00
parent 9c205cf3d0
commit 175e0dc5af
11 changed files with 123 additions and 128 deletions

View File

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

View File

@ -39,8 +39,8 @@ public function rules(): array
'distinct',
Rule::exists('raw_material_prices', 'id')->whereNull('deleted_at'),
];
$rules['materials.*.material_usage'] = ['required', 'numeric', 'decimal:0,2', 'gt:0'];
$rules['materials.*.remaining_material'] = ['required', 'numeric', 'decimal:0,2', 'gte: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.*.product_variant_id'] = [

View File

@ -31,8 +31,8 @@ class CuttingMaterial extends Model
protected function casts(): array
{
return [
'material_usage' => 'decimal:2',
'remaining_material' => 'decimal:2',
'material_usage' => 'decimal:4',
'remaining_material' => 'decimal:4',
];
}
@ -172,7 +172,7 @@ private function formatCmQuantity(float|string|null $value): ?string
return null;
}
$formatted = rtrim(rtrim(number_format($cm, 2, ',', '.'), '0'), ',');
$formatted = rtrim(rtrim(number_format($cm, 4, ',', '.'), '0'), ',');
return "{$formatted} cm";
}
@ -195,18 +195,18 @@ private function formatCmQuantityInput(float|string|null $value): ?string
return null;
}
return rtrim(rtrim(number_format($cm, 2, '.', ''), '0'), '.');
return rtrim(rtrim(number_format($cm, 4, '.', ''), '0'), '.');
}
private function formatQuantity(float|string|null $value): string
{
$formatted = rtrim(rtrim(number_format((float) $value, 2, ',', '.'), '0'), ',');
$formatted = rtrim(rtrim(number_format((float) $value, 4, ',', '.'), '0'), ',');
return "{$formatted} {$this->unitAbbreviation}";
}
private function formatQuantityInput(float|string|null $value): string
{
return rtrim(rtrim(number_format((float) $value, 2, '.', ''), '0'), '.');
return rtrim(rtrim(number_format((float) $value, 4, '.', ''), '0'), '.');
}
}

View File

@ -284,12 +284,12 @@ public function syncDraftMaterial(array $validated, User $user): array
->findOrFail($validated['raw_material_price_id']);
$unit = $price->rawMaterial?->unit;
$usageInput = round((float) $validated['material_usage'], 2);
$remainingInput = round((float) $validated['remaining_material'], 2);
$usageInput = round((float) $validated['material_usage'], 4);
$remainingInput = round((float) $validated['remaining_material'], 4);
if ($unit !== null && $unit->usesLengthUnit()) {
$materialUsage = round($unit->fromCm($usageInput), 2);
$remainingMaterial = round($unit->fromCm($remainingInput), 2);
$materialUsage = round($unit->fromCm($usageInput), 4);
$remainingMaterial = round($unit->fromCm($remainingInput), 4);
} else {
$materialUsage = $usageInput;
$remainingMaterial = $remainingInput;
@ -655,12 +655,12 @@ private function buildMaterials(array $materials): array
}
$unit = $price->rawMaterial?->unit;
$usageInput = round((float) $itemData['material_usage'], 2);
$remainingInput = round((float) $itemData['remaining_material'], 2);
$usageInput = round((float) $itemData['material_usage'], 4);
$remainingInput = round((float) $itemData['remaining_material'], 4);
if ($unit !== null && $unit->usesLengthUnit()) {
$materialUsage = round($unit->fromCm($usageInput), 2);
$remainingMaterial = round($unit->fromCm($remainingInput), 2);
$materialUsage = round($unit->fromCm($usageInput), 4);
$remainingMaterial = round($unit->fromCm($remainingInput), 4);
} else {
$materialUsage = $usageInput;
$remainingMaterial = $remainingInput;
@ -898,10 +898,10 @@ private function presentDraftMaterial(CuttingMaterial $item): array
// Convert stored native unit back to cm for display
if ($usesLengthUnit && $unit !== null) {
$materialUsageCm = round($unit->toCm((float) $item->material_usage), 2);
$remainingMaterialCm = round($unit->toCm((float) $item->remaining_material), 2);
$materialUsageDisplay = (string) $materialUsageCm;
$remainingMaterialDisplay = (string) $remainingMaterialCm;
$materialUsageCm = round($unit->toCm((float) $item->material_usage), 4);
$remainingMaterialCm = round($unit->toCm((float) $item->remaining_material), 4);
$materialUsageDisplay = rtrim(rtrim(number_format($materialUsageCm, 4, '.', ''), '0'), '.');
$remainingMaterialDisplay = rtrim(rtrim(number_format($remainingMaterialCm, 4, '.', ''), '0'), '.');
} else {
$materialUsageDisplay = $this->formatQuantityInput((float) $item->material_usage);
$remainingMaterialDisplay = $this->formatQuantityInput((float) $item->remaining_material);
@ -942,7 +942,7 @@ private function presentDraftResult(CuttingResult $item): array
private function formatQuantityInput(float $value): string
{
return rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.');
return rtrim(rtrim(number_format($value, 4, '.', ''), '0'), '.');
}
private function appendCostPreview(Cutting $cutting): void

View File

@ -0,0 +1,24 @@
<?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
{
Schema::table('cutting_materials', function (Blueprint $table): void {
$table->decimal('material_usage', 18, 4)->change();
$table->decimal('remaining_material', 18, 4)->change();
});
}
public function down(): void
{
Schema::table('cutting_materials', function (Blueprint $table): void {
$table->decimal('material_usage', 18, 2)->change();
$table->decimal('remaining_material', 18, 2)->change();
});
}
};

View File

@ -36,7 +36,7 @@ return '';
let decimalPart = parts[1];
if (decimalPart !== undefined) {
decimalPart = decimalPart.slice(0, 2);
decimalPart = decimalPart.slice(0, 4);
return `${integerPart},${decimalPart}`;
}
@ -68,7 +68,7 @@ function parseDecimal(value: string): string {
const cleanParts = clean.split('.');
if (cleanParts[1] !== undefined) {
clean = `${cleanParts[0]}.${cleanParts[1].slice(0, 2)}`;
clean = `${cleanParts[0]}.${cleanParts[1].slice(0, 4)}`;
}
return clean;

View File

@ -3,11 +3,8 @@ import { useForm } from '@inertiajs/vue3';
import { Minus, Plus, Save, Scissors, Search, Trash2 } from '@lucide/vue';
import { computed, ref, watch } from 'vue';
import { toast } from 'vue-sonner';
import PosCatalogCard from '../../shared/PosCatalogCard.vue';
import PosCatalogVariantThumb from '../../shared/PosCatalogVariantThumb.vue';
import { DecimalInput } from '@/components/form/decimal-input';
import { NumberInput } from '@/components/form/number-input';
import { RupiahInput } from '@/components/form/rupiah-input';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@ -45,6 +42,8 @@ import type {
CuttingRawMaterialCatalogItem,
CuttingResultCartItem,
} from '@/types/cutting';
import PosCatalogCard from '../../shared/PosCatalogCard.vue';
import PosCatalogVariantThumb from '../../shared/PosCatalogVariantThumb.vue';
function usesLengthUnit(unit: string): boolean {
return unit === 'yard' || unit === 'meter';
@ -167,7 +166,7 @@ const estimatedCostPerUnit = computed(() => {
return 0;
}
return Math.round(totalProductionCost.value / totalResultPieces.value);
return Math.round(totalMaterialCost.value / totalResultPieces.value);
});
function populateForm() {
@ -314,7 +313,8 @@ async function decreaseMaterialQty(priceId: number) {
return;
}
const nextQty = (Number(item.material_usage) || 0) - 1;
const decrement = item.uses_length_unit ? 100 : 1;
const nextQty = (Number(item.material_usage) || 0) - decrement;
if (nextQty <= 0) {
const index = materialCart.value.findIndex(
@ -355,8 +355,9 @@ async function addMaterial(
);
const defaultUsage = usesLengthUnit(rawMaterial.unit) ? '100' : '1';
const increment = usesLengthUnit(rawMaterial.unit) ? 100 : 1;
const nextUsage = existing
? String((Number(existing.material_usage) || 0) + 1)
? String((Number(existing.material_usage) || 0) + increment)
: defaultUsage;
const remaining = existing?.remaining_material ?? '0';
@ -1002,7 +1003,7 @@ function submit() {
</Button>
</div>
<div class="grid grid-cols-3 gap-2">
<div class="grid grid-cols-2 gap-2">
<Field>
<FieldLabel class="text-xs">
Pemakaian ({{ item.uses_length_unit ? 'cm' : item.unit_abbreviation }})
@ -1035,16 +1036,6 @@ function submit() {
"
/>
</Field>
<Field>
<FieldLabel class="text-xs">
Harga Modal
</FieldLabel>
<div
class="flex h-8 items-center rounded-md border bg-muted/40 px-2 text-xs font-medium tabular-nums"
>
{{ formatRupiah(materialLineCost(item)) }}
</div>
</Field>
</div>
</div>
</div>
@ -1151,41 +1142,6 @@ function submit() {
</div>
</div>
<Separator />
<div class="grid gap-3 sm:grid-cols-2">
<Field>
<FieldLabel for="sewing_cost">Jasa Jahit</FieldLabel>
<RupiahInput
id="sewing_cost"
v-model="form.sewing_cost"
/>
<FieldError
:errors="formErrors(form, 'sewing_cost')"
/>
</Field>
<Field>
<FieldLabel for="other_cost">Biaya Lainnya</FieldLabel>
<RupiahInput
id="other_cost"
v-model="form.other_cost"
/>
<FieldError
:errors="formErrors(form, 'other_cost')"
/>
</Field>
</div>
<Button
type="button"
variant="outline"
class="w-full"
:disabled="materialCart.length === 0"
@click="costModalOpen = true"
>
Lihat Harga Modal
</Button>
<Button
type="submit"
class="w-full"
@ -1238,17 +1194,9 @@ function submit() {
{{ formatRupiah(materialLineCost(item)) }}
</span>
</div>
<div class="flex items-center justify-between gap-3 border-t pt-2">
<span class="text-muted-foreground">Jasa Jahit</span>
<span class="font-medium tabular-nums">{{ formatRupiah(sewingCostAmount) }}</span>
</div>
<div class="flex items-center justify-between gap-3">
<span class="text-muted-foreground">Biaya Lainnya</span>
<span class="font-medium tabular-nums">{{ formatRupiah(otherCostAmount) }}</span>
</div>
<div class="flex items-center justify-between gap-3 border-t pt-2 font-semibold">
<span>Total Harga Modal</span>
<span class="tabular-nums">{{ formatRupiah(totalProductionCost) }}</span>
<span class="tabular-nums">{{ formatRupiah(totalMaterialCost) }}</span>
</div>
</div>
@ -1345,7 +1293,7 @@ function submit() {
<div class="border-t pt-3">
<div class="flex items-center justify-between text-sm font-semibold">
<span>Total Harga Modal</span>
<span class="text-primary">{{ formatRupiah(totalProductionCost) }}</span>
<span class="text-primary">{{ formatRupiah(totalMaterialCost) }}</span>
</div>
</div>
</DialogContent>

View File

@ -15,6 +15,37 @@ import {
EmptyHeader,
EmptyTitle,
} from '@/components/ui/empty';
const CM_PER_YARD = 91.44;
const CM_PER_METER = 100;
function formatTotalMaterialUsage(totalUsage: number | null | undefined, materials: any[]): string {
if (!totalUsage || !materials.length) {
return '0';
}
const unit = materials[0]?.raw_material_price?.raw_material?.unit;
if (unit === 'yard') {
return (totalUsage * CM_PER_YARD).toFixed(4);
}
if (unit === 'meter') {
return (totalUsage * CM_PER_METER).toFixed(4);
}
return totalUsage.toFixed(4);
}
function getTotalMaterialUsageUnit(materials: any[]): string {
const unit = materials[0]?.raw_material_price?.raw_material?.unit;
if (unit === 'yard' || unit === 'meter') {
return 'cm';
}
return materials[0]?.raw_material_price?.raw_material?.unit_abbreviation ?? '';
}
import type { CuttingListItem } from '@/types/cutting';
const props = defineProps<{
@ -100,11 +131,7 @@ const totalCompleted = computed(() => props.cuttings.length);
</div>
<div class="flex items-center justify-between text-[11px]">
<span class="text-muted-foreground">Total Pemakaian Bahan:</span>
<span class="font-semibold tabular-nums">{{ cutting.total_material_usage ?? 0 }}</span>
</div>
<div v-if="cutting.estimated_cost_per_unit_formatted" class="flex items-center justify-between text-[11px]">
<span class="text-muted-foreground">Harga Modal:</span>
<span class="font-semibold">{{ cutting.estimated_cost_per_unit_formatted }}/pcs</span>
<span class="font-semibold tabular-nums">{{ formatTotalMaterialUsage(cutting.total_material_usage, cutting.materials) }} {{ getTotalMaterialUsageUnit(cutting.materials) }}</span>
</div>
</div>

View File

@ -207,11 +207,6 @@ function getTotalMaterialUsageUnit(materials: any[]): string {
<span>Total Pemakaian Bahan <strong class="text-primary">{{
formatTotalMaterialUsage(cutting.total_material_usage, cutting.materials) }}
{{ getTotalMaterialUsageUnit(cutting.materials) }}</strong></span>
<span>Total Biaya Produksi <strong class="text-primary">{{
cutting.total_production_cost_formatted ?? 0 }}</strong></span>
<span>Harga Modal <strong class="text-primary">{{
cutting.estimated_cost_per_unit_formatted ??
0 }}</strong></span>
</div>
</div>
</div>

View File

@ -15,6 +15,37 @@ import {
EmptyHeader,
EmptyTitle,
} from '@/components/ui/empty';
const CM_PER_YARD = 91.44;
const CM_PER_METER = 100;
function formatTotalMaterialUsage(totalUsage: number | null | undefined, materials: any[]): string {
if (!totalUsage || !materials.length) {
return '0';
}
const unit = materials[0]?.raw_material_price?.raw_material?.unit;
if (unit === 'yard') {
return (totalUsage * CM_PER_YARD).toFixed(4);
}
if (unit === 'meter') {
return (totalUsage * CM_PER_METER).toFixed(4);
}
return totalUsage.toFixed(4);
}
function getTotalMaterialUsageUnit(materials: any[]): string {
const unit = materials[0]?.raw_material_price?.raw_material?.unit;
if (unit === 'yard' || unit === 'meter') {
return 'cm';
}
return materials[0]?.raw_material_price?.raw_material?.unit_abbreviation ?? '';
}
import type { CuttingListItem } from '@/types/cutting';
const props = defineProps<{
@ -100,7 +131,7 @@ const totalInProgress = computed(() => props.cuttings.length);
</div>
<div class="flex items-center justify-between text-[11px]">
<span class="text-muted-foreground">Total Pemakaian Bahan:</span>
<span class="font-semibold tabular-nums">{{ cutting.total_material_usage ?? 0 }}</span>
<span class="font-semibold tabular-nums">{{ formatTotalMaterialUsage(cutting.total_material_usage, cutting.materials) }} {{ getTotalMaterialUsageUnit(cutting.materials) }}</span>
</div>
</div>

View File

@ -477,36 +477,6 @@ function actionIcon(status: string) {
/>
</div>
<div
v-if="cutting.estimated_cost_per_unit_formatted"
class="rounded-lg border bg-muted/40 p-3 text-sm space-y-1"
>
<p class="font-medium">Harga Modal Batch</p>
<p class="flex justify-between gap-2 text-muted-foreground">
<span>Bahan baku</span>
<span>{{ cutting.total_material_cost_formatted ?? '-' }}</span>
</p>
<p class="flex justify-between gap-2 text-muted-foreground">
<span>Jasa jahit</span>
<span>{{ cutting.sewing_cost_formatted ?? '-' }}</span>
</p>
<p class="flex justify-between gap-2 text-muted-foreground">
<span>Biaya lainnya</span>
<span>{{ cutting.other_cost_formatted ?? '-' }}</span>
</p>
<p class="flex justify-between gap-2 font-medium">
<span>Total</span>
<span>{{ cutting.total_production_cost_formatted ?? '-' }}</span>
</p>
<p class="flex justify-between gap-2 text-muted-foreground">
<span>Total hasil</span>
<span>{{ cutting.total_result_pieces ?? 0 }} pcs</span>
</p>
<p class="font-semibold tabular-nums pt-1 border-t">
{{ cutting.estimated_cost_per_unit_formatted }} / pcs
</p>
</div>
<div class="space-y-3">
<div v-for="(result, index) in verifyForm.results" :key="result.product_variant_id" class="p-3 border rounded-lg space-y-3">
<div class="font-medium text-sm">