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

View File

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

View File

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

View File

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