feat: enhance form validation in Cutting module by adding error handling and displaying field-specific error messages in CuttingPosForm and related components
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (8.3) (push) Waiting to run
tests / ci (8.4) (push) Waiting to run
tests / ci (8.5) (push) Waiting to run

This commit is contained in:
Yoga Pangestu 2026-07-03 14:28:43 +07:00
parent 33afec2513
commit 64a678890e
4 changed files with 51 additions and 52 deletions

View File

@ -9,14 +9,14 @@ import type {
CuttingRawMaterialCatalogItem,
CuttingResultCartItem,
} from '@/types/cutting';
import type { CategoryOption } from '@/types/product';
import type { EnumOption } from '@/types/raw-material';
import type { MediaItem } from '@/types/media';
import {
appendRootPhotosToFormData,
createMediaUploadState,
} from '@/types/media';
import type { MediaItem } from '@/types/media';
import type { MediaUploadState } from '@/types/media';
import type { CategoryOption } from '@/types/product';
import type { EnumOption } from '@/types/raw-material';
import CuttingPosCartDetailDialog from './CuttingPosCartDetailDialog.vue';
import CuttingPosMaterialCatalogPanel from './CuttingPosMaterialCatalogPanel.vue';
import CuttingPosResultCatalogPanel from './CuttingPosResultCatalogPanel.vue';
@ -108,6 +108,7 @@ const {
function populateForm() {
if (!props.initialData) {
imageState.value = createMediaUploadState();
return;
}
@ -191,6 +192,14 @@ function submit() {
onError: (errors: Record<string, string>) => {
if (errors.system) {
toast.error(errors.system);
} else {
const firstError = Object.values(errors)[0];
if (firstError) {
toast.error(firstError);
} else {
toast.error('Perbaiki kesalahan pada form.');
}
}
},
});
@ -224,53 +233,25 @@ function onProductCreated(product: CuttingProductCatalogItem) {
<template>
<div class="grid gap-4 xl:grid-cols-[1fr_400px]">
<div class="space-y-4">
<CuttingPosMaterialCatalogPanel
v-model:material-search="materialSearch"
:filtered-raw-materials="filteredRawMaterials"
:get-material-cart-item="getMaterialCartItem"
:units="units"
@add-material="addMaterial"
@decrease-material-qty="decreaseMaterialQty"
@raw-material-created="onRawMaterialCreated"
/>
<CuttingPosMaterialCatalogPanel v-model:material-search="materialSearch"
:filtered-raw-materials="filteredRawMaterials" :get-material-cart-item="getMaterialCartItem"
:units="units" @add-material="addMaterial" @decrease-material-qty="decreaseMaterialQty"
@raw-material-created="onRawMaterialCreated" />
<CuttingPosResultCatalogPanel
v-model:product-search="productSearch"
:filtered-products="filteredProducts"
:get-result-cart-item="getResultCartItem"
@add-result="addResult"
:is-create-mode="isCreateMode"
:categories="categories"
@decrease-result-qty="decreaseResultQty"
@product-created="onProductCreated"
/>
<CuttingPosResultCatalogPanel v-model:product-search="productSearch" :filtered-products="filteredProducts"
:get-result-cart-item="getResultCartItem" @add-result="addResult" :is-create-mode="isCreateMode"
:categories="categories" @decrease-result-qty="decreaseResultQty" @product-created="onProductCreated" />
</div>
<CuttingPosSummaryPanel
v-model:image-state="imageState"
:form="form"
:material-cart="materialCart"
:result-cart="resultCart"
:total-result-pieces="totalResultPieces"
:submit-label="submitLabel"
@submit="submit"
:is-create-mode="isCreateMode"
:is-uploading="isUploading"
@open-detail="cartDetailOpen = true"
@remove-material="removeMaterial"
@sync-material-field="syncMaterialField"
@remove-result="removeResult"
@sync-result-totals="syncResultTotals"
@sync-result-field="syncResultField"
/>
<CuttingPosSummaryPanel v-model:image-state="imageState" :form="form" :material-cart="materialCart"
:result-cart="resultCart" :total-result-pieces="totalResultPieces" :submit-label="submitLabel"
@submit="submit" :is-create-mode="isCreateMode" :is-uploading="isUploading"
@open-detail="cartDetailOpen = true" @remove-material="removeMaterial"
@sync-material-field="syncMaterialField" @remove-result="removeResult"
@sync-result-totals="syncResultTotals" @sync-result-field="syncResultField" />
</div>
<CuttingPosCartDetailDialog
v-model:open="cartDetailOpen"
:material-cart="materialCart"
:result-cart="resultCart"
:material-line-cost="materialLineCost"
:total-material-cost="totalMaterialCost"
:is-create-mode="isCreateMode"
/>
<CuttingPosCartDetailDialog v-model:open="cartDetailOpen" :material-cart="materialCart" :result-cart="resultCart"
:material-line-cost="materialLineCost" :total-material-cost="totalMaterialCost"
:is-create-mode="isCreateMode" />
</template>

View File

@ -4,11 +4,15 @@ import { DecimalInput } from '@/components/form/decimal-input';
import { Button } from '@/components/ui/button';
import {
Field,
FieldError,
FieldLabel,
} from '@/components/ui/field';
import { formErrors } from '@/lib/form';
import type { FormWithErrors } from '@/lib/form';
import type { CuttingMaterialCartItem } from '@/types/cutting';
defineProps<{
form: FormWithErrors;
materialCart: CuttingMaterialCartItem[];
}>();
@ -55,15 +59,17 @@ const emit = defineEmits<{
</Button>
</div>
<Field>
<Field :data-invalid="formErrors(form, `materials.${index}.material_usage`).length > 0 ? 'true' : undefined">
<FieldLabel class="text-xs">
Pemakaian ({{ item.unit_abbreviation }})
</FieldLabel>
<DecimalInput
v-model="item.material_usage"
class="h-8"
:aria-invalid="formErrors(form, `materials.${index}.material_usage`).length > 0"
@change="emit('sync-field', index)"
/>
<FieldError :errors="formErrors(form, `materials.${index}.material_usage`)" class="text-[10px] mt-0.5 leading-tight" />
</Field>
</div>
</div>

View File

@ -5,11 +5,15 @@ import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Field,
FieldError,
FieldLabel,
} from '@/components/ui/field';
import { formErrors } from '@/lib/form';
import type { FormWithErrors } from '@/lib/form';
import type { CuttingResultCartItem } from '@/types/cutting';
defineProps<{
form: FormWithErrors;
resultCart: CuttingResultCartItem[];
totalResultPieces: number;
isCreateMode: boolean;
@ -54,19 +58,25 @@ const emit = defineEmits<{
</div>
<div v-if="!isCreateMode" class="grid grid-cols-3 gap-2">
<Field>
<Field :data-invalid="formErrors(form, `results.${index}.cutting_result`).length > 0 ? 'true' : undefined">
<FieldLabel class="text-xs">Hasil</FieldLabel>
<NumberInput v-model="item.cutting_result" class="h-8"
:aria-invalid="formErrors(form, `results.${index}.cutting_result`).length > 0"
@change="emit('sync-totals', item); emit('sync-field', index)" />
<FieldError :errors="formErrors(form, `results.${index}.cutting_result`)" class="text-[10px] mt-0.5 leading-tight" />
</Field>
<Field>
<Field :data-invalid="formErrors(form, `results.${index}.sample`).length > 0 ? 'true' : undefined">
<FieldLabel class="text-xs">Sample</FieldLabel>
<NumberInput v-model="item.sample" class="h-8"
:aria-invalid="formErrors(form, `results.${index}.sample`).length > 0"
@change="emit('sync-totals', item); emit('sync-field', index)" />
<FieldError :errors="formErrors(form, `results.${index}.sample`)" class="text-[10px] mt-0.5 leading-tight" />
</Field>
<Field>
<Field :data-invalid="formErrors(form, `results.${index}.original_outside_sample`).length > 0 ? 'true' : undefined">
<FieldLabel class="text-xs">Diluar Sample</FieldLabel>
<NumberInput v-model="item.original_outside_sample" class="h-8" />
<NumberInput v-model="item.original_outside_sample" class="h-8"
:aria-invalid="formErrors(form, `results.${index}.original_outside_sample`).length > 0" />
<FieldError :errors="formErrors(form, `results.${index}.original_outside_sample`)" class="text-[10px] mt-0.5 leading-tight" />
</Field>
</div>
</div>

View File

@ -106,6 +106,7 @@ const imageState = defineModel<MediaUploadState>('imageState', {
/>
<CuttingPosMaterialSummaryItems
:form="form"
:material-cart="materialCart"
@remove="emit('remove-material', $event)"
@sync-field="emit('sync-material-field', $event)"
@ -114,6 +115,7 @@ const imageState = defineModel<MediaUploadState>('imageState', {
<Separator />
<CuttingPosResultSummaryItems
:form="form"
:result-cart="resultCart"
:total-result-pieces="totalResultPieces"
:is-create-mode="isCreateMode"