updates
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
"use client"
|
||||
|
||||
import { use } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { ResourcePage } from "@/shared/data-view/resource-page"
|
||||
import { ColumnHeader } from "@/shared/data-view/table-view"
|
||||
import FormDialog from "@/shared/components/form-dialog"
|
||||
import { AppointmentForm } from "@/modules/appointments/appointment-form"
|
||||
import { APPOINTMENT_ROUTES } from "@garage/api"
|
||||
import type { AppointmentsClient } from "@garage/api"
|
||||
import { CalendarCheck2Icon, ClockIcon } from "lucide-react"
|
||||
import { Badge } from "@/shared/components/ui/badge"
|
||||
import { useJobCard } from "@/modules/job-cards/job-card-context"
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
requested: "bg-yellow-100 text-yellow-800",
|
||||
confirmed: "bg-blue-100 text-blue-800",
|
||||
in_progress: "bg-purple-100 text-purple-800",
|
||||
completed: "bg-green-100 text-green-800",
|
||||
cancelled: "bg-red-100 text-red-800",
|
||||
}
|
||||
|
||||
export default function JobCardAppointmentsPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>
|
||||
}) {
|
||||
const { id: jobCardId } = use(params)
|
||||
const router = useRouter()
|
||||
const jobCard = useJobCard()
|
||||
|
||||
const defaultJobCard = jobCard
|
||||
? { value: String((jobCard as any).id), label: (jobCard as any).label || (jobCard as any).title || `Job Card` }
|
||||
: null
|
||||
|
||||
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>
|
||||
)}
|
||||
columns={({ actionsColumn }) => [
|
||||
{
|
||||
accessorKey: "title",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Title" />,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarCheck2Icon className="size-4 text-muted-foreground" />
|
||||
<span>{(row.original as any).title}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "date",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Date" />,
|
||||
},
|
||||
{
|
||||
accessorKey: "from_time",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Time" />,
|
||||
cell: ({ row }) => {
|
||||
const r = row.original as any
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<ClockIcon className="size-3 text-muted-foreground" />
|
||||
<span>{r.from_time} - {r.to_time}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Status" />,
|
||||
cell: ({ row }) => {
|
||||
const status = (row.original as any).status
|
||||
const colorClass = STATUS_COLORS[status] ?? "bg-gray-100 text-gray-800"
|
||||
return (
|
||||
<Badge className={colorClass}>
|
||||
{status?.replace("_", " ") ?? "—"}
|
||||
</Badge>
|
||||
)
|
||||
},
|
||||
},
|
||||
actionsColumn(),
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -10,15 +10,11 @@ import { useAuthApi } from "@/shared/useApi"
|
||||
import { confirm } from "@/shared/components/confirm-dialog"
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import { Card, CardContent } from "@/shared/components/ui/card"
|
||||
import { JOB_CARD_ROUTES } from "@garage/api"
|
||||
import { JOB_CARD_ROUTES, JobCardResponseData } from "@garage/api"
|
||||
import DashboardPage from "@/base/components/layout/dashboard/dashboard-page"
|
||||
import { useJobCard } from "@/modules/job-cards/job-card-context"
|
||||
import { CONSTANTS } from "@/config/constants"
|
||||
|
||||
type Attachment = {
|
||||
id: number
|
||||
file_name: string
|
||||
url: string
|
||||
mime_type?: string
|
||||
created_at?: string
|
||||
}
|
||||
|
||||
function getFileIcon(mimeType?: string) {
|
||||
if (mimeType?.startsWith("image/")) return ImageIcon
|
||||
@@ -35,13 +31,9 @@ export default function JobCardAttachmentsPage() {
|
||||
|
||||
const queryKey = [JOB_CARD_ROUTES.INDEX, jobCardId, "attachments"]
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey,
|
||||
queryFn: () => api.jobCards.show(jobCardId),
|
||||
})
|
||||
const jobcard = useJobCard()
|
||||
|
||||
const jobCard = (data as any)?.data ?? data
|
||||
const attachments: Attachment[] = jobCard?.documents ?? jobCard?.attachments ?? []
|
||||
const attachments = jobcard?.attachment_files
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (attachmentId: number) =>
|
||||
@@ -55,10 +47,10 @@ export default function JobCardAttachmentsPage() {
|
||||
},
|
||||
})
|
||||
|
||||
const handleDelete = async (attachment: Attachment) => {
|
||||
const handleDelete = async (attachment: JobCardResponseData['attachment_files'][number]) => {
|
||||
const confirmed = await confirm({
|
||||
title: "Delete Attachment",
|
||||
description: `Are you sure you want to delete "${attachment.file_name}"?`,
|
||||
description: `Are you sure you want to delete "${attachment.original_name}"?`,
|
||||
confirmLabel: "Delete",
|
||||
variant: "destructive",
|
||||
})
|
||||
@@ -91,31 +83,30 @@ export default function JobCardAttachmentsPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<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>
|
||||
<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"
|
||||
>
|
||||
|
||||
{isLoading ? (
|
||||
<Card>
|
||||
<CardContent className="py-8 text-center text-muted-foreground">
|
||||
Loading attachments...
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : attachments.length === 0 ? (
|
||||
|
||||
{attachments?.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="py-8 text-center text-muted-foreground">
|
||||
No attachments yet. Click "Upload Attachment" to add files.
|
||||
@@ -123,8 +114,8 @@ 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.mime_type)
|
||||
{attachments?.map((attachment) => {
|
||||
const Icon = getFileIcon(attachment.attachment_path)
|
||||
return (
|
||||
<Card key={attachment.id}>
|
||||
<CardContent className="flex items-center gap-3 p-4">
|
||||
@@ -133,13 +124,13 @@ export default function JobCardAttachmentsPage() {
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||
<a
|
||||
href={attachment.url}
|
||||
href={CONSTANTS.getAssetUrl(attachment.attachment_path)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="truncate text-sm font-medium hover:underline"
|
||||
title={attachment.file_name}
|
||||
title={attachment.original_name}
|
||||
>
|
||||
{attachment.file_name}
|
||||
{attachment.original_name}
|
||||
</a>
|
||||
{attachment.created_at && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
@@ -161,6 +152,6 @@ export default function JobCardAttachmentsPage() {
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DashboardPage>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"use client"
|
||||
|
||||
import { use } from "react"
|
||||
import FormDialog from "@/shared/components/form-dialog"
|
||||
import { Badge } from "@/shared/components/ui/badge"
|
||||
import { ResourcePage } from "@/shared/data-view/resource-page"
|
||||
import { ColumnHeader } from "@/shared/data-view/table-view"
|
||||
import { BillForm } from "@/modules/bills/bill-form"
|
||||
import { BILL_ROUTES } from "@garage/api"
|
||||
import type { BillsClient } from "@garage/api"
|
||||
import { useJobCard } from "@/modules/job-cards/job-card-context"
|
||||
|
||||
export default function JobCardBillsPage({
|
||||
params,
|
||||
}: {
|
||||
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).label || (jobCard as any).title || `Job Card` }
|
||||
: null
|
||||
|
||||
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>
|
||||
)}
|
||||
columns={({ actionsColumn }) => [
|
||||
{
|
||||
accessorKey: "bill_number",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Bill #" />,
|
||||
cell: ({ row }) => (row.original as any).bill_number || "—",
|
||||
},
|
||||
{
|
||||
accessorKey: "title",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Title" />,
|
||||
},
|
||||
{
|
||||
accessorKey: "vendor_name",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Vendor" />,
|
||||
cell: ({ row }) => (row.original as any).vendor_name || "—",
|
||||
},
|
||||
{
|
||||
accessorKey: "bill_date",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Bill Date" />,
|
||||
cell: ({ row }) => {
|
||||
const value = (row.original as any).bill_date
|
||||
return value ? new Date(value).toLocaleDateString() : "—"
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "bill_due_date",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Due Date" />,
|
||||
cell: ({ row }) => {
|
||||
const value = (row.original as any).bill_due_date
|
||||
return value ? new Date(value).toLocaleDateString() : "—"
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Status" />,
|
||||
cell: ({ row }) => {
|
||||
const status = (row.original as any).status
|
||||
return <Badge variant={status === "paid" ? "default" : "secondary"}>{status || "—"}</Badge>
|
||||
},
|
||||
},
|
||||
actionsColumn(),
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
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
|
||||
@@ -39,9 +40,7 @@ export default function CustomerRemarksPage() {
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey,
|
||||
queryFn: async () => {
|
||||
const result = await api.jobCards.show(jobCardId)
|
||||
const d = (result as any)?.data ?? result
|
||||
return d?.customer_remarks ?? []
|
||||
// const result = await api.jobCards.
|
||||
},
|
||||
})
|
||||
|
||||
@@ -112,41 +111,43 @@ export default function CustomerRemarksPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-end">
|
||||
<Button onClick={() => setDialogOpen(true)}>
|
||||
<Plus className="size-4" />
|
||||
Add Customer Remark
|
||||
</Button>
|
||||
<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>
|
||||
|
||||
<Card>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={remarks}
|
||||
pagination={pagination}
|
||||
sorting={[]}
|
||||
onChange={() => {}}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"use client"
|
||||
|
||||
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 { ExpenseForm } from "@/modules/expenses/expense-form"
|
||||
import { Badge } from "@/shared/components/ui/badge"
|
||||
import { EXPENSE_ROUTES } from "@garage/api"
|
||||
import type { ExpensesClient } from "@garage/api"
|
||||
import { useJobCard } from "@/modules/job-cards/job-card-context"
|
||||
|
||||
export default function JobCardExpensesPage({
|
||||
params,
|
||||
}: {
|
||||
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).label || (jobCard as any).title || `Job Card` }
|
||||
: null
|
||||
|
||||
return (
|
||||
<ResourcePage<ExpensesClient>
|
||||
pageTitle="Expenses"
|
||||
routeKey={EXPENSE_ROUTES.INDEX}
|
||||
getClient={(api) => api.expenses}
|
||||
extraParams={{ job_card_id: jobCardId }}
|
||||
header={null}
|
||||
toolbar={({ invalidateQuery, selectedItem, closeDialog }) => (
|
||||
<FormDialog title="Expense">
|
||||
{(resourceId) => (
|
||||
<ExpenseForm
|
||||
resourceId={resourceId}
|
||||
initialData={selectedItem ?? { job_card: defaultJobCard }}
|
||||
onSuccess={() => { closeDialog(); invalidateQuery() }}
|
||||
/>
|
||||
)}
|
||||
</FormDialog>
|
||||
)}
|
||||
columns={({ actionsColumn }) => [
|
||||
{
|
||||
accessorKey: "title",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Title" />,
|
||||
},
|
||||
{
|
||||
accessorKey: "invoice_number",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Invoice #" />,
|
||||
cell: ({ row }) => (row.original as any).invoice_number || "—",
|
||||
},
|
||||
{
|
||||
accessorKey: "vendor_name",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Vendor" />,
|
||||
cell: ({ row }) => (row.original as any).vendor_name || "—",
|
||||
},
|
||||
{
|
||||
accessorKey: "expense_date",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Date" />,
|
||||
cell: ({ row }) => {
|
||||
const val = (row.original as any).expense_date
|
||||
return val ? new Date(val).toLocaleDateString() : "—"
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Status" />,
|
||||
cell: ({ row }) => {
|
||||
const status = (row.original as any).status
|
||||
return (
|
||||
<Badge variant={status === "paid" ? "default" : "secondary"}>
|
||||
{status || "—"}
|
||||
</Badge>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "created_at",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Created" />,
|
||||
cell: ({ row }) => {
|
||||
const val = (row.original as any).created_at
|
||||
return val ? new Date(val).toLocaleDateString() : "—"
|
||||
},
|
||||
},
|
||||
actionsColumn(),
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -3,26 +3,32 @@ import { getServerApi } from '@garage/api/server'
|
||||
import { JobCardActions } from '@/modules/job-cards/job-card-actions'
|
||||
import { JobCardProvider } from '@/modules/job-cards/job-card-context'
|
||||
import { JobCardStatusStepper } from '@/modules/job-cards/job-card-status-stepper'
|
||||
import { ClipboardListIcon } from 'lucide-react'
|
||||
import { ClipboardListIcon, Ellipsis, Pencil, Trash2 } from 'lucide-react'
|
||||
import React from 'react'
|
||||
import JobCardDropdown from '@/modules/job-cards/job-card-dropdown'
|
||||
|
||||
|
||||
export default async function JobCardDetailLayout(props: { params: Promise<{ id: string }>, children: React.ReactNode }) {
|
||||
const { id } = await props.params
|
||||
const api = await getServerApi()
|
||||
const jobCard = await api.jobCards.show(id)
|
||||
const data = (jobCard as any)?.data ?? jobCard
|
||||
const title = data?.title || 'Job Card Details'
|
||||
const status = data?.status || 'draft'
|
||||
|
||||
const jobCard = await api.jobCards.show(id).then(res => res.data)
|
||||
|
||||
const title = jobCard?.title || 'Job Card Details'
|
||||
const status = jobCard?.status || 'draft'
|
||||
const docs = jobCard?.documents
|
||||
|
||||
return (
|
||||
<JobCardProvider jobCard={{ id, label: title, status }}>
|
||||
<JobCardProvider jobCard={{ ...jobCard, label: title }}>
|
||||
<DashboardDetailsPage
|
||||
className='p-0 lg:p-0'
|
||||
title={title}
|
||||
description={data?.status ? `Status: ${data.status.split("_").map((w: string) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")}` : undefined}
|
||||
description={status ? `Status: ${status.split("_").map((w: string) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")}` : undefined}
|
||||
icon={<ClipboardListIcon className="size-5" />}
|
||||
backHref="/sales/job-cards"
|
||||
actions={<JobCardActions jobCardId={id} />}
|
||||
actions={
|
||||
<JobCardDropdown id={id} />
|
||||
}
|
||||
subHeader={<JobCardStatusStepper jobCardId={id} />}
|
||||
tabs={[
|
||||
{
|
||||
@@ -39,7 +45,27 @@ export default async function JobCardDetailLayout(props: { params: Promise<{ id:
|
||||
},
|
||||
{
|
||||
href: `/sales/job-cards/${id}/attachments`,
|
||||
label: 'Attachments'
|
||||
label: `Attachments (${docs?.length || 0})`
|
||||
},
|
||||
{
|
||||
href: `/sales/job-cards/${id}/purchase-orders`,
|
||||
label: `Purchase Orders (${jobCard?.purchase_orders_count || 0})`
|
||||
},
|
||||
{
|
||||
href: `/sales/job-cards/${id}/bills`,
|
||||
label: `Bills (${jobCard?.bills_count || 0})`
|
||||
},
|
||||
{
|
||||
href: `/sales/job-cards/${id}/appointments`,
|
||||
label: `Appointments (${jobCard?.appointments_count || 0})`
|
||||
},
|
||||
{
|
||||
href: `/sales/job-cards/${id}/expenses`,
|
||||
label: `Expenses (${jobCard?.expenses_count || 0})`
|
||||
},
|
||||
{
|
||||
href: `/sales/job-cards/${id}/tasks`,
|
||||
label: `Tasks (${jobCard?.tasks_count || 0})`
|
||||
},
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
"use client"
|
||||
|
||||
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 { PurchaseOrderForm } from "@/modules/purchase-orders/purchase-order-form"
|
||||
import { PURCHASE_ORDER_ROUTES } from "@garage/api"
|
||||
import type { PurchaseOrdersClient } from "@garage/api"
|
||||
import { useJobCard } from "@/modules/job-cards/job-card-context"
|
||||
|
||||
export default function JobCardPurchaseOrdersPage({
|
||||
params,
|
||||
}: {
|
||||
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).label || (jobCard as any).title || `Job Card` }
|
||||
: null
|
||||
|
||||
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 }) => (
|
||||
<FormDialog title="Purchase Order">
|
||||
{(resourceId) => (
|
||||
<PurchaseOrderForm
|
||||
resourceId={resourceId}
|
||||
initialData={selectedItem ?? { job_card: defaultJobCard }}
|
||||
onSuccess={() => { closeDialog(); invalidateQuery() }}
|
||||
/>
|
||||
)}
|
||||
</FormDialog>
|
||||
)}
|
||||
columns={({ actionsColumn }) => [
|
||||
{
|
||||
accessorKey: "order_number",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Order #" />,
|
||||
cell: ({ row }) => (row.original as any).order_number || "—",
|
||||
},
|
||||
{
|
||||
accessorKey: "title",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Title" />,
|
||||
},
|
||||
{
|
||||
accessorKey: "vendor_name",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Vendor" />,
|
||||
cell: ({ row }) => (row.original as any).vendor_name || "—",
|
||||
},
|
||||
{
|
||||
accessorKey: "order_date",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Order Date" />,
|
||||
cell: ({ row }) => {
|
||||
const val = (row.original as any).order_date
|
||||
return val ? new Date(val).toLocaleDateString() : "—"
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "delivery_date",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Delivery Date" />,
|
||||
cell: ({ row }) => {
|
||||
const val = (row.original as any).delivery_date
|
||||
return val ? new Date(val).toLocaleDateString() : "—"
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "created_at",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Created" />,
|
||||
cell: ({ row }) => {
|
||||
const val = (row.original as any).created_at
|
||||
return val ? new Date(val).toLocaleDateString() : "—"
|
||||
},
|
||||
},
|
||||
actionsColumn(),
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
+13
-8
@@ -19,6 +19,7 @@ import {
|
||||
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
|
||||
@@ -112,13 +113,17 @@ export default function ShopRecommendationsPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-end">
|
||||
<Button onClick={() => setDialogOpen(true)}>
|
||||
<Plus className="size-4" />
|
||||
Add Shop Recommendation
|
||||
</Button>
|
||||
</div>
|
||||
<DashboardPage
|
||||
header={null}
|
||||
title="Shop Recommendations"
|
||||
toolbar={
|
||||
<Button onClick={() => setDialogOpen(true)}>
|
||||
<Plus className="size-4 me-2" />
|
||||
Add Recommendation
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
|
||||
|
||||
<Card>
|
||||
<CardContent>
|
||||
@@ -147,6 +152,6 @@ export default function ShopRecommendationsPage() {
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</DashboardPage>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"use client"
|
||||
|
||||
import { use } from "react"
|
||||
import { ResourcePage } from "@/shared/data-view/resource-page"
|
||||
import { ColumnHeader } from "@/shared/data-view/table-view"
|
||||
import { TASK_ROUTES } from "@garage/api"
|
||||
import type { TasksClient } from "@garage/api"
|
||||
import { Badge } from "@/shared/components/ui/badge"
|
||||
|
||||
export default function JobCardTasksPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>
|
||||
}) {
|
||||
const { id: jobCardId } = use(params)
|
||||
|
||||
return (
|
||||
<ResourcePage<TasksClient>
|
||||
pageTitle="Tasks"
|
||||
routeKey={TASK_ROUTES.TASKS}
|
||||
getClient={(api) => api.tasks}
|
||||
extraParams={{ job_card_id: jobCardId }}
|
||||
header={null}
|
||||
columns={() => [
|
||||
{
|
||||
accessorKey: "title",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Task" />,
|
||||
cell: ({ row }) => (row.original as any).title || "—",
|
||||
},
|
||||
{
|
||||
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: "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() : "—"
|
||||
},
|
||||
},
|
||||
{
|
||||
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> : "—"
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user