diff --git a/apps/dashboard/app/(authenticated)/purchase/expense/[id]/page.tsx b/apps/dashboard/app/(authenticated)/purchase/expense/[id]/page.tsx index 2ee8beb..d4c3bf6 100644 --- a/apps/dashboard/app/(authenticated)/purchase/expense/[id]/page.tsx +++ b/apps/dashboard/app/(authenticated)/purchase/expense/[id]/page.tsx @@ -2,6 +2,7 @@ import DashboardPage from '@/base/components/layout/dashboard/dashboard-page' import { ExpenseGeneralInfo } from '@/modules/expenses/expense-general-info' import { ExpenseItemsSection } from '@/modules/expenses/expense-items-section' import { ExpensePaymentsSection } from '@/modules/expenses/expense-payments-section' +import { formatTaxLabel } from '@/shared/utils/formatters' import { getServerApi } from '@garage/api/server' export default async function ExpenseDetailPage(props: { params: Promise<{ id: string }> }) { @@ -14,7 +15,7 @@ export default async function ExpenseDetailPage(props: { params: Promise<{ id: s return
Expense not found.
} - const taxLabel = data.tax?.title ? `${data.tax.title} (${data.tax.rate}%)` : undefined + const taxLabel = formatTaxLabel(data.tax, '') || undefined return ( diff --git a/apps/dashboard/modules/expense-items/expense-item.schema.ts b/apps/dashboard/modules/expense-items/expense-item.schema.ts index 42ab5c7..d7f88b4 100644 --- a/apps/dashboard/modules/expense-items/expense-item.schema.ts +++ b/apps/dashboard/modules/expense-items/expense-item.schema.ts @@ -17,7 +17,7 @@ export const expenseItemFormSchema = z.object({ purchase_information: z.boolean().default(true), purchase_price: z.coerce.number().min(0).optional(), purchase_chart_of_account: z.string().optional(), - // purchase_preferred_vendor: relationFieldSchema, + purchase_preferred_vendor: relationFieldSchema, // Sales sales_information: z.boolean().default(false), selling_price: z.coerce.number().min(0).optional(), diff --git a/apps/dashboard/modules/expenses/expense-context.tsx b/apps/dashboard/modules/expenses/expense-context.tsx index 70f1916..f370824 100644 --- a/apps/dashboard/modules/expenses/expense-context.tsx +++ b/apps/dashboard/modules/expenses/expense-context.tsx @@ -3,7 +3,47 @@ import { CrudShowResponse, ExpensesClient } from "@garage/api" import { createContext, useContext } from "react" -export type ExpenseContextValue = CrudShowResponse['data'] +type BaseExpenseContextValue = NonNullable["data"]> + +type ExpenseNamedRelation = { + id?: number + name?: string | null + title?: string | null +} + +type ExpenseVendorRelation = ExpenseNamedRelation & { + company_name?: string | null + first_name?: string | null + last_name?: string | null +} + +type ExpenseJobCardRelation = { + id?: number + order_number?: string | null + estimate_number?: string | null + title?: string | null +} + +type ExpenseTaxRelation = ExpenseNamedRelation & { + rate?: string | number | null +} + +type ExpenseLabel = { + id?: number + title?: string | null + color_code?: string | null +} + +export type ExpenseContextValue = BaseExpenseContextValue & { + discount?: string | null + discount_amount_major?: number | null + vendor?: ExpenseVendorRelation | null + department?: ExpenseNamedRelation | null + category?: ExpenseNamedRelation | null + job_card?: ExpenseJobCardRelation | null + tax?: ExpenseTaxRelation | null + labels?: ExpenseLabel[] | null +} const ExpenseContext = createContext(null) diff --git a/apps/dashboard/modules/expenses/expense-form.tsx b/apps/dashboard/modules/expenses/expense-form.tsx index a293406..f0e7cd3 100644 --- a/apps/dashboard/modules/expenses/expense-form.tsx +++ b/apps/dashboard/modules/expenses/expense-form.tsx @@ -19,6 +19,7 @@ import { useAuthApi } from "@/shared/useApi" import { useResourceForm } from "@/shared/hooks/use-resource-form" import { useFormMutation } from "@/shared/hooks/use-form-mutation" import { getTodayDate, toRelation, toId } from "@/shared/lib/utils" +import { formatTaxLabel } from "@/shared/utils/formatters" import { expenseFormSchema, @@ -89,7 +90,7 @@ function mapToFormValues(data: unknown): ExpenseFormValues { category: toRelation(d.category_id, d.category?.name ?? d.category?.title ?? d.category_name), vendor: toRelation(d.vendor_id, d.vendor?.company_name ?? d.vendor?.name ?? d.vendor_name), department: toRelation(d.department_id, d.department?.name ?? d.department_name), - tax: toRelation(d.tax_id, d.tax?.title ? `${d.tax.title} (${d.tax.rate}%)` : undefined), + tax: toRelation(d.tax_id, formatTaxLabel(d.tax, d.tax_title ?? "") || undefined), title: d.title || "", invoice_number: d.invoice_number || "", expense_date: d.expense_date ? d.expense_date.split("T")[0] : "", diff --git a/apps/dashboard/modules/expenses/expense-general-info.tsx b/apps/dashboard/modules/expenses/expense-general-info.tsx index 0ca08c9..00aaebe 100644 --- a/apps/dashboard/modules/expenses/expense-general-info.tsx +++ b/apps/dashboard/modules/expenses/expense-general-info.tsx @@ -20,8 +20,7 @@ import { } from "@/shared/components/ui/card" import { Badge } from "@/shared/components/ui/badge" import { cn } from "@/shared/lib/utils" -import { formatDate, formatCurrency, formatEnum } from "@/shared/utils/formatters" -import { getFullName } from "@/shared/utils/getFullName" +import { formatDate, formatCurrency, formatEnum, formatTaxLabel } from "@/shared/utils/formatters" import { useExpense } from "./expense-context" function InfoItem({ @@ -68,9 +67,7 @@ export function ExpenseGeneralInfo() { const labels = (expense as any)?.labels || [] const balanceDue = expense.balance_due ?? null const paymentsM = expense.payments_made ?? null - const taxLabel = expense.tax?.title - ? `${expense.tax.title} (${expense.tax.rate}%)` - : "Tax" + const taxLabel = formatTaxLabel(expense.tax, "Tax") return (
@@ -122,7 +119,7 @@ export function ExpenseGeneralInfo() { {/* ── Vendor ── */} Vendor - {vendor.company_name || getFullName(vendor as any) || "—"} + {vendor.name || "—"} {/* ── Expense Details ── */} diff --git a/apps/dashboard/modules/invoices/invoice-form.tsx b/apps/dashboard/modules/invoices/invoice-form.tsx index 34c6c76..f62ca65 100644 --- a/apps/dashboard/modules/invoices/invoice-form.tsx +++ b/apps/dashboard/modules/invoices/invoice-form.tsx @@ -21,6 +21,7 @@ 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 { formatTaxLabel } from "@/shared/utils/formatters" import { useFormContext } from "react-hook-form" import { @@ -120,7 +121,7 @@ function mapToFormValues(data: unknown): InvoiceFormValues { has_insurance: d.has_insurance ?? false, discount: d.discount || "no", discount_amount: d.discount_amount != null ? Number(d.discount_amount) : undefined, - tax: toRelation(d.tax_id, d.tax_title ?? d.tax?.title), + tax: toRelation(d.tax_id, d.tax_title ?? (formatTaxLabel(d.tax, "") || undefined)), deposit_to: d.deposit_to || "", notes: d.notes || "", terms_and_conditions: d.terms_and_conditions || "", diff --git a/apps/dashboard/modules/invoices/invoice-sequence-form.tsx b/apps/dashboard/modules/invoices/invoice-sequence-form.tsx index 4793723..0de503e 100644 --- a/apps/dashboard/modules/invoices/invoice-sequence-form.tsx +++ b/apps/dashboard/modules/invoices/invoice-sequence-form.tsx @@ -36,7 +36,7 @@ export function InvoiceSequenceForm({ resourceId, initialData, onSuccess }: Invo const isEditing = !!resourceId const form = useForm({ - resolver: zodResolver(invoiceSequenceSchema), + resolver: zodResolver(invoiceSequenceSchema) as any, defaultValues: { title: "", sequence_title: "", diff --git a/apps/dashboard/modules/job-cards/job-card-expense-item-form.tsx b/apps/dashboard/modules/job-cards/job-card-expense-item-form.tsx index 4d0f2d5..a1dd230 100644 --- a/apps/dashboard/modules/job-cards/job-card-expense-item-form.tsx +++ b/apps/dashboard/modules/job-cards/job-card-expense-item-form.tsx @@ -16,6 +16,7 @@ import { import { DepartmentInlineForm } from "@/modules/services/inline-forms/department-inline-form" import { toast } from "sonner" import { useAuthApi } from "@/shared/useApi" +import { formatTaxLabel } from "@/shared/utils/formatters" import { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import { DEPARTMENT_ROUTES, TAX_ROUTES } from "@garage/api" @@ -71,7 +72,7 @@ function mapToFormValues(data: unknown): JobCardExpenseItemFormValues { ? { value: String(d.department.id), label: d.department.name ?? String(d.department.id) } : null, tax: d.tax_id != null - ? { value: String(d.tax_id), label: d.tax ? `${d.tax.title} (${d.tax.rate}%)` : String(d.tax_id) } + ? { value: String(d.tax_id), label: formatTaxLabel(d.tax, String(d.tax_id)) } : null, quantity: d.quantity ?? 1, rate: d.rate != null ? Number(d.rate) : 0, diff --git a/apps/dashboard/modules/job-cards/job-card-form.tsx b/apps/dashboard/modules/job-cards/job-card-form.tsx index e10ea10..656626c 100644 --- a/apps/dashboard/modules/job-cards/job-card-form.tsx +++ b/apps/dashboard/modules/job-cards/job-card-form.tsx @@ -20,6 +20,7 @@ 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 { formatTaxLabel } from "@/shared/utils/formatters" import { jobCardFormSchema, @@ -106,7 +107,7 @@ function mapToFormValues(data: unknown): JobCardFormValues { sales_person: toRelation(d.sales_person_id, d.sales_person ? `${d.sales_person.first_name} ${d.sales_person.last_name}` : undefined), insurance_type: toRelation(d.insurance_type_id, d.insurance_type?.name), insurer: toRelation(d.insurer_id, d.insurer ? `${d.insurer.first_name} ${d.insurer.last_name}`.trim() || d.insurer.company_name : undefined), - tax: toRelation(d.tax_id, d.tax ? `${d.tax.title} (${d.tax.rate}%)` : undefined), + tax: toRelation(d.tax_id, formatTaxLabel(d.tax, "") || undefined), order_number: d.order_number || "", estimate_number: d.estimate_number || "", status: d.status || "draft", diff --git a/apps/dashboard/modules/job-cards/job-card-part-form.tsx b/apps/dashboard/modules/job-cards/job-card-part-form.tsx index d2a267b..35206f4 100644 --- a/apps/dashboard/modules/job-cards/job-card-part-form.tsx +++ b/apps/dashboard/modules/job-cards/job-card-part-form.tsx @@ -16,6 +16,7 @@ import { import { DepartmentInlineForm } from "@/modules/services/inline-forms/department-inline-form" import { toast } from "sonner" import { useAuthApi } from "@/shared/useApi" +import { formatTaxLabel } from "@/shared/utils/formatters" import { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import { DEPARTMENT_ROUTES, TAX_ROUTES } from "@garage/api" @@ -71,7 +72,7 @@ function mapToFormValues(data: unknown): JobCardPartFormValues { ? { value: String(d.department.id), label: d.department.name ?? String(d.department.id) } : null, tax: d.tax_id != null - ? { value: String(d.tax_id), label: d.tax ? `${d.tax.title} (${d.tax.rate}%)` : String(d.tax_id) } + ? { value: String(d.tax_id), label: formatTaxLabel(d.tax, String(d.tax_id)) } : null, quantity: d.quantity ?? 1, rate: d.rate != null ? Number(d.rate) : 0, diff --git a/apps/dashboard/modules/job-cards/job-card-service-form.tsx b/apps/dashboard/modules/job-cards/job-card-service-form.tsx index e5db3c5..2faa41c 100644 --- a/apps/dashboard/modules/job-cards/job-card-service-form.tsx +++ b/apps/dashboard/modules/job-cards/job-card-service-form.tsx @@ -17,6 +17,7 @@ import { import { DepartmentInlineForm } from "@/modules/services/inline-forms/department-inline-form" import { toast } from "sonner" import { useAuthApi } from "@/shared/useApi" +import { formatTaxLabel } from "@/shared/utils/formatters" import { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import { RateType, DEPARTMENT_ROUTES, TAX_ROUTES } from "@garage/api" @@ -80,7 +81,7 @@ function mapToFormValues(data: unknown): JobCardServiceFormValues { ? { value: String(d.department.id), label: d.department.name ?? String(d.department.id) } : undefined as any, tax: d.tax_id != null - ? { value: String(d.tax_id), label: d.tax ? `${d.tax.title} (${d.tax.rate}%)` : String(d.tax_id) } + ? { value: String(d.tax_id), label: formatTaxLabel(d.tax, String(d.tax_id)) } : null, rate_type: d.rate_type ?? "flat_rate", labor_rate: d.labor_rate diff --git a/apps/dashboard/shared/utils/formatters.ts b/apps/dashboard/shared/utils/formatters.ts index c0f7bb9..aed158c 100644 --- a/apps/dashboard/shared/utils/formatters.ts +++ b/apps/dashboard/shared/utils/formatters.ts @@ -56,6 +56,25 @@ export function formatEnum(value?: string | null): string { .join(" ") } +type TaxLabelValue = { + name?: string | null + title?: string | null + rate?: string | number | null +} + +export function formatTaxLabel( + value?: TaxLabelValue | null, + emptyFallback = "—", +): string { + const label = value?.name ?? value?.title + const rate = value?.rate + + if (!label) return emptyFallback + if (rate == null || rate === "") return label + + return `${label} (${rate}%)` +} + /** * Format a number with locale-aware thousand separators. * e.g. 150000 → "150,000" diff --git a/apps/dashboard/shared/utils/getFullName.ts b/apps/dashboard/shared/utils/getFullName.ts index eacf6c0..6eaf643 100644 --- a/apps/dashboard/shared/utils/getFullName.ts +++ b/apps/dashboard/shared/utils/getFullName.ts @@ -1,4 +1,9 @@ -export const getFullName = (user?: T) => { +type NameLike = { + first_name?: string | null + last_name?: string | null +} + +export const getFullName = (user?: T | null) => { const firstName = user?.first_name ?? "" const lastName = user?.last_name ?? "" return `${firstName} ${lastName}`.trim()