This commit is contained in:
2026-04-23 14:38:41 +03:00
parent f17dd1486c
commit c0f78c6e18
90 changed files with 10819 additions and 1583 deletions
@@ -8,10 +8,13 @@ import { FieldGroup } from "@/shared/components/ui/field"
import {
Rhform,
RhfTextField,
RhfTextareaField,
RhfSelectField,
RhfAsyncSelectField,
RhfCheckboxField,
} from "@/shared/components/form"
import { UnitTypeInlineForm } from "@/modules/services/inline-forms/unit-type-inline-form"
import { DepartmentInlineForm } from "@/modules/services/inline-forms/department-inline-form"
import { toast } from "sonner"
import { useAuthApi } from "@/shared/useApi"
import { useResourceForm } from "@/shared/hooks/use-resource-form"
@@ -19,7 +22,13 @@ 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 {
EXPENSE_ITEM_ROUTES,
INVENTORY_CATEGORY_ROUTES,
INVENTORY_ROUTES,
DEPARTMENT_ROUTES,
VENDOR_ROUTES,
} from "@garage/api"
import { InventoryCategoryCrudDialog } from "./inventory-category-crud-dialog"
// ── Constants ──
@@ -48,10 +57,19 @@ export type ExpenseItemFormProps = {
const DEFAULT_VALUES: ExpenseItemFormValues = {
item_type: "Expense",
item_name: "",
sku: "",
item_code: "",
description: "",
category: null,
unit_type: null,
department: null,
purchase_information: true,
purchase_price: undefined,
purchase_chart_of_account: "",
purchase_information: true,
purchase_preferred_vendor: null,
sales_information: false,
selling_price: undefined,
sales_chart_of_account: "",
is_active: true,
}
@@ -63,10 +81,22 @@ function mapToFormValues(data: unknown): ExpenseItemFormValues {
return {
item_type: d.item_type || "Expense",
item_name: d.item_name || "",
sku: d.sku || "",
item_code: d.item_code || "",
description: d.description || "",
category: toRelation(d.category_id, d.category_title ?? d.category_name),
unit_type: toRelation(d.unit_type_id, d.unit_type_title ?? d.unit_type_name),
department: toRelation(d.department_id, d.department_name ?? d.department_title),
purchase_information: d.purchase_information ?? true,
purchase_price: d.purchase_price ?? undefined,
purchase_chart_of_account: d.purchase_chart_of_account || "",
purchase_information: d.purchase_information ?? true,
purchase_preferred_vendor: toRelation(
d.purchase_preferred_vendor_id,
d.purchase_preferred_vendor_name,
),
sales_information: d.sales_information ?? false,
selling_price: d.selling_price ?? undefined,
sales_chart_of_account: d.sales_chart_of_account || "",
is_active: d.is_active ?? true,
}
}
@@ -75,10 +105,19 @@ function mapFormToPayload(values: ExpenseItemFormValues) {
return {
item_type: values.item_type,
item_name: values.item_name,
sku: values.sku || undefined,
item_code: values.item_code || undefined,
description: values.description || undefined,
category_id: toId(values.category),
unit_type_id: toId(values.unit_type),
department_id: toId(values.department),
purchase_information: values.purchase_information,
purchase_price: values.purchase_price,
purchase_chart_of_account: values.purchase_chart_of_account || undefined,
purchase_information: values.purchase_information,
purchase_preferred_vendor_id: toId(values.purchase_preferred_vendor),
sales_information: values.sales_information,
selling_price: values.selling_price,
sales_chart_of_account: values.sales_chart_of_account || undefined,
is_active: values.is_active,
}
}
@@ -129,6 +168,7 @@ export function ExpenseItemForm({ resourceId, initialData, onSuccess }: ExpenseI
)}
<FieldGroup>
{/* Basic Info */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfSelectField
name="item_type"
@@ -144,6 +184,27 @@ export function ExpenseItemForm({ resourceId, initialData, onSuccess }: ExpenseI
/>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfTextField
name="sku"
label="SKU"
placeholder="e.g. EXP-001"
/>
<RhfTextField
name="item_code"
label="Item Code"
placeholder="e.g. EXP-001"
/>
</div>
<RhfTextareaField
name="description"
label="Description"
placeholder="Optional description"
rows={3}
/>
{/* Classification */}
<div>
<div className="mb-1 flex items-center justify-between">
<span className="text-sm font-medium">Category</span>
@@ -160,6 +221,37 @@ export function ExpenseItemForm({ resourceId, initialData, onSuccess }: ExpenseI
/>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfAsyncSelectField
name="unit_type"
label="Unit Type"
placeholder="Select unit type"
queryKey={[INVENTORY_ROUTES.UNIT_TYPES]}
listFn={() => api.inventory.listUnitTypes()}
mapOption={mapLookupOption}
createForm={(props) => <UnitTypeInlineForm {...props} />}
createLabel="Unit Type"
{...STORE_OBJECT}
/>
<RhfAsyncSelectField
name="department"
label="Department"
placeholder="Select department"
queryKey={[DEPARTMENT_ROUTES.INDEX]}
listFn={() => api.departments.list()}
mapOption={(item: any) => ({ value: String(item.id), label: item.name ?? String(item.id) })}
createForm={(props) => <DepartmentInlineForm {...props} />}
createLabel="Department"
{...STORE_OBJECT}
/>
</div>
{/* Purchase Information */}
{/* <RhfCheckboxField
name="purchase_information"
label="Purchase Information"
/>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfTextField
name="purchase_price"
@@ -174,16 +266,41 @@ export function ExpenseItemForm({ resourceId, initialData, onSuccess }: ExpenseI
/>
</div>
<div className="flex flex-col gap-3">
<RhfCheckboxField
name="purchase_information"
label="Purchase Information"
<RhfAsyncSelectField
name="purchase_preferred_vendor"
label="Preferred Vendor"
placeholder="Select vendor"
queryKey={[VENDOR_ROUTES.INDEX]}
listFn={() => api.vendors.list()}
mapOption={(item: any) => ({ value: String(item.id), label: item.name ?? String(item.id) })}
{...STORE_OBJECT}
/> */}
{/* Sales Information */}
{/* <RhfCheckboxField
name="sales_information"
label="Sales Information"
/>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfTextField
name="selling_price"
label="Selling Price"
placeholder="0.00"
type="number"
/>
<RhfCheckboxField
name="is_active"
label="Active"
<RhfTextField
name="sales_chart_of_account"
label="Sales Chart of Account"
placeholder="e.g. Revenue"
/>
</div>
</div> */}
{/* Status */}
<RhfCheckboxField
name="is_active"
label="Active"
/>
</FieldGroup>
<div className="mt-6 flex justify-end">
@@ -7,10 +7,22 @@ export const relationFieldSchema = z
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"),
sku: z.string().optional(),
item_code: z.string().optional(),
description: z.string().optional(),
category: relationFieldSchema,
unit_type: relationFieldSchema,
department: relationFieldSchema,
// Purchase
purchase_information: z.boolean().default(true),
purchase_price: z.coerce.number().min(0).optional(),
purchase_chart_of_account: z.string().optional(),
purchase_information: z.boolean().default(true),
// purchase_preferred_vendor: relationFieldSchema,
// Sales
sales_information: z.boolean().default(false),
selling_price: z.coerce.number().min(0).optional(),
sales_chart_of_account: z.string().optional(),
// Status
is_active: z.boolean().default(true),
})
@@ -22,6 +22,8 @@ type ExpenseLineItem = {
title: string
quantity: number
rate: number
chart_of_account?: string
discount_amount?: number
description?: string
}
@@ -34,6 +36,8 @@ export type ExpenseItemsSelectorFieldProps<
name: TName & (TValues[TName] extends ExpenseItemsFieldConstraint ? TName : never)
label?: string
triggerLabel?: string
showChartOfAccount?: boolean
showDiscount?: boolean
}
export function ExpenseItemsSelectorField<
@@ -43,6 +47,8 @@ export function ExpenseItemsSelectorField<
name,
label = "Expense Items",
triggerLabel = "Add Expense Items",
showChartOfAccount = false,
showDiscount = false,
}: ExpenseItemsSelectorFieldProps<TValues, TName>) {
return (
<RhfResourceField<TValues, TName, ExpenseItemsClient>
@@ -69,6 +75,7 @@ export function ExpenseItemsSelectorField<
title: r.item_name || "",
quantity: 1,
rate: Number(r.purchase_price) || 0,
chart_of_account: r.purchase_chart_of_account ? String(r.purchase_chart_of_account) : "",
description: "",
} as any
}}
@@ -79,6 +86,8 @@ export function ExpenseItemsSelectorField<
<TableHead>Expense Item</TableHead>
<TableHead className="w-24">Qty</TableHead>
<TableHead className="w-28">Rate</TableHead>
{showChartOfAccount && <TableHead className="w-36">Chart of Account</TableHead>}
{showDiscount && <TableHead className="w-28">Discount</TableHead>}
<TableHead>Description</TableHead>
<TableHead className="w-12" />
</TableRow>
@@ -110,6 +119,32 @@ export function ExpenseItemsSelectorField<
className="h-8 w-24"
/>
</TableCell>
{showChartOfAccount && (
<TableCell>
<Input
value={item.chart_of_account ?? ""}
onChange={(e) =>
update(index, { ...item, chart_of_account: e.target.value } as any)
}
placeholder="Optional"
className="h-8 w-32"
/>
</TableCell>
)}
{showDiscount && (
<TableCell>
<Input
type="number"
min={0}
step={0.01}
value={item.discount_amount ?? 0}
onChange={(e) =>
update(index, { ...item, discount_amount: Number(e.target.value) || 0 } as any)
}
className="h-8 w-24"
/>
</TableCell>
)}
<TableCell>
<Input
value={item.description ?? ""}