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,
|
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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user