This commit is contained in:
2026-04-07 06:32:40 +03:00
parent 24a44481a0
commit 11db1e6941
101 changed files with 54585 additions and 39160 deletions
@@ -12,7 +12,7 @@ import { redirect } from "next/navigation"
function Logo() {
return (
<div className="flex items-center gap-2">
<Image alt="Logo" src={'/assets/logo.png'} height={200} width={200} />
<Image alt="Logo" src={'/assets/logo.png'} height={100} width={100} />
</div>
)
}
+1 -1
View File
@@ -4,7 +4,7 @@ import { DashboardContent } from "@/modules/home/dashboard-content";
export default function page() {
return (
<DashboardPage header={<DashboardHeader />} title="Dashboard">
<DashboardPage headerProps={{title: "Dashboard"}} >
<DashboardContent />
</DashboardPage>
)
@@ -0,0 +1,57 @@
"use client"
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"
export default function TasksPage() {
return (
<ResourcePage<TasksClient>
routeKey={TASK_ROUTES.INDEX}
getClient={(api) => api.tasks}
headerProps={({ selectedItem, invalidateQuery }) => ({
title: "Tasks",
actions: (
<FormDialog title="Task">
{(resourceId) => (
<TaskForm
resourceId={resourceId}
initialData={selectedItem}
onSuccess={invalidateQuery}
/>
)}
</FormDialog>
),
})}
columns={({ actionsColumn }) => [
{
accessorKey: "task_number",
header: ({ column }) => <ColumnHeader column={column} title="Task #" />,
},
{
accessorKey: "subject",
header: ({ column }) => <ColumnHeader column={column} title="Subject" />,
},
{
accessorKey: "priority",
header: ({ column }) => <ColumnHeader column={column} title="Priority" />,
},
{
accessorKey: "status",
header: ({ column }) => <ColumnHeader column={column} title="Status" />,
},
{
accessorKey: "due_date",
header: ({ column }) => <ColumnHeader column={column} title="Due Date" />,
},
actionsColumn(),
]}
/>
)
}
@@ -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 }) => [
{
@@ -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: (
@@ -1,12 +1,55 @@
"use client"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { toast } from "sonner"
import { StarIcon, StarOffIcon } from "lucide-react"
import { DepartmentForm } from "@/modules/settings/departments/department-form"
import { ResourcePage } from "@/shared/data-view/resource-page"
import { ColumnHeader } from "@/shared/data-view/table-view"
import FormDialog from "@/shared/components/form-dialog"
import { Button } from "@/shared/components/ui/button"
import { useAuthApi } from "@/shared/useApi"
import { DEPARTMENT_ROUTES } from "@garage/api"
import type { DepartmentsClient } from "@garage/api"
function FavoriteCell({ row }: { row: any }) {
const api = useAuthApi()
const queryClient = useQueryClient()
const isFavorite: boolean = row.is_favorite ?? false
const { mutate, isPending } = useMutation({
mutationFn: () => {
const promise = isFavorite
? api.departments.removeFavorite({ id: row.id })
: api.departments.setFavorite({ id: row.id })
toast.promise(promise, {
loading: isFavorite ? "Removing from favourites..." : "Setting as favourite...",
success: isFavorite ? "Removed from favourites" : "Set as favourite",
error: isFavorite ? "Failed to remove favourite" : "Failed to set favourite",
})
return promise
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [DEPARTMENT_ROUTES.INDEX] })
},
})
return (
<Button
variant="ghost"
size="icon-sm"
disabled={isPending}
onClick={() => mutate()}
title={isFavorite ? "Remove from favourites" : "Set as favourite"}
>
{isFavorite
? <StarIcon className="h-4 w-4 text-yellow-500 fill-yellow-400" />
: <StarOffIcon className="h-4 w-4 text-muted-foreground" />}
</Button>
)
}
export default function DepartmentsPage() {
return (
<ResourcePage<DepartmentsClient>
@@ -36,6 +79,12 @@ export default function DepartmentsPage() {
header: ({ column }) => <ColumnHeader column={column} title="Assignment Type" />,
cell: ({ row }) => (row.original as any).assignment_type ?? "none",
},
{
id: "favourite",
header: () => <span className="sr-only">Favourite</span>,
enableSorting: false,
cell: ({ row }) => <FavoriteCell row={row.original} />,
},
actionsColumn(),
]}
/>