diff --git a/.turbo/turbo-build.log b/.turbo/turbo-build.log new file mode 100644 index 0000000..e69de29 diff --git a/app/(authenticated)/productivity/employees/page.tsx b/app/(authenticated)/productivity/employees/page.tsx new file mode 100644 index 0000000..53848b1 --- /dev/null +++ b/app/(authenticated)/productivity/employees/page.tsx @@ -0,0 +1,65 @@ +"use client" + +import { ResourcePage } from "@/shared/data-view/resource-page" +import { ColumnHeader } from "@/shared/data-view/table-view" +import { EmployeeForm } from "@/modules/employees/employee-form" +import { EMPLOYEE_ROUTES } from "@repo/api" +import type { EmployeesClient } from "@repo/api" + +export default function EmployeesPage() { + return ( + + pageTitle="Employees" + title="Employee" + routeKey={EMPLOYEE_ROUTES.INDEX} + getClient={(api) => api.employees} + columns={({ actionsColumn }) => [ + { + accessorKey: "first_name", + header: ({ column }) => , + cell: ({ row }) => { + const { first_name, last_name } = row.original + return `${first_name ?? ""} ${last_name ?? ""}`.trim() + }, + }, + { + accessorKey: "email", + header: ({ column }) => , + }, + { + accessorKey: "phone", + header: ({ column }) => , + }, + { + accessorKey: "position", + header: ({ column }) => , + }, + { + accessorKey: "department", + header: ({ column }) => , + cell: ({ row }) => (row.original as any).department?.name ?? "—", + }, + { + accessorKey: "status", + header: ({ column }) => , + cell: ({ row }) => { + const status = row.original.status + return ( + + {status} + + ) + }, + }, + actionsColumn(), + ]} + renderForm={({ resourceId, initialData, onSuccess }) => ( + + )} + /> + ) +} diff --git a/app/(authenticated)/productivity/shop-calendars/page.tsx b/app/(authenticated)/productivity/shop-calendars/page.tsx new file mode 100644 index 0000000..173e6fd --- /dev/null +++ b/app/(authenticated)/productivity/shop-calendars/page.tsx @@ -0,0 +1,50 @@ +"use client" + +import { ResourcePage } from "@/shared/data-view/resource-page" +import { ColumnHeader } from "@/shared/data-view/table-view" +import { ShopCalendarForm } from "@/modules/shop-calendars/shop-calendar-form" +import { SHOP_CALENDAR_ROUTES } from "@repo/api" +import type { ShopCalendarsClient } from "@repo/api" +import { CheckCircle2Icon } from "lucide-react" + +export default function ShopCalendarsPage() { + return ( + + pageTitle="Shop Calendars" + title="Shop Calendar" + routeKey={SHOP_CALENDAR_ROUTES.INDEX} + getClient={(api) => api.shopCalendars} + columns={({ actionsColumn }) => [ + { + accessorKey: "title", + header: ({ column }) => , + }, + { + accessorKey: "is_default", + header: ({ column }) => , + cell: ({ row }) => + (row.original as any).is_default ? ( + + ) : null, + }, + { + accessorKey: "shop_calender_days", + header: () => Days, + enableSorting: false, + cell: ({ row }) => { + const days = (row.original as any).shop_calender_days + return days?.length ?? 0 + }, + }, + actionsColumn({ onEdit: undefined }), + ]} + renderForm={({ resourceId, initialData, onSuccess }) => ( + + )} + /> + ) +} diff --git a/app/(authenticated)/productivity/shop-timings/page.tsx b/app/(authenticated)/productivity/shop-timings/page.tsx new file mode 100644 index 0000000..7e4a571 --- /dev/null +++ b/app/(authenticated)/productivity/shop-timings/page.tsx @@ -0,0 +1,57 @@ +"use client" + +import { ResourcePage } from "@/shared/data-view/resource-page" +import { ColumnHeader } from "@/shared/data-view/table-view" +import { ShopTimingForm } from "@/modules/shop-timings/shop-timing-form" +import { SHOP_TIMING_ROUTES } from "@repo/api" +import type { ShopTimingsClient } from "@repo/api" +import { CheckCircle2Icon } from "lucide-react" + +export default function ShopTimingsPage() { + return ( + + pageTitle="Shop Timings" + title="Shop Timing" + routeKey={SHOP_TIMING_ROUTES.INDEX} + getClient={(api) => api.shopTimings} + columns={({ actionsColumn }) => [ + { + accessorKey: "title", + header: ({ column }) => , + }, + { + accessorKey: "in_time", + header: ({ column }) => , + }, + { + accessorKey: "out_time", + header: ({ column }) => , + }, + { + accessorKey: "full_day_hours", + header: ({ column }) => , + }, + { + accessorKey: "half_day_hours", + header: ({ column }) => , + }, + { + accessorKey: "is_default", + header: ({ column }) => , + cell: ({ row }) => + row.original.is_default ? ( + + ) : null, + }, + actionsColumn(), + ]} + renderForm={({ resourceId, initialData, onSuccess }) => ( + + )} + /> + ) +} diff --git a/app/(authenticated)/sales/inspections/page.tsx b/app/(authenticated)/sales/inspections/page.tsx new file mode 100644 index 0000000..07130ba --- /dev/null +++ b/app/(authenticated)/sales/inspections/page.tsx @@ -0,0 +1,65 @@ +"use client" + +import { ResourcePage } from "@/shared/data-view/resource-page" +import { ColumnHeader } from "@/shared/data-view/table-view" +import { InspectionForm } from "@/modules/inspections/inspection-form" +import { INSPECTION_ROUTES } from "@repo/api" +import type { InspectionsClient } from "@repo/api" + +export default function InspectionsPage() { + return ( + + pageTitle="Inspections" + title="Inspection" + routeKey={INSPECTION_ROUTES.INDEX} + getClient={(api) => api.inspections} + columns={({ actionsColumn }) => [ + { + accessorKey: "title", + header: ({ column }) => , + }, + { + accessorKey: "customer", + header: ({ column }) => , + cell: ({ row }) => { + const c = (row.original as any).customer + return c ? `${c.first_name ?? ""} ${c.last_name ?? ""}`.trim() : "—" + }, + }, + { + accessorKey: "vehicle", + header: ({ column }) => , + cell: ({ row }) => { + const v = (row.original as any).vehicle + return v ? `${v.make ?? ""} ${v.model ?? ""}`.trim() : "—" + }, + }, + { + accessorKey: "inspection_category", + header: ({ column }) => , + cell: ({ row }) => (row.original as any).inspection_category?.name ?? "—", + }, + { + accessorKey: "status", + header: ({ column }) => , + cell: ({ row }) => { + const status = (row.original as any).status + return ( + + {status ?? "—"} + + ) + }, + }, + actionsColumn(), + ]} + renderForm={({ resourceId, initialData, onSuccess }) => ( + + )} + /> + ) +} diff --git a/app/(authenticated)/sales/vehicles/page.tsx b/app/(authenticated)/sales/vehicles/page.tsx index 528c6f9..84becc0 100644 --- a/app/(authenticated)/sales/vehicles/page.tsx +++ b/app/(authenticated)/sales/vehicles/page.tsx @@ -14,6 +14,8 @@ export default function VehiclesPage() { title="Vehicle" routeKey={VEHICLE_ROUTES.INDEX} getClient={(api) => api.vehicles} + + columns={({ actionsColumn }) => [ { accessorKey: "name", diff --git a/base/components/layout/dashboard/app-sidebar.tsx b/base/components/layout/dashboard/app-sidebar.tsx index a3cbfc6..7e89e1e 100644 --- a/base/components/layout/dashboard/app-sidebar.tsx +++ b/base/components/layout/dashboard/app-sidebar.tsx @@ -213,7 +213,7 @@ function CollapsibleNavItem({ item, isCollapsed }: { item: NavItem; isCollapsed: {sub.icon ? ( - svg]:size-4", isSubActive ? "text-primary" : "text-muted-foreground/70 group-hover/menu-sub-item:text-foreground")}> + svg]:size-4", isSubActive ? "text-primary" : "text-muted-foreground/70 group-hover/menu-sub-item:text-primary")}> {sub.icon} ) : ( @@ -222,7 +222,7 @@ function CollapsibleNavItem({ item, isCollapsed }: { item: NavItem; isCollapsed: "size-1.5 transition-colors duration-200", isSubActive ? "fill-primary text-primary" - : "fill-muted-foreground/40 text-muted-foreground/40 group-hover/menu-sub-item:fill-foreground group-hover/menu-sub-item:text-foreground" + : "fill-muted-foreground/40 text-muted-foreground/40 group-hover/menu-sub-item:fill-foreground group-hover/menu-sub-item:text-primary" )} /> )} diff --git a/modules/employees/employee-form.tsx b/modules/employees/employee-form.tsx new file mode 100644 index 0000000..3554b1a --- /dev/null +++ b/modules/employees/employee-form.tsx @@ -0,0 +1,236 @@ +"use client" + +import { AlertTriangle, Plus, Save } from "lucide-react" + +import { Button } from "@/shared/components/ui/button" +import { Alert, AlertTitle } from "@/shared/components/ui/alert" +import { FieldGroup } from "@/shared/components/ui/field" +import { + Rhform, + RhfTextField, + RhfSelectField, + RhfAsyncSelectField, + RhfCheckboxField, +} from "@/shared/components/form" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" +import { useResourceForm } from "@/shared/hooks/use-resource-form" +import { useFormMutation } from "@/shared/hooks/use-form-mutation" +import { toRelation, toId } from "@/shared/lib/utils" + +import { + employeeFormSchema, + type EmployeeFormValues, +} from "./employee.schema" +import { EMPLOYEE_ROUTES, DEPARTMENT_ROUTES, SHOP_TIMING_ROUTES, SHOP_CALENDAR_ROUTES } from "@repo/api" + +// ── Constants ── + +const STATUS_OPTIONS = [ + { value: "active", label: "Active" }, + { value: "inactive", label: "Inactive" }, +] + +const TYPE_OPTIONS = [ + { value: "employee", label: "Employee" }, + { value: "contractor", label: "Contractor" }, +] + +// ── Props ── + +export type EmployeeFormProps = { + resourceId?: string | null + initialData?: unknown + onSuccess?: () => void +} + +// ── Default values ── + +const DEFAULT_VALUES: EmployeeFormValues = { + department: null, + shop_calender: null, + shop_timing: null, + first_name: "", + last_name: "", + email: "", + phone: "", + position: "", + status: "active", + type: "employee", + track_attendance: true, + notify_owner_when_punch_in_out: false, + geo_fence_radius: 100, +} + +// ── Mapping helpers ── + +function mapToFormValues(data: unknown): EmployeeFormValues { + const d = (data as any)?.data ?? data ?? {} + + return { + department: toRelation(d.department_id, d.department?.name), + shop_calender: toRelation(d.shop_calender_id, d.shop_calender?.title), + shop_timing: toRelation(d.shop_timing_id, d.shop_timing?.title), + first_name: d.first_name || "", + last_name: d.last_name || "", + email: d.email || "", + phone: d.phone || "", + position: d.position || "", + 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, + } +} + +function mapFormToPayload(values: EmployeeFormValues) { + return { + department_id: toId(values.department), + shop_calender_id: toId(values.shop_calender), + shop_timing_id: toId(values.shop_timing), + first_name: values.first_name, + last_name: values.last_name, + email: values.email || undefined, + phone: values.phone || undefined, + position: values.position || undefined, + status: values.status || undefined, + type: values.type || undefined, + 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, + } +} + +// ── Shared mapOption for async selects ── + +const mapLookupOption = (item: any) => ({ + value: String(item.id), + label: item.name ?? item.title ?? String(item.id), +}) + +const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label } + +// ── Component ── + +export function EmployeeForm({ resourceId, initialData, onSuccess }: EmployeeFormProps) { + const api = useAuthApi() + + const { form, isEditing } = useResourceForm({ + schema: employeeFormSchema, + defaultValues: DEFAULT_VALUES, + resourceId, + initialData, + initialize: (id) => api.employees.show(id), + queryKey: [EMPLOYEE_ROUTES.BY_ID, resourceId], + mapToFormValues: mapToFormValues, + }) + + const { mutate, error, isPending } = useFormMutation(form, { + mutationFn: (values: EmployeeFormValues) => { + const payload = mapFormToPayload(values) + const promise = isEditing && resourceId + ? api.employees.update(resourceId, payload) + : api.employees.create(payload) + toast.promise(promise, { + loading: isEditing ? "Updating employee..." : "Creating employee...", + success: isEditing ? "Employee updated successfully" : "Employee created successfully", + error: isEditing ? "Failed to update employee" : "Failed to create employee", + }) + return promise + }, + onSuccess: () => { + form.reset() + onSuccess?.() + }, + }) + + return ( + mutate(values)}> + {error && ( + + + + {isEditing ? "Failed to update employee" : "Failed to create employee"} + + {error.message} + + )} + + +
+ + +
+ +
+ + +
+ +
+ + api.departments.list()} + mapOption={mapLookupOption} + {...STORE_OBJECT} + /> +
+ +
+ + +
+ +
+ api.shopCalendars.list()} + mapOption={mapLookupOption} + {...STORE_OBJECT} + /> + api.shopTimings.list()} + mapOption={mapLookupOption} + {...STORE_OBJECT} + /> +
+ + + +
+ + +
+ + +
+
+ ) +} diff --git a/modules/employees/employee.schema.ts b/modules/employees/employee.schema.ts new file mode 100644 index 0000000..6a0f374 --- /dev/null +++ b/modules/employees/employee.schema.ts @@ -0,0 +1,32 @@ +import { z } from "zod" + +const relationFieldSchema = z + .object({ value: z.string(), label: z.string() }) + .nullable() + +const STATUS_OPTIONS = ["active", "inactive"] as const +const TYPE_OPTIONS = ["employee", "contractor"] as const + +const employeeFormSchema = z.object({ + department: 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(), + track_attendance: z.boolean(), + notify_owner_when_punch_in_out: z.boolean(), + geo_fence_radius: z.coerce.number().min(0).optional(), +}) + +type EmployeeFormValues = z.infer + +export { employeeFormSchema, relationFieldSchema, STATUS_OPTIONS, TYPE_OPTIONS } +export type { EmployeeFormValues } diff --git a/modules/inspections/inline-forms/inspection-category-inline-form.tsx b/modules/inspections/inline-forms/inspection-category-inline-form.tsx new file mode 100644 index 0000000..215f0bc --- /dev/null +++ b/modules/inspections/inline-forms/inspection-category-inline-form.tsx @@ -0,0 +1,57 @@ +"use client" + +import { z } from "zod" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { Plus } from "lucide-react" +import { Button } from "@/shared/components/ui/button" +import { FieldGroup } from "@/shared/components/ui/field" +import { Rhform, RhfTextField, type InlineCreateFormProps } from "@/shared/components/form" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" + +const schema = z.object({ + inspection_name: z.string().min(1, "Name is required"), +}) + +type FormValues = z.infer + +export function InspectionCategoryInlineForm({ onSuccess }: InlineCreateFormProps) { + const api = useAuthApi() + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { inspection_name: "" }, + }) + + const handleSubmit = async (values: FormValues) => { + try { + const result = await api.inspections.createCategory({ + inspection_name: values.inspection_name, + }) + toast.success("Inspection category created") + form.reset() + const item = (result as any)?.data ?? result + onSuccess({ value: String(item.id), label: item.name ?? values.inspection_name }) + } catch { + toast.error("Failed to create inspection category") + } + } + + return ( + + + + + + + ) +} diff --git a/modules/inspections/inspection-form.tsx b/modules/inspections/inspection-form.tsx new file mode 100644 index 0000000..bebdf4f --- /dev/null +++ b/modules/inspections/inspection-form.tsx @@ -0,0 +1,231 @@ +"use client" + +import { AlertTriangle, Plus, Save } from "lucide-react" + +import { Button } from "@/shared/components/ui/button" +import { Alert, AlertTitle } from "@/shared/components/ui/alert" +import { FieldGroup } from "@/shared/components/ui/field" +import { + Rhform, + RhfTextField, + RhfAsyncSelectField, +} from "@/shared/components/form" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" +import { useResourceForm } from "@/shared/hooks/use-resource-form" +import { useFormMutation } from "@/shared/hooks/use-form-mutation" +import { toRelation, toId } from "@/shared/lib/utils" +import { InspectionCategoryInlineForm } from "./inline-forms/inspection-category-inline-form" +import { DepartmentInlineForm } from "@/modules/services/inline-forms/department-inline-form" + +import { + inspectionFormSchema, + type InspectionFormValues, +} from "./inspection.schema" +import { + INSPECTION_ROUTES, + CUSTOMER_ROUTES, + VEHICLE_ROUTES, + DEPARTMENT_ROUTES, + EMPLOYEE_ROUTES, +} from "@repo/api" + +// ── Props ── + +export type InspectionFormProps = { + resourceId?: string | null + initialData?: unknown + onSuccess?: () => void +} + +// ── Default values ── + +const DEFAULT_VALUES: InspectionFormValues = { + customer: null, + vehicle: null, + department: null, + inspection_category: null, + employee: null, + title: "", + order_number: "", + date: "", + time: "", +} + +// ── Mapping helpers ── + +function mapToFormValues(data: unknown): InspectionFormValues { + const d = (data as any)?.data ?? data ?? {} + + return { + customer: toRelation(d.customer_id, d.customer?.first_name ? `${d.customer.first_name} ${d.customer.last_name ?? ""}`.trim() : undefined), + vehicle: toRelation(d.vehicle_id, d.vehicle?.make ? `${d.vehicle.make} ${d.vehicle.model ?? ""}`.trim() : undefined), + department: toRelation(d.department_id, d.department?.name), + inspection_category: toRelation(d.inspection_category_id, d.inspection_category?.name), + employee: toRelation(d.employee_id, d.employee?.first_name ? `${d.employee.first_name} ${d.employee.last_name ?? ""}`.trim() : undefined), + title: d.title ?? "", + order_number: d.order_number ?? "", + date: d.date ?? "", + time: d.time ?? "", + } +} + +function mapFormToPayload(values: InspectionFormValues) { + return { + customer_id: toId(values.customer), + vehicle_id: toId(values.vehicle), + department_id: toId(values.department), + inspection_category_id: toId(values.inspection_category), + employee_id: toId(values.employee), + title: values.title, + order_number: values.order_number || undefined, + date: values.date || undefined, + time: values.time || undefined, + } +} + +// ── Shared mapOption for async selects ── + +const mapLookupOption = (item: any) => ({ + value: String(item.id), + label: item.name ?? item.title ?? String(item.id), +}) + +const mapCustomerOption = (item: any) => ({ + value: String(item.id), + label: `${item.first_name ?? ""} ${item.last_name ?? ""}`.trim() || String(item.id), +}) + +const mapVehicleOption = (item: any) => ({ + value: String(item.id), + label: `${item.make ?? ""} ${item.model ?? ""} ${item.year ?? ""}`.trim() || String(item.id), +}) + +const mapEmployeeOption = (item: any) => ({ + value: String(item.id), + label: `${item.first_name ?? ""} ${item.last_name ?? ""}`.trim() || String(item.id), +}) + +const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label } + +// ── Component ── + +export function InspectionForm({ resourceId, initialData, onSuccess }: InspectionFormProps) { + const api = useAuthApi() + + const { form, isEditing } = useResourceForm({ + schema: inspectionFormSchema, + defaultValues: DEFAULT_VALUES, + resourceId, + initialData, + queryKey: [INSPECTION_ROUTES.BY_ID, resourceId], + mapToFormValues: mapToFormValues, + }) + + const { mutate, error, isPending } = useFormMutation(form, { + mutationFn: (values: InspectionFormValues) => { + const payload = mapFormToPayload(values) + const promise = isEditing && resourceId + ? api.inspections.update(resourceId, payload) + : api.inspections.create(payload) + toast.promise(promise, { + loading: isEditing ? "Updating inspection..." : "Creating inspection...", + success: isEditing ? "Inspection updated successfully" : "Inspection created successfully", + error: isEditing ? "Failed to update inspection" : "Failed to create inspection", + }) + return promise + }, + onSuccess: () => { + form.reset() + onSuccess?.() + }, + }) + + return ( + mutate(values)}> + {error && ( + + + + {isEditing ? "Failed to update inspection" : "Failed to create inspection"} + + {error.message} + + )} + + + + +
+ api.customers.list()} + mapOption={mapCustomerOption} + {...STORE_OBJECT} + /> + api.vehicles.list()} + mapOption={mapVehicleOption} + {...STORE_OBJECT} + /> +
+ +
+ api.departments.list()} + mapOption={mapLookupOption} + createForm={(props) => } + createLabel="Department" + {...STORE_OBJECT} + /> + api.inspections.listCategories()} + mapOption={mapLookupOption} + createForm={(props) => } + createLabel="Inspection Category" + {...STORE_OBJECT} + /> +
+ + api.employees.list()} + mapOption={mapEmployeeOption} + {...STORE_OBJECT} + /> + + + +
+ + +
+ + +
+
+ ) +} diff --git a/modules/inspections/inspection.schema.ts b/modules/inspections/inspection.schema.ts new file mode 100644 index 0000000..05cdc71 --- /dev/null +++ b/modules/inspections/inspection.schema.ts @@ -0,0 +1,22 @@ +import { z } from "zod" + +const relationFieldSchema = z + .object({ value: z.string(), label: z.string() }) + .nullable() + +const inspectionFormSchema = z.object({ + customer: relationFieldSchema, + vehicle: relationFieldSchema, + department: relationFieldSchema, + inspection_category: relationFieldSchema, + employee: relationFieldSchema, + title: z.string().min(1, "Title is required"), + order_number: z.string().optional(), + date: z.string().optional(), + time: z.string().optional(), +}) + +type InspectionFormValues = z.infer + +export { inspectionFormSchema, relationFieldSchema } +export type { InspectionFormValues } diff --git a/modules/shop-calendars/shop-calendar-form.tsx b/modules/shop-calendars/shop-calendar-form.tsx new file mode 100644 index 0000000..447c921 --- /dev/null +++ b/modules/shop-calendars/shop-calendar-form.tsx @@ -0,0 +1,99 @@ +"use client" + +import { AlertTriangle, Plus } from "lucide-react" + +import { Button } from "@/shared/components/ui/button" +import { Alert, AlertTitle } from "@/shared/components/ui/alert" +import { FieldGroup } from "@/shared/components/ui/field" +import { + Rhform, + RhfTextField, + RhfCheckboxField, +} from "@/shared/components/form" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" +import { useResourceForm } from "@/shared/hooks/use-resource-form" +import { useFormMutation } from "@/shared/hooks/use-form-mutation" + +import { + shopCalendarFormSchema, + type ShopCalendarFormValues, +} from "./shop-calendar.schema" +import { SHOP_CALENDAR_ROUTES } from "@repo/api" + +// ── Props ── + +export type ShopCalendarFormProps = { + resourceId?: string | null + initialData?: unknown + onSuccess?: () => void +} + +// ── Default values ── + +const DEFAULT_VALUES: ShopCalendarFormValues = { + title: "", + is_default: false, +} + +// ── Component ── + +export function ShopCalendarForm({ resourceId, onSuccess }: ShopCalendarFormProps) { + const api = useAuthApi() + + const { form } = useResourceForm({ + schema: shopCalendarFormSchema, + defaultValues: DEFAULT_VALUES, + resourceId: null, + queryKey: [SHOP_CALENDAR_ROUTES.INDEX], + mapToFormValues: (data: unknown) => { + const d = (data as any)?.data ?? data ?? {} + return { + title: d.title ?? "", + is_default: d.is_default ?? false, + } + }, + }) + + const { mutate, error, isPending } = useFormMutation(form, { + mutationFn: (values: ShopCalendarFormValues) => { + const payload = { + title: values.title, + is_default: values.is_default, + } + const promise = api.shopCalendars.create(payload) + toast.promise(promise, { + loading: "Creating shop calendar...", + success: "Shop calendar created successfully", + error: "Failed to create shop calendar", + }) + return promise + }, + onSuccess: () => { + form.reset() + onSuccess?.() + }, + }) + + return ( + mutate(values)}> + {error && ( + + + Failed to create shop calendar + {error.message} + + )} + + + + + + + + + ) +} diff --git a/modules/shop-calendars/shop-calendar.schema.ts b/modules/shop-calendars/shop-calendar.schema.ts new file mode 100644 index 0000000..c9bbf14 --- /dev/null +++ b/modules/shop-calendars/shop-calendar.schema.ts @@ -0,0 +1,11 @@ +import { z } from "zod" + +const shopCalendarFormSchema = z.object({ + title: z.string().min(1, "Title is required"), + is_default: z.boolean(), +}) + +type ShopCalendarFormValues = z.infer + +export { shopCalendarFormSchema } +export type { ShopCalendarFormValues } diff --git a/modules/shop-timings/shop-timing-form.tsx b/modules/shop-timings/shop-timing-form.tsx new file mode 100644 index 0000000..045b679 --- /dev/null +++ b/modules/shop-timings/shop-timing-form.tsx @@ -0,0 +1,161 @@ +"use client" + +import { AlertTriangle, Plus, Save } from "lucide-react" + +import { Button } from "@/shared/components/ui/button" +import { Alert, AlertTitle } from "@/shared/components/ui/alert" +import { FieldGroup } from "@/shared/components/ui/field" +import { + Rhform, + RhfTextField, + RhfCheckboxField, +} from "@/shared/components/form" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" +import { useResourceForm } from "@/shared/hooks/use-resource-form" +import { useFormMutation } from "@/shared/hooks/use-form-mutation" + +import { + shopTimingFormSchema, + type ShopTimingFormValues, +} from "./shop-timing.schema" +import { SHOP_TIMING_ROUTES } from "@repo/api" + +// ── Props ── + +export type ShopTimingFormProps = { + resourceId?: string | null + initialData?: unknown + onSuccess?: () => void +} + +// ── Default values ── + +const DEFAULT_VALUES: ShopTimingFormValues = { + title: "", + in_time: "", + out_time: "", + full_day_hours: "", + half_day_hours: "", + punch_in: "", + punch_out: "", + before_time: "", + after_time: "", + is_default: false, +} + +// ── Mapping helpers ── + +function mapToFormValues(data: unknown): ShopTimingFormValues { + const d = (data as any)?.data ?? data ?? {} + + return { + title: d.title ?? "", + in_time: d.in_time ?? "", + out_time: d.out_time ?? "", + full_day_hours: d.full_day_hours ?? "", + half_day_hours: d.half_day_hours ?? "", + punch_in: d.punch_in ?? "", + punch_out: d.punch_out ?? "", + before_time: d.before_time ?? "", + after_time: d.after_time ?? "", + is_default: d.is_default ?? false, + } +} + +function mapFormToPayload(values: ShopTimingFormValues) { + return { + title: values.title, + in_time: values.in_time, + out_time: values.out_time, + full_day_hours: values.full_day_hours || undefined, + half_day_hours: values.half_day_hours || undefined, + punch_in: values.punch_in || undefined, + punch_out: values.punch_out || undefined, + before_time: values.before_time || undefined, + after_time: values.after_time || undefined, + is_default: values.is_default, + } +} + +// ── Component ── + +export function ShopTimingForm({ resourceId, initialData, onSuccess }: ShopTimingFormProps) { + const api = useAuthApi() + + const { form, isEditing } = useResourceForm({ + schema: shopTimingFormSchema, + defaultValues: DEFAULT_VALUES, + resourceId, + initialData, + initialize: (id) => api.shopTimings.show(id), + queryKey: [SHOP_TIMING_ROUTES.BY_ID, resourceId], + mapToFormValues: mapToFormValues, + }) + + const { mutate, error, isPending } = useFormMutation(form, { + mutationFn: (values: ShopTimingFormValues) => { + const payload = mapFormToPayload(values) + const promise = isEditing && resourceId + ? api.shopTimings.update(resourceId, payload) + : api.shopTimings.create(payload) + toast.promise(promise, { + loading: isEditing ? "Updating shop timing..." : "Creating shop timing...", + success: isEditing ? "Shop timing updated successfully" : "Shop timing created successfully", + error: isEditing ? "Failed to update shop timing" : "Failed to create shop timing", + }) + return promise + }, + onSuccess: () => { + form.reset() + onSuccess?.() + }, + }) + + return ( + mutate(values)}> + {error && ( + + + + {isEditing ? "Failed to update shop timing" : "Failed to create shop timing"} + + {error.message} + + )} + + + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + +
+
+ ) +} diff --git a/modules/shop-timings/shop-timing.schema.ts b/modules/shop-timings/shop-timing.schema.ts new file mode 100644 index 0000000..7d66abe --- /dev/null +++ b/modules/shop-timings/shop-timing.schema.ts @@ -0,0 +1,19 @@ +import { z } from "zod" + +const shopTimingFormSchema = z.object({ + title: z.string().min(1, "Title is required"), + in_time: z.string().min(1, "In time is required"), + out_time: z.string().min(1, "Out time is required"), + full_day_hours: z.string().optional(), + half_day_hours: z.string().optional(), + punch_in: z.string().optional(), + punch_out: z.string().optional(), + before_time: z.string().optional(), + after_time: z.string().optional(), + is_default: z.boolean().default(false), +}) + +type ShopTimingFormValues = z.infer + +export { shopTimingFormSchema } +export type { ShopTimingFormValues } diff --git a/shared/data-view/table-view/search-params.ts b/shared/data-view/table-view/search-params.ts index 2e49baf..cdb421a 100644 --- a/shared/data-view/table-view/search-params.ts +++ b/shared/data-view/table-view/search-params.ts @@ -6,10 +6,11 @@ import { parseAsStringEnum, createSearchParamsCache, } from "nuqs/server" +import { DEFAULT_PER_PAGE } from "@repo/api" export const dataTableSearchParams = { page: parseAsInteger.withDefault(1), - per_page: parseAsInteger.withDefault(10), + per_page: parseAsInteger.withDefault(DEFAULT_PER_PAGE), sort_by: parseAsString, sort_order: parseAsStringEnum(["asc", "desc"] as const), }