refactor: remove length unit handling from CuttingMaterial and related components for improved clarity and consistency in material usage representation

This commit is contained in:
Yoga Pangestu 2026-06-21 12:59:11 +07:00
parent ec80f2145b
commit fc234a6afd
8 changed files with 34 additions and 316 deletions

View File

@ -15,14 +15,9 @@
#[Appends([
'material_usage_formatted',
'material_usage_input',
'material_usage_cm_formatted',
'material_usage_cm_input',
'remaining_material_formatted',
'remaining_material_input',
'remaining_material_cm_formatted',
'remaining_material_cm_input',
'unit_abbreviation',
'uses_length_unit',
])]
class CuttingMaterial extends Model
{
@ -86,63 +81,6 @@ public function unitAbbreviation(): Attribute
);
}
public function usesLengthUnit(): Attribute
{
return Attribute::make(
get: fn () => $this->rawMaterialPrice?->rawMaterial?->unit?->usesLengthUnit() ?? false,
);
}
public function materialUsageCmFormatted(): Attribute
{
return Attribute::make(
get: fn () => $this->formatCmQuantity($this->material_usage),
);
}
public function materialUsageCmInput(): Attribute
{
return Attribute::make(
get: fn () => $this->formatCmQuantityInput($this->material_usage),
);
}
public function remainingMaterialCmFormatted(): Attribute
{
return Attribute::make(
get: fn () => $this->formatCmQuantity($this->remaining_material),
);
}
public function remainingMaterialCmInput(): Attribute
{
return Attribute::make(
get: fn () => $this->formatCmQuantityInput($this->remaining_material),
);
}
public function materialUsageInCm(): ?float
{
$unit = $this->rawMaterialPrice?->rawMaterial?->unit;
if ($unit === null) {
return null;
}
return $unit->toCm((float) $this->material_usage);
}
public function remainingMaterialInCm(): ?float
{
$unit = $this->rawMaterialPrice?->rawMaterial?->unit;
if ($unit === null) {
return null;
}
return $unit->toCm((float) $this->remaining_material);
}
public function materialCost(): int
{
$price = $this->rawMaterialPrice;
@ -154,50 +92,6 @@ public function materialCost(): int
return (int) round((float) $this->material_usage * (int) $price->price);
}
private function formatCmQuantity(float|string|null $value): ?string
{
$unit = $this->rawMaterialPrice?->rawMaterial?->unit;
if ($unit === null) {
return null;
}
if (! $unit->usesLengthUnit()) {
return $this->formatQuantity($value);
}
$cm = $unit->toCm((float) $value);
if ($cm === null) {
return null;
}
$formatted = rtrim(rtrim(number_format($cm, 4, ',', '.'), '0'), ',');
return "{$formatted} cm";
}
private function formatCmQuantityInput(float|string|null $value): ?string
{
$unit = $this->rawMaterialPrice?->rawMaterial?->unit;
if ($unit === null) {
return null;
}
if (! $unit->usesLengthUnit()) {
return $this->formatQuantityInput($value);
}
$cm = $unit->toCm((float) $value);
if ($cm === null) {
return null;
}
return rtrim(rtrim(number_format($cm, 4, '.', ''), '0'), '.');
}
private function formatQuantity(float|string|null $value): string
{
$formatted = rtrim(rtrim(number_format((float) $value, 4, ',', '.'), '0'), ',');

View File

@ -281,17 +281,8 @@ public function syncDraftMaterial(array $validated, User $user): array
->with('rawMaterial')
->findOrFail($validated['raw_material_price_id']);
$unit = $price->rawMaterial?->unit;
$usageInput = round((float) $validated['material_usage'], 2);
$remainingInput = round((float) $validated['remaining_material'], 2);
if ($unit !== null && $unit->usesLengthUnit()) {
$materialUsage = round($unit->fromCm($usageInput), 2);
$remainingMaterial = round($unit->fromCm($remainingInput), 2);
} else {
$materialUsage = $usageInput;
$remainingMaterial = $remainingInput;
}
$materialUsage = round((float) $validated['material_usage'], 2);
$remainingMaterial = round((float) $validated['remaining_material'], 2);
if ($materialUsage <= 0) {
throw ValidationException::withMessages([
@ -652,17 +643,8 @@ private function buildMaterials(array $materials): array
]);
}
$unit = $price->rawMaterial?->unit;
$usageInput = round((float) $itemData['material_usage'], 2);
$remainingInput = round((float) $itemData['remaining_material'], 2);
if ($unit !== null && $unit->usesLengthUnit()) {
$materialUsage = round($unit->fromCm($usageInput), 2);
$remainingMaterial = round($unit->fromCm($remainingInput), 2);
} else {
$materialUsage = $usageInput;
$remainingMaterial = $remainingInput;
}
$materialUsage = round((float) $itemData['material_usage'], 2);
$remainingMaterial = round((float) $itemData['remaining_material'], 2);
if ($materialUsage <= 0) {
throw ValidationException::withMessages([
@ -890,31 +872,16 @@ private function presentDraftMaterial(CuttingMaterial $item): array
{
$price = $item->rawMaterialPrice;
$rawMaterial = $price?->rawMaterial;
$unit = $rawMaterial?->unit;
$usesLengthUnit = $unit?->usesLengthUnit() ?? false;
// 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 = rtrim(rtrim(number_format($materialUsageCm, 2, '.', ''), '0'), '.');
$remainingMaterialDisplay = rtrim(rtrim(number_format($remainingMaterialCm, 2, '.', ''), '0'), '.');
} else {
$materialUsageDisplay = $this->formatQuantityInput((float) $item->material_usage);
$remainingMaterialDisplay = $this->formatQuantityInput((float) $item->remaining_material);
}
return [
'raw_material_price_id' => $item->raw_material_price_id,
'raw_material_name' => $rawMaterial?->name ?? '',
'variant' => $price?->variant ?? '',
'unit' => $rawMaterial?->unit?->value ?? '',
'uses_length_unit' => $usesLengthUnit,
'unit_abbreviation' => $rawMaterial?->unit?->abbreviation() ?? '',
'stock_input' => $price?->stock_input ?? '',
'material_usage' => $materialUsageDisplay,
'remaining_material' => $remainingMaterialDisplay,
'material_usage' => $this->formatQuantityInput((float) $item->material_usage),
'remaining_material' => $this->formatQuantityInput((float) $item->remaining_material),
'images' => $price ? MediaPresenter::collection($price, 'images') : [],
];
}

View File

@ -17,35 +17,21 @@ const props = defineProps<{
productCatalog: CuttingProductCatalogItem[];
}>();
function usesLengthUnit(unit?: string): boolean {
return unit === 'yard' || unit === 'meter';
}
const initialData = computed(() => ({
description: props.cutting.description ?? '',
sewing_cost: String(props.cutting.sewing_cost ?? 0),
other_cost: String(props.cutting.other_cost ?? 0),
materials: props.cutting.materials.map((item) => {
const unit = item.raw_material_price?.raw_material?.unit ?? 'kilogram';
const usesCm = usesLengthUnit(unit);
return {
raw_material_price_id: item.raw_material_price_id,
raw_material_name: item.raw_material_price?.raw_material?.name ?? '',
variant: item.raw_material_price?.variant ?? '',
unit,
uses_length_unit: usesCm,
unit_abbreviation: item.raw_material_price?.raw_material?.unit_abbreviation ?? '',
stock_input: item.raw_material_price?.stock_input ?? '',
material_usage: usesCm
? (item.material_usage_cm_input ?? item.material_usage_input)
: item.material_usage_input,
remaining_material: usesCm
? (item.remaining_material_cm_input ?? item.remaining_material_input)
: item.remaining_material_input,
images: item.raw_material_price?.images ?? [],
};
}),
materials: props.cutting.materials.map((item) => ({
raw_material_price_id: item.raw_material_price_id,
raw_material_name: item.raw_material_price?.raw_material?.name ?? '',
variant: item.raw_material_price?.variant ?? '',
unit: item.raw_material_price?.raw_material?.unit ?? 'kilogram',
unit_abbreviation: item.raw_material_price?.raw_material?.unit_abbreviation ?? '',
stock_input: item.raw_material_price?.stock_input ?? '',
material_usage: item.material_usage_input,
remaining_material: item.remaining_material_input,
images: item.raw_material_price?.images ?? [],
})),
results: props.cutting.results.map((item) => ({
product_variant_id: item.product_variant_id,
product_name: item.product_variant?.product?.name ?? '',

View File

@ -45,10 +45,6 @@ import type {
import PosCatalogCard from '../../shared/PosCatalogCard.vue';
import PosCatalogVariantThumb from '../../shared/PosCatalogVariantThumb.vue';
function usesLengthUnit(unit: string): boolean {
return unit === 'yard' || unit === 'meter';
}
const props = defineProps<{
rawMaterialCatalog: CuttingRawMaterialCatalogItem[];
productCatalog: CuttingProductCatalogItem[];
@ -81,9 +77,6 @@ const form = useForm({
other_cost: '0',
});
const CM_PER_YARD = 91.44;
const CM_PER_METER = 100;
function findCatalogPrice(priceId: number) {
for (const rawMaterial of props.rawMaterialCatalog) {
const price = rawMaterial.prices.find((item) => item.id === priceId);
@ -99,18 +92,6 @@ function findCatalogPrice(priceId: number) {
return null;
}
function usageInNativeUnit(usageInput: number, unit: string): number {
if (unit === 'yard') {
return usageInput / CM_PER_YARD;
}
if (unit === 'meter') {
return usageInput / CM_PER_METER;
}
return usageInput;
}
const totalMaterialCost = computed(() =>
materialCart.value.reduce((sum, item) => {
const catalogPrice = findCatalogPrice(item.raw_material_price_id);
@ -119,10 +100,7 @@ const totalMaterialCost = computed(() =>
return sum;
}
const usage = usageInNativeUnit(
Number(item.material_usage) || 0,
catalogPrice.unit,
);
const usage = Number(item.material_usage) || 0;
return sum + Math.round(usage * catalogPrice.price);
}, 0),
@ -142,10 +120,7 @@ function materialLineCost(item: CuttingMaterialCartItem): number {
return 0;
}
const usage = usageInNativeUnit(
Number(item.material_usage) || 0,
catalogPrice.unit,
);
const usage = Number(item.material_usage) || 0;
return Math.round(usage * catalogPrice.price);
}
@ -382,7 +357,6 @@ async function addMaterial(
raw_material_name: rawMaterial.name,
variant: price.variant,
unit: rawMaterial.unit,
uses_length_unit: usesLengthUnit(rawMaterial.unit),
unit_abbreviation: rawMaterial.unit_abbreviation,
stock_input: price.stock_input,
material_usage: defaultUsage,
@ -1006,7 +980,7 @@ function submit() {
<div class="grid grid-cols-2 gap-2">
<Field>
<FieldLabel class="text-xs">
Pemakaian ({{ item.uses_length_unit ? 'cm' : item.unit_abbreviation }})
Pemakaian ({{ item.unit_abbreviation }})
</FieldLabel>
<DecimalInput
v-model="
@ -1022,7 +996,7 @@ function submit() {
</Field>
<Field>
<FieldLabel class="text-xs">
Sisa ({{ item.uses_length_unit ? 'cm' : item.unit_abbreviation }})
Sisa ({{ item.unit_abbreviation }})
</FieldLabel>
<DecimalInput
v-model="
@ -1253,11 +1227,11 @@ function submit() {
</p>
</div>
<span class="shrink-0 text-xs font-medium tabular-nums">
{{ item.material_usage }} {{ item.uses_length_unit ? 'cm' : item.unit_abbreviation }}
{{ item.material_usage }} {{ item.unit_abbreviation }}
</span>
</div>
<div class="mt-1.5 flex items-center justify-between gap-2 text-xs text-muted-foreground">
<span>Sisa: {{ item.remaining_material }} {{ item.uses_length_unit ? 'cm' : item.unit_abbreviation }}</span>
<span>Sisa: {{ item.remaining_material }} {{ item.unit_abbreviation }}</span>
<span class="font-medium text-foreground">{{ formatRupiah(materialLineCost(item)) }}</span>
</div>
</div>

View File

@ -12,59 +12,23 @@ import {
} from '@/components/ui/empty';
import type { CuttingListItem } from '@/types/cutting';
const CM_PER_YARD = 91.44;
const CM_PER_METER = 100;
function formatTotalMaterialUsage(totalUsage: number | null | undefined, materials: any[]): string {
if (!totalUsage || !materials.length) {
function formatTotalMaterialUsage(totalUsage: number | null | undefined): string {
if (!totalUsage) {
return '0';
}
const unit = materials[0]?.raw_material_price?.raw_material?.unit;
if (unit === 'yard') {
return (totalUsage * CM_PER_YARD).toFixed(2);
}
if (unit === 'meter') {
return (totalUsage * CM_PER_METER).toFixed(2);
}
return totalUsage.toFixed(2);
}
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 ?? '';
}
function formatMaterialUsage(mat: any): string {
const usage = mat.material_usage ?? 0;
const unit = mat.raw_material_price?.raw_material?.unit;
if (unit === 'yard') {
return (usage * CM_PER_YARD).toFixed(2);
}
if (unit === 'meter') {
return (usage * CM_PER_METER).toFixed(2);
}
return usage.toFixed(2);
return (mat.material_usage ?? 0).toFixed(2);
}
function getMaterialUnit(mat: any): string {
const unit = mat.raw_material_price?.raw_material?.unit;
if (unit === 'yard' || unit === 'meter') {
return 'cm';
}
return mat.raw_material_price?.raw_material?.unit_abbreviation ?? '';
}
@ -143,7 +107,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">{{ formatTotalMaterialUsage(cutting.total_material_usage, cutting.materials) }} {{ getTotalMaterialUsageUnit(cutting.materials) }}</span>
<span class="font-semibold tabular-nums">{{ formatTotalMaterialUsage(cutting.total_material_usage) }} {{ getTotalMaterialUsageUnit(cutting.materials) }}</span>
</div>
</div>

View File

@ -137,34 +137,15 @@ function getMaterialUnit(materials: any[]): string | undefined {
return materials[0]?.raw_material_price?.raw_material?.unit_abbreviation;
}
const CM_PER_YARD = 91.44;
const CM_PER_METER = 100;
function formatTotalMaterialUsage(totalUsage: number | null | undefined, materials: any[]): string {
if (!totalUsage || !materials.length) {
function formatTotalMaterialUsage(totalUsage: number | null | undefined): string {
if (!totalUsage) {
return '0';
}
const unit = materials[0]?.raw_material_price?.raw_material?.unit;
if (unit === 'yard') {
return (totalUsage * CM_PER_YARD).toFixed(2);
}
if (unit === 'meter') {
return (totalUsage * CM_PER_METER).toFixed(2);
}
return totalUsage.toFixed(2);
}
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 ?? '';
}
@ -205,7 +186,7 @@ function getTotalMaterialUsageUnit(materials: any[]): string {
<span>Total Hasil Cutting <strong class="text-primary">{{ cutting.total_result_pieces ??
0 }} pcs</strong></span>
<span>Total Pemakaian Bahan <strong class="text-primary">{{
formatTotalMaterialUsage(cutting.total_material_usage, cutting.materials) }}
formatTotalMaterialUsage(cutting.total_material_usage) }}
{{ getTotalMaterialUsageUnit(cutting.materials) }}</strong></span>
<span>Biaya Bahan <strong class="text-primary">{{
cutting.total_material_cost_formatted ?? 'Rp 0' }}</strong></span>
@ -253,15 +234,9 @@ function getTotalMaterialUsageUnit(materials: any[]): string {
</TableCell>
<TableCell class="tabular-nums">
{{ material.material_usage_formatted }}
<span v-if="material.uses_length_unit" class="text-muted-foreground">
({{ material.material_usage_cm_formatted }})
</span>
</TableCell>
<TableCell class="tabular-nums">
{{ material.remaining_material_formatted }}
<span v-if="material.uses_length_unit" class="text-muted-foreground">
({{ material.remaining_material_cm_formatted }})
</span>
</TableCell>
</TableRow>
</template>

View File

@ -3,59 +3,23 @@ import { Card } from '@/components/ui/card';
import type { CuttingListItem } from '@/types/cutting';
import DataTableActions from './data-table-actions.vue';
const CM_PER_YARD = 91.44;
const CM_PER_METER = 100;
function formatTotalMaterialUsage(totalUsage: number | null | undefined, materials: any[]): string {
if (!totalUsage || !materials.length) {
function formatTotalMaterialUsage(totalUsage: number | null | undefined): string {
if (!totalUsage) {
return '0';
}
const unit = materials[0]?.raw_material_price?.raw_material?.unit;
if (unit === 'yard') {
return (totalUsage * CM_PER_YARD).toFixed(2);
}
if (unit === 'meter') {
return (totalUsage * CM_PER_METER).toFixed(2);
}
return totalUsage.toFixed(2);
}
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 ?? '';
}
function formatMaterialUsage(mat: any): string {
const usage = mat.material_usage ?? 0;
const unit = mat.raw_material_price?.raw_material?.unit;
if (unit === 'yard') {
return (usage * CM_PER_YARD).toFixed(2);
}
if (unit === 'meter') {
return (usage * CM_PER_METER).toFixed(2);
}
return usage.toFixed(2);
return (mat.material_usage ?? 0).toFixed(2);
}
function getMaterialUnit(mat: any): string {
const unit = mat.raw_material_price?.raw_material?.unit;
if (unit === 'yard' || unit === 'meter') {
return 'cm';
}
return mat.raw_material_price?.raw_material?.unit_abbreviation ?? '';
}
@ -118,7 +82,7 @@ defineProps<{
</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">{{ formatTotalMaterialUsage(cutting.total_material_usage, cutting.materials) }} {{ getTotalMaterialUsageUnit(cutting.materials) }}</span>
<span class="font-semibold tabular-nums">{{ formatTotalMaterialUsage(cutting.total_material_usage) }} {{ getTotalMaterialUsageUnit(cutting.materials) }}</span>
</div>
</div>

View File

@ -14,9 +14,6 @@ export type CuttingMaterialListItem = {
id: number;
material_usage_formatted: string;
remaining_material_formatted: string;
uses_length_unit?: boolean;
material_usage_cm_formatted?: string;
remaining_material_cm_formatted?: string;
raw_material_price?: {
id: number;
variant: string;
@ -88,7 +85,6 @@ export type CuttingMaterialCartItem = {
raw_material_name: string;
variant: string;
unit: string;
uses_length_unit: boolean;
unit_abbreviation: string;
stock_input: string;
material_usage: string;
@ -117,8 +113,6 @@ export type CuttingEditItem = {
raw_material_price_id: number;
material_usage_input: string;
remaining_material_input: string;
material_usage_cm_input?: string | null;
remaining_material_cm_input?: string | null;
raw_material_price?: {
variant: string;
stock_input?: string;