diff --git a/.github/skills/invoice-pattern/SKILL.md b/.github/skills/invoice-pattern/SKILL.md new file mode 100644 index 0000000..405f415 --- /dev/null +++ b/.github/skills/invoice-pattern/SKILL.md @@ -0,0 +1,95 @@ +# Invoice Pattern Skill + +This skill defines the standard for implementing invoice-like forms (Invoice, Estimate, Job Card, Purchase Order, Bill, etc.) in the carage-erp dashboard. All such forms must follow this pattern for layout, discount/tax handling, and summary calculation. The current Invoice form is the canonical reference. + +--- + +## 1. Layout +- **Two-column grid:** + - **Main column (9/12):** + - Subject, invoice number/title + - Status select, Discount Type select + - Conditional Transaction Discount field + - Line item selectors: Parts, Services, Expense Items (with optional line-level discount) + - Notes, Terms & Conditions + - Submit button + - **Sidebar column (3/12):** + - Invoice date, Due date + - Customer, Vehicle selectors + - Tax select (see below) + - Department, Payment Terms, Invoice Sequence, Insurance fields + - Summary card (see below) + +## 2. Discount Implementation +- **Discount Type:** + - Field: `discount` (enum: 'no', 'line_item_level', 'transaction_level') + - Select field in main column +- **Transaction-level Discount:** + - Field: `discount_amount` (number) + - Only shown when `discount === "transaction_level"` +- **Line-level Discount:** + - Each line item (parts, services, expenses) has `discount_amount` field + - Only shown when `discount === "line_item_level"` +- **Payload Mapping:** + - Only include `discount_amount` at transaction level if `discount === "transaction_level"` + - Only include per-line `discount_amount` if `discount === "line_item_level"` + +## 3. Tax Type Implementation +- **Tax Field:** + - Field: `tax` (relationFieldSchema: `{ value: string, label: string } | null`) + - Uses `RhfAsyncSelectField` in sidebar + - `mapOption`: `{ value: String(item.id), label: `${item.title} (${item.rate}%)` }` + - The selected tax's rate is parsed from the label string in the summary (regex: `/\((\d+(?:\.\d+)?)%\)/`) + +## 4. Summary Implementation +- **Summary Card:** + - Always rendered in the sidebar below the Details card + - Uses `InvoiceFormSummary` (form-aware adapter) + - `InvoiceFormSummary` flattens all line items, reads discount/tax fields, and passes them to `useDocumentTotals` hook + - `useDocumentTotals` (pure hook) computes subtotal, discounts, tax, and total + - `DocumentTotalsSummary` (pure display component) renders the summary + +--- + +## Reference: Invoice Form +- See `apps/dashboard/modules/invoices/invoice-form.tsx` for the canonical implementation. +- Schema: `apps/dashboard/modules/invoices/invoice.schema.ts` +- Summary logic: `apps/dashboard/modules/invoices/invoice-form-summary.tsx`, `shared/hooks/use-document-totals.ts`, `shared/components/document-totals-summary.tsx` + +--- + +## Required for All Invoice-like Forms +- Follow the above layout and field conventions +- Use the same discount/tax logic and summary calculation +- Use the same field and payload mapping patterns +- Use the same summary component structure + +--- + +## Example: Tax Field (in sidebar) +```tsx + api.taxes.list()} + mapOption={(item: any) => ({ + value: String(item.id), + label: item.title ? `${item.title} (${item.rate}%)` : `#${item.id}`, + })} + {...STORE_OBJECT} +/> +``` + +## Example: Discount Type Select (in main column) +```tsx + +``` + +## Example: Summary Card +```tsx +
+ +
+``` diff --git a/apps/dashboard/app/(authenticated)/purchase/bill/[id]/edit/page.tsx b/apps/dashboard/app/(authenticated)/purchase/bill/[id]/edit/page.tsx new file mode 100644 index 0000000..4a8c2b8 --- /dev/null +++ b/apps/dashboard/app/(authenticated)/purchase/bill/[id]/edit/page.tsx @@ -0,0 +1,42 @@ +"use client" + +import { use } from "react" +import { useQuery } from "@tanstack/react-query" +import { useAuthApi } from "@/shared/useApi" +import { useRouter } from "next/navigation" +import { BillForm } from "@/modules/bills/bill-form" +import DashboardPage from "@/base/components/layout/dashboard/dashboard-page" +import { BILL_ROUTES } from "@garage/api" + +export default function BillEditPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = use(params) + const api = useAuthApi() + const router = useRouter() + + const { data, isLoading } = useQuery({ + queryKey: [BILL_ROUTES.BY_ID, id], + queryFn: () => api.bills.show(id), + }) + + if (isLoading) { + return ( + +
+ Loading... +
+
+ ) + } + + return ( + +
+ router.push(`/purchase/bill/${id}`)} + /> +
+
+ ) +} diff --git a/apps/dashboard/app/(authenticated)/purchase/bill/[id]/layout.tsx b/apps/dashboard/app/(authenticated)/purchase/bill/[id]/layout.tsx new file mode 100644 index 0000000..035551c --- /dev/null +++ b/apps/dashboard/app/(authenticated)/purchase/bill/[id]/layout.tsx @@ -0,0 +1,43 @@ +import { DashboardDetailsPage } from '@/base/components/layout/dashboard' +import { getServerApi } from '@garage/api/server' +import { BillActions } from '@/modules/bills/bill-actions' +import { BillProvider, type BillResponse } from '@/modules/bills/bill-context' +import BillStatusBadge from '@/modules/bills/bill-status-badge' +import { ReceiptIcon } from 'lucide-react' +import React from 'react' + +export default async function BillDetailLayout(props: { + params: Promise<{ id: string }> + children: React.ReactNode +}) { + const { id } = await props.params + const api = await getServerApi() + const bill = await api.bills.show(id) + const data = bill.data as BillResponse + const title = data?.title || data?.bill_number || 'Bill Details' + return ( + + } + backHref="/purchase/bill" + actions={ +
+ + +
+ } + tabs={[ + { + href: `/purchase/bill/${id}`, + label: 'Details', + }, + ]} + > + {props.children} +
+
+ ) +} diff --git a/apps/dashboard/app/(authenticated)/purchase/bill/[id]/page.tsx b/apps/dashboard/app/(authenticated)/purchase/bill/[id]/page.tsx new file mode 100644 index 0000000..84ea18a --- /dev/null +++ b/apps/dashboard/app/(authenticated)/purchase/bill/[id]/page.tsx @@ -0,0 +1,32 @@ +import { getServerApi } from '@garage/api/server' +import { BillGeneralInfo } from '@/modules/bills/bill-general-info' +import { BillPartsSection } from '@/modules/bills/bill-parts-section' +import { BillServicesSection } from '@/modules/bills/bill-services-section' +import { BillExpensesSection } from '@/modules/bills/bill-expenses-section' +import { BillTotalsSummary } from '@/modules/bills/bill-totals-summary' +import { BillPaymentsSection } from '@/modules/bills/bill-payments-section' +import DashboardPage from '@/base/components/layout/dashboard/dashboard-page' + +export default async function BillDetailPage(props: { params: Promise<{ id: string }> }) { + const { id } = await props.params + const api = await getServerApi() + const bill = await api.bills.show(id) + const data = (bill as any)?.data ?? bill + + if (!data) { + return
Bill not found.
+ } + + return ( + +
+ + + + + + +
+
+ ) +} diff --git a/apps/dashboard/app/(authenticated)/purchase/bill/page.tsx b/apps/dashboard/app/(authenticated)/purchase/bill/page.tsx index 4204e99..38ce5c3 100644 --- a/apps/dashboard/app/(authenticated)/purchase/bill/page.tsx +++ b/apps/dashboard/app/(authenticated)/purchase/bill/page.tsx @@ -1,5 +1,6 @@ "use client" +import { useRouter } from "next/navigation" import FormDialog from "@/shared/components/form-dialog" import { Badge } from "@/shared/components/ui/badge" import { ResourcePage } from "@/shared/data-view/resource-page" @@ -7,16 +8,20 @@ import { ColumnHeader } from "@/shared/data-view/table-view" import { BillForm } from "@/modules/bills/bill-form" import { BILL_ROUTES } from "@garage/api" import type { BillsClient } from "@garage/api" +import { formatDate } from "@/shared/utils/formatters" export default function BillsPage() { + const router = useRouter() + return ( pageTitle="Bills" routeKey={BILL_ROUTES.INDEX} getClient={(api) => api.bills} + onRowClick={(row) => router.push(`/purchase/bill/${(row as any).id}`)} headerProps={({ selectedItem, invalidateQuery }) => ({ actions: ( - + {(resourceId) => ( , cell: ({ row }) => (row.original as any).bill_number || "—", }, + { + accessorKey: "total", + header: ({ column }) => , + cell: ({ row }) => (row.original as any).total || "—", + }, + { + accessorKey: "balance_due", + header: ({ column }) => , + cell: ({ row }) => (row.original as any).balance_due || "—", + }, { accessorKey: "title", header: ({ column }) => , }, { - accessorKey: "vendor_name", + accessorKey: "vendor", header: ({ column }) => , - cell: ({ row }) => (row.original as any).vendor_name || "—", + cell: ({ row }) => (row.original as any).vendor?.name || "—", }, { accessorKey: "bill_date", header: ({ column }) => , - cell: ({ row }) => { - const value = (row.original as any).bill_date - return value ? new Date(value).toLocaleDateString() : "—" - }, + cell: ({ row }) => formatDate((row.original as any).bill_date) || "—", }, { accessorKey: "bill_due_date", header: ({ column }) => , - cell: ({ row }) => { - const value = (row.original as any).bill_due_date - return value ? new Date(value).toLocaleDateString() : "—" - }, + cell: ({ row }) => formatDate((row.original as any).bill_due_date) || "—", }, { accessorKey: "status", header: ({ column }) => , cell: ({ row }) => { const status = (row.original as any).status - return {status || "—"} + return ( + + {status?.replace(/_/g, " ") || "—"} + + ) }, }, actionsColumn(), @@ -71,3 +84,4 @@ export default function BillsPage() { /> ) } + diff --git a/apps/dashboard/app/(authenticated)/purchase/expense/[id]/layout.tsx b/apps/dashboard/app/(authenticated)/purchase/expense/[id]/layout.tsx new file mode 100644 index 0000000..f44686c --- /dev/null +++ b/apps/dashboard/app/(authenticated)/purchase/expense/[id]/layout.tsx @@ -0,0 +1,38 @@ +import { DashboardDetailsPage } from '@/base/components/layout/dashboard' +import { getServerApi } from '@garage/api/server' +import { ExpenseActions } from '@/modules/expenses/expense-actions' +import { ExpenseProvider, type ExpenseContextValue } from '@/modules/expenses/expense-context' +import { ReceiptIcon } from 'lucide-react' +import React from 'react' + +export default async function ExpenseDetailLayout(props: { + params: Promise<{ id: string }> + children: React.ReactNode +}) { + const { id } = await props.params + const api = await getServerApi() + const expense = await api.expenses.getById(id) + const data = expense.data as ExpenseContextValue + const title = data?.title || data?.invoice_number || 'Expense Details' + + return ( + + } + backHref="/purchase/expense" + actions={} + tabs={[ + { + href: `/purchase/expense/${id}`, + label: 'Details', + }, + ]} + > + {props.children} + + + ) +} diff --git a/apps/dashboard/app/(authenticated)/purchase/expense/[id]/page.tsx b/apps/dashboard/app/(authenticated)/purchase/expense/[id]/page.tsx new file mode 100644 index 0000000..2ee8beb --- /dev/null +++ b/apps/dashboard/app/(authenticated)/purchase/expense/[id]/page.tsx @@ -0,0 +1,36 @@ +import DashboardPage from '@/base/components/layout/dashboard/dashboard-page' +import { ExpenseGeneralInfo } from '@/modules/expenses/expense-general-info' +import { ExpenseItemsSection } from '@/modules/expenses/expense-items-section' +import { ExpensePaymentsSection } from '@/modules/expenses/expense-payments-section' +import { getServerApi } from '@garage/api/server' + +export default async function ExpenseDetailPage(props: { params: Promise<{ id: string }> }) { + const { id } = await props.params + const api = await getServerApi() + const expense = await api.expenses.show(id) + const data = (expense as any)?.data ?? expense + + if (!data) { + return
Expense not found.
+ } + + const taxLabel = data.tax?.title ? `${data.tax.title} (${data.tax.rate}%)` : undefined + + return ( + +
+ + + +
+
+ ) +} diff --git a/apps/dashboard/app/(authenticated)/purchase/expense/page.tsx b/apps/dashboard/app/(authenticated)/purchase/expense/page.tsx index 573d092..97b1783 100644 --- a/apps/dashboard/app/(authenticated)/purchase/expense/page.tsx +++ b/apps/dashboard/app/(authenticated)/purchase/expense/page.tsx @@ -7,16 +7,23 @@ import { ExpenseForm } from "@/modules/expenses/expense-form" import { Badge } from "@/shared/components/ui/badge" import { EXPENSE_ROUTES } from "@garage/api" import type { ExpensesClient } from "@garage/api" +import { useRouter } from "next/navigation" +import { formatCurrency, formatDate, formatEnum } from "@/shared/utils/formatters" export default function ExpensesPage() { + const router = useRouter() return ( pageTitle="Expenses" routeKey={EXPENSE_ROUTES.INDEX} getClient={(api) => api.expenses} + onRowClick={(row)=>router.push(`/purchase/expense/${row.id}`)} headerProps={({ selectedItem, invalidateQuery }) => ({ actions: ( - + {(resourceId) => ( (row.original as any).invoice_number || "—", }, { - accessorKey: "vendor_name", - header: ({ column }) => , - cell: ({ row }) => (row.original as any).vendor_name || "—", + accessorKey: "vendor", + header: () => "Vendor", + cell: ({ row }) => { + const vendor = (row.original as any).vendor + return vendor?.company_name || vendor?.name || "—" + }, }, { accessorKey: "expense_date", header: ({ column }) => , - cell: ({ row }) => { - const val = (row.original as any).expense_date - return val ? new Date(val).toLocaleDateString() : "—" - }, + cell: ({ row }) => formatDate((row.original as any).expense_date), + }, + { + accessorKey: "total", + header: () => "Total", + cell: ({ row }) => formatCurrency((row.original as any).total ?? 0), + }, + { + accessorKey: "balance_due", + header: () => "Balance Due", + cell: ({ row }) => formatCurrency((row.original as any).balance_due ?? 0), }, { accessorKey: "status", header: ({ column }) => , cell: ({ row }) => { const status = (row.original as any).status + const variantMap: Record = { + draft: "secondary", + open: "default", + un_paid: "destructive", + partially_paid: "secondary", + paid: "default", + } return ( - - {status || "—"} + + {formatEnum(status) || "—"} ) }, }, - { - accessorKey: "created_at", - header: ({ column }) => , - cell: ({ row }) => { - const val = (row.original as any).created_at - return val ? new Date(val).toLocaleDateString() : "—" - }, - }, actionsColumn(), ]} /> diff --git a/apps/dashboard/app/(authenticated)/purchase/purchase-order/page.tsx b/apps/dashboard/app/(authenticated)/purchase/purchase-order/page.tsx index 94911ec..9f11930 100644 --- a/apps/dashboard/app/(authenticated)/purchase/purchase-order/page.tsx +++ b/apps/dashboard/app/(authenticated)/purchase/purchase-order/page.tsx @@ -19,12 +19,12 @@ export default function PurchaseOrdersPage() { onRowClick={(row) => router.push(`/purchase/purchase-order/${(row as any).id}`)} headerProps={({ selectedItem, invalidateQuery }) => ({ actions: ( - - {(resourceId) => ( + + {(resourceId, { close }) => ( { invalidateQuery(); close()}} /> )} diff --git a/apps/dashboard/app/(authenticated)/sales/estimates/[id]/layout.tsx b/apps/dashboard/app/(authenticated)/sales/estimates/[id]/layout.tsx index a46d74a..5bf62dd 100644 --- a/apps/dashboard/app/(authenticated)/sales/estimates/[id]/layout.tsx +++ b/apps/dashboard/app/(authenticated)/sales/estimates/[id]/layout.tsx @@ -3,6 +3,7 @@ import { getServerApi } from '@garage/api/server' import { EstimateActions } from '@/modules/estimates/estimate-actions' import { EstimateProvider } from '@/modules/estimates/estimate-context' import { CreateInvoiceFromEstimateButton } from '@/modules/estimates/create-invoice-from-estimate-button' +import { CreateJobCardFromEstimateButton } from '@/modules/estimates/create-job-card-from-estimate-button' import { FileTextIcon } from 'lucide-react' import React from 'react' @@ -12,9 +13,9 @@ export default async function layout(props: { }) { const { id } = await props.params const api = await getServerApi() - const estimate = await api.estimates.getById(id) + const estimate = await api.estimates.show(id) - const estimateData = (estimate as any)?.data + const estimateData = estimate?.data const title = estimateData?.title || estimateData?.estimate_number || `Estimate #${id}` const estimateLabel = estimateData?.estimate_number ? `${estimateData.estimate_number}${estimateData.title ? ` — ${estimateData.title}` : ''}` @@ -33,6 +34,7 @@ export default async function layout(props: { actions={
+
} diff --git a/apps/dashboard/app/(authenticated)/sales/estimates/[id]/page.tsx b/apps/dashboard/app/(authenticated)/sales/estimates/[id]/page.tsx index ff5a616..24b9499 100644 --- a/apps/dashboard/app/(authenticated)/sales/estimates/[id]/page.tsx +++ b/apps/dashboard/app/(authenticated)/sales/estimates/[id]/page.tsx @@ -8,9 +8,9 @@ import DashboardPage from '@/base/components/layout/dashboard/dashboard-page' export default async function page(props: { params: Promise<{ id: string }> }) { const { id } = await props.params const api = await getServerApi() - const estimate = await api.estimates.getById(id) + const estimate = await api.estimates.show(id) - const estimateData = (estimate as any)?.data + const estimateData = estimate?.data if (!estimateData) { return
Estimate not found.
diff --git a/apps/dashboard/app/(authenticated)/sales/invoice/[id]/layout.tsx b/apps/dashboard/app/(authenticated)/sales/invoice/[id]/layout.tsx index 7f2322b..5f49848 100644 --- a/apps/dashboard/app/(authenticated)/sales/invoice/[id]/layout.tsx +++ b/apps/dashboard/app/(authenticated)/sales/invoice/[id]/layout.tsx @@ -4,6 +4,7 @@ import { InvoiceActions } from '@/modules/invoices/invoice-actions' import { InvoiceProvider } from '@/modules/invoices/invoice-context' import { ReceiptIcon } from 'lucide-react' import React from 'react' +import InvoiceStatusBadge from '@/modules/invoices/invoice-status-badge' export default async function InvoiceDetailLayout(props: { params: Promise<{ id: string }>, children: React.ReactNode }) { const { id } = await props.params @@ -13,19 +14,26 @@ export default async function InvoiceDetailLayout(props: { params: Promise<{ id: const title = data?.subject || data?.invoice_number || 'Invoice Details' return ( - + } backHref="/sales/invoice" - actions={} + actions={ +
+ + + +
+ } tabs={[ { href: `/sales/invoice/${id}`, label: 'Details' }, + { href: `/sales/invoice/${id}/documents`, label: 'Documents' diff --git a/apps/dashboard/app/(authenticated)/sales/invoice/[id]/page.tsx b/apps/dashboard/app/(authenticated)/sales/invoice/[id]/page.tsx index 9a51427..dae1700 100644 --- a/apps/dashboard/app/(authenticated)/sales/invoice/[id]/page.tsx +++ b/apps/dashboard/app/(authenticated)/sales/invoice/[id]/page.tsx @@ -3,7 +3,10 @@ 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 { InvoiceTotalsSummary } from '@/modules/invoices/invoice-totals-summary' import DashboardPage from '@/base/components/layout/dashboard/dashboard-page' +import InvoicePaymentsSection from '@/modules/invoices/invoice-payments-section' + export default async function InvoiceDetailPage(props: { params: Promise<{ id: string }> }) { const { id } = await props.params @@ -18,10 +21,12 @@ export default async function InvoiceDetailPage(props: { params: Promise<{ id: s return (
- + + +
) diff --git a/apps/dashboard/app/(authenticated)/sales/invoice/page.tsx b/apps/dashboard/app/(authenticated)/sales/invoice/page.tsx index ae182e7..c655779 100644 --- a/apps/dashboard/app/(authenticated)/sales/invoice/page.tsx +++ b/apps/dashboard/app/(authenticated)/sales/invoice/page.tsx @@ -30,7 +30,7 @@ export default function InvoicesPage() { onRowClick={(row) => router.push(`/sales/invoice/${(row as any).id}`)} headerProps={({ selectedItem, invalidateQuery }) => ({ actions: ( - + {(resourceId) => ( +}) { + const { id: jobCardId } = use(params) + const api = useAuthApi() + const queryClient = useQueryClient() + const router = useRouter() + const queryKey = ["job-card-expense-items", jobCardId] + + const [dialogOpen, setDialogOpen] = useState(false) + const [editItem, setEditItem] = useState(null) + + const { data, isLoading } = useQuery({ + queryKey, + queryFn: () => api.jobCards.getExpenseItems(jobCardId), + }) + + const rows = (data as any)?.data ?? [] + + const invalidate = () => queryClient.invalidateQueries({ queryKey }).then(() => router.refresh()) + + async function handleDelete(row: any) { + const confirmed = await confirm({ + title: "Delete this expense item?", + description: `Remove "${row.expense_item?.item_name ?? "this expense item"}" from the job card?`, + }) + if (!confirmed) return + const promise = api.jobCards.deleteExpenseItem(jobCardId, row.id) + toast.promise(promise, { + loading: "Deleting...", + success: "Expense item deleted", + error: "Failed to delete expense item", + }) + await promise + invalidate() + } + + const columns: ColumnDef[] = [ + { + accessorKey: "expense_item.item_name", + header: ({ column }) => , + cell: ({ row }) => { + const item = row.original.expense_item + return item ? ( +
+ {item.item_name} + {item.sku && ( + {item.sku} + )} +
+ ) : "—" + }, + }, + { + accessorKey: "quantity", + header: ({ column }) => , + cell: ({ row }) => row.original.quantity ?? "—", + }, + { + accessorKey: "rate", + header: ({ column }) => , + cell: ({ row }) => { + const val = row.original.rate + return val != null ? `$${Number(val).toFixed(2)}` : "—" + }, + }, + { + accessorKey: "discount_amount", + header: ({ column }) => , + cell: ({ row }) => { + const val = row.original.discount_amount + return val != null && val > 0 ? `$${Number(val).toFixed(2)}` : "—" + }, + }, + { + accessorKey: "tax_id", + header: ({ column }) => , + cell: ({ row }) => row.original.tax_id ?? "—", + }, + { + accessorKey: "department.name", + header: ({ column }) => , + cell: ({ row }) => row.original.department?.name || "—", + }, + { + accessorKey: "description", + header: ({ column }) => , + cell: ({ row }) => row.original.description || "—", + }, + { + accessorKey: "created_at", + header: ({ column }) => , + cell: ({ row }) => formatDate(row.original.created_at), + }, + { + id: "actions", + cell: ({ row }) => ( + + + + + + { + setEditItem(row.original) + setDialogOpen(true) + }} + > + Edit + + handleDelete(row.original)} + > + Delete + + + + ), + }, + ] + + return ( +
+
+ { + setDialogOpen(open) + if (!open) setEditItem(null) + }} + > + + + + + + {editItem ? "Edit Expense Item" : "Add Expense Item"} + + { + setDialogOpen(false) + setEditItem(null) + invalidate() + }} + onCancel={() => { + setDialogOpen(false) + setEditItem(null) + }} + /> + + +
+ + +
+ ) +} diff --git a/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/expenses/page.tsx b/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/expenses/page.tsx deleted file mode 100644 index db5647f..0000000 --- a/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/expenses/page.tsx +++ /dev/null @@ -1,92 +0,0 @@ -"use client" - -import { use } from "react" -import { ResourcePage } from "@/shared/data-view/resource-page" -import { ColumnHeader } from "@/shared/data-view/table-view" -import FormDialog from "@/shared/components/form-dialog" -import { ExpenseForm } from "@/modules/expenses/expense-form" -import { Badge } from "@/shared/components/ui/badge" -import { EXPENSE_ROUTES } from "@garage/api" -import type { ExpensesClient } from "@garage/api" -import { useJobCard } from "@/modules/job-cards/job-card-context" - -export default function JobCardExpensesPage({ - params, -}: { - params: Promise<{ id: string }> -}) { - const { id: jobCardId } = use(params) - const jobCard = useJobCard() - - const defaultJobCard = jobCard - ? { value: String((jobCard as any).id), label: (jobCard as any).label || (jobCard as any).title || `Job Card` } - : null - - return ( - - routeKey={EXPENSE_ROUTES.INDEX} - getClient={(api) => api.expenses} - extraParams={{ job_card_id: jobCardId }} - header={null} - tableHeader={({ invalidateQuery, selectedItem, closeDialog }) => ( -
- - - {(resourceId) => ( - { closeDialog(); invalidateQuery() }} - /> - )} - -
- )} - columns={({ actionsColumn }) => [ - { - accessorKey: "title", - header: ({ column }) => , - }, - { - accessorKey: "invoice_number", - header: ({ column }) => , - cell: ({ row }) => (row.original as any).invoice_number || "—", - }, - { - accessorKey: "vendor_name", - header: ({ column }) => , - cell: ({ row }) => (row.original as any).vendor_name || "—", - }, - { - accessorKey: "expense_date", - header: ({ column }) => , - cell: ({ row }) => { - const val = (row.original as any).expense_date - return val ? new Date(val).toLocaleDateString() : "—" - }, - }, - { - accessorKey: "status", - header: ({ column }) => , - cell: ({ row }) => { - const status = (row.original as any).status - return ( - - {status || "—"} - - ) - }, - }, - { - accessorKey: "created_at", - header: ({ column }) => , - cell: ({ row }) => { - const val = (row.original as any).created_at - return val ? new Date(val).toLocaleDateString() : "—" - }, - }, - actionsColumn(), - ]} - /> - ) -} diff --git a/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/layout.tsx b/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/layout.tsx index 5e65617..07da768 100644 --- a/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/layout.tsx +++ b/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/layout.tsx @@ -12,14 +12,14 @@ export default async function JobCardDetailLayout(props: { params: Promise<{ id: const { id } = await props.params const api = await getServerApi() - const jobCard = await api.jobCards.show(id).then(res => res.data) + const jobCard:any = await api.jobCards.show(id).then(res => res.data) const title = jobCard?.title || 'Job Card Details' const status = jobCard?.status || 'draft' const docs = jobCard?.documents return ( - + {props.children} diff --git a/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/page.tsx b/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/page.tsx index 9f13a6e..99cfd93 100644 --- a/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/page.tsx +++ b/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/page.tsx @@ -1,19 +1,20 @@ import { getServerApi } from '@garage/api/server' import { JobCardGeneralInfo } from '@/modules/job-cards/job-card-general-info' import DashboardPage from '@/base/components/layout/dashboard/dashboard-page' +import type { JobCardShowData } from '@garage/api' export default async function JobCardDetailPage(props: { params: Promise<{ id: string }> }) { const { id } = await props.params const api = await getServerApi() - const jobCard = await api.jobCards.show(id) - const data = (jobCard as any)?.data ?? jobCard + const response = await api.jobCards.show(id) + const data = response.data if (!data) { return
Job card not found.
} return ( - + ) diff --git a/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/parts/page.tsx b/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/parts/page.tsx index d336255..5a30e0c 100644 --- a/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/parts/page.tsx +++ b/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/parts/page.tsx @@ -1,6 +1,7 @@ "use client" import { use, useState } from "react" +import { useRouter } from "next/navigation" import { useQuery, useQueryClient } from "@tanstack/react-query" import { useAuthApi } from "@/shared/useApi" import { ColumnHeader, DataTable } from "@/shared/data-view/table-view" @@ -33,6 +34,7 @@ export default function JobCardPartsPage({ const { id: jobCardId } = use(params) const api = useAuthApi() const queryClient = useQueryClient() + const router = useRouter() const queryKey = ["job-card-parts", jobCardId] const [dialogOpen, setDialogOpen] = useState(false) @@ -45,7 +47,7 @@ export default function JobCardPartsPage({ const rows = (data as any)?.data ?? [] - const invalidate = () => queryClient.invalidateQueries({ queryKey }) + const invalidate = () => queryClient.invalidateQueries({ queryKey }).then(() => router.refresh()) async function handleDelete(row: any) { const confirmed = await confirm({ @@ -93,9 +95,17 @@ export default function JobCardPartsPage({ }, }, { - accessorKey: "tax", + accessorKey: "discount_amount", + header: ({ column }) => , + cell: ({ row }) => { + const val = row.original.discount_amount + return val != null && val > 0 ? `$${Number(val).toFixed(2)}` : "—" + }, + }, + { + accessorKey: "tax_id", header: ({ column }) => , - cell: ({ row }) => row.original.tax || "—", + cell: ({ row }) => row.original.tax_id ?? "—", }, { accessorKey: "department.name", diff --git a/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/services/page.tsx b/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/services/page.tsx index a84ce6a..b744122 100644 --- a/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/services/page.tsx +++ b/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/services/page.tsx @@ -1,6 +1,7 @@ "use client" import { use, useState } from "react" +import { useRouter } from "next/navigation" import { useQuery, useQueryClient } from "@tanstack/react-query" import { useAuthApi } from "@/shared/useApi" import { ColumnHeader, DataTable } from "@/shared/data-view/table-view" @@ -26,6 +27,8 @@ import { JobCardServiceForm } from "@/modules/job-cards/job-card-service-form" import { formatDate } from "@/shared/utils/formatters" import { Badge } from "@/shared/components/ui/badge" +// TODO: services invalidation is not working properly when create new service line. Need to investigate why and fix it. + export default function JobCardServicesPage({ params, }: { @@ -34,6 +37,7 @@ export default function JobCardServicesPage({ const { id: jobCardId } = use(params) const api = useAuthApi() const queryClient = useQueryClient() + const router = useRouter() const queryKey = ["job-card-services", jobCardId] const [dialogOpen, setDialogOpen] = useState(false) @@ -46,7 +50,7 @@ export default function JobCardServicesPage({ const rows = (data as any)?.data ?? [] - const invalidate = () => queryClient.invalidateQueries({ queryKey }) + const invalidate = () => queryClient.invalidateQueries({ queryKey }).then(() => router.refresh()) async function handleDelete(row: any) { const confirmed = await confirm({ @@ -125,9 +129,17 @@ export default function JobCardServicesPage({ cell: ({ row }) => row.original.labor_hours ?? "—", }, { - accessorKey: "tax", + accessorKey: "discount_amount", + header: ({ column }) => , + cell: ({ row }) => { + const val = row.original.discount_amount + return val != null && val > 0 ? `$${Number(val).toFixed(2)}` : "—" + }, + }, + { + accessorKey: "tax_id", header: ({ column }) => , - cell: ({ row }) => row.original.tax || "—", + cell: ({ row }) => row.original.tax_id ?? "—", }, { accessorKey: "department.name", diff --git a/apps/dashboard/app/(authenticated)/settings/company/page.tsx b/apps/dashboard/app/(authenticated)/settings/company/page.tsx index 9aece74..a485bbe 100644 --- a/apps/dashboard/app/(authenticated)/settings/company/page.tsx +++ b/apps/dashboard/app/(authenticated)/settings/company/page.tsx @@ -1,17 +1,17 @@ "use client" +import DashboardPage from "@/base/components/layout/dashboard/dashboard-page" import { SettingsForm } from "@/modules/settings/company/settings-form" export default function CompanySettingsPage() { return ( -
+
-

Company Settings

Manage your workshop profile, contact details, and preferences.

-
+
) } diff --git a/apps/dashboard/config/navGroups.tsx b/apps/dashboard/config/navGroups.tsx index db9c701..13159b6 100644 --- a/apps/dashboard/config/navGroups.tsx +++ b/apps/dashboard/config/navGroups.tsx @@ -75,7 +75,7 @@ export const navGroups: NavGroup[] = [ href: "/calendars", icon: , items: [ - { title: "Work Schedule", href: "/calendar/work-schedule/list", icon: }, + // { title: "Work Schedule", href: "/calendar/work-schedule/list", icon: }, { title: "Appointments", href: "/calendar/appointment/list", icon: }, ], }, @@ -144,10 +144,10 @@ export const navGroups: NavGroup[] = [ { title: "Employees", href: "/productivity/employees", icon: }, { title: "Time Clocks", href: "/productivity/time-clocks", icon: }, { title: "Time Sheets", href: "/productivity/timesheet", icon: }, - { title: "Payroll", href: "/productivity/payroll", icon: }, - { title: "Payments Made", href: "/productivity/employee-payments-made", icon: }, - { title: "Shop Calendars", href: "/productivity/shop-calendars", icon: }, - { title: "Shop Timing", href: "/productivity/shop-timings", icon: }, + // { title: "Payroll", href: "/productivity/payroll", icon: }, + // { title: "Payments Made", href: "/productivity/employee-payments-made", icon: }, + // { title: "Shop Calendars", href: "/productivity/shop-calendars", icon: }, + // { title: "Shop Timing", href: "/productivity/shop-timings", icon: }, { title: "Holidays", href: "/productivity/holidays", icon: }, { title: "Tasks", href: "/productivity/tasks", icon: }, ], @@ -161,7 +161,7 @@ export const navGroups: NavGroup[] = [ { title: "Parts", href: "/items/parts", icon: }, { title: "Expense Item", href: "/items/expense-item", icon: }, { title: "Service Group", href: "/items/service-group", icon: }, - { title: "Inspections", href: "/items/inspection", icon: }, + // { title: "Inspections", href: "/items/inspection", icon: }, { title: "Inventory Adjustments", href: "/items/adjustment", icon: }, ], }, @@ -177,9 +177,9 @@ export const navGroups: NavGroup[] = [ { title: "Tax & Rates", href: "/settings/tax-rates", icon: }, { title: "Make & Models", href: "/settings/make-and-models", icon: }, { title: "Configurations", href: "/settings/configurations/preferences/sales", icon: }, - { title: "Templates", href: "/settings/templates", icon: }, - { title: "Integrations", href: "/settings/integrations/providers", icon: }, - { title: "Master", href: "/settings/master/body-type", icon: }, + // { title: "Templates", href: "/settings/templates", icon: }, + // { title: "Integrations", href: "/settings/integrations/providers", icon: }, + // { title: "Master", href: "/settings/master/body-type", icon: }, ], }, ], diff --git a/apps/dashboard/modules/bills/bill-actions.tsx b/apps/dashboard/modules/bills/bill-actions.tsx new file mode 100644 index 0000000..efbfdf1 --- /dev/null +++ b/apps/dashboard/modules/bills/bill-actions.tsx @@ -0,0 +1,50 @@ +"use client" + +import { useAuthApi } from "@/shared/useApi" +import { useRouter } from "next/navigation" +import { Button } from "@/shared/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/shared/components/ui/dropdown-menu" +import { Ellipsis, Pencil, Trash2 } from "lucide-react" + +type BillActionsProps = { + billId: string +} + +export function BillActions({ billId }: BillActionsProps) { + const api = useAuthApi() + const router = useRouter() + + const handleEdit = () => { + router.push(`/purchase/bill/${billId}/edit`) + } + + const handleDelete = async () => { + await api.bills.destroy(billId) + router.push("/purchase/bill") + } + + return ( + + + + + + + + Edit + + + + Delete + + + + ) +} diff --git a/apps/dashboard/modules/bills/bill-context.tsx b/apps/dashboard/modules/bills/bill-context.tsx new file mode 100644 index 0000000..6c1a210 --- /dev/null +++ b/apps/dashboard/modules/bills/bill-context.tsx @@ -0,0 +1,75 @@ +"use client" + +import { createContext, useContext } from "react" + +export type BillVendor = { + id?: number | null + first_name?: string | null + last_name?: string | null + company_name?: string | null + name?: string | null +} + +export type BillResponse = { + id?: number | null + title?: string | null + job_card_id?: number | null + vendor_id?: number | null + vendor_address_id?: number | null + purchase_order_id?: number | null + bill_number?: string | null + bill_date?: string | null + bill_due_date?: string | null + payment_terms_id?: number | null + department_id?: number | null + notes?: string | null + status?: string | null + discount_type?: string | null + tax_id?: number | null + sub_total?: number | null + tax_amount?: number | null + total?: number | null + payments_made?: number | null + balance_due?: number | null + discount_amount_major?: number | null + created_at?: string | null + updated_at?: string | null + vendor?: BillVendor | null + vendor_address?: { + id?: number | null + address?: string | null + country?: { id?: number; name?: string } | null + state?: { id?: number; name?: string } | null + } | null + department?: { id?: number | null; name?: string | null } | null + job_card?: { id?: number | null; order_number?: string | null; estimate_number?: string | null } | null + purchase_order?: { id?: number | null; order_number?: string | null } | null + tax?: { id?: number | null; name?: string | null; rate?: string | null } | null + payment_terms?: { id?: number | null; name?: string | null } | null + labels?: { id?: number; title?: string; color_code?: string }[] + parts?: any[] + services?: any[] + expenses?: any[] +} + +type BillContextValue = BillResponse + +const BillContext = createContext(null) + +export function BillProvider({ + bill, + children, +}: { + bill: BillContextValue + children: React.ReactNode +}) { + return ( + + {children} + + ) +} + +export function useBill() { + return useContext(BillContext) +} diff --git a/apps/dashboard/modules/bills/bill-expenses-section.tsx b/apps/dashboard/modules/bills/bill-expenses-section.tsx new file mode 100644 index 0000000..290bbc4 --- /dev/null +++ b/apps/dashboard/modules/bills/bill-expenses-section.tsx @@ -0,0 +1,80 @@ +"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 BillExpense = { + id: number + bill_id: number + expense_id: number + quantity: string | number + rate: string | number + description?: string + expense?: { id?: number; title?: string; invoice_number?: string } +} + +type BillExpensesSectionProps = { + expenses?: BillExpense[] +} + +export function BillExpensesSection({ expenses = [] }: BillExpensesSectionProps) { + 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 ( + + + + + Expenses + + + +
+ + + + Expense + Description + Quantity + Rate + Amount + + + + {expenses.map((expense) => { + const qty = parseFloat(String(expense.quantity)) + const rate = parseFloat(String(expense.rate)) + const amount = qty * rate + return ( + + + {expense.expense?.title || `Expense #${expense.expense_id}`} + + + {expense.description || "—"} + + {formatNumber(qty)} + {formatCurrency(rate)} + {formatCurrency(amount)} + + ) + })} + + Subtotal + {formatCurrency(subtotal)} + + +
+
+
+
+ ) +} diff --git a/apps/dashboard/modules/bills/bill-form-summary.tsx b/apps/dashboard/modules/bills/bill-form-summary.tsx new file mode 100644 index 0000000..5a4900e --- /dev/null +++ b/apps/dashboard/modules/bills/bill-form-summary.tsx @@ -0,0 +1,97 @@ +"use client" + +import { useFormContext } from "react-hook-form" + +import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" +import { + useDocumentTotals, + type DocumentLineItem, +} from "@/shared/hooks/use-document-totals" +import { DocumentTotalsSummary } from "@/shared/components/document-totals-summary" + +import type { BillFormValues } from "./bill.schema" + +function parseTaxRateFromLabel(label: string | undefined | null): number | undefined { + if (!label) return undefined + const match = label.match(/\((\d+(?:\.\d+)?)%\)/) + return match ? Number(match[1]) : undefined +} + +export function BillFormSummary() { + const { watch } = useFormContext() + + const partItems = watch("part_items") ?? [] + const serviceItems = watch("service_items") ?? [] + const expenseItems = watch("expense_items") ?? [] + const discountType = watch("discount") + const discountAmount = watch("discount_amount") + const taxRelation = watch("tax") + + const taxRate = parseTaxRateFromLabel(taxRelation?.label) + const taxLabel = taxRelation?.label ?? "Tax" + + const lineItems: DocumentLineItem[] = [ + ...partItems.map((p) => ({ + quantity: p.quantity, + rate: p.rate, + discount_amount: p.discount_amount, + })), + ...serviceItems.map((s) => ({ + quantity: s.quantity, + rate: s.rate, + discount_amount: s.discount_amount, + })), + ...expenseItems.map((e) => ({ + quantity: e.quantity, + rate: e.rate, + discount_amount: e.discount_amount, + })), + ] + + const groupLabels: Record = {} + if (partItems.length > 0) { + groupLabels[`Parts (${partItems.length})`] = partItems.reduce( + (s, p) => s + (Number(p.quantity) || 0) * (Number(p.rate) || 0), + 0, + ) + } + if (serviceItems.length > 0) { + groupLabels[`Services (${serviceItems.length})`] = serviceItems.reduce( + (s, sv) => s + (Number(sv.quantity) || 0) * (Number(sv.rate) || 0), + 0, + ) + } + if (expenseItems.length > 0) { + groupLabels[`Expenses (${expenseItems.length})`] = expenseItems.reduce( + (s, e) => s + (Number(e.quantity) || 0) * (Number(e.rate) || 0), + 0, + ) + } + + const totals = useDocumentTotals({ + lineItems, + discountType, + discountAmount, + taxRate, + }) + + if (!totals.hasLineItems) return null + + return ( + + + + Summary + + + + 1 ? groupLabels : undefined} + /> + + + ) +} diff --git a/apps/dashboard/modules/bills/bill-form.tsx b/apps/dashboard/modules/bills/bill-form.tsx index 5d0c0c2..c00b8ef 100644 --- a/apps/dashboard/modules/bills/bill-form.tsx +++ b/apps/dashboard/modules/bills/bill-form.tsx @@ -5,24 +5,39 @@ import { AlertTriangle, Plus, Save } from "lucide-react" import { Button } from "@/shared/components/ui/button" import { Alert, AlertTitle } from "@/shared/components/ui/alert" import { FieldGroup } from "@/shared/components/ui/field" +import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" import { Rhform, RhfAsyncSelectField, RhfSelectField, RhfTextField, RhfTextareaField, + RhfAutoGenerateField, + RhfDateField, } from "@/shared/components/form" import { useResourceForm } from "@/shared/hooks/use-resource-form" import { useFormMutation } from "@/shared/hooks/use-form-mutation" -import { toId, toRelation } from "@/shared/lib/utils" +import { getTodayDate, toId, toRelation } from "@/shared/lib/utils" import { useAuthApi } from "@/shared/useApi" -import { BillStatus, BILL_ROUTES, DEPARTMENT_ROUTES, JOB_CARD_ROUTES, PAYMENT_TERM_ROUTES, PURCHASE_ORDER_ROUTES, VENDOR_ROUTES } from "@garage/api" +import { + BillStatus, + DiscountType, + BILL_ROUTES, + DEPARTMENT_ROUTES, + JOB_CARD_ROUTES, + PAYMENT_TERM_ROUTES, + PURCHASE_ORDER_ROUTES, + VENDOR_ROUTES, + TAX_ROUTES, +} from "@garage/api" import { toast } from "sonner" 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" +import { useFormContext } from "react-hook-form" import { billFormSchema, type BillFormValues } from "./bill.schema" +import { BillFormSummary } from "./bill-form-summary" export type BillFormProps = { resourceId?: string | null @@ -31,16 +46,20 @@ export type BillFormProps = { } const DEFAULT_VALUES: BillFormValues = { + title: "", vendor: null, + vendor_address: null, purchase_order: null, job_card: null, payment_term: null, department: null, - title: "", + tax: null, bill_number: "", - bill_date: "", + bill_date: getTodayDate(), bill_due_date: "", status: "draft", + discount: "no", + discount_amount: undefined, notes: "", part_items: [], service_items: [], @@ -49,12 +68,17 @@ const DEFAULT_VALUES: BillFormValues = { const STATUS_OPTIONS = BillStatus.map((value) => ({ value, - label: value.replaceAll("_", " "), + label: value.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()), +})) + +const DISCOUNT_OPTIONS = DiscountType.map((value) => ({ + value, + label: value.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()), })) const mapLookupOption = (item: any) => ({ value: String(item.id), - label: item.name ?? item.title ?? item.bill_number ?? `#${item.id}`, + label: item.name ?? item.title ?? `#${item.id}`, }) const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label } @@ -63,36 +87,43 @@ function mapToFormValues(data: unknown): BillFormValues { const d = (data as any)?.data ?? data ?? {} return { - vendor: toRelation(d.vendor_id, d.vendor_name), - purchase_order: toRelation(d.purchase_order_id, d.purchase_order_number ?? d.purchase_order_title), - job_card: toRelation(d.job_card_id, d.job_card_number ?? d.job_card_name), - payment_term: toRelation(d.payment_terms_id, d.payment_terms_name), - department: toRelation(d.department_id, d.department_name), title: d.title || "", + vendor: toRelation(d.vendor_id, d.vendor?.name ?? d.vendor_name), + vendor_address: toRelation(d.vendor_address_id, d.vendor_address?.address), + purchase_order: toRelation(d.purchase_order_id, d.purchase_order?.order_number ?? d.purchase_order_number), + job_card: toRelation(d.job_card_id, d.job_card?.order_number ?? d.job_card_number), + payment_term: toRelation(d.payment_terms_id, d.payment_terms?.name ?? d.payment_terms_name), + department: toRelation(d.department_id, d.department?.name ?? d.department_name), + tax: toRelation(d.tax_id, d.tax?.name ? `${d.tax.name} (${d.tax.rate}%)` : d.tax_title), bill_number: d.bill_number || "", - bill_date: d.bill_date || "", - bill_due_date: d.bill_due_date || "", + bill_date: d.bill_date ? d.bill_date.split("T")[0] : "", + bill_due_date: d.bill_due_date ? d.bill_due_date.split("T")[0] : "", status: d.status || "draft", + discount: d.discount_type || "no", + discount_amount: d.discount_amount != null ? Number(d.discount_amount) : undefined, notes: d.notes || "", part_items: (d.parts ?? []).map((p: any) => ({ part_id: p.part_id ?? p.id, - title: p.part?.title ?? p.title ?? "", + title: p.part?.name ?? p.part_name ?? p.title ?? "", quantity: Number(p.quantity) || 1, rate: Number(p.rate) || 0, + discount_amount: p.discount_amount != null ? Number(p.discount_amount) : undefined, description: p.description ?? "", })), service_items: (d.services ?? []).map((s: any) => ({ service_id: s.service_id ?? s.id, - title: s.service?.labor_name ?? s.labor_name ?? s.title ?? "", + title: s.service?.name ?? s.service_name ?? s.title ?? "", quantity: Number(s.quantity) || 1, rate: Number(s.rate) || 0, + discount_amount: s.discount_amount != null ? Number(s.discount_amount) : undefined, description: s.description ?? "", })), expense_items: (d.expenses ?? []).map((e: any) => ({ expense_id: e.expense_id ?? e.id, - title: e.expense?.item_name ?? e.item_name ?? e.title ?? "", + title: e.expense?.title ?? e.expense_title ?? e.title ?? "", quantity: Number(e.quantity) || 1, rate: Number(e.rate) || 0, + discount_amount: e.discount_amount != null ? Number(e.discount_amount) : undefined, description: e.description ?? "", })), } @@ -102,36 +133,61 @@ function mapFormToPayload(values: BillFormValues) { return { title: values.title, vendor_id: toId(values.vendor), + vendor_address_id: toId(values.vendor_address), purchase_order_id: toId(values.purchase_order), job_card_id: toId(values.job_card), payment_terms_id: toId(values.payment_term), department_id: toId(values.department), + tax_id: toId(values.tax) ? Number(toId(values.tax)) : undefined, bill_number: values.bill_number || undefined, bill_date: values.bill_date || undefined, bill_due_date: values.bill_due_date || undefined, status: values.status || undefined, + discount_type: values.discount || undefined, + discount_amount: values.discount === "transaction_level" ? (values.discount_amount ?? 0) : undefined, notes: values.notes || undefined, part_items: (values.part_items ?? []).map((item) => ({ part_id: item.part_id, quantity: item.quantity, rate: item.rate, + discount_amount: values.discount === "line_item_level" ? (item.discount_amount ?? 0) : undefined, description: item.description || undefined, })), service_items: (values.service_items ?? []).map((item) => ({ service_id: item.service_id, quantity: item.quantity, rate: item.rate, + discount_amount: values.discount === "line_item_level" ? (item.discount_amount ?? 0) : undefined, description: item.description || undefined, })), expense_items: (values.expense_items ?? []).map((item) => ({ expense_id: item.expense_id, quantity: item.quantity, rate: item.rate, + discount_amount: values.discount === "line_item_level" ? (item.discount_amount ?? 0) : undefined, description: item.description || undefined, })), } } +// ── Transaction-level discount field (conditional) ── + +function TransactionDiscountField() { + const { watch } = useFormContext() + const discount = watch("discount") + if (discount !== "transaction_level") return null + return ( + + ) +} + +// ── Component ── + export function BillForm({ resourceId, initialData, onSuccess }: BillFormProps) { const api = useAuthApi() @@ -140,22 +196,24 @@ export function BillForm({ resourceId, initialData, onSuccess }: BillFormProps) defaultValues: DEFAULT_VALUES, resourceId, initialData, + queryKey: [BILL_ROUTES.BY_ID, resourceId], mapToFormValues, }) + const discount = form.watch("discount") + const isLineItemDiscount = discount === "line_item_level" + const { mutate, error, isPending } = useFormMutation(form, { mutationFn: (values: BillFormValues) => { const payload = mapFormToPayload(values) - const promise = isEditing && resourceId + const promise = (isEditing && resourceId ? api.bills.update(resourceId, payload) - : api.bills.create(payload) - + : api.bills.create(payload)) as Promise toast.promise(promise, { loading: isEditing ? "Updating bill..." : "Creating bill...", success: isEditing ? "Bill updated successfully" : "Bill created successfully", error: isEditing ? "Failed to update bill" : "Failed to create bill", }) - return promise }, onSuccess: () => { @@ -169,102 +227,141 @@ export function BillForm({ resourceId, initialData, onSuccess }: BillFormProps) {error && ( - {isEditing ? "Failed to update bill" : "Failed to create bill"} + + {isEditing ? "Failed to update bill" : "Failed to create bill"} + {error.message} )} - - +
-
- - + {/* ── Main column (9/12) ── */} +
+ + + +
+ + +
+ +
+ +
+ + + + name="part_items" showDiscount={isLineItemDiscount} /> + name="service_items" showDiscount={isLineItemDiscount} /> + name="expense_items" showDiscount={isLineItemDiscount} /> + + + + +
-
- - + {/* ── Sidebar column (3/12) ── */} +
+ + + Details + + + +
+ + +
+ + api.vendors.list()} + mapOption={mapLookupOption} + {...STORE_OBJECT} + /> + + api.taxes.list()} + mapOption={(item: any) => ({ + value: String(item.id), + label: item.title ? `${item.title} (${item.rate}%)` : `#${item.id}`, + })} + {...STORE_OBJECT} + /> + + api.departments.list()} + mapOption={mapLookupOption} + {...STORE_OBJECT} + /> + + api.paymentTerms.list()} + mapOption={mapLookupOption} + {...STORE_OBJECT} + /> + + api.jobCards.list()} + mapOption={(item: any) => ({ + value: String(item.id), + label: item.order_number || item.estimate_number || `#${item.id}`, + })} + {...STORE_OBJECT} + /> + + api.purchaseOrders.list()} + mapOption={(item: any) => ({ + value: String(item.id), + label: item.order_number || item.title || `#${item.id}`, + })} + {...STORE_OBJECT} + /> +
+
+
+ +
+ +
-
- api.vendors.list()} - mapOption={mapLookupOption} - {...STORE_OBJECT} - /> - api.departments.list()} - mapOption={mapLookupOption} - {...STORE_OBJECT} - /> -
- -
- api.jobCards.list()} - mapOption={(item: any) => ({ value: String(item.id), label: item.job_card_number || item.name || `#${item.id}` })} - {...STORE_OBJECT} - /> - api.paymentTerms.list()} - mapOption={mapLookupOption} - {...STORE_OBJECT} - /> -
- - api.purchaseOrders.list()} - mapOption={(item: any) => ({ - value: String(item.id), - label: item.order_number || item.title || `#${item.id}`, - })} - {...STORE_OBJECT} - /> - - - - name="part_items" /> - name="service_items" /> - name="expense_items" /> - - - +
) } + + + diff --git a/apps/dashboard/modules/bills/bill-general-info.tsx b/apps/dashboard/modules/bills/bill-general-info.tsx new file mode 100644 index 0000000..2434d4a --- /dev/null +++ b/apps/dashboard/modules/bills/bill-general-info.tsx @@ -0,0 +1,180 @@ +"use client" + +import { + FileText, + Calendar, + Hash, + Building2, + AlertTriangle, + CheckCircle2, + TimerIcon, + ShoppingCart, + CreditCard, +} from "lucide-react" +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/shared/components/ui/card" +import { Badge } from "@/shared/components/ui/badge" +import { cn } from "@/shared/lib/utils" +import { formatDate, formatCurrency, formatEnum } from "@/shared/utils/formatters" +import { getFullName } from "@/shared/utils/getFullName" +import { useBill } from "./bill-context" + +function InfoItem({ + icon: Icon, + label, + value, +}: { + icon: React.ComponentType<{ className?: string }> + label: string + value?: string | null +}) { + return ( +
+
+ +
+
+ {label} + + {value || } + +
+
+ ) +} + +const statusVariantMap: Record = { + draft: "secondary", + open: "default", + un_paid: "destructive", + partially_paid: "secondary", + paid: "default", +} + +function getDueInfo(dueDateStr?: string, status?: string) { + if (!dueDateStr) return null + const now = new Date() + const due = new Date(dueDateStr) + const diffMs = due.getTime() - now.getTime() + const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24)) + const isPaid = status === "paid" + if (isPaid) return { label: formatDate(dueDateStr), variant: "neutral" as const } + if (diffDays < 0) return { label: `${Math.abs(diffDays)} days overdue`, variant: "overdue" as const } + if (diffDays === 0) return { label: "Due today", variant: "today" as const } + if (diffDays <= 7) return { label: `Due in ${diffDays} day${diffDays === 1 ? "" : "s"}`, variant: "soon" as const } + return { label: formatDate(dueDateStr), variant: "neutral" as const } +} + +export function BillGeneralInfo() { + const bill = useBill() + if (!bill) return null + + const vendor = bill.vendor || {} + const department = bill.department || null + const jobCard = bill.job_card || null + const purchaseOrder = bill.purchase_order || null + + const dueInfo = getDueInfo(bill.bill_due_date as string | undefined, bill.status as string | undefined) + + return ( +
+ + {/* ── Summary Hero ── */} +
+ {/* Status */} + + Status +
+ {bill.status === "paid" && } + {bill.status === "un_paid" && } + {(bill.status === "draft" || bill.status === "open") && } + + {formatEnum(String(bill.status ?? ""))} + +
+ {bill.bill_number && ( + {bill.bill_number} + )} +
+ + {/* Due Date */} + + Due Date + + {formatDate(bill.bill_due_date) || "—"} + + {dueInfo && dueInfo.variant !== "neutral" && ( + + {dueInfo.label} + + )} + + + {/* Vendor */} + + Vendor + {vendor.company_name || getFullName(vendor) || "—"} + +
+ + {/* ── Bill Details ── */} + + + + + Bill Details + + + +
+ + + + + + {jobCard?.order_number && ( + + )} + {purchaseOrder?.order_number && ( + + )} + {bill.tax?.name && ( + + )} +
+
+
+ + {/* ── Notes ── */} + {bill.notes && ( + + + Notes + + +

{bill.notes}

+
+
+ )} +
+ ) +} diff --git a/apps/dashboard/modules/bills/bill-parts-section.tsx b/apps/dashboard/modules/bills/bill-parts-section.tsx new file mode 100644 index 0000000..3c1d358 --- /dev/null +++ b/apps/dashboard/modules/bills/bill-parts-section.tsx @@ -0,0 +1,80 @@ +"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 BillPart = { + id: number + bill_id: number + part_id: number + quantity: string | number + rate: string | number + description?: string + part?: { id?: number; name?: string; part_number?: string } +} + +type BillPartsSectionProps = { + parts?: BillPart[] +} + +export function BillPartsSection({ parts = [] }: BillPartsSectionProps) { + 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 ( + + + + + Parts + + + +
+ + + + Part + Description + Quantity + Rate + Amount + + + + {parts.map((part) => { + const qty = parseFloat(String(part.quantity)) + const rate = parseFloat(String(part.rate)) + const amount = qty * rate + return ( + + + {part.part?.name || `Part #${part.part_id}`} + + + {part.description || "—"} + + {formatNumber(qty)} + {formatCurrency(rate)} + {formatCurrency(amount)} + + ) + })} + + Subtotal + {formatCurrency(subtotal)} + + +
+
+
+
+ ) +} diff --git a/apps/dashboard/modules/bills/bill-payments-section.tsx b/apps/dashboard/modules/bills/bill-payments-section.tsx new file mode 100644 index 0000000..4231857 --- /dev/null +++ b/apps/dashboard/modules/bills/bill-payments-section.tsx @@ -0,0 +1,157 @@ +"use client" + +import { useRouter } from "next/navigation" +import { + BadgeDollarSignIcon, + CalendarIcon, + CreditCardIcon, + HashIcon, + UserIcon, +} from "lucide-react" +import { CrudResource } from "@/shared/data-view/resource-page" +import { ColumnHeader } from "@/shared/data-view/table-view" +import FormDialog from "@/shared/components/form-dialog" +import { Card, CardContent } from "@/shared/components/ui/card" +import { PAYMENT_MADE_ROUTES } from "@garage/api" +import { PaymentMadeForm } from "@/modules/payment-mades/payment-made-form" +import { useBill } from "./bill-context" +import { formatDate } from "@/shared/utils/formatters" +import { getFullName } from "@/shared/utils/getFullName" +import { toRelation } from "@/shared/lib/utils" + +export function BillPaymentsSection() { + const bill = useBill() + const router = useRouter() + + return ( + + extraParams={{ bill_id: bill?.id }} + routeKey={PAYMENT_MADE_ROUTES.INDEX} + getClient={(api) => ({ + list: (query?: any) => api.paymentMades.list(query), + destroy: (id: string) => api.paymentMades.destroy(id), + })} + tableHeader={({ invalidateQuery }) => ( + + +

Payments Made

+ + {(resourceId) => ( + { + router.refresh() + invalidateQuery() + }} + /> + )} + +
+
+ )} + columns={({ actionsColumn }) => [ + { + accessorKey: "payment_number", + header: ({ column }) => , + cell: ({ row }) => { + const item = row.original as any + return ( +
+ + {item.payment_number || "—"} +
+ ) + }, + }, + { + accessorKey: "vendor", + header: ({ column }) => , + cell: ({ row }) => { + const item = row.original as any + return ( +
+ + {item.vendor?.name || item.vendor_name || "—"} +
+ ) + }, + }, + { + accessorKey: "payment_made", + header: ({ column }) => , + cell: ({ row }) => { + const item = row.original as any + const amount = item.payment_made != null + ? Number(item.payment_made).toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }) + : "—" + return ( +
+ + + {amount} + +
+ ) + }, + }, + { + accessorKey: "payment_mode", + header: ({ column }) => , + cell: ({ row }) => { + const item = row.original as any + return ( +
+ + + {item.payment_mode?.name || item.payment_mode_name || "—"} + +
+ ) + }, + }, + { + accessorKey: "payment_date", + header: ({ column }) => , + cell: ({ row }) => { + const item = row.original as any + return ( +
+ + {formatDate(item.payment_date) || "—"} +
+ ) + }, + }, + { + accessorKey: "notes", + header: () => Notes, + enableSorting: false, + cell: ({ row }) => { + const item = row.original as any + const notes = item.notes + if (!notes) return + return ( + + {notes} + + ) + }, + }, + actionsColumn(), + ]} + /> + ) +} diff --git a/apps/dashboard/modules/bills/bill-services-section.tsx b/apps/dashboard/modules/bills/bill-services-section.tsx new file mode 100644 index 0000000..654c9dd --- /dev/null +++ b/apps/dashboard/modules/bills/bill-services-section.tsx @@ -0,0 +1,80 @@ +"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 BillService = { + id: number + bill_id: number + service_id: number + quantity: string | number + rate: string | number + description?: string + service?: { id?: number; name?: string; price?: string } +} + +type BillServicesSectionProps = { + services?: BillService[] +} + +export function BillServicesSection({ services = [] }: BillServicesSectionProps) { + 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 ( + + + + + Services + + + +
+ + + + Service + Description + Quantity + Rate + Amount + + + + {services.map((service) => { + const qty = parseFloat(String(service.quantity)) + const rate = parseFloat(String(service.rate)) + const amount = qty * rate + return ( + + + {service.service?.name || `Service #${service.service_id}`} + + + {service.description || "—"} + + {formatNumber(qty)} + {formatCurrency(rate)} + {formatCurrency(amount)} + + ) + })} + + Subtotal + {formatCurrency(subtotal)} + + +
+
+
+
+ ) +} diff --git a/apps/dashboard/modules/bills/bill-status-badge.tsx b/apps/dashboard/modules/bills/bill-status-badge.tsx new file mode 100644 index 0000000..c0af6ec --- /dev/null +++ b/apps/dashboard/modules/bills/bill-status-badge.tsx @@ -0,0 +1,85 @@ +"use client" + +import { BillStatus } from "@garage/api" +import { Badge, badgeVariants } from "@/shared/components/ui/badge" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/shared/components/ui/select" +import { confirm } from "@/shared/components/confirm-dialog" +import { useAuthApi } from "@/shared/useApi" +import { useRouter } from "next/navigation" +import { toast } from "sonner" +import { useState } from "react" +import { formatEnum } from "@/shared/utils/formatters" + +const STATUS_TRIGGER_CLASS_NAMES: Record = { + draft: badgeVariants({ variant: "outline" }), + open: badgeVariants({ variant: "secondary" }), + un_paid: badgeVariants({ variant: "destructive" }), + partially_paid: badgeVariants({ variant: "default" }), + paid: badgeVariants({ variant: "default" }), +} + +function isBillStatus(value: unknown): value is BillStatus { + return typeof value === "string" && BillStatus.includes(value as BillStatus) +} + +type BillStatusBadgeProps = { + bill: { + id: string + status: string | null | undefined + } +} + +export default function BillStatusBadge({ bill }: BillStatusBadgeProps) { + const api = useAuthApi() + const router = useRouter() + const [isLoading, setIsLoading] = useState(false) + + const { id: billId, status } = bill + + if (!isBillStatus(status)) return null + + const handleStatusChange = async (nextStatus: string) => { + if (!isBillStatus(nextStatus)) return + + const confirmed = await confirm({ + title: "Update Bill Status", + description: `Change bill status to ${formatEnum(nextStatus)}?`, + }) + + if (!confirmed) return + + try { + setIsLoading(true) + await api.bills.update(billId, { status: nextStatus }) + toast.success("Bill status updated") + router.refresh() + } catch (error) { + toast.error("Failed to update bill status") + } finally { + setIsLoading(false) + } + } + + return ( + + ) +} diff --git a/apps/dashboard/modules/bills/bill-totals-summary.tsx b/apps/dashboard/modules/bills/bill-totals-summary.tsx new file mode 100644 index 0000000..be77fe8 --- /dev/null +++ b/apps/dashboard/modules/bills/bill-totals-summary.tsx @@ -0,0 +1,117 @@ +"use client" + +import { Card, CardContent } from "@/shared/components/ui/card" +import { Separator } from "@/shared/components/ui/separator" +import { formatCurrency, formatEnum } from "@/shared/utils/formatters" +import { cn } from "@/shared/lib/utils" +import { useBill } from "./bill-context" + +export function BillTotalsSummary() { + const bill = useBill() + + if (!bill) return null + + const parts = bill.parts ?? [] + const services = bill.services ?? [] + const expenses = bill.expenses ?? [] + + const hasItems = parts.length > 0 || services.length > 0 || expenses.length > 0 + if (!hasItems) return null + + function lineTotal(items: { quantity?: string | number; rate?: string | number }[]) { + return items.reduce((sum, item) => { + const qty = parseFloat(String(item.quantity ?? 0)) + const rate = parseFloat(String(item.rate ?? 0)) + return sum + (isNaN(qty) || isNaN(rate) ? 0 : qty * rate) + }, 0) + } + + const partsTotal = lineTotal(parts) + const servicesTotal = lineTotal(services) + const expensesTotal = lineTotal(expenses) + + // Use API-computed values when available, fall back to manual calc + const subTotal = bill.sub_total ?? (partsTotal + servicesTotal + expensesTotal) + const taxAmount = bill.tax_amount ?? 0 + const discountAmount = bill.discount_amount_major ?? 0 + const total = bill.total ?? subTotal + const paymentsMade = bill.payments_made ?? 0 + const balanceDue = bill.balance_due ?? total + const discount = bill.discount_type + + return ( + + +
+ {parts.length > 0 && ( +
+ Parts ({parts.length}) + {formatCurrency(partsTotal)} +
+ )} + {services.length > 0 && ( +
+ Services ({services.length}) + {formatCurrency(servicesTotal)} +
+ )} + {expenses.length > 0 && ( +
+ Expenses ({expenses.length}) + {formatCurrency(expensesTotal)} +
+ )} + + +
+ Subtotal + {formatCurrency(subTotal)} +
+ + {discountAmount > 0 && ( +
+ Discount{discount && discount !== "no" ? ` (${formatEnum(discount)})` : ""} + −{formatCurrency(discountAmount)} +
+ )} + + {taxAmount > 0 && ( +
+ Tax{bill.tax?.name ? ` (${bill.tax.name})` : ""} + {formatCurrency(taxAmount)} +
+ )} + + + +
+ Total + {formatCurrency(total)} +
+ + {paymentsMade > 0 && ( +
+ Payments Made + −{formatCurrency(paymentsMade)} +
+ )} + + {paymentsMade > 0 && ( + <> + +
0 ? "text-destructive bg-destructive/5" : "text-emerald-700 bg-emerald-50 dark:bg-emerald-950/20", + )}> + Balance Due + {formatCurrency(balanceDue)} +
+ + )} +
+
+
+ ) +} diff --git a/apps/dashboard/modules/bills/bill.schema.ts b/apps/dashboard/modules/bills/bill.schema.ts index 4efeacd..de852ca 100644 --- a/apps/dashboard/modules/bills/bill.schema.ts +++ b/apps/dashboard/modules/bills/bill.schema.ts @@ -9,6 +9,7 @@ const billPartItemSchema = z.object({ title: z.string(), quantity: z.number().min(1), rate: z.number().min(0), + discount_amount: z.number().min(0).optional(), description: z.string().optional(), }) @@ -17,6 +18,7 @@ const billServiceItemSchema = z.object({ title: z.string(), quantity: z.number().min(1), rate: z.number().min(0), + discount_amount: z.number().min(0).optional(), description: z.string().optional(), }) @@ -25,21 +27,33 @@ const billExpenseItemSchema = z.object({ title: z.string(), quantity: z.number().min(1), rate: z.number().min(0), + discount_amount: z.number().min(0).optional(), description: z.string().optional(), }) const billFormSchema = z.object({ + // ── Required ── + title: z.string().min(1, "Title is required"), + + // ── Relations ── vendor: relationFieldSchema, + vendor_address: relationFieldSchema, purchase_order: relationFieldSchema, job_card: relationFieldSchema, payment_term: relationFieldSchema, department: relationFieldSchema, - title: z.string().min(1, "Title is required"), + tax: relationFieldSchema, + + // ── Optional fields ── bill_number: z.string().optional(), bill_date: z.string().optional(), bill_due_date: z.string().optional(), status: z.string().optional(), + discount: z.string().optional(), + discount_amount: z.coerce.number().min(0).optional(), notes: z.string().optional(), + + // ── Line items ── part_items: z.array(billPartItemSchema).optional(), service_items: z.array(billServiceItemSchema).optional(), expense_items: z.array(billExpenseItemSchema).optional(), diff --git a/apps/dashboard/modules/estimates/create-job-card-from-estimate-button.tsx b/apps/dashboard/modules/estimates/create-job-card-from-estimate-button.tsx new file mode 100644 index 0000000..4eeb214 --- /dev/null +++ b/apps/dashboard/modules/estimates/create-job-card-from-estimate-button.tsx @@ -0,0 +1,64 @@ +"use client" + +import { useState } from "react" +import { ClipboardList } from "lucide-react" +import { useRouter } from "next/navigation" +import { ApiError } from "@garage/api" +import { Button } from "@/shared/components/ui/button" +import { confirm } from "@/shared/components/confirm-dialog" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" +import { useEstimate } from "./estimate-context" + +export function CreateJobCardFromEstimateButton() { + const [isConverting, setIsConverting] = useState(false) + const estimateContext = useEstimate() + const api = useAuthApi() + const router = useRouter() + + const estimateId = estimateContext?.id ?? "" + + if (!estimateContext || !estimateId) return null + + const handleConvert = async () => { + const confirmed = await confirm({ + title: "Generate Job Card", + description: "This will create a job card from this estimate. Do you want to continue?", + confirmLabel: "Generate", + }) + + if (!confirmed) return + + setIsConverting(true) + try { + const response = await api.estimates.convertToJobCard(estimateId, {}) + const jobCardId = response?.data?.id + + toast.success("Estimate converted to job card successfully") + + if (jobCardId) { + router.push(`/sales/job-cards/${jobCardId}`) + } + } catch (error) { + if (error instanceof ApiError && error.status === 409) { + const jobCardId = (error.payload?.data as { job_card_id?: number } | undefined)?.job_card_id + toast.info("A job card already exists for this estimate.") + if (jobCardId) { + router.push(`/sales/job-cards/${jobCardId}`) + return + } + } + + toast.error("Failed to convert estimate to job card") + } finally { + setIsConverting(false) + } + } + + return ( + + ) +} \ No newline at end of file diff --git a/apps/dashboard/modules/expense-items/expense-item-form.tsx b/apps/dashboard/modules/expense-items/expense-item-form.tsx index 0a5b0c9..814e1f4 100644 --- a/apps/dashboard/modules/expense-items/expense-item-form.tsx +++ b/apps/dashboard/modules/expense-items/expense-item-form.tsx @@ -8,10 +8,13 @@ import { FieldGroup } from "@/shared/components/ui/field" import { Rhform, RhfTextField, + RhfTextareaField, RhfSelectField, RhfAsyncSelectField, RhfCheckboxField, } from "@/shared/components/form" +import { UnitTypeInlineForm } from "@/modules/services/inline-forms/unit-type-inline-form" +import { DepartmentInlineForm } from "@/modules/services/inline-forms/department-inline-form" import { toast } from "sonner" import { useAuthApi } from "@/shared/useApi" import { useResourceForm } from "@/shared/hooks/use-resource-form" @@ -19,7 +22,13 @@ import { useFormMutation } from "@/shared/hooks/use-form-mutation" import { toRelation, toId } from "@/shared/lib/utils" import { expenseItemFormSchema, type ExpenseItemFormValues } from "./expense-item.schema" -import { EXPENSE_ITEM_ROUTES, INVENTORY_CATEGORY_ROUTES } from "@garage/api" +import { + EXPENSE_ITEM_ROUTES, + INVENTORY_CATEGORY_ROUTES, + INVENTORY_ROUTES, + DEPARTMENT_ROUTES, + VENDOR_ROUTES, +} from "@garage/api" import { InventoryCategoryCrudDialog } from "./inventory-category-crud-dialog" // ── Constants ── @@ -48,10 +57,19 @@ export type ExpenseItemFormProps = { const DEFAULT_VALUES: ExpenseItemFormValues = { item_type: "Expense", item_name: "", + sku: "", + item_code: "", + description: "", category: null, + unit_type: null, + department: null, + purchase_information: true, purchase_price: undefined, purchase_chart_of_account: "", - purchase_information: true, + purchase_preferred_vendor: null, + sales_information: false, + selling_price: undefined, + sales_chart_of_account: "", is_active: true, } @@ -63,10 +81,22 @@ function mapToFormValues(data: unknown): ExpenseItemFormValues { return { item_type: d.item_type || "Expense", item_name: d.item_name || "", + sku: d.sku || "", + item_code: d.item_code || "", + description: d.description || "", category: toRelation(d.category_id, d.category_title ?? d.category_name), + unit_type: toRelation(d.unit_type_id, d.unit_type_title ?? d.unit_type_name), + department: toRelation(d.department_id, d.department_name ?? d.department_title), + purchase_information: d.purchase_information ?? true, purchase_price: d.purchase_price ?? undefined, purchase_chart_of_account: d.purchase_chart_of_account || "", - purchase_information: d.purchase_information ?? true, + purchase_preferred_vendor: toRelation( + d.purchase_preferred_vendor_id, + d.purchase_preferred_vendor_name, + ), + sales_information: d.sales_information ?? false, + selling_price: d.selling_price ?? undefined, + sales_chart_of_account: d.sales_chart_of_account || "", is_active: d.is_active ?? true, } } @@ -75,10 +105,19 @@ function mapFormToPayload(values: ExpenseItemFormValues) { return { item_type: values.item_type, item_name: values.item_name, + sku: values.sku || undefined, + item_code: values.item_code || undefined, + description: values.description || undefined, category_id: toId(values.category), + unit_type_id: toId(values.unit_type), + department_id: toId(values.department), + purchase_information: values.purchase_information, purchase_price: values.purchase_price, purchase_chart_of_account: values.purchase_chart_of_account || undefined, - purchase_information: values.purchase_information, + purchase_preferred_vendor_id: toId(values.purchase_preferred_vendor), + sales_information: values.sales_information, + selling_price: values.selling_price, + sales_chart_of_account: values.sales_chart_of_account || undefined, is_active: values.is_active, } } @@ -129,6 +168,7 @@ export function ExpenseItemForm({ resourceId, initialData, onSuccess }: ExpenseI )} + {/* Basic Info */}
+
+ + +
+ + + + {/* Classification */}
Category @@ -160,6 +221,37 @@ export function ExpenseItemForm({ resourceId, initialData, onSuccess }: ExpenseI />
+
+ api.inventory.listUnitTypes()} + mapOption={mapLookupOption} + createForm={(props) => } + createLabel="Unit Type" + {...STORE_OBJECT} + /> + api.departments.list()} + mapOption={(item: any) => ({ value: String(item.id), label: item.name ?? String(item.id) })} + createForm={(props) => } + createLabel="Department" + {...STORE_OBJECT} + /> +
+ + {/* Purchase Information */} + {/* +
-
- api.vendors.list()} + mapOption={(item: any) => ({ value: String(item.id), label: item.name ?? String(item.id) })} + {...STORE_OBJECT} + /> */} + + {/* Sales Information */} + {/* + +
+ - -
+
*/} + + {/* Status */} +
diff --git a/apps/dashboard/modules/expense-items/expense-item.schema.ts b/apps/dashboard/modules/expense-items/expense-item.schema.ts index 3156be0..42ab5c7 100644 --- a/apps/dashboard/modules/expense-items/expense-item.schema.ts +++ b/apps/dashboard/modules/expense-items/expense-item.schema.ts @@ -7,10 +7,22 @@ export const relationFieldSchema = z export const expenseItemFormSchema = z.object({ item_type: z.string().min(1, "Item type is required"), item_name: z.string().min(1, "Item name is required"), + sku: z.string().optional(), + item_code: z.string().optional(), + description: z.string().optional(), category: relationFieldSchema, + unit_type: relationFieldSchema, + department: relationFieldSchema, + // Purchase + purchase_information: z.boolean().default(true), purchase_price: z.coerce.number().min(0).optional(), purchase_chart_of_account: z.string().optional(), - purchase_information: z.boolean().default(true), + // purchase_preferred_vendor: relationFieldSchema, + // Sales + sales_information: z.boolean().default(false), + selling_price: z.coerce.number().min(0).optional(), + sales_chart_of_account: z.string().optional(), + // Status is_active: z.boolean().default(true), }) diff --git a/apps/dashboard/modules/expense-items/expense-items-selector-field.tsx b/apps/dashboard/modules/expense-items/expense-items-selector-field.tsx index aeea3bb..442d112 100644 --- a/apps/dashboard/modules/expense-items/expense-items-selector-field.tsx +++ b/apps/dashboard/modules/expense-items/expense-items-selector-field.tsx @@ -22,6 +22,8 @@ type ExpenseLineItem = { title: string quantity: number rate: number + chart_of_account?: string + discount_amount?: number description?: string } @@ -34,6 +36,8 @@ export type ExpenseItemsSelectorFieldProps< name: TName & (TValues[TName] extends ExpenseItemsFieldConstraint ? TName : never) label?: string triggerLabel?: string + showChartOfAccount?: boolean + showDiscount?: boolean } export function ExpenseItemsSelectorField< @@ -43,6 +47,8 @@ export function ExpenseItemsSelectorField< name, label = "Expense Items", triggerLabel = "Add Expense Items", + showChartOfAccount = false, + showDiscount = false, }: ExpenseItemsSelectorFieldProps) { return ( @@ -69,6 +75,7 @@ export function ExpenseItemsSelectorField< title: r.item_name || "", quantity: 1, rate: Number(r.purchase_price) || 0, + chart_of_account: r.purchase_chart_of_account ? String(r.purchase_chart_of_account) : "", description: "", } as any }} @@ -79,6 +86,8 @@ export function ExpenseItemsSelectorField< Expense Item Qty Rate + {showChartOfAccount && Chart of Account} + {showDiscount && Discount} Description @@ -110,6 +119,32 @@ export function ExpenseItemsSelectorField< className="h-8 w-24" /> + {showChartOfAccount && ( + + + update(index, { ...item, chart_of_account: e.target.value } as any) + } + placeholder="Optional" + className="h-8 w-32" + /> + + )} + {showDiscount && ( + + + update(index, { ...item, discount_amount: Number(e.target.value) || 0 } as any) + } + className="h-8 w-24" + /> + + )} { + router.push(`/purchase/expense/${expenseId}/edit`) + } + + const handleDelete = async () => { + await api.expenses.destroy(expenseId) + router.push("/purchase/expense") + } + + return ( + + + + + + + + Edit + + + + Delete + + + + ) +} diff --git a/apps/dashboard/modules/expenses/expense-context.tsx b/apps/dashboard/modules/expenses/expense-context.tsx new file mode 100644 index 0000000..70f1916 --- /dev/null +++ b/apps/dashboard/modules/expenses/expense-context.tsx @@ -0,0 +1,27 @@ +"use client" + +import { CrudShowResponse, ExpensesClient } from "@garage/api" +import { createContext, useContext } from "react" + +export type ExpenseContextValue = CrudShowResponse['data'] + + +const ExpenseContext = createContext(null) + +export function ExpenseProvider({ + expense, + children, +}: { + expense: ExpenseContextValue + children: React.ReactNode +}) { + return ( + + {children} + + ) +} + +export function useExpense() { + return useContext(ExpenseContext) +} diff --git a/apps/dashboard/modules/expenses/expense-form-summary.tsx b/apps/dashboard/modules/expenses/expense-form-summary.tsx new file mode 100644 index 0000000..4caaa66 --- /dev/null +++ b/apps/dashboard/modules/expenses/expense-form-summary.tsx @@ -0,0 +1,62 @@ +"use client" + +import { useFormContext } from "react-hook-form" + +import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" +import { + useDocumentTotals, + type DocumentLineItem, +} from "@/shared/hooks/use-document-totals" +import { DocumentTotalsSummary } from "@/shared/components/document-totals-summary" + +import type { ExpenseFormValues } from "./expense.schema" + +function parseTaxRateFromLabel(label: string | undefined | null): number | undefined { + if (!label) return undefined + const match = label.match(/\((\d+(?:\.\d+)?)%\)/) + return match ? Number(match[1]) : undefined +} + +export function ExpenseFormSummary() { + const { watch } = useFormContext() + + const items = watch("items") ?? [] + const discountType = watch("discount") + const discountAmount = watch("discount_amount") + const taxRelation = watch("tax") + + const taxRate = parseTaxRateFromLabel(taxRelation?.label) + const taxLabel = taxRelation?.label ?? "Tax" + + const lineItems: DocumentLineItem[] = items.map((item) => ({ + quantity: item.quantity, + rate: item.rate, + discount_amount: item.discount_amount, + })) + + const totals = useDocumentTotals({ + lineItems, + discountType, + discountAmount, + taxRate, + }) + + if (!totals.hasLineItems) return null + + return ( + + + + Summary + + + + + + + ) +} \ No newline at end of file diff --git a/apps/dashboard/modules/expenses/expense-form.tsx b/apps/dashboard/modules/expenses/expense-form.tsx index abe5e9d..a293406 100644 --- a/apps/dashboard/modules/expenses/expense-form.tsx +++ b/apps/dashboard/modules/expenses/expense-form.tsx @@ -5,29 +5,49 @@ import { AlertTriangle, Plus, Save } from "lucide-react" import { Button } from "@/shared/components/ui/button" import { Alert, AlertTitle } from "@/shared/components/ui/alert" import { FieldGroup } from "@/shared/components/ui/field" +import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" import { Rhform, RhfTextField, RhfSelectField, RhfAsyncSelectField, RhfTextareaField, + 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 { getTodayDate, toRelation, toId } from "@/shared/lib/utils" import { expenseFormSchema, type ExpenseFormValues, } from "./expense.schema" -import { EXPENSE_ROUTES, JOB_CARD_ROUTES, VENDOR_ROUTES, DEPARTMENT_ROUTES, ExpenseStatus } from "@garage/api" -import { getFullName } from "@/shared/utils/getFullName" +import { + EXPENSE_ROUTES, + JOB_CARD_ROUTES, + VENDOR_ROUTES, + DEPARTMENT_ROUTES, + INVENTORY_CATEGORY_ROUTES, + TAX_ROUTES, + ExpenseStatus, + InvoiceDiscount, +} from "@garage/api" +import { useFormContext } from "react-hook-form" +import { RhfLabelPickerField, type LabelItem } from "@/modules/labels/rhf-label-picker-field" +import { ExpenseItemsSelectorField } from "@/modules/expense-items/expense-items-selector-field" +import { InventoryCategoryCrudDialog } from "@/modules/expense-items/inventory-category-crud-dialog" +import { ExpenseFormSummary } from "./expense-form-summary" // ── Constants ── -const STATUS_OPTIONS = ExpenseStatus.map((v) => ({ +const STATUS_OPTIONS = ExpenseStatus.map((value) => ({ + value, + label: value.replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase()), +})) + +const DISCOUNT_OPTIONS = InvoiceDiscount.map((v) => ({ value: v, label: v.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()), })) @@ -47,11 +67,16 @@ const DEFAULT_VALUES: ExpenseFormValues = { category: null, vendor: null, department: null, + tax: null, title: "", invoice_number: "", - expense_date: "", + expense_date: getTodayDate(), notes: "", status: "open", + discount: "no", + discount_amount: undefined, + labels: [], + items: [], } // ── Mapping helpers ── @@ -60,15 +85,32 @@ function mapToFormValues(data: unknown): ExpenseFormValues { const d = (data as any)?.data ?? data ?? {} return { - job_card: toRelation(d.job_card_id, d.job_card_name), - category: toRelation(d.category_id, d.category_name), - vendor: toRelation(d.vendor_id, d.vendor_name), - department: toRelation(d.department_id, d.department_name), + job_card: toRelation(d.job_card_id, d.job_card?.order_number ?? d.job_card_name), + category: toRelation(d.category_id, d.category?.name ?? d.category?.title ?? d.category_name), + vendor: toRelation(d.vendor_id, d.vendor?.company_name ?? d.vendor?.name ?? d.vendor_name), + department: toRelation(d.department_id, d.department?.name ?? d.department_name), + tax: toRelation(d.tax_id, d.tax?.title ? `${d.tax.title} (${d.tax.rate}%)` : undefined), title: d.title || "", invoice_number: d.invoice_number || "", - expense_date: d.expense_date || "", + expense_date: d.expense_date ? d.expense_date.split("T")[0] : "", notes: d.notes || "", status: d.status || "open", + discount: d.discount || "no", + discount_amount: d.discount_amount != null ? Number(d.discount_amount) : undefined, + labels: (d.labels ?? []).map((label: any): LabelItem => ({ + id: Number(label.id), + title: label.title ?? label.name ?? "", + color_code: label.color_code ?? "", + })), + items: (d.expense_items ?? d.items ?? []).map((item: any) => ({ + expense_id: item.expense_item_id ?? item.expense_item?.id ?? item.id, + title: item.expense_item?.item_name ?? item.expense_item?.name ?? item.item_name ?? item.title ?? "", + quantity: Number(item.quantity) || 1, + rate: Number(item.rate) || 0, + discount_amount: item.discount_amount != null ? Number(item.discount_amount) : undefined, + chart_of_account: item.chart_of_account != null ? String(item.chart_of_account) : "", + description: item.description ?? "", + })), } } @@ -78,11 +120,23 @@ function mapFormToPayload(values: ExpenseFormValues) { category_id: toId(values.category), vendor_id: toId(values.vendor), department_id: toId(values.department), + tax_id: toId(values.tax) ? Number(toId(values.tax)) : undefined, title: values.title, invoice_number: values.invoice_number || undefined, expense_date: values.expense_date || undefined, notes: values.notes || undefined, status: values.status || undefined, + discount: values.discount || undefined, + discount_amount: values.discount === "transaction_level" ? (values.discount_amount ?? 0) : undefined, + label_ids: values.labels?.map((label) => label.id) ?? [], + items: (values.items ?? []).map((item) => ({ + expense_item_id: item.expense_id, + quantity: item.quantity, + rate: item.rate, + discount_amount: values.discount === "line_item_level" ? (item.discount_amount ?? 0) : undefined, + chart_of_account: item.chart_of_account || undefined, + description: item.description || undefined, + })), } } @@ -90,11 +144,32 @@ function mapFormToPayload(values: ExpenseFormValues) { const mapLookupOption = (item: any) => ({ value: String(item.id), - label: item.name, + label: item.name ?? item.title ?? `#${item.id}`, +}) + +const mapVendorOption = (item: any) => ({ + value: String(item.id), + label: item.company_name ?? item.name ?? `#${item.id}`, }) const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label } +// ── Transaction-level discount amount field ── + +function TransactionDiscountField() { + const { watch } = useFormContext() + const discount = watch("discount") + if (discount !== "transaction_level") return null + return ( + + ) +} + // ── Component ── export function ExpenseForm({ resourceId, initialData, onSuccess }: ExpenseFormProps) { @@ -105,15 +180,19 @@ export function ExpenseForm({ resourceId, initialData, onSuccess }: ExpenseFormP defaultValues: DEFAULT_VALUES, resourceId, initialData, + queryKey: [EXPENSE_ROUTES.BY_ID, resourceId], mapToFormValues, }) + const discount = form.watch("discount") + const isLineItemDiscount = discount === "line_item_level" + const { mutate, error, isPending } = useFormMutation(form, { mutationFn: (values: ExpenseFormValues) => { const payload = mapFormToPayload(values) - const promise = isEditing && resourceId - ? api.expenses.update(resourceId, payload) - : api.expenses.create(payload) + const promise = (isEditing && resourceId + ? api.expenses.update(resourceId, payload as any) + : api.expenses.create(payload as any)) as Promise toast.promise(promise, { loading: isEditing ? "Updating expense..." : "Creating expense...", success: isEditing ? "Expense updated successfully" : "Expense created successfully", @@ -139,72 +218,136 @@ export function ExpenseForm({ resourceId, initialData, onSuccess }: ExpenseFormP )} - - +
+
+ + -
- - +
+ + +
+ + + + + + + name="items" + label="Expense Items" + triggerLabel="Add Expense Items" + showChartOfAccount + showDiscount={isLineItemDiscount} + /> + + + + +
-
- api.vendors.list()} - mapOption={(op: any) => ({ value: String(op.id), label: getFullName(op)})} - {...STORE_OBJECT} - /> - api.departments.list()} - mapOption={mapLookupOption} - {...STORE_OBJECT} - /> +
+ + + + Details + + + + + + + api.taxes.list()} + mapOption={(item: any) => ({ + value: String(item.id), + label: item.title ? `${item.title} (${item.rate}%)` : `#${item.id}`, + })} + {...STORE_OBJECT} + /> + + api.vendors.list()} + mapOption={mapVendorOption} + {...STORE_OBJECT} + /> + + api.departments.list()} + mapOption={mapLookupOption} + {...STORE_OBJECT} + /> + + api.jobCards.list()} + mapOption={(item: any) => ({ + value: String(item.id), + label: item.order_number ?? item.estimate_number ?? `#${item.id}`, + })} + {...STORE_OBJECT} + /> + +
+
+ Category + +
+ api.inventoryCategories.list()} + mapOption={mapLookupOption} + {...STORE_OBJECT} + /> +
+ + +
+
+
+ +
+ +
- -
- api.jobCards.list()} - mapOption={(item: any) => ({ value: String(item.id), label: item.job_card_number || item.name || `#${item.id}` })} - {...STORE_OBJECT} - /> - api.expenses.listItems()} - mapOption={mapLookupOption} - {...STORE_OBJECT} - /> -
- - - - - - - +
) } diff --git a/apps/dashboard/modules/expenses/expense-general-info.tsx b/apps/dashboard/modules/expenses/expense-general-info.tsx new file mode 100644 index 0000000..0ca08c9 --- /dev/null +++ b/apps/dashboard/modules/expenses/expense-general-info.tsx @@ -0,0 +1,235 @@ +"use client" + +import { + Calendar, + Hash, + Building2, + AlertTriangle, + CheckCircle2, + TimerIcon, + ReceiptIcon, + Wrench, + Tag, + Percent, +} from "lucide-react" +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/shared/components/ui/card" +import { Badge } from "@/shared/components/ui/badge" +import { cn } from "@/shared/lib/utils" +import { formatDate, formatCurrency, formatEnum } from "@/shared/utils/formatters" +import { getFullName } from "@/shared/utils/getFullName" +import { useExpense } from "./expense-context" + +function InfoItem({ + icon: Icon, + label, + value, +}: { + icon: React.ComponentType<{ className?: string }> + label: string + value?: string | null +}) { + return ( +
+
+ +
+
+ {label} + + {value || } + +
+
+ ) +} + +const statusVariantMap: Record = { + draft: "secondary", + open: "default", + un_paid: "destructive", + partially_paid: "secondary", + paid: "default", + cancelled: "destructive", +} + +export function ExpenseGeneralInfo() { + const expense = useExpense() + if (!expense) return null + + const vendor = expense.vendor || {} + const department = expense.department || null + const jobCard = expense.job_card || null + const category = expense.category || null + const labels = (expense as any)?.labels || [] + const balanceDue = expense.balance_due ?? null + const paymentsM = expense.payments_made ?? null + const taxLabel = expense.tax?.title + ? `${expense.tax.title} (${expense.tax.rate}%)` + : "Tax" + + return ( +
+ + {/* ── Summary Hero ── */} +
+ {/* Status */} + + Status +
+ {expense.status === "paid" && } + {expense.status === "pending" && } + {expense.status === "open" && } + {expense.status === "un_paid" && } + + {formatEnum(String(expense.status ?? ""))} + +
+
+ + {/* Total */} + + Total + + {formatCurrency(expense.total ?? 0)} + + {expense.sub_total != null && expense.sub_total !== expense.total && ( + Subtotal: {formatCurrency(expense.sub_total)} + )} + + + {/* Payments Made */} + + Paid + + {formatCurrency(paymentsM ?? 0)} + + + + {/* Balance Due */} + 0 && "border-destructive/50 bg-destructive/5")}> + Balance Due + 0 ? "text-destructive" : "text-green-600 dark:text-green-400")}> + {formatCurrency(balanceDue ?? 0)} + + +
+ + {/* ── Vendor ── */} + + Vendor + {vendor.company_name || getFullName(vendor as any) || "—"} + + + {/* ── Expense Details ── */} + + + + + Expense Details + + + +
+ + + + {expense.discount && expense.discount !== "no" && ( + + )} + {expense.discount_amount_major != null && expense.discount_amount_major > 0 && ( + + )} + {expense.tax_amount != null && expense.tax_amount > 0 && ( + + )} +
+
+
+ + {/* ── Job Card & Category ── */} + {(jobCard || category) && ( + + + + + Related Information + + + +
+ {jobCard && ( +
+
+ +
+
+ Job Card + {jobCard.order_number || "—"} + {jobCard.title || ""} +
+
+ )} + {category && ( +
+
+ +
+
+ Category + {category.title || "—"} +
+
+ )} +
+
+
+ )} + + {/* ── Labels ── */} + {labels && labels.length > 0 && ( + + + + + Labels + + + +
+ {labels.map((label: any) => ( + + {label.title} + + ))} +
+
+
+ )} + + {/* ── Notes ── */} + {expense.notes && ( + + + Notes + + +

{expense.notes}

+
+
+ )} +
+ ) +} diff --git a/apps/dashboard/modules/expenses/expense-items-section.tsx b/apps/dashboard/modules/expenses/expense-items-section.tsx new file mode 100644 index 0000000..62a32d8 --- /dev/null +++ b/apps/dashboard/modules/expenses/expense-items-section.tsx @@ -0,0 +1,133 @@ +"use client" + +import { ShoppingCartIcon } 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 } from "@/shared/utils/formatters" + +type ExpenseItem = { + id?: number + expense_id?: number + expense_item_id?: number + quantity?: number | string + rate?: string | number + description?: string | null + expense_item?: { + id?: number + item_name?: string + name?: string + item_code?: string + sku?: string + } +} + +type ExpenseItemsSectionProps = { + items?: ExpenseItem[] + discountType?: string | null + subTotal?: number | null + discountAmount?: number | null + taxAmount?: number | null + total?: number | null + taxLabel?: string | null +} + +export function ExpenseItemsSection({ + items, + discountType, + subTotal, + discountAmount, + taxAmount, + total, + taxLabel, +}: ExpenseItemsSectionProps) { + if (!items || items.length === 0) return null + + return ( + + + + + Expense Items ({items.length}) + + + +
+ + + + Item + Description + Qty + Rate + Amount + + + + {items.map((item) => { + const qty = Number(item.quantity || 0) + const rate = Number(item.rate || 0) + const amount = qty * rate + const name = item.expense_item?.item_name || item.expense_item?.name || "—" + return ( + + +
{name}
+ {(item.expense_item?.sku || item.expense_item?.item_code) && ( +
+ {item.expense_item?.sku || item.expense_item?.item_code} +
+ )} +
+ + {item.description || "—"} + + {qty.toLocaleString()} + {formatCurrency(rate)} + {formatCurrency(amount)} +
+ ) + })} +
+
+
+ + {/* ── Totals summary ── */} +
+
+ {subTotal != null && ( +
+ Subtotal + {formatCurrency(subTotal)} +
+ )} + {discountAmount != null && discountAmount > 0 && discountType !== "no" && ( +
+ Discount + − {formatCurrency(discountAmount)} +
+ )} + {taxAmount != null && taxAmount > 0 && ( +
+ {taxLabel || "Tax"} + {formatCurrency(taxAmount)} +
+ )} + {total != null && ( +
+ Total + {formatCurrency(total)} +
+ )} +
+
+
+
+ ) +} diff --git a/apps/dashboard/modules/expenses/expense-payments-section.tsx b/apps/dashboard/modules/expenses/expense-payments-section.tsx new file mode 100644 index 0000000..a2059df --- /dev/null +++ b/apps/dashboard/modules/expenses/expense-payments-section.tsx @@ -0,0 +1,157 @@ +"use client" + +import { useRouter } from "next/navigation" +import { + BadgeDollarSignIcon, + CalendarIcon, + CreditCardIcon, + HashIcon, + UserIcon, +} from "lucide-react" +import { CrudResource } from "@/shared/data-view/resource-page" +import { ColumnHeader } from "@/shared/data-view/table-view" +import FormDialog from "@/shared/components/form-dialog" +import { Card, CardContent } from "@/shared/components/ui/card" +import { PAYMENT_MADE_ROUTES } from "@garage/api" +import { PaymentMadeForm } from "@/modules/payment-mades/payment-made-form" +import { useExpense } from "./expense-context" +import { formatDate } from "@/shared/utils/formatters" +import { getFullName } from "@/shared/utils/getFullName" +import { toRelation } from "@/shared/lib/utils" + +export function ExpensePaymentsSection() { + const expense = useExpense() + const router = useRouter() + + return ( + + extraParams={{ expense_id: expense?.id }} + routeKey={PAYMENT_MADE_ROUTES.INDEX} + + getClient={(api) => ({ + list: (query?: any) => api.paymentMades.list(query), + destroy: (id: string) => api.paymentMades.destroy(id), + })} + tableHeader={({ invalidateQuery }) => ( + + +

Payments Made

+ + {(resourceId) => ( + { + router.refresh() + invalidateQuery() + }} + /> + )} + +
+
+ )} + columns={({ actionsColumn }) => [ + { + accessorKey: "payment_number", + header: ({ column }) => , + cell: ({ row }) => { + const item = row.original as any + return ( +
+ + {item.payment_number || "—"} +
+ ) + }, + }, + { + accessorKey: "vendor", + header: ({ column }) => , + cell: ({ row }) => { + const item = row.original as any + return ( +
+ + {item.vendor?.name || item.vendor_name || "—"} +
+ ) + }, + }, + { + accessorKey: "payment_made", + header: ({ column }) => , + cell: ({ row }) => { + const item = row.original as any + const amount = item.payment_made != null + ? Number(item.payment_made).toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }) + : "—" + return ( +
+ + + {amount} + +
+ ) + }, + }, + { + accessorKey: "payment_mode", + header: ({ column }) => , + cell: ({ row }) => { + const item = row.original as any + return ( +
+ + + {item.payment_mode?.name || item.payment_mode_name || "—"} + +
+ ) + }, + }, + { + accessorKey: "payment_date", + header: ({ column }) => , + cell: ({ row }) => { + const item = row.original as any + return ( +
+ + {formatDate(item.payment_date) || "—"} +
+ ) + }, + }, + { + accessorKey: "notes", + header: () => Notes, + enableSorting: false, + cell: ({ row }) => { + const item = row.original as any + const notes = item.notes + if (!notes) return + return ( + + {notes} + + ) + }, + }, + actionsColumn(), + ]} + /> + ) +} diff --git a/apps/dashboard/modules/expenses/expense.schema.ts b/apps/dashboard/modules/expenses/expense.schema.ts index e87e3a8..b277d7d 100644 --- a/apps/dashboard/modules/expenses/expense.schema.ts +++ b/apps/dashboard/modules/expenses/expense.schema.ts @@ -4,12 +4,29 @@ const relationFieldSchema = z .object({ value: z.string(), label: z.string() }) .nullable() +const labelItemSchema = z.object({ + id: z.number(), + title: z.string(), + color_code: z.string(), +}) + +const expenseLineItemSchema = z.object({ + expense_id: z.number(), + title: z.string(), + quantity: z.number().min(1), + rate: z.number().min(0), + discount_amount: z.number().min(0).optional(), + chart_of_account: z.string().optional(), + description: z.string().optional(), +}) + const expenseFormSchema = z.object({ // ── Relations ── job_card: relationFieldSchema, category: relationFieldSchema, vendor: relationFieldSchema, department: relationFieldSchema, + tax: relationFieldSchema, // ── Basic info ── title: z.string().min(1, "Title is required"), @@ -17,6 +34,13 @@ const expenseFormSchema = z.object({ expense_date: z.string().optional(), notes: z.string().optional(), status: z.string().optional(), + + // ── Discount / Tax ── + discount: z.string().optional(), + discount_amount: z.coerce.number().min(0).optional(), + + labels: z.array(labelItemSchema).optional(), + items: z.array(expenseLineItemSchema).optional(), }) type ExpenseFormValues = z.infer diff --git a/apps/dashboard/modules/invoices/invoice-actions.tsx b/apps/dashboard/modules/invoices/invoice-actions.tsx index a0e0d39..dab9c8d 100644 --- a/apps/dashboard/modules/invoices/invoice-actions.tsx +++ b/apps/dashboard/modules/invoices/invoice-actions.tsx @@ -2,6 +2,7 @@ import { useAuthApi } from "@/shared/useApi" import { useRouter } from "next/navigation" +import { useState } from "react" import { Button } from "@/shared/components/ui/button" import { DropdownMenu, @@ -9,7 +10,10 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/shared/components/ui/dropdown-menu" +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/shared/components/ui/dialog" import { Ellipsis, Pencil, Trash2 } from "lucide-react" +import { InvoiceEditForm } from "./invoice-edit-form" +import { useInvoice } from "./invoice-context" type InvoiceActionsProps = { invoiceId: string @@ -18,9 +22,11 @@ type InvoiceActionsProps = { export function InvoiceActions({ invoiceId }: InvoiceActionsProps) { const api = useAuthApi() const router = useRouter() - - const handleEdit = () => { - router.push(`/sales/invoice/${invoiceId}/edit`) + const [isEditOpen, setIsEditOpen] = useState(false) + const invoice = useInvoice() + const handleEditSuccess = () => { + setIsEditOpen(false) + router.refresh() } const handleDelete = async () => { @@ -29,22 +35,39 @@ export function InvoiceActions({ invoiceId }: InvoiceActionsProps) { } return ( - - - - - - - - Edit - - - - Delete - - - + <> + + + + + + {/* setIsEditOpen(true)}> + + Edit + */} + + + Delete + + + + + {/* + + + Edit Invoice + + + + */} + ) } diff --git a/apps/dashboard/modules/invoices/invoice-context.tsx b/apps/dashboard/modules/invoices/invoice-context.tsx index 2b37622..0d70a31 100644 --- a/apps/dashboard/modules/invoices/invoice-context.tsx +++ b/apps/dashboard/modules/invoices/invoice-context.tsx @@ -1,10 +1,22 @@ "use client" +import { ApiResponse } from "@garage/api" import { createContext, useContext } from "react" - type InvoiceContextValue = { - id: string - label: string + id: number | string + subject?: string | null + invoice_number?: string | null + discount?: string | null + sub_total?: number | string | null + total?: number | string | null + payments_recieved?: number | string | null + received_payment?: number | string | null + balance_due?: number | string | null + amount?: number | string | null + invoice_parts?: { quantity?: string | number; rate?: string | number }[] + invoice_services?: { quantity?: string | number; rate?: string | number }[] + invoice_expenses?: { quantity?: string | number; rate?: string | number }[] + [key: string]: unknown } const InvoiceContext = createContext(null) diff --git a/apps/dashboard/modules/invoices/invoice-edit-form.tsx b/apps/dashboard/modules/invoices/invoice-edit-form.tsx new file mode 100644 index 0000000..70d00e4 --- /dev/null +++ b/apps/dashboard/modules/invoices/invoice-edit-form.tsx @@ -0,0 +1,192 @@ +"use client" + +import { AlertTriangle, Save } from "lucide-react" +import { Button } from "@/shared/components/ui/button" +import { Alert, AlertTitle } from "@/shared/components/ui/alert" +import { FieldGroup } from "@/shared/components/ui/field" +import { + Rhform, + RhfTextField, + RhfSelectField, + RhfTextareaField, + 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 { toId } from "@/shared/lib/utils" +import { InvoiceStatus, InvoiceDiscount, INVOICE_ROUTES } from "@garage/api" +import { z } from "zod" + +// ── Schema for edit form (simplified, edit only) ── + +const invoiceEditFormSchema = z.object({ + subject: z.string().min(1, "Subject is required"), + due_date: z.string().optional(), + invoice_title: z.string().optional(), + notes: z.string().optional(), + terms_and_conditions: z.string().optional(), + status: z.string().optional(), + discount: z.string().optional(), +}) + +type InvoiceEditFormValues = z.infer + +// ── Constants ── + +const STATUS_OPTIONS = InvoiceStatus.map((v) => ({ + value: v, + label: v.charAt(0).toUpperCase() + v.slice(1).replace(/_/g, " "), +})) + +const DISCOUNT_OPTIONS = InvoiceDiscount.map((v) => ({ + value: v, + label: v.charAt(0).toUpperCase() + v.slice(1).replace(/_/g, " "), +})) + +// ── Default values ── + +const DEFAULT_VALUES: InvoiceEditFormValues = { + subject: "", + due_date: "", + invoice_title: "", + notes: "", + terms_and_conditions: "", + status: "", + discount: "no", +} + +// ── Mapping helpers ── + +function mapToFormValues(data: unknown): InvoiceEditFormValues { + const d = (data as any)?.data ?? data ?? {} + return { + subject: d.subject || "", + due_date: d.due_date || "", + invoice_title: d.invoice_title || "", + notes: d.notes || "", + terms_and_conditions: d.terms_and_conditions || "", + status: d.status || "", + discount: d.discount || "no", + } +} + +function mapFormToPayload(values: InvoiceEditFormValues) { + return { + subject: values.subject, + due_date: values.due_date || undefined, + invoice_title: values.invoice_title || undefined, + notes: values.notes || undefined, + terms_and_conditions: values.terms_and_conditions || undefined, + status: values.status || undefined, + discount: values.discount || "no", + } +} + +// ── Props ── + +export type InvoiceEditFormProps = { + resourceId: string + initialData?: unknown + onSuccess?: () => void +} + +// ── Component ── + +export function InvoiceEditForm({ resourceId, initialData, onSuccess }: InvoiceEditFormProps) { + const api = useAuthApi() + + const { form, isEditing } = useResourceForm({ + schema: invoiceEditFormSchema, + defaultValues: DEFAULT_VALUES, + resourceId, + initialData, + mapToFormValues, + }) + + const { mutate, error, isPending } = useFormMutation(form, { + mutationFn: (values: InvoiceEditFormValues) => { + const payload = mapFormToPayload(values) + const promise = api.invoices.update(resourceId, payload as any) + toast.promise(promise, { + loading: "Updating invoice...", + success: "Invoice updated successfully", + error: "Failed to update invoice", + }) + return promise + }, + onSuccess: () => { + onSuccess?.() + }, + }) + + return ( + mutate(values)}> + {error && ( + + + Failed to update invoice + {error.message} + + )} + + + + +
+ + + +
+ + + +
+ +
+ + + + + + +
+
+ ) +} diff --git a/apps/dashboard/modules/invoices/invoice-form-summary.tsx b/apps/dashboard/modules/invoices/invoice-form-summary.tsx new file mode 100644 index 0000000..4baa23d --- /dev/null +++ b/apps/dashboard/modules/invoices/invoice-form-summary.tsx @@ -0,0 +1,104 @@ +"use client" + +import { useFormContext } from "react-hook-form" + +import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" +import { + useDocumentTotals, + type DocumentLineItem, +} from "@/shared/hooks/use-document-totals" +import { DocumentTotalsSummary } from "@/shared/components/document-totals-summary" + +import type { InvoiceFormValues } from "./invoice.schema" + +/** + * Parse the numeric rate from an option label formatted as "Title (15%)". + * Returns undefined when no match. + */ +function parseTaxRateFromLabel(label: string | undefined | null): number | undefined { + if (!label) return undefined + const match = label.match(/\((\d+(?:\.\d+)?)%\)/) + return match ? Number(match[1]) : undefined +} + +export function InvoiceFormSummary() { + const { watch } = useFormContext() + + const parts = watch("parts") ?? [] + const services = watch("services") ?? [] + const expenseItems = watch("expense_items") ?? [] + const discountType = watch("discount") + const discountAmount = watch("discount_amount") + const taxRelation = watch("tax") + + // Rate is embedded in the label: "VAT 15% (15%)" → parseTaxRateFromLabel → 15 + const taxRate = parseTaxRateFromLabel(taxRelation?.label) + const taxLabel = taxRelation?.label ?? "Tax" + + // Flatten all line items into the generic shape + const lineItems: DocumentLineItem[] = [ + ...parts.map((p) => ({ + quantity: p.quantity, + rate: p.rate, + discount_amount: p.discount_amount, + })), + ...services.map((s) => ({ + quantity: s.quantity, + rate: s.rate, + discount_amount: s.discount_amount, + })), + ...expenseItems.map((e) => ({ + quantity: e.quantity, + rate: e.rate, + discount_amount: e.discount_amount, + })), + ] + + // Group breakdowns for display (only when more than one group has items) + const groupLabels: Record = {} + if (parts.length > 0) { + groupLabels[`Parts (${parts.length})`] = parts.reduce( + (s, p) => s + (Number(p.quantity) || 0) * (Number(p.rate) || 0), + 0, + ) + } + if (services.length > 0) { + groupLabels[`Services (${services.length})`] = services.reduce( + (s, sv) => s + (Number(sv.quantity) || 0) * (Number(sv.rate) || 0), + 0, + ) + } + if (expenseItems.length > 0) { + groupLabels[`Expenses (${expenseItems.length})`] = expenseItems.reduce( + (s, e) => s + (Number(e.quantity) || 0) * (Number(e.rate) || 0), + 0, + ) + } + + const totals = useDocumentTotals({ + lineItems, + discountType, + discountAmount, + taxRate, + }) + + if (!totals.hasLineItems) return null + + return ( + + + + Summary + + + + 1 ? groupLabels : undefined} + /> + + + ) +} diff --git a/apps/dashboard/modules/invoices/invoice-form.tsx b/apps/dashboard/modules/invoices/invoice-form.tsx index 18dc8a8..34c6c76 100644 --- a/apps/dashboard/modules/invoices/invoice-form.tsx +++ b/apps/dashboard/modules/invoices/invoice-form.tsx @@ -5,6 +5,7 @@ import { AlertTriangle, Plus, Save } from "lucide-react" import { Button } from "@/shared/components/ui/button" import { Alert, AlertTitle } from "@/shared/components/ui/alert" import { FieldGroup } from "@/shared/components/ui/field" +import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" import { Rhform, RhfTextField, @@ -29,19 +30,19 @@ import { import { INVOICE_ROUTES, DEPARTMENT_ROUTES, - ESTIMATE_ROUTES, PAYMENT_TERM_ROUTES, INVOICE_SEQUENCE_ROUTES, - PAYMENT_MODE_ROUTES, - CUSTOMER_ROUTES, + TAX_ROUTES, InvoiceStatus, InvoiceDiscount, } from "@garage/api" import { RhfCustomerSelectField } from "@/modules/customers/rhf-customer-select-field" +import { InvoiceSequenceCrudDialog } from "./invoice-sequence-crud-dialog" 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" +import { InvoiceFormSummary } from "./invoice-form-summary" // ── Constants ── @@ -78,12 +79,14 @@ const DEFAULT_VALUES: InvoiceFormValues = { invoice_to: null, invoice_number: "", invoice_title: "", - invoice_date: "", + invoice_date: new Date().toISOString().split("T")[0], due_date: "", status: "draft", kms_in: undefined, has_insurance: false, discount: "no", + discount_amount: undefined, + tax: null, deposit_to: "", notes: "", terms_and_conditions: "", @@ -116,6 +119,8 @@ function mapToFormValues(data: unknown): InvoiceFormValues { kms_in: d.kms_in ? Number(d.kms_in) : undefined, has_insurance: d.has_insurance ?? false, discount: d.discount || "no", + discount_amount: d.discount_amount != null ? Number(d.discount_amount) : undefined, + tax: toRelation(d.tax_id, d.tax_title ?? d.tax?.title), deposit_to: d.deposit_to || "", notes: d.notes || "", terms_and_conditions: d.terms_and_conditions || "", @@ -124,6 +129,7 @@ function mapToFormValues(data: unknown): InvoiceFormValues { title: p.part?.title ?? p.title ?? "", quantity: Number(p.quantity) || 1, rate: Number(p.rate) || 0, + discount_amount: p.discount_amount != null ? Number(p.discount_amount) : undefined, description: p.description ?? "", })), services: (d.invoice_services ?? d.services ?? []).map((s: any) => ({ @@ -131,6 +137,7 @@ function mapToFormValues(data: unknown): InvoiceFormValues { title: s.service?.labor_name ?? s.labor_name ?? s.title ?? "", quantity: Number(s.quantity) || 1, rate: Number(s.rate) || 0, + discount_amount: s.discount_amount != null ? Number(s.discount_amount) : undefined, description: s.description ?? "", })), expense_items: (d.invoice_expenses ?? d.expenses ?? []).map((e: any) => ({ @@ -138,6 +145,7 @@ function mapToFormValues(data: unknown): InvoiceFormValues { title: e.expense?.item_name ?? e.item_name ?? e.title ?? "", quantity: Number(e.quantity) || 1, rate: Number(e.rate) || 0, + discount_amount: e.discount_amount != null ? Number(e.discount_amount) : undefined, description: e.description ?? "", })), } @@ -163,6 +171,8 @@ function mapFormToPayload(values: InvoiceFormValues) { kms_in: values.kms_in || undefined, has_insurance: values.has_insurance, discount: values.discount || undefined, + discount_amount: values.discount === "transaction_level" ? (values.discount_amount ?? 0) : undefined, + tax_id: toId(values.tax) ? Number(toId(values.tax)) : undefined, deposit_to: values.deposit_to || undefined, notes: values.notes || undefined, terms_and_conditions: values.terms_and_conditions || undefined, @@ -170,18 +180,21 @@ function mapFormToPayload(values: InvoiceFormValues) { part_id: item.part_id, quantity: item.quantity, rate: item.rate, + discount_amount: values.discount === "line_item_level" ? (item.discount_amount ?? 0) : undefined, description: item.description || undefined, })), services: (values.services ?? []).map((item) => ({ service_id: item.service_id, quantity: item.quantity, rate: item.rate, + discount_amount: values.discount === "line_item_level" ? (item.discount_amount ?? 0) : undefined, description: item.description || undefined, })), expenses: (values.expense_items ?? []).map((item) => ({ expense_id: item.expense_id, quantity: item.quantity, rate: item.rate, + discount_amount: values.discount === "line_item_level" ? (item.discount_amount ?? 0) : undefined, description: item.description || undefined, })), } @@ -213,6 +226,25 @@ function InsurerField() { ) } +// ── Transaction-level discount amount field ── + +function TransactionDiscountField() { + const { watch, register } = useFormContext() + const discount = watch("discount") + + if (discount !== "transaction_level") return null + + return ( + + ) +} + // ── Component ── export function InvoiceForm({ resourceId, initialData, onSuccess }: InvoiceFormProps) { @@ -227,6 +259,9 @@ export function InvoiceForm({ resourceId, initialData, onSuccess }: InvoiceFormP mapToFormValues, }) + const discount = form.watch("discount") + const isLineItemDiscount = discount === "line_item_level" + const { mutate, error, isPending } = useFormMutation(form, { mutationFn: (values: InvoiceFormValues) => { const payload = mapFormToPayload(values) @@ -258,116 +293,121 @@ export function InvoiceForm({ resourceId, initialData, onSuccess }: InvoiceFormP )} - - +
-
- - + {/* ── Main column (8/12) ── */} +
+ + + +
+ + +
+ +
+ + +
+ + + + name="parts" showDiscount={isLineItemDiscount} /> + name="services" showDiscount={isLineItemDiscount} /> + name="expense_items" showDiscount={isLineItemDiscount} /> + + + + + +
-
- - + {/* ── Sidebar column (4/12) ── */} +
+ + + Details + + + +
+ + +
+ + name="customer" /> + + + api.taxes.list()} + mapOption={(item: any) => ({ + value: String(item.id), + label: item.title ? `${item.title} (${item.rate}%)` : `#${item.id}`, + })} + {...STORE_OBJECT} + /> + + api.departments.list()} + mapOption={mapLookupOption} + {...STORE_OBJECT} + /> + + api.paymentTerms.list()} + mapOption={mapLookupOption} + {...STORE_OBJECT} + /> + +
+
+ Invoice Sequence + +
+ api.invoiceSequences.list()} + mapOption={(item: any) => ({ + value: String(item.id), + label: item.title || `#${item.id}`, + })} + {...STORE_OBJECT} + /> +
+ + + +
+
+
+ +
+ +
-
- - -
- -
- name="customer" /> - -
- -
- - name="invoice_to" - label="Invoice To" - placeholder="Select billing contact..." - /> - api.departments.list()} - mapOption={mapLookupOption} - {...STORE_OBJECT} - /> -
- -
- api.estimates.list()} - mapOption={(item: any) => ({ - value: String(item.id), - label: item.title||item.estimate_number || `#${item.id}`, - })} - {...STORE_OBJECT} - /> - api.paymentTerms.list()} - mapOption={mapLookupOption} - {...STORE_OBJECT} - /> -
- -
- api.invoiceSequences.list()} - mapOption={(item: any) => ({ - value: String(item.id), - label: item.title || `#${item.id}`, - })} - {...STORE_OBJECT} - /> - -
- -
- api.paymentModes.list()} - mapOption={mapLookupOption} - {...STORE_OBJECT} - /> - -
- - - - - - - - name="parts" /> - name="services" /> - name="expense_items" /> - - - +
) } diff --git a/apps/dashboard/modules/invoices/invoice-general-info.tsx b/apps/dashboard/modules/invoices/invoice-general-info.tsx index 5ec9433..e34c81f 100644 --- a/apps/dashboard/modules/invoices/invoice-general-info.tsx +++ b/apps/dashboard/modules/invoices/invoice-general-info.tsx @@ -1,3 +1,4 @@ +"use client" import { FileText, Calendar, @@ -5,11 +6,12 @@ import { Users, Car, Building2, - CircleDollarSign, Clock, Mail, Phone, - DollarSign, + AlertTriangle, + CheckCircle2, + TimerIcon, } from "lucide-react" import { Card, @@ -19,45 +21,9 @@ import { } from "@/shared/components/ui/card" import { Badge } from "@/shared/components/ui/badge" import { Separator } from "@/shared/components/ui/separator" +import { cn } from "@/shared/lib/utils" 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 -} - -type InvoiceGeneralInfoProps = { - invoice: InvoiceData -} +import { useInvoice } from "./invoice-context" function InfoItem({ icon: Icon, @@ -83,7 +49,7 @@ function InfoItem({ ) } -const statusColorMap: Record = { +const statusVariantMap: Record = { draft: "secondary", open: "default", paid: "default", @@ -91,56 +57,120 @@ const statusColorMap: Record = { void: "outline", } -export function InvoiceGeneralInfo({ invoice }: InvoiceGeneralInfoProps) { +function getDueInfo(dueDateStr?: string, status?: string) { + if (!dueDateStr) return null + const now = new Date() + const due = new Date(dueDateStr) + const diffMs = due.getTime() - now.getTime() + const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24)) + const isPaid = status === "paid" || status === "void" + if (isPaid) return { label: formatDate(dueDateStr), variant: "neutral" as const } + if (diffDays < 0) return { label: `${Math.abs(diffDays)} days overdue`, variant: "overdue" as const } + if (diffDays === 0) return { label: "Due today", variant: "today" as const } + if (diffDays <= 7) return { label: `Due in ${diffDays} day${diffDays === 1 ? "" : "s"}`, variant: "soon" as const } + return { label: formatDate(dueDateStr), variant: "neutral" as const } +} + +export function InvoiceGeneralInfo() { + const _invoice = useInvoice() + if (!_invoice) return null + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const invoice = _invoice as any + const customer = invoice.customer || {} const vehicle = invoice.vehicle || {} const insurer = invoice.insurer || {} + const department = invoice.department || null + + const total = parseFloat(String(invoice.total ?? 0)) || 0 + const paid = parseFloat(String(invoice.payments_recieved ?? invoice.received_payment ?? 0)) || 0 + const balanceDue = parseFloat(String(invoice.balance_due ?? 0)) || 0 + + const dueInfo = getDueInfo(invoice.due_date as string | undefined, invoice.status as string | undefined) return (
- {/* Invoice Details */} - - - - - Invoice Details - - - -
- {invoice.subject && ( - {invoice.subject} - )} - {invoice.status && ( - - {formatEnum(invoice.status)} - - )} -
- -
- - - -
-
-
- {/* Customer & Vehicle Information */} + {/* ── Summary Hero ── */} +
+ {/* Status */} + + Status +
+ {invoice.status === "paid" && } + {invoice.status === "overdue" && } + {(invoice.status === "draft" || invoice.status === "open") && } + + {formatEnum(String(invoice.status ?? ""))} + +
+ {invoice.invoice_number && ( + {invoice.invoice_number} + )} +
+ + {/* Due Date */} + + Due Date + + {formatDate(invoice.due_date) || "—"} + + {dueInfo && dueInfo.variant !== "neutral" && ( + + {dueInfo.label} + + )} + + + {/* Total Amount */} + + Total Amount + {formatCurrency(total)} + {paid > 0 && ( + {formatCurrency(paid)} received + )} + + + {/* Balance Due */} + 0 && invoice.status !== "paid" && "border-primary/40 bg-primary/5", + balanceDue <= 0 && "border-green-500/40 bg-green-50 dark:bg-green-950/20", + )}> + Balance Due + 0 && invoice.status !== "paid" && "text-primary", + balanceDue <= 0 && "text-green-600", + )}> + {formatCurrency(balanceDue)} + + {balanceDue <= 0 && ( + Fully paid + )} + {invoice.discount && invoice.discount !== "no" && ( + Discount: {formatEnum(invoice.discount)} + )} + +
+ + {/* ── Customer & Vehicle ── */}
- {/* Customer Details */} @@ -155,21 +185,9 @@ export function InvoiceGeneralInfo({ invoice }: InvoiceGeneralInfoProps) { label="Customer Name" value={customer.first_name && customer.last_name ? `${customer.first_name} ${customer.last_name}` : invoice.customer_name} /> - - - + + +
{customer.address_line_1 && ( <> @@ -180,8 +198,7 @@ export function InvoiceGeneralInfo({ invoice }: InvoiceGeneralInfoProps) { {customer.address_line_1} {customer.address_line_2 ? `, ${customer.address_line_2}` : ""}
- {customer.city ? `${customer.city}` : ""} - {customer.zip_code ? `, ${customer.zip_code}` : ""} + {customer.city ?? ""}{customer.zip_code ? `, ${customer.zip_code}` : ""}

@@ -189,7 +206,6 @@ export function InvoiceGeneralInfo({ invoice }: InvoiceGeneralInfoProps) { - {/* Vehicle Details */} @@ -204,107 +220,49 @@ export function InvoiceGeneralInfo({ invoice }: InvoiceGeneralInfoProps) { label="Vehicle" value={vehicle.make && vehicle.model ? `${vehicle.make} ${vehicle.model}` : invoice.vehicle_name} /> - - - + + +
{vehicle.mileage && ( <> - + )}
- {/* Payment & Insurance Information */} -
- {/* Payment Information */} - - - - - Payment Information - - - -
- - - -
-
-
+ {/* ── Invoice Meta ── */} + + + + + Invoice Details + + + +
+ + + + + + {invoice.has_insurance && insurer.id && ( + + )} + {invoice.kms_in ? ( + + ) : null} + {invoice.invoice_title ? ( + + ) : null} +
+
+
- {/* Insurance & Additional Info */} - - - - - Additional Information - - - -
- - - {invoice.has_insurance && insurer.id && ( - - )} - {invoice.kms_in && ( - - )} -
-
-
-
- - {/* Notes & Terms */} + {/* ── Notes & Terms ── */} {(invoice.notes || invoice.terms_and_conditions) && (
{invoice.notes && ( @@ -313,7 +271,7 @@ export function InvoiceGeneralInfo({ invoice }: InvoiceGeneralInfoProps) { Notes -

{invoice.notes}

+

{invoice.notes}

)} @@ -323,13 +281,12 @@ export function InvoiceGeneralInfo({ invoice }: InvoiceGeneralInfoProps) { Terms & Conditions -

{invoice.terms_and_conditions}

+

{invoice.terms_and_conditions}

)}
)}
- ) } diff --git a/apps/dashboard/modules/invoices/invoice-payment-button.tsx b/apps/dashboard/modules/invoices/invoice-payment-button.tsx new file mode 100644 index 0000000..20b4ca4 --- /dev/null +++ b/apps/dashboard/modules/invoices/invoice-payment-button.tsx @@ -0,0 +1,61 @@ +"use client" + +import { useQuery } from "@tanstack/react-query" +import { useAuthApi } from "@/shared/useApi" +import { INVOICE_ROUTES, type InvoiceShowData } from "@garage/api" +import { toRelation } from "@/shared/lib/utils" +import FormDialog from "@/shared/components/form-dialog" +import { PaymentReceivedForm } from "@/modules/payment-received/payment-received-form" + +type InvoicePaymentButtonProps = { + invoiceId: string + onSuccess?: () => void +} + +function toTodayString() { + return new Date().toISOString().split("T")[0]! +} + +export function InvoicePaymentButton({ invoiceId, onSuccess }: InvoicePaymentButtonProps) { + const api = useAuthApi() + + const { data } = useQuery({ + queryKey: [INVOICE_ROUTES.BY_ID, invoiceId], + queryFn: () => api.invoices.show(invoiceId), + }) + + const invoice = data?.data + + const total = parseFloat(String(invoice?.amount ?? 0)) || 0 + const paid = parseFloat(String(invoice?.received_payment ?? 0)) || 0 + const balanceDue = Math.max(0, total - paid) + + const customer = invoice?.customer || {} + const customerLabel = customer.first_name + ? `${customer.first_name} ${customer.last_name ?? ""}`.trim() + : invoice?.customer_name ?? undefined + + const prefilledInitialData = { + ...(invoice?.customer_id + ? { customer: toRelation(invoice?.customer_id, customerLabel) } + : {}), + amount_received: balanceDue > 0 ? String(balanceDue) : "", + payment_date: toTodayString(), + } + + return ( + + {(resourceId, { close }) => ( + { + close() + onSuccess?.() + }} + /> + )} + + ) +} diff --git a/apps/dashboard/modules/invoices/invoice-payments-section.tsx b/apps/dashboard/modules/invoices/invoice-payments-section.tsx new file mode 100644 index 0000000..15923f1 --- /dev/null +++ b/apps/dashboard/modules/invoices/invoice-payments-section.tsx @@ -0,0 +1,161 @@ +"use client" +import { CrudResource } from '@/shared/data-view/resource-page' +import { ColumnHeader } from '@/shared/data-view/table-view' +import { BadgeDollarSignIcon, CalendarIcon, ClipboardListIcon, CreditCardIcon, HashIcon, UserIcon } from 'lucide-react' +import { PAYMENT_RECEIVED_ROUTES } from '@garage/api' +import FormDialog from '@/shared/components/form-dialog' +import { PaymentReceivedForm } from '@/modules/payment-received/payment-received-form' +import { useInvoice } from './invoice-context' +import { Card, CardContent } from '@/shared/components/ui/card' +import { useRouter } from 'next/navigation' +export default function InvoicePaymentsSection() { + const invoice = useInvoice() + const router = useRouter() + + console.log("InvoicePaymentsSection invoice:", invoice) + return ( +
+ + extraParams={{ invoice_id: invoice?.id }} + routeKey={PAYMENT_RECEIVED_ROUTES.INDEX} + getClient={(api) => ({ + list: (query?: any) => api.paymentReceived.list(query), + destroy: (id: string) => api.paymentReceived.destroy(id), + })} + tableHeader={({ invalidateQuery }) => + + +

Payments

+ + {(resourceId) => ( + + {router.refresh(); invalidateQuery()}} + /> + + )} + + +
+
+ } + columns={({ actionsColumn }) => [ + { + accessorKey: "payment_number", + header: ({ column }) => , + cell: ({ row }) => { + const item = row.original as any + return ( +
+ + {item.payment_number || "—"} +
+ ) + }, + }, + { + accessorKey: "customer_name", + header: ({ column }) => , + cell: ({ row }) => { + const item = row.original as any + return ( +
+ + {item.customer_name || "—"} +
+ ) + }, + }, + { + accessorKey: "job_card_name", + header: ({ column }) => , + cell: ({ row }) => { + const item = row.original as any + const label = item.job_card_number || item.job_card_name + return ( +
+ + {label || "—"} +
+ ) + }, + }, + { + accessorKey: "amount_received", + header: ({ column }) => , + cell: ({ row }) => { + const item = row.original as any + const amount = item.amount_received + ? Number(item.amount_received).toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }) + : "—" + return ( +
+ + + {amount} + +
+ ) + }, + }, + { + accessorKey: "payment_mode", + header: ({ column }) => , + cell: ({ row }) => { + const item = row.original as any + return ( +
+ + {item.payment_mode?.title || "—"} +
+ ) + }, + }, + { + accessorKey: "payment_date", + header: ({ column }) => , + cell: ({ row }) => { + const item = row.original as any + const formatted = item.payment_date + ? new Date(item.payment_date).toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + }) + : "—" + return ( +
+ + {formatted} +
+ ) + }, + }, + { + accessorKey: "note", + header: () => Note, + enableSorting: false, + cell: ({ row }) => { + const item = row.original as any + const note = item.note + if (!note) return + return ( + + {note} + + ) + }, + }, + // actionsColumn(), + ]} + > +
+ ) +} diff --git a/apps/dashboard/modules/invoices/invoice-sequence-crud-dialog.tsx b/apps/dashboard/modules/invoices/invoice-sequence-crud-dialog.tsx new file mode 100644 index 0000000..18cc3e3 --- /dev/null +++ b/apps/dashboard/modules/invoices/invoice-sequence-crud-dialog.tsx @@ -0,0 +1,45 @@ +"use client" + +import { CrudDialog } from "@/shared/components/crud-dialog" +import { ColumnHeader } from "@/shared/data-view/table-view" +import { useAuthApi } from "@/shared/useApi" +import { INVOICE_SEQUENCE_ROUTES } from "@garage/api" +import { InvoiceSequenceForm } from "./invoice-sequence-form" + +export function InvoiceSequenceCrudDialog() { + const api = useAuthApi() + + return ( + api.invoiceSequences} + resourceLabel="invoice sequence" + columns={() => [ + { + accessorKey: "title", + header: ({ column }) => , + }, + { + accessorKey: "sequence_title", + header: ({ column }) => , + }, + { + accessorKey: "prefix", + header: ({ column }) => , + }, + { + accessorKey: "start_number", + header: ({ column }) => , + }, + ]} + renderForm={({ resourceId, initialData, onSuccess }) => ( + + )} + /> + ) +} diff --git a/apps/dashboard/modules/invoices/invoice-sequence-form.tsx b/apps/dashboard/modules/invoices/invoice-sequence-form.tsx new file mode 100644 index 0000000..4793723 --- /dev/null +++ b/apps/dashboard/modules/invoices/invoice-sequence-form.tsx @@ -0,0 +1,150 @@ +"use client" + +import { z } from "zod" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { Plus, Save } from "lucide-react" +import { Button } from "@/shared/components/ui/button" +import { FieldGroup } from "@/shared/components/ui/field" +import { Rhform, RhfTextField, RhfAsyncSelectField, RhfCheckboxField } from "@/shared/components/form" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" +import { useEffect } from "react" +import { DEPARTMENT_ROUTES } from "@garage/api" + +const invoiceSequenceSchema = z.object({ + title: z.string().min(1, "Title is required"), + sequence_title: z.string().optional(), + prefix: z.string().optional(), + start_number: z.coerce.number().int().min(1).optional(), + auto_generate: z.boolean().optional(), + department: z.object({ value: z.string(), label: z.string() }).nullable().optional(), +}) + +type InvoiceSequenceFormValues = z.infer + +const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label } + +type InvoiceSequenceFormProps = { + resourceId?: string | null + initialData?: any + onSuccess?: () => void +} + +export function InvoiceSequenceForm({ resourceId, initialData, onSuccess }: InvoiceSequenceFormProps) { + const api = useAuthApi() + const isEditing = !!resourceId + + const form = useForm({ + resolver: zodResolver(invoiceSequenceSchema), + defaultValues: { + title: "", + sequence_title: "", + prefix: "", + start_number: 1, + auto_generate: false, + department: null, + }, + }) + + useEffect(() => { + if (initialData) { + const d = initialData?.data ?? initialData + form.reset({ + title: d.title ?? "", + sequence_title: d.sequence_title ?? "", + prefix: d.prefix ?? "", + start_number: d.start_number ?? 1, + auto_generate: d.auto_generate ?? false, + department: d.department_id + ? { value: String(d.department_id), label: d.department_name ?? `#${d.department_id}` } + : null, + }) + } + }, [initialData, form]) + + const handleSubmit = async (values: InvoiceSequenceFormValues) => { + try { + const payload = { + title: values.title, + sequence_title: values.sequence_title || undefined, + prefix: values.prefix || undefined, + start_number: values.start_number, + auto_generate: values.auto_generate, + department_id: values.department ? Number(values.department.value) : undefined, + } + + const promise = isEditing + ? api.invoiceSequences.update(resourceId!, payload) + : api.invoiceSequences.create(payload) + + toast.promise(promise, { + loading: isEditing ? "Updating..." : "Creating...", + success: isEditing ? "Updated successfully" : "Created successfully", + error: isEditing ? "Failed to update" : "Failed to create", + }) + + await promise + form.reset() + onSuccess?.() + } catch { + // toast already shown + } + } + + return ( + + +
+ + +
+ +
+ + +
+ + api.departments.list()} + mapOption={(item: any) => ({ + value: String(item.id), + label: item.name ?? item.title ?? `#${item.id}`, + })} + {...STORE_OBJECT} + /> + + + + +
+
+ ) +} diff --git a/apps/dashboard/modules/invoices/invoice-status-badge.tsx b/apps/dashboard/modules/invoices/invoice-status-badge.tsx new file mode 100644 index 0000000..3a78fb6 --- /dev/null +++ b/apps/dashboard/modules/invoices/invoice-status-badge.tsx @@ -0,0 +1,117 @@ +"use client" + +import { useEffect, useState } from "react" +import { useRouter } from "next/navigation" +import { toast } from "sonner" +import { InvoiceStatus } from "@garage/api" +import { confirm } from "@/shared/components/confirm-dialog" +import { badgeVariants } from "@/shared/components/ui/badge" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/shared/components/ui/select" +import { cn } from "@/shared/lib/utils" +import { formatEnum } from "@/shared/utils/formatters" +import { useAuthApi } from "@/shared/useApi" + +type InvoiceStatusValue = (typeof InvoiceStatus)[number] + +type InvoiceStatusBadgeProps = { + invoice: { + status?: string | null + id: number | string + } +} + +const STATUS_TRIGGER_CLASS_NAMES: Record = { + draft: "border-slate-200 bg-slate-100 text-slate-700 dark:border-slate-800 dark:bg-slate-900/70 dark:text-slate-200", + open: "border-blue-200 bg-blue-100 text-blue-700 dark:border-blue-900 dark:bg-blue-950/70 dark:text-blue-200", + over_due: "border-red-200 bg-red-100 text-red-700 dark:border-red-900 dark:bg-red-950/70 dark:text-red-200", + paid: "border-emerald-200 bg-emerald-100 text-emerald-700 dark:border-emerald-900 dark:bg-emerald-950/70 dark:text-emerald-200", + partially_paid: "border-amber-200 bg-amber-100 text-amber-700 dark:border-amber-900 dark:bg-amber-950/70 dark:text-amber-200", + un_paid: "border-orange-200 bg-orange-100 text-orange-700 dark:border-orange-900 dark:bg-orange-950/70 dark:text-orange-200", +} + +function isInvoiceStatus(value?: string | null): value is InvoiceStatusValue { + return !!value && InvoiceStatus.includes(value as InvoiceStatusValue) +} + +export default function InvoiceStatusBadge({ invoice }: InvoiceStatusBadgeProps) { + const api = useAuthApi() + const router = useRouter() + const [isUpdating, setIsUpdating] = useState(false) + const [status, setStatus] = useState( + isInvoiceStatus(invoice.status) ? invoice.status : undefined, + ) + + useEffect(() => { + setStatus(isInvoiceStatus(invoice.status) ? invoice.status : undefined) + }, [invoice.status]) + + const handleStatusChange = async (nextStatus: string) => { + if (!isInvoiceStatus(nextStatus) || nextStatus === status || isUpdating) { + return + } + + const currentStatus = status + const confirmed = await confirm({ + title: "Change Invoice Status", + description: currentStatus + ? `Change invoice status from ${formatEnum(currentStatus)} to ${formatEnum(nextStatus)}?` + : `Change invoice status to ${formatEnum(nextStatus)}?`, + confirmLabel: "Update", + }) + + if (!confirmed) { + return + } + + setIsUpdating(true) + + try { + const promise = api.invoices.update(String(invoice.id), { status: nextStatus }) + + toast.promise(promise, { + loading: `Updating invoice status to ${formatEnum(nextStatus)}...`, + success: "Invoice status updated successfully.", + error: "Failed to update invoice status.", + }) + + await promise + setStatus(nextStatus) + router.refresh() + } finally { + setIsUpdating(false) + } + } + + return ( + + ) +} diff --git a/apps/dashboard/modules/invoices/invoice-totals-summary.tsx b/apps/dashboard/modules/invoices/invoice-totals-summary.tsx new file mode 100644 index 0000000..ae00a2d --- /dev/null +++ b/apps/dashboard/modules/invoices/invoice-totals-summary.tsx @@ -0,0 +1,102 @@ +"use client" +import { Card, CardContent } from "@/shared/components/ui/card" +import { Separator } from "@/shared/components/ui/separator" +import { formatCurrency, formatEnum } from "@/shared/utils/formatters" +import { cn } from "@/shared/lib/utils" +import { useInvoice } from "./invoice-context" + +export function InvoiceTotalsSummary() { + const invoice = useInvoice() + + if (!invoice) return null + + const parts = invoice.invoice_parts ?? [] + const services = invoice.invoice_services ?? [] + const expenses = invoice.invoice_expenses ?? [] + const discount = invoice.discount + const displayTotal = parseFloat(String(invoice.total ?? 0)) || 0 + const paid = parseFloat(String(invoice.payments_recieved ?? invoice.received_payment ?? 0)) || 0 + const balanceDue = parseFloat(String(invoice.balance_due ?? 0)) || 0 + + const hasItems = parts.length > 0 || services.length > 0 || expenses.length > 0 + + if (!hasItems && displayTotal === 0) return null + + const subTotal = parseFloat(String(invoice.sub_total ?? 0)) || 0 + + function lineTotal(items: { quantity?: string | number; rate?: string | number }[]) { + return items.reduce((sum, item) => { + const qty = parseFloat(String(item.quantity ?? 0)) + const rate = parseFloat(String(item.rate ?? 0)) + return sum + (isNaN(qty) || isNaN(rate) ? 0 : qty * rate) + }, 0) + } + + return ( + + +
+ {hasItems && ( + <> + {parts.length > 0 && ( +
+ Parts ({parts.length}) + {formatCurrency(lineTotal(parts))} +
+ )} + {services.length > 0 && ( +
+ Services ({services.length}) + {formatCurrency(lineTotal(services))} +
+ )} + {expenses.length > 0 && ( +
+ Expenses ({expenses.length}) + {formatCurrency(lineTotal(expenses))} +
+ )} + + + )} + +
+ Subtotal + {formatCurrency(subTotal)} +
+ + {discount && discount !== "no" && ( +
+ Discount ({formatEnum(discount)}) + Applied +
+ )} + + + +
+ Total + {formatCurrency(displayTotal)} +
+ + {paid > 0 && ( +
+ Amount Received + – {formatCurrency(paid)} +
+ )} + + + +
0 ? "bg-primary/10 text-primary" : "bg-green-500/10 text-green-600", + )}> + Balance Due + {formatCurrency(balanceDue)} +
+
+
+
+ ) +} diff --git a/apps/dashboard/modules/invoices/invoice.schema.ts b/apps/dashboard/modules/invoices/invoice.schema.ts index d7fbda6..8d3b7b8 100644 --- a/apps/dashboard/modules/invoices/invoice.schema.ts +++ b/apps/dashboard/modules/invoices/invoice.schema.ts @@ -9,6 +9,7 @@ const invoicePartItemSchema = z.object({ title: z.string(), quantity: z.number().min(1), rate: z.number().min(0), + discount_amount: z.number().min(0).optional(), description: z.string().optional(), }) @@ -17,6 +18,7 @@ const invoiceServiceItemSchema = z.object({ title: z.string(), quantity: z.number().min(1), rate: z.number().min(0), + discount_amount: z.number().min(0).optional(), description: z.string().optional(), }) @@ -25,6 +27,7 @@ const invoiceExpenseItemSchema = z.object({ title: z.string(), quantity: z.number().min(1), rate: z.number().min(0), + discount_amount: z.number().min(0).optional(), description: z.string().optional(), }) @@ -52,6 +55,8 @@ const invoiceFormSchema = z.object({ kms_in: z.coerce.number().optional(), has_insurance: z.boolean().default(false), discount: z.string().optional(), + discount_amount: z.coerce.number().min(0).optional(), + tax: relationFieldSchema, deposit_to: z.string().optional(), notes: z.string().optional(), terms_and_conditions: z.string().optional(), diff --git a/apps/dashboard/modules/job-cards/job-card-dropdown.tsx b/apps/dashboard/modules/job-cards/job-card-dropdown.tsx index f81861e..207063e 100644 --- a/apps/dashboard/modules/job-cards/job-card-dropdown.tsx +++ b/apps/dashboard/modules/job-cards/job-card-dropdown.tsx @@ -1,17 +1,26 @@ "use client" import { confirm } from '@/shared/components/confirm-dialog'; -import { api } from '@garage/api'; -import { useRouter } from 'next/dist/client/components/navigation'; + import { useRouter } from 'next/dist/client/components/navigation'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/shared/components/ui/dropdown-menu' import { Button } from '@/shared/components/ui/button' import { toast } from 'sonner' -import { Ellipsis, Pencil, Printer, Trash2 } from 'lucide-react'; +import { Ellipsis, FileText, Pencil, Printer, Trash2 } from 'lucide-react'; import { useDocumentPrint } from '@/shared/hooks/use-document-print'; +import { useJobCard } from './job-card-context'; +import { useState } from 'react'; +import { useAuthApi } from '@/shared/useApi'; + +// TODO: setting a sales person not working +// TODO: unable to set a Primary technician for the job card. Need to investigate and fix it. + export default function JobCardDropdown({ id }: { id: string }) { + const api = useAuthApi() const router = useRouter(); const { print, isPrinting } = useDocumentPrint() + const jobCard = useJobCard() + const [isConverting, setIsConverting] = useState(false) const handleEdit = () => { router.push(`/sales/job-cards/${id}/edit`) @@ -21,6 +30,35 @@ export default function JobCardDropdown({ id }: { id: string }) { print("job_card", id, "print") } + const handleConvertToInvoice = async () => { + const confirmed = await confirm({ + title: "Convert to Invoice", + description: "This will create a new invoice from this job card. Do you want to continue?", + confirmLabel: "Convert", + }) + if (!confirmed) return + + setIsConverting(true) + try { + const res = await api.jobCards.convertToInvoice(id, {}) as any + const invoiceId = res?.data?.id + toast.success("Job card converted to invoice successfully") + if (invoiceId) { + router.push(`/sales/invoice/${invoiceId}`) + } + } catch (err: any) { + const conflictId = err?.response?.data?.data?.invoice_id ?? err?.data?.data?.invoice_id + if (conflictId) { + toast.info("An invoice already exists for this job card.") + router.push(`/sales/invoice/${conflictId}`) + } else { + toast.error("Failed to convert job card to invoice") + } + } finally { + setIsConverting(false) + } + } + const handleDelete = async () => { const confirmed = await confirm({ title: "Delete Job Card", @@ -57,6 +95,15 @@ export default function JobCardDropdown({ id }: { id: string }) { {isPrinting ? "Printing..." : "Print"} + {jobCard?.status !== "draft" && ( + <> + + + + {isConverting ? "Converting..." : "Convert to Invoice"} + + + )} diff --git a/apps/dashboard/modules/job-cards/job-card-expense-item-form.tsx b/apps/dashboard/modules/job-cards/job-card-expense-item-form.tsx new file mode 100644 index 0000000..4d0f2d5 --- /dev/null +++ b/apps/dashboard/modules/job-cards/job-card-expense-item-form.tsx @@ -0,0 +1,280 @@ +"use client" + +import React from "react" +import { AlertTriangle, Plus, Save } from "lucide-react" +import { z } from "zod" + +import { Button } from "@/shared/components/ui/button" +import { Alert, AlertTitle } from "@/shared/components/ui/alert" +import { FieldGroup } from "@/shared/components/ui/field" +import { + Rhform, + RhfTextField, + RhfTextareaField, + RhfAsyncSelectField, +} from "@/shared/components/form" +import { DepartmentInlineForm } from "@/modules/services/inline-forms/department-inline-form" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { DEPARTMENT_ROUTES, TAX_ROUTES } from "@garage/api" +import { useJobCard } from "./job-card-context" + +// ── Schema ── + +const relationFieldSchema = z.object({ value: z.string(), label: z.string() }).nullable() + +const jobCardExpenseItemFormSchema = z.object({ + expense_item: relationFieldSchema, + department: relationFieldSchema.optional(), + tax: relationFieldSchema.optional(), + quantity: z.coerce.number().min(1, "Quantity is required"), + rate: z.coerce.number().min(0, "Rate is required"), + discount_amount: z.coerce.number().min(0).optional(), + chart_of_account: z.string().optional(), + description: z.string().optional(), +}) + +type JobCardExpenseItemFormValues = z.infer + +// ── Props ── + +export type JobCardExpenseItemFormProps = { + jobCardId: string + jobCardExpenseItemId?: number | null + initialData?: unknown + onSuccess?: () => void + onCancel?: () => void +} + +const DEFAULT_VALUES: JobCardExpenseItemFormValues = { + expense_item: null, + department: null, + tax: null, + quantity: 1, + rate: 0, + discount_amount: undefined, + chart_of_account: "", + description: "", +} + +const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label } + +function mapToFormValues(data: unknown): JobCardExpenseItemFormValues { + const d = (data as any) ?? {} + return { + expense_item: d.expense_item + ? { value: String(d.expense_item.id), label: d.expense_item.item_name ?? String(d.expense_item.id) } + : null, + department: d.department + ? { value: String(d.department.id), label: d.department.name ?? String(d.department.id) } + : null, + tax: d.tax_id != null + ? { value: String(d.tax_id), label: d.tax ? `${d.tax.title} (${d.tax.rate}%)` : String(d.tax_id) } + : null, + quantity: d.quantity ?? 1, + rate: d.rate != null ? Number(d.rate) : 0, + discount_amount: d.discount_amount != null ? Number(d.discount_amount) : undefined, + chart_of_account: d.chart_of_account ?? "", + description: d.description ?? "", + } +} + +// ── Component ── + +export function JobCardExpenseItemForm({ + jobCardId, + jobCardExpenseItemId, + initialData, + onSuccess, + onCancel, +}: JobCardExpenseItemFormProps) { + const api = useAuthApi() + const jobCard = useJobCard() + const isEditing = !!jobCardExpenseItemId + const isLineItemDiscount = (jobCard as any)?.discount_type === "line_item_level" + + const form = useForm({ + resolver: zodResolver(jobCardExpenseItemFormSchema) as any, + defaultValues: initialData + ? mapToFormValues(initialData) + : DEFAULT_VALUES, + }) + + const [error, setError] = React.useState(null) + const [isPending, setIsPending] = React.useState(false) + + async function handleSubmit(values: JobCardExpenseItemFormValues) { + setError(null) + setIsPending(true) + try { + if (isEditing && jobCardExpenseItemId) { + await toast.promise( + api.jobCards.updateExpenseItem(jobCardId, { + job_card_expense_item_id: jobCardExpenseItemId, + quantity: values.quantity, + rate: values.rate, + discount_amount: isLineItemDiscount ? (values.discount_amount ?? 0) : undefined, + description: values.description || undefined, + }), + { + loading: "Updating expense item...", + success: "Expense item updated successfully", + error: "Failed to update expense item", + } + ) + } else { + await toast.promise( + api.jobCards.addExpenseItem(jobCardId, { + expense_item_id: values.expense_item ? Number(values.expense_item.value) : undefined, + department_id: values.department ? Number(values.department.value) : undefined, + tax_id: values.tax ? Number(values.tax.value) : undefined, + quantity: values.quantity, + rate: values.rate, + discount_amount: isLineItemDiscount ? (values.discount_amount ?? 0) : undefined, + chart_of_account: values.chart_of_account || undefined, + description: values.description || undefined, + }), + { + loading: "Adding expense item...", + success: "Expense item added successfully", + error: "Failed to add expense item", + } + ) + } + form.reset() + onSuccess?.() + } catch (err: any) { + setError(err?.message ?? "An unexpected error occurred") + } finally { + setIsPending(false) + } + } + + return ( + + {error && ( + + + + {isEditing ? "Failed to update expense item" : "Failed to add expense item"} + + {error} + + )} + + + {!isEditing && ( + api.expenses.listItems()} + mapOption={(item: any) => ({ + value: String(item.id), + label: item.item_name ?? item.title ?? String(item.id), + })} + {...STORE_OBJECT} + /> + )} + +
+ + +
+ + {isLineItemDiscount && ( + + )} + + {!isEditing && ( + <> +
+ api.departments.list()} + mapOption={(item: any) => ({ + value: String(item.id), + label: item.name ?? String(item.id), + })} + createForm={(props) => } + createLabel="Department" + {...STORE_OBJECT} + /> + api.taxes.list()} + mapOption={(item: any) => ({ + value: String(item.id), + label: item.title ? `${item.title} (${item.rate}%)` : `#${item.id}`, + })} + {...STORE_OBJECT} + /> +
+ + + + )} + + +
+ +
+ {onCancel && ( + + )} + +
+
+ ) +} diff --git a/apps/dashboard/modules/job-cards/job-card-form.tsx b/apps/dashboard/modules/job-cards/job-card-form.tsx index 4031a7d..e10ea10 100644 --- a/apps/dashboard/modules/job-cards/job-card-form.tsx +++ b/apps/dashboard/modules/job-cards/job-card-form.tsx @@ -26,8 +26,9 @@ import { type JobCardFormValues, FUEL_LEVEL_OPTIONS, JOB_CARD_STATUS_OPTIONS, + DISCOUNT_TYPE_OPTIONS, } from "./job-card.schema" -import { JOB_CARD_ROUTES, EMPLOYEE_ROUTES, DEPARTMENT_ROUTES, INSURANCE_TYPE_ROUTES } from "@garage/api" +import { JOB_CARD_ROUTES, EMPLOYEE_ROUTES, DEPARTMENT_ROUTES, INSURANCE_TYPE_ROUTES, TAX_ROUTES } from "@garage/api" import { RhfCustomerSelectField } from "@/modules/customers/rhf-customer-select-field" import { RhfVehicleSelectField } from "@/modules/vehicles/rhf-vehicle-select-field" import { RhfLabelPickerField } from "@/modules/labels/rhf-label-picker-field" @@ -55,6 +56,7 @@ const DEFAULT_VALUES: JobCardFormValues = { sales_person: null, insurance_type: null, insurer: null, + tax: null, order_number: "", estimate_number: "", status: "check_in", @@ -104,6 +106,7 @@ function mapToFormValues(data: unknown): JobCardFormValues { sales_person: toRelation(d.sales_person_id, d.sales_person ? `${d.sales_person.first_name} ${d.sales_person.last_name}` : undefined), insurance_type: toRelation(d.insurance_type_id, d.insurance_type?.name), insurer: toRelation(d.insurer_id, d.insurer ? `${d.insurer.first_name} ${d.insurer.last_name}`.trim() || d.insurer.company_name : undefined), + tax: toRelation(d.tax_id, d.tax ? `${d.tax.title} (${d.tax.rate}%)` : undefined), order_number: d.order_number || "", estimate_number: d.estimate_number || "", status: d.status || "draft", @@ -148,6 +151,7 @@ function mapFormToPayload(values: JobCardFormValues) { sales_person_id: toId(values.sales_person), insurance_type_id: values.insurance_type ? String(toId(values.insurance_type)) : null, insurer_id: values.insurer ? String(toId(values.insurer)) : null, + tax_id: toId(values.tax) ?? undefined, estimate_number: values.estimate_number || undefined, order_number: values.order_number || undefined, status: values.status || undefined, @@ -347,6 +351,24 @@ export function JobCardForm({ resourceId, initialData, onSuccess }: JobCardFormP placeholder="Select status" options={JOB_CARD_STATUS_OPTIONS} /> + + api.taxes.list()} + mapOption={(item: any) => ({ + value: String(item.id), + label: item.title ? `${item.title} (${item.rate}%)` : `#${item.id}`, + })} + {...STORE_OBJECT} + /> ['data']> +type JobCard = JobCardShowData function InfoItem({ diff --git a/apps/dashboard/modules/job-cards/job-card-part-form.tsx b/apps/dashboard/modules/job-cards/job-card-part-form.tsx index 6ced717..d2a267b 100644 --- a/apps/dashboard/modules/job-cards/job-card-part-form.tsx +++ b/apps/dashboard/modules/job-cards/job-card-part-form.tsx @@ -1,4 +1,4 @@ -"use client" +"use client" import React from "react" import { AlertTriangle, Plus, Save } from "lucide-react" @@ -18,26 +18,27 @@ import { toast } from "sonner" import { useAuthApi } from "@/shared/useApi" import { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" +import { DEPARTMENT_ROUTES, TAX_ROUTES } from "@garage/api" +import { useJobCard } from "./job-card-context" -// ── Schema ── +// ── Schema ── + +const relationFieldSchema = z.object({ value: z.string(), label: z.string() }).nullable() const jobCardPartFormSchema = z.object({ - part: z - .object({ value: z.string(), label: z.string() }) - .nullable(), - department: z - .object({ value: z.string(), label: z.string() }) - .nullable() - .optional(), + part: relationFieldSchema, + department: relationFieldSchema.optional(), + tax: relationFieldSchema.optional(), quantity: z.coerce.number().min(1, "Quantity is required"), rate: z.coerce.number().min(0, "Rate is required"), - tax: z.string().optional(), + discount_amount: z.coerce.number().min(0).optional(), + chart_of_account: z.string().optional(), description: z.string().optional(), }) type JobCardPartFormValues = z.infer -// ── Props ── +// ── Props ── export type JobCardPartFormProps = { jobCardId: string @@ -50,9 +51,11 @@ export type JobCardPartFormProps = { const DEFAULT_VALUES: JobCardPartFormValues = { part: null, department: null, + tax: null, quantity: 1, rate: 0, - tax: "", + discount_amount: undefined, + chart_of_account: "", description: "", } @@ -67,14 +70,18 @@ function mapToFormValues(data: unknown): JobCardPartFormValues { department: d.department ? { value: String(d.department.id), label: d.department.name ?? String(d.department.id) } : null, + tax: d.tax_id != null + ? { value: String(d.tax_id), label: d.tax ? `${d.tax.title} (${d.tax.rate}%)` : String(d.tax_id) } + : null, quantity: d.quantity ?? 1, rate: d.rate != null ? Number(d.rate) : 0, - tax: d.tax ?? "", + discount_amount: d.discount_amount != null ? Number(d.discount_amount) : undefined, + chart_of_account: d.chart_of_account ?? "", description: d.description ?? "", } } -// ── Component ── +// ── Component ── export function JobCardPartForm({ jobCardId, @@ -84,7 +91,9 @@ export function JobCardPartForm({ onCancel, }: JobCardPartFormProps) { const api = useAuthApi() + const jobCard = useJobCard() const isEditing = !!jobCardPartId + const isLineItemDiscount = (jobCard as any)?.discount_type === "line_item_level" const form = useForm({ resolver: zodResolver(jobCardPartFormSchema) as any, @@ -106,6 +115,7 @@ export function JobCardPartForm({ job_card_part_id: jobCardPartId, quantity: values.quantity, rate: values.rate, + discount_amount: isLineItemDiscount ? (values.discount_amount ?? 0) : undefined, description: values.description || undefined, }), { @@ -119,9 +129,11 @@ export function JobCardPartForm({ api.jobCards.addPart(jobCardId, { part_id: values.part ? Number(values.part.value) : undefined, department_id: values.department ? Number(values.department.value) : undefined, + tax_id: values.tax ? Number(values.tax.value) : undefined, quantity: values.quantity, rate: values.rate, - tax: values.tax || undefined, + discount_amount: isLineItemDiscount ? (values.discount_amount ?? 0) : undefined, + chart_of_account: values.chart_of_account || undefined, description: values.description || undefined, }), { @@ -186,28 +198,52 @@ export function JobCardPartForm({ />
+ {isLineItemDiscount && ( + + )} + {!isEditing && ( -
- api.departments.list()} - mapOption={(item: any) => ({ - value: String(item.id), - label: item.name ?? String(item.id), - })} - createForm={(props) => } - createLabel="Department" - {...STORE_OBJECT} - /> + <> +
+ api.departments.list()} + mapOption={(item: any) => ({ + value: String(item.id), + label: item.name ?? String(item.id), + })} + createForm={(props) => } + createLabel="Department" + {...STORE_OBJECT} + /> + api.taxes.list()} + mapOption={(item: any) => ({ + value: String(item.id), + label: item.title ? `${item.title} (${item.rate}%)` : `#${item.id}`, + })} + {...STORE_OBJECT} + /> +
+ -
+ )} ({ resolver: zodResolver(jobCardServiceFormSchema) as any, @@ -120,6 +122,9 @@ export function JobCardServiceForm({ : DEFAULT_VALUES, }) + const rateType = form.watch("rate_type") + const isHourly = rateType === "hourly" + const [error, setError] = React.useState(null) const [isPending, setIsPending] = React.useState(false) @@ -133,6 +138,7 @@ export function JobCardServiceForm({ job_card_service_id: jobCardServiceId, quantity: values.quantity, rate: values.rate, + discount_amount: isLineItemDiscount ? (values.discount_amount ?? 0) : undefined, description: values.description || undefined, }), { @@ -146,13 +152,14 @@ export function JobCardServiceForm({ api.jobCards.addService(jobCardId, { service_id: values.service ? Number(values.service.value) : undefined, department_id: values.department ? Number(values.department.value) : undefined, + tax_id: values.tax ? Number(values.tax.value) : undefined, rate_type: values.rate_type || undefined, labor_rate_id: values.labor_rate ? Number(values.labor_rate.value) : undefined, quantity: values.quantity, rate: values.rate, working_hours: values.working_hours || undefined, labor_hours: values.labor_hours || undefined, - tax: values.tax || undefined, + discount_amount: isLineItemDiscount ? (values.discount_amount ?? 0) : undefined, chart_of_account: values.chart_of_account || undefined, description: values.description || undefined, }), @@ -218,6 +225,15 @@ export function JobCardServiceForm({ />
+ {isLineItemDiscount && ( + + )} + {!isEditing && ( <>
@@ -241,27 +257,30 @@ export function JobCardServiceForm({ />
-
- - -
+ {isHourly && ( +
+ + +
+ )}
api.departments.list()} mapOption={(item: any) => ({ value: String(item.id), @@ -271,10 +290,17 @@ export function JobCardServiceForm({ createLabel="Department" {...STORE_OBJECT} /> - api.taxes.list()} + mapOption={(item: any) => ({ + value: String(item.id), + label: item.title ? `${item.title} (${item.rate}%)` : `#${item.id}`, + })} + {...STORE_OBJECT} />
@@ -302,9 +328,21 @@ export function JobCardServiceForm({
) } + + \ No newline at end of file diff --git a/apps/dashboard/modules/job-cards/job-card.schema.ts b/apps/dashboard/modules/job-cards/job-card.schema.ts index d17489b..998cace 100644 --- a/apps/dashboard/modules/job-cards/job-card.schema.ts +++ b/apps/dashboard/modules/job-cards/job-card.schema.ts @@ -59,6 +59,7 @@ const jobCardFormSchema = z.object({ sales_person: relationFieldSchema, insurance_type: relationFieldSchema, insurer: relationFieldSchema, + tax: relationFieldSchema, // ── Numbers & identifiers ── order_number: z.string().optional(), diff --git a/apps/dashboard/modules/parts/parts-selector-field.tsx b/apps/dashboard/modules/parts/parts-selector-field.tsx index 10e5d9f..35c3e4f 100644 --- a/apps/dashboard/modules/parts/parts-selector-field.tsx +++ b/apps/dashboard/modules/parts/parts-selector-field.tsx @@ -22,6 +22,7 @@ type PartItem = { title: string quantity: number rate: number + discount_amount?: number description?: string } @@ -34,6 +35,7 @@ export type PartsSelectorFieldProps< name: TName & (TValues[TName] extends PartsItemsFieldConstraint ? TName : never) label?: string triggerLabel?: string + showDiscount?: boolean } export function PartsSelectorField< @@ -43,6 +45,7 @@ export function PartsSelectorField< name, label = "Parts", triggerLabel = "Add Parts", + showDiscount = false, }: PartsSelectorFieldProps) { return ( @@ -81,6 +84,7 @@ export function PartsSelectorField< Part Qty Rate + {showDiscount && Discount} Description @@ -112,6 +116,20 @@ export function PartsSelectorField< className="h-8 w-24" /> + {showDiscount && ( + + + update(index, { ...item, discount_amount: Number(e.target.value) || 0 } as any) + } + className="h-8 w-24" + /> + + )} void + billId?: string | number | null + expenseId?: string | number | null } // ── Default values ── -const DEFAULT_VALUES: PaymentMadeFormValues = { +const DEFAULT_VALUES: PaymentMadeFormValues & { details: Array<{ bill_id?: string | number | null; amount_paid?: number, expense_id?: string | number | null }> } = { vendor: null, employee: null, payment_mode: null, - payment_for: "", - payment_made: "", + payment_for: "bill", + amount: "", payment_number: "", payment_reference: "", - payment_date: "", + payment_date: getTodayDate(), paid_through: "", notes: "", + details: [] } // ── Mapping helpers ── -function mapToFormValues(data: unknown): PaymentMadeFormValues { +function mapToFormValues(data: unknown): typeof DEFAULT_VALUES { const d = (data as any)?.data ?? data ?? {} + // Resolve payment_mode label from nested object (title) or fallback to id + const paymentModeId = d.payment_mode_id ?? d.payment_mode?.id + const paymentModeLabel = d.payment_mode?.title ?? d.payment_mode?.name ?? d.payment_mode_name + return { - vendor: toRelation(d.vendor_id, d.vendor_name), - employee: toRelation(d.employee_id, d.employee_name), - payment_mode: toRelation(d.payment_mode_id, d.payment_mode_name), + vendor: toRelation(d.vendor_id, d.vendor?.company_name ?? d.vendor?.name ?? d.vendor_name), + employee: toRelation(d.employee_id, d.employee?.first_name ? `${d.employee.first_name} ${d.employee.last_name ?? ""}`.trim() : d.employee_name), + payment_mode: toRelation(paymentModeId, paymentModeLabel), payment_for: d.payment_for || "", - payment_made: d.payment_made ? String(d.payment_made) : "", + amount: d.payment_made ? String(d.payment_made) : "", payment_number: d.payment_number || "", payment_reference: d.payment_reference || "", payment_date: d.payment_date || "", - paid_through: d.paid_through || "", - notes: d.notes || "", + paid_through: d.paid_through || "-", + notes: d.notes || "-", + details: [{ bill_id: d.bill_id, amount_paid: d.amount_paid }], } } -function mapFormToPayload(values: PaymentMadeFormValues) { + +function mapFormToPayload(values: PaymentMadeFormValues, billId?: string | number | null, expenseId?: string | number | null) { + const paymentDetails = + expenseId ? [{ expense_id: expenseId, amount_paid: values.amount ? Number(values.amount) : 0 }] + : [{ bill_id: billId, amount_paid: values.amount ? Number(values.amount) : 0 }] return { vendor_id: toId(values.vendor), employee_id: toId(values.employee) || undefined, payment_mode_id: toId(values.payment_mode), payment_for: values.payment_for, - payment_made: values.payment_made, + amount: values.amount ? Number(values.amount) : 0, payment_number: values.payment_number || undefined, payment_reference: values.payment_reference || undefined, payment_date: values.payment_date, paid_through: values.paid_through || undefined, notes: values.notes || undefined, + payment_made: values.amount, + details: paymentDetails, + ...(billId ? { bill_id: Number(billId) } : {}), + ...(expenseId ? { expense_id: Number(expenseId) } : {}), } } @@ -117,20 +136,32 @@ const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) = // ── Component ── -export function PaymentMadeForm({ resourceId, initialData, onSuccess }: PaymentMadeFormProps) { +export function PaymentMadeForm({ resourceId, initialData, onSuccess, billId, expenseId }: PaymentMadeFormProps) { const api = useAuthApi() + const resolvedInitialData = useMemo(() => { + const base: any = { ...(initialData as any) } + if (!resourceId) { + if (billId) { + base.payment_for = "bill" + } else if (expenseId) { + base.payment_for = "expense" + } + } + return Object.keys(base).length ? base : initialData + }, [resourceId, billId, expenseId, initialData]) + const { form, isEditing } = useResourceForm({ schema: paymentMadeFormSchema, defaultValues: DEFAULT_VALUES, resourceId, - initialData, + initialData: resolvedInitialData, mapToFormValues, }) const { mutate, error, isPending } = useFormMutation(form, { mutationFn: (values: PaymentMadeFormValues) => { - const payload = mapFormToPayload(values) + const payload = mapFormToPayload(values, billId, expenseId) const promise = (isEditing && resourceId ? api.paymentMades.update(resourceId, payload as any) : api.paymentMades.create(payload as any)) as Promise @@ -161,14 +192,9 @@ export function PaymentMadeForm({ resourceId, initialData, onSuccess }: PaymentM
- api.vendors.list()} - mapOption={mapVendorOption} - {...STORE_OBJECT} />
-
- void defaultJobCard?: { id?: number | null; title?: string | null } | null + invoiceId?: string | null + invoiceCustomer?: { id?: number | null; first_name?: string | null; last_name?: string | null } | null + invoiceAmount?: number | string | null } // ── Default values ── @@ -41,7 +46,7 @@ const DEFAULT_VALUES: PaymentReceivedFormValues = { customer: null, amount_received: "", payment_number: "", - payment_date: "", + payment_date: new Date().toISOString().split("T")[0], note: "", } @@ -61,7 +66,7 @@ function mapToFormValues(data: unknown): PaymentReceivedFormValues { } } -function mapFormToPayload(values: PaymentReceivedFormValues) { +function mapFormToPayload(values: PaymentReceivedFormValues, invoiceId?: string | null) { return { job_card_id: toId(values.job_card), payment_mode_id: toId(values.payment_mode), @@ -70,6 +75,7 @@ function mapFormToPayload(values: PaymentReceivedFormValues) { payment_number: values.payment_number || undefined, payment_date: values.payment_date, note: values.note || undefined, + ...(invoiceId ? { invoice_id: Number(invoiceId) } : {}), } } @@ -84,18 +90,29 @@ const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) = // ── Component ── -export function PaymentReceivedForm({ resourceId, initialData, onSuccess, defaultJobCard }: PaymentReceivedFormProps) { +export function PaymentReceivedForm({ resourceId, initialData, onSuccess, defaultJobCard, invoiceId, invoiceCustomer, invoiceAmount }: PaymentReceivedFormProps) { const api = useAuthApi() const resolvedInitialData = useMemo(() => { - if (!resourceId && defaultJobCard?.id != null) { - return { - ...(initialData as any), - job_card: toRelation(defaultJobCard.id, defaultJobCard.title ?? undefined), + const base: any = { ...(initialData as any) } + if (!resourceId) { + if (defaultJobCard?.id != null) { + base.job_card = toRelation(defaultJobCard.id, defaultJobCard.title ?? undefined) + } + if (invoiceCustomer?.id != null) { + base.customer = toRelation( + invoiceCustomer.id, + invoiceCustomer.first_name + ? `${invoiceCustomer.first_name} ${invoiceCustomer.last_name || ""}`.trim() + : undefined + ) + } + if (invoiceAmount != null && invoiceAmount !== "") { + base.amount_received = String(invoiceAmount) } } - return initialData - }, [resourceId, defaultJobCard, initialData]) + return Object.keys(base).length ? base : initialData + }, [resourceId, defaultJobCard, initialData, invoiceCustomer, invoiceAmount]) const { form, isEditing } = useResourceForm({ schema: paymentReceivedFormSchema, @@ -107,7 +124,7 @@ export function PaymentReceivedForm({ resourceId, initialData, onSuccess, defaul const { mutate, error, isPending } = useFormMutation(form, { mutationFn: (values: PaymentReceivedFormValues) => { - const payload = mapFormToPayload(values) + const payload = mapFormToPayload(values, invoiceId) const promise = isEditing && resourceId ? api.paymentReceived.update(resourceId, payload as any) : api.paymentReceived.create(payload as any) @@ -186,16 +203,16 @@ export function PaymentReceivedForm({ resourceId, initialData, onSuccess, defaul
- -
diff --git a/apps/dashboard/modules/purchase-orders/create-bill-from-po-button.tsx b/apps/dashboard/modules/purchase-orders/create-bill-from-po-button.tsx index 2effd8c..33c31b5 100644 --- a/apps/dashboard/modules/purchase-orders/create-bill-from-po-button.tsx +++ b/apps/dashboard/modules/purchase-orders/create-bill-from-po-button.tsx @@ -13,6 +13,7 @@ import { ScrollArea } from "@/shared/components/ui/scroll-area" import { BillForm } from "@/modules/bills/bill-form" import { toRelation } from "@/shared/lib/utils" import { usePurchaseOrder } from "./purchase-order-context" +import { getFullName } from "@/shared/utils/getFullName" /** * Maps a Purchase Order data object to a complete BillFormValues shape so that @@ -26,9 +27,9 @@ function mapPOToBillInitialData(po: Record) { notes: po.notes ?? "", // Relation fields — must be { value, label } objects for RhfAsyncSelectField - vendor: toRelation(po.vendor_id, po.vendor_name), - department: toRelation(po.department_id, po.department_name), - job_card: toRelation(po.job_card_id, po.job_card_name ?? po.job_card_number), + vendor: toRelation(po.vendor_id, getFullName(po.vendor)), + department: toRelation(po.department_id, po.department.name), + job_card: toRelation(po.job_card_id, po.job_card.title ), // Link bill back to the source PO purchase_order: toRelation(po.id, po.order_number ?? po.title), diff --git a/apps/dashboard/modules/purchase-orders/purchase-order-form.tsx b/apps/dashboard/modules/purchase-orders/purchase-order-form.tsx index 410b395..25d177a 100644 --- a/apps/dashboard/modules/purchase-orders/purchase-order-form.tsx +++ b/apps/dashboard/modules/purchase-orders/purchase-order-form.tsx @@ -11,8 +11,10 @@ import { RhfTextareaField, RhfAsyncSelectField, RhfDateField, + RhfAutoGenerateField, } from "@/shared/components/form" import { PartsSelectorField } from "@/modules/parts/parts-selector-field" +import { VendorForm } from "@/modules/vendors/vendor-form" import { toast } from "sonner" import { useAuthApi } from "@/shared/useApi" import { useResourceForm } from "@/shared/hooks/use-resource-form" @@ -38,7 +40,7 @@ export type PurchaseOrderFormProps = { const DEFAULT_VALUES: PurchaseOrderFormValues = { vendor: null, - order_number: "" , + order_number: "", job_card: null, department: null, title: "", @@ -98,6 +100,13 @@ const mapLookupOption = (item: any) => ({ label: item.name, }) +const getVendorLabel = (item: any) => getFullName(item) || item.company_name || item.name || `#${item.id}` + +const mapVendorOption = (item: any) => ({ + value: String(item.id), + label: getVendorLabel(item), +}) + const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label } // ── Component ── @@ -147,8 +156,8 @@ export function PurchaseOrderForm({ resourceId, initialData, onSuccess }: Purcha
- - + +
@@ -160,7 +169,16 @@ export function PurchaseOrderForm({ resourceId, initialData, onSuccess }: Purcha placeholder="Select vendor" queryKey={[VENDOR_ROUTES.INDEX]} listFn={() => api.vendors.list()} - mapOption={(op: any) => ({ label: getFullName(op as any), value: String(op.id) })} + mapOption={mapVendorOption} + createForm={({ onSuccess }) => ( + { + const vendor = (data as any)?.data ?? data + onSuccess(vendor?.id ? mapVendorOption(vendor) : undefined) + }} + /> + )} + createLabel="Vendor" {...STORE_OBJECT} /> ) { return ( @@ -79,6 +82,7 @@ export function ServicesSelectorField< Service Qty Rate + {showDiscount && Discount} Description @@ -110,6 +114,20 @@ export function ServicesSelectorField< className="h-8 w-24" /> + {showDiscount && ( + + + update(index, { ...item, discount_amount: Number(e.target.value) || 0 } as any) + } + className="h-8 w-24" + /> + + )} )} - - {/* General Info */} -
- - +
+ {/* Main Content - 8/12 */} +
+ + {/* General Information Section */} +
+

General Information

+
+ + +
+ +
+ + +
+ +
+ + +
+
+ + {/* Address Section */} +
+

Address

+ + +
+ +
+ +
+ api.geo.countries()} + mapOption={mapLookupOption} + {...STORE_OBJECT} + /> + api.geo.states()} + mapOption={mapLookupOption} + {...STORE_OBJECT} + /> +
+ +
+ + +
+
+ + {/* More Information Section */} +
+

More Information

+ + +
+ +
+
+
-
- - + {/* Sidebar - 4/12 */} +
+ + {/* Location & Time Section */} +
+

Location & Time

+ + + +
+ +
+ +
+ +
+ +
+ +
+
+ + {/* Policies Section */} +
+

Policies

+ + + +
+ +
+
+ + {/* Action Button */} +
+ +
+
- -
- - -
- -
- - -
- - {/* Address */} - - - -
- api.geo.countries()} - mapOption={mapLookupOption} - {...STORE_OBJECT} - /> - api.geo.states()} - mapOption={mapLookupOption} - {...STORE_OBJECT} - /> -
- -
- - -
- - {/* Location */} -
- - -
- - {/* Other */} - - - - - -
- -
- +
) } diff --git a/apps/dashboard/modules/vendors/rhf-vendor-select-field.tsx b/apps/dashboard/modules/vendors/rhf-vendor-select-field.tsx new file mode 100644 index 0000000..7ea6a14 --- /dev/null +++ b/apps/dashboard/modules/vendors/rhf-vendor-select-field.tsx @@ -0,0 +1,237 @@ +"use client" + +import { useRef, useState } from "react" +import { useFormContext, useController, type FieldValues, type FieldPath } from "react-hook-form" +import { useQuery, useQueryClient } from "@tanstack/react-query" +import { Building2, Loader2, PlusIcon } from "lucide-react" +import { useAuthApi } from "@/shared/useApi" +import { VENDOR_ROUTES } from "@garage/api" +import { Field, FieldLabel, FieldError, FieldDescription } from "@/shared/components/ui/field" +import { Button } from "@/shared/components/ui/button" +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/shared/components/ui/dialog" +import { ScrollArea } from "@/shared/components/ui/scroll-area" +import { + Combobox, + ComboboxInput, + ComboboxContent, + ComboboxList, + ComboboxItem, + ComboboxEmpty, +} from "@/shared/components/ui/combobox" +import { VendorForm } from "./vendor-form" + +// ── Vendor option type ── + +type VendorOption = { + value: string + label: string + first_name?: string + last_name?: string + company_name?: string + email?: string + phone?: string +} + +function buildVendorOption(item: any): VendorOption { + const name = [item.first_name, item.last_name].filter(Boolean).join(" ") + const label = item.company_name || name || `Vendor #${item.id}` + return { + value: String(item.id), + label, + first_name: item.first_name, + last_name: item.last_name, + company_name: item.company_name, + email: item.email, + phone: item.phone, + } +} + +function extractItems(response: unknown): any[] { + if (Array.isArray(response)) return response + const obj = response as any + if (Array.isArray(obj?.data?.data)) return obj.data.data + if (Array.isArray(obj?.data)) return obj.data + return [] +} + +function getInitials(opt: VendorOption): string { + if (opt.company_name) return opt.company_name[0].toUpperCase() + if (opt.first_name || opt.last_name) { + return [opt.first_name?.[0], opt.last_name?.[0]].filter(Boolean).join("").toUpperCase() + } + return "V" +} + +// ── Props ── + +export type RhfVendorSelectFieldProps< + TValues extends FieldValues, + TName extends FieldPath, +> = { + name: TName + label?: string + description?: string + required?: boolean + disabled?: boolean + placeholder?: string +} + +// ── Component ── + +export function RhfVendorSelectField< + TValues extends FieldValues, + TName extends FieldPath, +>({ + name, + label = "Vendor", + description, + required, + disabled, + placeholder = "Search by company name, name, or phone...", +}: RhfVendorSelectFieldProps) { + const api = useAuthApi() + const anchorRef = useRef(null) + const [inputValue, setInputValue] = useState("") + const [isCreateOpen, setIsCreateOpen] = useState(false) + const queryClient = useQueryClient() + + const { control } = useFormContext() + const { + field, + fieldState: { error }, + } = useController({ name, control, disabled }) + + const { data: options = [], isLoading } = useQuery({ + queryKey: [VENDOR_ROUTES.INDEX, "vendor-select"], + queryFn: async () => { + const res = await api.vendors.list() + const items = extractItems(res) + return items.map(buildVendorOption) + }, + staleTime: 5 * 60 * 1000, + }) + + const filtered = inputValue + ? options.filter((v) => + [v.company_name, v.first_name, v.last_name, v.email, v.phone] + .filter(Boolean) + .join(" ") + .toLowerCase() + .includes(inputValue.toLowerCase()), + ) + : options + + const handleCreateSuccess = (data?: any) => { + const item = data?.data ?? data + if (item?.id) { + field.onChange(buildVendorOption(item)) + } + queryClient.invalidateQueries({ queryKey: [VENDOR_ROUTES.INDEX, "vendor-select"] }) + setIsCreateOpen(false) + } + + return ( + + {label && ( +
+ + {label} + {required && *} + + +
+ )} + +
+ { + const single = Array.isArray(val) ? val[0] ?? null : val + field.onChange( + single ? { value: single.value, label: single.label } : null, + ) + }} + disabled={field.disabled} + onInputValueChange={(val: string, { reason }: { reason: string }) => { + if (reason === "input-change") setInputValue(val) + }} + isItemEqualToValue={(item: VendorOption, val: any) => + item?.value === val?.value + } + > + + + + {isLoading && ( +
+ +
+ )} + + {!isLoading && + filtered.map((opt) => ( + +
+
+ {getInitials(opt)} +
+
+ + {opt.label} + +
+ {opt.company_name && opt.label !== opt.company_name && ( + + + {opt.company_name} + + )} + {opt.phone && ( + + {opt.phone} + + )} +
+
+
+
+ ))} + + {!isLoading && filtered.length === 0 && ( + No vendors found + )} +
+
+
+
+ + {description && {description}} + {error?.message && {error.message}} + + { if (!v) setIsCreateOpen(false) }}> + + + Add Vendor + + + + + + +
+ ) +} diff --git a/apps/dashboard/modules/vendors/vendor-form.tsx b/apps/dashboard/modules/vendors/vendor-form.tsx index 28423dd..8019f53 100644 --- a/apps/dashboard/modules/vendors/vendor-form.tsx +++ b/apps/dashboard/modules/vendors/vendor-form.tsx @@ -25,17 +25,18 @@ import { VENDOR_ROUTES } from "@garage/api" export type VendorFormProps = { resourceId?: string | null initialData?: unknown - onSuccess?: () => void + onSuccess?: (data?: unknown) => void } // ── Default values ── const DEFAULT_VALUES: VendorFormValues = { + salutation:"Mr.", first_name: "", last_name: "", company_name: "", email: "", -} +} as any // ── Mapping helpers ── @@ -85,9 +86,9 @@ export function VendorForm({ resourceId, initialData, onSuccess }: VendorFormPro }) return promise }, - onSuccess: () => { + onSuccess: (data) => { form.reset() - onSuccess?.() + onSuccess?.(data) }, }) diff --git a/apps/dashboard/shared/components/document-totals-summary.tsx b/apps/dashboard/shared/components/document-totals-summary.tsx new file mode 100644 index 0000000..7428161 --- /dev/null +++ b/apps/dashboard/shared/components/document-totals-summary.tsx @@ -0,0 +1,110 @@ +"use client" + +import { Separator } from "@/shared/components/ui/separator" +import { formatCurrency } from "@/shared/utils/formatters" +import { cn } from "@/shared/lib/utils" +import type { DocumentTotals } from "@/shared/hooks/use-document-totals" + +export type DocumentTotalsSummaryProps = { + totals: DocumentTotals + discountType?: string | null + taxLabel?: string | null + /** Override labels for line groups, keyed by group name */ + groupLabels?: Record +} + +function SummaryRow({ + label, + value, + muted = false, + className, +}: { + label: string + value: string + muted?: boolean + className?: string +}) { + return ( +
+ {label} + {value} +
+ ) +} + +export function DocumentTotalsSummary({ + totals, + discountType, + taxLabel, + groupLabels, +}: DocumentTotalsSummaryProps) { + const { + subTotal, + lineItemDiscount, + transactionDiscount, + totalDiscount, + taxAmount, + total, + hasLineItems, + } = totals + + if (!hasLineItems) return null + + return ( +
+ {/* Group breakdowns */} + {groupLabels && + Object.entries(groupLabels).map(([label, amount]) => ( + + ))} + + {groupLabels && Object.keys(groupLabels).length > 0 && } + + {/* Subtotal */} + + + {/* Line-item discount */} + {discountType === "line_item_level" && lineItemDiscount > 0 && ( + + )} + + {/* Transaction-level discount */} + {discountType === "transaction_level" && transactionDiscount > 0 && ( + + )} + + {/* Tax */} + {taxAmount > 0 && ( + + )} + + + + {/* Total */} +
+ Total + {formatCurrency(total)} +
+
+ ) +} diff --git a/apps/dashboard/shared/components/resource-selector/rhf-resource-field.tsx b/apps/dashboard/shared/components/resource-selector/rhf-resource-field.tsx index ff0f596..5865cee 100644 --- a/apps/dashboard/shared/components/resource-selector/rhf-resource-field.tsx +++ b/apps/dashboard/shared/components/resource-selector/rhf-resource-field.tsx @@ -109,15 +109,14 @@ export function RhfResourceField< {triggerLabel ?? label} - - {(items as any[]).length > 0 - ? renderItems(items, helpers) - : ( -

- No items added yet. -

- )} -
+ { + items.length > 0 && + + {(items as any[]).length > 0 + && renderItems(items, helpers) + } + + } {error && {error.message}} diff --git a/apps/dashboard/shared/data-view/table-view/actions-column.tsx b/apps/dashboard/shared/data-view/table-view/actions-column.tsx index 6f8fff4..1ae0ada 100644 --- a/apps/dashboard/shared/data-view/table-view/actions-column.tsx +++ b/apps/dashboard/shared/data-view/table-view/actions-column.tsx @@ -44,7 +44,11 @@ function ActionsCell({ {options.onEdit && ( - options.onEdit!(row.original)}> + { + e.stopPropagation() + options.onEdit!(row.original) + + }}> Edit @@ -52,7 +56,10 @@ function ActionsCell({ {options.onDelete && ( options.onDelete!(row.original)} + onClick={(e) => { + e.stopPropagation() + options.onDelete!(row.original) + }} > Delete diff --git a/apps/dashboard/shared/hooks/use-document-totals.ts b/apps/dashboard/shared/hooks/use-document-totals.ts new file mode 100644 index 0000000..79057aa --- /dev/null +++ b/apps/dashboard/shared/hooks/use-document-totals.ts @@ -0,0 +1,84 @@ +/** + * useDocumentTotals + * + * Pure calculation hook for invoice-style documents (invoices, bills, estimates, + * credit notes, vendor credits, etc.). + * + * Inputs + * ──────────────────────────────────────────────────────────────────────────── + * lineItems – flat array of all items (parts + services + expenses) + * discountType – 'no' | 'line_item_level' | 'transaction_level' + * discountAmount – used when discountType === 'transaction_level' + * taxRate – percentage (e.g. 15 for 15 %). Undefined → no tax. + * + * Calculation order + * ──────────────────────────────────────────────────────────────────────────── + * 1. subTotal = Σ (qty × rate) + * 2. lineItemDiscount = Σ discount_amount (only when discountType === 'line_item_level') + * 3. transactionDiscount = discountAmount (only when discountType === 'transaction_level') + * 4. totalDiscount = lineItemDiscount + transactionDiscount + * 5. afterDiscount = subTotal - totalDiscount + * 6. taxAmount = afterDiscount × (taxRate / 100) + * 7. total = afterDiscount + taxAmount + */ + +export type DocumentLineItem = { + quantity: number + rate: number + discount_amount?: number +} + +export type UseDocumentTotalsInput = { + lineItems: DocumentLineItem[] + discountType?: string | null + discountAmount?: number + taxRate?: number +} + +export type DocumentTotals = { + subTotal: number + lineItemDiscount: number + transactionDiscount: number + totalDiscount: number + afterDiscount: number + taxAmount: number + total: number + hasLineItems: boolean +} + +export function useDocumentTotals({ + lineItems, + discountType, + discountAmount, + taxRate, +}: UseDocumentTotalsInput): DocumentTotals { + const subTotal = lineItems.reduce((sum, item) => { + const qty = Number(item.quantity) || 0 + const rate = Number(item.rate) || 0 + return sum + qty * rate + }, 0) + + const lineItemDiscount = + discountType === "line_item_level" + ? lineItems.reduce((sum, item) => sum + (Number(item.discount_amount) || 0), 0) + : 0 + + const transactionDiscount = + discountType === "transaction_level" ? Number(discountAmount) || 0 : 0 + + const totalDiscount = lineItemDiscount + transactionDiscount + const afterDiscount = Math.max(0, subTotal - totalDiscount) + const taxAmount = taxRate ? afterDiscount * (taxRate / 100) : 0 + const total = afterDiscount + taxAmount + + return { + subTotal, + lineItemDiscount, + transactionDiscount, + totalDiscount, + afterDiscount, + taxAmount, + total, + hasLineItems: lineItems.length > 0, + } +} diff --git a/apps/dashboard/shared/lib/utils.ts b/apps/dashboard/shared/lib/utils.ts index 2b9886f..ed9a088 100644 --- a/apps/dashboard/shared/lib/utils.ts +++ b/apps/dashboard/shared/lib/utils.ts @@ -24,3 +24,8 @@ export function toId(relation: RelationFieldValue | undefined): number | undefin return relation ? Number(relation.value) : undefined } + +export function getTodayDate() { + const today = new Date().toISOString().split("T")[0] + return today +} \ No newline at end of file diff --git a/packages/api/open-api/schema.json b/packages/api/open-api/schema.json index 149b234..ff41c7c 100644 --- a/packages/api/open-api/schema.json +++ b/packages/api/open-api/schema.json @@ -15257,6 +15257,9 @@ "is_favorite": { "type": "boolean" }, + "type": { + "type": "string" + }, "created_at": { "type": "string", "format": "date-time" @@ -15312,6 +15315,7 @@ "title": "Engine Parts", "image": "string", "is_favorite": false, + "type": "part", "created_at": "2026-03-31T10:00:00.000000Z", "updated_at": "2026-03-31T10:00:00.000000Z" } @@ -15351,12 +15355,16 @@ }, "shop_type_id": { "type": "integer" + }, + "type": { + "type": "string" } } }, "example": { "title": "Oil & Fluids", - "shop_type_id": 1 + "shop_type_id": 1, + "type": "expense" } } } @@ -15390,6 +15398,9 @@ "is_favorite": { "type": "boolean" }, + "type": { + "type": "string" + }, "created_at": { "type": "string", "format": "date-time" @@ -15410,6 +15421,7 @@ "title": "Engine Parts", "image": "string", "is_favorite": false, + "type": "expense", "created_at": "2026-03-31T10:00:00.000000Z", "updated_at": "2026-03-31T10:00:00.000000Z" } @@ -15438,12 +15450,16 @@ }, "shop_type_id": { "type": "integer" + }, + "type": { + "type": "string" } } }, "example": { "title": "Oil & Fluids", - "shop_type_id": 1 + "shop_type_id": 1, + "type": "expense" } } } @@ -15487,6 +15503,9 @@ "is_favorite": { "type": "boolean" }, + "type": { + "type": "string" + }, "created_at": { "type": "string", "format": "date-time" @@ -15507,6 +15526,7 @@ "title": "Engine Parts", "image": "string", "is_favorite": false, + "type": "expense", "created_at": "2026-03-31T10:00:00.000000Z", "updated_at": "2026-03-31T10:00:00.000000Z" } @@ -17696,9 +17716,6 @@ "labor_hours": { "type": "string" }, - "tax": { - "type": "string" - }, "chart_of_account": { "type": "string" }, @@ -17775,6 +17792,9 @@ }, "is_favorite": { "type": "boolean" + }, + "discount_amount": { + "type": "integer" } } }, @@ -17785,6 +17805,12 @@ "updated_at": { "type": "string", "format": "date-time" + }, + "tax_id": { + "type": "integer" + }, + "discount_amount": { + "type": "integer" } } } @@ -17835,7 +17861,6 @@ "rate": "120.00", "working_hours": "2.50", "labor_hours": "2.00", - "tax": "5%", "chart_of_account": "COA-123", "department_id": 1, "description": "Full vehicle inspection.", @@ -17863,10 +17888,13 @@ "id": 1, "title": "Standard", "rate": "95.00", - "is_favorite": true + "is_favorite": true, + "discount_amount": 0 }, "created_at": "2026-03-31T10:00:00.000000Z", - "updated_at": "2026-03-31T10:00:00.000000Z" + "updated_at": "2026-03-31T10:00:00.000000Z", + "tax_id": 5, + "discount_amount": 0 } ], "meta": { @@ -17950,14 +17978,17 @@ "labor_hours": { "type": "integer" }, - "tax": { - "type": "string" - }, "chart_of_account": { "type": "string" }, "description": { "type": "string" + }, + "tax_id": { + "type": "integer" + }, + "discount_amount": { + "type": "integer" } } }, @@ -17980,9 +18011,10 @@ "rate": 120, "working_hours": 2.5, "labor_hours": 2, - "tax": "5%", "chart_of_account": "COA-123", - "description": "Full vehicle inspection." + "description": "Full vehicle inspection.", + "tax_id": 5, + "discount_amount": 0 } } } @@ -18056,9 +18088,6 @@ "labor_hours": { "type": "string" }, - "tax": { - "type": "string" - }, "chart_of_account": { "type": "string" }, @@ -18101,6 +18130,9 @@ }, "is_favorite": { "type": "boolean" + }, + "discount_amount": { + "type": "integer" } } }, @@ -18111,6 +18143,12 @@ "updated_at": { "type": "string", "format": "date-time" + }, + "tax_id": { + "type": "integer" + }, + "discount_amount": { + "type": "integer" } } } @@ -18137,7 +18175,6 @@ "rate": "120.00", "working_hours": "2.50", "labor_hours": "2.00", - "tax": "5%", "chart_of_account": "COA-123", "department_id": 1, "description": "Full vehicle inspection.", @@ -18151,10 +18188,13 @@ "id": 1, "title": "Standard", "rate": "95.00", - "is_favorite": true + "is_favorite": true, + "discount_amount": 0 }, "created_at": "2026-03-31T10:00:00.000000Z", - "updated_at": "2026-03-31T10:00:00.000000Z" + "updated_at": "2026-03-31T10:00:00.000000Z", + "tax_id": 5, + "discount_amount": 0 } } } @@ -18230,14 +18270,17 @@ "labor_hours": { "type": "number" }, - "tax": { - "type": "string" - }, "chart_of_account": { "type": "string" }, "description": { "type": "string" + }, + "tax_id": { + "type": "integer" + }, + "discount_amount": { + "type": "integer" } } }, @@ -18260,9 +18303,10 @@ "rate": 135.5, "working_hours": 3, "labor_hours": 2.75, - "tax": "8%", "chart_of_account": "COA-456", - "description": "Inspection completed." + "description": "Inspection completed.", + "tax_id": 8, + "discount_amount": 0 } } } @@ -18345,9 +18389,6 @@ "labor_hours": { "type": "string" }, - "tax": { - "type": "string" - }, "chart_of_account": { "type": "string" }, @@ -18400,6 +18441,9 @@ }, "is_favorite": { "type": "boolean" + }, + "discount_amount": { + "type": "integer" } } }, @@ -18410,6 +18454,12 @@ "updated_at": { "type": "string", "format": "date-time" + }, + "tax_id": { + "type": "integer" + }, + "discount_amount": { + "type": "integer" } } } @@ -18436,7 +18486,6 @@ "rate": "135.50", "working_hours": "3.00", "labor_hours": "2.75", - "tax": "8%", "chart_of_account": "COA-456", "department_id": 1, "description": "Inspection completed.", @@ -18454,10 +18503,13 @@ "id": 2, "title": "Premium", "rate": "110.00", - "is_favorite": false + "is_favorite": false, + "discount_amount": 0 }, "created_at": "2026-03-31T10:00:00.000000Z", - "updated_at": "2026-03-31T11:30:00.000000Z" + "updated_at": "2026-03-31T11:30:00.000000Z", + "tax_id": 8, + "discount_amount": 0 } } } @@ -20133,6 +20185,126 @@ } } }, + "/api/estimates/{id}/convert-to-job-card": { + "post": { + "tags": [ + "Appointments & estimate attachments" + ], + "summary": "POST /api/estimates/{id}/convert-to-job-card", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "allow_duplicate": { + "type": "boolean" + }, + "status": { + "type": "string" + }, + "order_number": { + "type": "string" + } + } + }, + "example": { + "allow_duplicate": false, + "status": "draft", + "order_number": "JC-EST-001" + } + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "estimate_id": { + "type": "integer" + }, + "estimate_number": { + "type": "string" + }, + "status": { + "type": "string" + } + } + } + } + }, + "example": { + "message": "Estimate converted to job card successfully.", + "data": { + "id": 10, + "title": "Estimate for Toyota Camry", + "estimate_id": 1, + "estimate_number": "EST-001", + "status": "draft" + } + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "data": { + "type": "object", + "properties": { + "job_card_id": { + "type": "integer" + } + } + } + } + }, + "example": { + "message": "A job card already exists for this estimate.", + "data": { + "job_card_id": 5 + } + } + } + } + } + } + } + }, "/api/estimates/{id}/add-attachment": { "post": { "tags": [ @@ -20428,9 +20600,6 @@ "labor_hours": { "type": "string" }, - "tax": { - "type": "string" - }, "chart_of_account": { "type": "string" }, @@ -20439,6 +20608,12 @@ }, "description": { "type": "string" + }, + "tax_id": { + "type": "integer" + }, + "discount_amount": { + "type": "integer" } } }, @@ -20450,10 +20625,11 @@ "rate": "120.00", "working_hours": "1.00", "labor_hours": "1.00", - "tax": "5", "chart_of_account": "4000", "department_id": 1, - "description": "Labor line" + "description": "Labor line", + "tax_id": 5, + "discount_amount": 0 } } } @@ -20493,12 +20669,16 @@ }, "quantity": { "type": "integer" + }, + "discount_amount": { + "type": "integer" } } }, "example": { "rate": "130.00", - "quantity": 2 + "quantity": 2, + "discount_amount": 0 } } } @@ -20708,9 +20888,6 @@ "rate": { "type": "string" }, - "tax": { - "type": "string" - }, "chart_of_account": { "type": "string" }, @@ -20719,6 +20896,12 @@ }, "description": { "type": "string" + }, + "tax_id": { + "type": "integer" + }, + "discount_amount": { + "type": "integer" } } }, @@ -20726,10 +20909,11 @@ "part_id": 1, "quantity": 2, "rate": "45.50", - "tax": "5", "chart_of_account": "5000", "department_id": 1, - "description": "Oil filter" + "description": "Oil filter", + "tax_id": 5, + "discount_amount": 0 } } } @@ -20766,11 +20950,15 @@ "properties": { "quantity": { "type": "integer" + }, + "discount_amount": { + "type": "integer" } } }, "example": { - "quantity": 3 + "quantity": 3, + "discount_amount": 0 } } } @@ -20980,9 +21168,6 @@ "rate": { "type": "string" }, - "tax": { - "type": "string" - }, "chart_of_account": { "type": "string" }, @@ -20991,6 +21176,12 @@ }, "description": { "type": "string" + }, + "tax_id": { + "type": "integer" + }, + "discount_amount": { + "type": "integer" } } }, @@ -20998,10 +21189,11 @@ "expense_item_id": 1, "quantity": 1, "rate": "25.00", - "tax": "5", "chart_of_account": "6000", "department_id": 1, - "description": "Shop supplies" + "description": "Shop supplies", + "tax_id": 5, + "discount_amount": 0 } } } @@ -21038,11 +21230,15 @@ "properties": { "quantity": { "type": "integer" + }, + "discount_amount": { + "type": "integer" } } }, "example": { - "quantity": 2 + "quantity": 2, + "discount_amount": 0 } } } @@ -21463,6 +21659,15 @@ "footer": { "type": "string" }, + "discount": { + "type": "string" + }, + "discount_amount": { + "type": "integer" + }, + "tax_id": { + "type": "integer" + }, "label_ids": { "type": "array", "items": { @@ -21490,6 +21695,9 @@ "insurer_id": null, "service_writer_id": 1, "footer": "Thank you for your business.", + "discount": "no", + "discount_amount": 0, + "tax_id": 0, "label_ids": [ 1, 2 @@ -22000,6 +22208,15 @@ "footer": { "type": "string" }, + "discount": { + "type": "string" + }, + "discount_amount": { + "type": "integer" + }, + "tax_id": { + "type": "integer" + }, "label_ids": { "type": "array", "items": { @@ -22027,6 +22244,9 @@ "insurer_id": 1, "service_writer_id": 1, "footer": "Updated footer text.", + "discount": "transaction_level", + "discount_amount": 15, + "tax_id": 1, "label_ids": [ 1 ], @@ -24936,6 +25156,12 @@ "discount_type": { "type": "string" }, + "discount_amount": { + "type": "integer" + }, + "tax_id": { + "type": "integer" + }, "discount_at": { "type": "string" }, @@ -25012,6 +25238,8 @@ "attachments": null, "tax_inclusive": "Tax Exclusive", "discount_type": "no", + "discount_amount": 0, + "tax_id": 0, "discount_at": "inclusive_of_tax", "label_ids": [ 1 @@ -25735,6 +25963,12 @@ "discount_type": { "type": "string" }, + "discount_amount": { + "type": "integer" + }, + "tax_id": { + "type": "integer" + }, "discount_at": { "type": "string" }, @@ -25766,6 +26000,8 @@ "estimate_to": "Customer", "tax_inclusive": "Tax Exclusive", "discount_type": "no", + "discount_amount": 0, + "tax_id": 0, "discount_at": "inclusive_of_tax", "department_id": 1, "check_in_date": "2026-03-31", @@ -26657,6 +26893,207 @@ } } }, + "/api/job-cards/{id}/convert-to-invoice": { + "post": { + "tags": [ + "Job Cards" + ], + "summary": "POST /api/job-cards/{id}/convert-to-invoice", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "subject": { + "type": "string" + }, + "invoice_number": { + "type": "string" + }, + "invoice_date": { + "type": "string" + }, + "due_date": { + "type": "string" + }, + "invoice_sequence_id": { + "type": "string", + "nullable": true + }, + "payment_terms_id": { + "type": "string", + "nullable": true + }, + "department_id": { + "type": "string", + "nullable": true + }, + "notes": { + "type": "string", + "nullable": true + }, + "terms_and_conditions": { + "type": "string", + "nullable": true + }, + "status": { + "type": "string" + }, + "allow_duplicate": { + "type": "boolean" + }, + "discount_amount": { + "type": "integer" + }, + "tax_id": { + "type": "integer" + } + } + }, + "example": { + "subject": "Work order invoice", + "invoice_number": "INV-2026-01001", + "invoice_date": "2026-04-19", + "due_date": "2026-05-19", + "invoice_sequence_id": null, + "payment_terms_id": null, + "department_id": null, + "notes": null, + "terms_and_conditions": null, + "status": "draft", + "allow_duplicate": false, + "discount_amount": 0, + "tax_id": 0 + } + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "subject": { + "type": "string" + }, + "job_card_id": { + "type": "integer" + }, + "estimate_id": { + "type": "integer" + }, + "invoice_number": { + "type": "string" + }, + "invoice_date": { + "type": "string" + }, + "due_date": { + "type": "string" + }, + "status": { + "type": "string" + }, + "tax_id": { + "type": "integer" + }, + "tax_amount": { + "type": "integer" + }, + "discount_amount_major": { + "type": "integer" + }, + "sub_total": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "amount": { + "type": "integer" + } + } + } + } + }, + "example": { + "message": "Job card converted to invoice successfully.", + "data": { + "id": 42, + "subject": "Work order invoice", + "job_card_id": 1, + "estimate_id": 1, + "invoice_number": "INV-2026-01001", + "invoice_date": "2026-04-19", + "due_date": "2026-05-19", + "status": "draft", + "tax_id": 0, + "tax_amount": 0, + "discount_amount_major": 0, + "sub_total": 1200, + "total": 1200, + "amount": 1200 + } + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "data": { + "type": "object", + "properties": { + "invoice_id": { + "type": "integer" + } + } + } + } + }, + "example": { + "message": "An invoice already exists for this job card.", + "data": { + "invoice_id": 40 + } + } + } + } + } + } + } + }, "/api/job-cards/{id}/add-customer-remark": { "post": { "tags": [ @@ -27392,9 +27829,6 @@ "labor_hours": { "type": "string" }, - "tax": { - "type": "string" - }, "chart_of_account": { "type": "string" }, @@ -27448,12 +27882,21 @@ }, "rate": { "type": "string" + }, + "discount_amount": { + "type": "integer" } } }, "attachments": { "type": "array", "items": {} + }, + "tax_id": { + "type": "integer" + }, + "discount_amount": { + "type": "integer" } } } @@ -27495,7 +27938,6 @@ "rate": "85.00", "working_hours": "1.00", "labor_hours": "1.00", - "tax": "5%", "chart_of_account": "COA-200", "department_id": 1, "description": "Oil change labor line", @@ -27513,9 +27955,12 @@ "labor_rate": { "id": 1, "title": "Standard", - "rate": "95.00" + "rate": "95.00", + "discount_amount": 0 }, - "attachments": [] + "attachments": [], + "tax_id": 1, + "discount_amount": 0 } ], "meta": { @@ -27570,14 +28015,17 @@ "labor_hours": { "type": "integer" }, - "tax": { - "type": "string" - }, "chart_of_account": { "type": "string" }, "description": { "type": "string" + }, + "tax_id": { + "type": "integer" + }, + "discount_amount": { + "type": "integer" } } }, @@ -27590,9 +28038,10 @@ "rate": 85, "working_hours": 1, "labor_hours": 1, - "tax": "5%", "chart_of_account": "COA-200", - "description": "Oil change labor line" + "description": "Oil change labor line", + "tax_id": 1, + "discount_amount": 0 } } } @@ -27648,9 +28097,6 @@ "labor_hours": { "type": "string" }, - "tax": { - "type": "string" - }, "chart_of_account": { "type": "string" }, @@ -27704,12 +28150,21 @@ }, "rate": { "type": "string" + }, + "discount_amount": { + "type": "integer" } } }, "attachments": { "type": "array", "items": {} + }, + "tax_id": { + "type": "integer" + }, + "discount_amount": { + "type": "integer" } } } @@ -27727,7 +28182,6 @@ "rate": "85.00", "working_hours": "1.00", "labor_hours": "1.00", - "tax": "5%", "chart_of_account": "COA-200", "department_id": 1, "description": "Oil change labor line", @@ -27745,9 +28199,12 @@ "labor_rate": { "id": 1, "title": "Standard", - "rate": "95.00" + "rate": "95.00", + "discount_amount": 0 }, - "attachments": [] + "attachments": [], + "tax_id": 1, + "discount_amount": 0 } } } @@ -27780,6 +28237,9 @@ }, "description": { "type": "string" + }, + "discount_amount": { + "type": "integer" } } }, @@ -27787,7 +28247,8 @@ "job_card_service_id": 1, "quantity": 2, "rate": 90, - "description": "Updated description" + "description": "Updated description", + "discount_amount": 0 } } } @@ -27843,9 +28304,6 @@ "labor_hours": { "type": "string" }, - "tax": { - "type": "string" - }, "chart_of_account": { "type": "string" }, @@ -27899,12 +28357,21 @@ }, "rate": { "type": "string" + }, + "discount_amount": { + "type": "integer" } } }, "attachments": { "type": "array", "items": {} + }, + "tax_id": { + "type": "integer" + }, + "discount_amount": { + "type": "integer" } } } @@ -27922,7 +28389,6 @@ "rate": "90.00", "working_hours": "1.00", "labor_hours": "1.00", - "tax": "5%", "chart_of_account": "COA-200", "department_id": 1, "description": "Updated description", @@ -27940,9 +28406,12 @@ "labor_rate": { "id": 1, "title": "Standard", - "rate": "95.00" + "rate": "95.00", + "discount_amount": 0 }, - "attachments": [] + "attachments": [], + "tax_id": 1, + "discount_amount": 0 } } } @@ -28311,9 +28780,6 @@ "rate": { "type": "string" }, - "tax": { - "type": "string" - }, "chart_of_account": { "type": "string" }, @@ -28359,6 +28825,12 @@ "attachments": { "type": "array", "items": {} + }, + "tax_id": { + "type": "integer" + }, + "discount_amount": { + "type": "integer" } } } @@ -28396,7 +28868,6 @@ "part_id": 1, "quantity": 2, "rate": "45.00", - "tax": "5%", "chart_of_account": "COA-300", "department_id": 1, "description": "Brake pads", @@ -28411,7 +28882,9 @@ "id": 1, "name": "Parts" }, - "attachments": [] + "attachments": [], + "tax_id": 1, + "discount_amount": 0 } ], "meta": { @@ -28454,14 +28927,17 @@ "rate": { "type": "integer" }, - "tax": { - "type": "string" - }, "chart_of_account": { "type": "string" }, "description": { "type": "string" + }, + "tax_id": { + "type": "integer" + }, + "discount_amount": { + "type": "integer" } } }, @@ -28470,9 +28946,10 @@ "department_id": 1, "quantity": 2, "rate": 45, - "tax": "5%", "chart_of_account": "COA-300", - "description": "Brake pads" + "description": "Brake pads", + "tax_id": 1, + "discount_amount": 0 } } } @@ -28516,9 +28993,6 @@ "rate": { "type": "string" }, - "tax": { - "type": "string" - }, "chart_of_account": { "type": "string" }, @@ -28564,6 +29038,12 @@ "attachments": { "type": "array", "items": {} + }, + "tax_id": { + "type": "integer" + }, + "discount_amount": { + "type": "integer" } } } @@ -28577,7 +29057,6 @@ "part_id": 1, "quantity": 2, "rate": "45.00", - "tax": "5%", "chart_of_account": "COA-300", "department_id": 1, "description": "Brake pads", @@ -28592,7 +29071,9 @@ "id": 1, "name": "Parts" }, - "attachments": [] + "attachments": [], + "tax_id": 1, + "discount_amount": 0 } } } @@ -28625,6 +29106,9 @@ }, "description": { "type": "string" + }, + "discount_amount": { + "type": "integer" } } }, @@ -28632,7 +29116,8 @@ "job_card_part_id": 1, "quantity": 3, "rate": 48.5, - "description": "Updated qty" + "description": "Updated qty", + "discount_amount": 0 } } } @@ -28676,9 +29161,6 @@ "rate": { "type": "string" }, - "tax": { - "type": "string" - }, "chart_of_account": { "type": "string" }, @@ -28724,6 +29206,12 @@ "attachments": { "type": "array", "items": {} + }, + "tax_id": { + "type": "integer" + }, + "discount_amount": { + "type": "integer" } } } @@ -28737,7 +29225,6 @@ "part_id": 1, "quantity": 3, "rate": "48.50", - "tax": "5%", "chart_of_account": "COA-300", "department_id": 1, "description": "Updated qty", @@ -28752,7 +29239,9 @@ "id": 1, "name": "Parts" }, - "attachments": [] + "attachments": [], + "tax_id": 1, + "discount_amount": 0 } } } @@ -29121,9 +29610,6 @@ "rate": { "type": "string" }, - "tax": { - "type": "string" - }, "chart_of_account": { "type": "string" }, @@ -29169,6 +29655,12 @@ "attachments": { "type": "array", "items": {} + }, + "tax_id": { + "type": "integer" + }, + "discount_amount": { + "type": "integer" } } } @@ -29206,7 +29698,6 @@ "expense_item_id": 1, "quantity": 1, "rate": "120.00", - "tax": "5%", "chart_of_account": "COA-400", "department_id": 1, "description": "Shop supplies", @@ -29221,7 +29712,9 @@ "id": 1, "name": "Admin" }, - "attachments": [] + "attachments": [], + "tax_id": 1, + "discount_amount": 0 } ], "meta": { @@ -29264,14 +29757,17 @@ "rate": { "type": "integer" }, - "tax": { - "type": "string" - }, "chart_of_account": { "type": "string" }, "description": { "type": "string" + }, + "tax_id": { + "type": "integer" + }, + "discount_amount": { + "type": "integer" } } }, @@ -29280,9 +29776,10 @@ "department_id": 1, "quantity": 1, "rate": 120, - "tax": "5%", "chart_of_account": "COA-400", - "description": "Shop supplies" + "description": "Shop supplies", + "tax_id": 1, + "discount_amount": 0 } } } @@ -29326,9 +29823,6 @@ "rate": { "type": "string" }, - "tax": { - "type": "string" - }, "chart_of_account": { "type": "string" }, @@ -29374,6 +29868,12 @@ "attachments": { "type": "array", "items": {} + }, + "tax_id": { + "type": "integer" + }, + "discount_amount": { + "type": "integer" } } } @@ -29387,7 +29887,6 @@ "expense_item_id": 1, "quantity": 1, "rate": "120.00", - "tax": "5%", "chart_of_account": "COA-400", "department_id": 1, "description": "Shop supplies", @@ -29402,7 +29901,9 @@ "id": 1, "name": "Admin" }, - "attachments": [] + "attachments": [], + "tax_id": 1, + "discount_amount": 0 } } } @@ -29435,6 +29936,9 @@ }, "description": { "type": "string" + }, + "discount_amount": { + "type": "integer" } } }, @@ -29442,7 +29946,8 @@ "job_card_expense_item_id": 1, "quantity": 2, "rate": 125.5, - "description": "Updated line" + "description": "Updated line", + "discount_amount": 0 } } } @@ -29486,9 +29991,6 @@ "rate": { "type": "string" }, - "tax": { - "type": "string" - }, "chart_of_account": { "type": "string" }, @@ -29534,6 +30036,12 @@ "attachments": { "type": "array", "items": {} + }, + "tax_id": { + "type": "integer" + }, + "discount_amount": { + "type": "integer" } } } @@ -29547,7 +30055,6 @@ "expense_item_id": 1, "quantity": 2, "rate": "125.50", - "tax": "5%", "chart_of_account": "COA-400", "department_id": 1, "description": "Updated line", @@ -29562,7 +30069,9 @@ "id": 1, "name": "Admin" }, - "attachments": [] + "attachments": [], + "tax_id": 1, + "discount_amount": 0 } } } @@ -30915,6 +31424,9 @@ "job_card_id": { "type": "integer" }, + "invoice_id": { + "type": "integer" + }, "payment_mode_id": { "type": "integer" }, @@ -30946,6 +31458,42 @@ "updated_at": { "type": "string", "format": "date-time" + }, + "job_card": { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + } + }, + "invoice": { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + } + }, + "payment_mode": { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + } + }, + "customer": { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + } + }, + "attachments": { + "type": "array", + "items": {} } } } @@ -30964,23 +31512,12 @@ }, "total": { "type": "integer" - } - } - }, - "links": { - "type": "object", - "properties": { - "first": { - "type": "string" }, - "last": { - "type": "string" + "from": { + "type": "integer" }, - "prev": { - "type": "string" - }, - "next": { - "type": "string" + "to": { + "type": "integer" } } } @@ -30991,29 +31528,39 @@ { "id": 1, "job_card_id": 1, + "invoice_id": 1, "payment_mode_id": 1, "customer_id": 1, "amount_received": 1000, "payment_number": "RCPT-001", "payment_date": "2026-03-31", "reference_date": "2026-03-31", - "deposit_to": "string", - "note": "string", + "deposit_to": "Cash on hand", + "note": "Payment received at front desk", "created_at": "2026-03-31T10:00:00.000000Z", - "updated_at": "2026-03-31T10:00:00.000000Z" + "updated_at": "2026-03-31T10:00:00.000000Z", + "job_card": { + "id": 1 + }, + "invoice": { + "id": 1 + }, + "payment_mode": { + "id": 1 + }, + "customer": { + "id": 1 + }, + "attachments": [] } ], "meta": { "current_page": 1, "last_page": 5, "per_page": 15, - "total": 75 - }, - "links": { - "first": "https://api.example.com/resource?page=1", - "last": "https://api.example.com/resource?page=5", - "prev": "string", - "next": "https://api.example.com/resource?page=2" + "total": 75, + "from": 1, + "to": 15 } } } @@ -31027,46 +31574,65 @@ ], "summary": "Store a newly created payment received.", "requestBody": { - "required": true, "content": { - "application/json": { + "multipart/form-data": { "schema": { "type": "object", "properties": { "job_card_id": { - "type": "integer" + "type": "string" }, "payment_mode_id": { - "type": "integer" + "type": "string" }, "customer_id": { - "type": "integer" + "type": "string" }, "amount_received": { - "type": "integer" + "type": "string" + }, + "invoice_id": { + "type": "string" + }, + "payment_number": { + "type": "string" }, "payment_date": { "type": "string" }, + "reference_date": { + "type": "string" + }, + "deposit_to": { + "type": "string" + }, "note": { "type": "string" + }, + "bill_payments[0][bill_id]": { + "type": "string" + }, + "bill_payments[0][amount]": { + "type": "string" + }, + "bill_payments[1][bill_id]": { + "type": "string" + }, + "bill_payments[1][amount]": { + "type": "string" + }, + "attachment_files[]": { + "type": "string", + "format": "binary" } } - }, - "example": { - "job_card_id": 1, - "payment_mode_id": 1, - "customer_id": 1, - "amount_received": 1000, - "payment_date": "2026-03-31", - "note": "string" } } } }, "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "content": { "application/json": { "schema": { @@ -31084,6 +31650,9 @@ "job_card_id": { "type": "integer" }, + "invoice_id": { + "type": "integer" + }, "payment_mode_id": { "type": "integer" }, @@ -31115,26 +31684,76 @@ "updated_at": { "type": "string", "format": "date-time" + }, + "job_card": { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + } + }, + "invoice": { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + } + }, + "payment_mode": { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + } + }, + "customer": { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + } + }, + "attachments": { + "type": "array", + "items": {} } } } } }, "example": { - "message": "Operation completed successfully.", + "message": "Payment received created successfully.", "data": { "id": 1, "job_card_id": 1, + "invoice_id": 1, "payment_mode_id": 1, "customer_id": 1, "amount_received": 1000, "payment_number": "RCPT-001", "payment_date": "2026-03-31", "reference_date": "2026-03-31", - "deposit_to": "string", - "note": "string", + "deposit_to": "Cash on hand", + "note": "Payment received at front desk", "created_at": "2026-03-31T10:00:00.000000Z", - "updated_at": "2026-03-31T10:00:00.000000Z" + "updated_at": "2026-03-31T10:00:00.000000Z", + "job_card": { + "id": 1 + }, + "invoice": { + "id": 1 + }, + "payment_mode": { + "id": 1 + }, + "customer": { + "id": 1 + }, + "attachments": [] } } } @@ -31150,39 +31769,49 @@ ], "summary": "Update the specified payment received.", "requestBody": { - "required": true, "content": { - "application/json": { + "multipart/form-data": { "schema": { "type": "object", "properties": { "job_card_id": { - "type": "integer" + "type": "string" }, "payment_mode_id": { - "type": "integer" + "type": "string" }, "customer_id": { - "type": "integer" + "type": "string" }, "amount_received": { - "type": "integer" + "type": "string" + }, + "invoice_id": { + "type": "string" + }, + "payment_number": { + "type": "string" }, "payment_date": { "type": "string" }, + "reference_date": { + "type": "string" + }, + "deposit_to": { + "type": "string" + }, "note": { "type": "string" + }, + "delete_attachment_ids[]": { + "type": "string" + }, + "attachment_files[]": { + "type": "string", + "format": "binary" } } - }, - "example": { - "job_card_id": 1, - "payment_mode_id": 1, - "customer_id": 1, - "amount_received": 1000, - "payment_date": "2026-03-31", - "note": "string" } } } @@ -31217,6 +31846,9 @@ "job_card_id": { "type": "integer" }, + "invoice_id": { + "type": "integer" + }, "payment_mode_id": { "type": "integer" }, @@ -31248,26 +31880,76 @@ "updated_at": { "type": "string", "format": "date-time" + }, + "job_card": { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + } + }, + "invoice": { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + } + }, + "payment_mode": { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + } + }, + "customer": { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + } + }, + "attachments": { + "type": "array", + "items": {} } } } } }, "example": { - "message": "Operation completed successfully.", + "message": "Payment received updated successfully.", "data": { "id": 1, "job_card_id": 1, + "invoice_id": 1, "payment_mode_id": 1, "customer_id": 1, "amount_received": 1000, "payment_number": "RCPT-001", "payment_date": "2026-03-31", "reference_date": "2026-03-31", - "deposit_to": "string", - "note": "string", + "deposit_to": "Cash on hand", + "note": "Updated payment note", "created_at": "2026-03-31T10:00:00.000000Z", - "updated_at": "2026-03-31T10:00:00.000000Z" + "updated_at": "2026-03-31T10:05:00.000000Z", + "job_card": { + "id": 1 + }, + "invoice": { + "id": 1 + }, + "payment_mode": { + "id": 1 + }, + "customer": { + "id": 1 + }, + "attachments": [] } } } @@ -32752,6 +33434,15 @@ "terms_and_conditions": { "type": "string" }, + "discount_type": { + "type": "string" + }, + "discount_amount": { + "type": "integer" + }, + "tax_id": { + "type": "integer" + }, "items": { "type": "array", "items": { @@ -32766,6 +33457,12 @@ "rate": { "type": "number" }, + "tax_id": { + "type": "integer" + }, + "discount_amount": { + "type": "integer" + }, "description": { "type": "string" } @@ -32784,11 +33481,16 @@ "department_id": 1, "notes": "string", "terms_and_conditions": "Net 30", + "discount_type": "no", + "discount_amount": 0, + "tax_id": 0, "items": [ { "part_id": 1, "quantity": 2, "rate": 45.5, + "tax_id": 0, + "discount_amount": 0, "description": "Oil filter" } ] @@ -32958,6 +33660,15 @@ "delivery_date": { "type": "string" }, + "discount_type": { + "type": "string" + }, + "discount_amount": { + "type": "integer" + }, + "tax_id": { + "type": "integer" + }, "items": { "type": "array", "items": { @@ -32972,6 +33683,12 @@ "rate": { "type": "number" }, + "tax_id": { + "type": "integer" + }, + "discount_amount": { + "type": "number" + }, "description": { "type": "string" } @@ -32984,17 +33701,24 @@ "title": "Purchase Order for Parts (updated)", "order_number": "PO-001", "delivery_date": "2026-04-08", + "discount_type": "line_item_level", + "discount_amount": 0, + "tax_id": 1, "items": [ { "part_id": 1, "quantity": 3, "rate": 45.5, + "tax_id": 1, + "discount_amount": 2.5, "description": "Oil filter - updated qty" }, { "part_id": 2, "quantity": 1, "rate": 120, + "tax_id": 0, + "discount_amount": 0, "description": "Air filter" } ] @@ -35221,6 +35945,109 @@ "status": { "type": "string" }, + "job_card": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "order_number": { + "type": "string" + }, + "estimate_number": { + "type": "string" + } + } + }, + "vendor": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "vendor_address": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "vendor_id": { + "type": "integer" + }, + "address": { + "type": "string" + }, + "country": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "state": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + } + } + }, + "department": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "tax": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "rate": { + "type": "string" + } + } + }, + "labels": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "color_code": { + "type": "string" + } + } + } + }, "services": { "type": "array", "items": { @@ -35246,6 +36073,20 @@ }, "description": { "type": "string" + }, + "service": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "price": { + "type": "string" + } + } } } } @@ -35275,6 +36116,20 @@ }, "description": { "type": "string" + }, + "part": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "part_number": { + "type": "string" + } + } } } } @@ -35304,6 +36159,20 @@ }, "description": { "type": "string" + }, + "expense": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "invoice_number": { + "type": "string" + } + } } } } @@ -35371,6 +36240,44 @@ "department_id": 1, "notes": "Vendor invoice for parts, service and expense lines", "status": "open", + "job_card": { + "id": 1, + "order_number": "ORD-0001", + "estimate_number": "EST-0001" + }, + "vendor": { + "id": 1, + "name": "Auto Parts Supplier" + }, + "vendor_address": { + "id": 1, + "vendor_id": 1, + "address": "123 Industrial Road", + "country": { + "id": 1, + "name": "United Arab Emirates" + }, + "state": { + "id": 1, + "name": "Dubai" + } + }, + "department": { + "id": 1, + "name": "Service Department" + }, + "tax": { + "id": 1, + "name": "VAT 5%", + "rate": "5.00" + }, + "labels": [ + { + "id": 1, + "title": "Urgent", + "color_code": "#FF6600" + } + ], "services": [ { "id": 1, @@ -35379,7 +36286,12 @@ "quantity": "2.00", "rate": "150.00", "chart_of_account": "COA-401", - "description": "Labor service line" + "description": "Labor service line", + "service": { + "id": 1, + "name": "Brake Service", + "price": "150.00" + } } ], "parts": [ @@ -35390,7 +36302,12 @@ "quantity": 3, "rate": "40.00", "chart_of_account": 1201, - "description": "Brake pad set" + "description": "Brake pad set", + "part": { + "id": 1, + "name": "Brake Pad Set", + "part_number": "PART-001" + } } ], "expenses": [ @@ -35401,7 +36318,12 @@ "quantity": "1.00", "rate": "75.00", "chart_of_account": "COA-402", - "description": "Consumables expense line" + "description": "Consumables expense line", + "expense": { + "id": 1, + "title": "Consumables", + "invoice_number": "EXP-001" + } } ], "created_at": "2026-03-31T10:00:00.000000Z", @@ -35468,6 +36390,18 @@ "department_id": { "type": "integer" }, + "discount_type": { + "type": "string" + }, + "discount_amount": { + "type": "integer" + }, + "tax_id": { + "type": "integer" + }, + "status": { + "type": "string" + }, "notes": { "type": "string" }, @@ -35496,6 +36430,9 @@ }, "description": { "type": "string" + }, + "discount_amount": { + "type": "integer" } } } @@ -35519,6 +36456,9 @@ }, "description": { "type": "string" + }, + "discount_amount": { + "type": "number" } } } @@ -35542,6 +36482,9 @@ }, "description": { "type": "string" + }, + "discount_amount": { + "type": "number" } } } @@ -35559,6 +36502,10 @@ "bill_due_date": "2026-04-14", "payment_terms_id": 1, "department_id": 1, + "discount_type": "line_item_level", + "discount_amount": 0, + "tax_id": 1, + "status": "open", "notes": "Vendor invoice for parts, service and expense lines", "label_ids": [ 1 @@ -35569,7 +36516,8 @@ "quantity": 3, "rate": 40, "chart_of_account": 1201, - "description": "Brake pad set" + "description": "Brake pad set", + "discount_amount": 5 } ], "service_items": [ @@ -35578,7 +36526,8 @@ "quantity": 2, "rate": 150, "chart_of_account": "COA-401", - "description": "Labor service line" + "description": "Labor service line", + "discount_amount": 12.5 } ], "expense_items": [ @@ -35587,7 +36536,8 @@ "quantity": 1, "rate": 75, "chart_of_account": "COA-402", - "description": "Consumables expense line" + "description": "Consumables expense line", + "discount_amount": 3.75 } ] } @@ -35641,6 +36591,9 @@ "department_id": { "type": "integer" }, + "discount_type": { + "type": "string" + }, "notes": { "type": "string" }, @@ -35760,6 +36713,7 @@ "bill_due_date": "2026-04-14", "payment_terms_id": 1, "department_id": 1, + "discount_type": "transaction", "notes": "Vendor invoice for parts, service and expense lines", "status": "open", "services": [ @@ -35806,6 +36760,425 @@ } }, "/api/bills/{id}": { + "get": { + "tags": [ + "Bills" + ], + "summary": "Display the specified bill.", + "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" + }, + "job_card_id": { + "type": "integer" + }, + "vendor_id": { + "type": "integer" + }, + "vendor_address_id": { + "type": "integer" + }, + "purchase_order_id": { + "type": "integer" + }, + "bill_number": { + "type": "string" + }, + "bill_date": { + "type": "string" + }, + "bill_due_date": { + "type": "string" + }, + "payment_terms_id": { + "type": "integer" + }, + "department_id": { + "type": "integer" + }, + "notes": { + "type": "string" + }, + "status": { + "type": "string" + }, + "job_card": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "order_number": { + "type": "string" + }, + "estimate_number": { + "type": "string" + } + } + }, + "vendor": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "vendor_address": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "vendor_id": { + "type": "integer" + }, + "address": { + "type": "string" + }, + "country": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "state": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + } + } + }, + "department": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "tax": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "rate": { + "type": "string" + } + } + }, + "labels": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "color_code": { + "type": "string" + } + } + } + }, + "services": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "bill_id": { + "type": "integer" + }, + "service_id": { + "type": "integer" + }, + "quantity": { + "type": "string" + }, + "rate": { + "type": "string" + }, + "chart_of_account": { + "type": "string" + }, + "description": { + "type": "string" + }, + "service": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "price": { + "type": "string" + } + } + } + } + } + }, + "parts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "bill_id": { + "type": "integer" + }, + "part_id": { + "type": "integer" + }, + "quantity": { + "type": "integer" + }, + "rate": { + "type": "string" + }, + "chart_of_account": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "part": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "part_number": { + "type": "string" + } + } + } + } + } + }, + "expenses": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "bill_id": { + "type": "integer" + }, + "expense_id": { + "type": "integer" + }, + "quantity": { + "type": "string" + }, + "rate": { + "type": "string" + }, + "chart_of_account": { + "type": "string" + }, + "description": { + "type": "string" + }, + "expense": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "invoice_number": { + "type": "string" + } + } + } + } + } + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + } + } + } + }, + "example": { + "data": { + "id": 1, + "title": "Workshop Bill", + "job_card_id": 1, + "vendor_id": 1, + "vendor_address_id": 1, + "purchase_order_id": 1, + "bill_number": "BILL-001", + "bill_date": "2026-03-31", + "bill_due_date": "2026-04-14", + "payment_terms_id": 1, + "department_id": 1, + "notes": "Vendor invoice for parts, service and expense lines", + "status": "open", + "job_card": { + "id": 1, + "order_number": "ORD-0001", + "estimate_number": "EST-0001" + }, + "vendor": { + "id": 1, + "name": "Auto Parts Supplier" + }, + "vendor_address": { + "id": 1, + "vendor_id": 1, + "address": "123 Industrial Road", + "country": { + "id": 1, + "name": "United Arab Emirates" + }, + "state": { + "id": 1, + "name": "Dubai" + } + }, + "department": { + "id": 1, + "name": "Service Department" + }, + "tax": { + "id": 1, + "name": "VAT 5%", + "rate": "5.00" + }, + "labels": [ + { + "id": 1, + "title": "Urgent", + "color_code": "#FF6600" + } + ], + "services": [ + { + "id": 1, + "bill_id": 1, + "service_id": 1, + "quantity": "2.00", + "rate": "150.00", + "chart_of_account": "COA-401", + "description": "Labor service line", + "service": { + "id": 1, + "name": "Brake Service", + "price": "150.00" + } + } + ], + "parts": [ + { + "id": 1, + "bill_id": 1, + "part_id": 1, + "quantity": 3, + "rate": "40.00", + "chart_of_account": 1201, + "description": "Brake pad set", + "part": { + "id": 1, + "name": "Brake Pad Set", + "part_number": "PART-001" + } + } + ], + "expenses": [ + { + "id": 1, + "bill_id": 1, + "expense_id": 1, + "quantity": "1.00", + "rate": "75.00", + "chart_of_account": "COA-402", + "description": "Consumables expense line", + "expense": { + "id": 1, + "title": "Consumables", + "invoice_number": "EXP-001" + } + } + ], + "created_at": "2026-03-31T10:00:00.000000Z", + "updated_at": "2026-03-31T10:00:00.000000Z" + } + } + } + } + } + } + }, "put": { "tags": [ "Bills" @@ -35848,6 +37221,18 @@ "department_id": { "type": "integer" }, + "discount_type": { + "type": "string" + }, + "discount_amount": { + "type": "integer" + }, + "tax_id": { + "type": "integer" + }, + "status": { + "type": "string" + }, "notes": { "type": "string" }, @@ -35876,6 +37261,9 @@ }, "description": { "type": "string" + }, + "discount_amount": { + "type": "integer" } } } @@ -35899,6 +37287,9 @@ }, "description": { "type": "string" + }, + "discount_amount": { + "type": "integer" } } } @@ -35922,6 +37313,9 @@ }, "description": { "type": "string" + }, + "discount_amount": { + "type": "integer" } } } @@ -35939,6 +37333,10 @@ "bill_due_date": "2026-04-20", "payment_terms_id": 1, "department_id": 1, + "discount_type": "line_item_level", + "discount_amount": 0, + "tax_id": 1, + "status": "open", "notes": "Updated vendor invoice lines", "label_ids": [ 1 @@ -35949,7 +37347,8 @@ "quantity": 1, "rate": 45, "chart_of_account": 1201, - "description": "Updated part line" + "description": "Updated part line", + "discount_amount": 2 } ], "service_items": [ @@ -35958,7 +37357,8 @@ "quantity": 1, "rate": 200, "chart_of_account": "COA-401", - "description": "Updated labor service line" + "description": "Updated labor service line", + "discount_amount": 15 } ], "expense_items": [ @@ -35967,7 +37367,8 @@ "quantity": 2, "rate": 50, "chart_of_account": "COA-402", - "description": "Updated consumables expense line" + "description": "Updated consumables expense line", + "discount_amount": 4 } ] } @@ -36813,17 +38214,159 @@ "type": "string" }, "paid_through": { - "type": "string" + "type": "integer" }, "department_id": { "type": "integer" }, + "tax_id": { + "type": "integer" + }, "notes": { "type": "string" }, "status": { "type": "string" }, + "discount_amount_major": { + "type": "integer" + }, + "sub_total": { + "type": "integer" + }, + "tax_amount": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "payments_made": { + "type": "integer" + }, + "balance_due": { + "type": "integer" + }, + "job_card": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "order_number": { + "type": "string" + }, + "estimate_number": { + "type": "string" + } + } + }, + "category": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "vendor": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "department": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "tax": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "rate": { + "type": "string" + } + } + }, + "labels": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "color_code": { + "type": "string" + } + } + } + }, + "expense_items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "expense_id": { + "type": "integer" + }, + "expense_item_id": { + "type": "integer" + }, + "quantity": { + "type": "integer" + }, + "rate": { + "type": "string" + }, + "chart_of_account": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "expense_item": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "unit_price": { + "type": "string" + } + } + } + } + } + }, "created_at": { "type": "string", "format": "date-time" @@ -36849,23 +38392,12 @@ }, "total": { "type": "integer" - } - } - }, - "links": { - "type": "object", - "properties": { - "first": { - "type": "string" }, - "last": { - "type": "string" + "from": { + "type": "integer" }, - "prev": { - "type": "string" - }, - "next": { - "type": "string" + "to": { + "type": "integer" } } } @@ -36881,10 +38413,62 @@ "vendor_id": 1, "invoice_number": "INV-001", "expense_date": "2026-03-31", - "paid_through": "string", + "paid_through": 1001, "department_id": 1, - "notes": "string", + "tax_id": 1, + "notes": "Shop supplies purchase", "status": "draft", + "discount_amount_major": 10, + "sub_total": 240, + "tax_amount": 12, + "total": 242, + "payments_made": 120, + "balance_due": 122, + "job_card": { + "id": 1, + "order_number": "ORD-0001", + "estimate_number": "EST-0001" + }, + "category": { + "id": 1, + "name": "Consumables" + }, + "vendor": { + "id": 1, + "name": "Auto Parts Supplier" + }, + "department": { + "id": 1, + "name": "Service Department" + }, + "tax": { + "id": 1, + "name": "VAT 5%", + "rate": "5.00" + }, + "labels": [ + { + "id": 1, + "title": "Urgent", + "color_code": "#FF6600" + } + ], + "expense_items": [ + { + "id": 1, + "expense_id": 1, + "expense_item_id": 1, + "quantity": 2, + "rate": "120.00", + "chart_of_account": 1201, + "description": "Cleaning materials", + "expense_item": { + "id": 1, + "name": "Workshop Consumable", + "unit_price": "120.00" + } + } + ], "created_at": "2026-03-31T10:00:00.000000Z", "updated_at": "2026-03-31T10:00:00.000000Z" } @@ -36893,13 +38477,9 @@ "current_page": 1, "last_page": 5, "per_page": 15, - "total": 75 - }, - "links": { - "first": "https://api.example.com/resource?page=1", - "last": "https://api.example.com/resource?page=5", - "prev": "string", - "next": "https://api.example.com/resource?page=2" + "total": 75, + "from": 1, + "to": 15 } } } @@ -36931,14 +38511,58 @@ "vendor_id": { "type": "integer" }, + "invoice_number": { + "type": "string" + }, "expense_date": { "type": "string" }, + "paid_through": { + "type": "integer" + }, "department_id": { "type": "integer" }, + "tax_id": { + "type": "integer" + }, + "discount_amount": { + "type": "integer" + }, + "notes": { + "type": "string" + }, "status": { "type": "string" + }, + "label_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "expense_item_id": { + "type": "integer" + }, + "quantity": { + "type": "integer" + }, + "rate": { + "type": "integer" + }, + "chart_of_account": { + "type": "integer" + }, + "description": { + "type": "string" + } + } + } } } }, @@ -36947,16 +38571,33 @@ "title": "Workshop Supplies", "category_id": 1, "vendor_id": 1, + "invoice_number": "INV-001", "expense_date": "2026-03-31", + "paid_through": 1001, "department_id": 1, - "status": "draft" + "tax_id": 1, + "discount_amount": 10, + "notes": "Shop supplies purchase", + "status": "draft", + "label_ids": [ + 1 + ], + "items": [ + { + "expense_item_id": 1, + "quantity": 2, + "rate": 120, + "chart_of_account": 1201, + "description": "Cleaning materials" + } + ] } } } }, "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "content": { "application/json": { "schema": { @@ -36990,17 +38631,159 @@ "type": "string" }, "paid_through": { - "type": "string" + "type": "integer" }, "department_id": { "type": "integer" }, + "tax_id": { + "type": "integer" + }, "notes": { "type": "string" }, "status": { "type": "string" }, + "discount_amount_major": { + "type": "integer" + }, + "sub_total": { + "type": "integer" + }, + "tax_amount": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "payments_made": { + "type": "integer" + }, + "balance_due": { + "type": "integer" + }, + "job_card": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "order_number": { + "type": "string" + }, + "estimate_number": { + "type": "string" + } + } + }, + "category": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "vendor": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "department": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "tax": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "rate": { + "type": "string" + } + } + }, + "labels": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "color_code": { + "type": "string" + } + } + } + }, + "expense_items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "expense_id": { + "type": "integer" + }, + "expense_item_id": { + "type": "integer" + }, + "quantity": { + "type": "integer" + }, + "rate": { + "type": "string" + }, + "chart_of_account": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "expense_item": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "unit_price": { + "type": "string" + } + } + } + } + } + }, "created_at": { "type": "string", "format": "date-time" @@ -37014,7 +38797,7 @@ } }, "example": { - "message": "Operation completed successfully.", + "message": "Expense created successfully.", "data": { "id": 1, "job_card_id": 1, @@ -37023,10 +38806,62 @@ "vendor_id": 1, "invoice_number": "INV-001", "expense_date": "2026-03-31", - "paid_through": "string", + "paid_through": 1001, "department_id": 1, - "notes": "string", + "tax_id": 1, + "notes": "Shop supplies purchase", "status": "draft", + "discount_amount_major": 10, + "sub_total": 240, + "tax_amount": 12, + "total": 242, + "payments_made": 120, + "balance_due": 122, + "job_card": { + "id": 1, + "order_number": "ORD-0001", + "estimate_number": "EST-0001" + }, + "category": { + "id": 1, + "name": "Consumables" + }, + "vendor": { + "id": 1, + "name": "Auto Parts Supplier" + }, + "department": { + "id": 1, + "name": "Service Department" + }, + "tax": { + "id": 1, + "name": "VAT 5%", + "rate": "5.00" + }, + "labels": [ + { + "id": 1, + "title": "Urgent", + "color_code": "#FF6600" + } + ], + "expense_items": [ + { + "id": 1, + "expense_id": 1, + "expense_item_id": 1, + "quantity": 2, + "rate": "120.00", + "chart_of_account": 1201, + "description": "Cleaning materials", + "expense_item": { + "id": 1, + "name": "Workshop Consumable", + "unit_price": "120.00" + } + } + ], "created_at": "2026-03-31T10:00:00.000000Z", "updated_at": "2026-03-31T10:00:00.000000Z" } @@ -37038,6 +38873,293 @@ } }, "/api/expenses/{id}": { + "get": { + "tags": [ + "Expenses" + ], + "summary": "Display the specified expense.", + "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" + }, + "job_card_id": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "category_id": { + "type": "integer" + }, + "vendor_id": { + "type": "integer" + }, + "invoice_number": { + "type": "string" + }, + "expense_date": { + "type": "string" + }, + "paid_through": { + "type": "integer" + }, + "department_id": { + "type": "integer" + }, + "tax_id": { + "type": "integer" + }, + "notes": { + "type": "string" + }, + "status": { + "type": "string" + }, + "discount_amount_major": { + "type": "integer" + }, + "sub_total": { + "type": "integer" + }, + "tax_amount": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "payments_made": { + "type": "integer" + }, + "balance_due": { + "type": "integer" + }, + "job_card": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "order_number": { + "type": "string" + }, + "estimate_number": { + "type": "string" + } + } + }, + "category": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "vendor": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "department": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "tax": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "rate": { + "type": "string" + } + } + }, + "labels": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "color_code": { + "type": "string" + } + } + } + }, + "expense_items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "expense_id": { + "type": "integer" + }, + "expense_item_id": { + "type": "integer" + }, + "quantity": { + "type": "integer" + }, + "rate": { + "type": "string" + }, + "chart_of_account": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "expense_item": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "unit_price": { + "type": "string" + } + } + } + } + } + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + } + } + } + }, + "example": { + "data": { + "id": 1, + "job_card_id": 1, + "title": "Workshop Supplies", + "category_id": 1, + "vendor_id": 1, + "invoice_number": "INV-001", + "expense_date": "2026-03-31", + "paid_through": 1001, + "department_id": 1, + "tax_id": 1, + "notes": "Shop supplies purchase", + "status": "draft", + "discount_amount_major": 10, + "sub_total": 240, + "tax_amount": 12, + "total": 242, + "payments_made": 120, + "balance_due": 122, + "job_card": { + "id": 1, + "order_number": "ORD-0001", + "estimate_number": "EST-0001" + }, + "category": { + "id": 1, + "name": "Consumables" + }, + "vendor": { + "id": 1, + "name": "Auto Parts Supplier" + }, + "department": { + "id": 1, + "name": "Service Department" + }, + "tax": { + "id": 1, + "name": "VAT 5%", + "rate": "5.00" + }, + "labels": [ + { + "id": 1, + "title": "Urgent", + "color_code": "#FF6600" + } + ], + "expense_items": [ + { + "id": 1, + "expense_id": 1, + "expense_item_id": 1, + "quantity": 2, + "rate": "120.00", + "chart_of_account": 1201, + "description": "Cleaning materials", + "expense_item": { + "id": 1, + "name": "Workshop Consumable", + "unit_price": "120.00" + } + } + ], + "created_at": "2026-03-31T10:00:00.000000Z", + "updated_at": "2026-03-31T10:00:00.000000Z" + } + } + } + } + } + } + }, "put": { "tags": [ "Expenses" @@ -37062,25 +39184,86 @@ "vendor_id": { "type": "integer" }, + "invoice_number": { + "type": "string" + }, "expense_date": { "type": "string" }, + "paid_through": { + "type": "integer" + }, "department_id": { "type": "integer" }, + "tax_id": { + "type": "integer" + }, + "discount_amount": { + "type": "integer" + }, + "notes": { + "type": "string" + }, "status": { "type": "string" + }, + "label_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "expense_item_id": { + "type": "integer" + }, + "quantity": { + "type": "integer" + }, + "rate": { + "type": "integer" + }, + "chart_of_account": { + "type": "integer" + }, + "description": { + "type": "string" + } + } + } } } }, "example": { "job_card_id": 1, - "title": "Workshop Supplies", + "title": "Workshop Supplies Updated", "category_id": 1, "vendor_id": 1, + "invoice_number": "INV-001", "expense_date": "2026-03-31", + "paid_through": 1001, "department_id": 1, - "status": "draft" + "tax_id": 1, + "discount_amount": 8, + "notes": "Updated shop supplies purchase", + "status": "open", + "label_ids": [ + 1 + ], + "items": [ + { + "expense_item_id": 1, + "quantity": 3, + "rate": 100, + "chart_of_account": 1201, + "description": "Updated cleaning materials" + } + ] } } } @@ -37131,17 +39314,159 @@ "type": "string" }, "paid_through": { - "type": "string" + "type": "integer" }, "department_id": { "type": "integer" }, + "tax_id": { + "type": "integer" + }, "notes": { "type": "string" }, "status": { "type": "string" }, + "discount_amount_major": { + "type": "integer" + }, + "sub_total": { + "type": "integer" + }, + "tax_amount": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "payments_made": { + "type": "integer" + }, + "balance_due": { + "type": "integer" + }, + "job_card": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "order_number": { + "type": "string" + }, + "estimate_number": { + "type": "string" + } + } + }, + "category": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "vendor": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "department": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "tax": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "rate": { + "type": "string" + } + } + }, + "labels": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "color_code": { + "type": "string" + } + } + } + }, + "expense_items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "expense_id": { + "type": "integer" + }, + "expense_item_id": { + "type": "integer" + }, + "quantity": { + "type": "integer" + }, + "rate": { + "type": "string" + }, + "chart_of_account": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "expense_item": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "unit_price": { + "type": "string" + } + } + } + } + } + }, "created_at": { "type": "string", "format": "date-time" @@ -37155,21 +39480,73 @@ } }, "example": { - "message": "Operation completed successfully.", + "message": "Expense updated successfully.", "data": { "id": 1, "job_card_id": 1, - "title": "Workshop Supplies", + "title": "Workshop Supplies Updated", "category_id": 1, "vendor_id": 1, "invoice_number": "INV-001", "expense_date": "2026-03-31", - "paid_through": "string", + "paid_through": 1001, "department_id": 1, - "notes": "string", - "status": "draft", + "tax_id": 1, + "notes": "Updated shop supplies purchase", + "status": "open", + "discount_amount_major": 8, + "sub_total": 300, + "tax_amount": 15, + "total": 307, + "payments_made": 120, + "balance_due": 187, + "job_card": { + "id": 1, + "order_number": "ORD-0001", + "estimate_number": "EST-0001" + }, + "category": { + "id": 1, + "name": "Consumables" + }, + "vendor": { + "id": 1, + "name": "Auto Parts Supplier" + }, + "department": { + "id": 1, + "name": "Service Department" + }, + "tax": { + "id": 1, + "name": "VAT 5%", + "rate": "5.00" + }, + "labels": [ + { + "id": 1, + "title": "Urgent", + "color_code": "#FF6600" + } + ], + "expense_items": [ + { + "id": 1, + "expense_id": 1, + "expense_item_id": 1, + "quantity": 3, + "rate": "100.00", + "chart_of_account": 1201, + "description": "Updated cleaning materials", + "expense_item": { + "id": 1, + "name": "Workshop Consumable", + "unit_price": "120.00" + } + } + ], "created_at": "2026-03-31T10:00:00.000000Z", - "updated_at": "2026-03-31T10:00:00.000000Z" + "updated_at": "2026-03-31T11:00:00.000000Z" } } } @@ -43314,12 +45691,15 @@ "deposit_to": { "type": "string" }, - "amount": { - "type": "integer" - }, "discount": { "type": "string" }, + "discount_amount": { + "type": "integer" + }, + "tax_id": { + "type": "integer" + }, "inspection_categories": { "type": "array", "items": { @@ -43349,6 +45729,9 @@ "description": { "type": "string" }, + "discount_amount": { + "type": "integer" + }, "department_id": { "type": "integer" } @@ -43375,6 +45758,9 @@ "description": { "type": "string" }, + "discount_amount": { + "type": "integer" + }, "department_id": { "type": "integer" } @@ -43401,6 +45787,9 @@ "description": { "type": "string" }, + "discount_amount": { + "type": "integer" + }, "department_id": { "type": "integer" } @@ -43439,6 +45828,9 @@ "description": { "type": "string" }, + "discount_amount": { + "type": "integer" + }, "department_id": { "type": "integer" } @@ -43474,6 +45866,9 @@ "description": { "type": "string" }, + "discount_amount": { + "type": "integer" + }, "department_id": { "type": "integer" } @@ -43506,8 +45901,9 @@ "received_payment": false, "payment_mode_id": 1, "deposit_to": "Main Account", - "amount": 2500, - "discount": "no", + "discount": "line_item_level", + "discount_amount": 0, + "tax_id": 1, "inspection_categories": [ { "inspection_category_id": 1, @@ -43518,6 +45914,7 @@ "rate": 500, "chart_of_account": "4000", "description": "General inspection", + "discount_amount": 10, "department_id": 1 } ], @@ -43528,6 +45925,7 @@ "rate": 150, "chart_of_account": "4100", "description": "Oil filter", + "discount_amount": 20, "department_id": 1 } ], @@ -43538,6 +45936,7 @@ "rate": 100, "chart_of_account": "4200", "description": "Shop supplies", + "discount_amount": 5, "department_id": 1 } ], @@ -43552,6 +45951,7 @@ "rate": 800, "chart_of_account": "4300", "description": "Engine service", + "discount_amount": 30, "department_id": 1 } ], @@ -43565,6 +45965,7 @@ "rate": 600, "chart_of_account": "4400", "description": "Major service package", + "discount_amount": 15, "department_id": 1 } ] @@ -43664,6 +46065,24 @@ "discount": { "type": "string" }, + "tax_id": { + "type": "integer" + }, + "discount_amount_major": { + "type": "integer" + }, + "sub_total": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "payments_recieved": { + "type": "integer" + }, + "balance_due": { + "type": "integer" + }, "created_at": { "type": "string", "format": "date-time" @@ -43931,8 +46350,14 @@ "received_payment": false, "payment_mode_id": 1, "deposit_to": "Main Account", - "amount": 2500, + "amount": 2595, "discount": "no", + "tax_id": 1, + "discount_amount_major": 50, + "sub_total": 2300, + "total": 2595, + "payments_recieved": 0, + "balance_due": 2595, "created_at": "2026-03-31T10:00:00.000000Z", "updated_at": "2026-03-31T10:00:00.000000Z", "customer": { @@ -44217,17 +46642,29 @@ "invoice_number": { "type": "string" }, + "invoice_title": { + "type": "string" + }, "department_id": { "type": "integer" }, "notes": { "type": "string" }, + "terms_and_conditions": { + "type": "string" + }, "status": { "type": "string" }, "discount": { "type": "string" + }, + "discount_amount": { + "type": "number" + }, + "tax_id": { + "type": "integer" } } }, @@ -44240,10 +46677,14 @@ "payment_terms_id": 1, "invoice_sequence_id": 1, "invoice_number": "INV-001", + "invoice_title": "Tax Invoice", "department_id": 1, "notes": "string", + "terms_and_conditions": "Payment due in 14 days.", "status": "draft", - "discount": "no" + "discount": "transaction_level", + "discount_amount": 25.5, + "tax_id": 1 } } } @@ -44347,6 +46788,27 @@ "discount": { "type": "string" }, + "tax_id": { + "type": "integer" + }, + "tax_amount": { + "type": "integer" + }, + "discount_amount_major": { + "type": "number" + }, + "sub_total": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "payments_recieved": { + "type": "integer" + }, + "balance_due": { + "type": "integer" + }, "created_at": { "type": "string", "format": "date-time" @@ -44387,6 +46849,13 @@ "deposit_to": "string", "amount": 0, "discount": "no", + "tax_id": 1, + "tax_amount": 0, + "discount_amount_major": 25.5, + "sub_total": 0, + "total": 0, + "payments_recieved": 0, + "balance_due": 0, "created_at": "2026-03-31T10:00:00.000000Z", "updated_at": "2026-03-31T10:00:00.000000Z" } diff --git a/packages/api/postman/collection.json b/packages/api/postman/collection.json index d2329f8..418d58b 100644 --- a/packages/api/postman/collection.json +++ b/packages/api/postman/collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "38ac2be3-7ab6-4dcc-ad76-6112212395af", + "_postman_id": "6ef9e473-ce87-4b4b-9e48-89d153d50fca", "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", @@ -9150,7 +9150,7 @@ } ], "cookie": [], - "body": "{\n \"data\": [\n {\n \"id\": 1,\n \"shop_type_id\": 1,\n \"title\": \"Engine Parts\",\n \"image\": \"string\",\n \"is_favorite\": false,\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\"\n }\n ],\n \"meta\": {\n \"current_page\": 1,\n \"last_page\": 5,\n \"per_page\": 15,\n \"total\": 75\n },\n \"links\": {\n \"first\": \"https://api.example.com/resource?page=1\",\n \"last\": \"https://api.example.com/resource?page=5\",\n \"prev\": \"string\",\n \"next\": \"https://api.example.com/resource?page=2\"\n }\n}" + "body": "{\n \"data\": [\n {\n \"id\": 1,\n \"shop_type_id\": 1,\n \"title\": \"Engine Parts\",\n \"image\": \"string\",\n \"is_favorite\": false,\n \"type\": \"part\",\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\"\n }\n ],\n \"meta\": {\n \"current_page\": 1,\n \"last_page\": 5,\n \"per_page\": 15,\n \"total\": 75\n },\n \"links\": {\n \"first\": \"https://api.example.com/resource?page=1\",\n \"last\": \"https://api.example.com/resource?page=5\",\n \"prev\": \"string\",\n \"next\": \"https://api.example.com/resource?page=2\"\n }\n}" } ] }, @@ -9180,7 +9180,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"title\": \"Oil & Fluids\",\n \"shop_type_id\": 1\n}", + "raw": "{\n \"title\": \"Oil & Fluids\",\n \"shop_type_id\": 1,\n \"type\": \"expense\"\n}", "options": { "raw": { "language": "json" @@ -9225,7 +9225,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"title\": \"Oil & Fluids\",\n \"shop_type_id\": 1\n}", + "raw": "{\n \"title\": \"Oil & Fluids\",\n \"shop_type_id\": 1,\n \"type\": \"expense\"\n}", "options": { "raw": { "language": "json" @@ -9253,7 +9253,7 @@ } ], "cookie": [], - "body": "{\n \"message\": \"Operation completed successfully.\",\n \"data\": {\n \"id\": 1,\n \"shop_type_id\": 1,\n \"title\": \"Engine Parts\",\n \"image\": \"string\",\n \"is_favorite\": false,\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\"\n }\n}" + "body": "{\n \"message\": \"Operation completed successfully.\",\n \"data\": {\n \"id\": 1,\n \"shop_type_id\": 1,\n \"title\": \"Engine Parts\",\n \"image\": \"string\",\n \"is_favorite\": false,\n \"type\": \"expense\",\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\"\n }\n}" } ] }, @@ -9283,7 +9283,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"title\": \"Oil & Fluids\",\n \"shop_type_id\": 1\n}", + "raw": "{\n \"title\": \"Oil & Fluids\",\n \"shop_type_id\": 1,\n \"type\": \"expense\"\n}", "options": { "raw": { "language": "json" @@ -9329,7 +9329,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"title\": \"Oil & Fluids\",\n \"shop_type_id\": 1\n}", + "raw": "{\n \"title\": \"Oil & Fluids\",\n \"shop_type_id\": 1,\n \"type\": \"expense\"\n}", "options": { "raw": { "language": "json" @@ -9358,7 +9358,7 @@ } ], "cookie": [], - "body": "{\n \"message\": \"Operation completed successfully.\",\n \"data\": {\n \"id\": 1,\n \"shop_type_id\": 1,\n \"title\": \"Engine Parts\",\n \"image\": \"string\",\n \"is_favorite\": false,\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\"\n }\n}" + "body": "{\n \"message\": \"Operation completed successfully.\",\n \"data\": {\n \"id\": 1,\n \"shop_type_id\": 1,\n \"title\": \"Engine Parts\",\n \"image\": \"string\",\n \"is_favorite\": false,\n \"type\": \"expense\",\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\"\n }\n}" } ] }, @@ -11384,7 +11384,7 @@ "inspections" ] }, - "description": "Optional query: search, customer_id, vehicle_id, job_card_id, department_id, inspection_category_id, employee_id, date_from, date_to, sort_by (id, title, order_number, date, time, status, created_at, updated_at), sort_order (asc|desc), per_page (1-100). Default order: date desc, time desc. Each item includes labels, job_card, inspection_category, labor_rate." + "description": "Optional query: search, customer_id, vehicle_id, job_card_id, department_id, inspection_category_id, employee_id, date_from, date_to, sort_by (id, title, order_number, date, time, status, created_at, updated_at), sort_order (asc|desc), per_page (1-100). Default order: date desc, time desc. Each item includes labels, job_card, inspection_category, labor_rate; optional `discount_amount` (major currency units)." }, "response": [ { @@ -11432,7 +11432,7 @@ } ], "cookie": [], - "body": "{\n \"data\": [\n {\n \"id\": 1,\n \"title\": \"Pre-Service Inspection\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"employee_id\": 1,\n \"order_number\": \"INSP-001\",\n \"date\": \"2026-03-31\",\n \"time\": \"09:00:00\",\n \"note\": \"Initial walk-around.\",\n \"status\": \"in_progress\",\n \"job_card_id\": 1,\n \"inspection_category_id\": 1,\n \"rate_type\": \"flat_rate\",\n \"labor_rate_id\": 1,\n \"quantity\": 1,\n \"rate\": \"120.00\",\n \"working_hours\": \"2.50\",\n \"labor_hours\": \"2.00\",\n \"tax\": \"5%\",\n \"chart_of_account\": \"COA-123\",\n \"department_id\": 1,\n \"description\": \"Full vehicle inspection.\",\n \"labels\": [\n {\n \"id\": 1,\n \"title\": \"Safety\",\n \"color_code\": \"#e63946\",\n \"pivot\": {\n \"inspection_id\": 1,\n \"label_id\": 1\n }\n }\n ],\n \"job_card\": {\n \"id\": 1,\n \"title\": \"JC-001\",\n \"order_number\": \"ORD-1001\"\n },\n \"inspection_category\": {\n \"id\": 1,\n \"inspection_name\": \"Pre-Delivery\"\n },\n \"labor_rate\": {\n \"id\": 1,\n \"title\": \"Standard\",\n \"rate\": \"95.00\",\n \"is_favorite\": true\n },\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\"\n }\n ],\n \"meta\": {\n \"current_page\": 1,\n \"last_page\": 5,\n \"per_page\": 15,\n \"total\": 75,\n \"from\": 1,\n \"to\": 15\n }\n}" + "body": "{\n \"data\": [\n {\n \"id\": 1,\n \"title\": \"Pre-Service Inspection\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"employee_id\": 1,\n \"order_number\": \"INSP-001\",\n \"date\": \"2026-03-31\",\n \"time\": \"09:00:00\",\n \"note\": \"Initial walk-around.\",\n \"status\": \"in_progress\",\n \"job_card_id\": 1,\n \"inspection_category_id\": 1,\n \"rate_type\": \"flat_rate\",\n \"labor_rate_id\": 1,\n \"quantity\": 1,\n \"rate\": \"120.00\",\n \"working_hours\": \"2.50\",\n \"labor_hours\": \"2.00\",\n \"chart_of_account\": \"COA-123\",\n \"department_id\": 1,\n \"description\": \"Full vehicle inspection.\",\n \"labels\": [\n {\n \"id\": 1,\n \"title\": \"Safety\",\n \"color_code\": \"#e63946\",\n \"pivot\": {\n \"inspection_id\": 1,\n \"label_id\": 1\n }\n }\n ],\n \"job_card\": {\n \"id\": 1,\n \"title\": \"JC-001\",\n \"order_number\": \"ORD-1001\"\n },\n \"inspection_category\": {\n \"id\": 1,\n \"inspection_name\": \"Pre-Delivery\"\n },\n \"labor_rate\": {\n \"id\": 1,\n \"title\": \"Standard\",\n \"rate\": \"95.00\",\n \"is_favorite\": true,\n \"discount_amount\": 0\n },\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\",\n \"tax_id\": 5,\n \"discount_amount\": 0\n }\n ],\n \"meta\": {\n \"current_page\": 1,\n \"last_page\": 5,\n \"per_page\": 15,\n \"total\": 75,\n \"from\": 1,\n \"to\": 15\n }\n}" } ] }, @@ -11462,7 +11462,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"title\": \"Pre-Service Inspection\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"employee_id\": 1,\n \"department_id\": 1,\n \"inspection_category_id\": 1,\n \"order_number\": \"INSP-001\",\n \"date\": \"2026-03-31\",\n \"time\": \"09:00:00\",\n \"note\": \"Initial walk-around notes.\",\n \"status\": \"in_progress\",\n \"job_card_id\": null,\n \"rate_type\": \"flat_rate\",\n \"labor_rate_id\": 1,\n \"quantity\": 1,\n \"rate\": 120.0,\n \"working_hours\": 2.5,\n \"labor_hours\": 2.0,\n \"tax\": \"5%\",\n \"chart_of_account\": \"COA-123\",\n \"description\": \"Full vehicle inspection.\"\n}", + "raw": "{\n \"title\": \"Pre-Service Inspection\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"employee_id\": 1,\n \"department_id\": 1,\n \"inspection_category_id\": 1,\n \"order_number\": \"INSP-001\",\n \"date\": \"2026-03-31\",\n \"time\": \"09:00:00\",\n \"note\": \"Initial walk-around notes.\",\n \"status\": \"in_progress\",\n \"job_card_id\": null,\n \"rate_type\": \"flat_rate\",\n \"labor_rate_id\": 1,\n \"quantity\": 1,\n \"rate\": 120.0,\n \"working_hours\": 2.5,\n \"labor_hours\": 2.0,\n \"chart_of_account\": \"COA-123\",\n \"description\": \"Full vehicle inspection.\",\n \"tax_id\": 5,\n \"discount_amount\": 0\n}", "options": { "raw": { "language": "json" @@ -11479,7 +11479,7 @@ "inspections" ] }, - "description": "Required: title, customer_id, vehicle_id, department_id, inspection_category_id, employee_id, order_number, date, time (H:i:s). Optional: note, status, job_card_id, rate_type (flat_rate|hourly), labor_rate_id, quantity, rate, working_hours, labor_hours, tax, chart_of_account, description." + "description": "Required: title, customer_id, vehicle_id, department_id, inspection_category_id, employee_id, order_number, date, time (H:i:s). Optional: note, status, job_card_id, rate_type (flat_rate|hourly), labor_rate_id, quantity, rate, working_hours, labor_hours, tax_id, chart_of_account, description; optional `discount_amount` (major currency units)." }, "response": [ { @@ -11508,7 +11508,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"title\": \"Pre-Service Inspection\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"employee_id\": 1,\n \"department_id\": 1,\n \"inspection_category_id\": 1,\n \"order_number\": \"INSP-001\",\n \"date\": \"2026-03-31\",\n \"time\": \"09:00:00\",\n \"note\": \"Initial walk-around notes.\",\n \"status\": \"in_progress\",\n \"job_card_id\": null,\n \"rate_type\": \"flat_rate\",\n \"labor_rate_id\": 1,\n \"quantity\": 1,\n \"rate\": 120.0,\n \"working_hours\": 2.5,\n \"labor_hours\": 2.0,\n \"tax\": \"5%\",\n \"chart_of_account\": \"COA-123\",\n \"description\": \"Full vehicle inspection.\"\n}", + "raw": "{\n \"title\": \"Pre-Service Inspection\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"employee_id\": 1,\n \"department_id\": 1,\n \"inspection_category_id\": 1,\n \"order_number\": \"INSP-001\",\n \"date\": \"2026-03-31\",\n \"time\": \"09:00:00\",\n \"note\": \"Initial walk-around notes.\",\n \"status\": \"in_progress\",\n \"job_card_id\": null,\n \"rate_type\": \"flat_rate\",\n \"labor_rate_id\": 1,\n \"quantity\": 1,\n \"rate\": 120.0,\n \"working_hours\": 2.5,\n \"labor_hours\": 2.0,\n \"chart_of_account\": \"COA-123\",\n \"description\": \"Full vehicle inspection.\",\n \"tax_id\": 5,\n \"discount_amount\": 0\n}", "options": { "raw": { "language": "json" @@ -11536,7 +11536,7 @@ } ], "cookie": [], - "body": "{\n \"message\": \"Inspection created successfully\",\n \"data\": {\n \"id\": 1,\n \"title\": \"Pre-Service Inspection\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"employee_id\": 1,\n \"order_number\": \"INSP-001\",\n \"date\": \"2026-03-31\",\n \"time\": \"09:00:00\",\n \"note\": \"Initial walk-around notes.\",\n \"status\": \"in_progress\",\n \"job_card_id\": null,\n \"inspection_category_id\": 1,\n \"rate_type\": \"flat_rate\",\n \"labor_rate_id\": 1,\n \"quantity\": 1,\n \"rate\": \"120.00\",\n \"working_hours\": \"2.50\",\n \"labor_hours\": \"2.00\",\n \"tax\": \"5%\",\n \"chart_of_account\": \"COA-123\",\n \"department_id\": 1,\n \"description\": \"Full vehicle inspection.\",\n \"labels\": [],\n \"job_card\": null,\n \"inspection_category\": {\n \"id\": 1,\n \"inspection_name\": \"Pre-Delivery\"\n },\n \"labor_rate\": {\n \"id\": 1,\n \"title\": \"Standard\",\n \"rate\": \"95.00\",\n \"is_favorite\": true\n },\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\"\n }\n}" + "body": "{\n \"message\": \"Inspection created successfully\",\n \"data\": {\n \"id\": 1,\n \"title\": \"Pre-Service Inspection\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"employee_id\": 1,\n \"order_number\": \"INSP-001\",\n \"date\": \"2026-03-31\",\n \"time\": \"09:00:00\",\n \"note\": \"Initial walk-around notes.\",\n \"status\": \"in_progress\",\n \"job_card_id\": null,\n \"inspection_category_id\": 1,\n \"rate_type\": \"flat_rate\",\n \"labor_rate_id\": 1,\n \"quantity\": 1,\n \"rate\": \"120.00\",\n \"working_hours\": \"2.50\",\n \"labor_hours\": \"2.00\",\n \"chart_of_account\": \"COA-123\",\n \"department_id\": 1,\n \"description\": \"Full vehicle inspection.\",\n \"labels\": [],\n \"job_card\": null,\n \"inspection_category\": {\n \"id\": 1,\n \"inspection_name\": \"Pre-Delivery\"\n },\n \"labor_rate\": {\n \"id\": 1,\n \"title\": \"Standard\",\n \"rate\": \"95.00\",\n \"is_favorite\": true,\n \"discount_amount\": 0\n },\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\",\n \"tax_id\": 5,\n \"discount_amount\": 0\n }\n}" } ] }, @@ -11566,7 +11566,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"title\": \"Pre-Service Inspection (updated)\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"employee_id\": 1,\n \"department_id\": 1,\n \"inspection_category_id\": 1,\n \"order_number\": \"INSP-001\",\n \"date\": \"2026-03-31\",\n \"time\": \"10:30:00\",\n \"note\": \"Updated notes.\",\n \"status\": \"completed\",\n \"job_card_id\": 1,\n \"rate_type\": \"hourly\",\n \"labor_rate_id\": 2,\n \"quantity\": 2,\n \"rate\": 135.5,\n \"working_hours\": 3.0,\n \"labor_hours\": 2.75,\n \"tax\": \"8%\",\n \"chart_of_account\": \"COA-456\",\n \"description\": \"Inspection completed.\"\n}", + "raw": "{\n \"title\": \"Pre-Service Inspection (updated)\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"employee_id\": 1,\n \"department_id\": 1,\n \"inspection_category_id\": 1,\n \"order_number\": \"INSP-001\",\n \"date\": \"2026-03-31\",\n \"time\": \"10:30:00\",\n \"note\": \"Updated notes.\",\n \"status\": \"completed\",\n \"job_card_id\": 1,\n \"rate_type\": \"hourly\",\n \"labor_rate_id\": 2,\n \"quantity\": 2,\n \"rate\": 135.5,\n \"working_hours\": 3.0,\n \"labor_hours\": 2.75,\n \"chart_of_account\": \"COA-456\",\n \"description\": \"Inspection completed.\",\n \"tax_id\": 8,\n \"discount_amount\": 0\n}", "options": { "raw": { "language": "json" @@ -11584,7 +11584,7 @@ "{{id}}" ] }, - "description": "Partial update: send only fields to change. Same optional fields as create (including quantity); use null for job_card_id or labor_rate_id to clear when allowed." + "description": "Partial update: send only fields to change. Same optional fields as create (including quantity); use null for job_card_id or labor_rate_id to clear when allowed; optional `discount_amount` (major currency units)." }, "response": [ { @@ -11613,7 +11613,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"title\": \"Pre-Service Inspection (updated)\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"employee_id\": 1,\n \"department_id\": 1,\n \"inspection_category_id\": 1,\n \"order_number\": \"INSP-001\",\n \"date\": \"2026-03-31\",\n \"time\": \"10:30:00\",\n \"note\": \"Updated notes.\",\n \"status\": \"completed\",\n \"job_card_id\": 1,\n \"rate_type\": \"hourly\",\n \"labor_rate_id\": 2,\n \"quantity\": 2,\n \"rate\": 135.5,\n \"working_hours\": 3.0,\n \"labor_hours\": 2.75,\n \"tax\": \"8%\",\n \"chart_of_account\": \"COA-456\",\n \"description\": \"Inspection completed.\"\n}", + "raw": "{\n \"title\": \"Pre-Service Inspection (updated)\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"employee_id\": 1,\n \"department_id\": 1,\n \"inspection_category_id\": 1,\n \"order_number\": \"INSP-001\",\n \"date\": \"2026-03-31\",\n \"time\": \"10:30:00\",\n \"note\": \"Updated notes.\",\n \"status\": \"completed\",\n \"job_card_id\": 1,\n \"rate_type\": \"hourly\",\n \"labor_rate_id\": 2,\n \"quantity\": 2,\n \"rate\": 135.5,\n \"working_hours\": 3.0,\n \"labor_hours\": 2.75,\n \"chart_of_account\": \"COA-456\",\n \"description\": \"Inspection completed.\",\n \"tax_id\": 8,\n \"discount_amount\": 0\n}", "options": { "raw": { "language": "json" @@ -11642,7 +11642,7 @@ } ], "cookie": [], - "body": "{\n \"message\": \"Inspection updated successfully\",\n \"data\": {\n \"id\": 1,\n \"title\": \"Pre-Service Inspection (updated)\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"employee_id\": 1,\n \"order_number\": \"INSP-001\",\n \"date\": \"2026-03-31\",\n \"time\": \"10:30:00\",\n \"note\": \"Updated notes.\",\n \"status\": \"completed\",\n \"job_card_id\": 1,\n \"inspection_category_id\": 1,\n \"rate_type\": \"hourly\",\n \"labor_rate_id\": 2,\n \"quantity\": 2,\n \"rate\": \"135.50\",\n \"working_hours\": \"3.00\",\n \"labor_hours\": \"2.75\",\n \"tax\": \"8%\",\n \"chart_of_account\": \"COA-456\",\n \"department_id\": 1,\n \"description\": \"Inspection completed.\",\n \"labels\": [],\n \"job_card\": {\n \"id\": 1,\n \"title\": \"JC-001\",\n \"order_number\": \"ORD-1001\"\n },\n \"inspection_category\": {\n \"id\": 1,\n \"inspection_name\": \"Pre-Delivery\"\n },\n \"labor_rate\": {\n \"id\": 2,\n \"title\": \"Premium\",\n \"rate\": \"110.00\",\n \"is_favorite\": false\n },\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T11:30:00.000000Z\"\n }\n}" + "body": "{\n \"message\": \"Inspection updated successfully\",\n \"data\": {\n \"id\": 1,\n \"title\": \"Pre-Service Inspection (updated)\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"employee_id\": 1,\n \"order_number\": \"INSP-001\",\n \"date\": \"2026-03-31\",\n \"time\": \"10:30:00\",\n \"note\": \"Updated notes.\",\n \"status\": \"completed\",\n \"job_card_id\": 1,\n \"inspection_category_id\": 1,\n \"rate_type\": \"hourly\",\n \"labor_rate_id\": 2,\n \"quantity\": 2,\n \"rate\": \"135.50\",\n \"working_hours\": \"3.00\",\n \"labor_hours\": \"2.75\",\n \"chart_of_account\": \"COA-456\",\n \"department_id\": 1,\n \"description\": \"Inspection completed.\",\n \"labels\": [],\n \"job_card\": {\n \"id\": 1,\n \"title\": \"JC-001\",\n \"order_number\": \"ORD-1001\"\n },\n \"inspection_category\": {\n \"id\": 1,\n \"inspection_name\": \"Pre-Delivery\"\n },\n \"labor_rate\": {\n \"id\": 2,\n \"title\": \"Premium\",\n \"rate\": \"110.00\",\n \"is_favorite\": false,\n \"discount_amount\": 0\n },\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T11:30:00.000000Z\",\n \"tax_id\": 8,\n \"discount_amount\": 0\n }\n}" } ] }, @@ -11776,7 +11776,7 @@ "add-attachment" ] }, - "description": "Multipart: `attachments[]` (one or more files). Max 20 per inspection total; 5 MB each. Mimes: images, video, audio, pdf, office docs." + "description": "Multipart: `attachments[]` (one or more files). Max 20 per inspection total; 5 MB each. Mimes: images, video, audio, pdf, office docs; optional `discount_amount` (major currency units)." }, "response": [ { @@ -11822,7 +11822,7 @@ "add-attachment" ] }, - "description": "Multipart: `attachments[]` (one or more files). Max 20 per inspection total; 5 MB each. Mimes: images, video, audio, pdf, office docs." + "description": "Multipart: `attachments[]` (one or more files). Max 20 per inspection total; 5 MB each. Mimes: images, video, audio, pdf, office docs; optional `discount_amount` (major currency units)." }, "status": "Created", "code": 201, @@ -11834,7 +11834,7 @@ } ], "cookie": [], - "body": "{\"message\":\"Attachment(s) added successfully\",\"data\":{\"id\":1,\"title\":\"Pre-Service Inspection\",\"attachments\":[{\"id\":1,\"inspection_id\":1,\"file_path\":\"inspection_attachments/photo.jpg\",\"created_at\":\"2026-03-31T12:00:00.000000Z\",\"updated_at\":\"2026-03-31T12:00:00.000000Z\"}]}}" + "body": "{\n \"message\": \"Attachment(s) added successfully\",\n \"data\": {\n \"id\": 1,\n \"title\": \"Pre-Service Inspection\",\n \"attachments\": [\n {\n \"id\": 1,\n \"inspection_id\": 1,\n \"file_path\": \"inspection_attachments/photo.jpg\",\n \"created_at\": \"2026-03-31T12:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T12:00:00.000000Z\"\n }\n ]\n }\n}" } ] }, @@ -11883,7 +11883,7 @@ "delete-attachment" ] }, - "description": "Body or query: `attachment_id` (inspection_attachments.id). Deletes file from public disk." + "description": "Body or query: `attachment_id` (inspection_attachments.id). Deletes file from public disk; optional `discount_amount` (major currency units)." }, "response": [ { @@ -11931,7 +11931,7 @@ "delete-attachment" ] }, - "description": "Body or query: `attachment_id` (inspection_attachments.id). Deletes file from public disk." + "description": "Body or query: `attachment_id` (inspection_attachments.id). Deletes file from public disk; optional `discount_amount` (major currency units)." }, "status": "OK", "code": 200, @@ -11943,7 +11943,7 @@ } ], "cookie": [], - "body": "{\"message\":\"Attachment deleted successfully\",\"data\":{\"id\":1,\"title\":\"Pre-Service Inspection\",\"attachments\":[]}}" + "body": "{\n \"message\": \"Attachment deleted successfully\",\n \"data\": {\n \"id\": 1,\n \"title\": \"Pre-Service Inspection\",\n \"attachments\": []\n }\n}" } ] }, @@ -11989,7 +11989,7 @@ } ] }, - "description": "Query or body: `attachment_id`. Returns `id`, `inspection_id`, `file_path`, public `url`, timestamps." + "description": "Query or body: `attachment_id`. Returns `id`, `inspection_id`, `file_path`, public `url`, timestamps; optional `discount_amount` (major currency units)." }, "response": [ { @@ -12034,7 +12034,7 @@ } ] }, - "description": "Query or body: `attachment_id`. Returns `id`, `inspection_id`, `file_path`, public `url`, timestamps." + "description": "Query or body: `attachment_id`. Returns `id`, `inspection_id`, `file_path`, public `url`, timestamps; optional `discount_amount` (major currency units)." }, "status": "OK", "code": 200, @@ -12046,7 +12046,7 @@ } ], "cookie": [], - "body": "{\"data\":{\"id\":1,\"inspection_id\":1,\"file_path\":\"inspection_attachments/photo.jpg\",\"url\":\"http://localhost/storage/inspection_attachments/photo.jpg\",\"created_at\":\"2026-03-31T12:00:00.000000Z\",\"updated_at\":\"2026-03-31T12:00:00.000000Z\"}}" + "body": "{\n \"data\": {\n \"id\": 1,\n \"inspection_id\": 1,\n \"file_path\": \"inspection_attachments/photo.jpg\",\n \"url\": \"http://localhost/storage/inspection_attachments/photo.jpg\",\n \"created_at\": \"2026-03-31T12:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T12:00:00.000000Z\"\n }\n}" } ] }, @@ -12095,7 +12095,7 @@ "add-label" ] }, - "description": "Body: `label_id` (required). Idempotent if label already linked." + "description": "Body: `label_id` (required). Idempotent if label already linked; optional `discount_amount` (major currency units)." }, "response": [ { @@ -12143,7 +12143,7 @@ "add-label" ] }, - "description": "Body: `label_id` (required). Idempotent if label already linked." + "description": "Body: `label_id` (required). Idempotent if label already linked; optional `discount_amount` (major currency units)." }, "status": "OK", "code": 200, @@ -12155,7 +12155,7 @@ } ], "cookie": [], - "body": "{\"message\":\"Label added to inspection successfully\",\"data\":{\"id\":1,\"title\":\"Pre-Service Inspection\",\"labels\":[{\"id\":1,\"title\":\"Safety\",\"color_code\":\"#e63946\"}]}}" + "body": "{\n \"message\": \"Label added to inspection successfully\",\n \"data\": {\n \"id\": 1,\n \"title\": \"Pre-Service Inspection\",\n \"labels\": [\n {\n \"id\": 1,\n \"title\": \"Safety\",\n \"color_code\": \"#e63946\"\n }\n ]\n }\n}" } ] }, @@ -12204,7 +12204,7 @@ "delete-label" ] }, - "description": "Body: `label_id` (required). Returns 404 if label not attached." + "description": "Body: `label_id` (required). Returns 404 if label not attached; optional `discount_amount` (major currency units)." }, "response": [ { @@ -12252,7 +12252,7 @@ "delete-label" ] }, - "description": "Body: `label_id` (required). Returns 404 if label not attached." + "description": "Body: `label_id` (required). Returns 404 if label not attached; optional `discount_amount` (major currency units)." }, "status": "OK", "code": 200, @@ -12264,7 +12264,7 @@ } ], "cookie": [], - "body": "{\"message\":\"Label removed from inspection successfully\",\"data\":{\"id\":1,\"title\":\"Pre-Service Inspection\",\"labels\":[]}}" + "body": "{\n \"message\": \"Label removed from inspection successfully\",\n \"data\": {\n \"id\": 1,\n \"title\": \"Pre-Service Inspection\",\n \"labels\": []\n }\n}" } ] }, @@ -13572,6 +13572,174 @@ } ] }, + { + "name": "POST /api/estimates/{id}/convert-to-job-card", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{auth_token}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"allow_duplicate\": false,\n \"status\": \"draft\",\n \"order_number\": \"JC-EST-001\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/api/estimates/{{id}}/convert-to-job-card", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "estimates", + "{{id}}", + "convert-to-job-card" + ] + }, + "description": "Creates a job card from the estimate: copies header fields, labels, customer remarks, documents, attachments, services/parts/expense items (with file copies), and inspections. Optional JSON: `allow_duplicate` (default false — 409 if a job card already exists for this estimate), `status` (job card status), `order_number`." + }, + "response": [ + { + "name": "201 Created", + "originalRequest": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{auth_token}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"allow_duplicate\": false,\n \"status\": \"draft\",\n \"order_number\": \"JC-EST-001\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/api/estimates/{{id}}/convert-to-job-card", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "estimates", + "{{id}}", + "convert-to-job-card" + ] + }, + "description": "Creates a job card from the estimate: copies header fields, labels, customer remarks, documents, attachments, services/parts/expense items (with file copies), and inspections. Optional JSON: `allow_duplicate` (default false — 409 if a job card already exists for this estimate), `status` (job card status), `order_number`." + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"message\": \"Estimate converted to job card successfully.\",\n \"data\": {\n \"id\": 10,\n \"title\": \"Estimate for Toyota Camry\",\n \"estimate_id\": 1,\n \"estimate_number\": \"EST-001\",\n \"status\": \"draft\"\n }\n}" + }, + { + "name": "409 Conflict (already converted)", + "originalRequest": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{auth_token}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"allow_duplicate\": false,\n \"status\": \"draft\",\n \"order_number\": \"JC-EST-001\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/api/estimates/{{id}}/convert-to-job-card", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "estimates", + "{{id}}", + "convert-to-job-card" + ] + }, + "description": "Creates a job card from the estimate: copies header fields, labels, customer remarks, documents, attachments, services/parts/expense items (with file copies), and inspections. Optional JSON: `allow_duplicate` (default false — 409 if a job card already exists for this estimate), `status` (job card status), `order_number`." + }, + "status": "Conflict", + "code": 409, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"message\": \"A job card already exists for this estimate.\",\n \"data\": {\n \"job_card_id\": 5\n }\n}" + } + ] + }, { "name": "POST /api/estimates/{id}/add-attachment", "request": { @@ -13961,7 +14129,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"service_id\": 1,\n \"rate_type\": \"flat_rate\",\n \"labor_rate_id\": 1,\n \"quantity\": 1,\n \"rate\": \"120.00\",\n \"working_hours\": \"1.00\",\n \"labor_hours\": \"1.00\",\n \"tax\": \"5\",\n \"chart_of_account\": \"4000\",\n \"department_id\": 1,\n \"description\": \"Labor line\"\n}", + "raw": "{\n \"service_id\": 1,\n \"rate_type\": \"flat_rate\",\n \"labor_rate_id\": 1,\n \"quantity\": 1,\n \"rate\": \"120.00\",\n \"working_hours\": \"1.00\",\n \"labor_hours\": \"1.00\",\n \"chart_of_account\": \"4000\",\n \"department_id\": 1,\n \"description\": \"Labor line\",\n \"tax_id\": 5,\n \"discount_amount\": 0\n}", "options": { "raw": { "language": "json" @@ -14009,7 +14177,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"rate\": \"130.00\",\n \"quantity\": 2\n}", + "raw": "{\n \"rate\": \"130.00\",\n \"quantity\": 2,\n \"discount_amount\": 0\n}", "options": { "raw": { "language": "json" @@ -14285,7 +14453,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"part_id\": 1,\n \"quantity\": 2,\n \"rate\": \"45.50\",\n \"tax\": \"5\",\n \"chart_of_account\": \"5000\",\n \"department_id\": 1,\n \"description\": \"Oil filter\"\n}", + "raw": "{\n \"part_id\": 1,\n \"quantity\": 2,\n \"rate\": \"45.50\",\n \"chart_of_account\": \"5000\",\n \"department_id\": 1,\n \"description\": \"Oil filter\",\n \"tax_id\": 5,\n \"discount_amount\": 0\n}", "options": { "raw": { "language": "json" @@ -14333,7 +14501,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"quantity\": 3\n}", + "raw": "{\n \"quantity\": 3,\n \"discount_amount\": 0\n}", "options": { "raw": { "language": "json" @@ -14609,7 +14777,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"expense_item_id\": 1,\n \"quantity\": 1,\n \"rate\": \"25.00\",\n \"tax\": \"5\",\n \"chart_of_account\": \"6000\",\n \"department_id\": 1,\n \"description\": \"Shop supplies\"\n}", + "raw": "{\n \"expense_item_id\": 1,\n \"quantity\": 1,\n \"rate\": \"25.00\",\n \"chart_of_account\": \"6000\",\n \"department_id\": 1,\n \"description\": \"Shop supplies\",\n \"tax_id\": 5,\n \"discount_amount\": 0\n}", "options": { "raw": { "language": "json" @@ -14657,7 +14825,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"quantity\": 2\n}", + "raw": "{\n \"quantity\": 2,\n \"discount_amount\": 0\n}", "options": { "raw": { "language": "json" @@ -15066,7 +15234,7 @@ ], "body": { "mode": "raw", - "raw": "{\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\": null,\n \"insurer_id\": null,\n \"service_writer_id\": 1,\n \"footer\": \"Thank you for your business.\",\n \"label_ids\": [1, 2],\n \"remarks\": [\"Oil change recommended.\"]\n}", + "raw": "{\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\": null,\n \"insurer_id\": null,\n \"service_writer_id\": 1,\n \"footer\": \"Thank you for your business.\",\n \"discount\": \"no\",\n \"discount_amount\": 0,\n \"tax_id\": 0,\n \"label_ids\": [1, 2],\n \"remarks\": [\"Oil change recommended.\"]\n}", "options": { "raw": { "language": "json" @@ -15111,7 +15279,7 @@ ], "body": { "mode": "raw", - "raw": "{\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\": null,\n \"insurer_id\": null,\n \"service_writer_id\": 1,\n \"footer\": \"Thank you for your business.\",\n \"label_ids\": [1, 2],\n \"remarks\": [\"Oil change recommended.\"]\n}", + "raw": "{\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\": null,\n \"insurer_id\": null,\n \"service_writer_id\": 1,\n \"footer\": \"Thank you for your business.\",\n \"discount\": \"no\",\n \"discount_amount\": 0,\n \"tax_id\": 0,\n \"label_ids\": [1, 2],\n \"remarks\": [\"Oil change recommended.\"]\n}", "options": { "raw": { "language": "json" @@ -15169,7 +15337,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"title\": \"Estimate for Toyota Camry (updated)\",\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\": true,\n \"enable_digital_authorisation\": false,\n \"insurance_type_id\": 1,\n \"insurer_id\": 1,\n \"service_writer_id\": 1,\n \"footer\": \"Updated footer text.\",\n \"label_ids\": [1],\n \"remarks\": [\"Updated remark.\"]\n}", + "raw": "{\n \"title\": \"Estimate for Toyota Camry (updated)\",\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\": true,\n \"enable_digital_authorisation\": false,\n \"insurance_type_id\": 1,\n \"insurer_id\": 1,\n \"service_writer_id\": 1,\n \"footer\": \"Updated footer text.\",\n \"discount\": \"transaction_level\",\n \"discount_amount\": 15,\n \"tax_id\": 1,\n \"label_ids\": [1],\n \"remarks\": [\"Updated remark.\"]\n}", "options": { "raw": { "language": "json" @@ -15215,7 +15383,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"title\": \"Estimate for Toyota Camry (updated)\",\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\": true,\n \"enable_digital_authorisation\": false,\n \"insurance_type_id\": 1,\n \"insurer_id\": 1,\n \"service_writer_id\": 1,\n \"footer\": \"Updated footer text.\",\n \"label_ids\": [1],\n \"remarks\": [\"Updated remark.\"]\n}", + "raw": "{\n \"title\": \"Estimate for Toyota Camry (updated)\",\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\": true,\n \"enable_digital_authorisation\": false,\n \"insurance_type_id\": 1,\n \"insurer_id\": 1,\n \"service_writer_id\": 1,\n \"footer\": \"Updated footer text.\",\n \"discount\": \"transaction_level\",\n \"discount_amount\": 15,\n \"tax_id\": 1,\n \"label_ids\": [1],\n \"remarks\": [\"Updated remark.\"]\n}", "options": { "raw": { "language": "json" @@ -17899,7 +18067,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"title\": \"Job Card 001\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"estimate_id\": 1,\n \"order_number\": \"ORD-001\",\n \"estimate_number\": \"EST-001\",\n \"order_date\": \"2026-03-31\",\n \"service_writer_id\": 1,\n \"primary_technician_id\": 2,\n \"status\": \"check_in\",\n \"footer\": \"Thank you for your business.\",\n \"has_insurance\": false,\n \"insurance_type_id\": null,\n \"insurer_id\": null,\n \"estimate_to\": \"Customer\",\n \"enable_parts_issuing\": false,\n \"enable_digital_authorisation\": false,\n \"department_id\": 1,\n \"check_in_date\": \"2026-03-31\",\n \"check_in_time\": \"09:00\",\n \"km_in\": 50000,\n \"fuel_level\": \"full\",\n \"start_date\": \"2026-03-31\",\n \"start_time\": \"09:00\",\n \"delivery_date\": \"2026-04-05\",\n \"delivery_time\": \"17:00\",\n \"attachments\": null,\n \"tax_inclusive\": \"Tax Exclusive\",\n \"discount_type\": \"no\",\n \"discount_at\": \"inclusive_of_tax\",\n \"label_ids\": [\n 1\n ],\n \"documents\": [\n {\n \"document_type_id\": 1,\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"document_number\": \"DOC-001\",\n \"show_in_invoice\": true,\n \"show_in_estimate\": false,\n \"show_in_statement\": false\n }\n ],\n \"customer_remarks\": [\n \"Customer prefers morning pickup.\"\n ]\n}", + "raw": "{\n \"title\": \"Job Card 001\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"estimate_id\": 1,\n \"order_number\": \"ORD-001\",\n \"estimate_number\": \"EST-001\",\n \"order_date\": \"2026-03-31\",\n \"service_writer_id\": 1,\n \"primary_technician_id\": 2,\n \"status\": \"check_in\",\n \"footer\": \"Thank you for your business.\",\n \"has_insurance\": false,\n \"insurance_type_id\": null,\n \"insurer_id\": null,\n \"estimate_to\": \"Customer\",\n \"enable_parts_issuing\": false,\n \"enable_digital_authorisation\": false,\n \"department_id\": 1,\n \"check_in_date\": \"2026-03-31\",\n \"check_in_time\": \"09:00\",\n \"km_in\": 50000,\n \"fuel_level\": \"full\",\n \"start_date\": \"2026-03-31\",\n \"start_time\": \"09:00\",\n \"delivery_date\": \"2026-04-05\",\n \"delivery_time\": \"17:00\",\n \"attachments\": null,\n \"tax_inclusive\": \"Tax Exclusive\",\n \"discount_type\": \"no\",\n \"discount_amount\": 0,\n \"tax_id\": 0,\n \"discount_at\": \"inclusive_of_tax\",\n \"label_ids\": [\n 1\n ],\n \"documents\": [\n {\n \"document_type_id\": 1,\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"document_number\": \"DOC-001\",\n \"show_in_invoice\": true,\n \"show_in_estimate\": false,\n \"show_in_statement\": false\n }\n ],\n \"customer_remarks\": [\n \"Customer prefers morning pickup.\"\n ]\n}", "options": { "raw": { "language": "json" @@ -17944,7 +18112,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"title\": \"Job Card 001\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"estimate_id\": 1,\n \"order_number\": \"ORD-001\",\n \"estimate_number\": \"EST-001\",\n \"order_date\": \"2026-03-31\",\n \"service_writer_id\": 1,\n \"primary_technician_id\": 2,\n \"status\": \"check_in\",\n \"footer\": \"Thank you for your business.\",\n \"has_insurance\": false,\n \"insurance_type_id\": null,\n \"insurer_id\": null,\n \"estimate_to\": \"Customer\",\n \"enable_parts_issuing\": false,\n \"enable_digital_authorisation\": false,\n \"department_id\": 1,\n \"check_in_date\": \"2026-03-31\",\n \"check_in_time\": \"09:00\",\n \"km_in\": 50000,\n \"fuel_level\": \"full\",\n \"start_date\": \"2026-03-31\",\n \"start_time\": \"09:00\",\n \"delivery_date\": \"2026-04-05\",\n \"delivery_time\": \"17:00\",\n \"attachments\": null,\n \"tax_inclusive\": \"Tax Exclusive\",\n \"discount_type\": \"no\",\n \"discount_at\": \"inclusive_of_tax\",\n \"label_ids\": [\n 1\n ],\n \"documents\": [\n {\n \"document_type_id\": 1,\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"document_number\": \"DOC-001\",\n \"show_in_invoice\": true,\n \"show_in_estimate\": false,\n \"show_in_statement\": false\n }\n ],\n \"customer_remarks\": [\n \"Customer prefers morning pickup.\"\n ]\n}", + "raw": "{\n \"title\": \"Job Card 001\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"estimate_id\": 1,\n \"order_number\": \"ORD-001\",\n \"estimate_number\": \"EST-001\",\n \"order_date\": \"2026-03-31\",\n \"service_writer_id\": 1,\n \"primary_technician_id\": 2,\n \"status\": \"check_in\",\n \"footer\": \"Thank you for your business.\",\n \"has_insurance\": false,\n \"insurance_type_id\": null,\n \"insurer_id\": null,\n \"estimate_to\": \"Customer\",\n \"enable_parts_issuing\": false,\n \"enable_digital_authorisation\": false,\n \"department_id\": 1,\n \"check_in_date\": \"2026-03-31\",\n \"check_in_time\": \"09:00\",\n \"km_in\": 50000,\n \"fuel_level\": \"full\",\n \"start_date\": \"2026-03-31\",\n \"start_time\": \"09:00\",\n \"delivery_date\": \"2026-04-05\",\n \"delivery_time\": \"17:00\",\n \"attachments\": null,\n \"tax_inclusive\": \"Tax Exclusive\",\n \"discount_type\": \"no\",\n \"discount_amount\": 0,\n \"tax_id\": 0,\n \"discount_at\": \"inclusive_of_tax\",\n \"label_ids\": [\n 1\n ],\n \"documents\": [\n {\n \"document_type_id\": 1,\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"document_number\": \"DOC-001\",\n \"show_in_invoice\": true,\n \"show_in_estimate\": false,\n \"show_in_statement\": false\n }\n ],\n \"customer_remarks\": [\n \"Customer prefers morning pickup.\"\n ]\n}", "options": { "raw": { "language": "json" @@ -18090,7 +18258,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"title\": \"Job Card 001\",\n \"status\": \"check_in\",\n \"estimate_to\": \"Customer\",\n \"tax_inclusive\": \"Tax Exclusive\",\n \"discount_type\": \"no\",\n \"discount_at\": \"inclusive_of_tax\",\n \"department_id\": 1,\n \"check_in_date\": \"2026-03-31\",\n \"check_in_time\": \"09:00\",\n \"km_in\": 50000,\n \"fuel_level\": \"full\",\n \"service_writer_id\": 1\n}", + "raw": "{\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"title\": \"Job Card 001\",\n \"status\": \"check_in\",\n \"estimate_to\": \"Customer\",\n \"tax_inclusive\": \"Tax Exclusive\",\n \"discount_type\": \"no\",\n \"discount_amount\": 0,\n \"tax_id\": 0,\n \"discount_at\": \"inclusive_of_tax\",\n \"department_id\": 1,\n \"check_in_date\": \"2026-03-31\",\n \"check_in_time\": \"09:00\",\n \"km_in\": 50000,\n \"fuel_level\": \"full\",\n \"service_writer_id\": 1\n}", "options": { "raw": { "language": "json" @@ -18136,7 +18304,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"title\": \"Job Card 001\",\n \"status\": \"check_in\",\n \"estimate_to\": \"Customer\",\n \"tax_inclusive\": \"Tax Exclusive\",\n \"discount_type\": \"no\",\n \"discount_at\": \"inclusive_of_tax\",\n \"department_id\": 1,\n \"check_in_date\": \"2026-03-31\",\n \"check_in_time\": \"09:00\",\n \"km_in\": 50000,\n \"fuel_level\": \"full\",\n \"service_writer_id\": 1\n}", + "raw": "{\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"title\": \"Job Card 001\",\n \"status\": \"check_in\",\n \"estimate_to\": \"Customer\",\n \"tax_inclusive\": \"Tax Exclusive\",\n \"discount_type\": \"no\",\n \"discount_amount\": 0,\n \"tax_id\": 0,\n \"discount_at\": \"inclusive_of_tax\",\n \"department_id\": 1,\n \"check_in_date\": \"2026-03-31\",\n \"check_in_time\": \"09:00\",\n \"km_in\": 50000,\n \"fuel_level\": \"full\",\n \"service_writer_id\": 1\n}", "options": { "raw": { "language": "json" @@ -18686,6 +18854,173 @@ } ] }, + { + "name": "POST /api/job-cards/{id}/convert-to-invoice", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{auth_token}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"subject\": \"Work order invoice\",\n \"invoice_number\": \"INV-2026-01001\",\n \"invoice_date\": \"2026-04-19\",\n \"due_date\": \"2026-05-19\",\n \"invoice_sequence_id\": null,\n \"payment_terms_id\": null,\n \"department_id\": null,\n \"notes\": null,\n \"terms_and_conditions\": null,\n \"status\": \"draft\",\n \"allow_duplicate\": false,\n \"discount_amount\": 0,\n \"tax_id\": 0\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/api/job-cards/{{id}}/convert-to-invoice", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "job-cards", + "{{id}}", + "convert-to-invoice" + ] + }, + "description": "Creates an invoice from the job card line items (services, parts, inspections, expense lines). Sets `job_card_id` and copies `estimate_id` from the job card when present. Optional: `subject`, `invoice_number` (auto-generated if omitted), `invoice_date`, `due_date`, `invoice_sequence_id`, `payment_terms_id`, `department_id`, `notes`, `terms_and_conditions`, `status`, `allow_duplicate` (default false — 409 if an invoice already exists for this job card), `discount_amount` (major currency units; stored as cents), `tax_id` (FK to `taxes.id`; use `0` for no tax — tax amount is computed from the linked `Tax` rate). Expense lines require matching `expenses.id` for each job card `expense_item_id` (same rule as estimate update-invoice)." + }, + "response": [ + { + "name": "201 Created", + "originalRequest": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{auth_token}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"allow_duplicate\": false\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/api/job-cards/{{id}}/convert-to-invoice", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "job-cards", + "{{id}}", + "convert-to-invoice" + ] + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"message\": \"Job card converted to invoice successfully.\",\n \"data\": {\n \"id\": 42,\n \"subject\": \"Work order invoice\",\n \"job_card_id\": 1,\n \"estimate_id\": 1,\n \"invoice_number\": \"INV-2026-01001\",\n \"invoice_date\": \"2026-04-19\",\n \"due_date\": \"2026-05-19\",\n \"status\": \"draft\",\n \"tax_id\": 0,\n \"tax_amount\": 0,\n \"discount_amount_major\": 0,\n \"sub_total\": 1200,\n \"total\": 1200,\n \"amount\": 1200\n }\n}" + }, + { + "name": "409 Conflict (invoice exists)", + "originalRequest": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{auth_token}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"subject\": \"Work order invoice\",\n \"invoice_number\": \"INV-2026-01001\",\n \"invoice_date\": \"2026-04-19\",\n \"due_date\": \"2026-05-19\",\n \"invoice_sequence_id\": null,\n \"payment_terms_id\": null,\n \"department_id\": null,\n \"notes\": null,\n \"terms_and_conditions\": null,\n \"status\": \"draft\",\n \"allow_duplicate\": false,\n \"discount_amount\": 0,\n \"tax_id\": 0\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/api/job-cards/{{id}}/convert-to-invoice", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "job-cards", + "{{id}}", + "convert-to-invoice" + ] + }, + "description": "Creates an invoice from the job card line items (services, parts, inspections, expense lines). Sets `job_card_id` and copies `estimate_id` from the job card when present. Optional: `subject`, `invoice_number` (auto-generated if omitted), `invoice_date`, `due_date`, `invoice_sequence_id`, `payment_terms_id`, `department_id`, `notes`, `terms_and_conditions`, `status`, `allow_duplicate` (default false — 409 if an invoice already exists for this job card), `discount_amount` (major currency units; stored as cents), `tax_id` (FK to `taxes.id`; use `0` for no tax — tax amount is computed from the linked `Tax` rate). Expense lines require matching `expenses.id` for each job card `expense_item_id` (same rule as estimate update-invoice)." + }, + "status": "Conflict", + "code": 409, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"message\": \"An invoice already exists for this job card.\",\n \"data\": {\n \"invoice_id\": 40\n }\n}" + } + ] + }, { "name": "POST /api/job-cards/{id}/add-customer-remark", "request": { @@ -19471,7 +19806,7 @@ } ] }, - "description": "Paginated catalog service lines on the job card (ordered by id desc). Each row includes `service`, `department`, `labor_rate`, `attachments`." + "description": "Paginated catalog service lines on the job card (ordered by id desc). Each row includes `service`, `department`, `labor_rate`, `attachments`; optional `discount_amount` (major currency units)." }, "response": [ { @@ -19521,7 +19856,7 @@ } ], "cookie": [], - "body": "{\n \"data\": [\n {\n \"id\": 1,\n \"job_card_id\": 1,\n \"service_id\": 1,\n \"rate_type\": \"flat_rate\",\n \"labor_rate_id\": 1,\n \"quantity\": 1,\n \"rate\": \"85.00\",\n \"working_hours\": \"1.00\",\n \"labor_hours\": \"1.00\",\n \"tax\": \"5%\",\n \"chart_of_account\": \"COA-200\",\n \"department_id\": 1,\n \"description\": \"Oil change labor line\",\n \"created_at\": \"2026-04-04T10:00:00.000000Z\",\n \"updated_at\": \"2026-04-04T10:00:00.000000Z\",\n \"service\": {\n \"id\": 1,\n \"labor_name\": \"Oil Change\",\n \"service_code\": \"SVC-001\"\n },\n \"department\": {\n \"id\": 1,\n \"name\": \"Workshop\"\n },\n \"labor_rate\": {\n \"id\": 1,\n \"title\": \"Standard\",\n \"rate\": \"95.00\"\n },\n \"attachments\": []\n }\n ],\n \"meta\": {\n \"current_page\": 1,\n \"last_page\": 1,\n \"per_page\": 15,\n \"total\": 1,\n \"from\": 1,\n \"to\": 1\n }\n}" + "body": "{\n \"data\": [\n {\n \"id\": 1,\n \"job_card_id\": 1,\n \"service_id\": 1,\n \"rate_type\": \"flat_rate\",\n \"labor_rate_id\": 1,\n \"quantity\": 1,\n \"rate\": \"85.00\",\n \"working_hours\": \"1.00\",\n \"labor_hours\": \"1.00\",\n \"chart_of_account\": \"COA-200\",\n \"department_id\": 1,\n \"description\": \"Oil change labor line\",\n \"created_at\": \"2026-04-04T10:00:00.000000Z\",\n \"updated_at\": \"2026-04-04T10:00:00.000000Z\",\n \"service\": {\n \"id\": 1,\n \"labor_name\": \"Oil Change\",\n \"service_code\": \"SVC-001\"\n },\n \"department\": {\n \"id\": 1,\n \"name\": \"Workshop\"\n },\n \"labor_rate\": {\n \"id\": 1,\n \"title\": \"Standard\",\n \"rate\": \"95.00\",\n \"discount_amount\": 0\n },\n \"attachments\": [],\n \"tax_id\": 1,\n \"discount_amount\": 0\n }\n ],\n \"meta\": {\n \"current_page\": 1,\n \"last_page\": 1,\n \"per_page\": 15,\n \"total\": 1,\n \"from\": 1,\n \"to\": 1\n }\n}" } ] }, @@ -19551,7 +19886,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"service_id\": 1,\n \"department_id\": 1,\n \"rate_type\": \"flat_rate\",\n \"labor_rate_id\": 1,\n \"quantity\": 1,\n \"rate\": 85.0,\n \"working_hours\": 1.0,\n \"labor_hours\": 1.0,\n \"tax\": \"5%\",\n \"chart_of_account\": \"COA-200\",\n \"description\": \"Oil change labor line\"\n}", + "raw": "{\n \"service_id\": 1,\n \"department_id\": 1,\n \"rate_type\": \"flat_rate\",\n \"labor_rate_id\": 1,\n \"quantity\": 1,\n \"rate\": 85.0,\n \"working_hours\": 1.0,\n \"labor_hours\": 1.0,\n \"chart_of_account\": \"COA-200\",\n \"description\": \"Oil change labor line\",\n \"tax_id\": 1,\n \"discount_amount\": 0\n}", "options": { "raw": { "language": "json" @@ -19570,7 +19905,7 @@ "add-service" ] }, - "description": "Required: `service_id`, `department_id`. Optional: `rate_type` (flat_rate|hourly), `labor_rate_id`, `quantity`, `rate`, `working_hours`, `labor_hours`, `tax`, `chart_of_account`, `description`." + "description": "Required: `service_id`, `department_id`. Optional: `rate_type` (flat_rate|hourly), `labor_rate_id`, `quantity`, `rate`, `working_hours`, `labor_hours`, `tax_id`, `chart_of_account`, `description`; optional `discount_amount` (major currency units)." }, "response": [ { @@ -19599,7 +19934,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"service_id\": 1,\n \"department_id\": 1,\n \"rate_type\": \"flat_rate\",\n \"labor_rate_id\": 1,\n \"quantity\": 1,\n \"rate\": 85.0,\n \"working_hours\": 1.0,\n \"labor_hours\": 1.0,\n \"tax\": \"5%\",\n \"chart_of_account\": \"COA-200\",\n \"description\": \"Oil change labor line\"\n}", + "raw": "{\n \"service_id\": 1,\n \"department_id\": 1,\n \"rate_type\": \"flat_rate\",\n \"labor_rate_id\": 1,\n \"quantity\": 1,\n \"rate\": 85.0,\n \"working_hours\": 1.0,\n \"labor_hours\": 1.0,\n \"chart_of_account\": \"COA-200\",\n \"description\": \"Oil change labor line\",\n \"tax_id\": 1,\n \"discount_amount\": 0\n}", "options": { "raw": { "language": "json" @@ -19629,7 +19964,7 @@ } ], "cookie": [], - "body": "{\n \"message\": \"Service line added to job card successfully\",\n \"data\": {\n \"id\": 1,\n \"job_card_id\": 1,\n \"service_id\": 1,\n \"rate_type\": \"flat_rate\",\n \"labor_rate_id\": 1,\n \"quantity\": 1,\n \"rate\": \"85.00\",\n \"working_hours\": \"1.00\",\n \"labor_hours\": \"1.00\",\n \"tax\": \"5%\",\n \"chart_of_account\": \"COA-200\",\n \"department_id\": 1,\n \"description\": \"Oil change labor line\",\n \"created_at\": \"2026-04-04T10:00:00.000000Z\",\n \"updated_at\": \"2026-04-04T10:00:00.000000Z\",\n \"service\": {\n \"id\": 1,\n \"labor_name\": \"Oil Change\",\n \"service_code\": \"SVC-001\"\n },\n \"department\": {\n \"id\": 1,\n \"name\": \"Workshop\"\n },\n \"labor_rate\": {\n \"id\": 1,\n \"title\": \"Standard\",\n \"rate\": \"95.00\"\n },\n \"attachments\": []\n }\n}" + "body": "{\n \"message\": \"Service line added to job card successfully\",\n \"data\": {\n \"id\": 1,\n \"job_card_id\": 1,\n \"service_id\": 1,\n \"rate_type\": \"flat_rate\",\n \"labor_rate_id\": 1,\n \"quantity\": 1,\n \"rate\": \"85.00\",\n \"working_hours\": \"1.00\",\n \"labor_hours\": \"1.00\",\n \"chart_of_account\": \"COA-200\",\n \"department_id\": 1,\n \"description\": \"Oil change labor line\",\n \"created_at\": \"2026-04-04T10:00:00.000000Z\",\n \"updated_at\": \"2026-04-04T10:00:00.000000Z\",\n \"service\": {\n \"id\": 1,\n \"labor_name\": \"Oil Change\",\n \"service_code\": \"SVC-001\"\n },\n \"department\": {\n \"id\": 1,\n \"name\": \"Workshop\"\n },\n \"labor_rate\": {\n \"id\": 1,\n \"title\": \"Standard\",\n \"rate\": \"95.00\",\n \"discount_amount\": 0\n },\n \"attachments\": [],\n \"tax_id\": 1,\n \"discount_amount\": 0\n }\n}" } ] }, @@ -19659,7 +19994,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"job_card_service_id\": 1,\n \"quantity\": 2,\n \"rate\": 90.0,\n \"description\": \"Updated description\"\n}", + "raw": "{\n \"job_card_service_id\": 1,\n \"quantity\": 2,\n \"rate\": 90.0,\n \"description\": \"Updated description\",\n \"discount_amount\": 0\n}", "options": { "raw": { "language": "json" @@ -19707,7 +20042,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"job_card_service_id\": 1,\n \"quantity\": 2,\n \"rate\": 90.0,\n \"description\": \"Updated description\"\n}", + "raw": "{\n \"job_card_service_id\": 1,\n \"quantity\": 2,\n \"rate\": 90.0,\n \"description\": \"Updated description\",\n \"discount_amount\": 0\n}", "options": { "raw": { "language": "json" @@ -19737,7 +20072,7 @@ } ], "cookie": [], - "body": "{\n \"message\": \"Service line updated successfully\",\n \"data\": {\n \"id\": 1,\n \"job_card_id\": 1,\n \"service_id\": 1,\n \"rate_type\": \"flat_rate\",\n \"labor_rate_id\": 1,\n \"quantity\": 2,\n \"rate\": \"90.00\",\n \"working_hours\": \"1.00\",\n \"labor_hours\": \"1.00\",\n \"tax\": \"5%\",\n \"chart_of_account\": \"COA-200\",\n \"department_id\": 1,\n \"description\": \"Updated description\",\n \"created_at\": \"2026-04-04T10:00:00.000000Z\",\n \"updated_at\": \"2026-04-04T11:00:00.000000Z\",\n \"service\": {\n \"id\": 1,\n \"labor_name\": \"Oil Change\",\n \"service_code\": \"SVC-001\"\n },\n \"department\": {\n \"id\": 1,\n \"name\": \"Workshop\"\n },\n \"labor_rate\": {\n \"id\": 1,\n \"title\": \"Standard\",\n \"rate\": \"95.00\"\n },\n \"attachments\": []\n }\n}" + "body": "{\n \"message\": \"Service line updated successfully\",\n \"data\": {\n \"id\": 1,\n \"job_card_id\": 1,\n \"service_id\": 1,\n \"rate_type\": \"flat_rate\",\n \"labor_rate_id\": 1,\n \"quantity\": 2,\n \"rate\": \"90.00\",\n \"working_hours\": \"1.00\",\n \"labor_hours\": \"1.00\",\n \"chart_of_account\": \"COA-200\",\n \"department_id\": 1,\n \"description\": \"Updated description\",\n \"created_at\": \"2026-04-04T10:00:00.000000Z\",\n \"updated_at\": \"2026-04-04T11:00:00.000000Z\",\n \"service\": {\n \"id\": 1,\n \"labor_name\": \"Oil Change\",\n \"service_code\": \"SVC-001\"\n },\n \"department\": {\n \"id\": 1,\n \"name\": \"Workshop\"\n },\n \"labor_rate\": {\n \"id\": 1,\n \"title\": \"Standard\",\n \"rate\": \"95.00\",\n \"discount_amount\": 0\n },\n \"attachments\": [],\n \"tax_id\": 1,\n \"discount_amount\": 0\n }\n}" } ] }, @@ -20276,7 +20611,7 @@ } ], "cookie": [], - "body": "{\n \"data\": [\n {\n \"id\": 1,\n \"job_card_id\": 1,\n \"part_id\": 1,\n \"quantity\": 2,\n \"rate\": \"45.00\",\n \"tax\": \"5%\",\n \"chart_of_account\": \"COA-300\",\n \"department_id\": 1,\n \"description\": \"Brake pads\",\n \"created_at\": \"2026-04-04T10:00:00.000000Z\",\n \"updated_at\": \"2026-04-04T10:00:00.000000Z\",\n \"part\": {\n \"id\": 1,\n \"title\": \"Brake Pad Set\",\n \"sku\": \"BP-100\"\n },\n \"department\": {\n \"id\": 1,\n \"name\": \"Parts\"\n },\n \"attachments\": []\n }\n ],\n \"meta\": {\n \"current_page\": 1,\n \"last_page\": 1,\n \"per_page\": 15,\n \"total\": 1,\n \"from\": 1,\n \"to\": 1\n }\n}" + "body": "{\n \"data\": [\n {\n \"id\": 1,\n \"job_card_id\": 1,\n \"part_id\": 1,\n \"quantity\": 2,\n \"rate\": \"45.00\",\n \"chart_of_account\": \"COA-300\",\n \"department_id\": 1,\n \"description\": \"Brake pads\",\n \"created_at\": \"2026-04-04T10:00:00.000000Z\",\n \"updated_at\": \"2026-04-04T10:00:00.000000Z\",\n \"part\": {\n \"id\": 1,\n \"title\": \"Brake Pad Set\",\n \"sku\": \"BP-100\"\n },\n \"department\": {\n \"id\": 1,\n \"name\": \"Parts\"\n },\n \"attachments\": [],\n \"tax_id\": 1,\n \"discount_amount\": 0\n }\n ],\n \"meta\": {\n \"current_page\": 1,\n \"last_page\": 1,\n \"per_page\": 15,\n \"total\": 1,\n \"from\": 1,\n \"to\": 1\n }\n}" } ] }, @@ -20306,7 +20641,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"part_id\": 1,\n \"department_id\": 1,\n \"quantity\": 2,\n \"rate\": 45.0,\n \"tax\": \"5%\",\n \"chart_of_account\": \"COA-300\",\n \"description\": \"Brake pads\"\n}", + "raw": "{\n \"part_id\": 1,\n \"department_id\": 1,\n \"quantity\": 2,\n \"rate\": 45.0,\n \"chart_of_account\": \"COA-300\",\n \"description\": \"Brake pads\",\n \"tax_id\": 1,\n \"discount_amount\": 0\n}", "options": { "raw": { "language": "json" @@ -20325,7 +20660,7 @@ "add-part" ] }, - "description": "Required: `part_id`, `department_id`. Optional: `quantity`, `rate`, `tax`, `chart_of_account`, `description`." + "description": "Required: `part_id`, `department_id`. Optional: `quantity`, `rate`, `tax_id`, `chart_of_account`, `description`; optional `discount_amount` (major currency units)." }, "response": [ { @@ -20354,7 +20689,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"part_id\": 1,\n \"department_id\": 1,\n \"quantity\": 2,\n \"rate\": 45.0,\n \"tax\": \"5%\",\n \"chart_of_account\": \"COA-300\",\n \"description\": \"Brake pads\"\n}", + "raw": "{\n \"part_id\": 1,\n \"department_id\": 1,\n \"quantity\": 2,\n \"rate\": 45.0,\n \"chart_of_account\": \"COA-300\",\n \"description\": \"Brake pads\",\n \"tax_id\": 1,\n \"discount_amount\": 0\n}", "options": { "raw": { "language": "json" @@ -20384,7 +20719,7 @@ } ], "cookie": [], - "body": "{\n \"message\": \"Part line added to job card successfully\",\n \"data\": {\n \"id\": 1,\n \"job_card_id\": 1,\n \"part_id\": 1,\n \"quantity\": 2,\n \"rate\": \"45.00\",\n \"tax\": \"5%\",\n \"chart_of_account\": \"COA-300\",\n \"department_id\": 1,\n \"description\": \"Brake pads\",\n \"created_at\": \"2026-04-04T10:00:00.000000Z\",\n \"updated_at\": \"2026-04-04T10:00:00.000000Z\",\n \"part\": {\n \"id\": 1,\n \"title\": \"Brake Pad Set\",\n \"sku\": \"BP-100\"\n },\n \"department\": {\n \"id\": 1,\n \"name\": \"Parts\"\n },\n \"attachments\": []\n }\n}" + "body": "{\n \"message\": \"Part line added to job card successfully\",\n \"data\": {\n \"id\": 1,\n \"job_card_id\": 1,\n \"part_id\": 1,\n \"quantity\": 2,\n \"rate\": \"45.00\",\n \"chart_of_account\": \"COA-300\",\n \"department_id\": 1,\n \"description\": \"Brake pads\",\n \"created_at\": \"2026-04-04T10:00:00.000000Z\",\n \"updated_at\": \"2026-04-04T10:00:00.000000Z\",\n \"part\": {\n \"id\": 1,\n \"title\": \"Brake Pad Set\",\n \"sku\": \"BP-100\"\n },\n \"department\": {\n \"id\": 1,\n \"name\": \"Parts\"\n },\n \"attachments\": [],\n \"tax_id\": 1,\n \"discount_amount\": 0\n }\n}" } ] }, @@ -20414,7 +20749,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"job_card_part_id\": 1,\n \"quantity\": 3,\n \"rate\": 48.5,\n \"description\": \"Updated qty\"\n}", + "raw": "{\n \"job_card_part_id\": 1,\n \"quantity\": 3,\n \"rate\": 48.5,\n \"description\": \"Updated qty\",\n \"discount_amount\": 0\n}", "options": { "raw": { "language": "json" @@ -20462,7 +20797,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"job_card_part_id\": 1,\n \"quantity\": 3,\n \"rate\": 48.5,\n \"description\": \"Updated qty\"\n}", + "raw": "{\n \"job_card_part_id\": 1,\n \"quantity\": 3,\n \"rate\": 48.5,\n \"description\": \"Updated qty\",\n \"discount_amount\": 0\n}", "options": { "raw": { "language": "json" @@ -20492,7 +20827,7 @@ } ], "cookie": [], - "body": "{\n \"message\": \"Part line updated successfully\",\n \"data\": {\n \"id\": 1,\n \"job_card_id\": 1,\n \"part_id\": 1,\n \"quantity\": 3,\n \"rate\": \"48.50\",\n \"tax\": \"5%\",\n \"chart_of_account\": \"COA-300\",\n \"department_id\": 1,\n \"description\": \"Updated qty\",\n \"created_at\": \"2026-04-04T10:00:00.000000Z\",\n \"updated_at\": \"2026-04-04T11:00:00.000000Z\",\n \"part\": {\n \"id\": 1,\n \"title\": \"Brake Pad Set\",\n \"sku\": \"BP-100\"\n },\n \"department\": {\n \"id\": 1,\n \"name\": \"Parts\"\n },\n \"attachments\": []\n }\n}" + "body": "{\n \"message\": \"Part line updated successfully\",\n \"data\": {\n \"id\": 1,\n \"job_card_id\": 1,\n \"part_id\": 1,\n \"quantity\": 3,\n \"rate\": \"48.50\",\n \"chart_of_account\": \"COA-300\",\n \"department_id\": 1,\n \"description\": \"Updated qty\",\n \"created_at\": \"2026-04-04T10:00:00.000000Z\",\n \"updated_at\": \"2026-04-04T11:00:00.000000Z\",\n \"part\": {\n \"id\": 1,\n \"title\": \"Brake Pad Set\",\n \"sku\": \"BP-100\"\n },\n \"department\": {\n \"id\": 1,\n \"name\": \"Parts\"\n },\n \"attachments\": [],\n \"tax_id\": 1,\n \"discount_amount\": 0\n }\n}" } ] }, @@ -21031,7 +21366,7 @@ } ], "cookie": [], - "body": "{\n \"data\": [\n {\n \"id\": 1,\n \"job_card_id\": 1,\n \"expense_item_id\": 1,\n \"quantity\": 1,\n \"rate\": \"120.00\",\n \"tax\": \"5%\",\n \"chart_of_account\": \"COA-400\",\n \"department_id\": 1,\n \"description\": \"Shop supplies\",\n \"created_at\": \"2026-04-04T10:00:00.000000Z\",\n \"updated_at\": \"2026-04-04T10:00:00.000000Z\",\n \"expense_item\": {\n \"id\": 1,\n \"item_name\": \"Supplies\",\n \"sku\": \"EXP-001\"\n },\n \"department\": {\n \"id\": 1,\n \"name\": \"Admin\"\n },\n \"attachments\": []\n }\n ],\n \"meta\": {\n \"current_page\": 1,\n \"last_page\": 1,\n \"per_page\": 15,\n \"total\": 1,\n \"from\": 1,\n \"to\": 1\n }\n}" + "body": "{\n \"data\": [\n {\n \"id\": 1,\n \"job_card_id\": 1,\n \"expense_item_id\": 1,\n \"quantity\": 1,\n \"rate\": \"120.00\",\n \"chart_of_account\": \"COA-400\",\n \"department_id\": 1,\n \"description\": \"Shop supplies\",\n \"created_at\": \"2026-04-04T10:00:00.000000Z\",\n \"updated_at\": \"2026-04-04T10:00:00.000000Z\",\n \"expense_item\": {\n \"id\": 1,\n \"item_name\": \"Supplies\",\n \"sku\": \"EXP-001\"\n },\n \"department\": {\n \"id\": 1,\n \"name\": \"Admin\"\n },\n \"attachments\": [],\n \"tax_id\": 1,\n \"discount_amount\": 0\n }\n ],\n \"meta\": {\n \"current_page\": 1,\n \"last_page\": 1,\n \"per_page\": 15,\n \"total\": 1,\n \"from\": 1,\n \"to\": 1\n }\n}" } ] }, @@ -21061,7 +21396,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"expense_item_id\": 1,\n \"department_id\": 1,\n \"quantity\": 1,\n \"rate\": 120.0,\n \"tax\": \"5%\",\n \"chart_of_account\": \"COA-400\",\n \"description\": \"Shop supplies\"\n}", + "raw": "{\n \"expense_item_id\": 1,\n \"department_id\": 1,\n \"quantity\": 1,\n \"rate\": 120.0,\n \"chart_of_account\": \"COA-400\",\n \"description\": \"Shop supplies\",\n \"tax_id\": 1,\n \"discount_amount\": 0\n}", "options": { "raw": { "language": "json" @@ -21080,7 +21415,7 @@ "add-expense-item" ] }, - "description": "Required: `expense_item_id`, `department_id`. Optional: `quantity`, `rate`, `tax`, `chart_of_account`, `description`." + "description": "Required: `expense_item_id`, `department_id`. Optional: `quantity`, `rate`, `tax_id`, `chart_of_account`, `description`; optional `discount_amount` (major currency units)." }, "response": [ { @@ -21109,7 +21444,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"expense_item_id\": 1,\n \"department_id\": 1,\n \"quantity\": 1,\n \"rate\": 120.0,\n \"tax\": \"5%\",\n \"chart_of_account\": \"COA-400\",\n \"description\": \"Shop supplies\"\n}", + "raw": "{\n \"expense_item_id\": 1,\n \"department_id\": 1,\n \"quantity\": 1,\n \"rate\": 120.0,\n \"chart_of_account\": \"COA-400\",\n \"description\": \"Shop supplies\",\n \"tax_id\": 1,\n \"discount_amount\": 0\n}", "options": { "raw": { "language": "json" @@ -21139,7 +21474,7 @@ } ], "cookie": [], - "body": "{\n \"message\": \"Expense item line added to job card successfully\",\n \"data\": {\n \"id\": 1,\n \"job_card_id\": 1,\n \"expense_item_id\": 1,\n \"quantity\": 1,\n \"rate\": \"120.00\",\n \"tax\": \"5%\",\n \"chart_of_account\": \"COA-400\",\n \"department_id\": 1,\n \"description\": \"Shop supplies\",\n \"created_at\": \"2026-04-04T10:00:00.000000Z\",\n \"updated_at\": \"2026-04-04T10:00:00.000000Z\",\n \"expense_item\": {\n \"id\": 1,\n \"item_name\": \"Supplies\",\n \"sku\": \"EXP-001\"\n },\n \"department\": {\n \"id\": 1,\n \"name\": \"Admin\"\n },\n \"attachments\": []\n }\n}" + "body": "{\n \"message\": \"Expense item line added to job card successfully\",\n \"data\": {\n \"id\": 1,\n \"job_card_id\": 1,\n \"expense_item_id\": 1,\n \"quantity\": 1,\n \"rate\": \"120.00\",\n \"chart_of_account\": \"COA-400\",\n \"department_id\": 1,\n \"description\": \"Shop supplies\",\n \"created_at\": \"2026-04-04T10:00:00.000000Z\",\n \"updated_at\": \"2026-04-04T10:00:00.000000Z\",\n \"expense_item\": {\n \"id\": 1,\n \"item_name\": \"Supplies\",\n \"sku\": \"EXP-001\"\n },\n \"department\": {\n \"id\": 1,\n \"name\": \"Admin\"\n },\n \"attachments\": [],\n \"tax_id\": 1,\n \"discount_amount\": 0\n }\n}" } ] }, @@ -21169,7 +21504,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"job_card_expense_item_id\": 1,\n \"quantity\": 2,\n \"rate\": 125.5,\n \"description\": \"Updated line\"\n}", + "raw": "{\n \"job_card_expense_item_id\": 1,\n \"quantity\": 2,\n \"rate\": 125.5,\n \"description\": \"Updated line\",\n \"discount_amount\": 0\n}", "options": { "raw": { "language": "json" @@ -21217,7 +21552,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"job_card_expense_item_id\": 1,\n \"quantity\": 2,\n \"rate\": 125.5,\n \"description\": \"Updated line\"\n}", + "raw": "{\n \"job_card_expense_item_id\": 1,\n \"quantity\": 2,\n \"rate\": 125.5,\n \"description\": \"Updated line\",\n \"discount_amount\": 0\n}", "options": { "raw": { "language": "json" @@ -21247,7 +21582,7 @@ } ], "cookie": [], - "body": "{\n \"message\": \"Expense item line updated successfully\",\n \"data\": {\n \"id\": 1,\n \"job_card_id\": 1,\n \"expense_item_id\": 1,\n \"quantity\": 2,\n \"rate\": \"125.50\",\n \"tax\": \"5%\",\n \"chart_of_account\": \"COA-400\",\n \"department_id\": 1,\n \"description\": \"Updated line\",\n \"created_at\": \"2026-04-04T10:00:00.000000Z\",\n \"updated_at\": \"2026-04-04T11:00:00.000000Z\",\n \"expense_item\": {\n \"id\": 1,\n \"item_name\": \"Supplies\",\n \"sku\": \"EXP-001\"\n },\n \"department\": {\n \"id\": 1,\n \"name\": \"Admin\"\n },\n \"attachments\": []\n }\n}" + "body": "{\n \"message\": \"Expense item line updated successfully\",\n \"data\": {\n \"id\": 1,\n \"job_card_id\": 1,\n \"expense_item_id\": 1,\n \"quantity\": 2,\n \"rate\": \"125.50\",\n \"chart_of_account\": \"COA-400\",\n \"department_id\": 1,\n \"description\": \"Updated line\",\n \"created_at\": \"2026-04-04T10:00:00.000000Z\",\n \"updated_at\": \"2026-04-04T11:00:00.000000Z\",\n \"expense_item\": {\n \"id\": 1,\n \"item_name\": \"Supplies\",\n \"sku\": \"EXP-001\"\n },\n \"department\": {\n \"id\": 1,\n \"name\": \"Admin\"\n },\n \"attachments\": [],\n \"tax_id\": 1,\n \"discount_amount\": 0\n }\n}" } ] }, @@ -23018,7 +23353,7 @@ } ], "cookie": [], - "body": "{\n \"data\": [\n {\n \"id\": 1,\n \"job_card_id\": 1,\n \"payment_mode_id\": 1,\n \"customer_id\": 1,\n \"amount_received\": 1000,\n \"payment_number\": \"RCPT-001\",\n \"payment_date\": \"2026-03-31\",\n \"reference_date\": \"2026-03-31\",\n \"deposit_to\": \"string\",\n \"note\": \"string\",\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\"\n }\n ],\n \"meta\": {\n \"current_page\": 1,\n \"last_page\": 5,\n \"per_page\": 15,\n \"total\": 75\n },\n \"links\": {\n \"first\": \"https://api.example.com/resource?page=1\",\n \"last\": \"https://api.example.com/resource?page=5\",\n \"prev\": \"string\",\n \"next\": \"https://api.example.com/resource?page=2\"\n }\n}" + "body": "{\n \"data\": [\n {\n \"id\": 1,\n \"job_card_id\": 1,\n \"invoice_id\": 1,\n \"payment_mode_id\": 1,\n \"customer_id\": 1,\n \"amount_received\": 1000,\n \"payment_number\": \"RCPT-001\",\n \"payment_date\": \"2026-03-31\",\n \"reference_date\": \"2026-03-31\",\n \"deposit_to\": \"Cash on hand\",\n \"note\": \"Payment received at front desk\",\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\",\n \"job_card\": {\n \"id\": 1\n },\n \"invoice\": {\n \"id\": 1\n },\n \"payment_mode\": {\n \"id\": 1\n },\n \"customer\": {\n \"id\": 1\n },\n \"attachments\": []\n }\n ],\n \"meta\": {\n \"current_page\": 1,\n \"last_page\": 5,\n \"per_page\": 15,\n \"total\": 75,\n \"from\": 1,\n \"to\": 15\n }\n}" } ] }, @@ -23040,20 +23375,87 @@ { "key": "Accept", "value": "application/json" - }, - { - "key": "Content-Type", - "value": "application/json" } ], "body": { - "mode": "raw", - "raw": "{\n \"job_card_id\": 1,\n \"payment_mode_id\": 1,\n \"customer_id\": 1,\n \"amount_received\": 1000,\n \"payment_date\": \"2026-03-31\",\n \"note\": \"string\"\n}", - "options": { - "raw": { - "language": "json" + "mode": "formdata", + "formdata": [ + { + "key": "job_card_id", + "value": "1", + "type": "text" + }, + { + "key": "payment_mode_id", + "value": "1", + "type": "text" + }, + { + "key": "customer_id", + "value": "1", + "type": "text" + }, + { + "key": "amount_received", + "value": "1000", + "type": "text" + }, + { + "key": "invoice_id", + "value": "1", + "type": "text" + }, + { + "key": "payment_number", + "value": "RCPT-001", + "type": "text" + }, + { + "key": "payment_date", + "value": "2026-03-31", + "type": "text" + }, + { + "key": "reference_date", + "value": "2026-03-31", + "type": "text" + }, + { + "key": "deposit_to", + "value": "Cash on hand", + "type": "text" + }, + { + "key": "note", + "value": "Payment received at front desk", + "type": "text" + }, + { + "key": "bill_payments[0][bill_id]", + "value": "1", + "type": "text" + }, + { + "key": "bill_payments[0][amount]", + "value": "600", + "type": "text" + }, + { + "key": "bill_payments[1][bill_id]", + "value": "2", + "type": "text" + }, + { + "key": "bill_payments[1][amount]", + "value": "400", + "type": "text" + }, + { + "key": "attachment_files[]", + "type": "file", + "value": "" } - } + ] }, "url": { "raw": "{{base_url}}/api/payment-recieved", @@ -23068,7 +23470,7 @@ }, "response": [ { - "name": "200 OK", + "name": "201 Created", "originalRequest": { "auth": { "type": "bearer", @@ -23085,20 +23487,89 @@ { "key": "Accept", "value": "application/json" - }, - { - "key": "Content-Type", - "value": "application/json" } ], "body": { - "mode": "raw", - "raw": "{\n \"job_card_id\": 1,\n \"payment_mode_id\": 1,\n \"customer_id\": 1,\n \"amount_received\": 1000,\n \"payment_date\": \"2026-03-31\",\n \"note\": \"string\"\n}", - "options": { - "raw": { - "language": "json" + "mode": "formdata", + "formdata": [ + { + "key": "job_card_id", + "value": "1", + "type": "text" + }, + { + "key": "payment_mode_id", + "value": "1", + "type": "text" + }, + { + "key": "customer_id", + "value": "1", + "type": "text" + }, + { + "key": "amount_received", + "value": "1000", + "type": "text" + }, + { + "key": "invoice_id", + "value": "1", + "type": "text" + }, + { + "key": "payment_number", + "value": "RCPT-001", + "type": "text" + }, + { + "key": "payment_date", + "value": "2026-03-31", + "type": "text" + }, + { + "key": "reference_date", + "value": "2026-03-31", + "type": "text" + }, + { + "key": "deposit_to", + "value": "Cash on hand", + "type": "text" + }, + { + "key": "note", + "value": "Payment received at front desk", + "type": "text" + }, + { + "key": "bill_payments[0][bill_id]", + "value": "1", + "type": "text" + }, + { + "key": "bill_payments[0][amount]", + "value": "600", + "type": "text" + }, + { + "key": "bill_payments[1][bill_id]", + "value": "2", + "type": "text" + }, + { + "key": "bill_payments[1][amount]", + "value": "400", + "type": "text" + }, + { + "key": "attachment_files[]", + "type": "file", + "src": [ + "" + ] } - } + ] }, "url": { "raw": "{{base_url}}/api/payment-recieved", @@ -23111,8 +23582,8 @@ ] } }, - "status": "OK", - "code": 200, + "status": "Created", + "code": 201, "_postman_previewlanguage": "json", "header": [ { @@ -23121,7 +23592,7 @@ } ], "cookie": [], - "body": "{\n \"message\": \"Operation completed successfully.\",\n \"data\": {\n \"id\": 1,\n \"job_card_id\": 1,\n \"payment_mode_id\": 1,\n \"customer_id\": 1,\n \"amount_received\": 1000,\n \"payment_number\": \"RCPT-001\",\n \"payment_date\": \"2026-03-31\",\n \"reference_date\": \"2026-03-31\",\n \"deposit_to\": \"string\",\n \"note\": \"string\",\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\"\n }\n}" + "body": "{\n \"message\": \"Payment received created successfully.\",\n \"data\": {\n \"id\": 1,\n \"job_card_id\": 1,\n \"invoice_id\": 1,\n \"payment_mode_id\": 1,\n \"customer_id\": 1,\n \"amount_received\": 1000,\n \"payment_number\": \"RCPT-001\",\n \"payment_date\": \"2026-03-31\",\n \"reference_date\": \"2026-03-31\",\n \"deposit_to\": \"Cash on hand\",\n \"note\": \"Payment received at front desk\",\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\",\n \"job_card\": {\n \"id\": 1\n },\n \"invoice\": {\n \"id\": 1\n },\n \"payment_mode\": {\n \"id\": 1\n },\n \"customer\": {\n \"id\": 1\n },\n \"attachments\": []\n }\n}" } ] }, @@ -23143,20 +23614,72 @@ { "key": "Accept", "value": "application/json" - }, - { - "key": "Content-Type", - "value": "application/json" } ], "body": { - "mode": "raw", - "raw": "{\n \"job_card_id\": 1,\n \"payment_mode_id\": 1,\n \"customer_id\": 1,\n \"amount_received\": 1000,\n \"payment_date\": \"2026-03-31\",\n \"note\": \"string\"\n}", - "options": { - "raw": { - "language": "json" + "mode": "formdata", + "formdata": [ + { + "key": "job_card_id", + "value": "1", + "type": "text" + }, + { + "key": "payment_mode_id", + "value": "1", + "type": "text" + }, + { + "key": "customer_id", + "value": "1", + "type": "text" + }, + { + "key": "amount_received", + "value": "1000", + "type": "text" + }, + { + "key": "invoice_id", + "value": "1", + "type": "text" + }, + { + "key": "payment_number", + "value": "RCPT-001", + "type": "text" + }, + { + "key": "payment_date", + "value": "2026-03-31", + "type": "text" + }, + { + "key": "reference_date", + "value": "2026-03-31", + "type": "text" + }, + { + "key": "deposit_to", + "value": "Cash on hand", + "type": "text" + }, + { + "key": "note", + "value": "Updated payment note", + "type": "text" + }, + { + "key": "delete_attachment_ids[]", + "value": "1", + "type": "text" + }, + { + "key": "attachment_files[]", + "type": "file", + "value": "" } - } + ] }, "url": { "raw": "{{base_url}}/api/payment-recieved/{{id}}", @@ -23189,20 +23712,74 @@ { "key": "Accept", "value": "application/json" - }, - { - "key": "Content-Type", - "value": "application/json" } ], "body": { - "mode": "raw", - "raw": "{\n \"job_card_id\": 1,\n \"payment_mode_id\": 1,\n \"customer_id\": 1,\n \"amount_received\": 1000,\n \"payment_date\": \"2026-03-31\",\n \"note\": \"string\"\n}", - "options": { - "raw": { - "language": "json" + "mode": "formdata", + "formdata": [ + { + "key": "job_card_id", + "value": "1", + "type": "text" + }, + { + "key": "payment_mode_id", + "value": "1", + "type": "text" + }, + { + "key": "customer_id", + "value": "1", + "type": "text" + }, + { + "key": "amount_received", + "value": "1000", + "type": "text" + }, + { + "key": "invoice_id", + "value": "1", + "type": "text" + }, + { + "key": "payment_number", + "value": "RCPT-001", + "type": "text" + }, + { + "key": "payment_date", + "value": "2026-03-31", + "type": "text" + }, + { + "key": "reference_date", + "value": "2026-03-31", + "type": "text" + }, + { + "key": "deposit_to", + "value": "Cash on hand", + "type": "text" + }, + { + "key": "note", + "value": "Updated payment note", + "type": "text" + }, + { + "key": "delete_attachment_ids[]", + "value": "1", + "type": "text" + }, + { + "key": "attachment_files[]", + "type": "file", + "src": [ + "" + ] } - } + ] }, "url": { "raw": "{{base_url}}/api/payment-recieved/{{id}}", @@ -23226,7 +23803,7 @@ } ], "cookie": [], - "body": "{\n \"message\": \"Operation completed successfully.\",\n \"data\": {\n \"id\": 1,\n \"job_card_id\": 1,\n \"payment_mode_id\": 1,\n \"customer_id\": 1,\n \"amount_received\": 1000,\n \"payment_number\": \"RCPT-001\",\n \"payment_date\": \"2026-03-31\",\n \"reference_date\": \"2026-03-31\",\n \"deposit_to\": \"string\",\n \"note\": \"string\",\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\"\n }\n}" + "body": "{\n \"message\": \"Payment received updated successfully.\",\n \"data\": {\n \"id\": 1,\n \"job_card_id\": 1,\n \"invoice_id\": 1,\n \"payment_mode_id\": 1,\n \"customer_id\": 1,\n \"amount_received\": 1000,\n \"payment_number\": \"RCPT-001\",\n \"payment_date\": \"2026-03-31\",\n \"reference_date\": \"2026-03-31\",\n \"deposit_to\": \"Cash on hand\",\n \"note\": \"Updated payment note\",\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:05:00.000000Z\",\n \"job_card\": {\n \"id\": 1\n },\n \"invoice\": {\n \"id\": 1\n },\n \"payment_mode\": {\n \"id\": 1\n },\n \"customer\": {\n \"id\": 1\n },\n \"attachments\": []\n }\n}" } ] }, @@ -24239,7 +24816,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"job_card_id\": 1,\n \"vendor_id\": 1,\n \"title\": \"Purchase Order for Parts\",\n \"order_number\": \"PO-001\",\n \"order_date\": \"2026-03-31\",\n \"delivery_date\": \"2026-04-07\",\n \"department_id\": 1,\n \"notes\": \"string\",\n \"terms_and_conditions\": \"Net 30\",\n \"items\": [\n {\n \"part_id\": 1,\n \"quantity\": 2,\n \"rate\": 45.5,\n \"description\": \"Oil filter\"\n }\n ]\n}", + "raw": "{\n \"job_card_id\": 1,\n \"vendor_id\": 1,\n \"title\": \"Purchase Order for Parts\",\n \"order_number\": \"PO-001\",\n \"order_date\": \"2026-03-31\",\n \"delivery_date\": \"2026-04-07\",\n \"department_id\": 1,\n \"notes\": \"string\",\n \"terms_and_conditions\": \"Net 30\",\n \"discount_type\": \"no\",\n \"discount_amount\": 0,\n \"tax_id\": 0,\n \"items\": [\n {\n \"part_id\": 1,\n \"quantity\": 2,\n \"rate\": 45.5,\n \"tax_id\": 0,\n \"discount_amount\": 0,\n \"description\": \"Oil filter\"\n }\n ]\n}", "options": { "raw": { "language": "json" @@ -24284,7 +24861,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"job_card_id\": 1,\n \"vendor_id\": 1,\n \"title\": \"Purchase Order for Parts\",\n \"order_number\": \"PO-001\",\n \"order_date\": \"2026-03-31\",\n \"delivery_date\": \"2026-04-07\",\n \"department_id\": 1,\n \"notes\": \"string\",\n \"terms_and_conditions\": \"Net 30\",\n \"items\": [\n {\n \"part_id\": 1,\n \"quantity\": 2,\n \"rate\": 45.5,\n \"description\": \"Oil filter\"\n }\n ]\n}", + "raw": "{\n \"job_card_id\": 1,\n \"vendor_id\": 1,\n \"title\": \"Purchase Order for Parts\",\n \"order_number\": \"PO-001\",\n \"order_date\": \"2026-03-31\",\n \"delivery_date\": \"2026-04-07\",\n \"department_id\": 1,\n \"notes\": \"string\",\n \"terms_and_conditions\": \"Net 30\",\n \"discount_type\": \"no\",\n \"discount_amount\": 0,\n \"tax_id\": 0,\n \"items\": [\n {\n \"part_id\": 1,\n \"quantity\": 2,\n \"rate\": 45.5,\n \"tax_id\": 0,\n \"discount_amount\": 0,\n \"description\": \"Oil filter\"\n }\n ]\n}", "options": { "raw": { "language": "json" @@ -24342,7 +24919,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"title\": \"Purchase Order for Parts (updated)\",\n \"order_number\": \"PO-001\",\n \"delivery_date\": \"2026-04-08\",\n \"items\": [\n {\n \"part_id\": 1,\n \"quantity\": 3,\n \"rate\": 45.5,\n \"description\": \"Oil filter - updated qty\"\n },\n {\n \"part_id\": 2,\n \"quantity\": 1,\n \"rate\": 120,\n \"description\": \"Air filter\"\n }\n ]\n}", + "raw": "{\n \"title\": \"Purchase Order for Parts (updated)\",\n \"order_number\": \"PO-001\",\n \"delivery_date\": \"2026-04-08\",\n \"discount_type\": \"line_item_level\",\n \"discount_amount\": 0,\n \"tax_id\": 1,\n \"items\": [\n {\n \"part_id\": 1,\n \"quantity\": 3,\n \"rate\": 45.5,\n \"tax_id\": 1,\n \"discount_amount\": 2.5,\n \"description\": \"Oil filter - updated qty\"\n },\n {\n \"part_id\": 2,\n \"quantity\": 1,\n \"rate\": 120,\n \"tax_id\": 0,\n \"discount_amount\": 0,\n \"description\": \"Air filter\"\n }\n ]\n}", "options": { "raw": { "language": "json" @@ -24388,7 +24965,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"title\": \"Purchase Order for Parts (updated)\",\n \"order_number\": \"PO-001\",\n \"delivery_date\": \"2026-04-08\",\n \"items\": [\n {\n \"part_id\": 1,\n \"quantity\": 3,\n \"rate\": 45.5,\n \"description\": \"Oil filter - updated qty\"\n },\n {\n \"part_id\": 2,\n \"quantity\": 1,\n \"rate\": 120,\n \"description\": \"Air filter\"\n }\n ]\n}", + "raw": "{\n \"title\": \"Purchase Order for Parts (updated)\",\n \"order_number\": \"PO-001\",\n \"delivery_date\": \"2026-04-08\",\n \"discount_type\": \"line_item_level\",\n \"discount_amount\": 0,\n \"tax_id\": 1,\n \"items\": [\n {\n \"part_id\": 1,\n \"quantity\": 3,\n \"rate\": 45.5,\n \"tax_id\": 1,\n \"discount_amount\": 2.5,\n \"description\": \"Oil filter - updated qty\"\n },\n {\n \"part_id\": 2,\n \"quantity\": 1,\n \"rate\": 120,\n \"tax_id\": 0,\n \"discount_amount\": 0,\n \"description\": \"Air filter\"\n }\n ]\n}", "options": { "raw": { "language": "json" @@ -26404,7 +26981,94 @@ } ], "cookie": [], - "body": "{\n \"data\": [\n {\n \"id\": 1,\n \"title\": \"Workshop Bill\",\n \"job_card_id\": 1,\n \"vendor_id\": 1,\n \"vendor_address_id\": 1,\n \"purchase_order_id\": 1,\n \"bill_number\": \"BILL-001\",\n \"bill_date\": \"2026-03-31\",\n \"bill_due_date\": \"2026-04-14\",\n \"payment_terms_id\": 1,\n \"department_id\": 1,\n \"notes\": \"Vendor invoice for parts, service and expense lines\",\n \"status\": \"open\",\n \"services\": [\n {\n \"id\": 1,\n \"bill_id\": 1,\n \"service_id\": 1,\n \"quantity\": \"2.00\",\n \"rate\": \"150.00\",\n \"chart_of_account\": \"COA-401\",\n \"description\": \"Labor service line\"\n }\n ],\n \"parts\": [\n {\n \"id\": 1,\n \"bill_id\": 1,\n \"part_id\": 1,\n \"quantity\": 3,\n \"rate\": \"40.00\",\n \"chart_of_account\": 1201,\n \"description\": \"Brake pad set\"\n }\n ],\n \"expenses\": [\n {\n \"id\": 1,\n \"bill_id\": 1,\n \"expense_id\": 1,\n \"quantity\": \"1.00\",\n \"rate\": \"75.00\",\n \"chart_of_account\": \"COA-402\",\n \"description\": \"Consumables expense line\"\n }\n ],\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\"\n }\n ],\n \"meta\": {\n \"current_page\": 1,\n \"last_page\": 5,\n \"per_page\": 15,\n \"total\": 75\n },\n \"links\": {\n \"first\": \"https://api.example.com/resource?page=1\",\n \"last\": \"https://api.example.com/resource?page=5\",\n \"prev\": \"string\",\n \"next\": \"https://api.example.com/resource?page=2\"\n }\n}" + "body": "{\n \"data\": [\n {\n \"id\": 1,\n \"title\": \"Workshop Bill\",\n \"job_card_id\": 1,\n \"vendor_id\": 1,\n \"vendor_address_id\": 1,\n \"purchase_order_id\": 1,\n \"bill_number\": \"BILL-001\",\n \"bill_date\": \"2026-03-31\",\n \"bill_due_date\": \"2026-04-14\",\n \"payment_terms_id\": 1,\n \"department_id\": 1,\n \"notes\": \"Vendor invoice for parts, service and expense lines\",\n \"status\": \"open\",\n \"job_card\": {\n \"id\": 1,\n \"order_number\": \"ORD-0001\",\n \"estimate_number\": \"EST-0001\"\n },\n \"vendor\": {\n \"id\": 1,\n \"name\": \"Auto Parts Supplier\"\n },\n \"vendor_address\": {\n \"id\": 1,\n \"vendor_id\": 1,\n \"address\": \"123 Industrial Road\",\n \"country\": {\n \"id\": 1,\n \"name\": \"United Arab Emirates\"\n },\n \"state\": {\n \"id\": 1,\n \"name\": \"Dubai\"\n }\n },\n \"department\": {\n \"id\": 1,\n \"name\": \"Service Department\"\n },\n \"tax\": {\n \"id\": 1,\n \"name\": \"VAT 5%\",\n \"rate\": \"5.00\"\n },\n \"labels\": [\n {\n \"id\": 1,\n \"title\": \"Urgent\",\n \"color_code\": \"#FF6600\"\n }\n ],\n \"services\": [\n {\n \"id\": 1,\n \"bill_id\": 1,\n \"service_id\": 1,\n \"quantity\": \"2.00\",\n \"rate\": \"150.00\",\n \"chart_of_account\": \"COA-401\",\n \"description\": \"Labor service line\",\n \"service\": {\n \"id\": 1,\n \"name\": \"Brake Service\",\n \"price\": \"150.00\"\n }\n }\n ],\n \"parts\": [\n {\n \"id\": 1,\n \"bill_id\": 1,\n \"part_id\": 1,\n \"quantity\": 3,\n \"rate\": \"40.00\",\n \"chart_of_account\": 1201,\n \"description\": \"Brake pad set\",\n \"part\": {\n \"id\": 1,\n \"name\": \"Brake Pad Set\",\n \"part_number\": \"PART-001\"\n }\n }\n ],\n \"expenses\": [\n {\n \"id\": 1,\n \"bill_id\": 1,\n \"expense_id\": 1,\n \"quantity\": \"1.00\",\n \"rate\": \"75.00\",\n \"chart_of_account\": \"COA-402\",\n \"description\": \"Consumables expense line\",\n \"expense\": {\n \"id\": 1,\n \"title\": \"Consumables\",\n \"invoice_number\": \"EXP-001\"\n }\n }\n ],\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\"\n }\n ],\n \"meta\": {\n \"current_page\": 1,\n \"last_page\": 5,\n \"per_page\": 15,\n \"total\": 75\n },\n \"links\": {\n \"first\": \"https://api.example.com/resource?page=1\",\n \"last\": \"https://api.example.com/resource?page=5\",\n \"prev\": \"string\",\n \"next\": \"https://api.example.com/resource?page=2\"\n }\n}" + } + ] + }, + { + "name": "Display the specified bill.", + "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/bills/{{id}}", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "bills", + "{{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/bills/{{id}}", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "bills", + "{{id}}" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"data\": {\n \"id\": 1,\n \"title\": \"Workshop Bill\",\n \"job_card_id\": 1,\n \"vendor_id\": 1,\n \"vendor_address_id\": 1,\n \"purchase_order_id\": 1,\n \"bill_number\": \"BILL-001\",\n \"bill_date\": \"2026-03-31\",\n \"bill_due_date\": \"2026-04-14\",\n \"payment_terms_id\": 1,\n \"department_id\": 1,\n \"notes\": \"Vendor invoice for parts, service and expense lines\",\n \"status\": \"open\",\n \"job_card\": {\n \"id\": 1,\n \"order_number\": \"ORD-0001\",\n \"estimate_number\": \"EST-0001\"\n },\n \"vendor\": {\n \"id\": 1,\n \"name\": \"Auto Parts Supplier\"\n },\n \"vendor_address\": {\n \"id\": 1,\n \"vendor_id\": 1,\n \"address\": \"123 Industrial Road\",\n \"country\": {\n \"id\": 1,\n \"name\": \"United Arab Emirates\"\n },\n \"state\": {\n \"id\": 1,\n \"name\": \"Dubai\"\n }\n },\n \"department\": {\n \"id\": 1,\n \"name\": \"Service Department\"\n },\n \"tax\": {\n \"id\": 1,\n \"name\": \"VAT 5%\",\n \"rate\": \"5.00\"\n },\n \"labels\": [\n {\n \"id\": 1,\n \"title\": \"Urgent\",\n \"color_code\": \"#FF6600\"\n }\n ],\n \"services\": [\n {\n \"id\": 1,\n \"bill_id\": 1,\n \"service_id\": 1,\n \"quantity\": \"2.00\",\n \"rate\": \"150.00\",\n \"chart_of_account\": \"COA-401\",\n \"description\": \"Labor service line\",\n \"service\": {\n \"id\": 1,\n \"name\": \"Brake Service\",\n \"price\": \"150.00\"\n }\n }\n ],\n \"parts\": [\n {\n \"id\": 1,\n \"bill_id\": 1,\n \"part_id\": 1,\n \"quantity\": 3,\n \"rate\": \"40.00\",\n \"chart_of_account\": 1201,\n \"description\": \"Brake pad set\",\n \"part\": {\n \"id\": 1,\n \"name\": \"Brake Pad Set\",\n \"part_number\": \"PART-001\"\n }\n }\n ],\n \"expenses\": [\n {\n \"id\": 1,\n \"bill_id\": 1,\n \"expense_id\": 1,\n \"quantity\": \"1.00\",\n \"rate\": \"75.00\",\n \"chart_of_account\": \"COA-402\",\n \"description\": \"Consumables expense line\",\n \"expense\": {\n \"id\": 1,\n \"title\": \"Consumables\",\n \"invoice_number\": \"EXP-001\"\n }\n }\n ],\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\"\n }\n}" } ] }, @@ -26434,7 +27098,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"title\": \"Workshop Bill\",\n \"job_card_id\": 1,\n \"vendor_id\": 1,\n \"vendor_address_id\": 1,\n \"purchase_order_id\": 1,\n \"bill_number\": \"BILL-001\",\n \"bill_date\": \"2026-03-31\",\n \"bill_due_date\": \"2026-04-14\",\n \"payment_terms_id\": 1,\n \"department_id\": 1,\n \"notes\": \"Vendor invoice for parts, service and expense lines\",\n \"label_ids\": [1],\n \"part_items\": [\n {\n \"part_id\": 1,\n \"quantity\": 3,\n \"rate\": 40,\n \"chart_of_account\": 1201,\n \"description\": \"Brake pad set\"\n }\n ],\n \"service_items\": [\n {\n \"service_id\": 1,\n \"quantity\": 2,\n \"rate\": 150,\n \"chart_of_account\": \"COA-401\",\n \"description\": \"Labor service line\"\n }\n ],\n \"expense_items\": [\n {\n \"expense_id\": 1,\n \"quantity\": 1,\n \"rate\": 75,\n \"chart_of_account\": \"COA-402\",\n \"description\": \"Consumables expense line\"\n }\n ]\n}", + "raw": "{\n \"title\": \"Workshop Bill\",\n \"job_card_id\": 1,\n \"vendor_id\": 1,\n \"vendor_address_id\": 1,\n \"purchase_order_id\": 1,\n \"bill_number\": \"BILL-001\",\n \"bill_date\": \"2026-03-31\",\n \"bill_due_date\": \"2026-04-14\",\n \"payment_terms_id\": 1,\n \"department_id\": 1,\n \"discount_type\": \"line_item_level\",\n \"discount_amount\": 0,\n \"tax_id\": 1,\n \"status\": \"open\",\n \"notes\": \"Vendor invoice for parts, service and expense lines\",\n \"label_ids\": [1],\n \"part_items\": [\n {\n \"part_id\": 1,\n \"quantity\": 3,\n \"rate\": 40,\n \"chart_of_account\": 1201,\n \"description\": \"Brake pad set\",\n \"discount_amount\": 5.00\n }\n ],\n \"service_items\": [\n {\n \"service_id\": 1,\n \"quantity\": 2,\n \"rate\": 150,\n \"chart_of_account\": \"COA-401\",\n \"description\": \"Labor service line\",\n \"discount_amount\": 12.50\n }\n ],\n \"expense_items\": [\n {\n \"expense_id\": 1,\n \"quantity\": 1,\n \"rate\": 75,\n \"chart_of_account\": \"COA-402\",\n \"description\": \"Consumables expense line\",\n \"discount_amount\": 3.75\n }\n ]\n}", "options": { "raw": { "language": "json" @@ -26479,7 +27143,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"title\": \"Workshop Bill\",\n \"job_card_id\": 1,\n \"vendor_id\": 1,\n \"vendor_address_id\": 1,\n \"purchase_order_id\": 1,\n \"bill_number\": \"BILL-001\",\n \"bill_date\": \"2026-03-31\",\n \"bill_due_date\": \"2026-04-14\",\n \"payment_terms_id\": 1,\n \"department_id\": 1,\n \"notes\": \"Vendor invoice for parts, service and expense lines\",\n \"label_ids\": [1],\n \"part_items\": [\n {\n \"part_id\": 1,\n \"quantity\": 3,\n \"rate\": 40,\n \"chart_of_account\": 1201,\n \"description\": \"Brake pad set\"\n }\n ],\n \"service_items\": [\n {\n \"service_id\": 1,\n \"quantity\": 2,\n \"rate\": 150,\n \"chart_of_account\": \"COA-401\",\n \"description\": \"Labor service line\"\n }\n ],\n \"expense_items\": [\n {\n \"expense_id\": 1,\n \"quantity\": 1,\n \"rate\": 75,\n \"chart_of_account\": \"COA-402\",\n \"description\": \"Consumables expense line\"\n }\n ]\n}", + "raw": "{\n \"title\": \"Workshop Bill\",\n \"job_card_id\": 1,\n \"vendor_id\": 1,\n \"vendor_address_id\": 1,\n \"purchase_order_id\": 1,\n \"bill_number\": \"BILL-001\",\n \"bill_date\": \"2026-03-31\",\n \"bill_due_date\": \"2026-04-14\",\n \"payment_terms_id\": 1,\n \"department_id\": 1,\n \"discount_type\": \"line_item_level\",\n \"discount_amount\": 0,\n \"tax_id\": 1,\n \"status\": \"open\",\n \"notes\": \"Vendor invoice for parts, service and expense lines\",\n \"label_ids\": [1],\n \"part_items\": [\n {\n \"part_id\": 1,\n \"quantity\": 3,\n \"rate\": 40,\n \"chart_of_account\": 1201,\n \"description\": \"Brake pad set\",\n \"discount_amount\": 5.00\n }\n ],\n \"service_items\": [\n {\n \"service_id\": 1,\n \"quantity\": 2,\n \"rate\": 150,\n \"chart_of_account\": \"COA-401\",\n \"description\": \"Labor service line\",\n \"discount_amount\": 12.50\n }\n ],\n \"expense_items\": [\n {\n \"expense_id\": 1,\n \"quantity\": 1,\n \"rate\": 75,\n \"chart_of_account\": \"COA-402\",\n \"description\": \"Consumables expense line\",\n \"discount_amount\": 3.75\n }\n ]\n}", "options": { "raw": { "language": "json" @@ -26507,7 +27171,7 @@ } ], "cookie": [], - "body": "{\n \"message\": \"Operation completed successfully.\",\n \"data\": {\n \"id\": 1,\n \"title\": \"Workshop Bill\",\n \"job_card_id\": 1,\n \"vendor_id\": 1,\n \"vendor_address_id\": 1,\n \"purchase_order_id\": 1,\n \"bill_number\": \"BILL-001\",\n \"bill_date\": \"2026-03-31\",\n \"bill_due_date\": \"2026-04-14\",\n \"payment_terms_id\": 1,\n \"department_id\": 1,\n \"notes\": \"Vendor invoice for parts, service and expense lines\",\n \"status\": \"open\",\n \"services\": [\n {\n \"id\": 1,\n \"bill_id\": 1,\n \"service_id\": 1,\n \"quantity\": \"2.00\",\n \"rate\": \"150.00\",\n \"chart_of_account\": \"COA-401\",\n \"description\": \"Labor service line\"\n }\n ],\n \"parts\": [\n {\n \"id\": 1,\n \"bill_id\": 1,\n \"part_id\": 1,\n \"quantity\": 3,\n \"rate\": \"40.00\",\n \"chart_of_account\": 1201,\n \"description\": \"Brake pad set\"\n }\n ],\n \"expenses\": [\n {\n \"id\": 1,\n \"bill_id\": 1,\n \"expense_id\": 1,\n \"quantity\": \"1.00\",\n \"rate\": \"75.00\",\n \"chart_of_account\": \"COA-402\",\n \"description\": \"Consumables expense line\"\n }\n ],\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\"\n }\n}" + "body": "{\n \"message\": \"Operation completed successfully.\",\n \"data\": {\n \"id\": 1,\n \"title\": \"Workshop Bill\",\n \"job_card_id\": 1,\n \"vendor_id\": 1,\n \"vendor_address_id\": 1,\n \"purchase_order_id\": 1,\n \"bill_number\": \"BILL-001\",\n \"bill_date\": \"2026-03-31\",\n \"bill_due_date\": \"2026-04-14\",\n \"payment_terms_id\": 1,\n \"department_id\": 1,\n \"discount_type\": \"transaction\",\n \"notes\": \"Vendor invoice for parts, service and expense lines\",\n \"status\": \"open\",\n \"services\": [\n {\n \"id\": 1,\n \"bill_id\": 1,\n \"service_id\": 1,\n \"quantity\": \"2.00\",\n \"rate\": \"150.00\",\n \"chart_of_account\": \"COA-401\",\n \"description\": \"Labor service line\"\n }\n ],\n \"parts\": [\n {\n \"id\": 1,\n \"bill_id\": 1,\n \"part_id\": 1,\n \"quantity\": 3,\n \"rate\": \"40.00\",\n \"chart_of_account\": 1201,\n \"description\": \"Brake pad set\"\n }\n ],\n \"expenses\": [\n {\n \"id\": 1,\n \"bill_id\": 1,\n \"expense_id\": 1,\n \"quantity\": \"1.00\",\n \"rate\": \"75.00\",\n \"chart_of_account\": \"COA-402\",\n \"description\": \"Consumables expense line\"\n }\n ],\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\"\n }\n}" } ] }, @@ -26537,7 +27201,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"title\": \"Workshop Bill - Updated\",\n \"job_card_id\": 1,\n \"vendor_id\": 1,\n \"vendor_address_id\": 1,\n \"purchase_order_id\": 1,\n \"bill_number\": \"BILL-001\",\n \"bill_date\": \"2026-03-31\",\n \"bill_due_date\": \"2026-04-20\",\n \"payment_terms_id\": 1,\n \"department_id\": 1,\n \"notes\": \"Updated vendor invoice lines\",\n \"label_ids\": [1],\n \"part_items\": [\n {\n \"part_id\": 1,\n \"quantity\": 1,\n \"rate\": 45,\n \"chart_of_account\": 1201,\n \"description\": \"Updated part line\"\n }\n ],\n \"service_items\": [\n {\n \"service_id\": 1,\n \"quantity\": 1,\n \"rate\": 200,\n \"chart_of_account\": \"COA-401\",\n \"description\": \"Updated labor service line\"\n }\n ],\n \"expense_items\": [\n {\n \"expense_id\": 1,\n \"quantity\": 2,\n \"rate\": 50,\n \"chart_of_account\": \"COA-402\",\n \"description\": \"Updated consumables expense line\"\n }\n ]\n}", + "raw": "{\n \"title\": \"Workshop Bill - Updated\",\n \"job_card_id\": 1,\n \"vendor_id\": 1,\n \"vendor_address_id\": 1,\n \"purchase_order_id\": 1,\n \"bill_number\": \"BILL-001\",\n \"bill_date\": \"2026-03-31\",\n \"bill_due_date\": \"2026-04-20\",\n \"payment_terms_id\": 1,\n \"department_id\": 1,\n \"discount_type\": \"line_item_level\",\n \"discount_amount\": 0,\n \"tax_id\": 1,\n \"status\": \"open\",\n \"notes\": \"Updated vendor invoice lines\",\n \"label_ids\": [1],\n \"part_items\": [\n {\n \"part_id\": 1,\n \"quantity\": 1,\n \"rate\": 45,\n \"chart_of_account\": 1201,\n \"description\": \"Updated part line\",\n \"discount_amount\": 2.00\n }\n ],\n \"service_items\": [\n {\n \"service_id\": 1,\n \"quantity\": 1,\n \"rate\": 200,\n \"chart_of_account\": \"COA-401\",\n \"description\": \"Updated labor service line\",\n \"discount_amount\": 15.00\n }\n ],\n \"expense_items\": [\n {\n \"expense_id\": 1,\n \"quantity\": 2,\n \"rate\": 50,\n \"chart_of_account\": \"COA-402\",\n \"description\": \"Updated consumables expense line\",\n \"discount_amount\": 4.00\n }\n ]\n}", "options": { "raw": { "language": "json" @@ -26583,7 +27247,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"title\": \"Workshop Bill - Updated\",\n \"job_card_id\": 1,\n \"vendor_id\": 1,\n \"vendor_address_id\": 1,\n \"purchase_order_id\": 1,\n \"bill_number\": \"BILL-001\",\n \"bill_date\": \"2026-03-31\",\n \"bill_due_date\": \"2026-04-20\",\n \"payment_terms_id\": 1,\n \"department_id\": 1,\n \"notes\": \"Updated vendor invoice lines\",\n \"label_ids\": [1],\n \"part_items\": [\n {\n \"part_id\": 1,\n \"quantity\": 1,\n \"rate\": 45,\n \"chart_of_account\": 1201,\n \"description\": \"Updated part line\"\n }\n ],\n \"service_items\": [\n {\n \"service_id\": 1,\n \"quantity\": 1,\n \"rate\": 200,\n \"chart_of_account\": \"COA-401\",\n \"description\": \"Updated labor service line\"\n }\n ],\n \"expense_items\": [\n {\n \"expense_id\": 1,\n \"quantity\": 2,\n \"rate\": 50,\n \"chart_of_account\": \"COA-402\",\n \"description\": \"Updated consumables expense line\"\n }\n ]\n}", + "raw": "{\n \"title\": \"Workshop Bill - Updated\",\n \"job_card_id\": 1,\n \"vendor_id\": 1,\n \"vendor_address_id\": 1,\n \"purchase_order_id\": 1,\n \"bill_number\": \"BILL-001\",\n \"bill_date\": \"2026-03-31\",\n \"bill_due_date\": \"2026-04-20\",\n \"payment_terms_id\": 1,\n \"department_id\": 1,\n \"discount_type\": \"line_item_level\",\n \"discount_amount\": 0,\n \"tax_id\": 1,\n \"status\": \"open\",\n \"notes\": \"Updated vendor invoice lines\",\n \"label_ids\": [1],\n \"part_items\": [\n {\n \"part_id\": 1,\n \"quantity\": 1,\n \"rate\": 45,\n \"chart_of_account\": 1201,\n \"description\": \"Updated part line\",\n \"discount_amount\": 2.00\n }\n ],\n \"service_items\": [\n {\n \"service_id\": 1,\n \"quantity\": 1,\n \"rate\": 200,\n \"chart_of_account\": \"COA-401\",\n \"description\": \"Updated labor service line\",\n \"discount_amount\": 15.00\n }\n ],\n \"expense_items\": [\n {\n \"expense_id\": 1,\n \"quantity\": 2,\n \"rate\": 50,\n \"chart_of_account\": \"COA-402\",\n \"description\": \"Updated consumables expense line\",\n \"discount_amount\": 4.00\n }\n ]\n}", "options": { "raw": { "language": "json" @@ -27414,7 +28078,94 @@ } ], "cookie": [], - "body": "{\n \"data\": [\n {\n \"id\": 1,\n \"job_card_id\": 1,\n \"title\": \"Workshop Supplies\",\n \"category_id\": 1,\n \"vendor_id\": 1,\n \"invoice_number\": \"INV-001\",\n \"expense_date\": \"2026-03-31\",\n \"paid_through\": \"string\",\n \"department_id\": 1,\n \"notes\": \"string\",\n \"status\": \"draft\",\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\"\n }\n ],\n \"meta\": {\n \"current_page\": 1,\n \"last_page\": 5,\n \"per_page\": 15,\n \"total\": 75\n },\n \"links\": {\n \"first\": \"https://api.example.com/resource?page=1\",\n \"last\": \"https://api.example.com/resource?page=5\",\n \"prev\": \"string\",\n \"next\": \"https://api.example.com/resource?page=2\"\n }\n}" + "body": "{\n \"data\": [\n {\n \"id\": 1,\n \"job_card_id\": 1,\n \"title\": \"Workshop Supplies\",\n \"category_id\": 1,\n \"vendor_id\": 1,\n \"invoice_number\": \"INV-001\",\n \"expense_date\": \"2026-03-31\",\n \"paid_through\": 1001,\n \"department_id\": 1,\n \"tax_id\": 1,\n \"notes\": \"Shop supplies purchase\",\n \"status\": \"draft\",\n \"discount_amount_major\": 10,\n \"sub_total\": 240,\n \"tax_amount\": 12,\n \"total\": 242,\n \"payments_made\": 120,\n \"balance_due\": 122,\n \"job_card\": {\n \"id\": 1,\n \"order_number\": \"ORD-0001\",\n \"estimate_number\": \"EST-0001\"\n },\n \"category\": {\n \"id\": 1,\n \"name\": \"Consumables\"\n },\n \"vendor\": {\n \"id\": 1,\n \"name\": \"Auto Parts Supplier\"\n },\n \"department\": {\n \"id\": 1,\n \"name\": \"Service Department\"\n },\n \"tax\": {\n \"id\": 1,\n \"name\": \"VAT 5%\",\n \"rate\": \"5.00\"\n },\n \"labels\": [\n {\n \"id\": 1,\n \"title\": \"Urgent\",\n \"color_code\": \"#FF6600\"\n }\n ],\n \"expense_items\": [\n {\n \"id\": 1,\n \"expense_id\": 1,\n \"expense_item_id\": 1,\n \"quantity\": 2,\n \"rate\": \"120.00\",\n \"chart_of_account\": 1201,\n \"description\": \"Cleaning materials\",\n \"expense_item\": {\n \"id\": 1,\n \"name\": \"Workshop Consumable\",\n \"unit_price\": \"120.00\"\n }\n }\n ],\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\"\n }\n ],\n \"meta\": {\n \"current_page\": 1,\n \"last_page\": 5,\n \"per_page\": 15,\n \"total\": 75,\n \"from\": 1,\n \"to\": 15\n }\n}" + } + ] + }, + { + "name": "Display the specified expense.", + "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/expenses/{{id}}", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "expenses", + "{{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/expenses/{{id}}", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "expenses", + "{{id}}" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"data\": {\n \"id\": 1,\n \"job_card_id\": 1,\n \"title\": \"Workshop Supplies\",\n \"category_id\": 1,\n \"vendor_id\": 1,\n \"invoice_number\": \"INV-001\",\n \"expense_date\": \"2026-03-31\",\n \"paid_through\": 1001,\n \"department_id\": 1,\n \"tax_id\": 1,\n \"notes\": \"Shop supplies purchase\",\n \"status\": \"draft\",\n \"discount_amount_major\": 10,\n \"sub_total\": 240,\n \"tax_amount\": 12,\n \"total\": 242,\n \"payments_made\": 120,\n \"balance_due\": 122,\n \"job_card\": {\n \"id\": 1,\n \"order_number\": \"ORD-0001\",\n \"estimate_number\": \"EST-0001\"\n },\n \"category\": {\n \"id\": 1,\n \"name\": \"Consumables\"\n },\n \"vendor\": {\n \"id\": 1,\n \"name\": \"Auto Parts Supplier\"\n },\n \"department\": {\n \"id\": 1,\n \"name\": \"Service Department\"\n },\n \"tax\": {\n \"id\": 1,\n \"name\": \"VAT 5%\",\n \"rate\": \"5.00\"\n },\n \"labels\": [\n {\n \"id\": 1,\n \"title\": \"Urgent\",\n \"color_code\": \"#FF6600\"\n }\n ],\n \"expense_items\": [\n {\n \"id\": 1,\n \"expense_id\": 1,\n \"expense_item_id\": 1,\n \"quantity\": 2,\n \"rate\": \"120.00\",\n \"chart_of_account\": 1201,\n \"description\": \"Cleaning materials\",\n \"expense_item\": {\n \"id\": 1,\n \"name\": \"Workshop Consumable\",\n \"unit_price\": \"120.00\"\n }\n }\n ],\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\"\n }\n}" } ] }, @@ -27444,7 +28195,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"job_card_id\": 1,\n \"title\": \"Workshop Supplies\",\n \"category_id\": 1,\n \"vendor_id\": 1,\n \"expense_date\": \"2026-03-31\",\n \"department_id\": 1,\n \"status\": \"draft\"\n}", + "raw": "{\n \"job_card_id\": 1,\n \"title\": \"Workshop Supplies\",\n \"category_id\": 1,\n \"vendor_id\": 1,\n \"invoice_number\": \"INV-001\",\n \"expense_date\": \"2026-03-31\",\n \"paid_through\": 1001,\n \"department_id\": 1,\n \"tax_id\": 1,\n \"discount_amount\": 10,\n \"notes\": \"Shop supplies purchase\",\n \"status\": \"draft\",\n \"label_ids\": [1],\n \"items\": [\n {\n \"expense_item_id\": 1,\n \"quantity\": 2,\n \"rate\": 120,\n \"chart_of_account\": 1201,\n \"description\": \"Cleaning materials\"\n }\n ]\n}", "options": { "raw": { "language": "json" @@ -27464,7 +28215,7 @@ }, "response": [ { - "name": "200 OK", + "name": "201 Created", "originalRequest": { "auth": { "type": "bearer", @@ -27489,7 +28240,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"job_card_id\": 1,\n \"title\": \"Workshop Supplies\",\n \"category_id\": 1,\n \"vendor_id\": 1,\n \"expense_date\": \"2026-03-31\",\n \"department_id\": 1,\n \"status\": \"draft\"\n}", + "raw": "{\n \"job_card_id\": 1,\n \"title\": \"Workshop Supplies\",\n \"category_id\": 1,\n \"vendor_id\": 1,\n \"invoice_number\": \"INV-001\",\n \"expense_date\": \"2026-03-31\",\n \"paid_through\": 1001,\n \"department_id\": 1,\n \"tax_id\": 1,\n \"discount_amount\": 10,\n \"notes\": \"Shop supplies purchase\",\n \"status\": \"draft\",\n \"label_ids\": [1],\n \"items\": [\n {\n \"expense_item_id\": 1,\n \"quantity\": 2,\n \"rate\": 120,\n \"chart_of_account\": 1201,\n \"description\": \"Cleaning materials\"\n }\n ]\n}", "options": { "raw": { "language": "json" @@ -27507,8 +28258,8 @@ ] } }, - "status": "OK", - "code": 200, + "status": "Created", + "code": 201, "_postman_previewlanguage": "json", "header": [ { @@ -27517,7 +28268,7 @@ } ], "cookie": [], - "body": "{\n \"message\": \"Operation completed successfully.\",\n \"data\": {\n \"id\": 1,\n \"job_card_id\": 1,\n \"title\": \"Workshop Supplies\",\n \"category_id\": 1,\n \"vendor_id\": 1,\n \"invoice_number\": \"INV-001\",\n \"expense_date\": \"2026-03-31\",\n \"paid_through\": \"string\",\n \"department_id\": 1,\n \"notes\": \"string\",\n \"status\": \"draft\",\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\"\n }\n}" + "body": "{\n \"message\": \"Expense created successfully.\",\n \"data\": {\n \"id\": 1,\n \"job_card_id\": 1,\n \"title\": \"Workshop Supplies\",\n \"category_id\": 1,\n \"vendor_id\": 1,\n \"invoice_number\": \"INV-001\",\n \"expense_date\": \"2026-03-31\",\n \"paid_through\": 1001,\n \"department_id\": 1,\n \"tax_id\": 1,\n \"notes\": \"Shop supplies purchase\",\n \"status\": \"draft\",\n \"discount_amount_major\": 10,\n \"sub_total\": 240,\n \"tax_amount\": 12,\n \"total\": 242,\n \"payments_made\": 120,\n \"balance_due\": 122,\n \"job_card\": {\n \"id\": 1,\n \"order_number\": \"ORD-0001\",\n \"estimate_number\": \"EST-0001\"\n },\n \"category\": {\n \"id\": 1,\n \"name\": \"Consumables\"\n },\n \"vendor\": {\n \"id\": 1,\n \"name\": \"Auto Parts Supplier\"\n },\n \"department\": {\n \"id\": 1,\n \"name\": \"Service Department\"\n },\n \"tax\": {\n \"id\": 1,\n \"name\": \"VAT 5%\",\n \"rate\": \"5.00\"\n },\n \"labels\": [\n {\n \"id\": 1,\n \"title\": \"Urgent\",\n \"color_code\": \"#FF6600\"\n }\n ],\n \"expense_items\": [\n {\n \"id\": 1,\n \"expense_id\": 1,\n \"expense_item_id\": 1,\n \"quantity\": 2,\n \"rate\": \"120.00\",\n \"chart_of_account\": 1201,\n \"description\": \"Cleaning materials\",\n \"expense_item\": {\n \"id\": 1,\n \"name\": \"Workshop Consumable\",\n \"unit_price\": \"120.00\"\n }\n }\n ],\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\"\n }\n}" } ] }, @@ -27547,7 +28298,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"job_card_id\": 1,\n \"title\": \"Workshop Supplies\",\n \"category_id\": 1,\n \"vendor_id\": 1,\n \"expense_date\": \"2026-03-31\",\n \"department_id\": 1,\n \"status\": \"draft\"\n}", + "raw": "{\n \"job_card_id\": 1,\n \"title\": \"Workshop Supplies Updated\",\n \"category_id\": 1,\n \"vendor_id\": 1,\n \"invoice_number\": \"INV-001\",\n \"expense_date\": \"2026-03-31\",\n \"paid_through\": 1001,\n \"department_id\": 1,\n \"tax_id\": 1,\n \"discount_amount\": 8,\n \"notes\": \"Updated shop supplies purchase\",\n \"status\": \"open\",\n \"label_ids\": [1],\n \"items\": [\n {\n \"expense_item_id\": 1,\n \"quantity\": 3,\n \"rate\": 100,\n \"chart_of_account\": 1201,\n \"description\": \"Updated cleaning materials\"\n }\n ]\n}", "options": { "raw": { "language": "json" @@ -27593,7 +28344,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"job_card_id\": 1,\n \"title\": \"Workshop Supplies\",\n \"category_id\": 1,\n \"vendor_id\": 1,\n \"expense_date\": \"2026-03-31\",\n \"department_id\": 1,\n \"status\": \"draft\"\n}", + "raw": "{\n \"job_card_id\": 1,\n \"title\": \"Workshop Supplies Updated\",\n \"category_id\": 1,\n \"vendor_id\": 1,\n \"invoice_number\": \"INV-001\",\n \"expense_date\": \"2026-03-31\",\n \"paid_through\": 1001,\n \"department_id\": 1,\n \"tax_id\": 1,\n \"discount_amount\": 8,\n \"notes\": \"Updated shop supplies purchase\",\n \"status\": \"open\",\n \"label_ids\": [1],\n \"items\": [\n {\n \"expense_item_id\": 1,\n \"quantity\": 3,\n \"rate\": 100,\n \"chart_of_account\": 1201,\n \"description\": \"Updated cleaning materials\"\n }\n ]\n}", "options": { "raw": { "language": "json" @@ -27622,7 +28373,7 @@ } ], "cookie": [], - "body": "{\n \"message\": \"Operation completed successfully.\",\n \"data\": {\n \"id\": 1,\n \"job_card_id\": 1,\n \"title\": \"Workshop Supplies\",\n \"category_id\": 1,\n \"vendor_id\": 1,\n \"invoice_number\": \"INV-001\",\n \"expense_date\": \"2026-03-31\",\n \"paid_through\": \"string\",\n \"department_id\": 1,\n \"notes\": \"string\",\n \"status\": \"draft\",\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\"\n }\n}" + "body": "{\n \"message\": \"Expense updated successfully.\",\n \"data\": {\n \"id\": 1,\n \"job_card_id\": 1,\n \"title\": \"Workshop Supplies Updated\",\n \"category_id\": 1,\n \"vendor_id\": 1,\n \"invoice_number\": \"INV-001\",\n \"expense_date\": \"2026-03-31\",\n \"paid_through\": 1001,\n \"department_id\": 1,\n \"tax_id\": 1,\n \"notes\": \"Updated shop supplies purchase\",\n \"status\": \"open\",\n \"discount_amount_major\": 8,\n \"sub_total\": 300,\n \"tax_amount\": 15,\n \"total\": 307,\n \"payments_made\": 120,\n \"balance_due\": 187,\n \"job_card\": {\n \"id\": 1,\n \"order_number\": \"ORD-0001\",\n \"estimate_number\": \"EST-0001\"\n },\n \"category\": {\n \"id\": 1,\n \"name\": \"Consumables\"\n },\n \"vendor\": {\n \"id\": 1,\n \"name\": \"Auto Parts Supplier\"\n },\n \"department\": {\n \"id\": 1,\n \"name\": \"Service Department\"\n },\n \"tax\": {\n \"id\": 1,\n \"name\": \"VAT 5%\",\n \"rate\": \"5.00\"\n },\n \"labels\": [\n {\n \"id\": 1,\n \"title\": \"Urgent\",\n \"color_code\": \"#FF6600\"\n }\n ],\n \"expense_items\": [\n {\n \"id\": 1,\n \"expense_id\": 1,\n \"expense_item_id\": 1,\n \"quantity\": 3,\n \"rate\": \"100.00\",\n \"chart_of_account\": 1201,\n \"description\": \"Updated cleaning materials\",\n \"expense_item\": {\n \"id\": 1,\n \"name\": \"Workshop Consumable\",\n \"unit_price\": \"120.00\"\n }\n }\n ],\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T11:00:00.000000Z\"\n }\n}" } ] }, @@ -33656,7 +34407,7 @@ ], "body": { "mode": "raw", - "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}", + "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 \"discount\": \"line_item_level\",\n \"discount_amount\": 0,\n \"tax_id\": 1,\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 \"discount_amount\": 10.00,\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 \"discount_amount\": 20.00,\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 \"discount_amount\": 5.00,\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 \"discount_amount\": 30.00,\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 \"discount_amount\": 15.00,\n \"department_id\": 1\n }\n ]\n}", "options": { "raw": { "language": "json" @@ -33701,7 +34452,7 @@ ], "body": { "mode": "raw", - "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}", + "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 \"discount\": \"line_item_level\",\n \"discount_amount\": 0,\n \"tax_id\": 1,\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 \"discount_amount\": 10.00,\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 \"discount_amount\": 20.00,\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 \"discount_amount\": 5.00,\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 \"discount_amount\": 30.00,\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 \"discount_amount\": 15.00,\n \"department_id\": 1\n }\n ]\n}", "options": { "raw": { "language": "json" @@ -33729,7 +34480,7 @@ } ], "cookie": [], - "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}" + "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\": 2595,\n \"discount\": \"no\",\n \"tax_id\": 1,\n \"discount_amount_major\": 50,\n \"sub_total\": 2300,\n \"total\": 2595,\n \"payments_recieved\": 0,\n \"balance_due\": 2595,\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}" } ] }, @@ -33846,7 +34597,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"subject\": \"Invoice for Service\",\n \"customer_id\": 1,\n \"vehicle_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 \"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\": \"Payment due in 14 days.\",\n \"status\": \"draft\",\n \"discount\": \"transaction_level\",\n \"discount_amount\": 25.50,\n \"tax_id\": 1\n}", "options": { "raw": { "language": "json" @@ -33892,7 +34643,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"subject\": \"Invoice for Service\",\n \"customer_id\": 1,\n \"vehicle_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 \"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\": \"Payment due in 14 days.\",\n \"status\": \"draft\",\n \"discount\": \"transaction_level\",\n \"discount_amount\": 25.50,\n \"tax_id\": 1\n}", "options": { "raw": { "language": "json" @@ -33921,7 +34672,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 \"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\": \"Operation completed successfully.\",\n \"data\": {\n \"id\": 1,\n \"subject\": \"Invoice for Job Card 001\",\n \"customer_id\": 1,\n \"vehicle_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 \"tax_id\": 1,\n \"tax_amount\": 0,\n \"discount_amount_major\": 25.5,\n \"sub_total\": 0,\n \"total\": 0,\n \"payments_recieved\": 0,\n \"balance_due\": 0,\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\"\n }\n}" } ] }, diff --git a/packages/api/src/clients/estimates.ts b/packages/api/src/clients/estimates.ts index 02c7750..62f2226 100644 --- a/packages/api/src/clients/estimates.ts +++ b/packages/api/src/clients/estimates.ts @@ -1,6 +1,6 @@ import { CrudClient } from "../infra/crud-client" import type { ApiClientOptions } from "../infra/client" -import type { ApiPath, ApiResponse } from "../infra/types" +import type { ApiPath, ApiRequestBody, ApiResponse } from "../infra/types" export const ESTIMATE_ROUTES = { INDEX: "/api/estimates", @@ -12,6 +12,7 @@ export const ESTIMATE_ROUTES = { EXPENSE_ITEMS: "/api/estimate/{id}/expense-items", EXPENSE_ITEM_BY_ID: "/api/estimate/{id}/expense-items/{expense_item_id}", STORE_AUTHORISATION: "/api/estimates/{id}/store-authorisation", + CONVERT_TO_JOB_CARD: "/api/estimates/{id}/convert-to-job-card", } as const satisfies Record export class EstimatesClient extends CrudClient< @@ -22,13 +23,6 @@ export class EstimatesClient extends CrudClient< super(baseUrl, defaultOptions, ESTIMATE_ROUTES.INDEX, ESTIMATE_ROUTES.BY_ID) } - // 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.BY_ID, { params: { id } }) - return data; - } - // ── Estimate Services ── async listServices(estimateId: string) { return this.get(ESTIMATE_ROUTES.SERVICES, { params: { id: estimateId } } as never) @@ -91,4 +85,8 @@ export class EstimatesClient extends CrudClient< }) { return this.post(ESTIMATE_ROUTES.STORE_AUTHORISATION, payload as never, { params: { id: estimateId } } as never) } + + async convertToJobCard(estimateId: string, payload: ApiRequestBody) { + return this.post(ESTIMATE_ROUTES.CONVERT_TO_JOB_CARD, payload, { params: { id: estimateId } }) + } } diff --git a/packages/api/src/clients/expenses.ts b/packages/api/src/clients/expenses.ts index 133c228..4d5aed9 100644 --- a/packages/api/src/clients/expenses.ts +++ b/packages/api/src/clients/expenses.ts @@ -66,6 +66,10 @@ export class ExpensesClient extends CrudClient< return this.list(query) } + async getById(id: string) { + const res = await this.show(id) + return res; + } async createExpense(payload: ApiRequestBody) { return this.create(payload) } diff --git a/packages/api/src/clients/index.ts b/packages/api/src/clients/index.ts index 4262e1b..02e20b3 100644 --- a/packages/api/src/clients/index.ts +++ b/packages/api/src/clients/index.ts @@ -18,7 +18,7 @@ export { EstimatesClient, ESTIMATE_ROUTES } from "./estimates" export { QuickRemarksClient, QUICK_REMARK_ROUTES } from "./quick-remarks" export { QuickNotesClient, QUICK_NOTE_ROUTES } from "./quick-notes" export { ShopRecommendationsClient, SHOP_RECOMMENDATION_ROUTES } from "./shop-recommendations" -export { JobCardsClient, JOB_CARD_ROUTES } from "./job-cards" +export { JobCardsClient, JOB_CARD_ROUTES, type JobCardShowData } from "./job-cards" export { PaymentModesClient, PAYMENT_MODE_ROUTES } from "./payment-modes" export { PaymentReceivedClient, PAYMENT_RECEIVED_ROUTES } from "./payment-received" export { PartsClient, PARTS_ROUTES } from "./parts" @@ -34,7 +34,7 @@ export { ShopTimingsClient, SHOP_TIMING_ROUTES } from "./shop-timings" export { ShopCalendarsClient, SHOP_CALENDAR_ROUTES } from "./shop-calendars" export { HolidayYearsClient, HOLIDAY_YEAR_ROUTES } from "./holiday-years" export { TaxesClient, TAX_ROUTES } from "./taxes" -export { InvoicesClient, INVOICE_ROUTES } from "./invoices" +export { InvoicesClient, INVOICE_ROUTES, type InvoiceShowData } from "./invoices" export { HomeClient, HOME_ROUTES, type HomeDashboardResponse } from "./home" export { BillsClient, BILL_ROUTES } from "./bills" export { ReasonsClient, REASON_ROUTES } from "./reasons" diff --git a/packages/api/src/clients/invoices.ts b/packages/api/src/clients/invoices.ts index 5b34ff6..93e6c7a 100644 --- a/packages/api/src/clients/invoices.ts +++ b/packages/api/src/clients/invoices.ts @@ -1,7 +1,7 @@ import { CrudClient } from "../infra/crud-client" import { type ApiClientOptions } from "../infra/client" import type { ApiPath, ApiRequestBody } from "../infra/types" -import type { ApiListQueryParams } from "../contracts/types" +import type { ApiListQueryParams, ApiBaseResponse } from "../contracts/types" export const INVOICE_ROUTES = { INDEX: "/api/invoices", @@ -16,9 +16,28 @@ export const INVOICE_ROUTES = { LABEL_BY_ID: "/api/invoice-labels/{id}", } as const satisfies Record +// Enhanced invoice type with additional fields +export interface InvoiceShowData { + id?: string | number + amount?: string | number + received_payment?: string | number + customer?: { + id?: string | number + first_name?: string + last_name?: string + company_name?: string + } + customer_id?: string | number + customer_name?: string + [key: string]: any // Allow additional properties +} + export class InvoicesClient extends CrudClient< typeof INVOICE_ROUTES.INDEX, - typeof INVOICE_ROUTES.BY_ID + typeof INVOICE_ROUTES.BY_ID, + { + showResponse: ApiBaseResponse + } > { constructor(baseUrl?: string, defaultOptions?: ApiClientOptions) { super(baseUrl, defaultOptions, INVOICE_ROUTES.INDEX, INVOICE_ROUTES.BY_ID) diff --git a/packages/api/src/clients/job-cards.ts b/packages/api/src/clients/job-cards.ts index 9348e67..19bf810 100644 --- a/packages/api/src/clients/job-cards.ts +++ b/packages/api/src/clients/job-cards.ts @@ -30,6 +30,11 @@ export const JOB_CARD_ROUTES = { DELETE_SERVICE: "/api/job-cards/{id}/delete-service", ADD_SERVICE_ATTACHMENT: "/api/job-cards/{id}/add-service-attachment", DELETE_SERVICE_ATTACHMENT: "/api/job-cards/{id}/delete-service-attachment", + GET_EXPENSE_ITEMS: "/api/job-cards/{id}/get-expense-items", + ADD_EXPENSE_ITEM: "/api/job-cards/{id}/add-expense-item", + UPDATE_EXPENSE_ITEM: "/api/job-cards/{id}/update-expense-item", + DELETE_EXPENSE_ITEM: "/api/job-cards/{id}/delete-expense-item", + CONVERT_TO_INVOICE: "/api/job-cards/{id}/convert-to-invoice", } as const satisfies Record @@ -157,4 +162,24 @@ export class JobCardsClient extends CrudClient< async deleteServiceAttachment(id: string, jobCardServiceId: number, attachmentId: number) { return this.delete(JOB_CARD_ROUTES.DELETE_SERVICE_ATTACHMENT, { params: { id }, body: { job_card_service_id: jobCardServiceId, attachment_id: attachmentId } } as never) } + + async getExpenseItems(id: string, params?: Record) { + return this.get(JOB_CARD_ROUTES.GET_EXPENSE_ITEMS, { params: { id }, query: params as any }) + } + + async addExpenseItem(id: string, payload: ApiRequestBody) { + return this.post(JOB_CARD_ROUTES.ADD_EXPENSE_ITEM, payload, { params: { id } }) + } + + async updateExpenseItem(id: string, payload: ApiRequestBody) { + return this.put(JOB_CARD_ROUTES.UPDATE_EXPENSE_ITEM, payload, { params: { id } }) + } + + async deleteExpenseItem(id: string, jobCardExpenseItemId: number) { + return this.delete(JOB_CARD_ROUTES.DELETE_EXPENSE_ITEM, { params: { id }, body: { job_card_expense_item_id: jobCardExpenseItemId } } as never) + } + + async convertToInvoice(id: string, payload: ApiRequestBody) { + return this.post(JOB_CARD_ROUTES.CONVERT_TO_INVOICE, payload, { params: { id } }) + } } diff --git a/packages/api/src/contracts/enums.ts b/packages/api/src/contracts/enums.ts index 713354d..b8dba53 100644 --- a/packages/api/src/contracts/enums.ts +++ b/packages/api/src/contracts/enums.ts @@ -109,7 +109,7 @@ export const SellRatesTaxInclusive = ['Tax Inclusive', 'Tax Exclusive'] as const export type SellRatesTaxInclusive = (typeof SellRatesTaxInclusive)[number]; // Tables -export const Tables= ['bills', 'expenses', 'invoices', 'job_cards', 'credit_notes', 'vendor_credits', 'estimates'] as const; +export const Tables= ['bills', 'payments', 'expenses', 'invoices', 'job_cards', 'credit_notes', 'vendor_credits', 'estimates','purchase-order'] as const; export type Tables = (typeof Tables)[number]; export const GiveDiscounts = ['no', 'line_item_level', 'transaction_level'] as const; diff --git a/packages/api/types/index.ts b/packages/api/types/index.ts index 7bb6d0f..ad6fe1a 100644 --- a/packages/api/types/index.ts +++ b/packages/api/types/index.ts @@ -9632,6 +9632,7 @@ export interface paths { * "title": "Engine Parts", * "image": "string", * "is_favorite": false, + * "type": "part", * "created_at": "2026-03-31T10:00:00.000000Z", * "updated_at": "2026-03-31T10:00:00.000000Z" * } @@ -9657,6 +9658,7 @@ export interface paths { title?: string; image?: string; is_favorite?: boolean; + type?: string; /** Format: date-time */ created_at?: string; /** Format: date-time */ @@ -9693,12 +9695,14 @@ export interface paths { /** * @example { * "title": "Oil & Fluids", - * "shop_type_id": 1 + * "shop_type_id": 1, + * "type": "expense" * } */ "application/json": { title?: string; shop_type_id?: number; + type?: string; }; }; }; @@ -9718,6 +9722,7 @@ export interface paths { * "title": "Engine Parts", * "image": "string", * "is_favorite": false, + * "type": "expense", * "created_at": "2026-03-31T10:00:00.000000Z", * "updated_at": "2026-03-31T10:00:00.000000Z" * } @@ -9731,6 +9736,7 @@ export interface paths { title?: string; image?: string; is_favorite?: boolean; + type?: string; /** Format: date-time */ created_at?: string; /** Format: date-time */ @@ -9770,12 +9776,14 @@ export interface paths { /** * @example { * "title": "Oil & Fluids", - * "shop_type_id": 1 + * "shop_type_id": 1, + * "type": "expense" * } */ "application/json": { title?: string; shop_type_id?: number; + type?: string; }; }; }; @@ -9795,6 +9803,7 @@ export interface paths { * "title": "Engine Parts", * "image": "string", * "is_favorite": false, + * "type": "expense", * "created_at": "2026-03-31T10:00:00.000000Z", * "updated_at": "2026-03-31T10:00:00.000000Z" * } @@ -9808,6 +9817,7 @@ export interface paths { title?: string; image?: string; is_favorite?: boolean; + type?: string; /** Format: date-time */ created_at?: string; /** Format: date-time */ @@ -11424,7 +11434,6 @@ export interface paths { * "rate": "120.00", * "working_hours": "2.50", * "labor_hours": "2.00", - * "tax": "5%", * "chart_of_account": "COA-123", * "department_id": 1, * "description": "Full vehicle inspection.", @@ -11452,10 +11461,13 @@ export interface paths { * "id": 1, * "title": "Standard", * "rate": "95.00", - * "is_favorite": true + * "is_favorite": true, + * "discount_amount": 0 * }, * "created_at": "2026-03-31T10:00:00.000000Z", - * "updated_at": "2026-03-31T10:00:00.000000Z" + * "updated_at": "2026-03-31T10:00:00.000000Z", + * "tax_id": 5, + * "discount_amount": 0 * } * ], * "meta": { @@ -11488,7 +11500,6 @@ export interface paths { rate?: string; working_hours?: string; labor_hours?: string; - tax?: string; chart_of_account?: string; department_id?: number; description?: string; @@ -11515,11 +11526,14 @@ export interface paths { title?: string; rate?: string; is_favorite?: boolean; + discount_amount?: number; }; /** Format: date-time */ created_at?: string; /** Format: date-time */ updated_at?: string; + tax_id?: number; + discount_amount?: number; }[]; meta?: { current_page?: number; @@ -11565,9 +11579,10 @@ export interface paths { * "rate": 120, * "working_hours": 2.5, * "labor_hours": 2, - * "tax": "5%", * "chart_of_account": "COA-123", - * "description": "Full vehicle inspection." + * "description": "Full vehicle inspection.", + * "tax_id": 5, + * "discount_amount": 0 * } */ "application/json": { @@ -11589,9 +11604,10 @@ export interface paths { rate?: number; working_hours?: number; labor_hours?: number; - tax?: string; chart_of_account?: string; description?: string; + tax_id?: number; + discount_amount?: number; }; }; }; @@ -11624,7 +11640,6 @@ export interface paths { * "rate": "120.00", * "working_hours": "2.50", * "labor_hours": "2.00", - * "tax": "5%", * "chart_of_account": "COA-123", * "department_id": 1, * "description": "Full vehicle inspection.", @@ -11638,10 +11653,13 @@ export interface paths { * "id": 1, * "title": "Standard", * "rate": "95.00", - * "is_favorite": true + * "is_favorite": true, + * "discount_amount": 0 * }, * "created_at": "2026-03-31T10:00:00.000000Z", - * "updated_at": "2026-03-31T10:00:00.000000Z" + * "updated_at": "2026-03-31T10:00:00.000000Z", + * "tax_id": 5, + * "discount_amount": 0 * } * } */ @@ -11666,7 +11684,6 @@ export interface paths { rate?: string; working_hours?: string; labor_hours?: string; - tax?: string; chart_of_account?: string; department_id?: number; description?: string; @@ -11681,11 +11698,14 @@ export interface paths { title?: string; rate?: string; is_favorite?: boolean; + discount_amount?: number; }; /** Format: date-time */ created_at?: string; /** Format: date-time */ updated_at?: string; + tax_id?: number; + discount_amount?: number; }; }; }; @@ -11738,9 +11758,10 @@ export interface paths { * "rate": 135.5, * "working_hours": 3, * "labor_hours": 2.75, - * "tax": "8%", * "chart_of_account": "COA-456", - * "description": "Inspection completed." + * "description": "Inspection completed.", + * "tax_id": 8, + * "discount_amount": 0 * } */ "application/json": { @@ -11762,9 +11783,10 @@ export interface paths { rate?: number; working_hours?: number; labor_hours?: number; - tax?: string; chart_of_account?: string; description?: string; + tax_id?: number; + discount_amount?: number; }; }; }; @@ -11797,7 +11819,6 @@ export interface paths { * "rate": "135.50", * "working_hours": "3.00", * "labor_hours": "2.75", - * "tax": "8%", * "chart_of_account": "COA-456", * "department_id": 1, * "description": "Inspection completed.", @@ -11815,10 +11836,13 @@ export interface paths { * "id": 2, * "title": "Premium", * "rate": "110.00", - * "is_favorite": false + * "is_favorite": false, + * "discount_amount": 0 * }, * "created_at": "2026-03-31T10:00:00.000000Z", - * "updated_at": "2026-03-31T11:30:00.000000Z" + * "updated_at": "2026-03-31T11:30:00.000000Z", + * "tax_id": 8, + * "discount_amount": 0 * } * } */ @@ -11843,7 +11867,6 @@ export interface paths { rate?: string; working_hours?: string; labor_hours?: string; - tax?: string; chart_of_account?: string; department_id?: number; description?: string; @@ -11862,11 +11885,14 @@ export interface paths { title?: string; rate?: string; is_favorite?: boolean; + discount_amount?: number; }; /** Format: date-time */ created_at?: string; /** Format: date-time */ updated_at?: string; + tax_id?: number; + discount_amount?: number; }; }; }; @@ -13188,6 +13214,102 @@ export interface paths { patch?: never; trace?: never; }; + "/api/estimates/{id}/convert-to-job-card": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** POST /api/estimates/{id}/convert-to-job-card */ + post: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "allow_duplicate": false, + * "status": "draft", + * "order_number": "JC-EST-001" + * } + */ + "application/json": { + allow_duplicate?: boolean; + status?: string; + order_number?: string; + }; + }; + }; + responses: { + /** @description Created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "message": "Estimate converted to job card successfully.", + * "data": { + * "id": 10, + * "title": "Estimate for Toyota Camry", + * "estimate_id": 1, + * "estimate_number": "EST-001", + * "status": "draft" + * } + * } + */ + "application/json": { + message?: string; + data?: { + id?: number; + title?: string; + estimate_id?: number; + estimate_number?: string; + status?: string; + }; + }; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "message": "A job card already exists for this estimate.", + * "data": { + * "job_card_id": 5 + * } + * } + */ + "application/json": { + message?: string; + data?: { + job_card_id?: number; + }; + }; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/estimates/{id}/add-attachment": { parameters: { query?: never; @@ -13497,10 +13619,11 @@ export interface paths { * "rate": "120.00", * "working_hours": "1.00", * "labor_hours": "1.00", - * "tax": "5", * "chart_of_account": "4000", * "department_id": 1, - * "description": "Labor line" + * "description": "Labor line", + * "tax_id": 5, + * "discount_amount": 0 * } */ "application/json": { @@ -13511,10 +13634,11 @@ export interface paths { rate?: string; working_hours?: string; labor_hours?: string; - tax?: string; chart_of_account?: string; department_id?: number; description?: string; + tax_id?: number; + discount_amount?: number; }; }; }; @@ -13558,12 +13682,14 @@ export interface paths { /** * @example { * "rate": "130.00", - * "quantity": 2 + * "quantity": 2, + * "discount_amount": 0 * } */ "application/json": { rate?: string; quantity?: number; + discount_amount?: number; }; }; }; @@ -13769,20 +13895,22 @@ export interface paths { * "part_id": 1, * "quantity": 2, * "rate": "45.50", - * "tax": "5", * "chart_of_account": "5000", * "department_id": 1, - * "description": "Oil filter" + * "description": "Oil filter", + * "tax_id": 5, + * "discount_amount": 0 * } */ "application/json": { part_id?: number; quantity?: number; rate?: string; - tax?: string; chart_of_account?: string; department_id?: number; description?: string; + tax_id?: number; + discount_amount?: number; }; }; }; @@ -13825,11 +13953,13 @@ export interface paths { content: { /** * @example { - * "quantity": 3 + * "quantity": 3, + * "discount_amount": 0 * } */ "application/json": { quantity?: number; + discount_amount?: number; }; }; }; @@ -14035,20 +14165,22 @@ export interface paths { * "expense_item_id": 1, * "quantity": 1, * "rate": "25.00", - * "tax": "5", * "chart_of_account": "6000", * "department_id": 1, - * "description": "Shop supplies" + * "description": "Shop supplies", + * "tax_id": 5, + * "discount_amount": 0 * } */ "application/json": { expense_item_id?: number; quantity?: number; rate?: string; - tax?: string; chart_of_account?: string; department_id?: number; description?: string; + tax_id?: number; + discount_amount?: number; }; }; }; @@ -14091,11 +14223,13 @@ export interface paths { content: { /** * @example { - * "quantity": 2 + * "quantity": 2, + * "discount_amount": 0 * } */ "application/json": { quantity?: number; + discount_amount?: number; }; }; }; @@ -14405,6 +14539,9 @@ export interface paths { * "insurer_id": null, * "service_writer_id": 1, * "footer": "Thank you for your business.", + * "discount": "no", + * "discount_amount": 0, + * "tax_id": 0, * "label_ids": [ * 1, * 2 @@ -14427,6 +14564,9 @@ export interface paths { insurer_id?: string | null; service_writer_id?: number; footer?: string; + discount?: string; + discount_amount?: number; + tax_id?: number; label_ids?: number[]; remarks?: string[]; }; @@ -14724,6 +14864,9 @@ export interface paths { * "insurer_id": 1, * "service_writer_id": 1, * "footer": "Updated footer text.", + * "discount": "transaction_level", + * "discount_amount": 15, + * "tax_id": 1, * "label_ids": [ * 1 * ], @@ -14745,6 +14888,9 @@ export interface paths { insurer_id?: number; service_writer_id?: number; footer?: string; + discount?: string; + discount_amount?: number; + tax_id?: number; label_ids?: number[]; remarks?: string[]; }; @@ -16825,6 +16971,8 @@ export interface paths { * "attachments": null, * "tax_inclusive": "Tax Exclusive", * "discount_type": "no", + * "discount_amount": 0, + * "tax_id": 0, * "discount_at": "inclusive_of_tax", * "label_ids": [ * 1 @@ -16875,6 +17023,8 @@ export interface paths { attachments?: string | null; tax_inclusive?: string; discount_type?: string; + discount_amount?: number; + tax_id?: number; discount_at?: string; label_ids?: number[]; documents?: { @@ -17279,6 +17429,8 @@ export interface paths { * "estimate_to": "Customer", * "tax_inclusive": "Tax Exclusive", * "discount_type": "no", + * "discount_amount": 0, + * "tax_id": 0, * "discount_at": "inclusive_of_tax", * "department_id": 1, * "check_in_date": "2026-03-31", @@ -17296,6 +17448,8 @@ export interface paths { estimate_to?: string; tax_inclusive?: string; discount_type?: string; + discount_amount?: number; + tax_id?: number; discount_at?: string; department_id?: number; check_in_date?: string; @@ -17876,6 +18030,140 @@ export interface paths { patch?: never; trace?: never; }; + "/api/job-cards/{id}/convert-to-invoice": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** POST /api/job-cards/{id}/convert-to-invoice */ + post: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "subject": "Work order invoice", + * "invoice_number": "INV-2026-01001", + * "invoice_date": "2026-04-19", + * "due_date": "2026-05-19", + * "invoice_sequence_id": null, + * "payment_terms_id": null, + * "department_id": null, + * "notes": null, + * "terms_and_conditions": null, + * "status": "draft", + * "allow_duplicate": false, + * "discount_amount": 0, + * "tax_id": 0 + * } + */ + "application/json": { + subject?: string; + invoice_number?: string; + invoice_date?: string; + due_date?: string; + invoice_sequence_id?: string | null; + payment_terms_id?: string | null; + department_id?: string | null; + notes?: string | null; + terms_and_conditions?: string | null; + status?: string; + allow_duplicate?: boolean; + discount_amount?: number; + tax_id?: number; + }; + }; + }; + responses: { + /** @description Created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "message": "Job card converted to invoice successfully.", + * "data": { + * "id": 42, + * "subject": "Work order invoice", + * "job_card_id": 1, + * "estimate_id": 1, + * "invoice_number": "INV-2026-01001", + * "invoice_date": "2026-04-19", + * "due_date": "2026-05-19", + * "status": "draft", + * "tax_id": 0, + * "tax_amount": 0, + * "discount_amount_major": 0, + * "sub_total": 1200, + * "total": 1200, + * "amount": 1200 + * } + * } + */ + "application/json": { + message?: string; + data?: { + id?: number; + subject?: string; + job_card_id?: number; + estimate_id?: number; + invoice_number?: string; + invoice_date?: string; + due_date?: string; + status?: string; + tax_id?: number; + tax_amount?: number; + discount_amount_major?: number; + sub_total?: number; + total?: number; + amount?: number; + }; + }; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "message": "An invoice already exists for this job card.", + * "data": { + * "invoice_id": 40 + * } + * } + */ + "application/json": { + message?: string; + data?: { + invoice_id?: number; + }; + }; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/job-cards/{id}/add-customer-remark": { parameters: { query?: never; @@ -18455,7 +18743,6 @@ export interface paths { * "rate": "85.00", * "working_hours": "1.00", * "labor_hours": "1.00", - * "tax": "5%", * "chart_of_account": "COA-200", * "department_id": 1, * "description": "Oil change labor line", @@ -18473,9 +18760,12 @@ export interface paths { * "labor_rate": { * "id": 1, * "title": "Standard", - * "rate": "95.00" + * "rate": "95.00", + * "discount_amount": 0 * }, - * "attachments": [] + * "attachments": [], + * "tax_id": 1, + * "discount_amount": 0 * } * ], * "meta": { @@ -18499,7 +18789,6 @@ export interface paths { rate?: string; working_hours?: string; labor_hours?: string; - tax?: string; chart_of_account?: string; department_id?: number; description?: string; @@ -18520,8 +18809,11 @@ export interface paths { id?: number; title?: string; rate?: string; + discount_amount?: number; }; attachments?: unknown[]; + tax_id?: number; + discount_amount?: number; }[]; meta?: { current_page?: number; @@ -18575,9 +18867,10 @@ export interface paths { * "rate": 85, * "working_hours": 1, * "labor_hours": 1, - * "tax": "5%", * "chart_of_account": "COA-200", - * "description": "Oil change labor line" + * "description": "Oil change labor line", + * "tax_id": 1, + * "discount_amount": 0 * } */ "application/json": { @@ -18589,9 +18882,10 @@ export interface paths { rate?: number; working_hours?: number; labor_hours?: number; - tax?: string; chart_of_account?: string; description?: string; + tax_id?: number; + discount_amount?: number; }; }; }; @@ -18615,7 +18909,6 @@ export interface paths { * "rate": "85.00", * "working_hours": "1.00", * "labor_hours": "1.00", - * "tax": "5%", * "chart_of_account": "COA-200", * "department_id": 1, * "description": "Oil change labor line", @@ -18633,9 +18926,12 @@ export interface paths { * "labor_rate": { * "id": 1, * "title": "Standard", - * "rate": "95.00" + * "rate": "95.00", + * "discount_amount": 0 * }, - * "attachments": [] + * "attachments": [], + * "tax_id": 1, + * "discount_amount": 0 * } * } */ @@ -18651,7 +18947,6 @@ export interface paths { rate?: string; working_hours?: string; labor_hours?: string; - tax?: string; chart_of_account?: string; department_id?: number; description?: string; @@ -18672,8 +18967,11 @@ export interface paths { id?: number; title?: string; rate?: string; + discount_amount?: number; }; attachments?: unknown[]; + tax_id?: number; + discount_amount?: number; }; }; }; @@ -18711,7 +19009,8 @@ export interface paths { * "job_card_service_id": 1, * "quantity": 2, * "rate": 90, - * "description": "Updated description" + * "description": "Updated description", + * "discount_amount": 0 * } */ "application/json": { @@ -18719,6 +19018,7 @@ export interface paths { quantity?: number; rate?: number; description?: string; + discount_amount?: number; }; }; }; @@ -18742,7 +19042,6 @@ export interface paths { * "rate": "90.00", * "working_hours": "1.00", * "labor_hours": "1.00", - * "tax": "5%", * "chart_of_account": "COA-200", * "department_id": 1, * "description": "Updated description", @@ -18760,9 +19059,12 @@ export interface paths { * "labor_rate": { * "id": 1, * "title": "Standard", - * "rate": "95.00" + * "rate": "95.00", + * "discount_amount": 0 * }, - * "attachments": [] + * "attachments": [], + * "tax_id": 1, + * "discount_amount": 0 * } * } */ @@ -18778,7 +19080,6 @@ export interface paths { rate?: string; working_hours?: string; labor_hours?: string; - tax?: string; chart_of_account?: string; department_id?: number; description?: string; @@ -18799,8 +19100,11 @@ export interface paths { id?: number; title?: string; rate?: string; + discount_amount?: number; }; attachments?: unknown[]; + tax_id?: number; + discount_amount?: number; }; }; }; @@ -19116,7 +19420,6 @@ export interface paths { * "part_id": 1, * "quantity": 2, * "rate": "45.00", - * "tax": "5%", * "chart_of_account": "COA-300", * "department_id": 1, * "description": "Brake pads", @@ -19131,7 +19434,9 @@ export interface paths { * "id": 1, * "name": "Parts" * }, - * "attachments": [] + * "attachments": [], + * "tax_id": 1, + * "discount_amount": 0 * } * ], * "meta": { @@ -19151,7 +19456,6 @@ export interface paths { part_id?: number; quantity?: number; rate?: string; - tax?: string; chart_of_account?: string; department_id?: number; description?: string; @@ -19169,6 +19473,8 @@ export interface paths { name?: string; }; attachments?: unknown[]; + tax_id?: number; + discount_amount?: number; }[]; meta?: { current_page?: number; @@ -19218,9 +19524,10 @@ export interface paths { * "department_id": 1, * "quantity": 2, * "rate": 45, - * "tax": "5%", * "chart_of_account": "COA-300", - * "description": "Brake pads" + * "description": "Brake pads", + * "tax_id": 1, + * "discount_amount": 0 * } */ "application/json": { @@ -19228,9 +19535,10 @@ export interface paths { department_id?: number; quantity?: number; rate?: number; - tax?: string; chart_of_account?: string; description?: string; + tax_id?: number; + discount_amount?: number; }; }; }; @@ -19250,7 +19558,6 @@ export interface paths { * "part_id": 1, * "quantity": 2, * "rate": "45.00", - * "tax": "5%", * "chart_of_account": "COA-300", * "department_id": 1, * "description": "Brake pads", @@ -19265,7 +19572,9 @@ export interface paths { * "id": 1, * "name": "Parts" * }, - * "attachments": [] + * "attachments": [], + * "tax_id": 1, + * "discount_amount": 0 * } * } */ @@ -19277,7 +19586,6 @@ export interface paths { part_id?: number; quantity?: number; rate?: string; - tax?: string; chart_of_account?: string; department_id?: number; description?: string; @@ -19295,6 +19603,8 @@ export interface paths { name?: string; }; attachments?: unknown[]; + tax_id?: number; + discount_amount?: number; }; }; }; @@ -19332,7 +19642,8 @@ export interface paths { * "job_card_part_id": 1, * "quantity": 3, * "rate": 48.5, - * "description": "Updated qty" + * "description": "Updated qty", + * "discount_amount": 0 * } */ "application/json": { @@ -19340,6 +19651,7 @@ export interface paths { quantity?: number; rate?: number; description?: string; + discount_amount?: number; }; }; }; @@ -19359,7 +19671,6 @@ export interface paths { * "part_id": 1, * "quantity": 3, * "rate": "48.50", - * "tax": "5%", * "chart_of_account": "COA-300", * "department_id": 1, * "description": "Updated qty", @@ -19374,7 +19685,9 @@ export interface paths { * "id": 1, * "name": "Parts" * }, - * "attachments": [] + * "attachments": [], + * "tax_id": 1, + * "discount_amount": 0 * } * } */ @@ -19386,7 +19699,6 @@ export interface paths { part_id?: number; quantity?: number; rate?: string; - tax?: string; chart_of_account?: string; department_id?: number; description?: string; @@ -19404,6 +19716,8 @@ export interface paths { name?: string; }; attachments?: unknown[]; + tax_id?: number; + discount_amount?: number; }; }; }; @@ -19719,7 +20033,6 @@ export interface paths { * "expense_item_id": 1, * "quantity": 1, * "rate": "120.00", - * "tax": "5%", * "chart_of_account": "COA-400", * "department_id": 1, * "description": "Shop supplies", @@ -19734,7 +20047,9 @@ export interface paths { * "id": 1, * "name": "Admin" * }, - * "attachments": [] + * "attachments": [], + * "tax_id": 1, + * "discount_amount": 0 * } * ], * "meta": { @@ -19754,7 +20069,6 @@ export interface paths { expense_item_id?: number; quantity?: number; rate?: string; - tax?: string; chart_of_account?: string; department_id?: number; description?: string; @@ -19772,6 +20086,8 @@ export interface paths { name?: string; }; attachments?: unknown[]; + tax_id?: number; + discount_amount?: number; }[]; meta?: { current_page?: number; @@ -19821,9 +20137,10 @@ export interface paths { * "department_id": 1, * "quantity": 1, * "rate": 120, - * "tax": "5%", * "chart_of_account": "COA-400", - * "description": "Shop supplies" + * "description": "Shop supplies", + * "tax_id": 1, + * "discount_amount": 0 * } */ "application/json": { @@ -19831,9 +20148,10 @@ export interface paths { department_id?: number; quantity?: number; rate?: number; - tax?: string; chart_of_account?: string; description?: string; + tax_id?: number; + discount_amount?: number; }; }; }; @@ -19853,7 +20171,6 @@ export interface paths { * "expense_item_id": 1, * "quantity": 1, * "rate": "120.00", - * "tax": "5%", * "chart_of_account": "COA-400", * "department_id": 1, * "description": "Shop supplies", @@ -19868,7 +20185,9 @@ export interface paths { * "id": 1, * "name": "Admin" * }, - * "attachments": [] + * "attachments": [], + * "tax_id": 1, + * "discount_amount": 0 * } * } */ @@ -19880,7 +20199,6 @@ export interface paths { expense_item_id?: number; quantity?: number; rate?: string; - tax?: string; chart_of_account?: string; department_id?: number; description?: string; @@ -19898,6 +20216,8 @@ export interface paths { name?: string; }; attachments?: unknown[]; + tax_id?: number; + discount_amount?: number; }; }; }; @@ -19935,7 +20255,8 @@ export interface paths { * "job_card_expense_item_id": 1, * "quantity": 2, * "rate": 125.5, - * "description": "Updated line" + * "description": "Updated line", + * "discount_amount": 0 * } */ "application/json": { @@ -19943,6 +20264,7 @@ export interface paths { quantity?: number; rate?: number; description?: string; + discount_amount?: number; }; }; }; @@ -19962,7 +20284,6 @@ export interface paths { * "expense_item_id": 1, * "quantity": 2, * "rate": "125.50", - * "tax": "5%", * "chart_of_account": "COA-400", * "department_id": 1, * "description": "Updated line", @@ -19977,7 +20298,9 @@ export interface paths { * "id": 1, * "name": "Admin" * }, - * "attachments": [] + * "attachments": [], + * "tax_id": 1, + * "discount_amount": 0 * } * } */ @@ -19989,7 +20312,6 @@ export interface paths { expense_item_id?: number; quantity?: number; rate?: string; - tax?: string; chart_of_account?: string; department_id?: number; description?: string; @@ -20007,6 +20329,8 @@ export interface paths { name?: string; }; attachments?: unknown[]; + tax_id?: number; + discount_amount?: number; }; }; }; @@ -21135,29 +21459,39 @@ export interface paths { * { * "id": 1, * "job_card_id": 1, + * "invoice_id": 1, * "payment_mode_id": 1, * "customer_id": 1, * "amount_received": 1000, * "payment_number": "RCPT-001", * "payment_date": "2026-03-31", * "reference_date": "2026-03-31", - * "deposit_to": "string", - * "note": "string", + * "deposit_to": "Cash on hand", + * "note": "Payment received at front desk", * "created_at": "2026-03-31T10:00:00.000000Z", - * "updated_at": "2026-03-31T10:00:00.000000Z" + * "updated_at": "2026-03-31T10:00:00.000000Z", + * "job_card": { + * "id": 1 + * }, + * "invoice": { + * "id": 1 + * }, + * "payment_mode": { + * "id": 1 + * }, + * "customer": { + * "id": 1 + * }, + * "attachments": [] * } * ], * "meta": { * "current_page": 1, * "last_page": 5, * "per_page": 15, - * "total": 75 - * }, - * "links": { - * "first": "https://api.example.com/resource?page=1", - * "last": "https://api.example.com/resource?page=5", - * "prev": "string", - * "next": "https://api.example.com/resource?page=2" + * "total": 75, + * "from": 1, + * "to": 15 * } * } */ @@ -21165,6 +21499,7 @@ export interface paths { data?: { id?: number; job_card_id?: number; + invoice_id?: number; payment_mode_id?: number; customer_id?: number; amount_received?: number; @@ -21177,18 +21512,27 @@ export interface paths { created_at?: string; /** Format: date-time */ updated_at?: string; + job_card?: { + id?: number; + }; + invoice?: { + id?: number; + }; + payment_mode?: { + id?: number; + }; + customer?: { + id?: number; + }; + attachments?: unknown[]; }[]; meta?: { current_page?: number; last_page?: number; per_page?: number; total?: number; - }; - links?: { - first?: string; - last?: string; - prev?: string; - next?: string; + from?: number; + to?: number; }; }; }; @@ -21204,51 +21548,65 @@ export interface paths { path?: never; cookie?: never; }; - requestBody: { + requestBody?: { content: { - /** - * @example { - * "job_card_id": 1, - * "payment_mode_id": 1, - * "customer_id": 1, - * "amount_received": 1000, - * "payment_date": "2026-03-31", - * "note": "string" - * } - */ - "application/json": { - job_card_id?: number; - payment_mode_id?: number; - customer_id?: number; - amount_received?: number; + "multipart/form-data": { + job_card_id?: string; + payment_mode_id?: string; + customer_id?: string; + amount_received?: string; + invoice_id?: string; + payment_number?: string; payment_date?: string; + reference_date?: string; + deposit_to?: string; note?: string; + "bill_payments[0][bill_id]"?: string; + "bill_payments[0][amount]"?: string; + "bill_payments[1][bill_id]"?: string; + "bill_payments[1][amount]"?: string; + /** Format: binary */ + "attachment_files[]"?: string; }; }; }; responses: { - /** @description OK */ - 200: { + /** @description Created */ + 201: { headers: { [name: string]: unknown; }; content: { /** * @example { - * "message": "Operation completed successfully.", + * "message": "Payment received created successfully.", * "data": { * "id": 1, * "job_card_id": 1, + * "invoice_id": 1, * "payment_mode_id": 1, * "customer_id": 1, * "amount_received": 1000, * "payment_number": "RCPT-001", * "payment_date": "2026-03-31", * "reference_date": "2026-03-31", - * "deposit_to": "string", - * "note": "string", + * "deposit_to": "Cash on hand", + * "note": "Payment received at front desk", * "created_at": "2026-03-31T10:00:00.000000Z", - * "updated_at": "2026-03-31T10:00:00.000000Z" + * "updated_at": "2026-03-31T10:00:00.000000Z", + * "job_card": { + * "id": 1 + * }, + * "invoice": { + * "id": 1 + * }, + * "payment_mode": { + * "id": 1 + * }, + * "customer": { + * "id": 1 + * }, + * "attachments": [] * } * } */ @@ -21257,6 +21615,7 @@ export interface paths { data?: { id?: number; job_card_id?: number; + invoice_id?: number; payment_mode_id?: number; customer_id?: number; amount_received?: number; @@ -21269,6 +21628,19 @@ export interface paths { created_at?: string; /** Format: date-time */ updated_at?: string; + job_card?: { + id?: number; + }; + invoice?: { + id?: number; + }; + payment_mode?: { + id?: number; + }; + customer?: { + id?: number; + }; + attachments?: unknown[]; }; }; }; @@ -21299,25 +21671,22 @@ export interface paths { }; cookie?: never; }; - requestBody: { + requestBody?: { content: { - /** - * @example { - * "job_card_id": 1, - * "payment_mode_id": 1, - * "customer_id": 1, - * "amount_received": 1000, - * "payment_date": "2026-03-31", - * "note": "string" - * } - */ - "application/json": { - job_card_id?: number; - payment_mode_id?: number; - customer_id?: number; - amount_received?: number; + "multipart/form-data": { + job_card_id?: string; + payment_mode_id?: string; + customer_id?: string; + amount_received?: string; + invoice_id?: string; + payment_number?: string; payment_date?: string; + reference_date?: string; + deposit_to?: string; note?: string; + "delete_attachment_ids[]"?: string; + /** Format: binary */ + "attachment_files[]"?: string; }; }; }; @@ -21330,20 +21699,34 @@ export interface paths { content: { /** * @example { - * "message": "Operation completed successfully.", + * "message": "Payment received updated successfully.", * "data": { * "id": 1, * "job_card_id": 1, + * "invoice_id": 1, * "payment_mode_id": 1, * "customer_id": 1, * "amount_received": 1000, * "payment_number": "RCPT-001", * "payment_date": "2026-03-31", * "reference_date": "2026-03-31", - * "deposit_to": "string", - * "note": "string", + * "deposit_to": "Cash on hand", + * "note": "Updated payment note", * "created_at": "2026-03-31T10:00:00.000000Z", - * "updated_at": "2026-03-31T10:00:00.000000Z" + * "updated_at": "2026-03-31T10:05:00.000000Z", + * "job_card": { + * "id": 1 + * }, + * "invoice": { + * "id": 1 + * }, + * "payment_mode": { + * "id": 1 + * }, + * "customer": { + * "id": 1 + * }, + * "attachments": [] * } * } */ @@ -21352,6 +21735,7 @@ export interface paths { data?: { id?: number; job_card_id?: number; + invoice_id?: number; payment_mode_id?: number; customer_id?: number; amount_received?: number; @@ -21364,6 +21748,19 @@ export interface paths { created_at?: string; /** Format: date-time */ updated_at?: string; + job_card?: { + id?: number; + }; + invoice?: { + id?: number; + }; + payment_mode?: { + id?: number; + }; + customer?: { + id?: number; + }; + attachments?: unknown[]; }; }; }; @@ -22344,11 +22741,16 @@ export interface paths { * "department_id": 1, * "notes": "string", * "terms_and_conditions": "Net 30", + * "discount_type": "no", + * "discount_amount": 0, + * "tax_id": 0, * "items": [ * { * "part_id": 1, * "quantity": 2, * "rate": 45.5, + * "tax_id": 0, + * "discount_amount": 0, * "description": "Oil filter" * } * ] @@ -22364,10 +22766,15 @@ export interface paths { department_id?: number; notes?: string; terms_and_conditions?: string; + discount_type?: string; + discount_amount?: number; + tax_id?: number; items?: { part_id?: number; quantity?: number; rate?: number; + tax_id?: number; + discount_amount?: number; description?: string; }[]; }; @@ -22485,17 +22892,24 @@ export interface paths { * "title": "Purchase Order for Parts (updated)", * "order_number": "PO-001", * "delivery_date": "2026-04-08", + * "discount_type": "line_item_level", + * "discount_amount": 0, + * "tax_id": 1, * "items": [ * { * "part_id": 1, * "quantity": 3, * "rate": 45.5, + * "tax_id": 1, + * "discount_amount": 2.5, * "description": "Oil filter - updated qty" * }, * { * "part_id": 2, * "quantity": 1, * "rate": 120, + * "tax_id": 0, + * "discount_amount": 0, * "description": "Air filter" * } * ] @@ -22505,10 +22919,15 @@ export interface paths { title?: string; order_number?: string; delivery_date?: string; + discount_type?: string; + discount_amount?: number; + tax_id?: number; items?: { part_id?: number; quantity?: number; rate?: number; + tax_id?: number; + discount_amount?: number; description?: string; }[]; }; @@ -24115,6 +24534,44 @@ export interface paths { * "department_id": 1, * "notes": "Vendor invoice for parts, service and expense lines", * "status": "open", + * "job_card": { + * "id": 1, + * "order_number": "ORD-0001", + * "estimate_number": "EST-0001" + * }, + * "vendor": { + * "id": 1, + * "name": "Auto Parts Supplier" + * }, + * "vendor_address": { + * "id": 1, + * "vendor_id": 1, + * "address": "123 Industrial Road", + * "country": { + * "id": 1, + * "name": "United Arab Emirates" + * }, + * "state": { + * "id": 1, + * "name": "Dubai" + * } + * }, + * "department": { + * "id": 1, + * "name": "Service Department" + * }, + * "tax": { + * "id": 1, + * "name": "VAT 5%", + * "rate": "5.00" + * }, + * "labels": [ + * { + * "id": 1, + * "title": "Urgent", + * "color_code": "#FF6600" + * } + * ], * "services": [ * { * "id": 1, @@ -24123,7 +24580,12 @@ export interface paths { * "quantity": "2.00", * "rate": "150.00", * "chart_of_account": "COA-401", - * "description": "Labor service line" + * "description": "Labor service line", + * "service": { + * "id": 1, + * "name": "Brake Service", + * "price": "150.00" + * } * } * ], * "parts": [ @@ -24134,7 +24596,12 @@ export interface paths { * "quantity": 3, * "rate": "40.00", * "chart_of_account": 1201, - * "description": "Brake pad set" + * "description": "Brake pad set", + * "part": { + * "id": 1, + * "name": "Brake Pad Set", + * "part_number": "PART-001" + * } * } * ], * "expenses": [ @@ -24145,7 +24612,12 @@ export interface paths { * "quantity": "1.00", * "rate": "75.00", * "chart_of_account": "COA-402", - * "description": "Consumables expense line" + * "description": "Consumables expense line", + * "expense": { + * "id": 1, + * "title": "Consumables", + * "invoice_number": "EXP-001" + * } * } * ], * "created_at": "2026-03-31T10:00:00.000000Z", @@ -24181,6 +24653,42 @@ export interface paths { department_id?: number; notes?: string; status?: string; + job_card?: { + id?: number; + order_number?: string; + estimate_number?: string; + }; + vendor?: { + id?: number; + name?: string; + }; + vendor_address?: { + id?: number; + vendor_id?: number; + address?: string; + country?: { + id?: number; + name?: string; + }; + state?: { + id?: number; + name?: string; + }; + }; + department?: { + id?: number; + name?: string; + }; + tax?: { + id?: number; + name?: string; + rate?: string; + }; + labels?: { + id?: number; + title?: string; + color_code?: string; + }[]; services?: { id?: number; bill_id?: number; @@ -24189,6 +24697,11 @@ export interface paths { rate?: string; chart_of_account?: string; description?: string; + service?: { + id?: number; + name?: string; + price?: string; + }; }[]; parts?: { id?: number; @@ -24198,6 +24711,11 @@ export interface paths { rate?: string; chart_of_account?: number; description?: string; + part?: { + id?: number; + name?: string; + part_number?: string; + }; }[]; expenses?: { id?: number; @@ -24207,6 +24725,11 @@ export interface paths { rate?: string; chart_of_account?: string; description?: string; + expense?: { + id?: number; + title?: string; + invoice_number?: string; + }; }[]; /** Format: date-time */ created_at?: string; @@ -24253,6 +24776,10 @@ export interface paths { * "bill_due_date": "2026-04-14", * "payment_terms_id": 1, * "department_id": 1, + * "discount_type": "line_item_level", + * "discount_amount": 0, + * "tax_id": 1, + * "status": "open", * "notes": "Vendor invoice for parts, service and expense lines", * "label_ids": [ * 1 @@ -24263,7 +24790,8 @@ export interface paths { * "quantity": 3, * "rate": 40, * "chart_of_account": 1201, - * "description": "Brake pad set" + * "description": "Brake pad set", + * "discount_amount": 5 * } * ], * "service_items": [ @@ -24272,7 +24800,8 @@ export interface paths { * "quantity": 2, * "rate": 150, * "chart_of_account": "COA-401", - * "description": "Labor service line" + * "description": "Labor service line", + * "discount_amount": 12.5 * } * ], * "expense_items": [ @@ -24281,7 +24810,8 @@ export interface paths { * "quantity": 1, * "rate": 75, * "chart_of_account": "COA-402", - * "description": "Consumables expense line" + * "description": "Consumables expense line", + * "discount_amount": 3.75 * } * ] * } @@ -24297,6 +24827,10 @@ export interface paths { bill_due_date?: string; payment_terms_id?: number; department_id?: number; + discount_type?: string; + discount_amount?: number; + tax_id?: number; + status?: string; notes?: string; label_ids?: number[]; part_items?: { @@ -24305,6 +24839,7 @@ export interface paths { rate?: number; chart_of_account?: number; description?: string; + discount_amount?: number; }[]; service_items?: { service_id?: number; @@ -24312,6 +24847,7 @@ export interface paths { rate?: number; chart_of_account?: string; description?: string; + discount_amount?: number; }[]; expense_items?: { expense_id?: number; @@ -24319,6 +24855,7 @@ export interface paths { rate?: number; chart_of_account?: string; description?: string; + discount_amount?: number; }[]; }; }; @@ -24345,6 +24882,7 @@ export interface paths { * "bill_due_date": "2026-04-14", * "payment_terms_id": 1, * "department_id": 1, + * "discount_type": "transaction", * "notes": "Vendor invoice for parts, service and expense lines", * "status": "open", * "services": [ @@ -24399,6 +24937,7 @@ export interface paths { bill_due_date?: string; payment_terms_id?: number; department_id?: number; + discount_type?: string; notes?: string; status?: string; services?: { @@ -24451,7 +24990,234 @@ export interface paths { path?: never; cookie?: never; }; - get?: never; + /** Display the specified bill. */ + 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": "Workshop Bill", + * "job_card_id": 1, + * "vendor_id": 1, + * "vendor_address_id": 1, + * "purchase_order_id": 1, + * "bill_number": "BILL-001", + * "bill_date": "2026-03-31", + * "bill_due_date": "2026-04-14", + * "payment_terms_id": 1, + * "department_id": 1, + * "notes": "Vendor invoice for parts, service and expense lines", + * "status": "open", + * "job_card": { + * "id": 1, + * "order_number": "ORD-0001", + * "estimate_number": "EST-0001" + * }, + * "vendor": { + * "id": 1, + * "name": "Auto Parts Supplier" + * }, + * "vendor_address": { + * "id": 1, + * "vendor_id": 1, + * "address": "123 Industrial Road", + * "country": { + * "id": 1, + * "name": "United Arab Emirates" + * }, + * "state": { + * "id": 1, + * "name": "Dubai" + * } + * }, + * "department": { + * "id": 1, + * "name": "Service Department" + * }, + * "tax": { + * "id": 1, + * "name": "VAT 5%", + * "rate": "5.00" + * }, + * "labels": [ + * { + * "id": 1, + * "title": "Urgent", + * "color_code": "#FF6600" + * } + * ], + * "services": [ + * { + * "id": 1, + * "bill_id": 1, + * "service_id": 1, + * "quantity": "2.00", + * "rate": "150.00", + * "chart_of_account": "COA-401", + * "description": "Labor service line", + * "service": { + * "id": 1, + * "name": "Brake Service", + * "price": "150.00" + * } + * } + * ], + * "parts": [ + * { + * "id": 1, + * "bill_id": 1, + * "part_id": 1, + * "quantity": 3, + * "rate": "40.00", + * "chart_of_account": 1201, + * "description": "Brake pad set", + * "part": { + * "id": 1, + * "name": "Brake Pad Set", + * "part_number": "PART-001" + * } + * } + * ], + * "expenses": [ + * { + * "id": 1, + * "bill_id": 1, + * "expense_id": 1, + * "quantity": "1.00", + * "rate": "75.00", + * "chart_of_account": "COA-402", + * "description": "Consumables expense line", + * "expense": { + * "id": 1, + * "title": "Consumables", + * "invoice_number": "EXP-001" + * } + * } + * ], + * "created_at": "2026-03-31T10:00:00.000000Z", + * "updated_at": "2026-03-31T10:00:00.000000Z" + * } + * } + */ + "application/json": { + data?: { + id?: number; + title?: string; + job_card_id?: number; + vendor_id?: number; + vendor_address_id?: number; + purchase_order_id?: number; + bill_number?: string; + bill_date?: string; + bill_due_date?: string; + payment_terms_id?: number; + department_id?: number; + notes?: string; + status?: string; + job_card?: { + id?: number; + order_number?: string; + estimate_number?: string; + }; + vendor?: { + id?: number; + name?: string; + }; + vendor_address?: { + id?: number; + vendor_id?: number; + address?: string; + country?: { + id?: number; + name?: string; + }; + state?: { + id?: number; + name?: string; + }; + }; + department?: { + id?: number; + name?: string; + }; + tax?: { + id?: number; + name?: string; + rate?: string; + }; + labels?: { + id?: number; + title?: string; + color_code?: string; + }[]; + services?: { + id?: number; + bill_id?: number; + service_id?: number; + quantity?: string; + rate?: string; + chart_of_account?: string; + description?: string; + service?: { + id?: number; + name?: string; + price?: string; + }; + }[]; + parts?: { + id?: number; + bill_id?: number; + part_id?: number; + quantity?: number; + rate?: string; + chart_of_account?: number; + description?: string; + part?: { + id?: number; + name?: string; + part_number?: string; + }; + }[]; + expenses?: { + id?: number; + bill_id?: number; + expense_id?: number; + quantity?: string; + rate?: string; + chart_of_account?: string; + description?: string; + expense?: { + id?: number; + title?: string; + invoice_number?: string; + }; + }[]; + /** Format: date-time */ + created_at?: string; + /** Format: date-time */ + updated_at?: string; + }; + }; + }; + }; + }; + }; /** Update the specified bill. */ put: { parameters: { @@ -24476,6 +25242,10 @@ export interface paths { * "bill_due_date": "2026-04-20", * "payment_terms_id": 1, * "department_id": 1, + * "discount_type": "line_item_level", + * "discount_amount": 0, + * "tax_id": 1, + * "status": "open", * "notes": "Updated vendor invoice lines", * "label_ids": [ * 1 @@ -24486,7 +25256,8 @@ export interface paths { * "quantity": 1, * "rate": 45, * "chart_of_account": 1201, - * "description": "Updated part line" + * "description": "Updated part line", + * "discount_amount": 2 * } * ], * "service_items": [ @@ -24495,7 +25266,8 @@ export interface paths { * "quantity": 1, * "rate": 200, * "chart_of_account": "COA-401", - * "description": "Updated labor service line" + * "description": "Updated labor service line", + * "discount_amount": 15 * } * ], * "expense_items": [ @@ -24504,7 +25276,8 @@ export interface paths { * "quantity": 2, * "rate": 50, * "chart_of_account": "COA-402", - * "description": "Updated consumables expense line" + * "description": "Updated consumables expense line", + * "discount_amount": 4 * } * ] * } @@ -24520,6 +25293,10 @@ export interface paths { bill_due_date?: string; payment_terms_id?: number; department_id?: number; + discount_type?: string; + discount_amount?: number; + tax_id?: number; + status?: string; notes?: string; label_ids?: number[]; part_items?: { @@ -24528,6 +25305,7 @@ export interface paths { rate?: number; chart_of_account?: number; description?: string; + discount_amount?: number; }[]; service_items?: { service_id?: number; @@ -24535,6 +25313,7 @@ export interface paths { rate?: number; chart_of_account?: string; description?: string; + discount_amount?: number; }[]; expense_items?: { expense_id?: number; @@ -24542,6 +25321,7 @@ export interface paths { rate?: number; chart_of_account?: string; description?: string; + discount_amount?: number; }[]; }; }; @@ -25126,10 +25906,62 @@ export interface paths { * "vendor_id": 1, * "invoice_number": "INV-001", * "expense_date": "2026-03-31", - * "paid_through": "string", + * "paid_through": 1001, * "department_id": 1, - * "notes": "string", + * "tax_id": 1, + * "notes": "Shop supplies purchase", * "status": "draft", + * "discount_amount_major": 10, + * "sub_total": 240, + * "tax_amount": 12, + * "total": 242, + * "payments_made": 120, + * "balance_due": 122, + * "job_card": { + * "id": 1, + * "order_number": "ORD-0001", + * "estimate_number": "EST-0001" + * }, + * "category": { + * "id": 1, + * "name": "Consumables" + * }, + * "vendor": { + * "id": 1, + * "name": "Auto Parts Supplier" + * }, + * "department": { + * "id": 1, + * "name": "Service Department" + * }, + * "tax": { + * "id": 1, + * "name": "VAT 5%", + * "rate": "5.00" + * }, + * "labels": [ + * { + * "id": 1, + * "title": "Urgent", + * "color_code": "#FF6600" + * } + * ], + * "expense_items": [ + * { + * "id": 1, + * "expense_id": 1, + * "expense_item_id": 1, + * "quantity": 2, + * "rate": "120.00", + * "chart_of_account": 1201, + * "description": "Cleaning materials", + * "expense_item": { + * "id": 1, + * "name": "Workshop Consumable", + * "unit_price": "120.00" + * } + * } + * ], * "created_at": "2026-03-31T10:00:00.000000Z", * "updated_at": "2026-03-31T10:00:00.000000Z" * } @@ -25138,13 +25970,9 @@ export interface paths { * "current_page": 1, * "last_page": 5, * "per_page": 15, - * "total": 75 - * }, - * "links": { - * "first": "https://api.example.com/resource?page=1", - * "last": "https://api.example.com/resource?page=5", - * "prev": "string", - * "next": "https://api.example.com/resource?page=2" + * "total": 75, + * "from": 1, + * "to": 15 * } * } */ @@ -25157,10 +25985,58 @@ export interface paths { vendor_id?: number; invoice_number?: string; expense_date?: string; - paid_through?: string; + paid_through?: number; department_id?: number; + tax_id?: number; notes?: string; status?: string; + discount_amount_major?: number; + sub_total?: number; + tax_amount?: number; + total?: number; + payments_made?: number; + balance_due?: number; + job_card?: { + id?: number; + order_number?: string; + estimate_number?: string; + }; + category?: { + id?: number; + name?: string; + }; + vendor?: { + id?: number; + name?: string; + }; + department?: { + id?: number; + name?: string; + }; + tax?: { + id?: number; + name?: string; + rate?: string; + }; + labels?: { + id?: number; + title?: string; + color_code?: string; + }[]; + expense_items?: { + id?: number; + expense_id?: number; + expense_item_id?: number; + quantity?: number; + rate?: string; + chart_of_account?: number; + description?: string; + expense_item?: { + id?: number; + name?: string; + unit_price?: string; + }; + }[]; /** Format: date-time */ created_at?: string; /** Format: date-time */ @@ -25171,12 +26047,8 @@ export interface paths { last_page?: number; per_page?: number; total?: number; - }; - links?: { - first?: string; - last?: string; - prev?: string; - next?: string; + from?: number; + to?: number; }; }; }; @@ -25200,9 +26072,26 @@ export interface paths { * "title": "Workshop Supplies", * "category_id": 1, * "vendor_id": 1, + * "invoice_number": "INV-001", * "expense_date": "2026-03-31", + * "paid_through": 1001, * "department_id": 1, - * "status": "draft" + * "tax_id": 1, + * "discount_amount": 10, + * "notes": "Shop supplies purchase", + * "status": "draft", + * "label_ids": [ + * 1 + * ], + * "items": [ + * { + * "expense_item_id": 1, + * "quantity": 2, + * "rate": 120, + * "chart_of_account": 1201, + * "description": "Cleaning materials" + * } + * ] * } */ "application/json": { @@ -25210,22 +26099,35 @@ export interface paths { title?: string; category_id?: number; vendor_id?: number; + invoice_number?: string; expense_date?: string; + paid_through?: number; department_id?: number; + tax_id?: number; + discount_amount?: number; + notes?: string; status?: string; + label_ids?: number[]; + items?: { + expense_item_id?: number; + quantity?: number; + rate?: number; + chart_of_account?: number; + description?: string; + }[]; }; }; }; responses: { - /** @description OK */ - 200: { + /** @description Created */ + 201: { headers: { [name: string]: unknown; }; content: { /** * @example { - * "message": "Operation completed successfully.", + * "message": "Expense created successfully.", * "data": { * "id": 1, * "job_card_id": 1, @@ -25234,10 +26136,62 @@ export interface paths { * "vendor_id": 1, * "invoice_number": "INV-001", * "expense_date": "2026-03-31", - * "paid_through": "string", + * "paid_through": 1001, * "department_id": 1, - * "notes": "string", + * "tax_id": 1, + * "notes": "Shop supplies purchase", * "status": "draft", + * "discount_amount_major": 10, + * "sub_total": 240, + * "tax_amount": 12, + * "total": 242, + * "payments_made": 120, + * "balance_due": 122, + * "job_card": { + * "id": 1, + * "order_number": "ORD-0001", + * "estimate_number": "EST-0001" + * }, + * "category": { + * "id": 1, + * "name": "Consumables" + * }, + * "vendor": { + * "id": 1, + * "name": "Auto Parts Supplier" + * }, + * "department": { + * "id": 1, + * "name": "Service Department" + * }, + * "tax": { + * "id": 1, + * "name": "VAT 5%", + * "rate": "5.00" + * }, + * "labels": [ + * { + * "id": 1, + * "title": "Urgent", + * "color_code": "#FF6600" + * } + * ], + * "expense_items": [ + * { + * "id": 1, + * "expense_id": 1, + * "expense_item_id": 1, + * "quantity": 2, + * "rate": "120.00", + * "chart_of_account": 1201, + * "description": "Cleaning materials", + * "expense_item": { + * "id": 1, + * "name": "Workshop Consumable", + * "unit_price": "120.00" + * } + * } + * ], * "created_at": "2026-03-31T10:00:00.000000Z", * "updated_at": "2026-03-31T10:00:00.000000Z" * } @@ -25253,10 +26207,58 @@ export interface paths { vendor_id?: number; invoice_number?: string; expense_date?: string; - paid_through?: string; + paid_through?: number; department_id?: number; + tax_id?: number; notes?: string; status?: string; + discount_amount_major?: number; + sub_total?: number; + tax_amount?: number; + total?: number; + payments_made?: number; + balance_due?: number; + job_card?: { + id?: number; + order_number?: string; + estimate_number?: string; + }; + category?: { + id?: number; + name?: string; + }; + vendor?: { + id?: number; + name?: string; + }; + department?: { + id?: number; + name?: string; + }; + tax?: { + id?: number; + name?: string; + rate?: string; + }; + labels?: { + id?: number; + title?: string; + color_code?: string; + }[]; + expense_items?: { + id?: number; + expense_id?: number; + expense_item_id?: number; + quantity?: number; + rate?: string; + chart_of_account?: number; + description?: string; + expense_item?: { + id?: number; + name?: string; + unit_price?: string; + }; + }[]; /** Format: date-time */ created_at?: string; /** Format: date-time */ @@ -25280,7 +26282,166 @@ export interface paths { path?: never; cookie?: never; }; - get?: never; + /** Display the specified expense. */ + 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, + * "job_card_id": 1, + * "title": "Workshop Supplies", + * "category_id": 1, + * "vendor_id": 1, + * "invoice_number": "INV-001", + * "expense_date": "2026-03-31", + * "paid_through": 1001, + * "department_id": 1, + * "tax_id": 1, + * "notes": "Shop supplies purchase", + * "status": "draft", + * "discount_amount_major": 10, + * "sub_total": 240, + * "tax_amount": 12, + * "total": 242, + * "payments_made": 120, + * "balance_due": 122, + * "job_card": { + * "id": 1, + * "order_number": "ORD-0001", + * "estimate_number": "EST-0001" + * }, + * "category": { + * "id": 1, + * "name": "Consumables" + * }, + * "vendor": { + * "id": 1, + * "name": "Auto Parts Supplier" + * }, + * "department": { + * "id": 1, + * "name": "Service Department" + * }, + * "tax": { + * "id": 1, + * "name": "VAT 5%", + * "rate": "5.00" + * }, + * "labels": [ + * { + * "id": 1, + * "title": "Urgent", + * "color_code": "#FF6600" + * } + * ], + * "expense_items": [ + * { + * "id": 1, + * "expense_id": 1, + * "expense_item_id": 1, + * "quantity": 2, + * "rate": "120.00", + * "chart_of_account": 1201, + * "description": "Cleaning materials", + * "expense_item": { + * "id": 1, + * "name": "Workshop Consumable", + * "unit_price": "120.00" + * } + * } + * ], + * "created_at": "2026-03-31T10:00:00.000000Z", + * "updated_at": "2026-03-31T10:00:00.000000Z" + * } + * } + */ + "application/json": { + data?: { + id?: number; + job_card_id?: number; + title?: string; + category_id?: number; + vendor_id?: number; + invoice_number?: string; + expense_date?: string; + paid_through?: number; + department_id?: number; + tax_id?: number; + notes?: string; + status?: string; + discount_amount_major?: number; + sub_total?: number; + tax_amount?: number; + total?: number; + payments_made?: number; + balance_due?: number; + job_card?: { + id?: number; + order_number?: string; + estimate_number?: string; + }; + category?: { + id?: number; + name?: string; + }; + vendor?: { + id?: number; + name?: string; + }; + department?: { + id?: number; + name?: string; + }; + tax?: { + id?: number; + name?: string; + rate?: string; + }; + labels?: { + id?: number; + title?: string; + color_code?: string; + }[]; + expense_items?: { + id?: number; + expense_id?: number; + expense_item_id?: number; + quantity?: number; + rate?: string; + chart_of_account?: number; + description?: string; + expense_item?: { + id?: number; + name?: string; + unit_price?: string; + }; + }[]; + /** Format: date-time */ + created_at?: string; + /** Format: date-time */ + updated_at?: string; + }; + }; + }; + }; + }; + }; /** Update the specified expense. */ put: { parameters: { @@ -25296,12 +26457,29 @@ export interface paths { /** * @example { * "job_card_id": 1, - * "title": "Workshop Supplies", + * "title": "Workshop Supplies Updated", * "category_id": 1, * "vendor_id": 1, + * "invoice_number": "INV-001", * "expense_date": "2026-03-31", + * "paid_through": 1001, * "department_id": 1, - * "status": "draft" + * "tax_id": 1, + * "discount_amount": 8, + * "notes": "Updated shop supplies purchase", + * "status": "open", + * "label_ids": [ + * 1 + * ], + * "items": [ + * { + * "expense_item_id": 1, + * "quantity": 3, + * "rate": 100, + * "chart_of_account": 1201, + * "description": "Updated cleaning materials" + * } + * ] * } */ "application/json": { @@ -25309,9 +26487,22 @@ export interface paths { title?: string; category_id?: number; vendor_id?: number; + invoice_number?: string; expense_date?: string; + paid_through?: number; department_id?: number; + tax_id?: number; + discount_amount?: number; + notes?: string; status?: string; + label_ids?: number[]; + items?: { + expense_item_id?: number; + quantity?: number; + rate?: number; + chart_of_account?: number; + description?: string; + }[]; }; }; }; @@ -25324,21 +26515,73 @@ export interface paths { content: { /** * @example { - * "message": "Operation completed successfully.", + * "message": "Expense updated successfully.", * "data": { * "id": 1, * "job_card_id": 1, - * "title": "Workshop Supplies", + * "title": "Workshop Supplies Updated", * "category_id": 1, * "vendor_id": 1, * "invoice_number": "INV-001", * "expense_date": "2026-03-31", - * "paid_through": "string", + * "paid_through": 1001, * "department_id": 1, - * "notes": "string", - * "status": "draft", + * "tax_id": 1, + * "notes": "Updated shop supplies purchase", + * "status": "open", + * "discount_amount_major": 8, + * "sub_total": 300, + * "tax_amount": 15, + * "total": 307, + * "payments_made": 120, + * "balance_due": 187, + * "job_card": { + * "id": 1, + * "order_number": "ORD-0001", + * "estimate_number": "EST-0001" + * }, + * "category": { + * "id": 1, + * "name": "Consumables" + * }, + * "vendor": { + * "id": 1, + * "name": "Auto Parts Supplier" + * }, + * "department": { + * "id": 1, + * "name": "Service Department" + * }, + * "tax": { + * "id": 1, + * "name": "VAT 5%", + * "rate": "5.00" + * }, + * "labels": [ + * { + * "id": 1, + * "title": "Urgent", + * "color_code": "#FF6600" + * } + * ], + * "expense_items": [ + * { + * "id": 1, + * "expense_id": 1, + * "expense_item_id": 1, + * "quantity": 3, + * "rate": "100.00", + * "chart_of_account": 1201, + * "description": "Updated cleaning materials", + * "expense_item": { + * "id": 1, + * "name": "Workshop Consumable", + * "unit_price": "120.00" + * } + * } + * ], * "created_at": "2026-03-31T10:00:00.000000Z", - * "updated_at": "2026-03-31T10:00:00.000000Z" + * "updated_at": "2026-03-31T11:00:00.000000Z" * } * } */ @@ -25352,10 +26595,58 @@ export interface paths { vendor_id?: number; invoice_number?: string; expense_date?: string; - paid_through?: string; + paid_through?: number; department_id?: number; + tax_id?: number; notes?: string; status?: string; + discount_amount_major?: number; + sub_total?: number; + tax_amount?: number; + total?: number; + payments_made?: number; + balance_due?: number; + job_card?: { + id?: number; + order_number?: string; + estimate_number?: string; + }; + category?: { + id?: number; + name?: string; + }; + vendor?: { + id?: number; + name?: string; + }; + department?: { + id?: number; + name?: string; + }; + tax?: { + id?: number; + name?: string; + rate?: string; + }; + labels?: { + id?: number; + title?: string; + color_code?: string; + }[]; + expense_items?: { + id?: number; + expense_id?: number; + expense_item_id?: number; + quantity?: number; + rate?: string; + chart_of_account?: number; + description?: string; + expense_item?: { + id?: number; + name?: string; + unit_price?: string; + }; + }[]; /** Format: date-time */ created_at?: string; /** Format: date-time */ @@ -29886,8 +31177,9 @@ export interface paths { * "received_payment": false, * "payment_mode_id": 1, * "deposit_to": "Main Account", - * "amount": 2500, - * "discount": "no", + * "discount": "line_item_level", + * "discount_amount": 0, + * "tax_id": 1, * "inspection_categories": [ * { * "inspection_category_id": 1, @@ -29898,6 +31190,7 @@ export interface paths { * "rate": 500, * "chart_of_account": "4000", * "description": "General inspection", + * "discount_amount": 10, * "department_id": 1 * } * ], @@ -29908,6 +31201,7 @@ export interface paths { * "rate": 150, * "chart_of_account": "4100", * "description": "Oil filter", + * "discount_amount": 20, * "department_id": 1 * } * ], @@ -29918,6 +31212,7 @@ export interface paths { * "rate": 100, * "chart_of_account": "4200", * "description": "Shop supplies", + * "discount_amount": 5, * "department_id": 1 * } * ], @@ -29932,6 +31227,7 @@ export interface paths { * "rate": 800, * "chart_of_account": "4300", * "description": "Engine service", + * "discount_amount": 30, * "department_id": 1 * } * ], @@ -29945,6 +31241,7 @@ export interface paths { * "rate": 600, * "chart_of_account": "4400", * "description": "Major service package", + * "discount_amount": 15, * "department_id": 1 * } * ] @@ -29974,8 +31271,9 @@ export interface paths { received_payment?: boolean; payment_mode_id?: number; deposit_to?: string; - amount?: number; discount?: string; + discount_amount?: number; + tax_id?: number; inspection_categories?: { inspection_category_id?: number; rate_type?: string; @@ -29985,6 +31283,7 @@ export interface paths { rate?: number; chart_of_account?: string; description?: string; + discount_amount?: number; department_id?: number; }[]; parts?: { @@ -29993,6 +31292,7 @@ export interface paths { rate?: number; chart_of_account?: string; description?: string; + discount_amount?: number; department_id?: number; }[]; expenses?: { @@ -30001,6 +31301,7 @@ export interface paths { rate?: number; chart_of_account?: string; description?: string; + discount_amount?: number; department_id?: number; }[]; services?: { @@ -30013,6 +31314,7 @@ export interface paths { rate?: number; chart_of_account?: string; description?: string; + discount_amount?: number; department_id?: number; }[]; service_groups?: { @@ -30024,6 +31326,7 @@ export interface paths { rate?: number; chart_of_account?: string; description?: string; + discount_amount?: number; department_id?: number; }[]; }; @@ -30064,8 +31367,14 @@ export interface paths { * "received_payment": false, * "payment_mode_id": 1, * "deposit_to": "Main Account", - * "amount": 2500, + * "amount": 2595, * "discount": "no", + * "tax_id": 1, + * "discount_amount_major": 50, + * "sub_total": 2300, + * "total": 2595, + * "payments_recieved": 0, + * "balance_due": 2595, * "created_at": "2026-03-31T10:00:00.000000Z", * "updated_at": "2026-03-31T10:00:00.000000Z", * "customer": { @@ -30184,6 +31493,12 @@ export interface paths { deposit_to?: string; amount?: number; discount?: string; + tax_id?: number; + discount_amount_major?: number; + sub_total?: number; + total?: number; + payments_recieved?: number; + balance_due?: number; /** Format: date-time */ created_at?: string; /** Format: date-time */ @@ -30393,10 +31708,14 @@ export interface paths { * "payment_terms_id": 1, * "invoice_sequence_id": 1, * "invoice_number": "INV-001", + * "invoice_title": "Tax Invoice", * "department_id": 1, * "notes": "string", + * "terms_and_conditions": "Payment due in 14 days.", * "status": "draft", - * "discount": "no" + * "discount": "transaction_level", + * "discount_amount": 25.5, + * "tax_id": 1 * } */ "application/json": { @@ -30408,10 +31727,14 @@ export interface paths { 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; discount?: string; + discount_amount?: number; + tax_id?: number; }; }; }; @@ -30451,6 +31774,13 @@ export interface paths { * "deposit_to": "string", * "amount": 0, * "discount": "no", + * "tax_id": 1, + * "tax_amount": 0, + * "discount_amount_major": 25.5, + * "sub_total": 0, + * "total": 0, + * "payments_recieved": 0, + * "balance_due": 0, * "created_at": "2026-03-31T10:00:00.000000Z", * "updated_at": "2026-03-31T10:00:00.000000Z" * } @@ -30484,6 +31814,13 @@ export interface paths { deposit_to?: string; amount?: number; discount?: string; + tax_id?: number; + tax_amount?: number; + discount_amount_major?: number; + sub_total?: number; + total?: number; + payments_recieved?: number; + balance_due?: number; /** Format: date-time */ created_at?: string; /** Format: date-time */