garage-erp/apps/dashboard/modules/purchase-orders/purchase-order-general-info.tsx
humam kerdiah 4bfd8c84a9 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.
2026-05-18 12:08:42 +04:00

283 lines
12 KiB
TypeScript

import {
ClipboardList,
Calendar,
Hash,
Building2,
Truck,
FileText,
Package,
} from "lucide-react"
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/shared/components/ui/card"
import { Badge } from "@/shared/components/ui/badge"
import { Separator } from "@/shared/components/ui/separator"
import {
Table,
TableHeader,
TableBody,
TableHead,
TableRow,
TableCell,
} from "@/shared/components/ui/table"
import { Money } from "@/shared/components/money"
type PurchaseOrderPart = {
id?: number
purchase_order_id?: number
part_id?: number
quantity?: number
rate?: string
description?: string
created_at?: string
updated_at?: string
part?: {
id?: number
title?: string
}
}
type PurchaseOrderData = {
id?: number
job_card_id?: number
vendor_id?: number
vendor_name?: string
job_card_name?: string
department_name?: string
title?: string
order_number?: string
order_date?: string
delivery_date?: string
department_id?: number
notes?: string
terms_and_conditions?: string
created_at?: string
updated_at?: string
parts?: PurchaseOrderPart[]
}
type PurchaseOrderGeneralInfoProps = {
purchaseOrder: PurchaseOrderData
}
function InfoItem({
icon: Icon,
label,
value,
}: {
icon: React.ComponentType<{ className?: string }>
label: string
value?: string | null
}) {
return (
<div className="flex items-start gap-3">
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
<Icon className="size-4" />
</div>
<div className="flex flex-col gap-0.5">
<span className="text-xs text-muted-foreground">{label}</span>
<span className="text-sm font-medium">
{value || <span className="text-muted-foreground"></span>}
</span>
</div>
</div>
)
}
function formatDate(dateStr?: string | null) {
if (!dateStr) return null
return new Date(dateStr).toLocaleDateString()
}
export function PurchaseOrderGeneralInfo({ purchaseOrder }: PurchaseOrderGeneralInfoProps) {
const parts = purchaseOrder.parts ?? []
const totalAmount = parts.reduce(
(sum, p) => sum + (p.quantity ?? 0) * Number(p.rate ?? 0),
0,
)
return (
<div className="grid gap-6">
{/* Top row: Order Info + Dates */}
<div className="grid gap-6 md:grid-cols-2">
{/* Order Information */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<ClipboardList className="size-4" />
Order Information
</CardTitle>
</CardHeader>
<CardContent className="grid gap-4">
<div className="flex flex-wrap items-center gap-2">
{purchaseOrder.order_number && (
<Badge variant="secondary">{purchaseOrder.order_number}</Badge>
)}
</div>
<Separator />
<div className="grid gap-4 sm:grid-cols-2">
<InfoItem
icon={Hash}
label="Order Number"
value={purchaseOrder.order_number}
/>
<InfoItem
icon={ClipboardList}
label="Title"
value={purchaseOrder.title}
/>
<InfoItem
icon={Truck}
label="Vendor"
value={purchaseOrder.vendor_name ?? (purchaseOrder.vendor_id ? `Vendor #${purchaseOrder.vendor_id}` : null)}
/>
<InfoItem
icon={Building2}
label="Department"
value={purchaseOrder.department_name ?? (purchaseOrder.department_id ? `Dept #${purchaseOrder.department_id}` : null)}
/>
</div>
</CardContent>
</Card>
{/* Dates & Notes */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Calendar className="size-4" />
Dates & Notes
</CardTitle>
</CardHeader>
<CardContent className="grid gap-4">
<div className="grid gap-4 sm:grid-cols-2">
<InfoItem
icon={Calendar}
label="Order Date"
value={formatDate(purchaseOrder.order_date)}
/>
<InfoItem
icon={Calendar}
label="Delivery Date"
value={formatDate(purchaseOrder.delivery_date)}
/>
<InfoItem
icon={Calendar}
label="Created"
value={formatDate(purchaseOrder.created_at)}
/>
<InfoItem
icon={Calendar}
label="Updated"
value={formatDate(purchaseOrder.updated_at)}
/>
</div>
{purchaseOrder.notes && (
<>
<Separator />
<div className="flex items-start gap-3">
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
<FileText className="size-4" />
</div>
<div className="flex flex-col gap-0.5">
<span className="text-xs text-muted-foreground">Notes</span>
<p className="text-sm whitespace-pre-wrap">{purchaseOrder.notes}</p>
</div>
</div>
</>
)}
{purchaseOrder.terms_and_conditions && (
<>
<Separator />
<div className="flex items-start gap-3">
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
<FileText className="size-4" />
</div>
<div className="flex flex-col gap-0.5">
<span className="text-xs text-muted-foreground">Terms & Conditions</span>
<p className="text-sm whitespace-pre-wrap">{purchaseOrder.terms_and_conditions}</p>
</div>
</div>
</>
)}
</CardContent>
</Card>
</div>
{/* Parts / Line Items */}
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2">
<Package className="size-4" />
Parts ({parts.length})
</span>
<span className="text-base whitespace-nowrap">
Total: <Money value={totalAmount} />
</span>
</CardTitle>
</CardHeader>
<CardContent>
{parts.length > 0 ? (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-12">#</TableHead>
<TableHead>Part</TableHead>
<TableHead>Description</TableHead>
<TableHead className="text-right">Qty</TableHead>
<TableHead className="text-right">Rate</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{parts.map((item, index) => {
const amount = (item.quantity ?? 0) * Number(item.rate ?? 0)
return (
<TableRow key={item.id ?? index}>
<TableCell className="text-muted-foreground">
{index + 1}
</TableCell>
<TableCell className="font-medium">
{item.part?.title ?? `Part #${item.part_id}`}
</TableCell>
<TableCell className="text-muted-foreground">
{item.description || "—"}
</TableCell>
<TableCell className="text-right">
{item.quantity ?? 0}
</TableCell>
<TableCell className="text-right">
{<Money value={item.rate} />}
</TableCell>
<TableCell className="text-right font-medium">
{<Money value={amount} />}
</TableCell>
</TableRow>
)
})}
<TableRow className="bg-muted/50 font-semibold">
<TableCell colSpan={5} className="text-right">
Total
</TableCell>
<TableCell className="text-right">
{<Money value={totalAmount} />}
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
) : (
<p className="text-sm text-muted-foreground text-center py-6">
No parts added to this purchase order.
</p>
)}
</CardContent>
</Card>
</div>
)
}