refactor: enhance employee and service forms with new fields and validation
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user