updates
This commit is contained in:
@@ -35,22 +35,23 @@ export default function JobCardAppointmentsPage({
|
||||
|
||||
return (
|
||||
<ResourcePage<AppointmentsClient>
|
||||
pageTitle="Appointments"
|
||||
routeKey={APPOINTMENT_ROUTES.INDEX}
|
||||
getClient={(api) => api.appointments}
|
||||
extraParams={{ job_card_id: jobCardId }}
|
||||
header={null}
|
||||
onRowClick={(row) => router.push(`/calendar/appointment/${(row as any).id}`)}
|
||||
toolbar={({ invalidateQuery, selectedItem, closeDialog }) => (
|
||||
<FormDialog title="Appointment">
|
||||
{(resourceId) => (
|
||||
<AppointmentForm
|
||||
resourceId={resourceId}
|
||||
initialData={selectedItem ?? { job_card: defaultJobCard }}
|
||||
onSuccess={() => { closeDialog(); invalidateQuery() }}
|
||||
/>
|
||||
)}
|
||||
</FormDialog>
|
||||
tableHeader={({ invalidateQuery, selectedItem, closeDialog }) => (
|
||||
<div className="flex justify-end">
|
||||
<FormDialog title="Appointment">
|
||||
{(resourceId) => (
|
||||
<AppointmentForm
|
||||
resourceId={resourceId}
|
||||
initialData={selectedItem ?? { job_card: defaultJobCard }}
|
||||
onSuccess={() => { closeDialog(); invalidateQuery() }}
|
||||
/>
|
||||
)}
|
||||
</FormDialog>
|
||||
</div>
|
||||
)}
|
||||
columns={({ actionsColumn }) => [
|
||||
{
|
||||
|
||||
@@ -85,27 +85,24 @@ export default function JobCardAttachmentsPage() {
|
||||
return (
|
||||
<DashboardPage
|
||||
header={null}
|
||||
toolbar={
|
||||
<div className="flex items-center justify-end">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handleUpload}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isUploading}
|
||||
>
|
||||
<Plus className="size-4" />
|
||||
{isUploading ? "Uploading..." : "Upload Attachment"}
|
||||
</Button>
|
||||
</div>}
|
||||
title="Attachments"
|
||||
>
|
||||
|
||||
|
||||
<div className="flex items-center justify-end mb-4">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handleUpload}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isUploading}
|
||||
>
|
||||
<Plus className="size-4" />
|
||||
{isUploading ? "Uploading..." : "Upload Attachment"}
|
||||
</Button>
|
||||
</div>
|
||||
{attachments?.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="py-8 text-center text-muted-foreground">
|
||||
@@ -114,6 +111,7 @@ export default function JobCardAttachmentsPage() {
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
|
||||
{attachments?.map((attachment) => {
|
||||
const Icon = getFileIcon(attachment.attachment_path)
|
||||
return (
|
||||
|
||||
@@ -24,21 +24,23 @@ export default function JobCardBillsPage({
|
||||
|
||||
return (
|
||||
<ResourcePage<BillsClient>
|
||||
pageTitle="Bills"
|
||||
routeKey={BILL_ROUTES.INDEX}
|
||||
getClient={(api) => api.bills}
|
||||
extraParams={{ job_card_id: jobCardId }}
|
||||
header={null}
|
||||
toolbar={({ invalidateQuery, selectedItem, closeDialog }) => (
|
||||
<FormDialog title="Bill">
|
||||
{(resourceId) => (
|
||||
<BillForm
|
||||
resourceId={resourceId}
|
||||
initialData={selectedItem ?? { job_card: defaultJobCard }}
|
||||
onSuccess={() => { closeDialog(); invalidateQuery() }}
|
||||
/>
|
||||
)}
|
||||
</FormDialog>
|
||||
tableHeader={({ invalidateQuery, selectedItem, closeDialog }) => (
|
||||
<div className="flex justify-end">
|
||||
|
||||
<FormDialog title="Bill">
|
||||
{(resourceId) => (
|
||||
<BillForm
|
||||
resourceId={resourceId}
|
||||
initialData={selectedItem ?? { job_card: defaultJobCard }}
|
||||
onSuccess={() => { closeDialog(); invalidateQuery() }}
|
||||
/>
|
||||
)}
|
||||
</FormDialog>
|
||||
</div>
|
||||
)}
|
||||
columns={({ actionsColumn }) => [
|
||||
{
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useParams } from "next/navigation"
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
import { type ColumnDef } from "@tanstack/react-table"
|
||||
import { useState } from "react"
|
||||
import { Plus, Trash2 } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { useAuthApi } from "@/shared/useApi"
|
||||
import { DataTable, ColumnHeader } from "@/shared/data-view/table-view"
|
||||
import { confirm } from "@/shared/components/confirm-dialog"
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import { Card, CardContent } from "@/shared/components/ui/card"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/components/ui/dialog"
|
||||
import { JobCardRemarkForm } from "@/modules/job-cards/job-card-remark-form"
|
||||
import DashboardPage from "@/base/components/layout/dashboard/dashboard-page"
|
||||
|
||||
type CustomerRemark = {
|
||||
id: number
|
||||
job_card_id?: number
|
||||
remark?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export default function CustomerRemarksPage() {
|
||||
const { id: jobCardId } = useParams<{ id: string }>()
|
||||
const api = useAuthApi()
|
||||
const queryClient = useQueryClient()
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
|
||||
const queryKey = ["job-card-remarks", jobCardId]
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey,
|
||||
queryFn: async () => {
|
||||
// const result = await api.jobCards.
|
||||
},
|
||||
})
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: () => api.jobCards.deleteCustomerRemark(jobCardId),
|
||||
onSuccess: () => {
|
||||
toast.success("Customer remark deleted successfully.")
|
||||
queryClient.invalidateQueries({ queryKey })
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Failed to delete customer remark.")
|
||||
},
|
||||
})
|
||||
|
||||
const handleDelete = async (remark: CustomerRemark) => {
|
||||
const confirmed = await confirm({
|
||||
title: "Delete Customer Remark",
|
||||
description: "Are you sure you want to delete this remark?",
|
||||
confirmLabel: "Delete",
|
||||
variant: "destructive",
|
||||
})
|
||||
if (confirmed) {
|
||||
deleteMutation.mutate()
|
||||
}
|
||||
}
|
||||
|
||||
const columns: ColumnDef<CustomerRemark>[] = [
|
||||
{
|
||||
accessorKey: "remark",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Remark" />,
|
||||
cell: ({ getValue }) => {
|
||||
const val = getValue<string>()
|
||||
return val || "—"
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "created_at",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Created" />,
|
||||
cell: ({ getValue }) => {
|
||||
const val = getValue<string>()
|
||||
return val ? new Date(val).toLocaleDateString() : "—"
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => <span className="sr-only">Actions</span>,
|
||||
cell: ({ row }) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => handleDelete(row.original)}
|
||||
title="Delete remark"
|
||||
>
|
||||
<Trash2 className="size-4 text-destructive" />
|
||||
</Button>
|
||||
),
|
||||
enableSorting: false,
|
||||
},
|
||||
]
|
||||
|
||||
const remarks = Array.isArray(data) ? data : []
|
||||
|
||||
const pagination = {
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
pageCount: 1,
|
||||
total: remarks.length,
|
||||
}
|
||||
|
||||
return (
|
||||
<DashboardPage header={null} title="Customer Remarks" toolbar={ <div className="flex items-center justify-end">
|
||||
<Button onClick={() => setDialogOpen(true)}>
|
||||
<Plus className="size-4" />
|
||||
Add Customer Remark
|
||||
</Button>
|
||||
</div>}>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
|
||||
|
||||
<Card>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={remarks}
|
||||
pagination={pagination}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Customer Remark</DialogTitle>
|
||||
</DialogHeader>
|
||||
<JobCardRemarkForm
|
||||
jobCardId={jobCardId}
|
||||
onSuccess={() => {
|
||||
setDialogOpen(false)
|
||||
queryClient.invalidateQueries({ queryKey })
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</DashboardPage>
|
||||
|
||||
)
|
||||
}
|
||||
@@ -24,12 +24,13 @@ export default function JobCardExpensesPage({
|
||||
|
||||
return (
|
||||
<ResourcePage<ExpensesClient>
|
||||
pageTitle="Expenses"
|
||||
routeKey={EXPENSE_ROUTES.INDEX}
|
||||
routeKey={EXPENSE_ROUTES.INDEX}
|
||||
getClient={(api) => api.expenses}
|
||||
extraParams={{ job_card_id: jobCardId }}
|
||||
header={null}
|
||||
toolbar={({ invalidateQuery, selectedItem, closeDialog }) => (
|
||||
tableHeader={({ invalidateQuery, selectedItem, closeDialog }) => (
|
||||
<div className="flex justify-end">
|
||||
|
||||
<FormDialog title="Expense">
|
||||
{(resourceId) => (
|
||||
<ExpenseForm
|
||||
@@ -39,6 +40,7 @@ export default function JobCardExpensesPage({
|
||||
/>
|
||||
)}
|
||||
</FormDialog>
|
||||
</div>
|
||||
)}
|
||||
columns={({ actionsColumn }) => [
|
||||
{
|
||||
|
||||
@@ -19,7 +19,7 @@ export default async function JobCardDetailLayout(props: { params: Promise<{ id:
|
||||
const docs = jobCard?.documents
|
||||
|
||||
return (
|
||||
<JobCardProvider jobCard={{ ...jobCard, label: title }}>
|
||||
<JobCardProvider jobCard={{ ...jobCard }}>
|
||||
<DashboardDetailsPage
|
||||
className='p-0 lg:p-0'
|
||||
title={title}
|
||||
@@ -35,14 +35,6 @@ export default async function JobCardDetailLayout(props: { params: Promise<{ id:
|
||||
href: `/sales/job-cards/${id}`,
|
||||
label: 'Details'
|
||||
},
|
||||
{
|
||||
href: `/sales/job-cards/${id}/customer-remarks`,
|
||||
label: 'Customer Remarks'
|
||||
},
|
||||
{
|
||||
href: `/sales/job-cards/${id}/shop-recommendations`,
|
||||
label: 'Shop Recommendations'
|
||||
},
|
||||
{
|
||||
href: `/sales/job-cards/${id}/attachments`,
|
||||
label: `Attachments (${docs?.length || 0})`
|
||||
@@ -67,6 +59,10 @@ export default async function JobCardDetailLayout(props: { params: Promise<{ id:
|
||||
href: `/sales/job-cards/${id}/tasks`,
|
||||
label: `Tasks (${jobCard?.tasks_count || 0})`
|
||||
},
|
||||
{
|
||||
href: `/sales/job-cards/${id}/parts`,
|
||||
label: `Parts (${jobCard?.parts_count || 0})`
|
||||
},
|
||||
]}
|
||||
>
|
||||
{props.children}
|
||||
|
||||
@@ -13,7 +13,7 @@ export default async function JobCardDetailPage(props: { params: Promise<{ id: s
|
||||
}
|
||||
|
||||
return (
|
||||
<DashboardPage header={null}>
|
||||
<DashboardPage header={null}>
|
||||
<JobCardGeneralInfo jobCard={data} />
|
||||
</DashboardPage>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
"use client"
|
||||
|
||||
import { use, useState } from "react"
|
||||
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 { JobCardPartForm } from "@/modules/job-cards/job-card-part-form"
|
||||
import { formatDate } from "@/shared/utils/formatters"
|
||||
|
||||
export default function JobCardPartsPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>
|
||||
}) {
|
||||
const { id: jobCardId } = use(params)
|
||||
const api = useAuthApi()
|
||||
const queryClient = useQueryClient()
|
||||
const queryKey = ["job-card-parts", jobCardId]
|
||||
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
const [editItem, setEditItem] = useState<any | null>(null)
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey,
|
||||
queryFn: () => api.jobCards.getParts(jobCardId),
|
||||
})
|
||||
|
||||
const rows = (data as any)?.data ?? []
|
||||
|
||||
const invalidate = () => queryClient.invalidateQueries({ queryKey })
|
||||
|
||||
async function handleDelete(row: any) {
|
||||
const confirmed = await confirm({
|
||||
title: "Delete this part?",
|
||||
description: `Remove part "${row.part?.title ?? "this part"}" from the job card?`,
|
||||
})
|
||||
if (!confirmed) return
|
||||
const promise = api.jobCards.deletePart(jobCardId, row.id)
|
||||
toast.promise(promise, {
|
||||
loading: "Deleting...",
|
||||
success: "Part deleted",
|
||||
error: "Failed to delete part",
|
||||
})
|
||||
await promise
|
||||
invalidate()
|
||||
}
|
||||
|
||||
const columns: ColumnDef<any>[] = [
|
||||
{
|
||||
accessorKey: "part.title",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Part" />,
|
||||
cell: ({ row }) => {
|
||||
const part = row.original.part
|
||||
return part ? (
|
||||
<div>
|
||||
<span className="font-medium">{part.title}</span>
|
||||
{part.sku && (
|
||||
<span className="ml-2 text-xs text-muted-foreground">{part.sku}</span>
|
||||
)}
|
||||
</div>
|
||||
) : "—"
|
||||
},
|
||||
},
|
||||
{
|
||||
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: "tax",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Tax" />,
|
||||
cell: ({ row }) => row.original.tax || "—",
|
||||
},
|
||||
{
|
||||
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 Part
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editItem ? "Edit Part" : "Add Part"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<JobCardPartForm
|
||||
jobCardId={jobCardId}
|
||||
jobCardPartId={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>
|
||||
)
|
||||
}
|
||||
@@ -23,12 +23,13 @@ export default function JobCardPurchaseOrdersPage({
|
||||
|
||||
return (
|
||||
<ResourcePage<PurchaseOrdersClient>
|
||||
pageTitle="Purchase Orders"
|
||||
routeKey={PURCHASE_ORDER_ROUTES.INDEX}
|
||||
getClient={(api) => api.purchaseOrders}
|
||||
extraParams={{ job_card_id: jobCardId }}
|
||||
header={null}
|
||||
toolbar={({ invalidateQuery, selectedItem, closeDialog }) => (
|
||||
tableHeader={({ invalidateQuery, selectedItem, closeDialog }) => (
|
||||
<div className="flex justify-end">
|
||||
|
||||
<FormDialog title="Purchase Order">
|
||||
{(resourceId) => (
|
||||
<PurchaseOrderForm
|
||||
@@ -38,6 +39,8 @@ export default function JobCardPurchaseOrdersPage({
|
||||
/>
|
||||
)}
|
||||
</FormDialog>
|
||||
</div>
|
||||
|
||||
)}
|
||||
columns={({ actionsColumn }) => [
|
||||
{
|
||||
|
||||
-157
@@ -1,157 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useParams } from "next/navigation"
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
import { type ColumnDef } from "@tanstack/react-table"
|
||||
import { useState } from "react"
|
||||
import { Plus, Trash2 } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { useAuthApi } from "@/shared/useApi"
|
||||
import { DataTable, ColumnHeader } from "@/shared/data-view/table-view"
|
||||
import { confirm } from "@/shared/components/confirm-dialog"
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import { Card, CardContent } from "@/shared/components/ui/card"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/components/ui/dialog"
|
||||
import { JobCardRecommendationForm } from "@/modules/job-cards/job-card-recommendation-form"
|
||||
import DashboardPage from "@/base/components/layout/dashboard/dashboard-page"
|
||||
|
||||
type ShopRecommendation = {
|
||||
id: number
|
||||
job_card_id?: number
|
||||
recommendation?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export default function ShopRecommendationsPage() {
|
||||
const { id: jobCardId } = useParams<{ id: string }>()
|
||||
const api = useAuthApi()
|
||||
const queryClient = useQueryClient()
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
|
||||
const queryKey = ["job-card-recommendations", jobCardId]
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey,
|
||||
queryFn: async () => {
|
||||
const result = await api.jobCards.show(jobCardId)
|
||||
const d = (result as any)?.data ?? result
|
||||
return d?.shop_recommendations ?? []
|
||||
},
|
||||
})
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: () => api.jobCards.deleteShopRecommendation(jobCardId),
|
||||
onSuccess: () => {
|
||||
toast.success("Shop recommendation deleted successfully.")
|
||||
queryClient.invalidateQueries({ queryKey })
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Failed to delete shop recommendation.")
|
||||
},
|
||||
})
|
||||
|
||||
const handleDelete = async (rec: ShopRecommendation) => {
|
||||
const confirmed = await confirm({
|
||||
title: "Delete Shop Recommendation",
|
||||
description: "Are you sure you want to delete this recommendation?",
|
||||
confirmLabel: "Delete",
|
||||
variant: "destructive",
|
||||
})
|
||||
if (confirmed) {
|
||||
deleteMutation.mutate()
|
||||
}
|
||||
}
|
||||
|
||||
const columns: ColumnDef<ShopRecommendation>[] = [
|
||||
{
|
||||
accessorKey: "recommendation",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Recommendation" />,
|
||||
cell: ({ getValue }) => {
|
||||
const val = getValue<string>()
|
||||
return val || "—"
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "created_at",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Created" />,
|
||||
cell: ({ getValue }) => {
|
||||
const val = getValue<string>()
|
||||
return val ? new Date(val).toLocaleDateString() : "—"
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => <span className="sr-only">Actions</span>,
|
||||
cell: ({ row }) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => handleDelete(row.original)}
|
||||
title="Delete recommendation"
|
||||
>
|
||||
<Trash2 className="size-4 text-destructive" />
|
||||
</Button>
|
||||
),
|
||||
enableSorting: false,
|
||||
},
|
||||
]
|
||||
|
||||
const recommendations = Array.isArray(data) ? data : []
|
||||
|
||||
const pagination = {
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
pageCount: 1,
|
||||
total: recommendations.length,
|
||||
}
|
||||
|
||||
return (
|
||||
<DashboardPage
|
||||
header={null}
|
||||
title="Shop Recommendations"
|
||||
toolbar={
|
||||
<Button onClick={() => setDialogOpen(true)}>
|
||||
<Plus className="size-4 me-2" />
|
||||
Add Recommendation
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
|
||||
|
||||
<Card>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={recommendations}
|
||||
pagination={pagination}
|
||||
sorting={[]}
|
||||
onChange={() => {}}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Shop Recommendation</DialogTitle>
|
||||
</DialogHeader>
|
||||
<JobCardRecommendationForm
|
||||
jobCardId={jobCardId}
|
||||
onSuccess={() => {
|
||||
setDialogOpen(false)
|
||||
queryClient.invalidateQueries({ queryKey })
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</DashboardPage>
|
||||
)
|
||||
}
|
||||
@@ -3,9 +3,13 @@
|
||||
import { use } from "react"
|
||||
import { ResourcePage } from "@/shared/data-view/resource-page"
|
||||
import { ColumnHeader } from "@/shared/data-view/table-view"
|
||||
import FormDialog from "@/shared/components/form-dialog"
|
||||
import { TaskForm } from "@/modules/tasks/task-form"
|
||||
import { TASK_ROUTES } from "@garage/api"
|
||||
import type { TasksClient } from "@garage/api"
|
||||
import { Badge } from "@/shared/components/ui/badge"
|
||||
import { useJobCard } from "@/modules/job-cards/job-card-context"
|
||||
import { formatDate, formatEnum } from "@/shared/utils/formatters"
|
||||
|
||||
export default function JobCardTasksPage({
|
||||
params,
|
||||
@@ -13,54 +17,58 @@ export default function JobCardTasksPage({
|
||||
params: Promise<{ id: string }>
|
||||
}) {
|
||||
const { id: jobCardId } = use(params)
|
||||
const jobCard = useJobCard()
|
||||
|
||||
const defaultJobCard = jobCard
|
||||
? { value: String((jobCard as any).id), label: (jobCard as any).title || `Job Card` }
|
||||
: null
|
||||
|
||||
return (
|
||||
<ResourcePage<TasksClient>
|
||||
pageTitle="Tasks"
|
||||
routeKey={TASK_ROUTES.TASKS}
|
||||
routeKey={TASK_ROUTES.INDEX}
|
||||
getClient={(api) => api.tasks}
|
||||
extraParams={{ job_card_id: jobCardId }}
|
||||
header={null}
|
||||
columns={() => [
|
||||
tableHeader={({ invalidateQuery, selectedItem, closeDialog }) => (
|
||||
<div className="flex justify-end">
|
||||
<FormDialog title="Task">
|
||||
{(resourceId) => (
|
||||
<TaskForm
|
||||
resourceId={resourceId}
|
||||
initialData={selectedItem ?? { job_card: defaultJobCard }}
|
||||
onSuccess={() => { closeDialog(); invalidateQuery() }}
|
||||
/>
|
||||
)}
|
||||
</FormDialog>
|
||||
</div>
|
||||
)}
|
||||
columns={({ actionsColumn }) => [
|
||||
{
|
||||
accessorKey: "title",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Task" />,
|
||||
cell: ({ row }) => (row.original as any).title || "—",
|
||||
accessorKey: "task_number",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Task #" />,
|
||||
cell: ({ row }) => (row.original as any).task_number || "—",
|
||||
},
|
||||
{
|
||||
accessorKey: "task_type_name",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Type" />,
|
||||
cell: ({ row }) => (row.original as any).task_type_name || "—",
|
||||
},
|
||||
{
|
||||
accessorKey: "section_name",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Section" />,
|
||||
cell: ({ row }) => (row.original as any).section_name || "—",
|
||||
accessorKey: "subject",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Subject" />,
|
||||
cell: ({ row }) => (row.original as any).subject || "—",
|
||||
},
|
||||
{
|
||||
accessorKey: "due_date",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Due Date" />,
|
||||
cell: ({ row }) => {
|
||||
const val = (row.original as any).due_date
|
||||
return val ? new Date(val).toLocaleDateString() : "—"
|
||||
},
|
||||
cell: ({ row }) => formatDate((row.original as any).due_date),
|
||||
},
|
||||
{
|
||||
accessorKey: "priority",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Priority" />,
|
||||
cell: ({ row }) => {
|
||||
const value = (row.original as any).priority
|
||||
return value ? <Badge variant="outline">{value}</Badge> : "—"
|
||||
},
|
||||
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Status" />,
|
||||
cell: ({ row }) => {
|
||||
const value = (row.original as any).status
|
||||
return value ? <Badge>{value}</Badge> : "—"
|
||||
},
|
||||
|
||||
},
|
||||
actionsColumn(),
|
||||
]}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Badge } from '@/shared/components/ui/badge'
|
||||
import { Input } from '@/shared/components/ui/input'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { formatDate, formatEnum, formatNumber } from '@/shared/utils/formatters'
|
||||
|
||||
type JobCardItem = {
|
||||
id: number
|
||||
@@ -31,19 +32,11 @@ const statusColorMap: Record<string, string> = {
|
||||
cancelled: "destructive",
|
||||
}
|
||||
|
||||
const formatStatus = (status?: string) => {
|
||||
if (!status) return "—"
|
||||
return status
|
||||
.split("_")
|
||||
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
export default function JobCardsPage() {
|
||||
const router = useRouter()
|
||||
const [searchInput, setSearchInput] = useState("")
|
||||
const [search, setSearch] = useState("")
|
||||
const [statusFilter, setStatusFilter] = useState<string>("all")
|
||||
const [statusFilter, setStatusFilter] = useState<string>("check_in")
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setSearch(searchInput), 400)
|
||||
@@ -56,14 +49,15 @@ export default function JobCardsPage() {
|
||||
|
||||
return (
|
||||
<ResourcePage<JobCardsClient>
|
||||
pageTitle="Job Cards"
|
||||
|
||||
routeKey={JOB_CARD_ROUTES.INDEX}
|
||||
getClient={(api) => api.jobCards}
|
||||
extraParams={extraParams}
|
||||
onRowClick={(row) => router.push(`/sales/job-cards/${row.id}`)}
|
||||
headerProps={({ selectedItem, invalidateQuery }) => ({
|
||||
title: "Job Cards",
|
||||
actions: (
|
||||
<FormDialog title="Job Card">
|
||||
<FormDialog classNames={{ dialogContent: 'min-w-6xl' }} title="Job Card" >
|
||||
{(resourceId) => (
|
||||
<JobCardForm
|
||||
resourceId={resourceId}
|
||||
@@ -88,28 +82,26 @@ export default function JobCardsPage() {
|
||||
)
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Status" />,
|
||||
cell: ({ row }) => {
|
||||
const item = row.original as unknown as JobCardItem
|
||||
return (
|
||||
<Badge variant={statusColorMap[item.status ?? ""] as any ?? "outline"}>
|
||||
{formatStatus(item.status)}
|
||||
</Badge>
|
||||
)
|
||||
},
|
||||
accessorKey: "order_number",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Order Number" />,
|
||||
|
||||
},
|
||||
{
|
||||
accessorKey: "check_in_date",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Check-in Date" />,
|
||||
cell: ({ getValue }) => {
|
||||
const val = getValue<string>()
|
||||
return formatDate(val)
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "km_in",
|
||||
accessorKey: "vehicle_id",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="KM In" />,
|
||||
cell: ({ row }) => {
|
||||
const item = row.original as unknown as JobCardItem
|
||||
return item.km_in ? Number(item.km_in).toLocaleString() : "—"
|
||||
return item.km_in ? formatNumber(item.km_in) : "—"
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -117,14 +109,36 @@ export default function JobCardsPage() {
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Created" />,
|
||||
cell: ({ row }) => {
|
||||
const item = row.original as unknown as JobCardItem
|
||||
return item.created_at ? new Date(item.created_at).toLocaleDateString() : "—"
|
||||
return formatDate(item.created_at)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Status" />,
|
||||
cell: ({ row }) => {
|
||||
const item = row.original as unknown as JobCardItem
|
||||
return (
|
||||
<Badge variant={statusColorMap[item.status ?? ""] as any ?? "outline"}>
|
||||
{formatEnum(item.status)}
|
||||
</Badge>
|
||||
)
|
||||
},
|
||||
},
|
||||
actionsColumn(),
|
||||
]}
|
||||
toolbar={
|
||||
<div className="flex gap-3 w-full">
|
||||
|
||||
|
||||
tableHeader={
|
||||
<div className="flex justify-between">
|
||||
<Tabs value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<TabsList variant="line">
|
||||
<TabsTrigger value="all" >All</TabsTrigger>
|
||||
{JobCardStatus.map((status) => (
|
||||
<TabsTrigger key={status} value={status}>
|
||||
{formatEnum(status)}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<div className="relative w-64">
|
||||
<SearchIcon className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
@@ -136,18 +150,6 @@ export default function JobCardsPage() {
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
tableHeader={
|
||||
<Tabs value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<TabsList variant="default">
|
||||
<TabsTrigger value="all" className='data-[state=active]:bg-primary/10 data-[state=active]:text-primary'>All</TabsTrigger>
|
||||
{JobCardStatus.map((status) => (
|
||||
<TabsTrigger className='data-[state=active]:bg-primary/10 data-[state=active]:text-primary ' key={status} value={status}>
|
||||
{formatStatus(status)}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ResourcePage } from "@/shared/data-view/resource-page"
|
||||
import { ColumnHeader } from "@/shared/data-view/table-view"
|
||||
import FormDialog from "@/shared/components/form-dialog"
|
||||
import { PaymentReceivedForm } from "@/modules/payment-received/payment-received-form"
|
||||
import { PAYMENT_ROUTES } from "@garage/api"
|
||||
import { PAYMENT_RECEIVED_ROUTES } from "@garage/api"
|
||||
import {
|
||||
BadgeDollarSignIcon,
|
||||
CalendarIcon,
|
||||
@@ -31,11 +31,10 @@ type PaymentReceivedItem = {
|
||||
export default function PaymentReceivedPage() {
|
||||
return (
|
||||
<ResourcePage<{ list(query?: any): Promise<any>; destroy(id: string): Promise<any> }>
|
||||
pageTitle="Payments Received"
|
||||
routeKey={PAYMENT_ROUTES.RECEIVED}
|
||||
routeKey={PAYMENT_RECEIVED_ROUTES.INDEX}
|
||||
getClient={(api) => ({
|
||||
list: (query?: any) => api.payments.listReceived(query),
|
||||
destroy: (id: string) => api.payments.destroyReceived(id),
|
||||
list: (query?: any) => api.paymentReceived.list(query),
|
||||
destroy: (id: string) => api.paymentReceived.destroy(id),
|
||||
})}
|
||||
headerProps={({ invalidateQuery }) => ({
|
||||
actions: (
|
||||
|
||||
Reference in New Issue
Block a user