Skip to content

Commit 34e8d50

Browse files
committed
feat: jobs
1 parent 640d243 commit 34e8d50

File tree

22 files changed

+1661
-98
lines changed

22 files changed

+1661
-98
lines changed

app/(protected)/admin/borrows/page.tsx

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { cookies } from 'next/headers'
3131
import { ModelFilter } from '@/components/common/ModelFilter'
3232
import { UserFilter, BookFilter, DateFilter } from '@/components/common/filters'
3333
import { BorrowCardErrorBoundary } from '@/components/borrows/BorrowCardErrorBoundary'
34+
import { ModalExportBorrow } from '@/components/borrows/ModalExportBorrow'
3435

3536
export const metadata: Metadata = {
3637
title: `Borrows · ${SITE_NAME}`,
@@ -159,23 +160,27 @@ export default async function Borrows({
159160
activeHref={`/admin/borrows${status ? `?status=${status}` : ''}`}
160161
/>
161162

162-
<ModelFilter
163-
filterKeys={[
164-
'user_id',
165-
'book_id',
166-
'borrowed_at',
167-
'due_at',
168-
'returned_at',
169-
'lost_at',
170-
]}
171-
>
172-
<UserFilter />
173-
<BookFilter />
174-
<DateFilter filterKey="borrowed_at" placeholder="Borrow Date" />
175-
<DateFilter filterKey="due_at" placeholder="Due Date" />
176-
<DateFilter filterKey="returned_at" placeholder="Returned Date" />
177-
<DateFilter filterKey="lost_at" placeholder="Lost Date" />
178-
</ModelFilter>
163+
<div className="self-end inline-flex gap-2">
164+
<ModelFilter
165+
filterKeys={[
166+
'user_id',
167+
'book_id',
168+
'borrowed_at',
169+
'due_at',
170+
'returned_at',
171+
'lost_at',
172+
]}
173+
>
174+
<UserFilter />
175+
<BookFilter />
176+
<DateFilter filterKey="borrowed_at" placeholder="Borrow Date" />
177+
<DateFilter filterKey="due_at" placeholder="Due Date" />
178+
<DateFilter filterKey="returned_at" placeholder="Returned Date" />
179+
<DateFilter filterKey="lost_at" placeholder="Lost Date" />
180+
</ModelFilter>
181+
182+
<ModalExportBorrow />
183+
</div>
179184
</div>
180185

181186
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import {
2+
Breadcrumb,
3+
BreadcrumbItem,
4+
BreadcrumbLink,
5+
BreadcrumbList,
6+
BreadcrumbPage,
7+
BreadcrumbSeparator,
8+
} from '@/components/ui/breadcrumb'
9+
import { Verify } from '@/lib/firebase/firebase'
10+
import { getJob } from '@/lib/api/job'
11+
import { DetailJob } from '@/components/jobs/DetailJob'
12+
13+
export default async function JobDetailsPage({
14+
params,
15+
}: {
16+
params: Promise<{ id: string }>
17+
}) {
18+
const { id } = await params
19+
20+
const headers = await Verify({ from: `/admin/jobs/${id}` })
21+
22+
const [jobRes] = await Promise.all([getJob(id, { headers })])
23+
24+
if ('error' in jobRes) {
25+
console.log({ libRes: jobRes })
26+
return <div>{JSON.stringify(jobRes.message)}</div>
27+
}
28+
29+
return (
30+
<div className="space-y-4">
31+
<nav className="backdrop-blur-sm sticky top-0 z-10">
32+
<h1 className="text-2xl font-semibold">{jobRes.data.id.slice(0, 8)}</h1>
33+
<div className="flex justify-between items-center">
34+
<Breadcrumb>
35+
<BreadcrumbList>
36+
<BreadcrumbItem>
37+
<BreadcrumbLink href="/admin">Home</BreadcrumbLink>
38+
</BreadcrumbItem>
39+
<BreadcrumbSeparator />
40+
<BreadcrumbItem>
41+
<BreadcrumbLink href="/admin/jobs">Jobs</BreadcrumbLink>
42+
</BreadcrumbItem>
43+
<BreadcrumbSeparator />
44+
<BreadcrumbItem>
45+
<BreadcrumbPage>{jobRes.data.id.slice(0, 8)}</BreadcrumbPage>
46+
</BreadcrumbItem>
47+
</BreadcrumbList>
48+
</Breadcrumb>
49+
50+
{/* <Badge
51+
variant={
52+
getJobStatus(jobRes.data) === 'active' ? 'default' : 'secondary'
53+
}
54+
className="uppercase h-8 min-w-24 justify-center"
55+
>
56+
{getJobStatus(jobRes.data)}
57+
</Badge> */}
58+
</div>
59+
</nav>
60+
<DetailJob job={jobRes.data} />
61+
</div>
62+
)
63+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import {
2+
Breadcrumb,
3+
BreadcrumbItem,
4+
BreadcrumbLink,
5+
BreadcrumbList,
6+
BreadcrumbPage,
7+
BreadcrumbSeparator,
8+
} from '@/components/ui/breadcrumb'
9+
import { Button } from '@/components/ui/button'
10+
import {
11+
Pagination,
12+
PaginationContent,
13+
PaginationItem,
14+
PaginationNext,
15+
PaginationPrevious,
16+
} from '@/components/ui/pagination'
17+
import { Verify } from '@/lib/firebase/firebase'
18+
import Link from 'next/link'
19+
import type { Metadata } from 'next'
20+
import { SITE_NAME } from '@/lib/consts'
21+
import { TabLink } from '@/components/borrows/TabLink'
22+
import { Badge } from '@/components/ui/badge'
23+
import { cookies } from 'next/headers'
24+
import { RefreshCw } from 'lucide-react'
25+
import { getListJobs } from '@/lib/api/job'
26+
import { revalidatePath } from 'next/cache'
27+
import { ListCardJob } from '@/components/jobs/ListCardJob'
28+
29+
const statusMap = {
30+
pending: 'PENDING',
31+
processing: 'PROCESSING',
32+
completed: 'COMPLETED',
33+
failed: 'FAILED',
34+
} as const
35+
36+
export const metadata: Metadata = {
37+
title: `Jobs · ${SITE_NAME}`,
38+
}
39+
40+
export default async function Jobs({
41+
searchParams,
42+
}: {
43+
searchParams: Promise<{
44+
skip?: number
45+
limit?: number
46+
status?: 'pending' | 'processing' | 'completed' | 'failed'
47+
type?: 'export:borrowings'
48+
}>
49+
}) {
50+
const sp = await searchParams
51+
const skip = Number(sp?.skip ?? 0)
52+
const limit = Number(sp?.limit ?? 20)
53+
const status = sp?.status
54+
const type = sp?.type
55+
56+
const headers = await Verify({
57+
from: '/admin/jobs',
58+
})
59+
60+
const cookieStore = await cookies()
61+
const cookieName = process.env.LIBRARY_COOKIE_NAME as string
62+
const libID = cookieStore.get(cookieName)?.value
63+
64+
const res = await getListJobs(
65+
{
66+
sort_by: 'created_at',
67+
sort_in: 'desc',
68+
limit: limit,
69+
skip: skip,
70+
...(status ? { status: statusMap[status] } : {}),
71+
library_id: libID,
72+
type,
73+
},
74+
{
75+
headers,
76+
}
77+
)
78+
79+
if ('error' in res) {
80+
console.log(res)
81+
return <div>{JSON.stringify(res.message)}</div>
82+
}
83+
84+
const prevSkip = skip - limit > 0 ? skip - limit : 0
85+
86+
const nextURL = `/admin/jobs?skip=${skip + limit}&limit=${limit}` as const
87+
const prevURL = `/admin/jobs?skip=${prevSkip}&limit=${limit}` as const
88+
89+
async function refreshAction() {
90+
'use server'
91+
revalidatePath('/admin/jobs')
92+
}
93+
94+
return (
95+
<div className="space-y-4">
96+
<nav className="backdrop-blur-sm sticky top-0 z-10">
97+
<h1 className="text-2xl font-semibold">Jobs</h1>
98+
<div className="flex justify-between items-center">
99+
<Breadcrumb>
100+
<BreadcrumbList>
101+
<BreadcrumbItem>
102+
<BreadcrumbLink href="/admin">Home</BreadcrumbLink>
103+
</BreadcrumbItem>
104+
<BreadcrumbSeparator />
105+
106+
<BreadcrumbItem>
107+
<BreadcrumbPage>
108+
Subscriptions
109+
<Badge className="ml-4" variant="outline">
110+
{res.meta.total}
111+
</Badge>
112+
</BreadcrumbPage>
113+
</BreadcrumbItem>
114+
</BreadcrumbList>
115+
</Breadcrumb>
116+
<Button onClick={refreshAction} variant="secondary">
117+
<RefreshCw className="mr-2 h-4 w-4" />
118+
Refresh
119+
</Button>
120+
</div>
121+
</nav>
122+
<div className="flex flex-col gap-2 md:flex-row justify-between">
123+
<TabLink
124+
tabs={[
125+
{ name: 'All', href: '/admin/jobs' },
126+
{ name: 'Completed', href: '/admin/jobs?status=completed' },
127+
{ name: 'Pending', href: '/admin/jobs?status=pending' },
128+
{ name: 'Processing', href: '/admin/jobs?status=processing' },
129+
{ name: 'Failed', href: '/admin/jobs?status=failed' },
130+
]}
131+
activeHref={`/admin/jobs${status ? `?status=${status}` : ''}`}
132+
/>
133+
134+
{/* <ModelFilter filterKeys={['user_id']}>
135+
<UserFilter />
136+
<DateFilter filterKey="created_at" placeholder="Subscribed Date" />
137+
</ModelFilter> */}
138+
</div>
139+
140+
<div className="grid gap-4">
141+
{res.data.map((job) => (
142+
<ListCardJob key={job.id} job={job} />
143+
))}
144+
</div>
145+
146+
<Pagination>
147+
<PaginationContent>
148+
{res.meta.skip > 0 && (
149+
<PaginationItem>
150+
<PaginationPrevious href={prevURL} />
151+
</PaginationItem>
152+
)}
153+
{res.meta.limit <= res.data.length && (
154+
<PaginationItem>
155+
<PaginationNext href={nextURL} />
156+
</PaginationItem>
157+
)}
158+
</PaginationContent>
159+
</Pagination>
160+
</div>
161+
)
162+
}

app/(protected)/admin/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
BookUser,
1111
Ticket,
1212
BookCopy,
13+
Workflow,
1314
} from 'lucide-react'
1415
import { Button } from '@/components/ui/button'
1516
import { IsLoggedIn } from '@/lib/firebase/firebase'
@@ -36,6 +37,7 @@ const menuItems = [
3637
},
3738
{ title: 'Borrows', icon: BookUser, href: '/admin/borrows' },
3839
{ title: 'Collections', icon: BookCopy, href: '/admin/collections' },
40+
{ title: 'Jobs', icon: Workflow, href: '/admin/jobs' },
3941
] as const
4042

4143
export default async function LibraryDashboard() {

components/borrows/DetailBorrow.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { Badge } from '@/components/ui/badge'
44
import {
55
getBorrowStatus,
66
getSubscriptionStatus,
7-
isBorrowDue,
87
isSubscriptionActive,
98
} from '@/lib/utils'
109
import Image from 'next/image'

0 commit comments

Comments
 (0)