175 lines
7.3 KiB
TypeScript
175 lines
7.3 KiB
TypeScript
import { PaginatedDocs } from 'payload'
|
|
import { Author, Book, HoldRequest, Repository, User } from '@/payload-types'
|
|
import { Button } from '../ui/button'
|
|
import Image from 'next/image'
|
|
import ApproveHoldRequestModal from './ApproveHoldRequestModal'
|
|
import { useCallback, useState } from 'react'
|
|
import CheckoutFromHoldModal from './CheckoutFromHoldModal'
|
|
import rejectHoldRequest from '@/serverActions/RejectHoldRequests'
|
|
import { getUserRepos } from '@/serverActions/GetUserRepos'
|
|
import { Loader2 } from 'lucide-react'
|
|
import { toast } from 'sonner'
|
|
|
|
type Props = {
|
|
repos: PaginatedDocs<Repository> | null
|
|
user: User
|
|
}
|
|
const HoldRequestNotifications = (props: Props) => {
|
|
const [repos, setRepos] = useState<PaginatedDocs<Repository> | null>(props.repos)
|
|
|
|
const [openedModalId, setOpenedModalId] = useState<number | null>(null)
|
|
|
|
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 handleRejectHoldClick = useCallback(
|
|
async (holdRequestId: number) => {
|
|
if (isRejectingId) return
|
|
setIsRejectingId(holdRequestId)
|
|
const rejectRequest = await rejectHoldRequest({ holdRequestId })
|
|
|
|
if (rejectRequest?.isRejected) {
|
|
const updatedRepos = await getUserRepos({ userId: props.user.id })
|
|
setRepos(updatedRepos)
|
|
toast('Request was rejected')
|
|
} else {
|
|
toast('Error rejecting request')
|
|
}
|
|
|
|
setIsRejectingId(null)
|
|
},
|
|
[isRejectingId, setIsRejectingId, props.user.id],
|
|
)
|
|
|
|
const holdRequestsByRepoElements = repos?.docs.map((r) => {
|
|
return (
|
|
<ul key={r.id} role="list" className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
{r.holdRequests?.docs
|
|
?.map((h) => {
|
|
const hold = h as HoldRequest
|
|
const book = hold.book as Book
|
|
const authors = book.authors as Author[]
|
|
const dateRequested = hold.dateRequested ? new Date(hold.dateRequested) : new Date()
|
|
const holdingUntilDate = hold.holdingUntilDate
|
|
? new Date(hold.holdingUntilDate)
|
|
: new Date()
|
|
const userRequested = hold.userRequested as User
|
|
const userName = `${userRequested.firstName} ${userRequested.lastName}`
|
|
|
|
if (hold.isRejected) return null
|
|
|
|
return (
|
|
<li key={hold.id} className="col-span-1 rounded-lg shadow-sm border border-accent">
|
|
<div className="flex w-full items-center justify-between space-x-6 p-6">
|
|
<div className="flex-1 truncate">
|
|
<div className="flex items-center space-x-3">
|
|
<h3 className="truncate text-sm font-medium text-foreground">{book.title}</h3>
|
|
</div>
|
|
<p className="mt-1 truncate text-sm text-gray-500">
|
|
{authors.map((a) => a.lf).join(' | ')}
|
|
</p>
|
|
{hold.isHolding ? (
|
|
<span className="text-wrap">
|
|
<span className="mr-0.5 text-xs">{userName} Until</span>
|
|
<time className="inline-flex shrink-0 items-center rounded-full bg-background/20 px-1.5 py-0.5 text-xs font-medium text-emerald-600 ring-1 ring-emerald-600/20 ring-inset">
|
|
{holdingUntilDate.toLocaleDateString()}
|
|
</time>
|
|
</span>
|
|
) : (
|
|
<span>
|
|
<time className="inline-flex shrink-0 items-center rounded-full bg-background/20 px-1.5 py-0.5 text-xs font-medium text-amber-500 ring-1 ring-amber-600/20 ring-inset">
|
|
{dateRequested.toLocaleDateString()}
|
|
</time>
|
|
<span className="text-xs">{userName}</span>
|
|
</span>
|
|
)}
|
|
</div>
|
|
<img
|
|
alt=""
|
|
className="h-16 shrink-0 rounded bg-gray-300"
|
|
src={
|
|
book.isbn
|
|
? `https://covers.openlibrary.org/b/isbn/${book.isbn}-M.jpg`
|
|
: '/images/book-48.svg'
|
|
}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<div className="flex gap-2 justify-around">
|
|
<Button
|
|
className="inline-flex flex-1 items-center justify-center gap-3 rounded-bl-lg border border-transparent py-4 text-sm font-semibold text-foreground bg-foreground/5 hover:bg-red-400/10 cursor-pointer [&:not(:disabled)]:hover:scale-105"
|
|
disabled={!!isRejectingId}
|
|
onClick={() => handleRejectHoldClick(hold.id)}
|
|
>
|
|
{isRejectingId === hold.id ? (
|
|
<>
|
|
<Loader2 className="animate-spin size-[24px] text-red-200 dark:text-red-700" />
|
|
<span className="text-red-200 dark:text-red-700">Rejecting</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Image
|
|
width={24}
|
|
height={24}
|
|
src="/images/reject.svg"
|
|
alt="approve hold"
|
|
/>
|
|
<span>Reject</span>
|
|
</>
|
|
)}
|
|
</Button>
|
|
|
|
<Button
|
|
className="inline-flex flex-1 items-center justify-center gap-3 rounded-br-lg border border-transparent py-4 text-sm font-semibold text-foreground bg-emerald-400/30 hover:bg-emerald-300/60 cursor-pointer hover:scale-105"
|
|
disabled={!!isRejectingId || !!openedModalId}
|
|
onClick={() => setOpenedModalId(hold.id)}
|
|
>
|
|
<Image width={24} height={24} src="/images/approve.svg" alt="approve hold" />
|
|
<span>{hold.isHolding ? 'Checkout' : 'Approve'}</span>
|
|
</Button>
|
|
|
|
{hold.isHolding ? (
|
|
<CheckoutFromHoldModal
|
|
isOpen={openedModalId === hold.id}
|
|
onOpenChange={() => setOpenedModalId(null)}
|
|
holdRequest={hold}
|
|
/>
|
|
) : (
|
|
<ApproveHoldRequestModal
|
|
isOpen={openedModalId === hold.id}
|
|
onOpenChange={() => setOpenedModalId(null)}
|
|
holdRequest={hold}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</li>
|
|
)
|
|
})
|
|
.filter((element) => !!element)}
|
|
</ul>
|
|
)
|
|
})
|
|
|
|
return (
|
|
<section className="py-6">
|
|
<div className="mb-4 flex justify-between items-end">
|
|
<h3 className="px-4 text-lg font-semibold">Incoming Hold Requests</h3>
|
|
{!!totalHoldNotifications && (
|
|
<span className="font-bold">{totalHoldNotifications} Unaddressed</span>
|
|
)}
|
|
</div>
|
|
{holdRequestsByRepoElements}
|
|
</section>
|
|
)
|
|
}
|
|
|
|
export default HoldRequestNotifications
|