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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user