fix invoice informations
This commit is contained in:
parent
973149e974
commit
f17dd1486c
@ -1,5 +1,8 @@
|
||||
import { getServerApi } from '@garage/api/server'
|
||||
import { InvoiceGeneralInfo } from '@/modules/invoices/invoice-general-info'
|
||||
import { InvoicePartsSection } from '@/modules/invoices/invoice-parts-section'
|
||||
import { InvoiceServicesSection } from '@/modules/invoices/invoice-services-section'
|
||||
import { InvoiceExpensesSection } from '@/modules/invoices/invoice-expenses-section'
|
||||
import DashboardPage from '@/base/components/layout/dashboard/dashboard-page'
|
||||
|
||||
export default async function InvoiceDetailPage(props: { params: Promise<{ id: string }> }) {
|
||||
@ -14,7 +17,12 @@ export default async function InvoiceDetailPage(props: { params: Promise<{ id: s
|
||||
|
||||
return (
|
||||
<DashboardPage header={null}>
|
||||
<InvoiceGeneralInfo invoice={data} />
|
||||
<div className="grid gap-6">
|
||||
<InvoiceGeneralInfo invoice={data} />
|
||||
<InvoicePartsSection parts={data.invoice_parts} />
|
||||
<InvoiceServicesSection services={data.invoice_services} />
|
||||
<InvoiceExpensesSection expenses={data.invoice_expenses} />
|
||||
</div>
|
||||
</DashboardPage>
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from "react"
|
||||
import { FileText } from "lucide-react"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
@ -12,37 +13,107 @@ import {
|
||||
import { ScrollArea } from "@/shared/components/ui/scroll-area"
|
||||
import { InvoiceForm } from "@/modules/invoices/invoice-form"
|
||||
import { toRelation } from "@/shared/lib/utils"
|
||||
import { useAuthApi } from "@/shared/useApi"
|
||||
import { ESTIMATE_ROUTES } from "@garage/api"
|
||||
import { useEstimate } from "./estimate-context"
|
||||
|
||||
/**
|
||||
* Maps an Estimate data object to an InvoiceFormValues shape so that
|
||||
* useResourceForm's shallow spread correctly pre-fills all relational fields.
|
||||
*/
|
||||
function mapEstimateToInvoiceInitialData(estimate: Record<string, any>) {
|
||||
function mapEstimateToInvoiceInitialData(
|
||||
estimate: Record<string, any>,
|
||||
services: any[],
|
||||
parts: any[],
|
||||
expenseItems: any[],
|
||||
) {
|
||||
return {
|
||||
subject: estimate.title ?? "",
|
||||
notes: estimate.footer ?? "",
|
||||
|
||||
// Relation fields — must be { value, label } objects for RhfAsyncSelectField
|
||||
customer: toRelation(estimate.customer_id, estimate.customer_name),
|
||||
vehicle: toRelation(estimate.vehicle_id, estimate.vehicle_name),
|
||||
department: toRelation(estimate.department_id, estimate.department_name),
|
||||
estimate: toRelation(estimate.id, estimate.title ?? `#${estimate.estimate_number ?? estimate.id}`),
|
||||
customer: toRelation(
|
||||
estimate.customer_id,
|
||||
estimate.customer
|
||||
? `${estimate.customer.first_name ?? ""} ${estimate.customer.last_name ?? ""}`.trim()
|
||||
: undefined
|
||||
),
|
||||
vehicle: toRelation(
|
||||
estimate.vehicle_id,
|
||||
estimate.vehicle
|
||||
? `${estimate.vehicle.make ?? ""} ${estimate.vehicle.model ?? ""} (${estimate.vehicle.registration_number ?? ""})`.trim()
|
||||
: undefined
|
||||
),
|
||||
department: toRelation(estimate.department_id, estimate.department?.name),
|
||||
|
||||
invoice_number: "",
|
||||
invoice_date: estimate.date ?? "",
|
||||
due_date: "",
|
||||
status: "draft" as const,
|
||||
|
||||
services: services.map((s: any) => ({
|
||||
service_id: s.service_id ?? s.id,
|
||||
title: s.service.labor_name ?? s.title ?? "",
|
||||
quantity: Number(s.quantity) || 1,
|
||||
rate: Number(s.rate) || 0,
|
||||
description: s.description ?? "",
|
||||
})),
|
||||
parts: parts.map((p: any) => ({
|
||||
part_id: p.part_id ?? p.id,
|
||||
title: p.part.title ?? "",
|
||||
quantity: Number(p.quantity) || 1,
|
||||
rate: Number(p.rate) || 0,
|
||||
description: p.description ?? "",
|
||||
})),
|
||||
expense_items: expenseItems.map((e: any) => ({
|
||||
expense_id: e.expense_item_id ?? e.id,
|
||||
title: e.expense_item.item_name ?? e.title ?? "",
|
||||
quantity: Number(e.quantity) || 1,
|
||||
rate: Number(e.rate) || 0,
|
||||
description: e.description ?? "",
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
export function CreateInvoiceFromEstimateButton() {
|
||||
const [open, setOpen] = useState(false)
|
||||
const estimateContext = useEstimate()
|
||||
const api = useAuthApi()
|
||||
|
||||
const estimateId = estimateContext?.id ?? ""
|
||||
|
||||
const { data: servicesData } = useQuery({
|
||||
queryKey: [ESTIMATE_ROUTES.SERVICES, estimateId, "for-invoice"],
|
||||
queryFn: async () => {
|
||||
const res = await api.estimates.listServices(estimateId)
|
||||
return ((res as any)?.data ?? []) as any[]
|
||||
},
|
||||
enabled: open && !!estimateId,
|
||||
})
|
||||
|
||||
const { data: partsData } = useQuery({
|
||||
queryKey: [ESTIMATE_ROUTES.PARTS, estimateId, "for-invoice"],
|
||||
queryFn: async () => {
|
||||
const res = await api.estimates.listParts(estimateId)
|
||||
return ((res as any)?.data ?? []) as any[]
|
||||
},
|
||||
enabled: open && !!estimateId,
|
||||
})
|
||||
|
||||
const { data: expenseItemsData } = useQuery({
|
||||
queryKey: [ESTIMATE_ROUTES.EXPENSE_ITEMS, estimateId, "for-invoice"],
|
||||
queryFn: async () => {
|
||||
const res = await api.estimates.listExpenseItems(estimateId)
|
||||
return ((res as any)?.data ?? []) as any[]
|
||||
},
|
||||
enabled: open && !!estimateId,
|
||||
})
|
||||
|
||||
if (!estimateContext) return null
|
||||
|
||||
const initialData = estimateContext.data
|
||||
? mapEstimateToInvoiceInitialData(estimateContext.data)
|
||||
? mapEstimateToInvoiceInitialData(
|
||||
estimateContext.data,
|
||||
servicesData ?? [],
|
||||
partsData ?? [],
|
||||
expenseItemsData ?? [],
|
||||
)
|
||||
: undefined
|
||||
|
||||
return (
|
||||
|
||||
@ -37,6 +37,7 @@ type ExpenseLine = {
|
||||
quantity: number
|
||||
rate: number | string
|
||||
description?: string
|
||||
expense_item: any
|
||||
}
|
||||
|
||||
type SelectedExpenseItem = {
|
||||
@ -92,7 +93,7 @@ export function EstimateExpenseItemsSection({ estimateId }: { estimateId: string
|
||||
}
|
||||
|
||||
const getDisplayName = (item: ExpenseLine) =>
|
||||
item.item_name ?? item.title ?? `Item #${item.expense_item_id ?? item.id}`
|
||||
item.expense_item.item_name ?? item.expense_item.title ?? `Item #${item.expense_item_id ?? item.id}`
|
||||
|
||||
return (
|
||||
<Card>
|
||||
@ -112,7 +113,7 @@ export function EstimateExpenseItemsSection({ estimateId }: { estimateId: string
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Item</TableHead>
|
||||
<TableHead className="w-100">Item</TableHead>
|
||||
<TableHead className="w-24">Qty</TableHead>
|
||||
<TableHead className="w-28">Rate</TableHead>
|
||||
<TableHead>Description</TableHead>
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
CardTitle,
|
||||
} from "@/shared/components/ui/card"
|
||||
import { Badge } from "@/shared/components/ui/badge"
|
||||
import { formatDate } from "@/shared/utils/formatters"
|
||||
|
||||
type EstimateData = {
|
||||
id?: number
|
||||
@ -43,10 +44,13 @@ type EstimateData = {
|
||||
remark?: string
|
||||
created_at?: string
|
||||
}[]
|
||||
// Joined fields that may come from the API
|
||||
customer_name?: string
|
||||
vehicle_name?: string
|
||||
department_name?: string
|
||||
// Nested relation objects from the API
|
||||
customer?: { id?: number; first_name?: string; last_name?: string }
|
||||
vehicle?: { id?: number; registration_number?: string; make?: string; model?: string }
|
||||
department?: { id?: number; name?: string }
|
||||
insurance_type?: { id?: number; title?: string }
|
||||
insurer?: { id?: number; first_name?: string; last_name?: string }
|
||||
service_writer?: { id?: number; first_name?: string; last_name?: string }
|
||||
}
|
||||
|
||||
type EstimateGeneralInfoProps = {
|
||||
@ -92,7 +96,7 @@ export function EstimateGeneralInfo({ estimate }: EstimateGeneralInfoProps) {
|
||||
<InfoItem icon={Hash} label="Estimate #" value={estimate.estimate_number} />
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<InfoItem icon={Calendar} label="Date" value={estimate.date} />
|
||||
<InfoItem icon={Calendar} label="Date" value={formatDate(estimate.date)} />
|
||||
<InfoItem
|
||||
icon={Shield}
|
||||
label="Insurance"
|
||||
@ -121,17 +125,39 @@ export function EstimateGeneralInfo({ estimate }: EstimateGeneralInfoProps) {
|
||||
<InfoItem
|
||||
icon={User}
|
||||
label="Customer"
|
||||
value={estimate.customer_name || (estimate.customer_id ? `#${estimate.customer_id}` : null)}
|
||||
value={
|
||||
estimate.customer
|
||||
? `${estimate.customer.first_name ?? ""} ${estimate.customer.last_name ?? ""}`.trim()
|
||||
: estimate.customer_id ? `#${estimate.customer_id}` : null
|
||||
}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={Car}
|
||||
label="Vehicle"
|
||||
value={estimate.vehicle_name || (estimate.vehicle_id ? `#${estimate.vehicle_id}` : null)}
|
||||
value={
|
||||
estimate.vehicle
|
||||
? `${estimate.vehicle.make ?? ""} ${estimate.vehicle.model ?? ""} (${estimate.vehicle.registration_number ?? ""})`.trim()
|
||||
: estimate.vehicle_id ? `#${estimate.vehicle_id}` : null
|
||||
}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={Building2}
|
||||
label="Department"
|
||||
value={estimate.department_name || (estimate.department_id ? `#${estimate.department_id}` : null)}
|
||||
value={estimate.department?.name ?? (estimate.department_id ? `#${estimate.department_id}` : null)}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={Shield}
|
||||
label="Insurance Type"
|
||||
value={estimate.insurance_type?.title ?? null}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={User}
|
||||
label="Service Writer"
|
||||
value={
|
||||
estimate.service_writer
|
||||
? `${estimate.service_writer.first_name ?? ""} ${estimate.service_writer.last_name ?? ""}`.trim()
|
||||
: null
|
||||
}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -36,6 +36,7 @@ type PartLine = {
|
||||
quantity: number
|
||||
rate: number | string
|
||||
description?: string
|
||||
part:any
|
||||
}
|
||||
|
||||
type SelectedPart = {
|
||||
@ -91,7 +92,7 @@ export function EstimatePartsSection({ estimateId }: { estimateId: string }) {
|
||||
}
|
||||
|
||||
const getDisplayName = (item: PartLine) =>
|
||||
item.title ?? `Part #${item.part_id ?? item.id}`
|
||||
item.part.title ?? `Part #${item.part_id ?? item.id}`
|
||||
|
||||
return (
|
||||
<Card>
|
||||
@ -111,7 +112,7 @@ export function EstimatePartsSection({ estimateId }: { estimateId: string }) {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Part</TableHead>
|
||||
<TableHead className="w-100">Part</TableHead>
|
||||
<TableHead className="w-24">Qty</TableHead>
|
||||
<TableHead className="w-28">Rate</TableHead>
|
||||
<TableHead>Description</TableHead>
|
||||
|
||||
@ -37,6 +37,7 @@ type ServiceLine = {
|
||||
quantity: number
|
||||
rate: number | string
|
||||
description?: string
|
||||
service:any
|
||||
}
|
||||
|
||||
type SelectedService = {
|
||||
@ -92,7 +93,7 @@ export function EstimateServicesSection({ estimateId }: { estimateId: string })
|
||||
}
|
||||
|
||||
const getDisplayName = (item: ServiceLine) =>
|
||||
item.labor_name ?? item.title ?? `Service #${item.service_id ?? item.id}`
|
||||
item.service.labor_name ?? item.service.title ?? `Service #${item.service_id ?? item.id}`
|
||||
|
||||
return (
|
||||
<Card>
|
||||
@ -112,7 +113,7 @@ export function EstimateServicesSection({ estimateId }: { estimateId: string })
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Service</TableHead>
|
||||
<TableHead className="w-100">Service</TableHead>
|
||||
<TableHead className="w-24">Qty</TableHead>
|
||||
<TableHead className="w-28">Rate</TableHead>
|
||||
<TableHead>Description</TableHead>
|
||||
|
||||
103
apps/dashboard/modules/invoices/invoice-expenses-section.tsx
Normal file
103
apps/dashboard/modules/invoices/invoice-expenses-section.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
"use client"
|
||||
|
||||
import { Receipt } from "lucide-react"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/shared/components/ui/card"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/components/ui/table"
|
||||
import { formatCurrency, formatNumber } from "@/shared/utils/formatters"
|
||||
|
||||
type InvoiceExpense = {
|
||||
id: number
|
||||
invoice_id: number
|
||||
expense_item_id: number
|
||||
quantity: string | number
|
||||
rate: string | number
|
||||
description?: string
|
||||
chart_of_account?: string
|
||||
department_id?: number
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
type InvoiceExpensesSectionProps = {
|
||||
expenses?: InvoiceExpense[]
|
||||
}
|
||||
|
||||
export function InvoiceExpensesSection({ expenses = [] }: InvoiceExpensesSectionProps) {
|
||||
if (!expenses || expenses.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const subtotal = expenses.reduce((sum, expense) => {
|
||||
const qty = parseFloat(String(expense.quantity))
|
||||
const rate = parseFloat(String(expense.rate))
|
||||
return sum + (qty * rate)
|
||||
}, 0)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Receipt className="size-4" />
|
||||
Expenses
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Description</TableHead>
|
||||
<TableHead className="text-right">Quantity</TableHead>
|
||||
<TableHead className="text-right">Rate</TableHead>
|
||||
<TableHead className="text-right">Amount</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{expenses.map((expense) => {
|
||||
const qty = parseFloat(String(expense.quantity))
|
||||
const rate = parseFloat(String(expense.rate))
|
||||
const amount = qty * rate
|
||||
return (
|
||||
<TableRow key={expense.id}>
|
||||
<TableCell className="max-w-xs truncate">
|
||||
{expense.description || `Expense #${expense.expense_item_id}`}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatNumber(qty)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatCurrency(rate)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-medium">
|
||||
{formatCurrency(amount)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
<TableRow className="bg-muted/50 font-medium">
|
||||
<TableCell colSpan={3} className="text-right">
|
||||
Subtotal
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatCurrency(subtotal)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@ -12,20 +12,36 @@ import {
|
||||
RhfTextareaField,
|
||||
RhfAsyncSelectField,
|
||||
RhfAutoGenerateField,
|
||||
RhfCheckboxField,
|
||||
RhfDateField,
|
||||
} 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 { useFormContext } from "react-hook-form"
|
||||
|
||||
import {
|
||||
invoiceFormSchema,
|
||||
type InvoiceFormValues,
|
||||
} from "./invoice.schema"
|
||||
import { INVOICE_ROUTES, DEPARTMENT_ROUTES, InvoiceStatus } from "@garage/api"
|
||||
import {
|
||||
INVOICE_ROUTES,
|
||||
DEPARTMENT_ROUTES,
|
||||
ESTIMATE_ROUTES,
|
||||
PAYMENT_TERM_ROUTES,
|
||||
INVOICE_SEQUENCE_ROUTES,
|
||||
PAYMENT_MODE_ROUTES,
|
||||
CUSTOMER_ROUTES,
|
||||
InvoiceStatus,
|
||||
InvoiceDiscount,
|
||||
} from "@garage/api"
|
||||
import { RhfCustomerSelectField } from "@/modules/customers/rhf-customer-select-field"
|
||||
import { RhfVehicleSelectField } from "@/modules/vehicles/rhf-vehicle-select-field"
|
||||
import { PartsSelectorField } from "@/modules/parts/parts-selector-field"
|
||||
import { ServicesSelectorField } from "@/modules/services/services-selector-field"
|
||||
import { ExpenseItemsSelectorField } from "@/modules/expense-items/expense-items-selector-field"
|
||||
|
||||
// ── Constants ──
|
||||
|
||||
@ -34,6 +50,11 @@ const STATUS_OPTIONS = InvoiceStatus.map((v) => ({
|
||||
label: v.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
|
||||
}))
|
||||
|
||||
const DISCOUNT_OPTIONS = InvoiceDiscount.map((v) => ({
|
||||
value: v,
|
||||
label: v.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
|
||||
}))
|
||||
|
||||
// ── Props ──
|
||||
|
||||
export type InvoiceFormProps = {
|
||||
@ -49,11 +70,26 @@ const DEFAULT_VALUES: InvoiceFormValues = {
|
||||
customer: null,
|
||||
vehicle: null,
|
||||
department: null,
|
||||
estimate: null,
|
||||
payment_terms: null,
|
||||
invoice_sequence: null,
|
||||
payment_mode: null,
|
||||
insurer: null,
|
||||
invoice_to: null,
|
||||
invoice_number: "",
|
||||
invoice_title: "",
|
||||
invoice_date: "",
|
||||
due_date: "",
|
||||
status: "draft",
|
||||
kms_in: undefined,
|
||||
has_insurance: false,
|
||||
discount: "no",
|
||||
deposit_to: "",
|
||||
notes: "",
|
||||
terms_and_conditions: "",
|
||||
parts: [],
|
||||
services: [],
|
||||
expense_items: [],
|
||||
}
|
||||
|
||||
// ── Mapping helpers ──
|
||||
@ -66,11 +102,44 @@ function mapToFormValues(data: unknown): InvoiceFormValues {
|
||||
customer: toRelation(d.customer_id, d.customer_name),
|
||||
vehicle: toRelation(d.vehicle_id, d.vehicle_name),
|
||||
department: toRelation(d.department_id, d.department_name),
|
||||
estimate: toRelation(d.estimate_id, d.estimate_number ?? d.estimate_title),
|
||||
payment_terms: toRelation(d.payment_terms_id, d.payment_terms_name),
|
||||
invoice_sequence: toRelation(d.invoice_sequence_id, d.invoice_sequence_title ?? d.invoice_sequence?.title),
|
||||
payment_mode: toRelation(d.payment_mode_id, d.payment_mode_name),
|
||||
insurer: toRelation(d.insurer_id, d.insurer_name),
|
||||
invoice_to: toRelation(d.invoice_to_id, d.invoice_to_name),
|
||||
invoice_number: d.invoice_number || "",
|
||||
invoice_date: d.invoice_date || "",
|
||||
due_date: d.due_date || "",
|
||||
invoice_title: d.invoice_title || "",
|
||||
invoice_date: d.invoice_date ? d.invoice_date.split("T")[0] : "",
|
||||
due_date: d.due_date ? d.due_date.split("T")[0] : "",
|
||||
status: d.status || "draft",
|
||||
kms_in: d.kms_in ? Number(d.kms_in) : undefined,
|
||||
has_insurance: d.has_insurance ?? false,
|
||||
discount: d.discount || "no",
|
||||
deposit_to: d.deposit_to || "",
|
||||
notes: d.notes || "",
|
||||
terms_and_conditions: d.terms_and_conditions || "",
|
||||
parts: (d.invoice_parts ?? d.parts ?? []).map((p: any) => ({
|
||||
part_id: p.part_id ?? p.id,
|
||||
title: p.part?.title ?? p.title ?? "",
|
||||
quantity: Number(p.quantity) || 1,
|
||||
rate: Number(p.rate) || 0,
|
||||
description: p.description ?? "",
|
||||
})),
|
||||
services: (d.invoice_services ?? d.services ?? []).map((s: any) => ({
|
||||
service_id: s.service_id ?? s.id,
|
||||
title: s.service?.labor_name ?? s.labor_name ?? s.title ?? "",
|
||||
quantity: Number(s.quantity) || 1,
|
||||
rate: Number(s.rate) || 0,
|
||||
description: s.description ?? "",
|
||||
})),
|
||||
expense_items: (d.invoice_expenses ?? d.expenses ?? []).map((e: any) => ({
|
||||
expense_id: e.expense_id ?? e.id,
|
||||
title: e.expense?.item_name ?? e.item_name ?? e.title ?? "",
|
||||
quantity: Number(e.quantity) || 1,
|
||||
rate: Number(e.rate) || 0,
|
||||
description: e.description ?? "",
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,11 +149,41 @@ function mapFormToPayload(values: InvoiceFormValues) {
|
||||
customer_id: toId(values.customer),
|
||||
vehicle_id: toId(values.vehicle),
|
||||
department_id: toId(values.department),
|
||||
estimate_id: toId(values.estimate),
|
||||
payment_terms_id: toId(values.payment_terms),
|
||||
invoice_sequence_id: toId(values.invoice_sequence),
|
||||
payment_mode_id: toId(values.payment_mode),
|
||||
insurer_id: toId(values.insurer),
|
||||
invoice_to_id: toId(values.invoice_to),
|
||||
invoice_number: values.invoice_number || undefined,
|
||||
invoice_title: values.invoice_title || undefined,
|
||||
invoice_date: values.invoice_date || undefined,
|
||||
due_date: values.due_date || undefined,
|
||||
status: values.status || undefined,
|
||||
kms_in: values.kms_in || undefined,
|
||||
has_insurance: values.has_insurance,
|
||||
discount: values.discount || undefined,
|
||||
deposit_to: values.deposit_to || undefined,
|
||||
notes: values.notes || undefined,
|
||||
terms_and_conditions: values.terms_and_conditions || undefined,
|
||||
parts: (values.parts ?? []).map((item) => ({
|
||||
part_id: item.part_id,
|
||||
quantity: item.quantity,
|
||||
rate: item.rate,
|
||||
description: item.description || undefined,
|
||||
})),
|
||||
services: (values.services ?? []).map((item) => ({
|
||||
service_id: item.service_id,
|
||||
quantity: item.quantity,
|
||||
rate: item.rate,
|
||||
description: item.description || undefined,
|
||||
})),
|
||||
expenses: (values.expense_items ?? []).map((item) => ({
|
||||
expense_id: item.expense_id,
|
||||
quantity: item.quantity,
|
||||
rate: item.rate,
|
||||
description: item.description || undefined,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,11 +191,28 @@ function mapFormToPayload(values: InvoiceFormValues) {
|
||||
|
||||
const mapLookupOption = (item: any) => ({
|
||||
value: String(item.id),
|
||||
label: item.name,
|
||||
label: item.name ?? item.title ?? `#${item.id}`,
|
||||
})
|
||||
|
||||
const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label }
|
||||
|
||||
// ── Insurance fields (conditionally rendered) ──
|
||||
|
||||
function InsurerField() {
|
||||
const { watch } = useFormContext<InvoiceFormValues>()
|
||||
const hasInsurance = watch("has_insurance")
|
||||
|
||||
if (!hasInsurance) return null
|
||||
|
||||
return (
|
||||
<RhfCustomerSelectField<InvoiceFormValues, "insurer">
|
||||
name="insurer"
|
||||
label="Insurer"
|
||||
placeholder="Select insurer..."
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Component ──
|
||||
|
||||
export function InvoiceForm({ resourceId, initialData, onSuccess }: InvoiceFormProps) {
|
||||
@ -146,36 +262,104 @@ export function InvoiceForm({ resourceId, initialData, onSuccess }: InvoiceFormP
|
||||
<RhfTextField name="subject" label="Subject" placeholder="Invoice subject" required />
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<RhfAutoGenerateField table="invoices" name="invoice_number" label="Invoice Number" placeholder="INV-0001" />
|
||||
<RhfSelectField
|
||||
name="status"
|
||||
label="Status"
|
||||
placeholder="Select status"
|
||||
options={STATUS_OPTIONS}
|
||||
<RhfAutoGenerateField autoFetch table="invoices" name="invoice_number" label="Invoice Number" placeholder="INV-0001" />
|
||||
<RhfTextField name="invoice_title" label="Invoice Title" placeholder="e.g. Tax Invoice" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<RhfSelectField name="status" label="Status" options={STATUS_OPTIONS} />
|
||||
<RhfSelectField name="discount" label="Discount" options={DISCOUNT_OPTIONS} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<RhfDateField name="invoice_date" label="Invoice Date" />
|
||||
<RhfDateField name="due_date" label="Due Date" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<RhfCustomerSelectField<InvoiceFormValues, "customer"> name="customer" />
|
||||
<RhfVehicleSelectField name="vehicle" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<RhfCustomerSelectField<InvoiceFormValues, "invoice_to">
|
||||
name="invoice_to"
|
||||
label="Invoice To"
|
||||
placeholder="Select billing contact..."
|
||||
/>
|
||||
<RhfAsyncSelectField
|
||||
name="department"
|
||||
label="Department"
|
||||
placeholder="Select department"
|
||||
queryKey={[DEPARTMENT_ROUTES.INDEX]}
|
||||
listFn={() => api.departments.list()}
|
||||
mapOption={mapLookupOption}
|
||||
{...STORE_OBJECT}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<RhfTextField name="invoice_date" label="Invoice Date" type="date" />
|
||||
<RhfTextField name="due_date" label="Due Date" type="date" />
|
||||
<RhfAsyncSelectField
|
||||
name="estimate"
|
||||
label="Estimate"
|
||||
placeholder="Select estimate"
|
||||
queryKey={[ESTIMATE_ROUTES.INDEX]}
|
||||
listFn={() => api.estimates.list()}
|
||||
mapOption={(item: any) => ({
|
||||
value: String(item.id),
|
||||
label: item.title||item.estimate_number || `#${item.id}`,
|
||||
})}
|
||||
{...STORE_OBJECT}
|
||||
/>
|
||||
<RhfAsyncSelectField
|
||||
name="payment_terms"
|
||||
label="Payment Terms"
|
||||
placeholder="Select payment terms"
|
||||
queryKey={[PAYMENT_TERM_ROUTES.INDEX]}
|
||||
listFn={() => api.paymentTerms.list()}
|
||||
mapOption={mapLookupOption}
|
||||
{...STORE_OBJECT}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<RhfCustomerSelectField name="customer" />
|
||||
<RhfVehicleSelectField name="vehicle" />
|
||||
<RhfAsyncSelectField
|
||||
name="invoice_sequence"
|
||||
label="Invoice Sequence"
|
||||
placeholder="Select sequence"
|
||||
queryKey={[INVOICE_SEQUENCE_ROUTES.INDEX]}
|
||||
listFn={() => api.invoiceSequences.list()}
|
||||
mapOption={(item: any) => ({
|
||||
value: String(item.id),
|
||||
label: item.title || `#${item.id}`,
|
||||
})}
|
||||
{...STORE_OBJECT}
|
||||
/>
|
||||
<RhfTextField name="kms_in" label="KMs In" type="number" placeholder="e.g. 50000" />
|
||||
</div>
|
||||
|
||||
<RhfAsyncSelectField
|
||||
name="department"
|
||||
label="Department"
|
||||
placeholder="Select department"
|
||||
queryKey={[DEPARTMENT_ROUTES.INDEX]}
|
||||
listFn={() => api.departments.list()}
|
||||
mapOption={mapLookupOption}
|
||||
{...STORE_OBJECT}
|
||||
/>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<RhfAsyncSelectField
|
||||
name="payment_mode"
|
||||
label="Payment Mode"
|
||||
placeholder="Select payment mode"
|
||||
queryKey={[PAYMENT_MODE_ROUTES.INDEX]}
|
||||
listFn={() => api.paymentModes.list()}
|
||||
mapOption={mapLookupOption}
|
||||
{...STORE_OBJECT}
|
||||
/>
|
||||
<RhfTextField name="deposit_to" label="Deposit To" placeholder="e.g. Main Account" />
|
||||
</div>
|
||||
|
||||
<RhfCheckboxField name="has_insurance" label="Has Insurance" />
|
||||
<InsurerField />
|
||||
|
||||
<RhfTextareaField name="notes" label="Notes" placeholder="Additional notes" rows={3} />
|
||||
<RhfTextareaField name="terms_and_conditions" label="Terms & Conditions" rows={3} />
|
||||
|
||||
<PartsSelectorField<InvoiceFormValues, "parts"> name="parts" />
|
||||
<ServicesSelectorField<InvoiceFormValues, "services"> name="services" />
|
||||
<ExpenseItemsSelectorField<InvoiceFormValues, "expense_items"> name="expense_items" />
|
||||
|
||||
<Button type="submit" variant="default" disabled={isPending}>
|
||||
{isEditing ? <Save /> : <Plus />}
|
||||
|
||||
@ -7,6 +7,9 @@ import {
|
||||
Building2,
|
||||
CircleDollarSign,
|
||||
Clock,
|
||||
Mail,
|
||||
Phone,
|
||||
DollarSign,
|
||||
} from "lucide-react"
|
||||
import {
|
||||
Card,
|
||||
@ -16,21 +19,38 @@ import {
|
||||
} from "@/shared/components/ui/card"
|
||||
import { Badge } from "@/shared/components/ui/badge"
|
||||
import { Separator } from "@/shared/components/ui/separator"
|
||||
import { formatDate, formatCurrency, formatEnum, formatNumber } from "@/shared/utils/formatters"
|
||||
|
||||
type InvoiceData = {
|
||||
id?: number
|
||||
subject?: string
|
||||
invoice_number?: string
|
||||
invoice_title?: string
|
||||
invoice_date?: string
|
||||
due_date?: string
|
||||
status?: string
|
||||
notes?: string
|
||||
terms_and_conditions?: string
|
||||
customer_name?: string
|
||||
customer_id?: number
|
||||
customer?: any
|
||||
vehicle_name?: string
|
||||
vehicle_id?: number
|
||||
vehicle?: any
|
||||
department_name?: string
|
||||
department_id?: number
|
||||
payment_terms_id?: number
|
||||
payment_mode_id?: number
|
||||
amount?: number | string | null
|
||||
received_payment?: number | string | null
|
||||
discount?: string
|
||||
has_insurance?: number | boolean
|
||||
insurer_id?: number | null
|
||||
insurer?: any
|
||||
kms_in?: number | null
|
||||
invoice_to_id?: number | null
|
||||
billing_address_id?: number | null
|
||||
delivery_address_id?: number | null
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
@ -72,8 +92,12 @@ const statusColorMap: Record<string, string> = {
|
||||
}
|
||||
|
||||
export function InvoiceGeneralInfo({ invoice }: InvoiceGeneralInfoProps) {
|
||||
const customer = invoice.customer || {}
|
||||
const vehicle = invoice.vehicle || {}
|
||||
const insurer = invoice.insurer || {}
|
||||
|
||||
return (
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="grid gap-6">
|
||||
{/* Invoice Details */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@ -89,12 +113,12 @@ export function InvoiceGeneralInfo({ invoice }: InvoiceGeneralInfoProps) {
|
||||
)}
|
||||
{invoice.status && (
|
||||
<Badge variant={statusColorMap[invoice.status] as any ?? "outline"}>
|
||||
{invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)}
|
||||
{formatEnum(invoice.status)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<InfoItem
|
||||
icon={Hash}
|
||||
label="Invoice Number"
|
||||
@ -103,59 +127,209 @@ export function InvoiceGeneralInfo({ invoice }: InvoiceGeneralInfoProps) {
|
||||
<InfoItem
|
||||
icon={Calendar}
|
||||
label="Invoice Date"
|
||||
value={invoice.invoice_date}
|
||||
value={formatDate(invoice.invoice_date)}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={Calendar}
|
||||
label="Due Date"
|
||||
value={invoice.due_date}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={Clock}
|
||||
label="Created"
|
||||
value={invoice.created_at ? new Date(invoice.created_at).toLocaleDateString() : null}
|
||||
value={formatDate(invoice.due_date)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Relations */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<CircleDollarSign className="size-4" />
|
||||
Related Information
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<InfoItem
|
||||
icon={Users}
|
||||
label="Customer"
|
||||
value={invoice.customer_name}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={Car}
|
||||
label="Vehicle"
|
||||
value={invoice.vehicle_name}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={Building2}
|
||||
label="Department"
|
||||
value={invoice.department_name}
|
||||
/>
|
||||
</div>
|
||||
{/* Customer & Vehicle Information */}
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{/* Customer Details */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Users className="size-4" />
|
||||
Customer Information
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<InfoItem
|
||||
icon={Users}
|
||||
label="Customer Name"
|
||||
value={customer.first_name && customer.last_name ? `${customer.first_name} ${customer.last_name}` : invoice.customer_name}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={Mail}
|
||||
label="Email"
|
||||
value={customer.email}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={Phone}
|
||||
label="Phone"
|
||||
value={customer.phone}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={Phone}
|
||||
label="Alternate Phone"
|
||||
value={customer.alternate_phone}
|
||||
/>
|
||||
</div>
|
||||
{customer.address_line_1 && (
|
||||
<>
|
||||
<Separator />
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-xs text-muted-foreground">Address</span>
|
||||
<p className="text-sm">
|
||||
{customer.address_line_1}
|
||||
{customer.address_line_2 ? `, ${customer.address_line_2}` : ""}
|
||||
<br />
|
||||
{customer.city ? `${customer.city}` : ""}
|
||||
{customer.zip_code ? `, ${customer.zip_code}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Vehicle Details */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Car className="size-4" />
|
||||
Vehicle Information
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<InfoItem
|
||||
icon={Car}
|
||||
label="Vehicle"
|
||||
value={vehicle.make && vehicle.model ? `${vehicle.make} ${vehicle.model}` : invoice.vehicle_name}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={Hash}
|
||||
label="License Plate"
|
||||
value={vehicle.license_plate}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={Hash}
|
||||
label="VIN"
|
||||
value={vehicle.vin_number}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={Hash}
|
||||
label="Engine Number"
|
||||
value={vehicle.engine_number}
|
||||
/>
|
||||
</div>
|
||||
{vehicle.mileage && (
|
||||
<>
|
||||
<Separator />
|
||||
<InfoItem
|
||||
icon={Clock}
|
||||
label="Mileage"
|
||||
value={formatNumber(vehicle.mileage)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Payment & Insurance Information */}
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{/* Payment Information */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<DollarSign className="size-4" />
|
||||
Payment Information
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<InfoItem
|
||||
icon={CircleDollarSign}
|
||||
label="Amount"
|
||||
value={invoice.amount ? formatCurrency(invoice.amount) : null}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={CircleDollarSign}
|
||||
label="Received Payment"
|
||||
value={invoice.received_payment ? formatCurrency(invoice.received_payment) : null}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={Hash}
|
||||
label="Discount"
|
||||
value={invoice.discount}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Insurance & Additional Info */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileText className="size-4" />
|
||||
Additional Information
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<InfoItem
|
||||
icon={Building2}
|
||||
label="Department"
|
||||
value={invoice.department_name}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={Hash}
|
||||
label="Has Insurance"
|
||||
value={invoice.has_insurance ? "Yes" : "No"}
|
||||
/>
|
||||
{invoice.has_insurance && insurer.id && (
|
||||
<InfoItem
|
||||
icon={Users}
|
||||
label="Insurer"
|
||||
value={insurer.name}
|
||||
/>
|
||||
)}
|
||||
{invoice.kms_in && (
|
||||
<InfoItem
|
||||
icon={Clock}
|
||||
label="KMs In"
|
||||
value={formatNumber(invoice.kms_in)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Notes & Terms */}
|
||||
{(invoice.notes || invoice.terms_and_conditions) && (
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{invoice.notes && (
|
||||
<>
|
||||
<Separator />
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-xs text-muted-foreground">Notes</span>
|
||||
<p className="text-sm">{invoice.notes}</p>
|
||||
</div>
|
||||
</>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Notes</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm whitespace-pre-wrap">{invoice.notes}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{invoice.terms_and_conditions && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Terms & Conditions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm whitespace-pre-wrap">{invoice.terms_and_conditions}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
103
apps/dashboard/modules/invoices/invoice-parts-section.tsx
Normal file
103
apps/dashboard/modules/invoices/invoice-parts-section.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
"use client"
|
||||
|
||||
import { Wrench } from "lucide-react"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/shared/components/ui/card"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/components/ui/table"
|
||||
import { formatCurrency, formatNumber } from "@/shared/utils/formatters"
|
||||
|
||||
type InvoicePart = {
|
||||
id: number
|
||||
invoice_id: number
|
||||
part_id: number
|
||||
quantity: string | number
|
||||
rate: string | number
|
||||
description?: string
|
||||
chart_of_account?: string
|
||||
department_id?: number
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
type InvoicePartsSectionProps = {
|
||||
parts?: InvoicePart[]
|
||||
}
|
||||
|
||||
export function InvoicePartsSection({ parts = [] }: InvoicePartsSectionProps) {
|
||||
if (!parts || parts.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const subtotal = parts.reduce((sum, part) => {
|
||||
const qty = parseFloat(String(part.quantity))
|
||||
const rate = parseFloat(String(part.rate))
|
||||
return sum + (qty * rate)
|
||||
}, 0)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Wrench className="size-4" />
|
||||
Parts
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Description</TableHead>
|
||||
<TableHead className="text-right">Quantity</TableHead>
|
||||
<TableHead className="text-right">Rate</TableHead>
|
||||
<TableHead className="text-right">Amount</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{parts.map((part) => {
|
||||
const qty = parseFloat(String(part.quantity))
|
||||
const rate = parseFloat(String(part.rate))
|
||||
const amount = qty * rate
|
||||
return (
|
||||
<TableRow key={part.id}>
|
||||
<TableCell className="max-w-xs truncate">
|
||||
{part.description || `Part #${part.part_id}`}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatNumber(qty)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatCurrency(rate)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-medium">
|
||||
{formatCurrency(amount)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
<TableRow className="bg-muted/50 font-medium">
|
||||
<TableCell colSpan={3} className="text-right">
|
||||
Subtotal
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatCurrency(subtotal)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
103
apps/dashboard/modules/invoices/invoice-services-section.tsx
Normal file
103
apps/dashboard/modules/invoices/invoice-services-section.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
"use client"
|
||||
|
||||
import { Briefcase } from "lucide-react"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/shared/components/ui/card"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/components/ui/table"
|
||||
import { formatCurrency, formatNumber } from "@/shared/utils/formatters"
|
||||
|
||||
type InvoiceService = {
|
||||
id: number
|
||||
invoice_id: number
|
||||
service_id: number
|
||||
quantity: string | number
|
||||
rate: string | number
|
||||
description?: string
|
||||
chart_of_account?: string
|
||||
department_id?: number
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
type InvoiceServicesSectionProps = {
|
||||
services?: InvoiceService[]
|
||||
}
|
||||
|
||||
export function InvoiceServicesSection({ services = [] }: InvoiceServicesSectionProps) {
|
||||
if (!services || services.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const subtotal = services.reduce((sum, service) => {
|
||||
const qty = parseFloat(String(service.quantity))
|
||||
const rate = parseFloat(String(service.rate))
|
||||
return sum + (qty * rate)
|
||||
}, 0)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Briefcase className="size-4" />
|
||||
Services
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Description</TableHead>
|
||||
<TableHead className="text-right">Quantity</TableHead>
|
||||
<TableHead className="text-right">Rate</TableHead>
|
||||
<TableHead className="text-right">Amount</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{services.map((service) => {
|
||||
const qty = parseFloat(String(service.quantity))
|
||||
const rate = parseFloat(String(service.rate))
|
||||
const amount = qty * rate
|
||||
return (
|
||||
<TableRow key={service.id}>
|
||||
<TableCell className="max-w-xs truncate">
|
||||
{service.description || `Service #${service.service_id}`}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatNumber(qty)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatCurrency(rate)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-medium">
|
||||
{formatCurrency(amount)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
<TableRow className="bg-muted/50 font-medium">
|
||||
<TableCell colSpan={3} className="text-right">
|
||||
Subtotal
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatCurrency(subtotal)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@ -4,6 +4,30 @@ const relationFieldSchema = z
|
||||
.object({ value: z.string(), label: z.string() })
|
||||
.nullable()
|
||||
|
||||
const invoicePartItemSchema = z.object({
|
||||
part_id: z.number(),
|
||||
title: z.string(),
|
||||
quantity: z.number().min(1),
|
||||
rate: z.number().min(0),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
|
||||
const invoiceServiceItemSchema = z.object({
|
||||
service_id: z.number(),
|
||||
title: z.string(),
|
||||
quantity: z.number().min(1),
|
||||
rate: z.number().min(0),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
|
||||
const invoiceExpenseItemSchema = z.object({
|
||||
expense_id: z.number(),
|
||||
title: z.string(),
|
||||
quantity: z.number().min(1),
|
||||
rate: z.number().min(0),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
|
||||
const invoiceFormSchema = z.object({
|
||||
// ── Required fields ──
|
||||
subject: z.string().min(1, "Subject is required"),
|
||||
@ -12,16 +36,36 @@ const invoiceFormSchema = z.object({
|
||||
customer: relationFieldSchema,
|
||||
vehicle: relationFieldSchema,
|
||||
department: relationFieldSchema,
|
||||
estimate: relationFieldSchema,
|
||||
payment_terms: relationFieldSchema,
|
||||
invoice_sequence: relationFieldSchema,
|
||||
payment_mode: relationFieldSchema,
|
||||
insurer: relationFieldSchema,
|
||||
invoice_to: relationFieldSchema,
|
||||
|
||||
// ── Optional fields ──
|
||||
invoice_number: z.string().optional(),
|
||||
invoice_title: z.string().optional(),
|
||||
invoice_date: z.string().optional(),
|
||||
due_date: z.string().optional(),
|
||||
status: z.string().optional(),
|
||||
kms_in: z.coerce.number().optional(),
|
||||
has_insurance: z.boolean().default(false),
|
||||
discount: z.string().optional(),
|
||||
deposit_to: z.string().optional(),
|
||||
notes: z.string().optional(),
|
||||
terms_and_conditions: z.string().optional(),
|
||||
|
||||
// ── Line items ──
|
||||
parts: z.array(invoicePartItemSchema).optional(),
|
||||
services: z.array(invoiceServiceItemSchema).optional(),
|
||||
expense_items: z.array(invoiceExpenseItemSchema).optional(),
|
||||
})
|
||||
|
||||
type InvoiceFormValues = z.infer<typeof invoiceFormSchema>
|
||||
type InvoicePartItem = z.infer<typeof invoicePartItemSchema>
|
||||
type InvoiceServiceItem = z.infer<typeof invoiceServiceItemSchema>
|
||||
type InvoiceExpenseItem = z.infer<typeof invoiceExpenseItemSchema>
|
||||
|
||||
export { invoiceFormSchema, relationFieldSchema }
|
||||
export type { InvoiceFormValues }
|
||||
export { invoiceFormSchema, relationFieldSchema, invoicePartItemSchema, invoiceServiceItemSchema, invoiceExpenseItemSchema }
|
||||
export type { InvoiceFormValues, InvoicePartItem, InvoiceServiceItem, InvoiceExpenseItem }
|
||||
|
||||
@ -462,8 +462,8 @@
|
||||
],
|
||||
"summary": "GET /api/profile",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@ -21668,6 +21668,290 @@
|
||||
}
|
||||
},
|
||||
"/api/estimates/{id}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Estimates"
|
||||
],
|
||||
"summary": "Display the specified estimate.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"customer_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"vehicle_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"department_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"estimate_number": {
|
||||
"type": "string"
|
||||
},
|
||||
"date": {
|
||||
"type": "string"
|
||||
},
|
||||
"has_insurance": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enable_digital_authorisation": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"insurance_type_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"insurer_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"service_writer_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"footer": {
|
||||
"type": "string"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"labels": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"color_code": {
|
||||
"type": "string"
|
||||
},
|
||||
"pivot": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"estimate_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"label_id": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"customer_remarks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"estimate_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"remark": {
|
||||
"type": "string"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"customer": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"last_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"vehicle": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"registration_number": {
|
||||
"type": "string"
|
||||
},
|
||||
"make": {
|
||||
"type": "string"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"department": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"insurance_type": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"insurer": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"last_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"service_writer": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"last_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"data": {
|
||||
"id": 1,
|
||||
"title": "Estimate for Toyota Camry",
|
||||
"customer_id": 1,
|
||||
"vehicle_id": 1,
|
||||
"department_id": 1,
|
||||
"estimate_number": "EST-001",
|
||||
"date": "2026-03-31",
|
||||
"has_insurance": false,
|
||||
"enable_digital_authorisation": false,
|
||||
"insurance_type_id": 1,
|
||||
"insurer_id": 2,
|
||||
"service_writer_id": 1,
|
||||
"footer": "Thank you for your business.",
|
||||
"created_at": "2026-03-31T10:00:00.000000Z",
|
||||
"updated_at": "2026-03-31T10:00:00.000000Z",
|
||||
"labels": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Urgent",
|
||||
"color_code": "#FF0000",
|
||||
"pivot": {
|
||||
"estimate_id": 1,
|
||||
"label_id": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"customer_remarks": [
|
||||
{
|
||||
"id": 1,
|
||||
"estimate_id": 1,
|
||||
"remark": "Oil change recommended.",
|
||||
"created_at": "2026-03-31T10:00:00.000000Z",
|
||||
"updated_at": "2026-03-31T10:00:00.000000Z"
|
||||
}
|
||||
],
|
||||
"customer": {
|
||||
"id": 1,
|
||||
"first_name": "John",
|
||||
"last_name": "Doe"
|
||||
},
|
||||
"vehicle": {
|
||||
"id": 1,
|
||||
"registration_number": "ABC-1234",
|
||||
"make": "Toyota",
|
||||
"model": "Camry"
|
||||
},
|
||||
"department": {
|
||||
"id": 1,
|
||||
"name": "Service"
|
||||
},
|
||||
"insurance_type": {
|
||||
"id": 1,
|
||||
"title": "Comprehensive"
|
||||
},
|
||||
"insurer": {
|
||||
"id": 2,
|
||||
"first_name": "Alex",
|
||||
"last_name": "Insurer"
|
||||
},
|
||||
"service_writer": {
|
||||
"id": 1,
|
||||
"first_name": "Sam",
|
||||
"last_name": "Writer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"tags": [
|
||||
"Estimates"
|
||||
@ -42973,6 +43257,24 @@
|
||||
"estimate_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kms_in": {
|
||||
"type": "integer"
|
||||
},
|
||||
"has_insurance": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"insurer_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"invoice_to_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"billing_address_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"delivery_address_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"invoice_date": {
|
||||
"type": "string"
|
||||
},
|
||||
@ -42988,17 +43290,195 @@
|
||||
"invoice_number": {
|
||||
"type": "string"
|
||||
},
|
||||
"invoice_title": {
|
||||
"type": "string"
|
||||
},
|
||||
"department_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"notes": {
|
||||
"type": "string"
|
||||
},
|
||||
"terms_and_conditions": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"received_payment": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"payment_mode_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"deposit_to": {
|
||||
"type": "string"
|
||||
},
|
||||
"amount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"discount": {
|
||||
"type": "string"
|
||||
},
|
||||
"inspection_categories": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"inspection_category_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"rate_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"labor_rate": {
|
||||
"type": "integer"
|
||||
},
|
||||
"working_hours": {
|
||||
"type": "integer"
|
||||
},
|
||||
"labor_hours": {
|
||||
"type": "integer"
|
||||
},
|
||||
"rate": {
|
||||
"type": "integer"
|
||||
},
|
||||
"chart_of_account": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"department_id": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"parts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"part_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"quantity": {
|
||||
"type": "integer"
|
||||
},
|
||||
"rate": {
|
||||
"type": "integer"
|
||||
},
|
||||
"chart_of_account": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"department_id": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"expenses": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"expense_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"quantity": {
|
||||
"type": "integer"
|
||||
},
|
||||
"rate": {
|
||||
"type": "integer"
|
||||
},
|
||||
"chart_of_account": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"department_id": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"service_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"rate_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"labor_rate_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"working_hours": {
|
||||
"type": "integer"
|
||||
},
|
||||
"labor_hours": {
|
||||
"type": "integer"
|
||||
},
|
||||
"quantity": {
|
||||
"type": "integer"
|
||||
},
|
||||
"rate": {
|
||||
"type": "integer"
|
||||
},
|
||||
"chart_of_account": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"department_id": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"service_groups": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"service_group_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"rate_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"labor_rate_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"working_hours": {
|
||||
"type": "integer"
|
||||
},
|
||||
"labor_hours": {
|
||||
"type": "integer"
|
||||
},
|
||||
"rate": {
|
||||
"type": "integer"
|
||||
},
|
||||
"chart_of_account": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"department_id": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -43007,22 +43487,94 @@
|
||||
"customer_id": 1,
|
||||
"vehicle_id": 1,
|
||||
"estimate_id": 1,
|
||||
"kms_in": 50000,
|
||||
"has_insurance": false,
|
||||
"insurer_id": 2,
|
||||
"invoice_to_id": 1,
|
||||
"billing_address_id": 10,
|
||||
"delivery_address_id": 11,
|
||||
"invoice_date": "2026-03-31",
|
||||
"due_date": "2026-04-14",
|
||||
"payment_terms_id": 1,
|
||||
"invoice_sequence_id": 1,
|
||||
"invoice_number": "INV-001",
|
||||
"invoice_title": "Tax Invoice",
|
||||
"department_id": 1,
|
||||
"notes": "string",
|
||||
"notes": "Vehicle service and parts",
|
||||
"terms_and_conditions": "Payment due in 14 days.",
|
||||
"status": "draft",
|
||||
"discount": "no"
|
||||
"received_payment": false,
|
||||
"payment_mode_id": 1,
|
||||
"deposit_to": "Main Account",
|
||||
"amount": 2500,
|
||||
"discount": "no",
|
||||
"inspection_categories": [
|
||||
{
|
||||
"inspection_category_id": 1,
|
||||
"rate_type": "flat_rate",
|
||||
"labor_rate": 500,
|
||||
"working_hours": 1,
|
||||
"labor_hours": 1,
|
||||
"rate": 500,
|
||||
"chart_of_account": "4000",
|
||||
"description": "General inspection",
|
||||
"department_id": 1
|
||||
}
|
||||
],
|
||||
"parts": [
|
||||
{
|
||||
"part_id": 1,
|
||||
"quantity": 2,
|
||||
"rate": 150,
|
||||
"chart_of_account": "4100",
|
||||
"description": "Oil filter",
|
||||
"department_id": 1
|
||||
}
|
||||
],
|
||||
"expenses": [
|
||||
{
|
||||
"expense_id": 1,
|
||||
"quantity": 1,
|
||||
"rate": 100,
|
||||
"chart_of_account": "4200",
|
||||
"description": "Shop supplies",
|
||||
"department_id": 1
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"service_id": 1,
|
||||
"rate_type": "hourly",
|
||||
"labor_rate_id": 1,
|
||||
"working_hours": 2,
|
||||
"labor_hours": 2,
|
||||
"quantity": 1,
|
||||
"rate": 800,
|
||||
"chart_of_account": "4300",
|
||||
"description": "Engine service",
|
||||
"department_id": 1
|
||||
}
|
||||
],
|
||||
"service_groups": [
|
||||
{
|
||||
"service_group_id": 1,
|
||||
"rate_type": "flat_rate",
|
||||
"labor_rate_id": 1,
|
||||
"working_hours": 1,
|
||||
"labor_hours": 1,
|
||||
"rate": 600,
|
||||
"chart_of_account": "4400",
|
||||
"description": "Major service package",
|
||||
"department_id": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@ -43098,7 +43650,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"received_payment": {
|
||||
"type": "integer"
|
||||
"type": "boolean"
|
||||
},
|
||||
"payment_mode_id": {
|
||||
"type": "integer"
|
||||
@ -43119,25 +43671,253 @@
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"customer": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"vehicle": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"invoice_sequence": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"department": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"invoice_inspection_categories": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"invoice_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"inspection_category_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"rate_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"labor_rate": {
|
||||
"type": "integer"
|
||||
},
|
||||
"working_hours": {
|
||||
"type": "integer"
|
||||
},
|
||||
"labor_hours": {
|
||||
"type": "integer"
|
||||
},
|
||||
"rate": {
|
||||
"type": "integer"
|
||||
},
|
||||
"chart_of_account": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"department_id": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"invoice_parts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"invoice_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"part_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"quantity": {
|
||||
"type": "integer"
|
||||
},
|
||||
"rate": {
|
||||
"type": "integer"
|
||||
},
|
||||
"chart_of_account": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"department_id": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"invoice_expenses": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"invoice_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"expense_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"quantity": {
|
||||
"type": "integer"
|
||||
},
|
||||
"rate": {
|
||||
"type": "integer"
|
||||
},
|
||||
"chart_of_account": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"department_id": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"invoice_services": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"invoice_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"service_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"rate_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"labor_rate_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"working_hours": {
|
||||
"type": "integer"
|
||||
},
|
||||
"labor_hours": {
|
||||
"type": "integer"
|
||||
},
|
||||
"quantity": {
|
||||
"type": "integer"
|
||||
},
|
||||
"rate": {
|
||||
"type": "integer"
|
||||
},
|
||||
"chart_of_account": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"department_id": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"invoice_service_groups": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"invoice_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"service_group_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"rate_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"labor_rate_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"working_hours": {
|
||||
"type": "integer"
|
||||
},
|
||||
"labor_hours": {
|
||||
"type": "integer"
|
||||
},
|
||||
"rate": {
|
||||
"type": "integer"
|
||||
},
|
||||
"chart_of_account": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"department_id": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"message": "Operation completed successfully.",
|
||||
"message": "Invoice created successfully.",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"subject": "Invoice for Job Card 001",
|
||||
"subject": "Invoice for Service",
|
||||
"customer_id": 1,
|
||||
"vehicle_id": 1,
|
||||
"estimate_id": 1,
|
||||
"kms_in": 50000,
|
||||
"has_insurance": false,
|
||||
"insurer_id": 1,
|
||||
"insurer_id": 2,
|
||||
"invoice_to_id": 1,
|
||||
"billing_address_id": 1,
|
||||
"delivery_address_id": 1,
|
||||
"billing_address_id": 10,
|
||||
"delivery_address_id": 11,
|
||||
"invoice_date": "2026-03-31",
|
||||
"due_date": "2026-04-14",
|
||||
"payment_terms_id": 1,
|
||||
@ -43145,16 +43925,100 @@
|
||||
"invoice_number": "INV-001",
|
||||
"invoice_title": "Tax Invoice",
|
||||
"department_id": 1,
|
||||
"notes": "string",
|
||||
"terms_and_conditions": "string",
|
||||
"notes": "Vehicle service and parts",
|
||||
"terms_and_conditions": "Payment due in 14 days.",
|
||||
"status": "draft",
|
||||
"received_payment": 0,
|
||||
"received_payment": false,
|
||||
"payment_mode_id": 1,
|
||||
"deposit_to": "string",
|
||||
"amount": 0,
|
||||
"deposit_to": "Main Account",
|
||||
"amount": 2500,
|
||||
"discount": "no",
|
||||
"created_at": "2026-03-31T10:00:00.000000Z",
|
||||
"updated_at": "2026-03-31T10:00:00.000000Z"
|
||||
"updated_at": "2026-03-31T10:00:00.000000Z",
|
||||
"customer": {
|
||||
"id": 1
|
||||
},
|
||||
"vehicle": {
|
||||
"id": 1
|
||||
},
|
||||
"invoice_sequence": {
|
||||
"id": 1,
|
||||
"title": "INV"
|
||||
},
|
||||
"department": {
|
||||
"id": 1,
|
||||
"name": "Service"
|
||||
},
|
||||
"invoice_inspection_categories": [
|
||||
{
|
||||
"id": 1,
|
||||
"invoice_id": 1,
|
||||
"inspection_category_id": 1,
|
||||
"rate_type": "flat_rate",
|
||||
"labor_rate": 500,
|
||||
"working_hours": 1,
|
||||
"labor_hours": 1,
|
||||
"rate": 500,
|
||||
"chart_of_account": "4000",
|
||||
"description": "General inspection",
|
||||
"department_id": 1
|
||||
}
|
||||
],
|
||||
"invoice_parts": [
|
||||
{
|
||||
"id": 1,
|
||||
"invoice_id": 1,
|
||||
"part_id": 1,
|
||||
"quantity": 2,
|
||||
"rate": 150,
|
||||
"chart_of_account": "4100",
|
||||
"description": "Oil filter",
|
||||
"department_id": 1
|
||||
}
|
||||
],
|
||||
"invoice_expenses": [
|
||||
{
|
||||
"id": 1,
|
||||
"invoice_id": 1,
|
||||
"expense_id": 1,
|
||||
"quantity": 1,
|
||||
"rate": 100,
|
||||
"chart_of_account": "4200",
|
||||
"description": "Shop supplies",
|
||||
"department_id": 1
|
||||
}
|
||||
],
|
||||
"invoice_services": [
|
||||
{
|
||||
"id": 1,
|
||||
"invoice_id": 1,
|
||||
"service_id": 1,
|
||||
"rate_type": "hourly",
|
||||
"labor_rate_id": 1,
|
||||
"working_hours": 2,
|
||||
"labor_hours": 2,
|
||||
"quantity": 1,
|
||||
"rate": 800,
|
||||
"chart_of_account": "4300",
|
||||
"description": "Engine service",
|
||||
"department_id": 1
|
||||
}
|
||||
],
|
||||
"invoice_service_groups": [
|
||||
{
|
||||
"id": 1,
|
||||
"invoice_id": 1,
|
||||
"service_group_id": 1,
|
||||
"rate_type": "flat_rate",
|
||||
"labor_rate_id": 1,
|
||||
"working_hours": 1,
|
||||
"labor_hours": 1,
|
||||
"rate": 600,
|
||||
"chart_of_account": "4400",
|
||||
"description": "Major service package",
|
||||
"department_id": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,7 +23,8 @@
|
||||
"check-types": "echo \"No typecheck configured for @garage/api\""
|
||||
},
|
||||
"dependencies": {
|
||||
"openapi-fetch": "^0.14.0"
|
||||
"openapi-fetch": "^0.14.0",
|
||||
"pino": "^10.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"openapi-typescript": "^7.10.1"
|
||||
@ -33,7 +34,11 @@
|
||||
"server-only": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"next": { "optional": true },
|
||||
"server-only": { "optional": true }
|
||||
"next": {
|
||||
"optional": true
|
||||
},
|
||||
"server-only": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"info": {
|
||||
"_postman_id": "351b2d30-cb82-4904-9a47-0d356181a8ea",
|
||||
"_postman_id": "38ac2be3-7ab6-4dcc-ad76-6112212395af",
|
||||
"name": "Reparee Collection",
|
||||
"description": "Auto-generated from OpenAPI spec. Import storage/app/openapi-default.json for the full schema.",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||
@ -135,7 +135,7 @@
|
||||
},
|
||||
"response": [
|
||||
{
|
||||
"name": "200 OK",
|
||||
"name": "201 Created",
|
||||
"originalRequest": {
|
||||
"auth": {
|
||||
"type": "bearer",
|
||||
@ -169,8 +169,8 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"status": "Created",
|
||||
"code": 201,
|
||||
"_postman_previewlanguage": "json",
|
||||
"header": [
|
||||
{
|
||||
@ -14953,6 +14953,93 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Display the specified estimate.",
|
||||
"request": {
|
||||
"auth": {
|
||||
"type": "bearer",
|
||||
"bearer": [
|
||||
{
|
||||
"key": "token",
|
||||
"value": "{{auth_token}}",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Accept",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/estimates/{{id}}",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"estimates",
|
||||
"{{id}}"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": [
|
||||
{
|
||||
"name": "200 OK",
|
||||
"originalRequest": {
|
||||
"auth": {
|
||||
"type": "bearer",
|
||||
"bearer": [
|
||||
{
|
||||
"key": "token",
|
||||
"value": "{{auth_token}}",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Accept",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/estimates/{{id}}",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"estimates",
|
||||
"{{id}}"
|
||||
]
|
||||
}
|
||||
},
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"_postman_previewlanguage": "json",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"cookie": [],
|
||||
"body": "{\n \"data\": {\n \"id\": 1,\n \"title\": \"Estimate for Toyota Camry\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"department_id\": 1,\n \"estimate_number\": \"EST-001\",\n \"date\": \"2026-03-31\",\n \"has_insurance\": false,\n \"enable_digital_authorisation\": false,\n \"insurance_type_id\": 1,\n \"insurer_id\": 2,\n \"service_writer_id\": 1,\n \"footer\": \"Thank you for your business.\",\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\",\n \"labels\": [\n {\n \"id\": 1,\n \"title\": \"Urgent\",\n \"color_code\": \"#FF0000\",\n \"pivot\": {\n \"estimate_id\": 1,\n \"label_id\": 1\n }\n }\n ],\n \"customer_remarks\": [\n {\n \"id\": 1,\n \"estimate_id\": 1,\n \"remark\": \"Oil change recommended.\",\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\"\n }\n ],\n \"customer\": {\n \"id\": 1,\n \"first_name\": \"John\",\n \"last_name\": \"Doe\"\n },\n \"vehicle\": {\n \"id\": 1,\n \"registration_number\": \"ABC-1234\",\n \"make\": \"Toyota\",\n \"model\": \"Camry\"\n },\n \"department\": {\n \"id\": 1,\n \"name\": \"Service\"\n },\n \"insurance_type\": {\n \"id\": 1,\n \"title\": \"Comprehensive\"\n },\n \"insurer\": {\n \"id\": 2,\n \"first_name\": \"Alex\",\n \"last_name\": \"Insurer\"\n },\n \"service_writer\": {\n \"id\": 1,\n \"first_name\": \"Sam\",\n \"last_name\": \"Writer\"\n }\n }\n}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Store a newly created estimate.",
|
||||
"request": {
|
||||
@ -33569,7 +33656,7 @@
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"subject\": \"Invoice for Service\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"estimate_id\": 1,\n \"invoice_date\": \"2026-03-31\",\n \"due_date\": \"2026-04-14\",\n \"payment_terms_id\": 1,\n \"invoice_sequence_id\": 1,\n \"invoice_number\": \"INV-001\",\n \"department_id\": 1,\n \"notes\": \"string\",\n \"status\": \"draft\",\n \"discount\": \"no\"\n}",
|
||||
"raw": "{\n \"subject\": \"Invoice for Service\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"estimate_id\": 1,\n \"kms_in\": 50000,\n \"has_insurance\": false,\n \"insurer_id\": 2,\n \"invoice_to_id\": 1,\n \"billing_address_id\": 10,\n \"delivery_address_id\": 11,\n \"invoice_date\": \"2026-03-31\",\n \"due_date\": \"2026-04-14\",\n \"payment_terms_id\": 1,\n \"invoice_sequence_id\": 1,\n \"invoice_number\": \"INV-001\",\n \"invoice_title\": \"Tax Invoice\",\n \"department_id\": 1,\n \"notes\": \"Vehicle service and parts\",\n \"terms_and_conditions\": \"Payment due in 14 days.\",\n \"status\": \"draft\",\n \"received_payment\": false,\n \"payment_mode_id\": 1,\n \"deposit_to\": \"Main Account\",\n \"amount\": 2500,\n \"discount\": \"no\",\n \"inspection_categories\": [\n {\n \"inspection_category_id\": 1,\n \"rate_type\": \"flat_rate\",\n \"labor_rate\": 500,\n \"working_hours\": 1,\n \"labor_hours\": 1,\n \"rate\": 500,\n \"chart_of_account\": \"4000\",\n \"description\": \"General inspection\",\n \"department_id\": 1\n }\n ],\n \"parts\": [\n {\n \"part_id\": 1,\n \"quantity\": 2,\n \"rate\": 150,\n \"chart_of_account\": \"4100\",\n \"description\": \"Oil filter\",\n \"department_id\": 1\n }\n ],\n \"expenses\": [\n {\n \"expense_id\": 1,\n \"quantity\": 1,\n \"rate\": 100,\n \"chart_of_account\": \"4200\",\n \"description\": \"Shop supplies\",\n \"department_id\": 1\n }\n ],\n \"services\": [\n {\n \"service_id\": 1,\n \"rate_type\": \"hourly\",\n \"labor_rate_id\": 1,\n \"working_hours\": 2,\n \"labor_hours\": 2,\n \"quantity\": 1,\n \"rate\": 800,\n \"chart_of_account\": \"4300\",\n \"description\": \"Engine service\",\n \"department_id\": 1\n }\n ],\n \"service_groups\": [\n {\n \"service_group_id\": 1,\n \"rate_type\": \"flat_rate\",\n \"labor_rate_id\": 1,\n \"working_hours\": 1,\n \"labor_hours\": 1,\n \"rate\": 600,\n \"chart_of_account\": \"4400\",\n \"description\": \"Major service package\",\n \"department_id\": 1\n }\n ]\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
@ -33589,7 +33676,7 @@
|
||||
},
|
||||
"response": [
|
||||
{
|
||||
"name": "200 OK",
|
||||
"name": "201 Created",
|
||||
"originalRequest": {
|
||||
"auth": {
|
||||
"type": "bearer",
|
||||
@ -33614,7 +33701,7 @@
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"subject\": \"Invoice for Service\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"estimate_id\": 1,\n \"invoice_date\": \"2026-03-31\",\n \"due_date\": \"2026-04-14\",\n \"payment_terms_id\": 1,\n \"invoice_sequence_id\": 1,\n \"invoice_number\": \"INV-001\",\n \"department_id\": 1,\n \"notes\": \"string\",\n \"status\": \"draft\",\n \"discount\": \"no\"\n}",
|
||||
"raw": "{\n \"subject\": \"Invoice for Service\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"estimate_id\": 1,\n \"kms_in\": 50000,\n \"has_insurance\": false,\n \"insurer_id\": 2,\n \"invoice_to_id\": 1,\n \"billing_address_id\": 10,\n \"delivery_address_id\": 11,\n \"invoice_date\": \"2026-03-31\",\n \"due_date\": \"2026-04-14\",\n \"payment_terms_id\": 1,\n \"invoice_sequence_id\": 1,\n \"invoice_number\": \"INV-001\",\n \"invoice_title\": \"Tax Invoice\",\n \"department_id\": 1,\n \"notes\": \"Vehicle service and parts\",\n \"terms_and_conditions\": \"Payment due in 14 days.\",\n \"status\": \"draft\",\n \"received_payment\": false,\n \"payment_mode_id\": 1,\n \"deposit_to\": \"Main Account\",\n \"amount\": 2500,\n \"discount\": \"no\",\n \"inspection_categories\": [\n {\n \"inspection_category_id\": 1,\n \"rate_type\": \"flat_rate\",\n \"labor_rate\": 500,\n \"working_hours\": 1,\n \"labor_hours\": 1,\n \"rate\": 500,\n \"chart_of_account\": \"4000\",\n \"description\": \"General inspection\",\n \"department_id\": 1\n }\n ],\n \"parts\": [\n {\n \"part_id\": 1,\n \"quantity\": 2,\n \"rate\": 150,\n \"chart_of_account\": \"4100\",\n \"description\": \"Oil filter\",\n \"department_id\": 1\n }\n ],\n \"expenses\": [\n {\n \"expense_id\": 1,\n \"quantity\": 1,\n \"rate\": 100,\n \"chart_of_account\": \"4200\",\n \"description\": \"Shop supplies\",\n \"department_id\": 1\n }\n ],\n \"services\": [\n {\n \"service_id\": 1,\n \"rate_type\": \"hourly\",\n \"labor_rate_id\": 1,\n \"working_hours\": 2,\n \"labor_hours\": 2,\n \"quantity\": 1,\n \"rate\": 800,\n \"chart_of_account\": \"4300\",\n \"description\": \"Engine service\",\n \"department_id\": 1\n }\n ],\n \"service_groups\": [\n {\n \"service_group_id\": 1,\n \"rate_type\": \"flat_rate\",\n \"labor_rate_id\": 1,\n \"working_hours\": 1,\n \"labor_hours\": 1,\n \"rate\": 600,\n \"chart_of_account\": \"4400\",\n \"description\": \"Major service package\",\n \"department_id\": 1\n }\n ]\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
@ -33632,8 +33719,8 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"status": "Created",
|
||||
"code": 201,
|
||||
"_postman_previewlanguage": "json",
|
||||
"header": [
|
||||
{
|
||||
@ -33642,7 +33729,7 @@
|
||||
}
|
||||
],
|
||||
"cookie": [],
|
||||
"body": "{\n \"message\": \"Operation completed successfully.\",\n \"data\": {\n \"id\": 1,\n \"subject\": \"Invoice for Job Card 001\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"estimate_id\": 1,\n \"kms_in\": 50000,\n \"has_insurance\": false,\n \"insurer_id\": 1,\n \"invoice_to_id\": 1,\n \"billing_address_id\": 1,\n \"delivery_address_id\": 1,\n \"invoice_date\": \"2026-03-31\",\n \"due_date\": \"2026-04-14\",\n \"payment_terms_id\": 1,\n \"invoice_sequence_id\": 1,\n \"invoice_number\": \"INV-001\",\n \"invoice_title\": \"Tax Invoice\",\n \"department_id\": 1,\n \"notes\": \"string\",\n \"terms_and_conditions\": \"string\",\n \"status\": \"draft\",\n \"received_payment\": 0,\n \"payment_mode_id\": 1,\n \"deposit_to\": \"string\",\n \"amount\": 0,\n \"discount\": \"no\",\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\"\n }\n}"
|
||||
"body": "{\n \"message\": \"Invoice created successfully.\",\n \"data\": {\n \"id\": 1,\n \"subject\": \"Invoice for Service\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"estimate_id\": 1,\n \"kms_in\": 50000,\n \"has_insurance\": false,\n \"insurer_id\": 2,\n \"invoice_to_id\": 1,\n \"billing_address_id\": 10,\n \"delivery_address_id\": 11,\n \"invoice_date\": \"2026-03-31\",\n \"due_date\": \"2026-04-14\",\n \"payment_terms_id\": 1,\n \"invoice_sequence_id\": 1,\n \"invoice_number\": \"INV-001\",\n \"invoice_title\": \"Tax Invoice\",\n \"department_id\": 1,\n \"notes\": \"Vehicle service and parts\",\n \"terms_and_conditions\": \"Payment due in 14 days.\",\n \"status\": \"draft\",\n \"received_payment\": false,\n \"payment_mode_id\": 1,\n \"deposit_to\": \"Main Account\",\n \"amount\": 2500,\n \"discount\": \"no\",\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\",\n \"customer\": {\n \"id\": 1\n },\n \"vehicle\": {\n \"id\": 1\n },\n \"invoice_sequence\": {\n \"id\": 1,\n \"title\": \"INV\"\n },\n \"department\": {\n \"id\": 1,\n \"name\": \"Service\"\n },\n \"invoice_inspection_categories\": [\n {\n \"id\": 1,\n \"invoice_id\": 1,\n \"inspection_category_id\": 1,\n \"rate_type\": \"flat_rate\",\n \"labor_rate\": 500,\n \"working_hours\": 1,\n \"labor_hours\": 1,\n \"rate\": 500,\n \"chart_of_account\": \"4000\",\n \"description\": \"General inspection\",\n \"department_id\": 1\n }\n ],\n \"invoice_parts\": [\n {\n \"id\": 1,\n \"invoice_id\": 1,\n \"part_id\": 1,\n \"quantity\": 2,\n \"rate\": 150,\n \"chart_of_account\": \"4100\",\n \"description\": \"Oil filter\",\n \"department_id\": 1\n }\n ],\n \"invoice_expenses\": [\n {\n \"id\": 1,\n \"invoice_id\": 1,\n \"expense_id\": 1,\n \"quantity\": 1,\n \"rate\": 100,\n \"chart_of_account\": \"4200\",\n \"description\": \"Shop supplies\",\n \"department_id\": 1\n }\n ],\n \"invoice_services\": [\n {\n \"id\": 1,\n \"invoice_id\": 1,\n \"service_id\": 1,\n \"rate_type\": \"hourly\",\n \"labor_rate_id\": 1,\n \"working_hours\": 2,\n \"labor_hours\": 2,\n \"quantity\": 1,\n \"rate\": 800,\n \"chart_of_account\": \"4300\",\n \"description\": \"Engine service\",\n \"department_id\": 1\n }\n ],\n \"invoice_service_groups\": [\n {\n \"id\": 1,\n \"invoice_id\": 1,\n \"service_group_id\": 1,\n \"rate_type\": \"flat_rate\",\n \"labor_rate_id\": 1,\n \"working_hours\": 1,\n \"labor_hours\": 1,\n \"rate\": 600,\n \"chart_of_account\": \"4400\",\n \"description\": \"Major service package\",\n \"department_id\": 1\n }\n ]\n }\n}"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { CrudClient } from "../infra/crud-client"
|
||||
import type { ApiClientOptions } from "../infra/client"
|
||||
import type { ApiPath } from "../infra/types"
|
||||
import type { ApiPath, ApiResponse } from "../infra/types"
|
||||
|
||||
export const ESTIMATE_ROUTES = {
|
||||
INDEX: "/api/estimates",
|
||||
@ -25,8 +25,8 @@ export class EstimatesClient extends CrudClient<
|
||||
// Note: GET /api/estimates/{id} is not in the OpenAPI schema.
|
||||
// This method uses a type cast and relies on the backend supporting the route.
|
||||
async getById(id: string) {
|
||||
const data = await this.get(ESTIMATE_ROUTES.INDEX, { query: { id } } as never)
|
||||
return {...data, data: (data as any)?.data?.[0] ?? null }
|
||||
const data = await this.get(ESTIMATE_ROUTES.BY_ID, { params: { id } })
|
||||
return data;
|
||||
}
|
||||
|
||||
// ── Estimate Services ──
|
||||
@ -48,7 +48,7 @@ export class EstimatesClient extends CrudClient<
|
||||
|
||||
// ── Estimate Parts ──
|
||||
async listParts(estimateId: string) {
|
||||
return this.get(ESTIMATE_ROUTES.PARTS, { params: { id: estimateId } } as never)
|
||||
return this.get(ESTIMATE_ROUTES.PARTS, { params: { id: estimateId } }) as Promise<ApiResponse<typeof ESTIMATE_ROUTES.PARTS, "get">>
|
||||
}
|
||||
|
||||
async addPart(estimateId: string, payload: { part_id?: number; quantity?: number; rate?: string; description?: string }) {
|
||||
|
||||
@ -8,6 +8,7 @@ import type {
|
||||
} from "./types"
|
||||
import createClient from "openapi-fetch"
|
||||
import type { paths } from "../../types/index"
|
||||
import { logger } from "./logger"
|
||||
|
||||
type HttpMethod = "get" | "post" | "put" | "delete" | "patch"
|
||||
|
||||
@ -73,6 +74,8 @@ export class ApiClient {
|
||||
const opts = options as never
|
||||
const body = (options as Record<string, unknown>).body as never
|
||||
|
||||
|
||||
|
||||
switch (method) {
|
||||
case "get":
|
||||
return this.get(ep, opts) as Promise<ApiResponse<Path, Method>>
|
||||
@ -96,12 +99,15 @@ export class ApiClient {
|
||||
options: ApiRequestOptions<Path, "get"> = {} as ApiRequestOptions<Path, "get">,
|
||||
): Promise<ApiResponse<Path, "get">> {
|
||||
const requestOptions = this.toFetchOptions(options)
|
||||
const startTime = Date.now()
|
||||
logger.debug({ method: "GET", endpoint }, "API request")
|
||||
|
||||
try {
|
||||
const { data, error, response } = await this.client.GET(endpoint, requestOptions as never)
|
||||
return this.resolveResult(endpoint, "get", data, error, response)
|
||||
return this.resolveResult(endpoint, "get", data, error, response, startTime)
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) throw err
|
||||
logger.error({ method: "GET", endpoint, duration: Date.now() - startTime }, "Network error")
|
||||
throw this.createNetworkError(endpoint, "get")
|
||||
}
|
||||
}
|
||||
@ -112,12 +118,15 @@ export class ApiClient {
|
||||
options: Omit<ApiRequestOptions<Path, "post">, "body"> = {} as Omit<ApiRequestOptions<Path, "post">, "body">,
|
||||
): Promise<ApiResponse<Path, "post">> {
|
||||
const requestOptions = this.toFetchOptions({ ...options, body })
|
||||
const startTime = Date.now()
|
||||
logger.debug({ method: "POST", endpoint }, "API request")
|
||||
|
||||
try {
|
||||
const { data, error, response } = await this.client.POST(endpoint, requestOptions as never)
|
||||
return this.resolveResult(endpoint, "post", data, error, response)
|
||||
return this.resolveResult(endpoint, "post", data, error, response, startTime)
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) throw err
|
||||
logger.error({ method: "POST", endpoint, duration: Date.now() - startTime }, "Network error")
|
||||
throw this.createNetworkError(endpoint, "post")
|
||||
}
|
||||
}
|
||||
@ -128,12 +137,15 @@ export class ApiClient {
|
||||
options: Omit<ApiRequestOptions<Path, "put">, "body"> = {} as Omit<ApiRequestOptions<Path, "put">, "body">,
|
||||
): Promise<ApiResponse<Path, "put">> {
|
||||
const requestOptions = this.toFetchOptions({ ...options, body })
|
||||
const startTime = Date.now()
|
||||
logger.debug({ method: "PUT", endpoint }, "API request")
|
||||
|
||||
try {
|
||||
const { data, error, response } = await this.client.PUT(endpoint, requestOptions as never)
|
||||
return this.resolveResult(endpoint, "put", data, error, response)
|
||||
return this.resolveResult(endpoint, "put", data, error, response, startTime)
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) throw err
|
||||
logger.error({ method: "PUT", endpoint, duration: Date.now() - startTime }, "Network error")
|
||||
throw this.createNetworkError(endpoint, "put")
|
||||
}
|
||||
}
|
||||
@ -143,12 +155,15 @@ export class ApiClient {
|
||||
options: ApiRequestOptions<Path, "delete"> = {} as ApiRequestOptions<Path, "delete">,
|
||||
): Promise<ApiResponse<Path, "delete">> {
|
||||
const requestOptions = this.toFetchOptions(options)
|
||||
const startTime = Date.now()
|
||||
logger.debug({ method: "DELETE", endpoint }, "API request")
|
||||
|
||||
try {
|
||||
const { data, error, response } = await this.client.DELETE(endpoint, requestOptions as never)
|
||||
return this.resolveResult(endpoint, "delete", data, error, response)
|
||||
return this.resolveResult(endpoint, "delete", data, error, response, startTime)
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) throw err
|
||||
logger.error({ method: "DELETE", endpoint, duration: Date.now() - startTime }, "Network error")
|
||||
throw this.createNetworkError(endpoint, "delete")
|
||||
}
|
||||
}
|
||||
@ -159,12 +174,15 @@ export class ApiClient {
|
||||
options: Omit<ApiRequestOptions<Path, "patch">, "body"> = {} as Omit<ApiRequestOptions<Path, "patch">, "body">,
|
||||
): Promise<ApiResponse<Path, "patch">> {
|
||||
const requestOptions = this.toFetchOptions({ ...options, body })
|
||||
const startTime = Date.now()
|
||||
logger.debug({ method: "PATCH", endpoint }, "API request")
|
||||
|
||||
try {
|
||||
const { data, error, response } = await this.client.PATCH(endpoint, requestOptions as never)
|
||||
return this.resolveResult(endpoint, "patch", data, error, response)
|
||||
return this.resolveResult(endpoint, "patch", data, error, response, startTime)
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) throw err
|
||||
logger.error({ method: "PATCH", endpoint, duration: Date.now() - startTime }, "Network error")
|
||||
throw this.createNetworkError(endpoint, "patch")
|
||||
}
|
||||
}
|
||||
@ -179,14 +197,21 @@ export class ApiClient {
|
||||
headers.set("Accept", "application/json")
|
||||
// Content-Type is intentionally omitted — fetch sets multipart/form-data + boundary automatically
|
||||
|
||||
const startTime = Date.now()
|
||||
logger.debug({ method: "POST", endpoint }, "API request (form-data)")
|
||||
|
||||
const response = await fetch(url, { method: "POST", headers, body: formData })
|
||||
const text = await response.text()
|
||||
const data = text ? JSON.parse(text) : null
|
||||
const duration = Date.now() - startTime
|
||||
|
||||
if (!response.ok) {
|
||||
const level = response.status >= 500 ? "error" : "warn"
|
||||
logger[level]({ method: "POST", endpoint, status: response.status, duration }, data?.message ?? "API request failed")
|
||||
throw new ApiError(response.status, response.statusText, endpoint, "post", data)
|
||||
}
|
||||
|
||||
logger.debug({ method: "POST", endpoint, status: response.status, duration }, "API request completed (form-data)")
|
||||
return data
|
||||
}
|
||||
|
||||
@ -201,14 +226,21 @@ export class ApiClient {
|
||||
init.body = JSON.stringify(options.body)
|
||||
}
|
||||
|
||||
const startTime = Date.now()
|
||||
logger.debug({ method, endpoint }, "API request (blob)")
|
||||
|
||||
const response = await fetch(url, init)
|
||||
const duration = Date.now() - startTime
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text()
|
||||
const data = text ? JSON.parse(text) : null
|
||||
const level = response.status >= 500 ? "error" : "warn"
|
||||
logger[level]({ method, endpoint, status: response.status, duration }, data?.message ?? "API request failed")
|
||||
throw new ApiError(response.status, response.statusText, endpoint, method.toLowerCase(), data)
|
||||
}
|
||||
|
||||
logger.debug({ method, endpoint, status: response.status, duration }, "API request completed (blob)")
|
||||
return response.blob()
|
||||
}
|
||||
|
||||
@ -250,17 +282,28 @@ export class ApiClient {
|
||||
data: unknown,
|
||||
error: unknown,
|
||||
response: Response,
|
||||
startTime: number,
|
||||
): ApiResponse<Path, Method> {
|
||||
const duration = Date.now() - startTime
|
||||
|
||||
if (error !== undefined) {
|
||||
const payload = this.normalizeErrorPayload(error)
|
||||
if (response.status >= 500) {
|
||||
logger.error({ method: method.toUpperCase(), endpoint, status: response.status, duration }, payload?.message ?? "API request failed")
|
||||
} else {
|
||||
logger.warn({ method: method.toUpperCase(), endpoint, status: response.status, duration }, payload?.message ?? "API request failed")
|
||||
}
|
||||
throw new ApiError(
|
||||
response.status,
|
||||
response.statusText,
|
||||
endpoint,
|
||||
method,
|
||||
this.normalizeErrorPayload(error),
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
logger.debug({ method: method.toUpperCase(), endpoint, status: response.status, duration, data }, "API request completed")
|
||||
|
||||
return data as ApiResponse<Path, Method>
|
||||
}
|
||||
|
||||
|
||||
32
packages/api/src/infra/logger.ts
Normal file
32
packages/api/src/infra/logger.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import pino from "pino"
|
||||
|
||||
/**
|
||||
* Isomorphic logger for the API client.
|
||||
*
|
||||
* - In Node.js (server / Next.js RSC / server actions): pino writes newline-delimited
|
||||
* JSON to stdout — structured and machine-parseable.
|
||||
* - In the browser (Next.js client components): pino switches to the browser
|
||||
* transport automatically and delegates to the appropriate `console.*` method
|
||||
* (console.debug, console.warn, console.error, …).
|
||||
*
|
||||
* Log level defaults:
|
||||
* development → debug (all messages)
|
||||
* production → warn (warnings + errors only)
|
||||
*
|
||||
* Override with the NEXT_PUBLIC_LOG_LEVEL env var (e.g. "info").
|
||||
*/
|
||||
const level =
|
||||
(process.env.NEXT_PUBLIC_LOG_LEVEL as pino.Level | undefined) ??
|
||||
(process.env.NODE_ENV === "production" ? "warn" : "debug")
|
||||
|
||||
export const logger = pino({
|
||||
name: "garage-api",
|
||||
level,
|
||||
browser: {
|
||||
/**
|
||||
* Pass a plain object to `console.*` so DevTools display
|
||||
* log fields (method, endpoint, status, duration) as expandable props.
|
||||
*/
|
||||
asObject: true,
|
||||
},
|
||||
})
|
||||
@ -95,8 +95,8 @@ export interface paths {
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
/** @description Created */
|
||||
201: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
@ -14538,7 +14538,166 @@ export interface paths {
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
/** Display the specified estimate. */
|
||||
get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
/**
|
||||
* @example {
|
||||
* "data": {
|
||||
* "id": 1,
|
||||
* "title": "Estimate for Toyota Camry",
|
||||
* "customer_id": 1,
|
||||
* "vehicle_id": 1,
|
||||
* "department_id": 1,
|
||||
* "estimate_number": "EST-001",
|
||||
* "date": "2026-03-31",
|
||||
* "has_insurance": false,
|
||||
* "enable_digital_authorisation": false,
|
||||
* "insurance_type_id": 1,
|
||||
* "insurer_id": 2,
|
||||
* "service_writer_id": 1,
|
||||
* "footer": "Thank you for your business.",
|
||||
* "created_at": "2026-03-31T10:00:00.000000Z",
|
||||
* "updated_at": "2026-03-31T10:00:00.000000Z",
|
||||
* "labels": [
|
||||
* {
|
||||
* "id": 1,
|
||||
* "title": "Urgent",
|
||||
* "color_code": "#FF0000",
|
||||
* "pivot": {
|
||||
* "estimate_id": 1,
|
||||
* "label_id": 1
|
||||
* }
|
||||
* }
|
||||
* ],
|
||||
* "customer_remarks": [
|
||||
* {
|
||||
* "id": 1,
|
||||
* "estimate_id": 1,
|
||||
* "remark": "Oil change recommended.",
|
||||
* "created_at": "2026-03-31T10:00:00.000000Z",
|
||||
* "updated_at": "2026-03-31T10:00:00.000000Z"
|
||||
* }
|
||||
* ],
|
||||
* "customer": {
|
||||
* "id": 1,
|
||||
* "first_name": "John",
|
||||
* "last_name": "Doe"
|
||||
* },
|
||||
* "vehicle": {
|
||||
* "id": 1,
|
||||
* "registration_number": "ABC-1234",
|
||||
* "make": "Toyota",
|
||||
* "model": "Camry"
|
||||
* },
|
||||
* "department": {
|
||||
* "id": 1,
|
||||
* "name": "Service"
|
||||
* },
|
||||
* "insurance_type": {
|
||||
* "id": 1,
|
||||
* "title": "Comprehensive"
|
||||
* },
|
||||
* "insurer": {
|
||||
* "id": 2,
|
||||
* "first_name": "Alex",
|
||||
* "last_name": "Insurer"
|
||||
* },
|
||||
* "service_writer": {
|
||||
* "id": 1,
|
||||
* "first_name": "Sam",
|
||||
* "last_name": "Writer"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
"application/json": {
|
||||
data?: {
|
||||
id?: number;
|
||||
title?: string;
|
||||
customer_id?: number;
|
||||
vehicle_id?: number;
|
||||
department_id?: number;
|
||||
estimate_number?: string;
|
||||
date?: string;
|
||||
has_insurance?: boolean;
|
||||
enable_digital_authorisation?: boolean;
|
||||
insurance_type_id?: number;
|
||||
insurer_id?: number;
|
||||
service_writer_id?: number;
|
||||
footer?: string;
|
||||
/** Format: date-time */
|
||||
created_at?: string;
|
||||
/** Format: date-time */
|
||||
updated_at?: string;
|
||||
labels?: {
|
||||
id?: number;
|
||||
title?: string;
|
||||
color_code?: string;
|
||||
pivot?: {
|
||||
estimate_id?: number;
|
||||
label_id?: number;
|
||||
};
|
||||
}[];
|
||||
customer_remarks?: {
|
||||
id?: number;
|
||||
estimate_id?: number;
|
||||
remark?: string;
|
||||
/** Format: date-time */
|
||||
created_at?: string;
|
||||
/** Format: date-time */
|
||||
updated_at?: string;
|
||||
}[];
|
||||
customer?: {
|
||||
id?: number;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
};
|
||||
vehicle?: {
|
||||
id?: number;
|
||||
registration_number?: string;
|
||||
make?: string;
|
||||
model?: string;
|
||||
};
|
||||
department?: {
|
||||
id?: number;
|
||||
name?: string;
|
||||
};
|
||||
insurance_type?: {
|
||||
id?: number;
|
||||
title?: string;
|
||||
};
|
||||
insurer?: {
|
||||
id?: number;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
};
|
||||
service_writer?: {
|
||||
id?: number;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/** Update the specified estimate. */
|
||||
put: {
|
||||
parameters: {
|
||||
@ -29708,15 +29867,87 @@ export interface paths {
|
||||
* "customer_id": 1,
|
||||
* "vehicle_id": 1,
|
||||
* "estimate_id": 1,
|
||||
* "kms_in": 50000,
|
||||
* "has_insurance": false,
|
||||
* "insurer_id": 2,
|
||||
* "invoice_to_id": 1,
|
||||
* "billing_address_id": 10,
|
||||
* "delivery_address_id": 11,
|
||||
* "invoice_date": "2026-03-31",
|
||||
* "due_date": "2026-04-14",
|
||||
* "payment_terms_id": 1,
|
||||
* "invoice_sequence_id": 1,
|
||||
* "invoice_number": "INV-001",
|
||||
* "invoice_title": "Tax Invoice",
|
||||
* "department_id": 1,
|
||||
* "notes": "string",
|
||||
* "notes": "Vehicle service and parts",
|
||||
* "terms_and_conditions": "Payment due in 14 days.",
|
||||
* "status": "draft",
|
||||
* "discount": "no"
|
||||
* "received_payment": false,
|
||||
* "payment_mode_id": 1,
|
||||
* "deposit_to": "Main Account",
|
||||
* "amount": 2500,
|
||||
* "discount": "no",
|
||||
* "inspection_categories": [
|
||||
* {
|
||||
* "inspection_category_id": 1,
|
||||
* "rate_type": "flat_rate",
|
||||
* "labor_rate": 500,
|
||||
* "working_hours": 1,
|
||||
* "labor_hours": 1,
|
||||
* "rate": 500,
|
||||
* "chart_of_account": "4000",
|
||||
* "description": "General inspection",
|
||||
* "department_id": 1
|
||||
* }
|
||||
* ],
|
||||
* "parts": [
|
||||
* {
|
||||
* "part_id": 1,
|
||||
* "quantity": 2,
|
||||
* "rate": 150,
|
||||
* "chart_of_account": "4100",
|
||||
* "description": "Oil filter",
|
||||
* "department_id": 1
|
||||
* }
|
||||
* ],
|
||||
* "expenses": [
|
||||
* {
|
||||
* "expense_id": 1,
|
||||
* "quantity": 1,
|
||||
* "rate": 100,
|
||||
* "chart_of_account": "4200",
|
||||
* "description": "Shop supplies",
|
||||
* "department_id": 1
|
||||
* }
|
||||
* ],
|
||||
* "services": [
|
||||
* {
|
||||
* "service_id": 1,
|
||||
* "rate_type": "hourly",
|
||||
* "labor_rate_id": 1,
|
||||
* "working_hours": 2,
|
||||
* "labor_hours": 2,
|
||||
* "quantity": 1,
|
||||
* "rate": 800,
|
||||
* "chart_of_account": "4300",
|
||||
* "description": "Engine service",
|
||||
* "department_id": 1
|
||||
* }
|
||||
* ],
|
||||
* "service_groups": [
|
||||
* {
|
||||
* "service_group_id": 1,
|
||||
* "rate_type": "flat_rate",
|
||||
* "labor_rate_id": 1,
|
||||
* "working_hours": 1,
|
||||
* "labor_hours": 1,
|
||||
* "rate": 600,
|
||||
* "chart_of_account": "4400",
|
||||
* "description": "Major service package",
|
||||
* "department_id": 1
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
"application/json": {
|
||||
@ -29724,40 +29955,102 @@ export interface paths {
|
||||
customer_id?: number;
|
||||
vehicle_id?: number;
|
||||
estimate_id?: number;
|
||||
kms_in?: number;
|
||||
has_insurance?: boolean;
|
||||
insurer_id?: number;
|
||||
invoice_to_id?: number;
|
||||
billing_address_id?: number;
|
||||
delivery_address_id?: number;
|
||||
invoice_date?: string;
|
||||
due_date?: string;
|
||||
payment_terms_id?: number;
|
||||
invoice_sequence_id?: number;
|
||||
invoice_number?: string;
|
||||
invoice_title?: string;
|
||||
department_id?: number;
|
||||
notes?: string;
|
||||
terms_and_conditions?: string;
|
||||
status?: string;
|
||||
received_payment?: boolean;
|
||||
payment_mode_id?: number;
|
||||
deposit_to?: string;
|
||||
amount?: number;
|
||||
discount?: string;
|
||||
inspection_categories?: {
|
||||
inspection_category_id?: number;
|
||||
rate_type?: string;
|
||||
labor_rate?: number;
|
||||
working_hours?: number;
|
||||
labor_hours?: number;
|
||||
rate?: number;
|
||||
chart_of_account?: string;
|
||||
description?: string;
|
||||
department_id?: number;
|
||||
}[];
|
||||
parts?: {
|
||||
part_id?: number;
|
||||
quantity?: number;
|
||||
rate?: number;
|
||||
chart_of_account?: string;
|
||||
description?: string;
|
||||
department_id?: number;
|
||||
}[];
|
||||
expenses?: {
|
||||
expense_id?: number;
|
||||
quantity?: number;
|
||||
rate?: number;
|
||||
chart_of_account?: string;
|
||||
description?: string;
|
||||
department_id?: number;
|
||||
}[];
|
||||
services?: {
|
||||
service_id?: number;
|
||||
rate_type?: string;
|
||||
labor_rate_id?: number;
|
||||
working_hours?: number;
|
||||
labor_hours?: number;
|
||||
quantity?: number;
|
||||
rate?: number;
|
||||
chart_of_account?: string;
|
||||
description?: string;
|
||||
department_id?: number;
|
||||
}[];
|
||||
service_groups?: {
|
||||
service_group_id?: number;
|
||||
rate_type?: string;
|
||||
labor_rate_id?: number;
|
||||
working_hours?: number;
|
||||
labor_hours?: number;
|
||||
rate?: number;
|
||||
chart_of_account?: string;
|
||||
description?: string;
|
||||
department_id?: number;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
/** @description Created */
|
||||
201: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
/**
|
||||
* @example {
|
||||
* "message": "Operation completed successfully.",
|
||||
* "message": "Invoice created successfully.",
|
||||
* "data": {
|
||||
* "id": 1,
|
||||
* "subject": "Invoice for Job Card 001",
|
||||
* "subject": "Invoice for Service",
|
||||
* "customer_id": 1,
|
||||
* "vehicle_id": 1,
|
||||
* "estimate_id": 1,
|
||||
* "kms_in": 50000,
|
||||
* "has_insurance": false,
|
||||
* "insurer_id": 1,
|
||||
* "insurer_id": 2,
|
||||
* "invoice_to_id": 1,
|
||||
* "billing_address_id": 1,
|
||||
* "delivery_address_id": 1,
|
||||
* "billing_address_id": 10,
|
||||
* "delivery_address_id": 11,
|
||||
* "invoice_date": "2026-03-31",
|
||||
* "due_date": "2026-04-14",
|
||||
* "payment_terms_id": 1,
|
||||
@ -29765,16 +30058,100 @@ export interface paths {
|
||||
* "invoice_number": "INV-001",
|
||||
* "invoice_title": "Tax Invoice",
|
||||
* "department_id": 1,
|
||||
* "notes": "string",
|
||||
* "terms_and_conditions": "string",
|
||||
* "notes": "Vehicle service and parts",
|
||||
* "terms_and_conditions": "Payment due in 14 days.",
|
||||
* "status": "draft",
|
||||
* "received_payment": 0,
|
||||
* "received_payment": false,
|
||||
* "payment_mode_id": 1,
|
||||
* "deposit_to": "string",
|
||||
* "amount": 0,
|
||||
* "deposit_to": "Main Account",
|
||||
* "amount": 2500,
|
||||
* "discount": "no",
|
||||
* "created_at": "2026-03-31T10:00:00.000000Z",
|
||||
* "updated_at": "2026-03-31T10:00:00.000000Z"
|
||||
* "updated_at": "2026-03-31T10:00:00.000000Z",
|
||||
* "customer": {
|
||||
* "id": 1
|
||||
* },
|
||||
* "vehicle": {
|
||||
* "id": 1
|
||||
* },
|
||||
* "invoice_sequence": {
|
||||
* "id": 1,
|
||||
* "title": "INV"
|
||||
* },
|
||||
* "department": {
|
||||
* "id": 1,
|
||||
* "name": "Service"
|
||||
* },
|
||||
* "invoice_inspection_categories": [
|
||||
* {
|
||||
* "id": 1,
|
||||
* "invoice_id": 1,
|
||||
* "inspection_category_id": 1,
|
||||
* "rate_type": "flat_rate",
|
||||
* "labor_rate": 500,
|
||||
* "working_hours": 1,
|
||||
* "labor_hours": 1,
|
||||
* "rate": 500,
|
||||
* "chart_of_account": "4000",
|
||||
* "description": "General inspection",
|
||||
* "department_id": 1
|
||||
* }
|
||||
* ],
|
||||
* "invoice_parts": [
|
||||
* {
|
||||
* "id": 1,
|
||||
* "invoice_id": 1,
|
||||
* "part_id": 1,
|
||||
* "quantity": 2,
|
||||
* "rate": 150,
|
||||
* "chart_of_account": "4100",
|
||||
* "description": "Oil filter",
|
||||
* "department_id": 1
|
||||
* }
|
||||
* ],
|
||||
* "invoice_expenses": [
|
||||
* {
|
||||
* "id": 1,
|
||||
* "invoice_id": 1,
|
||||
* "expense_id": 1,
|
||||
* "quantity": 1,
|
||||
* "rate": 100,
|
||||
* "chart_of_account": "4200",
|
||||
* "description": "Shop supplies",
|
||||
* "department_id": 1
|
||||
* }
|
||||
* ],
|
||||
* "invoice_services": [
|
||||
* {
|
||||
* "id": 1,
|
||||
* "invoice_id": 1,
|
||||
* "service_id": 1,
|
||||
* "rate_type": "hourly",
|
||||
* "labor_rate_id": 1,
|
||||
* "working_hours": 2,
|
||||
* "labor_hours": 2,
|
||||
* "quantity": 1,
|
||||
* "rate": 800,
|
||||
* "chart_of_account": "4300",
|
||||
* "description": "Engine service",
|
||||
* "department_id": 1
|
||||
* }
|
||||
* ],
|
||||
* "invoice_service_groups": [
|
||||
* {
|
||||
* "id": 1,
|
||||
* "invoice_id": 1,
|
||||
* "service_group_id": 1,
|
||||
* "rate_type": "flat_rate",
|
||||
* "labor_rate_id": 1,
|
||||
* "working_hours": 1,
|
||||
* "labor_hours": 1,
|
||||
* "rate": 600,
|
||||
* "chart_of_account": "4400",
|
||||
* "description": "Major service package",
|
||||
* "department_id": 1
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
@ -29802,7 +30179,7 @@ export interface paths {
|
||||
notes?: string;
|
||||
terms_and_conditions?: string;
|
||||
status?: string;
|
||||
received_payment?: number;
|
||||
received_payment?: boolean;
|
||||
payment_mode_id?: number;
|
||||
deposit_to?: string;
|
||||
amount?: number;
|
||||
@ -29811,6 +30188,80 @@ export interface paths {
|
||||
created_at?: string;
|
||||
/** Format: date-time */
|
||||
updated_at?: string;
|
||||
customer?: {
|
||||
id?: number;
|
||||
};
|
||||
vehicle?: {
|
||||
id?: number;
|
||||
};
|
||||
invoice_sequence?: {
|
||||
id?: number;
|
||||
title?: string;
|
||||
};
|
||||
department?: {
|
||||
id?: number;
|
||||
name?: string;
|
||||
};
|
||||
invoice_inspection_categories?: {
|
||||
id?: number;
|
||||
invoice_id?: number;
|
||||
inspection_category_id?: number;
|
||||
rate_type?: string;
|
||||
labor_rate?: number;
|
||||
working_hours?: number;
|
||||
labor_hours?: number;
|
||||
rate?: number;
|
||||
chart_of_account?: string;
|
||||
description?: string;
|
||||
department_id?: number;
|
||||
}[];
|
||||
invoice_parts?: {
|
||||
id?: number;
|
||||
invoice_id?: number;
|
||||
part_id?: number;
|
||||
quantity?: number;
|
||||
rate?: number;
|
||||
chart_of_account?: string;
|
||||
description?: string;
|
||||
department_id?: number;
|
||||
}[];
|
||||
invoice_expenses?: {
|
||||
id?: number;
|
||||
invoice_id?: number;
|
||||
expense_id?: number;
|
||||
quantity?: number;
|
||||
rate?: number;
|
||||
chart_of_account?: string;
|
||||
description?: string;
|
||||
department_id?: number;
|
||||
}[];
|
||||
invoice_services?: {
|
||||
id?: number;
|
||||
invoice_id?: number;
|
||||
service_id?: number;
|
||||
rate_type?: string;
|
||||
labor_rate_id?: number;
|
||||
working_hours?: number;
|
||||
labor_hours?: number;
|
||||
quantity?: number;
|
||||
rate?: number;
|
||||
chart_of_account?: string;
|
||||
description?: string;
|
||||
department_id?: number;
|
||||
}[];
|
||||
invoice_service_groups?: {
|
||||
id?: number;
|
||||
invoice_id?: number;
|
||||
service_group_id?: number;
|
||||
rate_type?: string;
|
||||
labor_rate_id?: number;
|
||||
working_hours?: number;
|
||||
labor_hours?: number;
|
||||
rate?: number;
|
||||
chart_of_account?: string;
|
||||
description?: string;
|
||||
department_id?: number;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
146
pnpm-lock.yaml
generated
146
pnpm-lock.yaml
generated
@ -159,6 +159,9 @@ importers:
|
||||
openapi-fetch:
|
||||
specifier: ^0.14.0
|
||||
version: 0.14.1
|
||||
pino:
|
||||
specifier: ^10.3.1
|
||||
version: 10.3.1
|
||||
server-only:
|
||||
specifier: '*'
|
||||
version: 0.0.1
|
||||
@ -816,6 +819,9 @@ packages:
|
||||
'@open-draft/until@2.1.0':
|
||||
resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
|
||||
|
||||
'@pinojs/redact@0.4.0':
|
||||
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
|
||||
|
||||
'@radix-ui/number@1.1.1':
|
||||
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
|
||||
|
||||
@ -2063,6 +2069,10 @@ packages:
|
||||
resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
|
||||
engines: {node: '>= 4.0.0'}
|
||||
|
||||
atomic-sleep@1.0.0:
|
||||
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
available-typed-arrays@1.0.7:
|
||||
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -3777,6 +3787,10 @@ packages:
|
||||
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
on-exit-leak-free@2.1.2:
|
||||
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
on-finished@2.4.1:
|
||||
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@ -3903,6 +3917,16 @@ packages:
|
||||
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
pino-abstract-transport@3.0.0:
|
||||
resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==}
|
||||
|
||||
pino-std-serializers@7.1.0:
|
||||
resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==}
|
||||
|
||||
pino@10.3.1:
|
||||
resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==}
|
||||
hasBin: true
|
||||
|
||||
pkce-challenge@5.0.1:
|
||||
resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
@ -4008,6 +4032,9 @@ packages:
|
||||
resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
process-warning@5.0.0:
|
||||
resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
|
||||
|
||||
process@0.11.10:
|
||||
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
@ -4044,6 +4071,9 @@ packages:
|
||||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
quick-format-unescaped@4.0.4:
|
||||
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
||||
|
||||
radix-ui@1.4.3:
|
||||
resolution: {integrity: sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==}
|
||||
peerDependencies:
|
||||
@ -4146,6 +4176,10 @@ packages:
|
||||
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
real-require@0.2.0:
|
||||
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
|
||||
engines: {node: '>= 12.13.0'}
|
||||
|
||||
recast@0.23.11:
|
||||
resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==}
|
||||
engines: {node: '>= 4'}
|
||||
@ -4251,6 +4285,10 @@ packages:
|
||||
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
safe-stable-stringify@2.5.0:
|
||||
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
@ -4342,6 +4380,9 @@ packages:
|
||||
resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
sonic-boom@4.2.1:
|
||||
resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
|
||||
|
||||
sonner@2.0.7:
|
||||
resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
|
||||
peerDependencies:
|
||||
@ -4356,6 +4397,10 @@ packages:
|
||||
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
split2@4.2.0:
|
||||
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
||||
engines: {node: '>= 10.x'}
|
||||
|
||||
sshpk@1.18.0:
|
||||
resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -4490,6 +4535,10 @@ packages:
|
||||
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
thread-stream@4.0.0:
|
||||
resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
throttleit@1.0.1:
|
||||
resolution: {integrity: sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==}
|
||||
|
||||
@ -4872,7 +4921,7 @@ snapshots:
|
||||
'@babel/types': 7.29.0
|
||||
'@jridgewell/remapping': 2.3.5
|
||||
convert-source-map: 2.0.0
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
gensync: 1.0.0-beta.2
|
||||
json5: 2.2.3
|
||||
semver: 6.3.1
|
||||
@ -5030,7 +5079,7 @@ snapshots:
|
||||
'@babel/parser': 7.29.2
|
||||
'@babel/template': 7.28.6
|
||||
'@babel/types': 7.29.0
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@ -5140,7 +5189,7 @@ snapshots:
|
||||
'@eslint/config-array@0.21.1':
|
||||
dependencies:
|
||||
'@eslint/object-schema': 2.1.7
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
minimatch: 3.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@ -5148,7 +5197,7 @@ snapshots:
|
||||
'@eslint/config-array@0.21.2':
|
||||
dependencies:
|
||||
'@eslint/object-schema': 2.1.7
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
minimatch: 3.1.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@ -5164,7 +5213,7 @@ snapshots:
|
||||
'@eslint/eslintrc@3.3.1':
|
||||
dependencies:
|
||||
ajv: 6.12.6
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
espree: 10.4.0
|
||||
globals: 14.0.0
|
||||
ignore: 5.3.2
|
||||
@ -5178,7 +5227,7 @@ snapshots:
|
||||
'@eslint/eslintrc@3.3.5':
|
||||
dependencies:
|
||||
ajv: 6.14.0
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
espree: 10.4.0
|
||||
globals: 14.0.0
|
||||
ignore: 5.3.2
|
||||
@ -5484,6 +5533,8 @@ snapshots:
|
||||
|
||||
'@open-draft/until@2.1.0': {}
|
||||
|
||||
'@pinojs/redact@0.4.0': {}
|
||||
|
||||
'@radix-ui/number@1.1.1': {}
|
||||
|
||||
'@radix-ui/primitive@1.1.3': {}
|
||||
@ -6506,7 +6557,7 @@ snapshots:
|
||||
'@typescript-eslint/types': 8.50.0
|
||||
'@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.2)
|
||||
'@typescript-eslint/visitor-keys': 8.50.0
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
eslint: 9.39.1(jiti@2.6.1)
|
||||
typescript: 5.9.2
|
||||
transitivePeerDependencies:
|
||||
@ -6518,7 +6569,7 @@ snapshots:
|
||||
'@typescript-eslint/types': 8.50.0
|
||||
'@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 8.50.0
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
@ -6528,7 +6579,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.2)
|
||||
'@typescript-eslint/types': 8.50.0
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
typescript: 5.9.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@ -6537,7 +6588,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.50.0
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@ -6560,7 +6611,7 @@ snapshots:
|
||||
'@typescript-eslint/types': 8.50.0
|
||||
'@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.2)
|
||||
'@typescript-eslint/utils': 8.50.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.2)
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
eslint: 9.39.1(jiti@2.6.1)
|
||||
ts-api-utils: 2.1.0(typescript@5.9.2)
|
||||
typescript: 5.9.2
|
||||
@ -6572,7 +6623,7 @@ snapshots:
|
||||
'@typescript-eslint/types': 8.50.0
|
||||
'@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.50.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
ts-api-utils: 2.1.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
@ -6587,7 +6638,7 @@ snapshots:
|
||||
'@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.2)
|
||||
'@typescript-eslint/types': 8.50.0
|
||||
'@typescript-eslint/visitor-keys': 8.50.0
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
minimatch: 9.0.5
|
||||
semver: 7.7.3
|
||||
tinyglobby: 0.2.15
|
||||
@ -6602,7 +6653,7 @@ snapshots:
|
||||
'@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.50.0
|
||||
'@typescript-eslint/visitor-keys': 8.50.0
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
minimatch: 9.0.5
|
||||
semver: 7.7.3
|
||||
tinyglobby: 0.2.15
|
||||
@ -6851,6 +6902,8 @@ snapshots:
|
||||
|
||||
at-least-node@1.0.0: {}
|
||||
|
||||
atomic-sleep@1.0.0: {}
|
||||
|
||||
available-typed-arrays@1.0.7:
|
||||
dependencies:
|
||||
possible-typed-array-names: 1.1.0
|
||||
@ -6883,7 +6936,7 @@ snapshots:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
content-type: 1.0.5
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
http-errors: 2.0.1
|
||||
iconv-lite: 0.7.2
|
||||
on-finished: 2.4.1
|
||||
@ -7476,7 +7529,7 @@ snapshots:
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
get-tsconfig: 4.13.7
|
||||
is-bun-module: 2.0.0
|
||||
@ -7640,7 +7693,7 @@ snapshots:
|
||||
ajv: 6.12.6
|
||||
chalk: 4.1.2
|
||||
cross-spawn: 7.0.6
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
escape-string-regexp: 4.0.0
|
||||
eslint-scope: 8.4.0
|
||||
eslint-visitor-keys: 4.2.1
|
||||
@ -7681,7 +7734,7 @@ snapshots:
|
||||
ajv: 6.14.0
|
||||
chalk: 4.1.2
|
||||
cross-spawn: 7.0.6
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
escape-string-regexp: 4.0.0
|
||||
eslint-scope: 8.4.0
|
||||
eslint-visitor-keys: 4.2.1
|
||||
@ -7793,7 +7846,7 @@ snapshots:
|
||||
content-type: 1.0.5
|
||||
cookie: 0.7.2
|
||||
cookie-signature: 1.2.2
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
depd: 2.0.0
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
@ -7891,7 +7944,7 @@ snapshots:
|
||||
|
||||
finalhandler@2.1.1:
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
on-finished: 2.4.1
|
||||
@ -8095,6 +8148,13 @@ snapshots:
|
||||
jsprim: 2.0.2
|
||||
sshpk: 1.18.0
|
||||
|
||||
https-proxy-agent@7.0.6:
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
https-proxy-agent@7.0.6(supports-color@10.2.2):
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
@ -8708,6 +8768,8 @@ snapshots:
|
||||
define-properties: 1.2.1
|
||||
es-object-atoms: 1.1.1
|
||||
|
||||
on-exit-leak-free@2.1.2: {}
|
||||
|
||||
on-finished@2.4.1:
|
||||
dependencies:
|
||||
ee-first: 1.1.1
|
||||
@ -8839,6 +8901,26 @@ snapshots:
|
||||
|
||||
pify@2.3.0: {}
|
||||
|
||||
pino-abstract-transport@3.0.0:
|
||||
dependencies:
|
||||
split2: 4.2.0
|
||||
|
||||
pino-std-serializers@7.1.0: {}
|
||||
|
||||
pino@10.3.1:
|
||||
dependencies:
|
||||
'@pinojs/redact': 0.4.0
|
||||
atomic-sleep: 1.0.0
|
||||
on-exit-leak-free: 2.1.2
|
||||
pino-abstract-transport: 3.0.0
|
||||
pino-std-serializers: 7.1.0
|
||||
process-warning: 5.0.0
|
||||
quick-format-unescaped: 4.0.4
|
||||
real-require: 0.2.0
|
||||
safe-stable-stringify: 2.5.0
|
||||
sonic-boom: 4.2.1
|
||||
thread-stream: 4.0.0
|
||||
|
||||
pkce-challenge@5.0.1: {}
|
||||
|
||||
pluralize@8.0.0: {}
|
||||
@ -8880,6 +8962,8 @@ snapshots:
|
||||
dependencies:
|
||||
parse-ms: 4.0.0
|
||||
|
||||
process-warning@5.0.0: {}
|
||||
|
||||
process@0.11.10: {}
|
||||
|
||||
prompts@2.4.2:
|
||||
@ -8917,6 +9001,8 @@ snapshots:
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
quick-format-unescaped@4.0.4: {}
|
||||
|
||||
radix-ui@1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
@ -9058,6 +9144,8 @@ snapshots:
|
||||
|
||||
react@19.2.4: {}
|
||||
|
||||
real-require@0.2.0: {}
|
||||
|
||||
recast@0.23.11:
|
||||
dependencies:
|
||||
ast-types: 0.16.1
|
||||
@ -9156,7 +9244,7 @@ snapshots:
|
||||
|
||||
router@2.2.0:
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
depd: 2.0.0
|
||||
is-promise: 4.0.0
|
||||
parseurl: 1.3.3
|
||||
@ -9195,6 +9283,8 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
is-regex: 1.2.1
|
||||
|
||||
safe-stable-stringify@2.5.0: {}
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
scheduler@0.27.0: {}
|
||||
@ -9205,7 +9295,7 @@ snapshots:
|
||||
|
||||
send@1.2.1:
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
etag: 1.8.1
|
||||
@ -9273,7 +9363,7 @@ snapshots:
|
||||
fast-glob: 3.3.3
|
||||
fs-extra: 11.3.4
|
||||
fuzzysort: 3.1.0
|
||||
https-proxy-agent: 7.0.6(supports-color@10.2.2)
|
||||
https-proxy-agent: 7.0.6
|
||||
kleur: 4.1.5
|
||||
msw: 2.12.14(@types/node@25.5.0)(typescript@5.9.3)
|
||||
node-fetch: 3.3.2
|
||||
@ -9381,6 +9471,10 @@ snapshots:
|
||||
astral-regex: 2.0.0
|
||||
is-fullwidth-code-point: 3.0.0
|
||||
|
||||
sonic-boom@4.2.1:
|
||||
dependencies:
|
||||
atomic-sleep: 1.0.0
|
||||
|
||||
sonner@2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
@ -9390,6 +9484,8 @@ snapshots:
|
||||
|
||||
source-map@0.6.1: {}
|
||||
|
||||
split2@4.2.0: {}
|
||||
|
||||
sshpk@1.18.0:
|
||||
dependencies:
|
||||
asn1: 0.2.6
|
||||
@ -9530,6 +9626,10 @@ snapshots:
|
||||
|
||||
tapable@2.3.0: {}
|
||||
|
||||
thread-stream@4.0.0:
|
||||
dependencies:
|
||||
real-require: 0.2.0
|
||||
|
||||
throttleit@1.0.1: {}
|
||||
|
||||
through@2.3.8: {}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user