updates
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
"use client"
|
||||
|
||||
import { useAuthApi } from "@/shared/useApi"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/shared/components/ui/dropdown-menu"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/components/ui/dialog"
|
||||
import { ScrollArea } from "@/shared/components/ui/scroll-area"
|
||||
import { Ellipsis, Pencil, Trash2 } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import { EstimateForm } from "./estimate-form"
|
||||
|
||||
type EstimateActionsProps = {
|
||||
estimateId: string
|
||||
}
|
||||
|
||||
export function EstimateActions({ estimateId }: EstimateActionsProps) {
|
||||
const api = useAuthApi()
|
||||
const router = useRouter()
|
||||
const [editOpen, setEditOpen] = useState(false)
|
||||
|
||||
const handleDelete = async () => {
|
||||
await api.estimates.destroy(estimateId)
|
||||
router.push("/sales/estimates")
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Ellipsis className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setEditOpen(true)}>
|
||||
<Pencil className="size-4" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem variant="destructive" onClick={handleDelete}>
|
||||
<Trash2 className="size-4" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<Dialog open={editOpen} onOpenChange={setEditOpen}>
|
||||
<DialogContent className="min-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl font-bold">Edit Estimate</DialogTitle>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="max-h-[80vh] px-4">
|
||||
<EstimateForm
|
||||
resourceId={estimateId}
|
||||
onSuccess={() => {
|
||||
setEditOpen(false)
|
||||
router.refresh()
|
||||
}}
|
||||
/>
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client"
|
||||
|
||||
import { createContext, useContext } from "react"
|
||||
|
||||
type EstimateContextValue = {
|
||||
id: string
|
||||
label: string
|
||||
}
|
||||
|
||||
const EstimateContext = createContext<EstimateContextValue | null>(null)
|
||||
|
||||
export function EstimateProvider({
|
||||
estimate,
|
||||
children,
|
||||
}: {
|
||||
estimate: EstimateContextValue
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<EstimateContext.Provider value={estimate}>
|
||||
{children}
|
||||
</EstimateContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useEstimate() {
|
||||
return useContext(EstimateContext)
|
||||
}
|
||||
@@ -23,7 +23,9 @@ import {
|
||||
estimateFormSchema,
|
||||
type EstimateFormValues,
|
||||
} from "./estimate.schema"
|
||||
import { ESTIMATE_ROUTES, CUSTOMER_ROUTES, VEHICLE_ROUTES, DEPARTMENT_ROUTES, LABEL_ROUTES } from "@garage/api"
|
||||
import { ESTIMATE_ROUTES, DEPARTMENT_ROUTES, LABEL_ROUTES } from "@garage/api"
|
||||
import { RhfCustomerSelectField } from "@/modules/customers/rhf-customer-select-field"
|
||||
import { RhfVehicleSelectField } from "@/modules/vehicles/rhf-vehicle-select-field"
|
||||
|
||||
// ── Props ──
|
||||
|
||||
@@ -91,16 +93,6 @@ const mapLookupOption = (item: any) => ({
|
||||
label: item.name,
|
||||
})
|
||||
|
||||
const mapCustomerOption = (item: any) => ({
|
||||
value: String(item.id),
|
||||
label: [item.first_name, item.last_name].filter(Boolean).join(" "),
|
||||
})
|
||||
|
||||
const mapVehicleOption = (item: any) => ({
|
||||
value: String(item.id),
|
||||
label: [item.year, item.make_name, item.model_name].filter(Boolean).join(" "),
|
||||
})
|
||||
|
||||
const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label }
|
||||
|
||||
// ── Component ──
|
||||
@@ -157,24 +149,8 @@ export function EstimateForm({ resourceId, initialData, onSuccess }: EstimateFor
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<RhfAsyncSelectField
|
||||
name="customer"
|
||||
label="Customer"
|
||||
placeholder="Select customer"
|
||||
queryKey={[CUSTOMER_ROUTES.INDEX]}
|
||||
listFn={() => api.customers.list()}
|
||||
mapOption={mapCustomerOption}
|
||||
{...STORE_OBJECT}
|
||||
/>
|
||||
<RhfAsyncSelectField
|
||||
name="vehicle"
|
||||
label="Vehicle"
|
||||
placeholder="Select vehicle"
|
||||
queryKey={[VEHICLE_ROUTES.INDEX]}
|
||||
listFn={() => api.vehicles.list()}
|
||||
mapOption={mapVehicleOption}
|
||||
{...STORE_OBJECT}
|
||||
/>
|
||||
<RhfCustomerSelectField name="customer" />
|
||||
<RhfVehicleSelectField name="vehicle" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
import {
|
||||
FileText,
|
||||
Hash,
|
||||
Calendar,
|
||||
User,
|
||||
Car,
|
||||
Building2,
|
||||
Shield,
|
||||
Tag,
|
||||
MessageSquare,
|
||||
} from "lucide-react"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/shared/components/ui/card"
|
||||
import { Badge } from "@/shared/components/ui/badge"
|
||||
|
||||
type EstimateData = {
|
||||
id?: number
|
||||
title?: string
|
||||
estimate_number?: string
|
||||
date?: string
|
||||
customer_id?: number
|
||||
vehicle_id?: number
|
||||
department_id?: number
|
||||
has_insurance?: boolean
|
||||
enable_digital_authorisation?: boolean
|
||||
insurance_type_id?: string | number | null
|
||||
insurer_id?: string | number | null
|
||||
service_writer_id?: number
|
||||
footer?: string | null
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
labels?: {
|
||||
id?: number
|
||||
title?: string
|
||||
color_code?: string
|
||||
}[]
|
||||
customer_remarks?: {
|
||||
id?: number
|
||||
remark?: string
|
||||
created_at?: string
|
||||
}[]
|
||||
// Joined fields that may come from the API
|
||||
customer_name?: string
|
||||
vehicle_name?: string
|
||||
department_name?: string
|
||||
}
|
||||
|
||||
type EstimateGeneralInfoProps = {
|
||||
estimate: EstimateData
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
export function EstimateGeneralInfo({ estimate }: EstimateGeneralInfoProps) {
|
||||
return (
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileText className="size-4" /> Estimate Details
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<InfoItem icon={FileText} label="Title" value={estimate.title} />
|
||||
<InfoItem icon={Hash} label="Estimate #" value={estimate.estimate_number} />
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<InfoItem icon={Calendar} label="Date" value={estimate.date} />
|
||||
<InfoItem
|
||||
icon={Shield}
|
||||
label="Insurance"
|
||||
value={estimate.has_insurance ? "Yes" : "No"}
|
||||
/>
|
||||
</div>
|
||||
<InfoItem
|
||||
icon={Calendar}
|
||||
label="Created"
|
||||
value={
|
||||
estimate.created_at
|
||||
? new Date(estimate.created_at).toLocaleDateString()
|
||||
: null
|
||||
}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<User className="size-4" /> Related Records
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4">
|
||||
<InfoItem
|
||||
icon={User}
|
||||
label="Customer"
|
||||
value={estimate.customer_name || (estimate.customer_id ? `#${estimate.customer_id}` : null)}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={Car}
|
||||
label="Vehicle"
|
||||
value={estimate.vehicle_name || (estimate.vehicle_id ? `#${estimate.vehicle_id}` : null)}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={Building2}
|
||||
label="Department"
|
||||
value={estimate.department_name || (estimate.department_id ? `#${estimate.department_id}` : null)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{estimate.labels && estimate.labels.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Tag className="size-4" /> Labels
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{estimate.labels.map((label) => (
|
||||
<Badge
|
||||
key={label.id}
|
||||
style={label.color_code ? { backgroundColor: label.color_code } : undefined}
|
||||
>
|
||||
{label.title}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{estimate.customer_remarks && estimate.customer_remarks.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<MessageSquare className="size-4" /> Customer Remarks
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-3">
|
||||
{estimate.customer_remarks.map((remark) => (
|
||||
<div key={remark.id} className="rounded-md border p-3 text-sm">
|
||||
<p>{remark.remark}</p>
|
||||
{remark.created_at && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{new Date(remark.created_at).toLocaleDateString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{estimate.footer && (
|
||||
<Card className="md:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileText className="size-4" /> Footer
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm">{estimate.footer}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user