From 6069b7ac906dab838fe1fd52432e66f5d8a286d8 Mon Sep 17 00:00:00 2001 From: vilinh Date: Sun, 5 May 2024 12:43:02 -0700 Subject: [PATCH 1/5] feat: organization approval workflow --- src/app/api/user/[id]/route.ts | 19 ++++++ src/app/api/user/route.ts | 15 +++++ src/app/sponsored-organizations/page.tsx | 72 +++++++++++++++------- src/components/sponsored-org-card.tsx | 77 +++++++++++++++++++++--- src/database/organization-schema.ts | 5 ++ 5 files changed, 155 insertions(+), 33 deletions(-) create mode 100644 src/app/api/user/[id]/route.ts create mode 100644 src/app/api/user/route.ts diff --git a/src/app/api/user/[id]/route.ts b/src/app/api/user/[id]/route.ts new file mode 100644 index 0000000..e73841a --- /dev/null +++ b/src/app/api/user/[id]/route.ts @@ -0,0 +1,19 @@ +import { NextRequest, NextResponse } from "next/server"; +import { clerkClient } from "@clerk/nextjs/server"; +import { ErrorResponse } from "@/lib/error"; + +export async function PUT(req: NextRequest, { params }: any) { + const { id } = params; + const body = await req.json(); + try { + const response = await clerkClient.users.updateUserMetadata(id, { + unsafeMetadata: body, + }); + return NextResponse.json(response, { status: 200 }); + } catch (err) { + const errorResponse: ErrorResponse = { + error: `Could not update user ${id}`, + }; + return NextResponse.json(errorResponse, { status: 404 }); + } +} diff --git a/src/app/api/user/route.ts b/src/app/api/user/route.ts new file mode 100644 index 0000000..b7c3104 --- /dev/null +++ b/src/app/api/user/route.ts @@ -0,0 +1,15 @@ +import { ErrorResponse } from "@/lib/error"; +import { clerkClient } from "@clerk/nextjs"; +import { NextRequest, NextResponse } from "next/server"; + +export async function GET() { + try { + const response = await clerkClient.users.getUserList(); + return NextResponse.json(response); + } catch (error) { + const errorResponse: ErrorResponse = { + error: "Error fetching reimbursements", + }; + return NextResponse.json(errorResponse, { status: 404 }); + } +} diff --git a/src/app/sponsored-organizations/page.tsx b/src/app/sponsored-organizations/page.tsx index 6126ad6..2d0e92a 100644 --- a/src/app/sponsored-organizations/page.tsx +++ b/src/app/sponsored-organizations/page.tsx @@ -21,6 +21,7 @@ const organizations: Organization[] = [ logo: "/images/sponsored_org_profile_picture_placeholder.png", reimbursements: [new Types.ObjectId("65c97b4056e2e2d7d225fe70")], status: "active", + approved: true, }, { name: "Organization 2", @@ -30,6 +31,7 @@ const organizations: Organization[] = [ logo: "/images/sponsored_org_profile_picture_placeholder.png", reimbursements: [], status: "active", + approved: true, }, { name: "Organization 3", @@ -39,6 +41,7 @@ const organizations: Organization[] = [ logo: "/images/sponsored_org_profile_picture_placeholder.png", reimbursements: [new Types.ObjectId("65c97b4056e2e2d7d225fe70")], status: "active", + approved: true, }, { name: "Organization 4", @@ -48,6 +51,7 @@ const organizations: Organization[] = [ logo: "/images/sponsored_org_profile_picture_placeholder.png", reimbursements: [], status: "active", + approved: true, }, { name: "Organization 5", @@ -57,6 +61,7 @@ const organizations: Organization[] = [ logo: "/images/sponsored_org_profile_picture_placeholder.png", reimbursements: [new Types.ObjectId("65c97b4056e2e2d7d225fe70")], status: "active", + approved: true, }, ]; @@ -68,18 +73,22 @@ async function filterOrganizationsWithPendingReimbursements( // Check each organization for (const org of organizations) { let pending = false; - for (const reimbursementId of org.reimbursements) { - // Fetch reimbursement from API - const reimbursement = await getReimbursement(reimbursementId.toString()); - // Check if reimbursement has a pending status - if (reimbursement && reimbursement.status === "Pending") { - pending = true; - break; // If it has at least one pending reimbursement, no need to keep checking the same organization + if (org?.reimbursements?.length) { + for (const reimbursementId of org.reimbursements) { + // Fetch reimbursement from API + const reimbursement = await getReimbursement( + reimbursementId.toString(), + ); + // Check if reimbursement has a pending status + if (reimbursement && reimbursement.status === "Pending") { + pending = true; + break; // If it has at least one pending reimbursement, no need to keep checking the same organization + } + } + // Add organization to list if it had a pending reimbursement + if (pending) { + filteredOrgs.push(org); } - } - // Add organization to list if it had a pending reimbursement - if (pending) { - filteredOrgs.push(org); } } return filteredOrgs; @@ -96,7 +105,8 @@ async function getOrganizations() { const organizations: Organization[] = []; data.forEach((obj: any) => { if (obj.unsafeMetadata.organization) { - organizations.push(obj.unsafeMetadata.organization); + let org = { clerkUser: obj.id, ...obj.unsafeMetadata.organization }; + organizations.push(org); } }); return organizations; @@ -126,19 +136,23 @@ async function getReimbursement(reimbursementId: string) { export default function Page() { const router = useRouter(); const { isLoaded, isSignedIn, user } = useUser(); - const [viewUpdates, setViewUpdates] = useState(false); // State for view updates toggle + const [viewTab, setViewTab] = useState(""); // State for tab toggle const [orgs, setOrgs] = useState([]); // State for currently displayed organizations based on view settings const [updatedOrgs, setUpdatedOrgs] = useState([]); // State for organizations with pending updates (filtered) + const [pendingOrgs, setPendingOrgs] = useState([]); // State for organizations pending approval const [allOrgs, setAllOrgs] = useState([]); // State for all organizations (unfiltered) - // Turn off viewUpdates when View All is toggled const handleViewAllToggle = () => { - setViewUpdates(false); + setViewTab(""); }; // Turn on viewUpdates when View Updates is toggled const handleViewUpdatesToggle = () => { - setViewUpdates(true); + setViewTab("updates"); + }; + + const handleViewPendingToggle = () => { + setViewTab("pending"); }; // Load information into orgs states @@ -152,19 +166,25 @@ export default function Page() { const fetchedOrgs = await getOrganizations(); if (fetchedOrgs) { setAllOrgs(fetchedOrgs); // Cache orgs for later - if (fetchedOrgs.length > 0) { const filteredUpdatedOrgs = await filterOrganizationsWithPendingReimbursements(fetchedOrgs); // Fetch organizations with updates console.log(filteredUpdatedOrgs); setUpdatedOrgs(filteredUpdatedOrgs); // Cache orgs for later + + let filteredPendingApprovalOrgs = fetchedOrgs.filter( + (org) => !org.approved, + ); + setPendingOrgs(filteredPendingApprovalOrgs); // Cache orgs for later } } } // Set orgs based on viewUpdates and availability of updatedOrgs - if (viewUpdates) { + if (viewTab === "updates") { filteredOrgs = updatedOrgs; // For displaying orgs with updates + } else if (viewTab === "pending") { + filteredOrgs = pendingOrgs; } else { filteredOrgs = allOrgs; // Otherwise display all orgs } @@ -210,14 +230,14 @@ export default function Page() {
VIEW ALL VIEW UPDATES @@ -228,6 +248,13 @@ export default function Page() {
+ + VIEW PENDING + {/* Search bar */}
@@ -244,10 +271,9 @@ export default function Page() { // Modify w to fit desired amount of cards in one row
))} diff --git a/src/components/sponsored-org-card.tsx b/src/components/sponsored-org-card.tsx index 190a59e..d683280 100644 --- a/src/components/sponsored-org-card.tsx +++ b/src/components/sponsored-org-card.tsx @@ -1,31 +1,86 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import Image from "next/image"; +import { Button } from "./ui/button"; +import { CheckIcon, Cross2Icon } from "@radix-ui/react-icons"; +import Organization from "@/database/organization-schema"; export interface SponsoredOrgCardProps { - image: string; - organization: string; - user: string; + organizationData: Organization; email: string; + toApprove: boolean; } +const updateOrg = (orgData: Organization, approve: Boolean) => { + let updatedOrg = orgData as any; + + if (approve) { + updatedOrg.approved = true; + if (updatedOrg.hasOwnProperty("clerkUser")) { + delete updatedOrg["clerkUser"]; + } + fetch(`/api/user/${orgData.clerkUser}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ organization: updatedOrg }), + }) + .then((response) => response.json()) + .catch((error) => { + console.log(error); + }); + } else { + fetch(`/api/user/${orgData.clerkUser}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ organization: null }), + }) + .then((response) => response.json()) + .catch((error) => { + console.log(error); + }); + } +}; + export default function SponsoredOrgCard({ - image, - organization, - user, + organizationData, email, + toApprove, }: SponsoredOrgCardProps) { return ( - + + {toApprove && ( +
+ { + updateOrg(organizationData, true); + console.log("clicked check"); + }} + /> + { + updateOrg(organizationData, false); + console.log("clicked x"); + }} + /> +
+ )} sponsored org logo - {organization} + {organizationData?.name} @@ -37,7 +92,9 @@ export default function SponsoredOrgCard({ height={18} alt="mail" /> -

{user}

+

+ {organizationData?.clerkUser} +

Date: Sun, 12 May 2024 10:09:42 -0700 Subject: [PATCH 2/5] fix: return arg --- src/app/sponsored-organizations/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/sponsored-organizations/page.tsx b/src/app/sponsored-organizations/page.tsx index 663629a..c7d8b90 100644 --- a/src/app/sponsored-organizations/page.tsx +++ b/src/app/sponsored-organizations/page.tsx @@ -127,7 +127,7 @@ export default function Page() { if (fetchedOrgs.length > 0) { const filteredUpdatedOrgs = await filterOrganizationsWithPendingReimbursements(fetchedOrgs); // Fetch organizations with updates - setUpdatedOrgs(filteredUpdatedOrgs); // Cache orgs for later + setUpdatedOrgs(filteredUpdatedOrgs.filteredOrgs); // Cache orgs for later let filteredPendingApprovalOrgs = fetchedOrgs.filter( (org) => !org.approved, From b4122ada92ef55925bf928786471adc427cdf20b Mon Sep 17 00:00:00 2001 From: vilinh Date: Sun, 12 May 2024 10:12:45 -0700 Subject: [PATCH 3/5] fix: type --- src/components/sponsored-org-card.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/sponsored-org-card.tsx b/src/components/sponsored-org-card.tsx index 6ea9b5e..4bf12b3 100644 --- a/src/components/sponsored-org-card.tsx +++ b/src/components/sponsored-org-card.tsx @@ -5,13 +5,13 @@ import { CheckIcon, Cross2Icon } from "@radix-ui/react-icons"; import Organization from "@/database/organization-schema"; export interface SponsoredOrgCardProps { - organizationData: Organization; + organizationData: typeof Organization; email: string; updates?: number; toApprove: boolean; } -const updateOrg = (orgData: Organization, approve: Boolean) => { +const updateOrg = (orgData: typeof Organization, approve: Boolean) => { let updatedOrg = orgData as any; if (approve) { From 4bfa045f1ea68dae0a321778ed3010e7faf57f3e Mon Sep 17 00:00:00 2001 From: vilinh Date: Sun, 12 May 2024 10:15:52 -0700 Subject: [PATCH 4/5] fix: import --- src/database/organization-schema.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/database/organization-schema.ts b/src/database/organization-schema.ts index 5adce5e..0e90f13 100644 --- a/src/database/organization-schema.ts +++ b/src/database/organization-schema.ts @@ -1,3 +1,5 @@ +import { Schema } from "mongoose"; + export interface Organization { name: string; description: string; From 8567d2d655af841a64cb2fc52c69195f31ca55e4 Mon Sep 17 00:00:00 2001 From: vilinh Date: Sun, 12 May 2024 10:22:06 -0700 Subject: [PATCH 5/5] fix: types and imports --- src/components/sponsored-org-card.tsx | 6 +++--- src/database/organization-schema.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/sponsored-org-card.tsx b/src/components/sponsored-org-card.tsx index 4bf12b3..3e0ec47 100644 --- a/src/components/sponsored-org-card.tsx +++ b/src/components/sponsored-org-card.tsx @@ -2,16 +2,16 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import Image from "next/image"; import { Button } from "./ui/button"; import { CheckIcon, Cross2Icon } from "@radix-ui/react-icons"; -import Organization from "@/database/organization-schema"; +import { Organization } from "@/database/organization-schema"; export interface SponsoredOrgCardProps { - organizationData: typeof Organization; + organizationData: Organization; email: string; updates?: number; toApprove: boolean; } -const updateOrg = (orgData: typeof Organization, approve: Boolean) => { +const updateOrg = (orgData: Organization, approve: Boolean) => { let updatedOrg = orgData as any; if (approve) { diff --git a/src/database/organization-schema.ts b/src/database/organization-schema.ts index 0e90f13..693a981 100644 --- a/src/database/organization-schema.ts +++ b/src/database/organization-schema.ts @@ -1,4 +1,4 @@ -import { Schema } from "mongoose"; +import { Schema, model, models } from "mongoose"; export interface Organization { name: string;