260 lines
9.9 KiB
TypeScript
260 lines
9.9 KiB
TypeScript
import {
|
|
ClipboardList,
|
|
Calendar,
|
|
Hash,
|
|
Users,
|
|
Car,
|
|
Gauge,
|
|
Clock,
|
|
UserCheck,
|
|
Briefcase,
|
|
Receipt,
|
|
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 { JobCardActions } from "./job-card-actions"
|
|
import { JobCardRemarksList } from "./job-card-remarks-list"
|
|
import { JobCardRecommendationsList } from "./job-card-recommendations-list"
|
|
import { getVehicleLabel } from "../vehicles/utils/getVehicleLabel"
|
|
import { getFullName } from "@/shared/utils/getFullName"
|
|
import { CrudShowResponse, JobCardsClient, PAYMENT_RECEIVED_ROUTES, PaymentReceivedClient } from "@garage/api"
|
|
import { ResourcePage } from "@/shared/data-view/resource-page"
|
|
import PaymentReceivedPage from "@/app/(authenticated)/sales/payment-received/page"
|
|
import JobCardPaymentsReceived from "./job-card-payments-received"
|
|
import { formatDate } from "@/shared/utils/formatters"
|
|
import type { JobCardShowData } from "@garage/api"
|
|
|
|
type JobCard = JobCardShowData
|
|
|
|
|
|
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>
|
|
)
|
|
}
|
|
|
|
const statusColorMap: Record<string, string> = {
|
|
draft: "secondary",
|
|
check_in: "default",
|
|
in_progress: "default",
|
|
completed: "default",
|
|
invoiced: "outline",
|
|
cancelled: "destructive",
|
|
}
|
|
|
|
export function JobCardGeneralInfo({ jobCard }: { jobCard: JobCard }) {
|
|
const formatStatus = (status?: string) => {
|
|
if (!status) return null
|
|
return status
|
|
.split("_")
|
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
.join(" ")
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col gap-6">
|
|
<JobCardActions
|
|
jobCardId={String(jobCard.id )}
|
|
orderDate={jobCard.order_date ?? null}
|
|
serviceWriterName={jobCard.service_writer?.first_name}
|
|
salesPersonName={jobCard.sales_person?.first_name}
|
|
primaryTechnicianName={jobCard.primary_technician?.first_name}
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div className="grid gap-6 md:grid-cols-2">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<ClipboardList className="size-4" />
|
|
Job Card Details
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="grid gap-4">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
{jobCard.title && (
|
|
<Badge variant="secondary">{jobCard.title}</Badge>
|
|
)}
|
|
{jobCard.status && (
|
|
<Badge variant={statusColorMap[jobCard.status] as any ?? "outline"}>
|
|
{formatStatus(jobCard.status)}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<Separator />
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<InfoItem
|
|
icon={Calendar}
|
|
label="Check-in Date"
|
|
value={formatDate(jobCard.check_in_date)}
|
|
/>
|
|
<InfoItem
|
|
icon={Gauge}
|
|
label="KM In"
|
|
value={jobCard.km_in ? Number(jobCard.km_in).toLocaleString() : null}
|
|
/>
|
|
<InfoItem
|
|
icon={Clock}
|
|
label="Created"
|
|
value={jobCard.created_at ? new Date(jobCard.created_at).toLocaleDateString() : null}
|
|
/>
|
|
<InfoItem
|
|
icon={Clock}
|
|
label="Updated"
|
|
value={jobCard.updated_at ? new Date(jobCard.updated_at).toLocaleDateString() : null}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Related Information */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Users className="size-4" />
|
|
Related Information
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="grid gap-4">
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<InfoItem
|
|
icon={Users}
|
|
label="Customer"
|
|
value={getFullName(jobCard?.customer)}
|
|
/>
|
|
<InfoItem
|
|
icon={Car}
|
|
label="Vehicle"
|
|
value={getVehicleLabel(jobCard?.vehicle as any)}
|
|
/>
|
|
{/* <InfoItem
|
|
icon={Building2}
|
|
label="Department"
|
|
value={jobCard?.department?.name as any}
|
|
/> */}
|
|
<InfoItem
|
|
icon={Briefcase}
|
|
label="Sales Person"
|
|
value={getFullName(jobCard.sales_person as any)}
|
|
/>
|
|
<InfoItem
|
|
icon={UserCheck}
|
|
label="Service Writer"
|
|
value={getFullName(jobCard?.service_writer as any)}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Tax & Discount Settings */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<DollarSign className="size-4" />
|
|
Tax & Discount Settings
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="grid gap-4 sm:grid-cols-2">
|
|
<InfoItem
|
|
icon={Receipt}
|
|
label="Tax Inclusive"
|
|
value={jobCard.tax_inclusive}
|
|
/>
|
|
<InfoItem
|
|
icon={DollarSign}
|
|
label="Discount Type"
|
|
value={formatStatus(jobCard.discount_type)}
|
|
/>
|
|
<InfoItem
|
|
icon={DollarSign}
|
|
label="Discount At"
|
|
value={formatStatus(jobCard.discount_at)}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Counts */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Hash className="size-4" />
|
|
Related Counts
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="grid gap-4 sm:grid-cols-2">
|
|
<InfoItem
|
|
icon={Receipt}
|
|
label="Purchase Orders"
|
|
value={String(jobCard.purchase_orders_count ?? 0)}
|
|
/>
|
|
<InfoItem
|
|
icon={Receipt}
|
|
label="Bills"
|
|
value={String(jobCard.bills_count ?? 0)}
|
|
/>
|
|
<InfoItem
|
|
icon={DollarSign}
|
|
label="Expenses"
|
|
value={String(jobCard.expenses_count ?? 0)}
|
|
/>
|
|
<InfoItem
|
|
icon={ClipboardList}
|
|
label="Tasks"
|
|
value={String(jobCard.tasks_count ?? 0)}
|
|
/>
|
|
<InfoItem
|
|
icon={Calendar}
|
|
label="Appointments"
|
|
value={String(jobCard.appointments_count ?? 0)}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<JobCardPaymentsReceived />
|
|
|
|
|
|
<div className="grid gap-6 md:grid-cols-2">
|
|
<JobCardRemarksList
|
|
jobCardId={String(jobCard.id)}
|
|
remarks={(jobCard as any).customer_remarks ?? []}
|
|
/>
|
|
<JobCardRecommendationsList
|
|
jobCardId={String(jobCard.id)}
|
|
recommendations={(jobCard as any).shop_recommendations ?? []}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|