refactor: enhance employee and service forms with new fields and validation

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-05-08 15:55:33 +03:00
co-authored by Copilot
parent b0361fcbfb
commit 4b51ffe457
7 changed files with 448 additions and 143 deletions
@@ -21,21 +21,29 @@ import { toRelation, toId } from "@/shared/lib/utils"
import {
employeeFormSchema,
type EmployeeFormValues,
STATUS_OPTIONS,
TYPE_OPTIONS,
WAGE_TYPE_OPTIONS,
} from "./employee.schema"
import { EMPLOYEE_ROUTES, DEPARTMENT_ROUTES, SHOP_TIMING_ROUTES, SHOP_CALENDAR_ROUTES, EmployeeType } from "@garage/api"
import { EMPLOYEE_ROUTES, DEPARTMENT_ROUTES, SHOP_TIMING_ROUTES, SHOP_CALENDAR_ROUTES, GEO_ROUTES } from "@garage/api"
// ── Constants ──
const STATUS_OPTIONS = [
{ value: "active", label: "Active" },
{ value: "inactive", label: "Inactive" },
]
const TYPE_OPTIONS = EmployeeType.map((v) => ({
const STATUS_SELECT_OPTIONS = STATUS_OPTIONS.map((v) => ({
value: v,
label: v.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
}))
const TYPE_SELECT_OPTIONS = TYPE_OPTIONS.map((v) => ({
value: v,
label: v.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
}))
const WAGE_TYPE_SELECT_OPTIONS = WAGE_TYPE_OPTIONS.map((v) => ({
value: v,
label: v,
}))
// ── Props ──
export type EmployeeFormProps = {
@@ -48,18 +56,23 @@ export type EmployeeFormProps = {
const DEFAULT_VALUES: EmployeeFormValues = {
department: null,
country: null,
shop_calender: null,
shop_timing: null,
role_id: null,
first_name: "",
last_name: "",
email: "",
password: "",
phone: "",
position: "",
designation: "",
salary: null,
wage_type: null,
status: "active",
type: "employee",
track_attendance: true,
notify_owner_when_punch_in_out: false,
geo_fence_radius: 100,
geo_fence_radius: null,
}
// ── Mapping helpers ──
@@ -69,36 +82,46 @@ function mapToFormValues(data: unknown): EmployeeFormValues {
return {
department: toRelation(d.department_id, d.department?.name),
country: toRelation(d.country_id, d.country?.name),
shop_calender: toRelation(d.shop_calender_id, d.shop_calender?.title),
shop_timing: toRelation(d.shop_timing_id, d.shop_timing?.title),
role_id: d.role_id ? Number(d.role_id) : null,
first_name: d.first_name || "",
last_name: d.last_name || "",
email: d.email || "",
password: "",
phone: d.phone || "",
position: d.position || "",
designation: d.designation || "",
salary: d.salary ?? null,
wage_type: d.wage_type ?? null,
status: d.status || "active",
type: d.type || "employee",
track_attendance: d.track_attendance ?? true,
notify_owner_when_punch_in_out: d.notify_owner_when_punch_in_out ?? false,
geo_fence_radius: d.geo_fence_radius ?? 100,
geo_fence_radius: d.geo_fence_radius ?? null,
}
}
function mapFormToPayload(values: EmployeeFormValues) {
return {
department_id: toId(values.department),
country_id: toId(values.country),
shop_calender_id: toId(values.shop_calender),
shop_timing_id: toId(values.shop_timing),
role_id: values.role_id ?? undefined,
first_name: values.first_name,
last_name: values.last_name,
email: values.email || undefined,
email: values.email,
password: values.password,
phone: values.phone || undefined,
position: values.position || undefined,
status: values.status || undefined,
type: values.type || undefined,
designation: values.designation || undefined,
salary: values.salary ?? undefined,
wage_type: values.wage_type ?? undefined,
status: values.status,
type: values.type,
track_attendance: values.track_attendance,
notify_owner_when_punch_in_out: values.notify_owner_when_punch_in_out,
geo_fence_radius: values.geo_fence_radius,
geo_fence_radius: values.geo_fence_radius ?? undefined,
}
}
@@ -165,11 +188,15 @@ export function EmployeeForm({ resourceId, initialData, onSuccess }: EmployeeFor
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfTextField name="email" label="Email" placeholder="jane@example.com" type="email" />
<RhfTextField name="phone" label="Phone" placeholder="0501234567" type="tel" />
<RhfTextField name="password" label="Password" placeholder="At least 6 characters" type="password" required />
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfTextField name="phone" label="Phone" placeholder="0501234567" type="tel" />
<RhfTextField name="designation" label="Designation" placeholder="Technician" />
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfTextField name="position" label="Position" placeholder="Technician" />
<RhfAsyncSelectField
name="department"
label="Department"
@@ -179,6 +206,7 @@ export function EmployeeForm({ resourceId, initialData, onSuccess }: EmployeeFor
mapOption={mapLookupOption}
{...STORE_OBJECT}
/>
<RhfTextField name="role_id" label="Role ID" placeholder="1" type="number" />
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
@@ -186,17 +214,36 @@ export function EmployeeForm({ resourceId, initialData, onSuccess }: EmployeeFor
name="status"
label="Status"
placeholder="Select status"
options={STATUS_OPTIONS}
options={STATUS_SELECT_OPTIONS}
/>
<RhfSelectField
name="type"
label="Type"
placeholder="Select type"
options={TYPE_OPTIONS}
options={TYPE_SELECT_OPTIONS}
/>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfTextField name="salary" label="Salary" placeholder="0" type="number" />
<RhfSelectField
name="wage_type"
label="Wage Type"
placeholder="Select wage type"
options={WAGE_TYPE_SELECT_OPTIONS}
/>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfAsyncSelectField
name="country"
label="Country"
placeholder="Select country"
queryKey={[GEO_ROUTES.COUNTRIES]}
listFn={() => api.geo.countries()}
mapOption={mapLookupOption}
{...STORE_OBJECT}
/>
<RhfAsyncSelectField
name="shop_calender"
label="Shop Calendar"
@@ -1,33 +1,56 @@
import { z } from "zod"
import { EmployeeType } from "@garage/api"
import { EmployeeType, WageType } from "@garage/api"
const relationFieldSchema = z
.object({ value: z.string(), label: z.string() })
.nullable()
const optionalNumericField = z.preprocess(
(value) => {
if (value === "" || value === null || value === undefined) {
return null
}
return Number(value)
},
z.number().finite().nullable(),
)
const optionalIntegerIdField = z.preprocess(
(value) => {
if (value === "" || value === null || value === undefined) {
return null
}
return Number(value)
},
z.number().int().positive().nullable(),
)
const STATUS_OPTIONS = ["active", "inactive"] as const
const TYPE_OPTIONS = EmployeeType
const WAGE_TYPE_OPTIONS = WageType
const employeeFormSchema = z.object({
department: relationFieldSchema,
country: relationFieldSchema,
shop_calender: relationFieldSchema,
shop_timing: relationFieldSchema,
first_name: z.string().min(1, "First name is required"),
last_name: z.string().min(1, "Last name is required"),
email: z.union([
z.string().email("Enter a valid email address"),
z.literal(""),
]).optional(),
phone: z.string().optional(),
position: z.string().optional(),
status: z.string().optional(),
type: z.string().optional(),
role_id: optionalIntegerIdField,
first_name: z.string().trim().min(1, "First name is required").max(50, "First name must be at most 50 characters"),
last_name: z.string().trim().min(1, "Last name is required").max(50, "Last name must be at most 50 characters"),
email: z.string().trim().min(1, "Email is required").email("Enter a valid email address").max(100, "Email must be at most 100 characters"),
password: z.string().min(6, "Password must be at least 6 characters"),
phone: z.string().trim().max(30, "Phone must be at most 30 characters").optional().or(z.literal("")),
designation: z.string().trim().max(50, "Designation must be at most 50 characters").optional().or(z.literal("")),
salary: optionalNumericField,
wage_type: z.enum(WAGE_TYPE_OPTIONS).nullable(),
status: z.enum(STATUS_OPTIONS),
type: z.enum(TYPE_OPTIONS),
track_attendance: z.boolean(),
notify_owner_when_punch_in_out: z.boolean(),
geo_fence_radius: z.coerce.number().min(0).optional(),
geo_fence_radius: optionalNumericField,
})
type EmployeeFormValues = z.infer<typeof employeeFormSchema>
export { employeeFormSchema, relationFieldSchema, STATUS_OPTIONS, TYPE_OPTIONS }
export { employeeFormSchema, relationFieldSchema, STATUS_OPTIONS, TYPE_OPTIONS, WAGE_TYPE_OPTIONS }
export type { EmployeeFormValues }