feat: enhance form validation in Cutting module by adding error handling and displaying field-specific error messages in CuttingPosForm and related components
This commit is contained in:
parent
33afec2513
commit
64a678890e
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user