289 lines
12 KiB
TypeScript
289 lines
12 KiB
TypeScript
import {
|
|
ClipboardList,
|
|
Calendar,
|
|
Hash,
|
|
Building2,
|
|
Truck,
|
|
FileText,
|
|
Package,
|
|
DollarSign,
|
|
} 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"
|
|
|
|
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()
|
|
}
|
|
|
|
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(
|
|
(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="flex items-center gap-2 text-base">
|
|
<DollarSign className="size-4" />
|
|
Total: {formatCurrency(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">
|
|
{formatCurrency(item.rate)}
|
|
</TableCell>
|
|
<TableCell className="text-right font-medium">
|
|
{formatCurrency(amount)}
|
|
</TableCell>
|
|
</TableRow>
|
|
)
|
|
})}
|
|
<TableRow className="bg-muted/50 font-semibold">
|
|
<TableCell colSpan={5} className="text-right">
|
|
Total
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
{formatCurrency(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>
|
|
)
|
|
}
|