-
-
- All
- {JobCardStatus.map((status) => (
-
- {formatEnum(status)}
-
- ))}
-
-
-
-
-
setSearchInput(e.target.value)}
- className="pl-8"
- />
+ tableHeader={
+
+
+
+ All
+ {JobCardStatus.map((status) => (
+
+ {formatEnum(status)}
+
+ ))}
+
+
+
+
+ setSearchInput(e.target.value)}
+ className="pl-8"
+ />
+
-
- }
- />
+ }
+ />
+
+
{ if (!open) filter.close() }}
+ onSubmit={filter.onSubmit}
+ onReset={filter.reset}
+ activeFilterCount={filter.activeFilterCount}
+ title="Filter Job Cards"
+ >
+
+
+ >
)
}
diff --git a/apps/dashboard/config/navGroups.tsx b/apps/dashboard/config/navGroups.tsx
index 8052ec0..7a5a0ab 100644
--- a/apps/dashboard/config/navGroups.tsx
+++ b/apps/dashboard/config/navGroups.tsx
@@ -57,14 +57,14 @@ export const navGroups: NavGroup[] = [
},
{
title: "Customer & Vehicles",
- href: "/customer-vehicles",
+ href: "/sales/vehicles",
icon:
,
},
- {
- title: "Reports",
- href: "/reports",
- icon:
,
- },
+ // {
+ // title: "Reports",
+ // href: "/reports",
+ // icon:
,
+ // },
],
},
{
@@ -107,35 +107,35 @@ export const navGroups: NavGroup[] = [
{ title: "Vendor Credits", href: "/purchase/vendor-credit", icon:
},
],
},
- {
- title: "CRM",
- href: "/crm",
- icon:
,
- items: [
- { title: "Leads", href: "/crm/leads/list", icon:
},
- { title: "Calls", href: "/crm/calls-follow-up/list", icon:
},
- { title: "Tasks", href: "/crm/tasks/list", icon:
},
- ],
- },
- {
- title: "Marketing",
- href: "/marketing",
- icon:
,
- items: [
- { title: "Service Reminders", href: "/marketing/service-reminder/list", icon:
},
- { title: "Rating & Reviews", href: "/marketing/rating-review", icon:
},
- { title: "Google Business Reviews", href: "/marketing/google-rating-review", icon:
},
- ],
- },
- {
- title: "Accountants",
- href: "/accountants",
- icon:
,
- items: [
- { title: "Manual Journals", href: "/accountants/manual-journal", icon:
},
- { title: "Chart of Accounts", href: "/accountants/chart-of-account", icon:
},
- ],
- },
+ // {
+ // title: "CRM",
+ // href: "/crm",
+ // icon:
,
+ // items: [
+ // { title: "Leads", href: "/crm/leads/list", icon:
},
+ // { title: "Calls", href: "/crm/calls-follow-up/list", icon:
},
+ // { title: "Tasks", href: "/crm/tasks/list", icon:
},
+ // ],
+ // },
+ // {
+ // title: "Marketing",
+ // href: "/marketing",
+ // icon:
,
+ // items: [
+ // { title: "Service Reminders", href: "/marketing/service-reminder/list", icon:
},
+ // { title: "Rating & Reviews", href: "/marketing/rating-review", icon:
},
+ // { title: "Google Business Reviews", href: "/marketing/google-rating-review", icon:
},
+ // ],
+ // },
+ // {
+ // title: "Accountants",
+ // href: "/accountants",
+ // icon:
,
+ // items: [
+ // { title: "Manual Journals", href: "/accountants/manual-journal", icon:
},
+ // { title: "Chart of Accounts", href: "/accountants/chart-of-account", icon:
},
+ // ],
+ // },
{
title: "Employees",
href: "/productivity",
diff --git a/apps/dashboard/modules/credit-notes/credit-note-actions.tsx b/apps/dashboard/modules/credit-notes/credit-note-actions.tsx
new file mode 100644
index 0000000..52574c3
--- /dev/null
+++ b/apps/dashboard/modules/credit-notes/credit-note-actions.tsx
@@ -0,0 +1,50 @@
+"use client"
+
+import { useAuthApi } from "@/shared/useApi"
+import { useRouter } from "next/navigation"
+import { Button } from "@/shared/components/ui/button"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/shared/components/ui/dropdown-menu"
+import { Ellipsis, Pencil, Trash2 } from "lucide-react"
+
+type CreditNoteActionsProps = {
+ creditNoteId: string
+}
+
+export function CreditNoteActions({ creditNoteId }: CreditNoteActionsProps) {
+ const api = useAuthApi()
+ const router = useRouter()
+
+ const handleEdit = () => {
+ router.push(`/sales/credit-notes/${creditNoteId}/edit`)
+ }
+
+ const handleDelete = async () => {
+ await api.creditNotes.destroy(creditNoteId)
+ router.push("/sales/credit-notes")
+ }
+
+ return (
+
+
+
+
+
+
+
+ Edit
+
+
+
+ Delete
+
+
+
+ )
+}
diff --git a/apps/dashboard/modules/credit-notes/credit-note-context.tsx b/apps/dashboard/modules/credit-notes/credit-note-context.tsx
new file mode 100644
index 0000000..b9b77ba
--- /dev/null
+++ b/apps/dashboard/modules/credit-notes/credit-note-context.tsx
@@ -0,0 +1,28 @@
+"use client"
+
+import { createContext, useContext } from "react"
+
+type CreditNoteContextValue = {
+ id: string
+ label: string
+}
+
+const CreditNoteContext = createContext
(null)
+
+export function CreditNoteProvider({
+ creditNote,
+ children,
+}: {
+ creditNote: CreditNoteContextValue
+ children: React.ReactNode
+}) {
+ return (
+
+ {children}
+
+ )
+}
+
+export function useCreditNote() {
+ return useContext(CreditNoteContext)
+}
diff --git a/apps/dashboard/modules/credit-notes/credit-note-document-form.tsx b/apps/dashboard/modules/credit-notes/credit-note-document-form.tsx
new file mode 100644
index 0000000..3adc6bd
--- /dev/null
+++ b/apps/dashboard/modules/credit-notes/credit-note-document-form.tsx
@@ -0,0 +1,70 @@
+"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 } from "@/shared/components/form"
+import { toast } from "sonner"
+import { useAuthApi } from "@/shared/useApi"
+
+const schema = z.object({
+ attachments: z.instanceof(FileList).refine((files) => files.length > 0, "At least one file is required"),
+})
+
+type FormValues = z.infer
+
+type CreditNoteDocumentFormProps = {
+ creditNoteId: string
+ onSuccess?: () => void
+}
+
+export function CreditNoteDocumentForm({ creditNoteId, onSuccess }: CreditNoteDocumentFormProps) {
+ const api = useAuthApi()
+
+ const form = useForm({
+ resolver: zodResolver(schema),
+ })
+
+ const handleSubmit = async (values: FormValues) => {
+ try {
+ const formData = new FormData()
+ Array.from(values.attachments).forEach((file) => {
+ formData.append("attachments[]", file)
+ })
+ await api.creditNotes.addAttachment(creditNoteId, formData)
+ toast.success("Attachment uploaded")
+ form.reset()
+ onSuccess?.()
+ } catch {
+ toast.error("Failed to upload attachment")
+ }
+ }
+
+ return (
+
+
+
+
+
+ {form.formState.errors.attachments && (
+
+ {form.formState.errors.attachments.message as string}
+
+ )}
+
+
+
+
+ )
+}
diff --git a/apps/dashboard/modules/credit-notes/credit-note-form.tsx b/apps/dashboard/modules/credit-notes/credit-note-form.tsx
new file mode 100644
index 0000000..a724fe5
--- /dev/null
+++ b/apps/dashboard/modules/credit-notes/credit-note-form.tsx
@@ -0,0 +1,182 @@
+"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,
+ RhfTextareaField,
+ 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 {
+ creditNoteFormSchema,
+ type CreditNoteFormValues,
+} from "./credit-note.schema"
+import { CREDIT_NOTE_ROUTES, DEPARTMENT_ROUTES } from "@garage/api"
+import { RhfCustomerSelectField } from "@/modules/customers/rhf-customer-select-field"
+
+// ── Shared mapOption for async selects ──
+
+const mapLookupOption = (item: any) => ({
+ value: String(item.id),
+ label: item.name,
+})
+
+const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label }
+
+// ── Constants ──
+
+const STATUS_OPTIONS = [
+ { value: "draft", label: "Draft" },
+ { value: "open", label: "Open" },
+ { value: "applied", label: "Applied" },
+ { value: "void", label: "Void" },
+]
+
+// ── Props ──
+
+export type CreditNoteFormProps = {
+ resourceId?: string | null
+ initialData?: unknown
+ onSuccess?: () => void
+}
+
+// ── Default values ──
+
+const DEFAULT_VALUES: CreditNoteFormValues = {
+ subject: "",
+ customer: null,
+ department: null,
+ date: "",
+ status: "draft",
+ notes: "",
+}
+
+// ── Mapping helpers ──
+
+function mapToFormValues(data: unknown): CreditNoteFormValues {
+ const d = (data as any)?.data ?? data ?? {}
+
+ return {
+ subject: d.subject || "",
+ customer: toRelation(d.customer_id, d.customer_name),
+ department: toRelation(d.department_id, d.department_name),
+ date: d.date || "",
+ status: d.status || "draft",
+ notes: d.notes || "",
+ }
+}
+
+function mapFormToPayload(values: CreditNoteFormValues) {
+ return {
+ subject: values.subject,
+ customer_id: toId(values.customer),
+ department_id: toId(values.department),
+ date: values.date || undefined,
+ status: values.status,
+ notes: values.notes || undefined,
+ }
+}
+
+// ── Component ──
+
+export function CreditNoteForm({ resourceId, initialData, onSuccess }: CreditNoteFormProps) {
+ const api = useAuthApi()
+
+ const { form, isEditing } = useResourceForm({
+ schema: creditNoteFormSchema,
+ defaultValues: DEFAULT_VALUES,
+ resourceId: resourceId ?? null,
+ initialData,
+ queryKey: [CREDIT_NOTE_ROUTES.BY_ID, resourceId],
+ initialize: (id) => api.creditNotes.show(id),
+ mapToFormValues,
+ })
+
+ const { mutate, error, isPending } = useFormMutation(form, {
+ mutationFn: (values: CreditNoteFormValues) => {
+ const payload = mapFormToPayload(values)
+ const promise = (isEditing && resourceId
+ ? api.creditNotes.update(resourceId, payload)
+ : api.creditNotes.create(payload)) as Promise
+ toast.promise(promise, {
+ loading: isEditing ? "Updating credit note..." : "Creating credit note...",
+ success: isEditing ? "Credit note updated successfully" : "Credit note created successfully",
+ error: isEditing ? "Failed to update credit note" : "Failed to create credit note",
+ })
+ return promise
+ },
+ onSuccess: () => {
+ form.reset()
+ onSuccess?.()
+ },
+ })
+
+ return (
+ mutate(values)}>
+ {error && (
+
+
+
+ {isEditing ? "Failed to update credit note" : "Failed to create credit note"}
+
+ {error.message}
+
+ )}
+
+
+
+
+
+
+
+ api.departments.list()}
+ mapOption={mapLookupOption}
+ {...STORE_OBJECT}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/dashboard/modules/credit-notes/credit-note-general-info.tsx b/apps/dashboard/modules/credit-notes/credit-note-general-info.tsx
new file mode 100644
index 0000000..b6b0a99
--- /dev/null
+++ b/apps/dashboard/modules/credit-notes/credit-note-general-info.tsx
@@ -0,0 +1,146 @@
+import {
+ FileText,
+ Calendar,
+ Hash,
+ Users,
+ Building2,
+ Clock,
+} from "lucide-react"
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from "@/shared/components/ui/card"
+import { Badge } from "@/shared/components/ui/badge"
+import { Separator } from "@/shared/components/ui/separator"
+
+type CreditNoteData = {
+ id?: number
+ subject?: string
+ credit_invoice?: string
+ date?: string
+ status?: string
+ notes?: string
+ customer_id?: number
+ customer_name?: string
+ department_name?: string
+ department_id?: number
+ invoice_id?: number
+ created_at?: string
+ updated_at?: string
+}
+
+type CreditNoteGeneralInfoProps = {
+ creditNote: CreditNoteData
+}
+
+function InfoItem({
+ icon: Icon,
+ label,
+ value,
+}: {
+ icon: React.ComponentType<{ className?: string }>
+ label: string
+ value?: string | null
+}) {
+ return (
+
+
+
+
+
+ {label}
+
+ {value || —}
+
+
+
+ )
+}
+
+const statusColorMap: Record = {
+ draft: "secondary",
+ open: "default",
+ applied: "default",
+ void: "outline",
+}
+
+export function CreditNoteGeneralInfo({ creditNote }: CreditNoteGeneralInfoProps) {
+ return (
+
+ {/* Credit Note Details */}
+
+
+
+
+ Credit Note Details
+
+
+
+
+ {creditNote.subject && (
+ {creditNote.subject}
+ )}
+ {creditNote.status && (
+
+ {creditNote.status.charAt(0).toUpperCase() + creditNote.status.slice(1)}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ {/* Relations */}
+
+
+
+
+ Related Info
+
+
+
+
+
+
+
+ {creditNote.notes && (
+ <>
+
+
+
Notes
+
{creditNote.notes}
+
+ >
+ )}
+
+
+
+ )
+}
diff --git a/apps/dashboard/modules/credit-notes/credit-note-note-form.tsx b/apps/dashboard/modules/credit-notes/credit-note-note-form.tsx
new file mode 100644
index 0000000..99e23c8
--- /dev/null
+++ b/apps/dashboard/modules/credit-notes/credit-note-note-form.tsx
@@ -0,0 +1,60 @@
+"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, RhfTextareaField } from "@/shared/components/form"
+import { toast } from "sonner"
+import { useAuthApi } from "@/shared/useApi"
+
+const schema = z.object({
+ note: z.string().min(1, "Note is required"),
+})
+
+type FormValues = z.infer
+
+type CreditNoteNoteFormProps = {
+ creditNoteId: string
+ onSuccess?: () => void
+}
+
+export function CreditNoteNoteForm({ creditNoteId, onSuccess }: CreditNoteNoteFormProps) {
+ const api = useAuthApi()
+
+ const form = useForm({
+ resolver: zodResolver(schema),
+ defaultValues: { note: "" },
+ })
+
+ const handleSubmit = async (values: FormValues) => {
+ try {
+ await api.creditNotes.addInternalNote(creditNoteId, { note: values.note })
+ toast.success("Note added")
+ form.reset()
+ onSuccess?.()
+ } catch {
+ toast.error("Failed to add note")
+ }
+ }
+
+ return (
+
+
+
+
+
+
+ )
+}
diff --git a/apps/dashboard/modules/credit-notes/credit-note.schema.ts b/apps/dashboard/modules/credit-notes/credit-note.schema.ts
new file mode 100644
index 0000000..4b1c280
--- /dev/null
+++ b/apps/dashboard/modules/credit-notes/credit-note.schema.ts
@@ -0,0 +1,24 @@
+import { z } from "zod"
+
+const relationFieldSchema = z
+ .object({ value: z.string(), label: z.string() })
+ .nullable()
+
+const creditNoteFormSchema = z.object({
+ // ── Required fields ──
+ subject: z.string().min(1, "Subject is required"),
+
+ // ── Relations ──
+ customer: relationFieldSchema,
+ department: relationFieldSchema,
+
+ // ── Optional fields ──
+ date: z.string().optional(),
+ status: z.string().optional(),
+ notes: z.string().optional(),
+})
+
+type CreditNoteFormValues = z.infer
+
+export { creditNoteFormSchema, relationFieldSchema }
+export type { CreditNoteFormValues }
diff --git a/apps/dashboard/modules/expense-items/expense-item-form.tsx b/apps/dashboard/modules/expense-items/expense-item-form.tsx
new file mode 100644
index 0000000..0a5b0c9
--- /dev/null
+++ b/apps/dashboard/modules/expense-items/expense-item-form.tsx
@@ -0,0 +1,206 @@
+"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 { expenseItemFormSchema, type ExpenseItemFormValues } from "./expense-item.schema"
+import { EXPENSE_ITEM_ROUTES, INVENTORY_CATEGORY_ROUTES } from "@garage/api"
+import { InventoryCategoryCrudDialog } from "./inventory-category-crud-dialog"
+
+// ── Constants ──
+
+const ITEM_TYPE_OPTIONS = [
+ { value: "Expense", label: "Expense" },
+]
+
+const mapLookupOption = (item: any) => ({
+ value: String(item.id),
+ label: item.title ?? item.name ?? String(item.id),
+})
+
+const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label }
+
+// ── Props ──
+
+export type ExpenseItemFormProps = {
+ resourceId?: string | null
+ initialData?: unknown
+ onSuccess?: () => void
+}
+
+// ── Default values ──
+
+const DEFAULT_VALUES: ExpenseItemFormValues = {
+ item_type: "Expense",
+ item_name: "",
+ category: null,
+ purchase_price: undefined,
+ purchase_chart_of_account: "",
+ purchase_information: true,
+ is_active: true,
+}
+
+// ── Mapping helpers ──
+
+function mapToFormValues(data: unknown): ExpenseItemFormValues {
+ const d = (data as any)?.data ?? data ?? {}
+
+ return {
+ item_type: d.item_type || "Expense",
+ item_name: d.item_name || "",
+ category: toRelation(d.category_id, d.category_title ?? d.category_name),
+ purchase_price: d.purchase_price ?? undefined,
+ purchase_chart_of_account: d.purchase_chart_of_account || "",
+ purchase_information: d.purchase_information ?? true,
+ is_active: d.is_active ?? true,
+ }
+}
+
+function mapFormToPayload(values: ExpenseItemFormValues) {
+ return {
+ item_type: values.item_type,
+ item_name: values.item_name,
+ category_id: toId(values.category),
+ purchase_price: values.purchase_price,
+ purchase_chart_of_account: values.purchase_chart_of_account || undefined,
+ purchase_information: values.purchase_information,
+ is_active: values.is_active,
+ }
+}
+
+// ── Component ──
+
+export function ExpenseItemForm({ resourceId, initialData, onSuccess }: ExpenseItemFormProps) {
+ const api = useAuthApi()
+
+ const { form, isEditing } = useResourceForm({
+ schema: expenseItemFormSchema,
+ defaultValues: DEFAULT_VALUES,
+ resourceId,
+ initialData,
+ initialize: (id) => api.expenseItems.show(id),
+ queryKey: [EXPENSE_ITEM_ROUTES.BY_ID, resourceId],
+ mapToFormValues,
+ })
+
+ const { mutate, error, isPending } = useFormMutation(form, {
+ mutationFn: (values: ExpenseItemFormValues) => {
+ const promise = isEditing && resourceId
+ ? api.expenseItems.update(resourceId, mapFormToPayload(values))
+ : api.expenseItems.create(mapFormToPayload(values))
+ toast.promise(promise, {
+ loading: isEditing ? "Updating expense item..." : "Creating expense item...",
+ success: isEditing ? "Expense item updated successfully" : "Expense item created successfully",
+ error: isEditing ? "Failed to update expense item" : "Failed to create expense item",
+ })
+ return promise
+ },
+ onSuccess: () => {
+ form.reset()
+ onSuccess?.()
+ },
+ })
+
+ return (
+ mutate(values)}>
+ {error && (
+
+
+
+ {isEditing ? "Failed to update expense item" : "Failed to create expense item"}
+
+ {error.message}
+
+ )}
+
+
+
+
+
+
+
+
+
+ Category
+
+
+
api.inventoryCategories.list()}
+ mapOption={mapLookupOption}
+ {...STORE_OBJECT}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/dashboard/modules/expense-items/expense-item.schema.ts b/apps/dashboard/modules/expense-items/expense-item.schema.ts
new file mode 100644
index 0000000..3156be0
--- /dev/null
+++ b/apps/dashboard/modules/expense-items/expense-item.schema.ts
@@ -0,0 +1,17 @@
+import { z } from "zod"
+
+export const relationFieldSchema = z
+ .object({ value: z.string(), label: z.string() })
+ .nullable()
+
+export const expenseItemFormSchema = z.object({
+ item_type: z.string().min(1, "Item type is required"),
+ item_name: z.string().min(1, "Item name is required"),
+ category: relationFieldSchema,
+ purchase_price: z.coerce.number().min(0).optional(),
+ purchase_chart_of_account: z.string().optional(),
+ purchase_information: z.boolean().default(true),
+ is_active: z.boolean().default(true),
+})
+
+export type ExpenseItemFormValues = z.infer
diff --git a/apps/dashboard/modules/expense-items/inventory-category-crud-dialog.tsx b/apps/dashboard/modules/expense-items/inventory-category-crud-dialog.tsx
new file mode 100644
index 0000000..64a0a6a
--- /dev/null
+++ b/apps/dashboard/modules/expense-items/inventory-category-crud-dialog.tsx
@@ -0,0 +1,33 @@
+"use client"
+
+import { CrudDialog } from "@/shared/components/crud-dialog"
+import { ColumnHeader } from "@/shared/data-view/table-view"
+import { useAuthApi } from "@/shared/useApi"
+import { INVENTORY_CATEGORY_ROUTES } from "@garage/api"
+import { InventoryCategoryForm } from "./inventory-category-form"
+
+export function InventoryCategoryCrudDialog() {
+ const api = useAuthApi()
+
+ return (
+ api.inventoryCategories}
+ resourceLabel="inventory category"
+ columns={() => [
+ {
+ accessorKey: "title",
+ header: ({ column }) => ,
+ },
+ ]}
+ renderForm={({ resourceId, initialData, onSuccess }) => (
+
+ )}
+ />
+ )
+}
diff --git a/apps/dashboard/modules/expense-items/inventory-category-form.tsx b/apps/dashboard/modules/expense-items/inventory-category-form.tsx
new file mode 100644
index 0000000..3eab15d
--- /dev/null
+++ b/apps/dashboard/modules/expense-items/inventory-category-form.tsx
@@ -0,0 +1,80 @@
+"use client"
+
+import { z } from "zod"
+import { useForm } from "react-hook-form"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { Plus, Save } from "lucide-react"
+import { Button } from "@/shared/components/ui/button"
+import { FieldGroup } from "@/shared/components/ui/field"
+import { Rhform, RhfTextField } from "@/shared/components/form"
+import { toast } from "sonner"
+import { useAuthApi } from "@/shared/useApi"
+import { useEffect } from "react"
+
+const inventoryCategorySchema = z.object({
+ title: z.string().min(1, "Title is required"),
+})
+
+type InventoryCategoryFormValues = z.infer
+
+type InventoryCategoryFormProps = {
+ resourceId?: string | null
+ initialData?: any
+ onSuccess?: () => void
+}
+
+export function InventoryCategoryForm({ resourceId, initialData, onSuccess }: InventoryCategoryFormProps) {
+ const api = useAuthApi()
+ const isEditing = !!resourceId
+
+ const form = useForm({
+ resolver: zodResolver(inventoryCategorySchema),
+ defaultValues: { title: "" },
+ })
+
+ useEffect(() => {
+ if (initialData) {
+ const d = initialData?.data ?? initialData
+ form.reset({ title: d.title ?? "" })
+ }
+ }, [initialData, form])
+
+ const handleSubmit = async (values: InventoryCategoryFormValues) => {
+ try {
+ const promise = isEditing
+ ? api.inventoryCategories.update(resourceId!, { title: values.title })
+ : api.inventoryCategories.create({ title: values.title })
+
+ toast.promise(promise, {
+ loading: isEditing ? "Updating..." : "Creating...",
+ success: isEditing ? "Updated successfully" : "Created successfully",
+ error: isEditing ? "Failed to update" : "Failed to create",
+ })
+
+ await promise
+ form.reset()
+ onSuccess?.()
+ } catch {
+ // toast already shown
+ }
+ }
+
+ return (
+
+
+
+
+
+
+ )
+}
diff --git a/apps/dashboard/modules/inspections/inspection-form.tsx b/apps/dashboard/modules/inspections/inspection-form.tsx
index 598c6ef..6ad79b4 100644
--- a/apps/dashboard/modules/inspections/inspection-form.tsx
+++ b/apps/dashboard/modules/inspections/inspection-form.tsx
@@ -8,7 +8,11 @@ import { FieldGroup } from "@/shared/components/ui/field"
import {
Rhform,
RhfTextField,
+ RhfTextareaField,
+ RhfSelectField,
RhfAsyncSelectField,
+ RhfDateField,
+ RhfTimeField,
} from "@/shared/components/form"
import { toast } from "sonner"
import { useAuthApi } from "@/shared/useApi"
@@ -25,6 +29,9 @@ import {
import {
INSPECTION_ROUTES,
DEPARTMENT_ROUTES,
+ JOB_CARD_ROUTES,
+ InspectionStatus,
+ RateType,
} from "@garage/api"
import { RhfVehicleSelectField } from "@/modules/vehicles/rhf-vehicle-select-field"
import { RhfCustomerSelectField } from "@/modules/customers/rhf-customer-select-field"
@@ -46,10 +53,22 @@ const DEFAULT_VALUES: InspectionFormValues = {
department: null,
inspection_category: null,
employee: null,
+ job_card: null,
+ labor_rate: null,
title: "",
order_number: "",
date: "",
time: "",
+ status: "in_progress",
+ note: "",
+ description: "",
+ rate_type: "flat_rate",
+ quantity: 1,
+ rate: 0,
+ working_hours: 0,
+ labor_hours: 0,
+ tax: "",
+ chart_of_account: "",
}
// ── Mapping helpers ──
@@ -63,10 +82,26 @@ function mapToFormValues(data: unknown): InspectionFormValues {
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),
+ job_card: d.job_card_id
+ ? { value: String(d.job_card_id), label: d.job_card?.title ?? d.job_card?.order_number ?? String(d.job_card_id) }
+ : null,
+ labor_rate: d.labor_rate_id
+ ? { value: String(d.labor_rate_id), label: d.labor_rate?.title ?? String(d.labor_rate_id) }
+ : null,
title: d.title ?? "",
order_number: d.order_number ?? "",
- date: d.date ?? "",
+ date: d.date ? d.date.split("T")[0] : "",
time: d.time ?? "",
+ status: d.status ?? "in_progress",
+ note: d.note ?? "",
+ description: d.description ?? "",
+ rate_type: d.rate_type ?? "flat_rate",
+ quantity: d.quantity != null ? Number(d.quantity) : 1,
+ rate: d.rate != null ? Number(d.rate) : 0,
+ working_hours: d.working_hours != null ? Number(d.working_hours) : 0,
+ labor_hours: d.labor_hours != null ? Number(d.labor_hours) : 0,
+ tax: d.tax ?? "",
+ chart_of_account: d.chart_of_account ?? "",
}
}
@@ -77,14 +112,36 @@ function mapFormToPayload(values: InspectionFormValues) {
department_id: toId(values.department),
inspection_category_id: toId(values.inspection_category),
employee_id: toId(values.employee),
+ job_card_id: toId(values.job_card) ?? null,
+ labor_rate_id: values.labor_rate ? Number(values.labor_rate.value) : undefined,
title: values.title,
order_number: values.order_number || undefined,
date: values.date || undefined,
time: values.time || undefined,
+ status: values.status || undefined,
+ note: values.note || undefined,
+ description: values.description || undefined,
+ rate_type: values.rate_type || undefined,
+ quantity: values.quantity ?? undefined,
+ rate: values.rate ?? undefined,
+ working_hours: values.working_hours ?? undefined,
+ labor_hours: values.labor_hours ?? undefined,
+ tax: values.tax || undefined,
+ chart_of_account: values.chart_of_account || undefined,
}
}
-// ── Shared mapOption for async selects ──
+// ── Select options ──
+
+const STATUS_OPTIONS = InspectionStatus.map((v) => ({
+ value: v,
+ label: v.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
+}))
+
+const RATE_TYPE_OPTIONS = RateType.map((v) => ({
+ value: v,
+ label: v === "flat_rate" ? "Flat Rate" : "Hourly",
+}))
const mapLookupOption = (item: any) => ({
value: String(item.id),
@@ -171,15 +228,74 @@ export function InspectionForm({ resourceId, initialData, onSuccess }: Inspectio
/>
-