feat: add template checkpoint edit dialog and vendor management components

- Implemented TemplateCheckpointEditDialog for creating and editing inspection checkpoints.
- Added VendorActions component for managing vendor actions including edit, activate/deactivate, and delete.
- Created VendorContext for managing vendor state across components.
- Developed VendorGeneralInfo component to display detailed vendor information.
- Introduced AedSymbol and Money components for consistent currency representation.
- Added PromptDialog for user input prompts throughout the application.
- Implemented RelationLink component for unified related-data display in CRUD tables.
- Created InspectionTemplatesClient for API interactions related to inspection templates.
This commit is contained in:
2026-05-18 12:08:42 +04:00
parent 54d11f01b4
commit 4bfd8c84a9
111 changed files with 5002 additions and 1047 deletions
@@ -2,6 +2,7 @@
import { useState } from "react"
import { FileText } from "lucide-react"
import { useQueryClient } from "@tanstack/react-query"
import { Button } from "@/shared/components/ui/button"
import {
Dialog,
@@ -11,50 +12,53 @@ import {
} from "@/shared/components/ui/dialog"
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"
import { getTodayDate } from "@/shared/lib/utils"
import { BILL_ROUTES } from "@garage/api"
/**
* Maps a Purchase Order data object to a complete BillFormValues shape so that
* useResourceForm's shallow spread (`{ ...defaultValues, ...initialData }`) correctly
* pre-fills all relational and line-item fields.
*/
function mapPOToBillInitialData(po: Record<string, any>) {
// Build an API-shaped seed that BillForm's `mapToFormValues` will consume,
// so the dialog opens with PO fields, vendor/department/job_card relations,
// and line items pre-filled.
function mapPOToBillSeed(po: Record<string, any>) {
return {
// Text fields
title: po.title ?? "",
notes: po.notes ?? "",
bill_date: getTodayDate(),
vendor_id: po.vendor_id,
vendor: po.vendor ? { name: getFullName(po.vendor) } : undefined,
department_id: po.department_id,
department: po.department,
job_card_id: po.job_card_id,
job_card: po.job_card
? { order_number: po.job_card.order_number ?? po.job_card.title }
: undefined,
// Relation fields — must be { value, label } objects for RhfAsyncSelectField
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),
purchase_order_id: po.id,
purchase_order: { order_number: po.order_number ?? po.title },
// Parts pre-filled from PO items
part_items: (po.parts ?? []).map((p: any) => ({
parts: (po.parts ?? []).map((p: any) => ({
part_id: p.part_id ?? p.id,
title: p.part?.title ?? p.title ?? "",
part: { name: p.part?.title ?? p.part?.name ?? p.title ?? "" },
quantity: Number(p.quantity) || 1,
rate: Number(p.rate) || 0,
description: p.description ?? "",
})),
// Services and expenses start empty (PO doesn't have them)
service_items: [],
expense_items: [],
}
}
export function CreateBillFromPOButton() {
const [open, setOpen] = useState(false)
const poContext = usePurchaseOrder()
const queryClient = useQueryClient()
if (!poContext) return null
const initialData = poContext.data ? mapPOToBillInitialData(poContext.data) : undefined
const initialData = poContext.data ? mapPOToBillSeed(poContext.data) : undefined
return (
<>
@@ -71,7 +75,12 @@ export function CreateBillFromPOButton() {
<ScrollArea className="max-h-[75vh] px-1">
<BillForm
initialData={initialData}
onSuccess={() => setOpen(false)}
onSuccess={() => {
// Invalidate bills list so when user lands on the bills
// page the new bill is present without a manual refresh.
queryClient.invalidateQueries({ queryKey: [BILL_ROUTES.INDEX] })
setOpen(false)
}}
/>
</ScrollArea>
</DialogContent>
@@ -6,7 +6,6 @@ import {
Truck,
FileText,
Package,
DollarSign,
} from "lucide-react"
import {
Card,
@@ -24,6 +23,7 @@ import {
TableRow,
TableCell,
} from "@/shared/components/ui/table"
import { Money } from "@/shared/components/money"
type PurchaseOrderPart = {
id?: number
@@ -92,11 +92,6 @@ function formatDate(dateStr?: string | null) {
return new Date(dateStr).toLocaleDateString()
}
function formatCurrency(value?: string | number | null) {
if (value == null) return "—"
return `$${Number(value).toFixed(2)}`
}
export function PurchaseOrderGeneralInfo({ purchaseOrder }: PurchaseOrderGeneralInfoProps) {
const parts = purchaseOrder.parts ?? []
const totalAmount = parts.reduce(
@@ -219,9 +214,8 @@ export function PurchaseOrderGeneralInfo({ purchaseOrder }: PurchaseOrderGeneral
<Package className="size-4" />
Parts ({parts.length})
</span>
<span className="flex items-center gap-2 text-base">
<DollarSign className="size-4" />
Total: {formatCurrency(totalAmount)}
<span className="text-base whitespace-nowrap">
Total: <Money value={totalAmount} />
</span>
</CardTitle>
</CardHeader>
@@ -257,10 +251,10 @@ export function PurchaseOrderGeneralInfo({ purchaseOrder }: PurchaseOrderGeneral
{item.quantity ?? 0}
</TableCell>
<TableCell className="text-right">
{formatCurrency(item.rate)}
{<Money value={item.rate} />}
</TableCell>
<TableCell className="text-right font-medium">
{formatCurrency(amount)}
{<Money value={amount} />}
</TableCell>
</TableRow>
)
@@ -270,7 +264,7 @@ export function PurchaseOrderGeneralInfo({ purchaseOrder }: PurchaseOrderGeneral
Total
</TableCell>
<TableCell className="text-right">
{formatCurrency(totalAmount)}
{<Money value={totalAmount} />}
</TableCell>
</TableRow>
</TableBody>