Refactor form error handling in various components to utilize a centralized formErrors function. This change improves code consistency and readability by standardizing error message retrieval across LoginForm, CashTransactionFormModal, EmployeeAdvanceFormModal, ExpenseFormModal, and others. Update error handling in fields for better user feedback.

This commit is contained in:
Yoga Pangestu 2026-06-12 16:00:03 +07:00
parent 1e32837d76
commit 09f35db9d8
21 changed files with 127 additions and 151 deletions

View File

@ -10,6 +10,7 @@ import {
FieldLabel
} from '@/components/ui/field'
import { Input } from '@/components/ui/input'
import { formErrors } from '@/lib/form'
import { cn } from "@/lib/utils"
import FieldSeparator from './ui/field/FieldSeparator.vue'
@ -54,7 +55,7 @@ function submit() {
Email/Username
</FieldLabel>
<Input id="login" v-model="form.login" type="text" placeholder="m@example.com" required />
<FieldError :errors="form.errors.login ? [form.errors.login] : []" />
<FieldError :errors="formErrors(form, 'login')" />
</Field>
<Field>
<div class="flex items-center">
@ -67,7 +68,7 @@ function submit() {
</div>
<Input id="password" v-model="form.password" type="password" placeholder="********"
required />
<FieldError :errors="form.errors.password ? [form.errors.password] : []" />
<FieldError :errors="formErrors(form, 'password')" />
</Field>
<Field>
<Button type="submit" :disabled="form.processing">

View File

@ -21,6 +21,7 @@ import {
} from '@/components/ui/field';
import { RupiahInput } from '@/components/ui/rupiah-input';
import { Textarea } from '@/components/ui/textarea';
import { formErrors } from '@/lib/form';
import { parseRupiah } from '@/lib/rupiah';
import type { CashTransactionListItem } from '@/types/cash';
import { appendRootPhotosToFormData, createMediaUploadState } from '@/types/media';
@ -86,10 +87,6 @@ function buildFormData(forUpdate: boolean): FormData {
return formData;
}
function formError(key: string): string | undefined {
return (form.errors as Record<string, string>)[key];
}
function submit() {
const options = {
forceFormData: true,
@ -127,16 +124,16 @@ function submit() {
<Field>
<FieldLabel for="cash-amount" required>Jumlah</FieldLabel>
<RupiahInput id="cash-amount" v-model="form.amount" autofocus />
<FieldError :errors="form.errors.amount ? [form.errors.amount] : []" />
<FieldError :errors="formErrors(form, 'amount')" />
</Field>
<Field>
<FieldLabel for="cash-description" required>Keterangan</FieldLabel>
<Textarea id="cash-description" v-model="form.description"
placeholder="Contoh: Setoran kas harian" rows="3" />
<FieldError :errors="form.errors.description ? [form.errors.description] : []" />
<FieldError :errors="formErrors(form, 'description')" />
</Field>
<MediaImageUpload id="cash-photos" v-model="form.media" label="Foto Bukti" required
:errors="formError('photos') ? [formError('photos')!] : []" />
:errors="formErrors(form, 'photos')" />
</FieldSet>
</FieldGroup>

View File

@ -22,6 +22,7 @@ import {
} from '@/components/ui/field';
import { RupiahInput } from '@/components/ui/rupiah-input';
import { Textarea } from '@/components/ui/textarea';
import { formErrors } from '@/lib/form';
import { parseRupiah } from '@/lib/rupiah';
import type { EmployeeAdvanceListItem } from '@/types/employee-advance';
import { appendRootPhotosToFormData, createMediaUploadState } from '@/types/media';
@ -90,10 +91,6 @@ function buildFormData(forUpdate: boolean): FormData {
return formData;
}
function formError(key: string): string | undefined {
return (form.errors as Record<string, string>)[key];
}
function submit() {
const options = {
forceFormData: true,
@ -129,21 +126,21 @@ function submit() {
<Field>
<FieldLabel for="employee-advance-amount" required>Jumlah</FieldLabel>
<RupiahInput id="employee-advance-amount" v-model="form.amount" autofocus />
<FieldError :errors="form.errors.amount ? [form.errors.amount] : []" />
<FieldError :errors="formErrors(form, 'amount')" />
</Field>
<Field>
<FieldLabel for="employee-advance-description" required>Keterangan</FieldLabel>
<Textarea id="employee-advance-description" v-model="form.description"
placeholder="Contoh: Kebutuhan mendesak" rows="3" />
<FieldError :errors="form.errors.description ? [form.errors.description] : []" />
<FieldError :errors="formErrors(form, 'description')" />
</Field>
<Field>
<FieldLabel for="employee-advance-due-date" required>Jatuh Tempo</FieldLabel>
<DatePicker id="employee-advance-due-date" v-model="form.due_date" />
<FieldError :errors="form.errors.due_date ? [form.errors.due_date] : []" />
<FieldError :errors="formErrors(form, 'due_date')" />
</Field>
<MediaImageUpload id="employee-advance-photos" v-model="form.media" label="Foto Bukti" required
:errors="formError('photos') ? [formError('photos')!] : []" />
:errors="formErrors(form, 'photos')" />
</FieldSet>
</FieldGroup>

View File

@ -19,6 +19,7 @@ import {
FieldSet,
} from '@/components/ui/field';
import { Textarea } from '@/components/ui/textarea';
import { formErrors } from '@/lib/form';
import type { EmployeeAdvanceListItem } from '@/types/employee-advance';
const open = defineModel<boolean>('open', { default: false });
@ -73,7 +74,7 @@ function submit() {
<FieldLabel for="reject-reason" required>Alasan Penolakan</FieldLabel>
<Textarea id="reject-reason" v-model="form.reason" placeholder="Tuliskan alasan penolakan"
rows="3" autofocus />
<FieldError :errors="form.errors.reason ? [form.errors.reason] : []" />
<FieldError :errors="formErrors(form, 'reason')" />
</Field>
</FieldSet>
</FieldGroup>

View File

@ -21,6 +21,7 @@ import {
} from '@/components/ui/field';
import { RupiahInput } from '@/components/ui/rupiah-input';
import { Textarea } from '@/components/ui/textarea';
import { formErrors } from '@/lib/form';
import { parseRupiah } from '@/lib/rupiah';
import type { ExpenseListItem } from '@/types/expense';
import { appendRootPhotosToFormData, createMediaUploadState } from '@/types/media';
@ -86,10 +87,6 @@ function buildFormData(forUpdate: boolean): FormData {
return formData;
}
function formError(key: string): string | undefined {
return (form.errors as Record<string, string>)[key];
}
function submit() {
const options = {
forceFormData: true,
@ -125,16 +122,16 @@ function submit() {
<Field>
<FieldLabel for="expense-amount" required>Jumlah</FieldLabel>
<RupiahInput id="expense-amount" v-model="form.amount" autofocus />
<FieldError :errors="form.errors.amount ? [form.errors.amount] : []" />
<FieldError :errors="formErrors(form, 'amount')" />
</Field>
<Field>
<FieldLabel for="expense-description" required>Keterangan</FieldLabel>
<Textarea id="expense-description" v-model="form.description"
placeholder="Contoh: Pembelian perlengkapan toko" rows="3" />
<FieldError :errors="form.errors.description ? [form.errors.description] : []" />
<FieldError :errors="formErrors(form, 'description')" />
</Field>
<MediaImageUpload id="expense-photos" v-model="form.media" label="Foto Bukti" required
:errors="formError('photos') ? [formError('photos')!] : []" />
:errors="formErrors(form, 'photos')" />
</FieldSet>
</FieldGroup>

View File

@ -25,6 +25,7 @@ import {
SelectValue,
} from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import { formErrors } from '@/lib/form';
import type { EmployeeFormData, EnumOption } from '@/types/employee';
const props = withDefaults(
@ -89,12 +90,12 @@ function submit() {
<Field>
<FieldLabel for="email" required>Email</FieldLabel>
<Input id="email" v-model="form.email" type="email" placeholder="nama@perusahaan.com" />
<FieldError :errors="form.errors.email ? [form.errors.email] : []" />
<FieldError :errors="formErrors(form, 'email')" />
</Field>
<Field>
<FieldLabel for="username" required>Username</FieldLabel>
<Input id="username" v-model="form.username" type="text" placeholder="username" />
<FieldError :errors="form.errors.username ? [form.errors.username] : []" />
<FieldError :errors="formErrors(form, 'username')" />
</Field>
<Field>
<FieldLabel for="role" required>Role</FieldLabel>
@ -108,7 +109,7 @@ function submit() {
</SelectItem>
</SelectContent>
</Select>
<FieldError :errors="form.errors.role ? [form.errors.role] : []" />
<FieldError :errors="formErrors(form, 'role')" />
</Field>
</FieldSet>
</FieldGroup>
@ -126,18 +127,18 @@ function submit() {
<FieldLabel for="full_name" required>Nama Lengkap</FieldLabel>
<Input id="full_name" v-model="form.full_name" type="text"
placeholder="Nama lengkap pegawai" />
<FieldError :errors="form.errors.full_name ? [form.errors.full_name] : []" />
<FieldError :errors="formErrors(form, 'full_name')" />
</Field>
<Field>
<FieldLabel for="phone_number">Nomor Telepon</FieldLabel>
<PhoneNumberInput id="phone_number" v-model="form.phone_number" />
<FieldError :errors="form.errors.phone_number ? [form.errors.phone_number] : []" />
<FieldError :errors="formErrors(form, 'phone_number')" />
</Field>
<Field>
<FieldLabel for="birth_date">Tanggal Lahir</FieldLabel>
<DatePicker id="birth_date" v-model="form.birth_date"
placeholder="Pilih tanggal lahir" />
<FieldError :errors="form.errors.birth_date ? [form.errors.birth_date] : []" />
<FieldError :errors="formErrors(form, 'birth_date')" />
</Field>
<Field>
<FieldLabel>Jenis Kelamin</FieldLabel>
@ -149,12 +150,12 @@ function submit() {
</Label>
</div>
</RadioGroup>
<FieldError :errors="form.errors.gender ? [form.errors.gender] : []" />
<FieldError :errors="formErrors(form, 'gender')" />
</Field>
<Field class="md:col-span-3">
<FieldLabel for="address">Alamat</FieldLabel>
<Textarea id="address" v-model="form.address" placeholder="Alamat lengkap" rows="3" />
<FieldError :errors="form.errors.address ? [form.errors.address] : []" />
<FieldError :errors="formErrors(form, 'address')" />
</Field>
</FieldSet>
</FieldGroup>
@ -172,12 +173,12 @@ function submit() {
<FieldLabel for="join_date" required>Tanggal Bergabung</FieldLabel>
<DatePicker id="join_date" v-model="form.join_date"
placeholder="Pilih tanggal bergabung" />
<FieldError :errors="form.errors.join_date ? [form.errors.join_date] : []" />
<FieldError :errors="formErrors(form, 'join_date')" />
</Field>
<Field>
<FieldLabel for="base_salary" required>Gaji Pokok</FieldLabel>
<RupiahInput id="base_salary" v-model="form.base_salary" />
<FieldError :errors="form.errors.base_salary ? [form.errors.base_salary] : []" />
<FieldError :errors="formErrors(form, 'base_salary')" />
</Field>
<Field>
<FieldLabel for="employment_status" required>Status Kepegawaian</FieldLabel>
@ -192,8 +193,7 @@ function submit() {
</SelectItem>
</SelectContent>
</Select>
<FieldError
:errors="form.errors.employment_status ? [form.errors.employment_status] : []" />
<FieldError :errors="formErrors(form, 'employment_status')" />
</Field>
</FieldSet>
</FieldGroup>

View File

@ -19,6 +19,7 @@ import {
FieldLabel,
FieldSet,
} from '@/components/ui/field';
import { formErrors } from '@/lib/form';
import type { LeaveRequestFormData, LeaveRequestListItem } from '@/types/leave-request';
const open = defineModel<boolean>('open', { default: false });
@ -100,12 +101,12 @@ function submit() {
<Field>
<FieldLabel for="leave-start-date" required>Tanggal Mulai</FieldLabel>
<DatePicker id="leave-start-date" v-model="form.start_date" autofocus />
<FieldError :errors="form.errors.start_date ? [form.errors.start_date] : []" />
<FieldError :errors="formErrors(form, 'start_date')" />
</Field>
<Field>
<FieldLabel for="leave-end-date" required>Tanggal Selesai</FieldLabel>
<DatePicker id="leave-end-date" v-model="form.end_date" />
<FieldError :errors="form.errors.end_date ? [form.errors.end_date] : []" />
<FieldError :errors="formErrors(form, 'end_date')" />
</Field>
</FieldSet>
</FieldGroup>

View File

@ -19,6 +19,7 @@ import {
FieldSet,
} from '@/components/ui/field';
import { Textarea } from '@/components/ui/textarea';
import { formErrors } from '@/lib/form';
import type { LeaveRequestListItem } from '@/types/leave-request';
const open = defineModel<boolean>('open', { default: false });
@ -73,7 +74,7 @@ function submit() {
<FieldLabel for="reject-reason" required>Alasan Penolakan</FieldLabel>
<Textarea id="reject-reason" v-model="form.reason" placeholder="Tuliskan alasan penolakan"
rows="3" autofocus />
<FieldError :errors="form.errors.reason ? [form.errors.reason] : []" />
<FieldError :errors="formErrors(form, 'reason')" />
</Field>
</FieldSet>
</FieldGroup>

View File

@ -31,6 +31,7 @@ import {
TableRow,
} from '@/components/ui/table';
import { Textarea } from '@/components/ui/textarea';
import { formErrors } from '@/lib/form';
import type {
CuttingMaterialCartItem,
CuttingProductCatalogItem,
@ -229,11 +230,11 @@ function submit() {
<template>
<div class="grid gap-4 xl:grid-cols-[1fr_400px]">
<div class="space-y-4">
<Card class="min-w-0">
<CardHeader class="pb-3">
<CardTitle class="text-base">Pilih Bahan Baku</CardTitle>
</CardHeader>
<CardContent>
<Card class="min-w-0">
<CardHeader class="pb-3">
<CardTitle class="text-base">Pilih Bahan Baku</CardTitle>
</CardHeader>
<CardContent>
<div class="space-y-4">
<div class="relative">
<Search class="absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
@ -300,14 +301,14 @@ function submit() {
</div>
</div>
</div>
</CardContent>
</Card>
</CardContent>
</Card>
<Card class="min-w-0">
<CardHeader class="pb-3">
<CardTitle class="text-base">Pilih Produk Hasil</CardTitle>
</CardHeader>
<CardContent>
<Card class="min-w-0">
<CardHeader class="pb-3">
<CardTitle class="text-base">Pilih Produk Hasil</CardTitle>
</CardHeader>
<CardContent>
<div class="space-y-4">
<div class="relative">
<Search class="absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
@ -369,8 +370,8 @@ function submit() {
</div>
</div>
</div>
</CardContent>
</Card>
</CardContent>
</Card>
</div>
<Card class="h-fit xl:sticky xl:top-4">
@ -388,7 +389,7 @@ function submit() {
<FieldLabel for="description">Keterangan</FieldLabel>
<Textarea id="description" v-model="form.description"
placeholder="Contoh: Cutting batch pagi" rows="2" />
<FieldError :errors="form.errors.description ? [form.errors.description] : []" />
<FieldError :errors="formErrors(form, 'description')" />
</Field>
<div class="space-y-2">
@ -410,7 +411,8 @@ function submit() {
{{ item.raw_material_name }}
</p>
<p class="truncate text-xs text-muted-foreground">
{{ item.variant }} · Stok {{ item.stock_input }} {{ item.unit_abbreviation }}
{{ item.variant }} · Stok {{ item.stock_input }} {{
item.unit_abbreviation }}
</p>
</div>
<Button type="button" variant="ghost" size="icon"
@ -474,13 +476,13 @@ function submit() {
<div class="grid grid-cols-3 gap-2">
<Field>
<FieldLabel class="text-xs">Hasil</FieldLabel>
<Input v-model="item.cutting_result" type="number" min="1"
class="h-8" @change="syncResultTotals(item)" />
<Input v-model="item.cutting_result" type="number" min="1" class="h-8"
@change="syncResultTotals(item)" />
</Field>
<Field>
<FieldLabel class="text-xs">Gudang</FieldLabel>
<Input v-model="item.warehouse_stock" type="number" min="0"
class="h-8" @change="syncResultTotals(item)" />
<Input v-model="item.warehouse_stock" type="number" min="0" class="h-8"
@change="syncResultTotals(item)" />
</Field>
<Field>
<FieldLabel class="text-xs">Reject</FieldLabel>

View File

@ -33,9 +33,10 @@ import {
import { Separator } from '@/components/ui/separator';
import { Textarea } from '@/components/ui/textarea';
import { getFirstCoverImage } from '@/lib/catalog-cover';
import { formErrors } from '@/lib/form';
import { formatRupiah, parseRupiah } from '@/lib/rupiah';
import type { ProductPriceItem, ProductVariantItem } from '@/types/product';
import type { EnumOption, OrderCartItem, OrderCatalogItem, SelectOption } from '@/types/order';
import type { ProductPriceItem, ProductVariantItem } from '@/types/product';
const props = defineProps<{
customers: SelectOption[];
@ -251,25 +252,15 @@ function submit() {
</div>
<div v-else class="columns-1 gap-4 sm:columns-2 xl:columns-3">
<PosCatalogCard
v-for="product in filteredCatalog"
:key="product.id"
:title="product.name"
:cover-image="getFirstCoverImage(product.variants)"
>
<p
v-if="!product.variants.length"
class="px-3 py-4 text-sm text-muted-foreground"
>
<PosCatalogCard v-for="product in filteredCatalog" :key="product.id" :title="product.name"
:cover-image="getFirstCoverImage(product.variants)">
<p v-if="!product.variants.length" class="px-3 py-4 text-sm text-muted-foreground">
Belum ada varian
</p>
<div
v-for="variant in product.variants"
:key="variant.id"
<div v-for="variant in product.variants" :key="variant.id"
class="flex items-center gap-2.5 px-3 py-2.5 transition-colors"
:class="getVariantPrice(variant) ? 'cursor-pointer hover:bg-muted/30' : 'opacity-60'"
@click="getVariantPrice(variant) && addToCart(product, variant)"
>
@click="getVariantPrice(variant) && addToCart(product, variant)">
<PosCatalogVariantThumb :items="variant.images" />
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium">
@ -284,14 +275,8 @@ function submit() {
<span v-else>-</span>
</p>
</div>
<Button
type="button"
variant="outline"
size="icon-sm"
class="shrink-0"
:disabled="!getVariantPrice(variant)"
@click.stop="addToCart(product, variant)"
>
<Button type="button" variant="outline" size="icon-sm" class="shrink-0"
:disabled="!getVariantPrice(variant)" @click.stop="addToCart(product, variant)">
<Plus class="size-3.5" />
</Button>
</div>
@ -326,7 +311,7 @@ function submit() {
</SelectGroup>
</SelectContent>
</Select>
<FieldError :errors="form.errors.channel ? [form.errors.channel] : []" />
<FieldError :errors="formErrors(form, 'channel')" />
</Field>
<Field v-if="isStoreChannel">
@ -344,7 +329,7 @@ function submit() {
</SelectGroup>
</SelectContent>
</Select>
<FieldError :errors="form.errors.price_type ? [form.errors.price_type] : []" />
<FieldError :errors="formErrors(form, 'price_type')" />
</Field>
<Field v-else>
@ -369,7 +354,7 @@ function submit() {
</SelectGroup>
</SelectContent>
</Select>
<FieldError :errors="form.errors.customer_id ? [form.errors.customer_id] : []" />
<FieldError :errors="formErrors(form, 'customer_id')" />
</Field>
<div v-if="cart.length === 0"
@ -443,13 +428,12 @@ function submit() {
<Field>
<FieldLabel for="discount">Diskon</FieldLabel>
<RupiahInput id="discount" v-model="form.discount" placeholder="0" />
<FieldError :errors="form.errors.discount ? [form.errors.discount] : []" />
<FieldError :errors="formErrors(form, 'discount')" />
</Field>
<Field v-if="isMarketplaceChannel">
<FieldLabel for="marketplace_fee">Biaya Marketplace</FieldLabel>
<RupiahInput id="marketplace_fee" v-model="form.marketplace_fee" placeholder="0" />
<FieldError
:errors="form.errors.marketplace_fee ? [form.errors.marketplace_fee] : []" />
<FieldError :errors="formErrors(form, 'marketplace_fee')" />
</Field>
<div class="flex justify-between text-base font-semibold">
<span>Net</span>
@ -461,7 +445,7 @@ function submit() {
<FieldLabel for="notes">Keterangan</FieldLabel>
<Textarea id="notes" v-model="form.notes" placeholder="Contoh: Pesanan walk-in"
rows="2" />
<FieldError :errors="form.errors.notes ? [form.errors.notes] : []" />
<FieldError :errors="formErrors(form, 'notes')" />
</Field>
<Button type="submit" class="w-full" :disabled="form.processing || cart.length === 0">

View File

@ -35,6 +35,7 @@ import {
import { Separator } from '@/components/ui/separator';
import { Textarea } from '@/components/ui/textarea';
import { getFirstCoverImage } from '@/lib/catalog-cover';
import { formErrors } from '@/lib/form';
import { formatRupiah, parseRupiah } from '@/lib/rupiah';
import type { MediaItem } from '@/types/media';
import { appendRootPhotosToFormData, createMediaUploadState } from '@/types/media';
@ -180,10 +181,6 @@ function buildFormData(): FormData {
return formData;
}
function formError(key: string): string | undefined {
return (form.errors as Record<string, string>)[key];
}
function submit() {
if (cart.value.length === 0) {
toast.error('Tambahkan minimal satu bahan baku ke keranjang.');
@ -232,30 +229,20 @@ function submit() {
</div>
<div v-else class="columns-1 gap-4 sm:columns-2 xl:columns-3">
<PosCatalogCard
v-for="rawMaterial in filteredCatalog"
:key="rawMaterial.id"
:title="rawMaterial.name"
:cover-image="getFirstCoverImage(rawMaterial.prices)"
>
<PosCatalogCard v-for="rawMaterial in filteredCatalog" :key="rawMaterial.id"
:title="rawMaterial.name" :cover-image="getFirstCoverImage(rawMaterial.prices)">
<template #header-extra>
<Badge variant="secondary" class="mt-1.5">
{{ rawMaterial.unit_label }}
</Badge>
</template>
<p
v-if="!rawMaterial.prices.length"
class="px-3 py-4 text-sm text-muted-foreground"
>
<p v-if="!rawMaterial.prices.length" class="px-3 py-4 text-sm text-muted-foreground">
Belum ada varian
</p>
<div
v-for="price in rawMaterial.prices"
:key="price.id"
<div v-for="price in rawMaterial.prices" :key="price.id"
class="flex cursor-pointer items-center gap-2.5 px-3 py-2.5 transition-colors hover:bg-muted/30"
@click="addToCart(rawMaterial, price)"
>
@click="addToCart(rawMaterial, price)">
<PosCatalogVariantThumb :items="price.images" />
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium">
@ -265,13 +252,8 @@ function submit() {
{{ price.price_formatted }}
</p>
</div>
<Button
type="button"
variant="outline"
size="icon-sm"
class="shrink-0"
@click.stop="addToCart(rawMaterial, price)"
>
<Button type="button" variant="outline" size="icon-sm" class="shrink-0"
@click.stop="addToCart(rawMaterial, price)">
<Plus class="size-3.5" />
</Button>
</div>
@ -306,7 +288,7 @@ function submit() {
</SelectGroup>
</SelectContent>
</Select>
<FieldError :errors="form.errors.supplier_id ? [form.errors.supplier_id] : []" />
<FieldError :errors="formErrors(form, 'supplier_id')" />
</Field>
<div v-if="cart.length === 0"
@ -381,7 +363,7 @@ function submit() {
<Field>
<FieldLabel for="discount">Diskon</FieldLabel>
<RupiahInput id="discount" v-model="form.discount" placeholder="0" />
<FieldError :errors="form.errors.discount ? [form.errors.discount] : []" />
<FieldError :errors="formErrors(form, 'discount')" />
</Field>
<div class="flex justify-between text-base font-semibold">
<span>Total</span>
@ -393,11 +375,11 @@ function submit() {
<FieldLabel for="notes">Keterangan</FieldLabel>
<Textarea id="notes" v-model="form.notes" placeholder="Contoh: Belanja batch 2"
rows="2" />
<FieldError :errors="form.errors.notes ? [form.errors.notes] : []" />
<FieldError :errors="formErrors(form, 'notes')" />
</Field>
<MediaImageUpload id="purchase-photos" v-model="form.media" label="Bukti Transaksi"
:errors="formError('photos') ? [formError('photos')!] : []" />
:errors="formErrors(form, 'photos')" />
<Button type="submit" class="w-full" :disabled="form.processing || cart.length === 0">
<Save class="size-4" />

View File

@ -19,6 +19,7 @@ import {
FieldSet,
} from '@/components/ui/field';
import { Input } from '@/components/ui/input';
import { formErrors } from '@/lib/form';
import type { CategoryFormData, CategoryListItem } from '@/types/category';
const open = defineModel<boolean>('open', { default: false });
@ -96,7 +97,7 @@ function submit() {
<FieldLabel for="category-name" required>Nama</FieldLabel>
<Input id="category-name" v-model="form.name" type="text" placeholder="Contoh: Daster"
autofocus />
<FieldError :errors="form.errors.name ? [form.errors.name] : []" />
<FieldError :errors="formErrors(form, 'name')" />
</Field>
</FieldSet>
</FieldGroup>

View File

@ -21,7 +21,8 @@ import {
import { Input } from '@/components/ui/input';
import { PhoneNumberInput } from '@/components/ui/phone-number-input';
import { Textarea } from '@/components/ui/textarea';
import type { CustomerFormData, CustomerListItem } from '@/types/customer';
import { formErrors } from '@/lib/form';
import type { CustomerFormData, CustomerListItem } from '@/types/customers';
const open = defineModel<boolean>('open', { default: false });
@ -102,19 +103,19 @@ function submit() {
<FieldLabel for="customer-name" required>Nama</FieldLabel>
<Input id="customer-name" v-model="form.name" type="text" placeholder="Nama customer"
autofocus />
<FieldError :errors="form.errors.name ? [form.errors.name] : []" />
<FieldError :errors="formErrors(form, 'name')" />
</Field>
<Field>
<FieldLabel for="customer-phone-number" required>Nomor Telepon</FieldLabel>
<PhoneNumberInput id="customer-phone-number" v-model="form.phone_number"
placeholder="Nomor telepon customer" />
<FieldError :errors="form.errors.phone_number ? [form.errors.phone_number] : []" />
<FieldError :errors="formErrors(form, 'phone_number')" />
</Field>
<Field>
<FieldLabel for="customer-address" required>Alamat</FieldLabel>
<Textarea id="customer-address" v-model="form.address" rows="3"
placeholder="Alamat customer" />
<FieldError :errors="form.errors.address ? [form.errors.address] : []" />
<FieldError :errors="formErrors(form, 'address')" />
</Field>
</FieldSet>
</FieldGroup>

View File

@ -16,6 +16,7 @@ import {
import { Input } from '@/components/ui/input';
import { RupiahInput } from '@/components/ui/rupiah-input';
import { Textarea } from '@/components/ui/textarea';
import { formErrors } from '@/lib/form';
import { parseRupiah } from '@/lib/rupiah';
import { appendMediaToFormData, createMediaUploadState } from '@/types/media';
import {
@ -282,7 +283,7 @@ function submit() {
<FieldLabel for="name" required>Nama Produk</FieldLabel>
<Input id="name" v-model="form.name" type="text"
placeholder="Contoh: Midi Olla Dress" />
<FieldError :errors="form.errors.name ? [form.errors.name] : []" />
<FieldError :errors="formErrors(form, 'name')" />
</Field>
<Field>
@ -290,7 +291,7 @@ function submit() {
<Textarea id="description" v-model="form.description"
placeholder="Contoh: Hi Sobat, New arrival dari DST dengan Olla Dress. Yuks check detail produk: Detail: * Bahan Cey Flow Bordir * Ld 120 cm * PB 110 cm * Bisa jadi dress..."
rows="4" />
<FieldError :errors="form.errors.description ? [form.errors.description] : []" />
<FieldError :errors="formErrors(form, 'description')" />
</Field>
<Field>

View File

@ -22,6 +22,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { formErrors } from '@/lib/form';
import { parseRupiah } from '@/lib/rupiah';
import { appendMediaToFormData, createMediaUploadState } from '@/types/media';
import type {
@ -228,7 +229,7 @@ function submit() {
<FieldLabel for="name" required>Nama Bahan Baku</FieldLabel>
<Input id="name" v-model="form.name" type="text"
placeholder="Masukkan nama bahan baku" />
<FieldError :errors="form.errors.name ? [form.errors.name] : []" />
<FieldError :errors="formErrors(form, 'name')" />
</Field>
<Field>
@ -243,7 +244,7 @@ function submit() {
</SelectItem>
</SelectContent>
</Select>
<FieldError :errors="form.errors.unit ? [form.errors.unit] : []" />
<FieldError :errors="formErrors(form, 'unit')" />
</Field>
</FieldSet>
</FieldGroup>
@ -334,7 +335,7 @@ function submit() {
</CardContent>
</Card>
<FieldError :errors="formError('prices') ? [formError('prices')!] : []" />
<FieldError :errors="formErrors(form, 'prices')" />
<div class="flex items-center justify-between gap-2">
<Button type="button" variant="outline" @click="addPrice">

View File

@ -21,6 +21,7 @@ import {
import { Input } from '@/components/ui/input';
import { PhoneNumberInput } from '@/components/ui/phone-number-input';
import { Textarea } from '@/components/ui/textarea';
import { formErrors } from '@/lib/form';
import type { SupplierFormData, SupplierListItem } from '@/types/supplier';
const open = defineModel<boolean>('open', { default: false });
@ -102,19 +103,19 @@ function submit() {
<FieldLabel for="supplier-name" required>Nama</FieldLabel>
<Input id="supplier-name" v-model="form.name" type="text" placeholder="Nama supplier"
autofocus />
<FieldError :errors="form.errors.name ? [form.errors.name] : []" />
<FieldError :errors="formErrors(form, 'name')" />
</Field>
<Field>
<FieldLabel for="supplier-phone-number" required>Nomor Telepon</FieldLabel>
<PhoneNumberInput id="supplier-phone-number" v-model="form.phone_number"
placeholder="Nomor telepon supplier" />
<FieldError :errors="form.errors.phone_number ? [form.errors.phone_number] : []" />
<FieldError :errors="formErrors(form, 'phone_number')" />
</Field>
<Field>
<FieldLabel for="supplier-address" required>Alamat</FieldLabel>
<Textarea id="supplier-address" v-model="form.address" rows="3"
placeholder="Alamat supplier" />
<FieldError :errors="form.errors.address ? [form.errors.address] : []" />
<FieldError :errors="formErrors(form, 'address')" />
</Field>
</FieldSet>
</FieldGroup>

10
resources/js/lib/form.ts Normal file
View File

@ -0,0 +1,10 @@
import type { InertiaForm } from '@inertiajs/vue3';
export function formErrors<T extends Record<string, unknown>>(
form: InertiaForm<T>,
key: keyof T | string,
): string[] {
const error = form.errors[key as keyof typeof form.errors];
return error ? [error] : [];
}

View File

@ -7,14 +7,13 @@ import { Card, CardContent } from '@/components/ui/card';
import {
Field,
FieldError,
FieldGroup,
FieldLabel,
FieldSet,
FieldGroup, FieldSet
} from '@/components/ui/field';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { applyAppearance } from '@/composables/useAppearance';
import AccountLayout from '@/layouts/AccountLayout.vue';
import { formErrors } from '@/lib/form';
import { cn } from '@/lib/utils';
import type { AppearanceFormData, AppearanceMode } from '@/types/account';
@ -102,7 +101,7 @@ function submit() {
</div>
</Label>
</RadioGroup>
<FieldError :errors="form.errors.appearance ? [form.errors.appearance] : []" />
<FieldError :errors="formErrors(form, 'appearance')" />
</Field>
</FieldGroup>
</FieldSet>

View File

@ -13,6 +13,7 @@ import {
} from '@/components/ui/field';
import { Input } from '@/components/ui/input';
import AccountLayout from '@/layouts/AccountLayout.vue';
import { formErrors } from '@/lib/form';
import type { PasswordFormData } from '@/types/account';
const form = useForm<PasswordFormData>({
@ -50,8 +51,7 @@ function submit() {
</FieldLabel>
<Input id="current_password" v-model="form.current_password" type="password"
placeholder="********" autocomplete="current-password" />
<FieldError
:errors="form.errors.current_password ? [form.errors.current_password] : []" />
<FieldError :errors="formErrors(form, 'current_password')" />
</Field>
<Field>
@ -60,7 +60,7 @@ function submit() {
</FieldLabel>
<Input id="password" v-model="form.password" type="password" placeholder="********"
autocomplete="new-password" />
<FieldError :errors="form.errors.password ? [form.errors.password] : []" />
<FieldError :errors="formErrors(form, 'password')" />
</Field>
<Field>
@ -69,8 +69,7 @@ function submit() {
</FieldLabel>
<Input id="password_confirmation" v-model="form.password_confirmation"
type="password" placeholder="********" autocomplete="new-password" />
<FieldError
:errors="form.errors.password_confirmation ? [form.errors.password_confirmation] : []" />
<FieldError :errors="formErrors(form, 'password_confirmation')" />
</Field>
</FieldGroup>
</FieldSet>

View File

@ -17,6 +17,7 @@ import { PhoneNumberInput } from '@/components/ui/phone-number-input';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Textarea } from '@/components/ui/textarea';
import AccountLayout from '@/layouts/AccountLayout.vue';
import { formErrors } from '@/lib/form';
import type { EnumOption, ProfileFormData, ProfileUser } from '@/types/account';
const props = defineProps<{
@ -60,7 +61,7 @@ function submit() {
</FieldLabel>
<Input id="email" v-model="form.email" type="email"
placeholder="nama@perusahaan.com" />
<FieldError :errors="form.errors.email ? [form.errors.email] : []" />
<FieldError :errors="formErrors(form, 'email')" />
</Field>
<Field>
@ -68,7 +69,7 @@ function submit() {
Username
</FieldLabel>
<Input id="username" v-model="form.username" type="text" placeholder="username" />
<FieldError :errors="form.errors.username ? [form.errors.username] : []" />
<FieldError :errors="formErrors(form, 'username')" />
</Field>
<Field>
@ -77,7 +78,7 @@ function submit() {
</FieldLabel>
<Input id="full_name" v-model="form.full_name" type="text"
placeholder="Nama lengkap" />
<FieldError :errors="form.errors.full_name ? [form.errors.full_name] : []" />
<FieldError :errors="formErrors(form, 'full_name')" />
</Field>
<Field>
@ -85,7 +86,7 @@ function submit() {
Nomor Telepon
</FieldLabel>
<PhoneNumberInput id="phone_number" v-model="form.phone_number" />
<FieldError :errors="form.errors.phone_number ? [form.errors.phone_number] : []" />
<FieldError :errors="formErrors(form, 'phone_number')" />
</Field>
<Field>
@ -93,7 +94,7 @@ function submit() {
Tanggal Lahir
</FieldLabel>
<DatePicker id="birth_date" v-model="form.birth_date" />
<FieldError :errors="form.errors.birth_date ? [form.errors.birth_date] : []" />
<FieldError :errors="formErrors(form, 'birth_date')" />
</Field>
<Field>
@ -109,7 +110,7 @@ function submit() {
</label>
</div>
</RadioGroup>
<FieldError :errors="form.errors.gender ? [form.errors.gender] : []" />
<FieldError :errors="formErrors(form, 'gender')" />
</Field>
<Field class="sm:col-span-3">
@ -118,7 +119,7 @@ function submit() {
</FieldLabel>
<Textarea id="address" v-model="form.address" placeholder="Alamat lengkap"
rows="3" />
<FieldError :errors="form.errors.address ? [form.errors.address] : []" />
<FieldError :errors="formErrors(form, 'address')" />
</Field>
</FieldGroup>
</FieldSet>

View File

@ -6,7 +6,6 @@ export type CategoryListItem = {
export type CategoryFormData = {
name: string;
slug: string;
};
export type CategoryFilters = {