feat: owner return book
This commit is contained in:
parent
4ca5e0a299
commit
1d817b762b
@ -7,11 +7,12 @@ import { PaginatedDocs } from 'payload'
|
|||||||
type Props = {
|
type Props = {
|
||||||
repos: PaginatedDocs<Repository> | null
|
repos: PaginatedDocs<Repository> | null
|
||||||
borrows: PaginatedDocs<Checkout> | null
|
borrows: PaginatedDocs<Checkout> | null
|
||||||
|
loans: PaginatedDocs<Checkout> | null
|
||||||
user: User | null
|
user: User | null
|
||||||
}
|
}
|
||||||
const ManagePageClient = (props: Props) => {
|
const ManagePageClient = (props: Props) => {
|
||||||
const { repos, borrows, user } = props
|
const { repos, borrows, loans, user } = props
|
||||||
return <Manage repos={repos} borrows={borrows} user={user} />
|
return <Manage repos={repos} borrows={borrows} user={user} loans={loans} />
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ManagePageClient
|
export default ManagePageClient
|
||||||
|
|||||||
@ -78,10 +78,40 @@ const ManagePage = async () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let userLoans: PaginatedDocs<Checkout> | null = null
|
||||||
|
if (user?.id)
|
||||||
|
userLoans = await payload.find({
|
||||||
|
collection: 'checkouts',
|
||||||
|
depth: 2,
|
||||||
|
limit: 10,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
copy: true,
|
||||||
|
dateDue: true,
|
||||||
|
ownerVerifiedReturnedDate: true,
|
||||||
|
loaneeReturnedDate: true,
|
||||||
|
},
|
||||||
|
sort: 'dateDue',
|
||||||
|
where: {
|
||||||
|
and: [
|
||||||
|
{
|
||||||
|
isReturned: {
|
||||||
|
not_equals: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'copy.repository.owner.id': {
|
||||||
|
equals: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageBreadCrumb routes={breadcrumRoutes} />
|
<PageBreadCrumb routes={breadcrumRoutes} />
|
||||||
<ManagePageClient repos={userRepos} borrows={userBorrows} user={user} />
|
<ManagePageClient repos={userRepos} borrows={userBorrows} user={user} loans={userLoans} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -89,6 +89,36 @@ export default async function HomePage() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let userLoans: PaginatedDocs<Checkout> | null = null
|
||||||
|
if (user?.id)
|
||||||
|
userLoans = await payload.find({
|
||||||
|
collection: 'checkouts',
|
||||||
|
depth: 2,
|
||||||
|
limit: 10,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
copy: true,
|
||||||
|
dateDue: true,
|
||||||
|
ownerVerifiedReturnedDate: true,
|
||||||
|
loaneeReturnedDate: true,
|
||||||
|
},
|
||||||
|
sort: 'dateDue',
|
||||||
|
where: {
|
||||||
|
and: [
|
||||||
|
{
|
||||||
|
isReturned: {
|
||||||
|
not_equals: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'copy.repository.owner.id': {
|
||||||
|
equals: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="home">
|
<div className="home">
|
||||||
<HomeHero user={user} />
|
<HomeHero user={user} />
|
||||||
@ -110,7 +140,7 @@ export default async function HomePage() {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="manage">
|
<TabsContent value="manage">
|
||||||
<Manage repos={userRepos} user={user} borrows={userBorrows} />
|
<Manage repos={userRepos} user={user} borrows={userBorrows} loans={userLoans} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -19,7 +19,13 @@ const HoldRequestNotifications = (props: Props) => {
|
|||||||
|
|
||||||
const [openedModalId, setOpenedModalId] = useState<number | null>(null)
|
const [openedModalId, setOpenedModalId] = useState<number | null>(null)
|
||||||
|
|
||||||
const totalHoldNotifications = repos?.docs.flatMap((r) => r.holdRequests?.docs).length || 0
|
const totalHoldNotifications =
|
||||||
|
repos?.docs
|
||||||
|
.flatMap((r) => r.holdRequests?.docs)
|
||||||
|
.filter((r) => {
|
||||||
|
const request = r as HoldRequest
|
||||||
|
return !request.isCheckedOut && !request.isRejected
|
||||||
|
}).length || 0
|
||||||
|
|
||||||
const [isRejectingId, setIsRejectingId] = useState<number | null>()
|
const [isRejectingId, setIsRejectingId] = useState<number | null>()
|
||||||
|
|
||||||
|
|||||||
218
src/components/Manage/LoanedBooks.tsx
Normal file
218
src/components/Manage/LoanedBooks.tsx
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Author, Book, Checkout, Copy, Repository } from '@/payload-types'
|
||||||
|
import { useGlobal } from '@/providers/GlobalProvider'
|
||||||
|
import { getUserLoans } from '@/serverActions/GetUserLoans'
|
||||||
|
import { ownerReturnCheckout } from '@/serverActions/ReturnCheckout'
|
||||||
|
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
|
||||||
|
import { EllipsisVerticalIcon } from '@heroicons/react/20/solid'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { Loader2Icon } from 'lucide-react'
|
||||||
|
import { PaginatedDocs, User } from 'payload'
|
||||||
|
import { useCallback, useMemo, useState } from 'react'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
|
const statuses = {
|
||||||
|
'Passed Due': 'text-gray-200 bg-red-500 ring-red-700/10',
|
||||||
|
'Due Soon': 'text-amber-800 bg-amber-50 ring-amber-600/20',
|
||||||
|
'': '',
|
||||||
|
}
|
||||||
|
|
||||||
|
type Row = {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
authors: string[]
|
||||||
|
owners: string[]
|
||||||
|
repositoryAbbreviation: string
|
||||||
|
status: 'Passed Due' | 'Due Soon' | ''
|
||||||
|
dueDate: Date
|
||||||
|
href: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListProps = {
|
||||||
|
rows: Row[]
|
||||||
|
onUpdate: () => Promise<void>
|
||||||
|
}
|
||||||
|
const LoanedBooksList = (props: ListProps) => {
|
||||||
|
const { rows, onUpdate } = props
|
||||||
|
|
||||||
|
const [returningBookId, setReturningBookId] = useState(0)
|
||||||
|
|
||||||
|
const isReturningBook = useCallback((id: number) => id === returningBookId, [returningBookId])
|
||||||
|
|
||||||
|
const handleReturnClick = useCallback(
|
||||||
|
async (id: number) => {
|
||||||
|
if (returningBookId) return
|
||||||
|
setReturningBookId(id)
|
||||||
|
|
||||||
|
const updatedCheckout = await ownerReturnCheckout({ checkoutId: id })
|
||||||
|
if (!updatedCheckout) {
|
||||||
|
setReturningBookId(0)
|
||||||
|
toast('There was an issue returning your book')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await onUpdate()
|
||||||
|
setReturningBookId(0)
|
||||||
|
toast('Book was returned into your collection')
|
||||||
|
},
|
||||||
|
[returningBookId, setReturningBookId, onUpdate],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul role="list" className="divide-y divide-gray-100">
|
||||||
|
{rows.map((r) => (
|
||||||
|
<li
|
||||||
|
key={r.id}
|
||||||
|
className={clsx(
|
||||||
|
'flex relative items-center justify-between gap-x-6 py-5',
|
||||||
|
isReturningBook(r.id) ? 'pointer-events-none opacity-30' : '',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex items-start gap-x-3">
|
||||||
|
<p className="text-sm/6 text-foreground">
|
||||||
|
<span className="font-semibold">{`[${r.repositoryAbbreviation}] `}</span>
|
||||||
|
<span>{r.title}</span>
|
||||||
|
</p>
|
||||||
|
{!!r.status && (
|
||||||
|
<p
|
||||||
|
className={clsx(
|
||||||
|
statuses[r.status as keyof typeof statuses],
|
||||||
|
'mt-0.5 rounded-md px-1.5 py-0.5 text-xs font-medium whitespace-nowrap ring-1 ring-inset',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{r.status}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 flex items-center gap-x-2 text-xs/5 text-gray-500">
|
||||||
|
<p className="whitespace-nowrap">
|
||||||
|
<span>
|
||||||
|
Due on{' '}
|
||||||
|
<time dateTime={r.dueDate.toString()}>{r.dueDate.toLocaleDateString()}</time>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<svg viewBox="0 0 2 2" className="size-0.5 fill-current">
|
||||||
|
<circle r={1} cx={1} cy={1} />
|
||||||
|
</svg>
|
||||||
|
<p className="truncate">Borrowed from {r.owners.join(', ')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-none items-center gap-x-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!!returningBookId}
|
||||||
|
className="hidden disabled:opacity-20 cursor-pointer disabled:cursor-auto rounded-md bg-background px-2.5 py-1.5 text-sm font-semibold text-muted-foreground shadow-xs ring-1 ring-gray-300 ring-inset hover:bg-gray-50 sm:flex items-center gap-1"
|
||||||
|
onClick={() => handleReturnClick(r.id)}
|
||||||
|
>
|
||||||
|
{isReturningBook(r.id) && <Loader2Icon className="animate-spin" />}
|
||||||
|
<span>Return</span>
|
||||||
|
</button>
|
||||||
|
<Menu as="div" className="relative flex-none">
|
||||||
|
<MenuButton className="-m-2.5 block p-2.5 text-gray-500 hover:text-gray-900">
|
||||||
|
<span className="sr-only">Open options</span>
|
||||||
|
<EllipsisVerticalIcon aria-hidden="true" className="size-5" />
|
||||||
|
</MenuButton>
|
||||||
|
<MenuItems
|
||||||
|
transition
|
||||||
|
className="absolute right-0 z-10 mt-2 w-32 origin-top-right rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 transition focus:outline-hidden data-closed:scale-95 data-closed:transform data-closed:opacity-0 data-enter:duration-100 data-enter:ease-out data-leave:duration-75 data-leave:ease-in"
|
||||||
|
>
|
||||||
|
<MenuItem>
|
||||||
|
<button
|
||||||
|
className="block w-full text-left cursor-pointer px-3 py-1 text-sm/6 text-gray-900 data-focus:bg-gray-50 data-focus:outline-hidden"
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleReturnClick(r.id)}
|
||||||
|
>
|
||||||
|
Return<span className="sr-only">, {r.title}</span>
|
||||||
|
</button>
|
||||||
|
</MenuItem>
|
||||||
|
{/*<MenuItem>
|
||||||
|
<a
|
||||||
|
href={`/repositories/${r.repositoryAbbreviation}`}
|
||||||
|
className="block px-3 py-1 text-sm/6 text-gray-900 data-focus:bg-gray-50 data-focus:outline-hidden"
|
||||||
|
>
|
||||||
|
Repository<span className="sr-only">, {r.repositoryAbbreviation}</span>
|
||||||
|
</a>
|
||||||
|
</MenuItem>*/}
|
||||||
|
<MenuItem>
|
||||||
|
<a
|
||||||
|
href={r.href}
|
||||||
|
className="block px-3 py-1 text-sm/6 text-gray-900 data-focus:bg-gray-50 data-focus:outline-hidden"
|
||||||
|
>
|
||||||
|
View<span className="sr-only"> {r.title}</span>
|
||||||
|
</a>
|
||||||
|
</MenuItem>
|
||||||
|
</MenuItems>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = { initialLoans: PaginatedDocs<Checkout> | null }
|
||||||
|
const LoanedBooks = (props: Props) => {
|
||||||
|
const { initialLoans } = props
|
||||||
|
|
||||||
|
const { user } = useGlobal()
|
||||||
|
|
||||||
|
const [loans, setLoans] = useState<PaginatedDocs<Checkout> | null>(initialLoans)
|
||||||
|
|
||||||
|
const onUpdate = useCallback(async () => {
|
||||||
|
if (!user) return
|
||||||
|
|
||||||
|
const updatedUserLoans = await getUserLoans({ userId: user.id })
|
||||||
|
setLoans(updatedUserLoans)
|
||||||
|
}, [setLoans, user])
|
||||||
|
|
||||||
|
const rows: Row[] = useMemo(
|
||||||
|
() =>
|
||||||
|
loans?.docs?.map((c) => {
|
||||||
|
const copy = c.copy as Copy
|
||||||
|
const book = copy.book as Book
|
||||||
|
const authors = book.authors as Author[]
|
||||||
|
const repository = copy.repository as Repository
|
||||||
|
const owners = repository.owner as (number | User)[]
|
||||||
|
|
||||||
|
const parsedDateDue = c.dateDue ? new Date(c.dateDue) : new Date()
|
||||||
|
const milisecondsUntilDue = parsedDateDue.getTime() - new Date().getTime()
|
||||||
|
const secondsUntilDue = Math.floor(milisecondsUntilDue / 1000)
|
||||||
|
const minutesUntilDue = Math.floor(secondsUntilDue / 60)
|
||||||
|
const hoursUntilDue = Math.floor(minutesUntilDue / 60)
|
||||||
|
const daysUntilDue = Math.floor(hoursUntilDue / 24)
|
||||||
|
|
||||||
|
let status = ''
|
||||||
|
if (daysUntilDue <= 0) status = 'Passed Due'
|
||||||
|
else if (daysUntilDue <= 5) status = 'Due Soon'
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: c.id || 0,
|
||||||
|
title: book.title || '',
|
||||||
|
authors: authors.map((a) => a.lf) || ([] as string[]),
|
||||||
|
owners:
|
||||||
|
owners.map((o) => `${(o as User).firstName} ${(o as User).lastName}`) ||
|
||||||
|
([] as string[]),
|
||||||
|
repositoryAbbreviation: repository.abbreviation || '',
|
||||||
|
status,
|
||||||
|
dueDate: parsedDateDue,
|
||||||
|
href: `/books/${book.id}`,
|
||||||
|
} as Row
|
||||||
|
}) || [],
|
||||||
|
[loans],
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!rows.length) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-6">
|
||||||
|
<div className="mb-4 flex justify-between items-end">
|
||||||
|
<h3 className="px-4 text-lg font-semibold">Loaned Out Books</h3>
|
||||||
|
</div>
|
||||||
|
<LoanedBooksList onUpdate={onUpdate} rows={rows} />
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoanedBooks
|
||||||
@ -5,14 +5,16 @@ import { PaginatedDocs } from 'payload'
|
|||||||
import RepoList from './RepoList'
|
import RepoList from './RepoList'
|
||||||
import HoldRequestNotifications from './HoldRequests'
|
import HoldRequestNotifications from './HoldRequests'
|
||||||
import BorrowedBooks from './BorrowedBooks'
|
import BorrowedBooks from './BorrowedBooks'
|
||||||
|
import LoanedBooks from './LoanedBooks'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repos: PaginatedDocs<Repository> | null
|
repos: PaginatedDocs<Repository> | null
|
||||||
borrows: PaginatedDocs<Checkout> | null
|
borrows: PaginatedDocs<Checkout> | null
|
||||||
|
loans: PaginatedDocs<Checkout> | null
|
||||||
user: User | null
|
user: User | null
|
||||||
}
|
}
|
||||||
const Manage = (props: Props) => {
|
const Manage = (props: Props) => {
|
||||||
const { repos, borrows, user } = props
|
const { repos, borrows, loans, user } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
@ -25,6 +27,10 @@ const Manage = (props: Props) => {
|
|||||||
<div>
|
<div>
|
||||||
<BorrowedBooks initialBorrows={borrows} />
|
<BorrowedBooks initialBorrows={borrows} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<LoanedBooks initialLoans={loans} />
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,7 +52,7 @@ export function LoginForm({ className, ...props }: React.ComponentProps<'div'>)
|
|||||||
if (loginResponse.user) setUser(loginResponse.user)
|
if (loginResponse.user) setUser(loginResponse.user)
|
||||||
|
|
||||||
if (loginResponse.token) router.push(searchParams.get('redirectUrl') || '/')
|
if (loginResponse.token) router.push(searchParams.get('redirectUrl') || '/')
|
||||||
} catch (error) {
|
} catch (_) {
|
||||||
toast('Unknown issue while authenticating. Try again')
|
toast('Unknown issue while authenticating. Try again')
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
48
src/serverActions/GetUserLoans.ts
Normal file
48
src/serverActions/GetUserLoans.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
'use server'
|
||||||
|
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
import config from '@/payload.config'
|
||||||
|
import type { PaginatedDocs } from "payload"
|
||||||
|
import { Checkout } from '@/payload-types'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
userId: number
|
||||||
|
}
|
||||||
|
export const getUserLoans = async (props: Props) => {
|
||||||
|
const { userId } = props
|
||||||
|
|
||||||
|
const payloadConfig = await config
|
||||||
|
const payload = await getPayload({ config: payloadConfig })
|
||||||
|
|
||||||
|
let userLoans: PaginatedDocs<Checkout> | null = null
|
||||||
|
if (userId)
|
||||||
|
userLoans = await payload.find({
|
||||||
|
collection: 'checkouts',
|
||||||
|
depth: 3,
|
||||||
|
limit: 10,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
copy: true,
|
||||||
|
dateDue: true,
|
||||||
|
ownerVerifiedReturnedDate: true,
|
||||||
|
loaneeReturnedDate: true,
|
||||||
|
},
|
||||||
|
sort: 'dateDue',
|
||||||
|
where: {
|
||||||
|
and: [
|
||||||
|
{
|
||||||
|
isReturned: {
|
||||||
|
not_equals: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'user.id': {
|
||||||
|
equals: userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}) as PaginatedDocs<Checkout>
|
||||||
|
|
||||||
|
return userLoans
|
||||||
|
}
|
||||||
@ -39,11 +39,13 @@ export const ownerReturnCheckout = async (props: Props): Promise<Checkout | null
|
|||||||
collection: 'checkouts',
|
collection: 'checkouts',
|
||||||
id: checkoutId,
|
id: checkoutId,
|
||||||
data: {
|
data: {
|
||||||
ownerVerifiedReturnedDate: new Date().toString(),
|
ownerVerifiedReturnedDate: new Date().toDateString(),
|
||||||
|
isReturned: true,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return updatedCheckout
|
return updatedCheckout
|
||||||
} catch (_) {
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user