241 lines
8.9 KiB
TypeScript
241 lines
8.9 KiB
TypeScript
"use client"
|
|
|
|
import { use, useState } from "react"
|
|
import { useRouter } from "next/navigation"
|
|
import { useQuery, useQueryClient } from "@tanstack/react-query"
|
|
import { useAuthApi } from "@/shared/useApi"
|
|
import { ColumnHeader, DataTable } from "@/shared/data-view/table-view"
|
|
import { Button } from "@/shared/components/ui/button"
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "@/shared/components/ui/dialog"
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "@/shared/components/ui/dropdown-menu"
|
|
import { confirm } from "@/shared/components/confirm-dialog"
|
|
import { toast } from "sonner"
|
|
import { Ellipsis, Plus } from "lucide-react"
|
|
import type { ColumnDef } from "@tanstack/react-table"
|
|
import { JobCardServiceForm } from "@/modules/job-cards/job-card-service-form"
|
|
import { formatDate } from "@/shared/utils/formatters"
|
|
import { Badge } from "@/shared/components/ui/badge"
|
|
|
|
// TODO: services invalidation is not working properly when create new service line. Need to investigate why and fix it.
|
|
|
|
export default function JobCardServicesPage({
|
|
params,
|
|
}: {
|
|
params: Promise<{ id: string }>
|
|
}) {
|
|
const { id: jobCardId } = use(params)
|
|
const api = useAuthApi()
|
|
const queryClient = useQueryClient()
|
|
const router = useRouter()
|
|
const queryKey = ["job-card-services", jobCardId]
|
|
|
|
const [dialogOpen, setDialogOpen] = useState(false)
|
|
const [editItem, setEditItem] = useState<any | null>(null)
|
|
|
|
const { data, isLoading } = useQuery({
|
|
queryKey,
|
|
queryFn: () => api.jobCards.getServices(jobCardId),
|
|
})
|
|
|
|
const rows = (data as any)?.data ?? []
|
|
|
|
const invalidate = () => queryClient.invalidateQueries({ queryKey , refetchType:'all'}).then(() => router.refresh())
|
|
|
|
async function handleDelete(row: any) {
|
|
const confirmed = await confirm({
|
|
title: "Delete this service?",
|
|
description: `Remove service "${row.service?.labor_name ?? "this service"}" from the job card?`,
|
|
})
|
|
if (!confirmed) return
|
|
const promise = api.jobCards.deleteService(jobCardId, row.id)
|
|
toast.promise(promise, {
|
|
loading: "Deleting...",
|
|
success: "Service deleted",
|
|
error: "Failed to delete service",
|
|
})
|
|
await promise
|
|
invalidate()
|
|
}
|
|
|
|
const columns: ColumnDef<any>[] = [
|
|
{
|
|
accessorKey: "service.labor_name",
|
|
header: ({ column }) => <ColumnHeader column={column} title="Service" />,
|
|
cell: ({ row }) => {
|
|
const service = row.original.service
|
|
return service ? (
|
|
<div>
|
|
<span className="font-medium">{service.labor_name}</span>
|
|
{service.service_code && (
|
|
<span className="ml-2 text-xs text-muted-foreground">{service.service_code}</span>
|
|
)}
|
|
</div>
|
|
) : "—"
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "rate_type",
|
|
header: ({ column }) => <ColumnHeader column={column} title="Rate Type" />,
|
|
cell: ({ row }) => {
|
|
const val = row.original.rate_type
|
|
if (!val) return "—"
|
|
return (
|
|
<Badge variant="outline">
|
|
{val === "flat_rate" ? "Flat Rate" : val === "hourly" ? "Hourly" : val}
|
|
</Badge>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "quantity",
|
|
header: ({ column }) => <ColumnHeader column={column} title="Qty" />,
|
|
cell: ({ row }) => row.original.quantity ?? "—",
|
|
},
|
|
{
|
|
accessorKey: "rate",
|
|
header: ({ column }) => <ColumnHeader column={column} title="Rate" />,
|
|
cell: ({ row }) => {
|
|
const val = row.original.rate
|
|
return val != null ? `$${Number(val).toFixed(2)}` : "—"
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "labor_rate.title",
|
|
header: ({ column }) => <ColumnHeader column={column} title="Labor Rate" />,
|
|
cell: ({ row }) => {
|
|
const lr = row.original.labor_rate
|
|
return lr ? `${lr.title} ($${Number(lr.rate).toFixed(2)})` : "—"
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "working_hours",
|
|
header: ({ column }) => <ColumnHeader column={column} title="Working Hrs" />,
|
|
cell: ({ row }) => row.original.working_hours ?? "—",
|
|
},
|
|
{
|
|
accessorKey: "labor_hours",
|
|
header: ({ column }) => <ColumnHeader column={column} title="Labor Hrs" />,
|
|
cell: ({ row }) => row.original.labor_hours ?? "—",
|
|
},
|
|
{
|
|
accessorKey: "discount_amount",
|
|
header: ({ column }) => <ColumnHeader column={column} title="Discount" />,
|
|
cell: ({ row }) => {
|
|
const val = row.original.discount_amount
|
|
return val != null && val > 0 ? `$${Number(val).toFixed(2)}` : "—"
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "tax_id",
|
|
header: ({ column }) => <ColumnHeader column={column} title="Tax" />,
|
|
cell: ({ row }) => row.original.tax_id ?? "—",
|
|
},
|
|
{
|
|
accessorKey: "department.name",
|
|
header: ({ column }) => <ColumnHeader column={column} title="Department" />,
|
|
cell: ({ row }) => row.original.department?.name || "—",
|
|
},
|
|
{
|
|
accessorKey: "description",
|
|
header: ({ column }) => <ColumnHeader column={column} title="Description" />,
|
|
cell: ({ row }) => row.original.description || "—",
|
|
},
|
|
{
|
|
accessorKey: "created_at",
|
|
header: ({ column }) => <ColumnHeader column={column} title="Added" />,
|
|
cell: ({ row }) => formatDate(row.original.created_at),
|
|
},
|
|
{
|
|
id: "actions",
|
|
cell: ({ row }) => (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="icon">
|
|
<Ellipsis className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem
|
|
onClick={() => {
|
|
setEditItem(row.original)
|
|
setDialogOpen(true)
|
|
}}
|
|
>
|
|
Edit
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
className="text-destructive"
|
|
onClick={() => handleDelete(row.original)}
|
|
>
|
|
Delete
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
),
|
|
},
|
|
]
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4 p-4">
|
|
<div className="flex justify-end">
|
|
<Dialog
|
|
open={dialogOpen}
|
|
onOpenChange={(open) => {
|
|
setDialogOpen(open)
|
|
if (!open) setEditItem(null)
|
|
}}
|
|
>
|
|
<DialogTrigger asChild>
|
|
<Button onClick={() => setEditItem(null)}>
|
|
<Plus className="me-2 h-4 w-4" />
|
|
Add Service
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="max-w-2xl">
|
|
<DialogHeader>
|
|
<DialogTitle>{editItem ? "Edit Service" : "Add Service"}</DialogTitle>
|
|
</DialogHeader>
|
|
<JobCardServiceForm
|
|
jobCardId={jobCardId}
|
|
jobCardServiceId={editItem?.id ?? null}
|
|
initialData={editItem}
|
|
onSuccess={() => {
|
|
setDialogOpen(false)
|
|
setEditItem(null)
|
|
invalidate()
|
|
}}
|
|
onCancel={() => {
|
|
setDialogOpen(false)
|
|
setEditItem(null)
|
|
}}
|
|
/>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
|
|
<DataTable
|
|
columns={columns}
|
|
data={rows}
|
|
pagination={{
|
|
page: 1,
|
|
pageSize: rows.length || 15,
|
|
pageCount: 1,
|
|
total: rows.length,
|
|
}}
|
|
isLoading={isLoading}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|