From 1d817b762b4e5598b32fe71f93a832b67f324dc2 Mon Sep 17 00:00:00 2001 From: Yehoshua Sandler Date: Wed, 14 May 2025 10:27:59 -0500 Subject: [PATCH] feat: owner return book --- src/app/(frontend)/manage/page.client.tsx | 5 +- src/app/(frontend)/manage/page.tsx | 32 +++- src/app/(frontend)/page.tsx | 32 +++- src/components/Manage/HoldRequests.tsx | 8 +- src/components/Manage/LoanedBooks.tsx | 218 ++++++++++++++++++++++ src/components/Manage/Manage.tsx | 8 +- src/components/login-form.tsx | 2 +- src/serverActions/GetUserLoans.ts | 48 +++++ src/serverActions/ReturnCheckout.ts | 6 +- 9 files changed, 350 insertions(+), 9 deletions(-) create mode 100644 src/components/Manage/LoanedBooks.tsx create mode 100644 src/serverActions/GetUserLoans.ts diff --git a/src/app/(frontend)/manage/page.client.tsx b/src/app/(frontend)/manage/page.client.tsx index 2bac0cc..09d2a49 100644 --- a/src/app/(frontend)/manage/page.client.tsx +++ b/src/app/(frontend)/manage/page.client.tsx @@ -7,11 +7,12 @@ import { PaginatedDocs } from 'payload' type Props = { repos: PaginatedDocs | null borrows: PaginatedDocs | null + loans: PaginatedDocs | null user: User | null } const ManagePageClient = (props: Props) => { - const { repos, borrows, user } = props - return + const { repos, borrows, loans, user } = props + return } export default ManagePageClient diff --git a/src/app/(frontend)/manage/page.tsx b/src/app/(frontend)/manage/page.tsx index 9ba0b0f..4413f85 100644 --- a/src/app/(frontend)/manage/page.tsx +++ b/src/app/(frontend)/manage/page.tsx @@ -78,10 +78,40 @@ const ManagePage = async () => { }, }) + let userLoans: PaginatedDocs | 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 ( <> - + ) } diff --git a/src/app/(frontend)/page.tsx b/src/app/(frontend)/page.tsx index 4289125..d20df56 100644 --- a/src/app/(frontend)/page.tsx +++ b/src/app/(frontend)/page.tsx @@ -89,6 +89,36 @@ export default async function HomePage() { }, }) + let userLoans: PaginatedDocs | 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 (
@@ -110,7 +140,7 @@ export default async function HomePage() { - + ) : ( diff --git a/src/components/Manage/HoldRequests.tsx b/src/components/Manage/HoldRequests.tsx index 3bc1435..683c5b2 100644 --- a/src/components/Manage/HoldRequests.tsx +++ b/src/components/Manage/HoldRequests.tsx @@ -19,7 +19,13 @@ const HoldRequestNotifications = (props: Props) => { const [openedModalId, setOpenedModalId] = useState(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() diff --git a/src/components/Manage/LoanedBooks.tsx b/src/components/Manage/LoanedBooks.tsx new file mode 100644 index 0000000..181c8a0 --- /dev/null +++ b/src/components/Manage/LoanedBooks.tsx @@ -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 +} +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 ( +
    + {rows.map((r) => ( +
  • +
    +
    +

    + {`[${r.repositoryAbbreviation}] `} + {r.title} +

    + {!!r.status && ( +

    + {r.status} +

    + )} +
    +
    +

    + + Due on{' '} + + +

    + + + +

    Borrowed from {r.owners.join(', ')}

    +
    +
    +
    + + + + Open options + + + + + + {/* + + Repository, {r.repositoryAbbreviation} + + */} + + + View {r.title} + + + + +
    +
  • + ))} +
+ ) +} + +type Props = { initialLoans: PaginatedDocs | null } +const LoanedBooks = (props: Props) => { + const { initialLoans } = props + + const { user } = useGlobal() + + const [loans, setLoans] = useState | 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 ( +
+
+

Loaned Out Books

+
+ +
+ ) +} + +export default LoanedBooks diff --git a/src/components/Manage/Manage.tsx b/src/components/Manage/Manage.tsx index 41e311c..f18e780 100644 --- a/src/components/Manage/Manage.tsx +++ b/src/components/Manage/Manage.tsx @@ -5,14 +5,16 @@ import { PaginatedDocs } from 'payload' import RepoList from './RepoList' import HoldRequestNotifications from './HoldRequests' import BorrowedBooks from './BorrowedBooks' +import LoanedBooks from './LoanedBooks' type Props = { repos: PaginatedDocs | null borrows: PaginatedDocs | null + loans: PaginatedDocs | null user: User | null } const Manage = (props: Props) => { - const { repos, borrows, user } = props + const { repos, borrows, loans, user } = props return (
@@ -25,6 +27,10 @@ const Manage = (props: Props) => {
+ +
+ +
) } diff --git a/src/components/login-form.tsx b/src/components/login-form.tsx index 007c70f..8ab68df 100644 --- a/src/components/login-form.tsx +++ b/src/components/login-form.tsx @@ -52,7 +52,7 @@ export function LoginForm({ className, ...props }: React.ComponentProps<'div'>) if (loginResponse.user) setUser(loginResponse.user) if (loginResponse.token) router.push(searchParams.get('redirectUrl') || '/') - } catch (error) { + } catch (_) { toast('Unknown issue while authenticating. Try again') setIsLoading(false) } diff --git a/src/serverActions/GetUserLoans.ts b/src/serverActions/GetUserLoans.ts new file mode 100644 index 0000000..4d629fb --- /dev/null +++ b/src/serverActions/GetUserLoans.ts @@ -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 | 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 + + return userLoans +} diff --git a/src/serverActions/ReturnCheckout.ts b/src/serverActions/ReturnCheckout.ts index 08c3097..44083b3 100644 --- a/src/serverActions/ReturnCheckout.ts +++ b/src/serverActions/ReturnCheckout.ts @@ -39,11 +39,13 @@ export const ownerReturnCheckout = async (props: Props): Promise