feat: enhance invoice and job card forms with new fields, validation, and print functionality
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
parent
25c3125894
commit
349a458c3c
@ -16,7 +16,6 @@ export default async function InvoiceDetailLayout(props: { params: Promise<{ id:
|
|||||||
return (
|
return (
|
||||||
<InvoiceProvider invoice={data}>
|
<InvoiceProvider invoice={data}>
|
||||||
<DashboardDetailsPage
|
<DashboardDetailsPage
|
||||||
className='p-0 lg:p-0'
|
|
||||||
title={title}
|
title={title}
|
||||||
description={data?.invoice_number ? `Invoice #: ${data.invoice_number}` : undefined}
|
description={data?.invoice_number ? `Invoice #: ${data.invoice_number}` : undefined}
|
||||||
icon={<ReceiptIcon className="size-5" />}
|
icon={<ReceiptIcon className="size-5" />}
|
||||||
|
|||||||
@ -5,18 +5,74 @@ import { ResourcePage } from "@/shared/data-view/resource-page"
|
|||||||
import { ColumnHeader } from "@/shared/data-view/table-view"
|
import { ColumnHeader } from "@/shared/data-view/table-view"
|
||||||
import FormDialog from "@/shared/components/form-dialog"
|
import FormDialog from "@/shared/components/form-dialog"
|
||||||
import { InvoiceForm } from "@/modules/invoices/invoice-form"
|
import { InvoiceForm } from "@/modules/invoices/invoice-form"
|
||||||
|
import { Badge } from "@/shared/components/ui/badge"
|
||||||
|
import { formatCurrency, formatDate, formatEnum } from "@/shared/utils/formatters"
|
||||||
import { INVOICE_ROUTES } from "@garage/api"
|
import { INVOICE_ROUTES } from "@garage/api"
|
||||||
import type { InvoicesClient } from "@garage/api"
|
import type { InvoicesClient } from "@garage/api"
|
||||||
|
|
||||||
type InvoiceItem = {
|
type InvoiceItem = {
|
||||||
id: number
|
id: number
|
||||||
subject?: string
|
subject?: string
|
||||||
|
invoice_title?: string
|
||||||
invoice_number?: string
|
invoice_number?: string
|
||||||
customer_name?: string
|
customer_name?: string
|
||||||
status?: string
|
status?: string
|
||||||
invoice_date?: string
|
invoice_date?: string
|
||||||
due_date?: string
|
due_date?: string
|
||||||
created_at?: string
|
created_at?: string
|
||||||
|
total?: number | string
|
||||||
|
balance_due?: number | string
|
||||||
|
customer?: {
|
||||||
|
first_name?: string
|
||||||
|
last_name?: string
|
||||||
|
company_name?: string
|
||||||
|
phone?: string
|
||||||
|
} | null
|
||||||
|
vehicle?: {
|
||||||
|
make?: string
|
||||||
|
model?: string
|
||||||
|
sub_model?: string
|
||||||
|
year?: number | string
|
||||||
|
license_plate?: string
|
||||||
|
} | null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCustomerLabel(item: InvoiceItem): string {
|
||||||
|
const firstName = item.customer?.first_name?.trim()
|
||||||
|
const lastName = item.customer?.last_name?.trim()
|
||||||
|
const fullName = [firstName, lastName].filter(Boolean).join(" ").trim()
|
||||||
|
|
||||||
|
return fullName || item.customer_name || item.customer?.company_name || "—"
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVehicleLabel(item: InvoiceItem): string {
|
||||||
|
const vehicle = item.vehicle
|
||||||
|
if (!vehicle) return "—"
|
||||||
|
|
||||||
|
const core = [vehicle.make, vehicle.model, vehicle.sub_model].filter(Boolean).join(" ")
|
||||||
|
const suffix = [vehicle.year, vehicle.license_plate].filter(Boolean).join(" • ")
|
||||||
|
|
||||||
|
if (core && suffix) return `${core} (${suffix})`
|
||||||
|
return core || suffix || "—"
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDueMeta(dueDate?: string) {
|
||||||
|
if (!dueDate) return { text: "No due date", tone: "muted" as const }
|
||||||
|
|
||||||
|
const due = new Date(dueDate)
|
||||||
|
if (isNaN(due.getTime())) return { text: "Invalid due date", tone: "muted" as const }
|
||||||
|
|
||||||
|
const today = new Date()
|
||||||
|
today.setHours(0, 0, 0, 0)
|
||||||
|
due.setHours(0, 0, 0, 0)
|
||||||
|
|
||||||
|
const days = Math.ceil((due.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))
|
||||||
|
|
||||||
|
if (days < 0) return { text: `${Math.abs(days)} day(s) overdue`, tone: "danger" as const }
|
||||||
|
if (days === 0) return { text: "Due today", tone: "warning" as const }
|
||||||
|
if (days <= 3) return { text: `Due in ${days} day(s)`, tone: "warning" as const }
|
||||||
|
|
||||||
|
return { text: `Due in ${days} day(s)`, tone: "ok" as const }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function InvoicesPage() {
|
export default function InvoicesPage() {
|
||||||
@ -44,15 +100,66 @@ export default function InvoicesPage() {
|
|||||||
columns={({ actionsColumn }) => [
|
columns={({ actionsColumn }) => [
|
||||||
{
|
{
|
||||||
accessorKey: "subject",
|
accessorKey: "subject",
|
||||||
header: ({ column }) => <ColumnHeader column={column} title="Subject" />,
|
header: ({ column }) => <ColumnHeader column={column} title="Invoice" />,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const item = row.original as unknown as InvoiceItem
|
||||||
|
const title = item.subject || item.invoice_title || "Untitled invoice"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-w-[220px]">
|
||||||
|
<p className="font-medium leading-none">{title}</p>
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
{item.invoice_number || "No invoice number"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "invoice_number",
|
accessorKey: "invoice_number",
|
||||||
header: ({ column }) => <ColumnHeader column={column} title="Invoice #" />,
|
header: ({ column }) => <ColumnHeader column={column} title="Invoice #" />,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const item = row.original as unknown as InvoiceItem
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="font-mono text-xs text-muted-foreground">
|
||||||
|
{item.invoice_number || "—"}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "customer_name",
|
accessorKey: "customer_name",
|
||||||
header: ({ column }) => <ColumnHeader column={column} title="Customer" />,
|
header: ({ column }) => <ColumnHeader column={column} title="Customer" />,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const item = row.original as unknown as InvoiceItem
|
||||||
|
const customerLabel = getCustomerLabel(item)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-w-[190px]">
|
||||||
|
<p className="font-medium leading-none">{customerLabel}</p>
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
{item.customer?.phone || item.customer?.company_name || "—"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "vehicle",
|
||||||
|
header: ({ column }) => <ColumnHeader column={column} title="Vehicle" />,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const item = row.original as unknown as InvoiceItem
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-w-[220px]">
|
||||||
|
<p className="font-medium leading-none">{getVehicleLabel(item)}</p>
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
{item.vehicle?.license_plate || "—"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "status",
|
accessorKey: "status",
|
||||||
@ -60,27 +167,71 @@ export default function InvoicesPage() {
|
|||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const item = row.original as unknown as InvoiceItem
|
const item = row.original as unknown as InvoiceItem
|
||||||
const status = item.status
|
const status = item.status
|
||||||
const colorMap: Record<string, string> = {
|
const classMap: Record<string, string> = {
|
||||||
draft: "text-muted-foreground",
|
draft: "bg-slate-100 text-slate-700",
|
||||||
open: "text-blue-600",
|
open: "bg-blue-100 text-blue-700",
|
||||||
paid: "text-green-600",
|
paid: "bg-emerald-100 text-emerald-700",
|
||||||
overdue: "text-red-600",
|
over_due: "bg-red-100 text-red-700",
|
||||||
void: "text-gray-400",
|
un_paid: "bg-orange-100 text-orange-700",
|
||||||
|
partially_paid: "bg-amber-100 text-amber-700",
|
||||||
|
void: "bg-zinc-100 text-zinc-700",
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={colorMap[status ?? ""] ?? ""}>
|
<Badge variant="outline" className={classMap[status ?? ""] ?? ""}>
|
||||||
{status ? status.charAt(0).toUpperCase() + status.slice(1) : "—"}
|
{formatEnum(status) || "—"}
|
||||||
</span>
|
</Badge>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "invoice_date",
|
accessorKey: "invoice_date",
|
||||||
header: ({ column }) => <ColumnHeader column={column} title="Invoice Date" />,
|
header: ({ column }) => <ColumnHeader column={column} title="Invoice Date" />,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const item = row.original as unknown as InvoiceItem
|
||||||
|
return formatDate(item.invoice_date)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "due_date",
|
accessorKey: "due_date",
|
||||||
header: ({ column }) => <ColumnHeader column={column} title="Due Date" />,
|
header: ({ column }) => <ColumnHeader column={column} title="Due Date" />,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const item = row.original as unknown as InvoiceItem
|
||||||
|
const dueMeta = getDueMeta(item.due_date)
|
||||||
|
const toneClassMap = {
|
||||||
|
danger: "text-red-600",
|
||||||
|
warning: "text-amber-600",
|
||||||
|
ok: "text-emerald-600",
|
||||||
|
muted: "text-muted-foreground",
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p className="font-medium leading-none">{formatDate(item.due_date)}</p>
|
||||||
|
<p className={`mt-1 text-xs ${toneClassMap[dueMeta.tone]}`}>
|
||||||
|
{dueMeta.text}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "amounts",
|
||||||
|
header: ({ column }) => <ColumnHeader column={column} title="Amounts" />,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const item = row.original as unknown as InvoiceItem
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-w-[150px]">
|
||||||
|
<p className="font-medium leading-none">
|
||||||
|
Total: {formatCurrency(item.total ?? 0)}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
Due: {formatCurrency(item.balance_due ?? 0)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actionsColumn(),
|
actionsColumn(),
|
||||||
]}
|
]}
|
||||||
|
|||||||
@ -67,7 +67,7 @@ export default function JobCardsPage() {
|
|||||||
actions: (
|
actions: (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FilterTrigger onClick={filter.open} activeFilterCount={filter.activeFilterCount} />
|
<FilterTrigger onClick={filter.open} activeFilterCount={filter.activeFilterCount} />
|
||||||
<FormDialog classNames={{ dialogContent: 'min-w-6xl' }} title="Job Card" >
|
<FormDialog classNames={{ dialogContent: 'xl:min-w-6xl' }} title="Job Card" >
|
||||||
{(resourceId, {close}) => (
|
{(resourceId, {close}) => (
|
||||||
<JobCardForm
|
<JobCardForm
|
||||||
resourceId={resourceId}
|
resourceId={resourceId}
|
||||||
|
|||||||
@ -24,7 +24,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/shared/components/ui/select"
|
} from "@/shared/components/ui/select"
|
||||||
import { Ellipsis, Pencil, Trash2, ShieldCheck, Check, X } from "lucide-react"
|
import { Ellipsis, Pencil, Trash2, ShieldCheck, Check, X, Printer } from "lucide-react"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query"
|
import { useMutation, useQuery } from "@tanstack/react-query"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
@ -33,6 +33,7 @@ import { ESTIMATE_ROUTES } from "@garage/api"
|
|||||||
import { DatePickerField, TimePickerField } from "@/shared/components/form"
|
import { DatePickerField, TimePickerField } from "@/shared/components/form"
|
||||||
import { cn } from "@/shared/lib/utils"
|
import { cn } from "@/shared/lib/utils"
|
||||||
import { EmployeeCombobox, type EmployeeOption } from "../employees/employee-combobox"
|
import { EmployeeCombobox, type EmployeeOption } from "../employees/employee-combobox"
|
||||||
|
import { useDocumentPrint } from "@/shared/hooks/use-document-print"
|
||||||
|
|
||||||
type EstimateActionsProps = {
|
type EstimateActionsProps = {
|
||||||
estimateId: string
|
estimateId: string
|
||||||
@ -131,6 +132,7 @@ function SectionHeading({ title }: { title: string }) {
|
|||||||
export function EstimateActions({ estimateId }: EstimateActionsProps) {
|
export function EstimateActions({ estimateId }: EstimateActionsProps) {
|
||||||
const api = useAuthApi()
|
const api = useAuthApi()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { print, isPrinting } = useDocumentPrint()
|
||||||
const [editOpen, setEditOpen] = useState(false)
|
const [editOpen, setEditOpen] = useState(false)
|
||||||
const [authOpen, setAuthOpen] = useState(false)
|
const [authOpen, setAuthOpen] = useState(false)
|
||||||
const [itemStatuses, setItemStatuses] = useState<Record<string, string>>({})
|
const [itemStatuses, setItemStatuses] = useState<Record<string, string>>({})
|
||||||
@ -218,6 +220,10 @@ export function EstimateActions({ estimateId }: EstimateActionsProps) {
|
|||||||
<Pencil className="size-4" />
|
<Pencil className="size-4" />
|
||||||
Edit
|
Edit
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => print("estimate", estimateId, "print")} disabled={isPrinting}>
|
||||||
|
<Printer className="size-4" />
|
||||||
|
{isPrinting ? "Printing..." : "Print"}
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={openAuthDialog}>
|
<DropdownMenuItem onClick={openAuthDialog}>
|
||||||
<ShieldCheck className="size-4" />
|
<ShieldCheck className="size-4" />
|
||||||
Store Authorisation
|
Store Authorisation
|
||||||
|
|||||||
@ -12,10 +12,11 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/shared/components/ui/dropdown-menu"
|
} from "@/shared/components/ui/dropdown-menu"
|
||||||
import { Ellipsis, Pencil, Trash2, Play, CheckCircle2 } from "lucide-react"
|
import { Ellipsis, Pencil, Trash2, Play, CheckCircle2, Printer } from "lucide-react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { useFormDialog } from "@/shared/components/form-dialog"
|
import { useFormDialog } from "@/shared/components/form-dialog"
|
||||||
import { InspectionForm } from "./inspection-form"
|
import { InspectionForm } from "./inspection-form"
|
||||||
|
import { useDocumentPrint } from "@/shared/hooks/use-document-print"
|
||||||
|
|
||||||
type InspectionActionsProps = {
|
type InspectionActionsProps = {
|
||||||
inspectionId: string
|
inspectionId: string
|
||||||
@ -32,6 +33,7 @@ export function InspectionActions({ inspectionId, status, onStatusChange }: Insp
|
|||||||
const api = useAuthApi()
|
const api = useAuthApi()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const editDialog = useFormDialog("inspection-details-edit")
|
const editDialog = useFormDialog("inspection-details-edit")
|
||||||
|
const { print, isPrinting } = useDocumentPrint()
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
const promise = api.inspections.destroy(inspectionId)
|
const promise = api.inspections.destroy(inspectionId)
|
||||||
@ -73,6 +75,10 @@ export function InspectionActions({ inspectionId, status, onStatusChange }: Insp
|
|||||||
<Pencil className="size-4" />
|
<Pencil className="size-4" />
|
||||||
Edit
|
Edit
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => print("inspection", inspectionId, "print")} disabled={isPrinting}>
|
||||||
|
<Printer className="size-4" />
|
||||||
|
{isPrinting ? "Printing..." : "Print"}
|
||||||
|
</DropdownMenuItem>
|
||||||
{transition && (
|
{transition && (
|
||||||
<DropdownMenuItem onClick={() => handleStatusChange(transition.next)}>
|
<DropdownMenuItem onClick={() => handleStatusChange(transition.next)}>
|
||||||
<transition.icon className="size-4" />
|
<transition.icon className="size-4" />
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { useAuthApi } from "@/shared/useApi"
|
import { useAuthApi } from "@/shared/useApi"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { useState } from "react"
|
|
||||||
import { Button } from "@/shared/components/ui/button"
|
import { Button } from "@/shared/components/ui/button"
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@ -10,10 +9,8 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/shared/components/ui/dropdown-menu"
|
} from "@/shared/components/ui/dropdown-menu"
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/shared/components/ui/dialog"
|
import { Ellipsis, Printer, Trash2 } from "lucide-react"
|
||||||
import { Ellipsis, Pencil, Trash2 } from "lucide-react"
|
import { useDocumentPrint } from "@/shared/hooks/use-document-print"
|
||||||
import { InvoiceEditForm } from "./invoice-edit-form"
|
|
||||||
import { useInvoice } from "./invoice-context"
|
|
||||||
|
|
||||||
type InvoiceActionsProps = {
|
type InvoiceActionsProps = {
|
||||||
invoiceId: string
|
invoiceId: string
|
||||||
@ -22,12 +19,7 @@ type InvoiceActionsProps = {
|
|||||||
export function InvoiceActions({ invoiceId }: InvoiceActionsProps) {
|
export function InvoiceActions({ invoiceId }: InvoiceActionsProps) {
|
||||||
const api = useAuthApi()
|
const api = useAuthApi()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [isEditOpen, setIsEditOpen] = useState(false)
|
const { print, isPrinting } = useDocumentPrint()
|
||||||
const invoice = useInvoice()
|
|
||||||
const handleEditSuccess = () => {
|
|
||||||
setIsEditOpen(false)
|
|
||||||
router.refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
await api.invoices.destroy(invoiceId)
|
await api.invoices.destroy(invoiceId)
|
||||||
@ -43,31 +35,16 @@ export function InvoiceActions({ invoiceId }: InvoiceActionsProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
{/* <DropdownMenuItem onClick={() => setIsEditOpen(true)}>
|
<DropdownMenuItem onClick={() => print("invoice", invoiceId, "print")} disabled={isPrinting}>
|
||||||
<Pencil className="size-4" />
|
<Printer className="size-4" />
|
||||||
Edit
|
{isPrinting ? "Printing..." : "Print"}
|
||||||
</DropdownMenuItem> */}
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem variant="destructive" onClick={handleDelete}>
|
<DropdownMenuItem variant="destructive" onClick={handleDelete}>
|
||||||
<Trash2 className="size-4" />
|
<Trash2 className="size-4" />
|
||||||
Delete
|
Delete
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
{/* <Dialog open={isEditOpen} onOpenChange={setIsEditOpen}>
|
|
||||||
<DialogContent className="max-w-2xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Edit Invoice</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<InvoiceEditForm
|
|
||||||
initialData={{
|
|
||||||
|
|
||||||
}}
|
|
||||||
resourceId={invoiceId}
|
|
||||||
onSuccess={handleEditSuccess}
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog> */}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,9 +26,18 @@ import { useJobCard } from "./job-card-context"
|
|||||||
|
|
||||||
const relationFieldSchema = z.object({ value: z.string(), label: z.string() }).nullable()
|
const relationFieldSchema = z.object({ value: z.string(), label: z.string() }).nullable()
|
||||||
|
|
||||||
|
const requiredRelationFieldSchema = relationFieldSchema.superRefine((value, ctx) => {
|
||||||
|
if (!value?.value) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "This field is required",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const jobCardExpenseItemFormSchema = z.object({
|
const jobCardExpenseItemFormSchema = z.object({
|
||||||
expense_item: relationFieldSchema,
|
expense_item: relationFieldSchema,
|
||||||
department: relationFieldSchema.optional(),
|
department: requiredRelationFieldSchema,
|
||||||
tax: relationFieldSchema.optional(),
|
tax: relationFieldSchema.optional(),
|
||||||
quantity: z.coerce.number().min(1, "Quantity is required"),
|
quantity: z.coerce.number().min(1, "Quantity is required"),
|
||||||
rate: z.coerce.number().min(0, "Rate is required"),
|
rate: z.coerce.number().min(0, "Rate is required"),
|
||||||
@ -62,14 +71,22 @@ const DEFAULT_VALUES: JobCardExpenseItemFormValues = {
|
|||||||
|
|
||||||
const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label }
|
const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label }
|
||||||
|
|
||||||
|
function mapDepartmentOption(item: any) {
|
||||||
|
const id = item?.id ?? item?.department_id
|
||||||
|
return {
|
||||||
|
value: String(id),
|
||||||
|
label: item?.name ?? item?.title ?? item?.department_name ?? String(id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function mapToFormValues(data: unknown): JobCardExpenseItemFormValues {
|
function mapToFormValues(data: unknown): JobCardExpenseItemFormValues {
|
||||||
const d = (data as any) ?? {}
|
const d = (data as any) ?? {}
|
||||||
return {
|
return {
|
||||||
expense_item: d.expense_item
|
expense_item: d.expense_item
|
||||||
? { value: String(d.expense_item.id), label: d.expense_item.item_name ?? String(d.expense_item.id) }
|
? { value: String(d.expense_item.id), label: d.expense_item.item_name ?? String(d.expense_item.id) }
|
||||||
: null,
|
: null,
|
||||||
department: d.department
|
department: (d.department || d.department_id != null)
|
||||||
? { value: String(d.department.id), label: d.department.name ?? String(d.department.id) }
|
? mapDepartmentOption(d.department ?? { id: d.department_id, name: d.department_name ?? d.department_title })
|
||||||
: null,
|
: null,
|
||||||
tax: d.tax_id != null
|
tax: d.tax_id != null
|
||||||
? { value: String(d.tax_id), label: formatTaxLabel(d.tax, String(d.tax_id)) }
|
? { value: String(d.tax_id), label: formatTaxLabel(d.tax, String(d.tax_id)) }
|
||||||
@ -82,6 +99,18 @@ function mapToFormValues(data: unknown): JobCardExpenseItemFormValues {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapJobCardDepartmentToRelation(jobCard: unknown) {
|
||||||
|
const d = (jobCard as any) ?? {}
|
||||||
|
const departmentId = d.department_id ?? d.department?.id
|
||||||
|
if (departmentId == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
value: String(departmentId),
|
||||||
|
label: d.department?.name ?? d.department_name ?? d.department_title ?? String(departmentId),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Component ──
|
// ── Component ──
|
||||||
|
|
||||||
export function JobCardExpenseItemForm({
|
export function JobCardExpenseItemForm({
|
||||||
@ -100,7 +129,10 @@ export function JobCardExpenseItemForm({
|
|||||||
resolver: zodResolver(jobCardExpenseItemFormSchema) as any,
|
resolver: zodResolver(jobCardExpenseItemFormSchema) as any,
|
||||||
defaultValues: initialData
|
defaultValues: initialData
|
||||||
? mapToFormValues(initialData)
|
? mapToFormValues(initialData)
|
||||||
: DEFAULT_VALUES,
|
: {
|
||||||
|
...DEFAULT_VALUES,
|
||||||
|
department: mapJobCardDepartmentToRelation(jobCard),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const [error, setError] = React.useState<string | null>(null)
|
const [error, setError] = React.useState<string | null>(null)
|
||||||
@ -126,10 +158,15 @@ export function JobCardExpenseItemForm({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
if (!values.department?.value) {
|
||||||
|
setError("Department is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
await toast.promise(
|
await toast.promise(
|
||||||
api.jobCards.addExpenseItem(jobCardId, {
|
api.jobCards.addExpenseItem(jobCardId, {
|
||||||
expense_item_id: values.expense_item ? Number(values.expense_item.value) : undefined,
|
expense_item_id: values.expense_item ? Number(values.expense_item.value) : undefined,
|
||||||
department_id: values.department ? Number(values.department.value) : undefined,
|
department_id: Number(values.department.value),
|
||||||
tax_id: values.tax ? Number(values.tax.value) : undefined,
|
tax_id: values.tax ? Number(values.tax.value) : undefined,
|
||||||
quantity: values.quantity,
|
quantity: values.quantity,
|
||||||
rate: values.rate,
|
rate: values.rate,
|
||||||
@ -215,12 +252,10 @@ export function JobCardExpenseItemForm({
|
|||||||
name="department"
|
name="department"
|
||||||
label="Department"
|
label="Department"
|
||||||
placeholder="Select department"
|
placeholder="Select department"
|
||||||
|
required
|
||||||
queryKey={[DEPARTMENT_ROUTES.INDEX]}
|
queryKey={[DEPARTMENT_ROUTES.INDEX]}
|
||||||
listFn={() => api.departments.list()}
|
listFn={() => api.departments.list()}
|
||||||
mapOption={(item: any) => ({
|
mapOption={mapDepartmentOption}
|
||||||
value: String(item.id),
|
|
||||||
label: item.name ?? String(item.id),
|
|
||||||
})}
|
|
||||||
createForm={(props) => <DepartmentInlineForm {...props} />}
|
createForm={(props) => <DepartmentInlineForm {...props} />}
|
||||||
createLabel="Department"
|
createLabel="Department"
|
||||||
{...STORE_OBJECT}
|
{...STORE_OBJECT}
|
||||||
|
|||||||
@ -118,7 +118,7 @@ function mapToFormValues(data: unknown): JobCardFormValues {
|
|||||||
customer: toRelation(d.customer_id, d.customer ? `${d.customer.first_name} ${d.customer.last_name}` : undefined),
|
customer: toRelation(d.customer_id, d.customer ? `${d.customer.first_name} ${d.customer.last_name}` : undefined),
|
||||||
vehicle: toRelation(d.vehicle_id, d.vehicle ? (d.vehicle.plate_number ?? `${d.vehicle.make ?? ""} ${d.vehicle.model ?? ""}`.trim()) : undefined),
|
vehicle: toRelation(d.vehicle_id, d.vehicle ? (d.vehicle.plate_number ?? `${d.vehicle.make ?? ""} ${d.vehicle.model ?? ""}`.trim()) : undefined),
|
||||||
estimate: toRelation(d.estimate_id, d.estimate?.estimate_number),
|
estimate: toRelation(d.estimate_id, d.estimate?.estimate_number),
|
||||||
department: toRelation(d.department_id, d.department?.name),
|
department: toRelation(d.department_id, d.department?.name ?? d.department_name ?? d.department_title),
|
||||||
service_writer: toRelation(d.service_writer_id, d.service_writer ? `${d.service_writer.first_name} ${d.service_writer.last_name}` : undefined),
|
service_writer: toRelation(d.service_writer_id, d.service_writer ? `${d.service_writer.first_name} ${d.service_writer.last_name}` : undefined),
|
||||||
primary_technician: toRelation(d.primary_technician_id, d.primary_technician ? `${d.primary_technician.first_name} ${d.primary_technician.last_name}` : undefined),
|
primary_technician: toRelation(d.primary_technician_id, d.primary_technician ? `${d.primary_technician.first_name} ${d.primary_technician.last_name}` : undefined),
|
||||||
sales_person: toRelation(d.sales_person_id, d.sales_person ? `${d.sales_person.first_name} ${d.sales_person.last_name}` : undefined),
|
sales_person: toRelation(d.sales_person_id, d.sales_person ? `${d.sales_person.first_name} ${d.sales_person.last_name}` : undefined),
|
||||||
@ -211,11 +211,13 @@ function mapFormToPayload(values: JobCardFormValues) {
|
|||||||
|
|
||||||
// ── Shared mapOption for async selects ──
|
// ── Shared mapOption for async selects ──
|
||||||
|
|
||||||
const mapLookupOption = (item: any) => ({
|
const mapLookupOption = (item: any, labelKey: string = "name") => ({
|
||||||
value: String(item.id),
|
value: String(item.id),
|
||||||
label: item.name,
|
label: item[labelKey],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const mapEmployeeOption = (item: any) => ({
|
const mapEmployeeOption = (item: any) => ({
|
||||||
value: String(item.id),
|
value: String(item.id),
|
||||||
label: `${item.first_name} ${item.last_name}`,
|
label: `${item.first_name} ${item.last_name}`,
|
||||||
@ -320,7 +322,7 @@ export function JobCardForm({ resourceId, initialData, onSuccess }: JobCardFormP
|
|||||||
placeholder="Select insurance type"
|
placeholder="Select insurance type"
|
||||||
queryKey={[INSURANCE_TYPE_ROUTES.INDEX]}
|
queryKey={[INSURANCE_TYPE_ROUTES.INDEX]}
|
||||||
listFn={() => api.insuranceTypes.list()}
|
listFn={() => api.insuranceTypes.list()}
|
||||||
mapOption={mapLookupOption}
|
mapOption={(op) => (mapLookupOption(op, 'title'))}
|
||||||
{...STORE_OBJECT}
|
{...STORE_OBJECT}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -341,7 +343,7 @@ export function JobCardForm({ resourceId, initialData, onSuccess }: JobCardFormP
|
|||||||
label="Service Writer"
|
label="Service Writer"
|
||||||
placeholder="Select service writer"
|
placeholder="Select service writer"
|
||||||
{...STORE_OBJECT}
|
{...STORE_OBJECT}
|
||||||
/>
|
/>
|
||||||
<RhfEmployeeSelectField
|
<RhfEmployeeSelectField
|
||||||
showCreate
|
showCreate
|
||||||
name="primary_technician"
|
name="primary_technician"
|
||||||
@ -436,6 +438,7 @@ export function JobCardForm({ resourceId, initialData, onSuccess }: JobCardFormP
|
|||||||
name="department"
|
name="department"
|
||||||
label="Department"
|
label="Department"
|
||||||
placeholder="Select department"
|
placeholder="Select department"
|
||||||
|
required
|
||||||
queryKey={[DEPARTMENT_ROUTES.INDEX]}
|
queryKey={[DEPARTMENT_ROUTES.INDEX]}
|
||||||
listFn={() => api.departments.list()}
|
listFn={() => api.departments.list()}
|
||||||
mapOption={mapLookupOption}
|
mapOption={mapLookupOption}
|
||||||
|
|||||||
@ -62,14 +62,22 @@ const DEFAULT_VALUES: JobCardPartFormValues = {
|
|||||||
|
|
||||||
const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label }
|
const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label }
|
||||||
|
|
||||||
|
function mapDepartmentOption(item: any) {
|
||||||
|
const id = item?.id ?? item?.department_id
|
||||||
|
return {
|
||||||
|
value: String(id),
|
||||||
|
label: item?.name ?? item?.title ?? item?.department_name ?? String(id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function mapToFormValues(data: unknown): JobCardPartFormValues {
|
function mapToFormValues(data: unknown): JobCardPartFormValues {
|
||||||
const d = (data as any) ?? {}
|
const d = (data as any) ?? {}
|
||||||
return {
|
return {
|
||||||
part: d.part
|
part: d.part
|
||||||
? { value: String(d.part.id), label: d.part.title ?? String(d.part.id) }
|
? { value: String(d.part.id), label: d.part.title ?? String(d.part.id) }
|
||||||
: null,
|
: null,
|
||||||
department: d.department
|
department: (d.department || d.department_id != null)
|
||||||
? { value: String(d.department.id), label: d.department.name ?? String(d.department.id) }
|
? mapDepartmentOption(d.department ?? { id: d.department_id, name: d.department_name ?? d.department_title })
|
||||||
: null,
|
: null,
|
||||||
tax: d.tax_id != null
|
tax: d.tax_id != null
|
||||||
? { value: String(d.tax_id), label: formatTaxLabel(d.tax, String(d.tax_id)) }
|
? { value: String(d.tax_id), label: formatTaxLabel(d.tax, String(d.tax_id)) }
|
||||||
@ -82,6 +90,18 @@ function mapToFormValues(data: unknown): JobCardPartFormValues {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapJobCardDepartmentToRelation(jobCard: unknown) {
|
||||||
|
const d = (jobCard as any) ?? {}
|
||||||
|
const departmentId = d.department_id ?? d.department?.id
|
||||||
|
if (departmentId == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
value: String(departmentId),
|
||||||
|
label: d.department?.name ?? d.department_name ?? d.department_title ?? String(departmentId),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Component ──
|
// ── Component ──
|
||||||
|
|
||||||
export function JobCardPartForm({
|
export function JobCardPartForm({
|
||||||
@ -100,7 +120,10 @@ export function JobCardPartForm({
|
|||||||
resolver: zodResolver(jobCardPartFormSchema) as any,
|
resolver: zodResolver(jobCardPartFormSchema) as any,
|
||||||
defaultValues: initialData
|
defaultValues: initialData
|
||||||
? mapToFormValues(initialData)
|
? mapToFormValues(initialData)
|
||||||
: DEFAULT_VALUES,
|
: {
|
||||||
|
...DEFAULT_VALUES,
|
||||||
|
department: mapJobCardDepartmentToRelation(jobCard),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const [error, setError] = React.useState<string | null>(null)
|
const [error, setError] = React.useState<string | null>(null)
|
||||||
@ -126,10 +149,15 @@ export function JobCardPartForm({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
if (!values.department?.value) {
|
||||||
|
setError("Department is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
await toast.promise(
|
await toast.promise(
|
||||||
api.jobCards.addPart(jobCardId, {
|
api.jobCards.addPart(jobCardId, {
|
||||||
part_id: values.part ? Number(values.part.value) : undefined,
|
part_id: values.part ? Number(values.part.value) : undefined,
|
||||||
department_id: values.department ? Number(values.department.value) : undefined,
|
department_id: Number(values.department.value),
|
||||||
tax_id: values.tax ? Number(values.tax.value) : undefined,
|
tax_id: values.tax ? Number(values.tax.value) : undefined,
|
||||||
quantity: values.quantity,
|
quantity: values.quantity,
|
||||||
rate: values.rate,
|
rate: values.rate,
|
||||||
@ -215,12 +243,10 @@ export function JobCardPartForm({
|
|||||||
name="department"
|
name="department"
|
||||||
label="Department"
|
label="Department"
|
||||||
placeholder="Select department"
|
placeholder="Select department"
|
||||||
|
required
|
||||||
queryKey={[DEPARTMENT_ROUTES.INDEX]}
|
queryKey={[DEPARTMENT_ROUTES.INDEX]}
|
||||||
listFn={() => api.departments.list()}
|
listFn={() => api.departments.list()}
|
||||||
mapOption={(item: any) => ({
|
mapOption={mapDepartmentOption}
|
||||||
value: String(item.id),
|
|
||||||
label: item.name ?? String(item.id),
|
|
||||||
})}
|
|
||||||
createForm={(props) => <DepartmentInlineForm {...props} />}
|
createForm={(props) => <DepartmentInlineForm {...props} />}
|
||||||
createLabel="Department"
|
createLabel="Department"
|
||||||
{...STORE_OBJECT}
|
{...STORE_OBJECT}
|
||||||
|
|||||||
@ -71,14 +71,22 @@ const DEFAULT_VALUES: JobCardServiceFormValues = {
|
|||||||
|
|
||||||
const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label }
|
const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label }
|
||||||
|
|
||||||
|
function mapDepartmentOption(item: any) {
|
||||||
|
const id = item?.id ?? item?.department_id
|
||||||
|
return {
|
||||||
|
value: String(id),
|
||||||
|
label: item?.name ?? item?.title ?? item?.department_name ?? String(id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function mapToFormValues(data: unknown): JobCardServiceFormValues {
|
function mapToFormValues(data: unknown): JobCardServiceFormValues {
|
||||||
const d = (data as any) ?? {}
|
const d = (data as any) ?? {}
|
||||||
return {
|
return {
|
||||||
service: d.service
|
service: d.service
|
||||||
? { value: String(d.service.id), label: d.service.labor_name ?? String(d.service.id) }
|
? { value: String(d.service.id), label: d.service.labor_name ?? String(d.service.id) }
|
||||||
: null,
|
: null,
|
||||||
department: d.department
|
department: (d.department || d.department_id != null)
|
||||||
? { value: String(d.department.id), label: d.department.name ?? String(d.department.id) }
|
? mapDepartmentOption(d.department ?? { id: d.department_id, name: d.department_name ?? d.department_title })
|
||||||
: undefined as any,
|
: undefined as any,
|
||||||
tax: d.tax_id != null
|
tax: d.tax_id != null
|
||||||
? { value: String(d.tax_id), label: formatTaxLabel(d.tax, String(d.tax_id)) }
|
? { value: String(d.tax_id), label: formatTaxLabel(d.tax, String(d.tax_id)) }
|
||||||
@ -97,6 +105,18 @@ function mapToFormValues(data: unknown): JobCardServiceFormValues {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapJobCardDepartmentToRelation(jobCard: unknown) {
|
||||||
|
const d = (jobCard as any) ?? {}
|
||||||
|
const departmentId = d.department_id ?? d.department?.id
|
||||||
|
if (departmentId == null) {
|
||||||
|
return undefined as any
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
value: String(departmentId),
|
||||||
|
label: d.department?.name ?? d.department_name ?? d.department_title ?? String(departmentId),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const RATE_TYPE_OPTIONS = RateType.map((v) => ({
|
const RATE_TYPE_OPTIONS = RateType.map((v) => ({
|
||||||
value: v,
|
value: v,
|
||||||
label: v === "flat_rate" ? "Flat Rate" : "Hourly",
|
label: v === "flat_rate" ? "Flat Rate" : "Hourly",
|
||||||
@ -120,7 +140,10 @@ export function JobCardServiceForm({
|
|||||||
resolver: zodResolver(jobCardServiceFormSchema) as any,
|
resolver: zodResolver(jobCardServiceFormSchema) as any,
|
||||||
defaultValues: initialData
|
defaultValues: initialData
|
||||||
? mapToFormValues(initialData)
|
? mapToFormValues(initialData)
|
||||||
: DEFAULT_VALUES,
|
: {
|
||||||
|
...DEFAULT_VALUES,
|
||||||
|
department: mapJobCardDepartmentToRelation(jobCard),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const rateType = form.watch("rate_type")
|
const rateType = form.watch("rate_type")
|
||||||
@ -149,10 +172,15 @@ export function JobCardServiceForm({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
if (!values.department?.value) {
|
||||||
|
setError("Department is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
await toast.promise(
|
await toast.promise(
|
||||||
api.jobCards.addService(jobCardId, {
|
api.jobCards.addService(jobCardId, {
|
||||||
service_id: values.service ? Number(values.service.value) : undefined,
|
service_id: values.service ? Number(values.service.value) : undefined,
|
||||||
department_id: values.department ? Number(values.department.value) : undefined,
|
department_id: Number(values.department.value),
|
||||||
tax_id: values.tax ? Number(values.tax.value) : undefined,
|
tax_id: values.tax ? Number(values.tax.value) : undefined,
|
||||||
rate_type: values.rate_type || undefined,
|
rate_type: values.rate_type || undefined,
|
||||||
labor_rate_id: values.labor_rate ? Number(values.labor_rate.value) : undefined,
|
labor_rate_id: values.labor_rate ? Number(values.labor_rate.value) : undefined,
|
||||||
@ -283,10 +311,7 @@ export function JobCardServiceForm({
|
|||||||
required
|
required
|
||||||
queryKey={[DEPARTMENT_ROUTES.INDEX]}
|
queryKey={[DEPARTMENT_ROUTES.INDEX]}
|
||||||
listFn={() => api.departments.list()}
|
listFn={() => api.departments.list()}
|
||||||
mapOption={(item: any) => ({
|
mapOption={mapDepartmentOption}
|
||||||
value: String(item.id),
|
|
||||||
label: item.name ?? String(item.id),
|
|
||||||
})}
|
|
||||||
createForm={(props) => <DepartmentInlineForm {...props} />}
|
createForm={(props) => <DepartmentInlineForm {...props} />}
|
||||||
createLabel="Department"
|
createLabel="Department"
|
||||||
{...STORE_OBJECT}
|
{...STORE_OBJECT}
|
||||||
|
|||||||
@ -10,8 +10,9 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/shared/components/ui/dropdown-menu"
|
} from "@/shared/components/ui/dropdown-menu"
|
||||||
import { confirm } from "@/shared/components/confirm-dialog"
|
import { confirm } from "@/shared/components/confirm-dialog"
|
||||||
import { Ellipsis, Pencil, Trash2 } from "lucide-react"
|
import { Ellipsis, Printer, Trash2 } from "lucide-react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
import { useDocumentPrint } from "@/shared/hooks/use-document-print"
|
||||||
|
|
||||||
type PurchaseOrderActionsProps = {
|
type PurchaseOrderActionsProps = {
|
||||||
purchaseOrderId: string
|
purchaseOrderId: string
|
||||||
@ -20,6 +21,7 @@ type PurchaseOrderActionsProps = {
|
|||||||
export function PurchaseOrderActions({ purchaseOrderId }: PurchaseOrderActionsProps) {
|
export function PurchaseOrderActions({ purchaseOrderId }: PurchaseOrderActionsProps) {
|
||||||
const api = useAuthApi()
|
const api = useAuthApi()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { print, isPrinting } = useDocumentPrint()
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
const confirmed = await confirm({
|
const confirmed = await confirm({
|
||||||
@ -48,6 +50,10 @@ export function PurchaseOrderActions({ purchaseOrderId }: PurchaseOrderActionsPr
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onClick={() => print("purchase_order", purchaseOrderId, "print")} disabled={isPrinting}>
|
||||||
|
<Printer className="size-4" />
|
||||||
|
{isPrinting ? "Printing..." : "Print"}
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem variant="destructive" onClick={handleDelete}>
|
<DropdownMenuItem variant="destructive" onClick={handleDelete}>
|
||||||
<Trash2 className="size-4" />
|
<Trash2 className="size-4" />
|
||||||
Delete
|
Delete
|
||||||
|
|||||||
@ -91,7 +91,7 @@ export function formatNumber(value?: number | string | null): string {
|
|||||||
*/
|
*/
|
||||||
export function formatCurrency(
|
export function formatCurrency(
|
||||||
value?: number | string | null,
|
value?: number | string | null,
|
||||||
currency = "USD",
|
currency = "AED",
|
||||||
locale?: string,
|
locale?: string,
|
||||||
): string {
|
): string {
|
||||||
if (value == null || value === "") return "—"
|
if (value == null || value === "") return "—"
|
||||||
|
|||||||
@ -5,7 +5,15 @@ export const DOCUMENT_PRINT_ROUTES = {
|
|||||||
INDEX: "/api/document-print",
|
INDEX: "/api/document-print",
|
||||||
} as const satisfies Record<string, ApiPath>
|
} as const satisfies Record<string, ApiPath>
|
||||||
|
|
||||||
export type DocumentPrintType = "job_card" | "invoice" | "estimate" | "purchase_order" | "bill" | string
|
export type DocumentPrintType =
|
||||||
|
| "inspection"
|
||||||
|
| "estimate"
|
||||||
|
| "job_card"
|
||||||
|
| "invoice"
|
||||||
|
| "payment_received"
|
||||||
|
| "purchase_order"
|
||||||
|
| "bill"
|
||||||
|
| string
|
||||||
|
|
||||||
export type DocumentPrintMode = "print" | "download"
|
export type DocumentPrintMode = "print" | "download"
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user