diff --git a/src/hooks/useDiscounts.tsx b/src/hooks/useDiscounts.tsx new file mode 100644 index 00000000..2285dcbf --- /dev/null +++ b/src/hooks/useDiscounts.tsx @@ -0,0 +1,22 @@ +import { Discount } from "@/interfaces/paypal"; +import { Code, Group, User } from "@/interfaces/user"; +import axios from "axios"; +import { useEffect, useState } from "react"; + +export default function useDiscounts(creator?: string) { + const [discounts, setDiscounts] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + + const getData = () => { + setIsLoading(true); + axios + .get("/api/discounts") + .then((response) => setDiscounts(response.data)) + .finally(() => setIsLoading(false)); + }; + + useEffect(getData, [creator]); + + return { discounts, isLoading, isError, reload: getData }; +} diff --git a/src/interfaces/paypal.ts b/src/interfaces/paypal.ts index 0606761d..d266b097 100644 --- a/src/interfaces/paypal.ts +++ b/src/interfaces/paypal.ts @@ -1,50 +1,55 @@ export interface TokenSuccess { - scope: string; - access_token: string; - token_type: string; - app_id: string; - expires_in: number; - nonce: string; + scope: string; + access_token: string; + token_type: string; + app_id: string; + expires_in: number; + nonce: string; } export interface TokenError { - error: string; - error_description: string; + error: string; + error_description: string; } export interface Package { - id: string; - currency: string; - duration: number; - duration_unit: DurationUnit; - price: number; + id: string; + currency: string; + duration: number; + duration_unit: DurationUnit; + price: number; +} + +export interface Discount { + id: string; + percentage: number; + domain: string; } export type DurationUnit = "weeks" | "days" | "months" | "years"; export interface Payment { - id: string; - corporate: string; - agent?: string; - agentCommission: number; - agentValue: number; - currency: string; - value: number; - isPaid: boolean; - date: Date | string; - corporateTransfer?: string; - commissionTransfer?: string; + id: string; + corporate: string; + agent?: string; + agentCommission: number; + agentValue: number; + currency: string; + value: number; + isPaid: boolean; + date: Date | string; + corporateTransfer?: string; + commissionTransfer?: string; } - export interface PaypalPayment { - orderId: string; - userId: string; - status: string; - createdAt: Date; - value: number; - currency: string; - subscriptionDuration: number; - subscriptionDurationUnit: DurationUnit; - subscriptionExpirationDate: Date; -} \ No newline at end of file + orderId: string; + userId: string; + status: string; + createdAt: Date; + value: number; + currency: string; + subscriptionDuration: number; + subscriptionDurationUnit: DurationUnit; + subscriptionExpirationDate: Date; +} diff --git a/src/pages/(admin)/Lists/DiscountList.tsx b/src/pages/(admin)/Lists/DiscountList.tsx new file mode 100644 index 00000000..46bdf08c --- /dev/null +++ b/src/pages/(admin)/Lists/DiscountList.tsx @@ -0,0 +1,342 @@ +import Button from "@/components/Low/Button"; +import Checkbox from "@/components/Low/Checkbox"; +import Input from "@/components/Low/Input"; +import Select from "@/components/Low/Select"; +import Modal from "@/components/Modal"; +import useCodes from "@/hooks/useCodes"; +import useDiscounts from "@/hooks/useDiscounts"; +import useUser from "@/hooks/useUser"; +import useUsers from "@/hooks/useUsers"; +import { Discount } from "@/interfaces/paypal"; +import { Code, User } from "@/interfaces/user"; +import { USER_TYPE_LABELS } from "@/resources/user"; +import { + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table"; +import axios from "axios"; +import moment from "moment"; +import { useEffect, useState } from "react"; +import { BsPencil, BsTrash } from "react-icons/bs"; +import { toast } from "react-toastify"; + +const columnHelper = createColumnHelper(); + +const DiscountCreator = ({ + discount, + onClose, +}: { + discount?: Discount; + onClose: () => void; +}) => { + const [percentage, setPercentage] = useState(discount?.percentage); + const [domain, setDomain] = useState(discount?.domain); + + const submit = async () => { + const body = { percentage, domain }; + + if (discount) { + return axios + .patch(`/api/discounts/${discount.id}`, body) + .then(() => { + toast.success("Discount has been edited successfully!"); + onClose(); + }) + .catch(() => { + toast.error("Something went wrong, please try again later!"); + }); + } + + return axios + .post(`/api/discounts`, body) + .then(() => { + toast.success("New discount has been created successfully!"); + onClose(); + }) + .catch(() => { + toast.error("Something went wrong, please try again later!"); + }); + }; + + return ( +
+
+
+ +
+ setDomain(e.replaceAll("@", ""))} + /> +
+
+
+ +
+ setPercentage(parseFloat(e))} + /> +
+
+
+
+ + +
+
+ ); +}; + +export default function DiscountList({ user }: { user: User }) { + const [selectedDiscounts, setSelectedDiscounts] = useState([]); + + const [isCreating, setIsCreating] = useState(false); + const [editingDiscount, setEditingDiscount] = useState(); + + const [filteredDiscounts, setFilteredDiscounts] = useState([]); + + const { users } = useUsers(); + const { discounts, reload } = useDiscounts(); + + useEffect(() => { + setFilteredDiscounts(discounts); + }, [discounts]); + + const toggleDiscount = (id: string) => { + setSelectedDiscounts((prev) => + prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id], + ); + }; + + const toggleAllDiscounts = (checked: boolean) => { + if (checked) + return setSelectedDiscounts(filteredDiscounts.map((x) => x.id)); + + return setSelectedDiscounts([]); + }; + + const deleteDiscounts = async (discounts: string[]) => { + if ( + !confirm( + `Are you sure you want to delete these ${discounts.length} discount(s)?`, + ) + ) + return; + + const params = new URLSearchParams(); + discounts.forEach((code) => params.append("discount", code)); + + axios + .delete(`/api/discounts?${params.toString()}`) + .then(() => toast.success(`Deleted the discount(s)!`)) + .catch((reason) => { + if (reason.response.status === 404) { + toast.error("Discount not found!"); + return; + } + + if (reason.response.status === 403) { + toast.error("You do not have permission to delete this discount!"); + return; + } + + toast.error("Something went wrong, please try again later."); + }) + .finally(reload); + }; + + const deleteDiscount = async (discount: Discount) => { + if ( + !confirm( + `Are you sure you want to delete this "${discount.id}" discount?`, + ) + ) + return; + + axios + .delete(`/api/discounts/${discount.id}`) + .then(() => toast.success(`Deleted the "${discount.id}" discount`)) + .catch((reason) => { + if (reason.response.status === 404) { + toast.error("Code not found!"); + return; + } + + if (reason.response.status === 403) { + toast.error("You do not have permission to delete this discount!"); + return; + } + + toast.error("Something went wrong, please try again later."); + }) + .finally(reload); + }; + + const defaultColumns = [ + columnHelper.accessor("id", { + id: "id", + header: () => ( + 0 + } + onChange={(checked) => toggleAllDiscounts(checked)} + > + {""} + + ), + cell: (info) => ( + toggleDiscount(info.getValue())} + > + {""} + + ), + }), + columnHelper.accessor("id", { + header: "ID", + cell: (info) => info.getValue(), + }), + columnHelper.accessor("domain", { + header: "Domain", + cell: (info) => `@${info.getValue()}`, + }), + columnHelper.accessor("percentage", { + header: "Percentage", + cell: (info) => `${info.getValue()}%`, + }), + { + header: "", + id: "actions", + cell: ({ row }: { row: { original: Discount } }) => { + return ( +
+
{ + setEditingDiscount(row.original); + }} + > + +
+
deleteDiscount(row.original)} + > + +
+
+ ); + }, + }, + ]; + + const table = useReactTable({ + data: filteredDiscounts, + columns: defaultColumns, + getCoreRowModel: getCoreRowModel(), + }); + + const closeModal = () => { + setIsCreating(false); + setEditingDiscount(undefined); + reload(); + }; + + return ( + <> + + + +
+
+ {selectedDiscounts.length} code(s) selected + +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + +
+ {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+ + + ); +} diff --git a/src/pages/(admin)/Lists/index.tsx b/src/pages/(admin)/Lists/index.tsx index e83e1017..0f8568eb 100644 --- a/src/pages/(admin)/Lists/index.tsx +++ b/src/pages/(admin)/Lists/index.tsx @@ -1,101 +1,138 @@ -import {User} from "@/interfaces/user"; -import {Tab} from "@headlessui/react"; +import { User } from "@/interfaces/user"; +import { Tab } from "@headlessui/react"; import clsx from "clsx"; import CodeList from "./CodeList"; +import DiscountList from "./DiscountList"; import ExamList from "./ExamList"; import GroupList from "./GroupList"; import PackageList from "./PackageList"; import UserList from "./UserList"; -export default function Lists({user}: {user: User}) { - return ( - - - - clsx( - "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light", - "ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2", - "transition duration-300 ease-in-out", - selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark", - ) - }> - User List - - {user?.type === "developer" && ( - - clsx( - "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light", - "ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2", - "transition duration-300 ease-in-out", - selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark", - ) - }> - Exam List - - )} - - clsx( - "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light", - "ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2", - "transition duration-300 ease-in-out", - selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark", - ) - }> - Group List - - {user && ["developer", "admin", "corporate"].includes(user.type) && ( - - clsx( - "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light", - "ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2", - "transition duration-300 ease-in-out", - selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark", - ) - }> - Code List - - )} - {user && ["developer", "admin"].includes(user.type) && ( - - clsx( - "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light", - "ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2", - "transition duration-300 ease-in-out", - selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark", - ) - }> - Package List - - )} - - - - - - {user?.type === "developer" && ( - - - - )} - - - - {user && ["developer", "admin", "corporate"].includes(user.type) && ( - - - - )} - {user && ["developer", "admin"].includes(user.type) && ( - - - - )} - - - ); +export default function Lists({ user }: { user: User }) { + return ( + + + + clsx( + "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light", + "ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2", + "transition duration-300 ease-in-out", + selected + ? "bg-white shadow" + : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark", + ) + } + > + User List + + {user?.type === "developer" && ( + + clsx( + "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light", + "ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2", + "transition duration-300 ease-in-out", + selected + ? "bg-white shadow" + : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark", + ) + } + > + Exam List + + )} + + clsx( + "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light", + "ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2", + "transition duration-300 ease-in-out", + selected + ? "bg-white shadow" + : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark", + ) + } + > + Group List + + {user && ["developer", "admin", "corporate"].includes(user.type) && ( + + clsx( + "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light", + "ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2", + "transition duration-300 ease-in-out", + selected + ? "bg-white shadow" + : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark", + ) + } + > + Code List + + )} + {user && ["developer", "admin"].includes(user.type) && ( + + clsx( + "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light", + "ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2", + "transition duration-300 ease-in-out", + selected + ? "bg-white shadow" + : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark", + ) + } + > + Package List + + )} + {user && ["developer", "admin"].includes(user.type) && ( + + clsx( + "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light", + "ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2", + "transition duration-300 ease-in-out", + selected + ? "bg-white shadow" + : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark", + ) + } + > + Discount List + + )} + + + + + + {user?.type === "developer" && ( + + + + )} + + + + {user && ["developer", "admin", "corporate"].includes(user.type) && ( + + + + )} + {user && ["developer", "admin"].includes(user.type) && ( + + + + )} + {user && ["developer", "admin"].includes(user.type) && ( + + + + )} + + + ); } diff --git a/src/pages/(status)/PaymentDue.tsx b/src/pages/(status)/PaymentDue.tsx index e6aab64b..5809653b 100644 --- a/src/pages/(status)/PaymentDue.tsx +++ b/src/pages/(status)/PaymentDue.tsx @@ -4,217 +4,331 @@ import PayPalPayment from "@/components/PayPalPayment"; import useGroups from "@/hooks/useGroups"; import usePackages from "@/hooks/usePackages"; import useUsers from "@/hooks/useUsers"; -import {User} from "@/interfaces/user"; +import { User } from "@/interfaces/user"; import clsx from "clsx"; -import {capitalize} from "lodash"; -import {useState} from "react"; +import { capitalize } from "lodash"; +import { useEffect, useState } from "react"; import getSymbolFromCurrency from "currency-symbol-map"; import useInvites from "@/hooks/useInvites"; -import {BsArrowRepeat} from "react-icons/bs"; +import { BsArrowRepeat } from "react-icons/bs"; import InviteCard from "@/components/Medium/InviteCard"; -import {useRouter} from "next/router"; -import {PayPalScriptProvider} from "@paypal/react-paypal-js"; +import { useRouter } from "next/router"; +import { PayPalScriptProvider } from "@paypal/react-paypal-js"; import { usePaypalTracking } from "@/hooks/usePaypalTracking"; -import {ToastContainer} from "react-toastify"; - +import { ToastContainer } from "react-toastify"; +import useDiscounts from "@/hooks/useDiscounts"; interface Props { - user: User; - hasExpired?: boolean; - clientID: string; - reload: () => void; + user: User; + hasExpired?: boolean; + clientID: string; + reload: () => void; } -export default function PaymentDue({user, hasExpired = false, clientID, reload}: Props) { - const [isLoading, setIsLoading] = useState(false); +export default function PaymentDue({ + user, + hasExpired = false, + clientID, + reload, +}: Props) { + const [isLoading, setIsLoading] = useState(false); + const [appliedDiscount, setAppliedDiscount] = useState(0); - const router = useRouter(); + const router = useRouter(); - const {packages} = usePackages(); - const {users} = useUsers(); - const {groups} = useGroups(); - const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user?.id}); - const trackingId = usePaypalTracking(); - - const isIndividual = () => { - if (user?.type === "developer") return true; - if (user?.type !== "student") return false; - const userGroups = groups.filter((g) => g.participants.includes(user?.id)); + const { packages } = usePackages(); + const { discounts } = useDiscounts(); + const { users } = useUsers(); + const { groups } = useGroups(); + const { + invites, + isLoading: isInvitesLoading, + reload: reloadInvites, + } = useInvites({ to: user?.id }); + const trackingId = usePaypalTracking(); - if (userGroups.length === 0) return true; + useEffect(() => { + const userDiscounts = discounts.filter((x) => + user.email.endsWith(`@${x.domain}`), + ); + if (userDiscounts.length === 0) return; - const userGroupsAdminTypes = userGroups.map((g) => users?.find((u) => u.id === g.admin)?.type).filter((t) => !!t); - return userGroupsAdminTypes.every((t) => t !== "corporate"); - }; + const biggestDiscount = [...userDiscounts] + .sort((a, b) => b.percentage - a.percentage) + .shift(); + if (!biggestDiscount) return; - return ( - <> - - {isLoading && ( -
-
- - Completing your payment... -
-
- )} - {user ? ( - - {invites.length > 0 && ( -
-
-
- Invites - -
-
- - {invites.map((invite) => ( - { - reloadInvites(); - router.reload(); - }} - /> - ))} - -
- )} + setAppliedDiscount(biggestDiscount.percentage); + }, [discounts, user]); -
- {hasExpired && You do not have time credits for your account type!} - {isIndividual() && ( -
- - To add to your use of EnCoach, please purchase one of the time packages available below: - -
- - {packages.map((p) => ( -
-
- EnCoach's Logo - - EnCoach - {p.duration}{" "} - {capitalize( - p.duration === 1 ? p.duration_unit.slice(0, p.duration_unit.length - 1) : p.duration_unit, - )} - -
-
- - {p.price} - {getSymbolFromCurrency(p.currency)} - - { - setTimeout(reload, 500); - }} - trackingId={trackingId} - /> -
-
- This includes: -
    -
  • - Train your abilities for the IELTS exam
  • -
  • - Gain insights into your weaknesses and strengths
  • -
  • - Allow yourself to correctly prepare for the exam
  • -
-
-
- ))} -
-
-
- )} - {!isIndividual() && user.type === "corporate" && user?.corporateInformation.payment && ( -
- - To add to your use of EnCoach and that of your students and teachers, please pay your designated package below: - -
-
- EnCoach's Logo - EnCoach - {user.corporateInformation?.monthlyDuration} Months -
-
- - {user.corporateInformation.payment.value} - {getSymbolFromCurrency(user.corporateInformation.payment.currency)} - - { - setIsLoading(false); - setTimeout(reload, 500); - }} - loadScript - trackingId={trackingId} - /> -
-
- This includes: -
    -
  • - - Allow a total of {user.corporateInformation.companyInformation.userAmount} students and teachers to - use EnCoach -
  • -
  • - Train their abilities for the IELTS exam
  • -
  • - Gain insights into your students' weaknesses and strengths
  • -
  • - Allow them to correctly prepare for the exam
  • -
-
-
-
- )} - {!isIndividual() && user.type !== "corporate" && ( -
- - You are not the person in charge of your time credits, please contact your administrator about this situation. - - - If you believe this to be a mistake, please contact the platform's administration, thank you for your - patience. - -
- )} - {!isIndividual() && user.type === "corporate" && !user.corporateInformation.payment && ( -
- - An admin nor your agent have yet set the price intended to your requirements in terms of the amount of users you - desire and your expected monthly duration. - - - Please try again later or contact your agent or an admin, thank you for your patience. - -
- )} -
-
- ) : ( -
- )} - - ); + const isIndividual = () => { + if (user?.type === "developer") return true; + if (user?.type !== "student") return false; + const userGroups = groups.filter((g) => g.participants.includes(user?.id)); + + if (userGroups.length === 0) return true; + + const userGroupsAdminTypes = userGroups + .map((g) => users?.find((u) => u.id === g.admin)?.type) + .filter((t) => !!t); + return userGroupsAdminTypes.every((t) => t !== "corporate"); + }; + + return ( + <> + + {isLoading && ( +
+
+ + + Completing your payment... + +
+
+ )} + {user ? ( + + {invites.length > 0 && ( +
+
+
+ + Invites + + +
+
+ + {invites.map((invite) => ( + { + reloadInvites(); + router.reload(); + }} + /> + ))} + +
+ )} + +
+ {hasExpired && ( + + You do not have time credits for your account type! + + )} + {isIndividual() && ( +
+ + To add to your use of EnCoach, please purchase one of the time + packages available below: + +
+ + {packages.map((p) => ( +
+
+ EnCoach's Logo + + EnCoach - {p.duration}{" "} + {capitalize( + p.duration === 1 + ? p.duration_unit.slice( + 0, + p.duration_unit.length - 1, + ) + : p.duration_unit, + )} + +
+
+ {!appliedDiscount && ( + + {p.price} + {getSymbolFromCurrency(p.currency)} + + )} + {appliedDiscount && ( +
+ + {p.price} + {getSymbolFromCurrency(p.currency)} + + + {( + p.price - + p.price * (appliedDiscount / 100) + ).toFixed(2)} + {getSymbolFromCurrency(p.currency)} + +
+ )} + { + setTimeout(reload, 500); + }} + trackingId={trackingId} + currency={p.currency} + duration={p.duration} + duration_unit={p.duration_unit} + price={ + +( + p.price - + p.price * (appliedDiscount / 100) + ).toFixed(2) + } + /> +
+
+ This includes: +
    +
  • - Train your abilities for the IELTS exam
  • +
  • + - Gain insights into your weaknesses and strengths +
  • +
  • + - Allow yourself to correctly prepare for the exam +
  • +
+
+
+ ))} +
+
+
+ )} + {!isIndividual() && + user.type === "corporate" && + user?.corporateInformation.payment && ( +
+ + To add to your use of EnCoach and that of your students and + teachers, please pay your designated package below: + +
+
+ EnCoach's Logo + + EnCoach - {user.corporateInformation?.monthlyDuration}{" "} + Months + +
+
+ + {user.corporateInformation.payment.value} + {getSymbolFromCurrency( + user.corporateInformation.payment.currency, + )} + + { + setIsLoading(false); + setTimeout(reload, 500); + }} + loadScript + trackingId={trackingId} + /> +
+
+ This includes: +
    +
  • + - Allow a total of{" "} + { + user.corporateInformation.companyInformation + .userAmount + }{" "} + students and teachers to use EnCoach +
  • +
  • - Train their abilities for the IELTS exam
  • +
  • + - Gain insights into your students' weaknesses + and strengths +
  • +
  • - Allow them to correctly prepare for the exam
  • +
+
+
+
+ )} + {!isIndividual() && user.type !== "corporate" && ( +
+ + You are not the person in charge of your time credits, please + contact your administrator about this situation. + + + If you believe this to be a mistake, please contact the + platform's administration, thank you for your patience. + +
+ )} + {!isIndividual() && + user.type === "corporate" && + !user.corporateInformation.payment && ( +
+ + An admin nor your agent have yet set the price intended to + your requirements in terms of the amount of users you desire + and your expected monthly duration. + + + Please try again later or contact your agent or an admin, + thank you for your patience. + +
+ )} +
+
+ ) : ( +
+ )} + + ); } diff --git a/src/pages/api/discounts/[id].ts b/src/pages/api/discounts/[id].ts new file mode 100644 index 00000000..9683f7a6 --- /dev/null +++ b/src/pages/api/discounts/[id].ts @@ -0,0 +1,94 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from "next"; +import { app } from "@/firebase"; +import { + getFirestore, + doc, + getDoc, + deleteDoc, + setDoc, +} from "firebase/firestore"; +import { withIronSessionApiRoute } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; +import { PERMISSIONS } from "@/constants/userPermissions"; + +const db = getFirestore(app); + +export default withIronSessionApiRoute(handler, sessionOptions); + +async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method === "GET") return get(req, res); + if (req.method === "DELETE") return del(req, res); + if (req.method === "PATCH") return patch(req, res); +} + +async function get(req: NextApiRequest, res: NextApiResponse) { + if (!req.session.user) { + res.status(401).json({ ok: false }); + return; + } + + const { id } = req.query as { id: string }; + + const docRef = doc(db, "discounts", id); + const docSnap = await getDoc(docRef); + + if (docSnap.exists()) { + res.status(200).json({ + id: docSnap.id, + ...docSnap.data(), + }); + } else { + res.status(404).json(undefined); + } +} + +async function patch(req: NextApiRequest, res: NextApiResponse) { + if (!req.session.user) { + res.status(401).json({ ok: false }); + return; + } + + const { id } = req.query as { id: string }; + + const docRef = doc(db, "discounts", id); + const docSnap = await getDoc(docRef); + + if (docSnap.exists()) { + if (!["developer", "admin"].includes(req.session.user.type)) { + res.status(403).json({ ok: false }); + return; + } + + await setDoc(docRef, req.body, { merge: true }); + + res.status(200).json({ ok: true }); + } else { + res.status(404).json({ ok: false }); + } +} + +async function del(req: NextApiRequest, res: NextApiResponse) { + if (!req.session.user) { + res.status(401).json({ ok: false }); + return; + } + + const { id } = req.query as { id: string }; + + const docRef = doc(db, "discounts", id); + const docSnap = await getDoc(docRef); + + if (docSnap.exists()) { + if (!["developer", "admin"].includes(req.session.user.type)) { + res.status(403).json({ ok: false }); + return; + } + + await deleteDoc(docRef); + + res.status(200).json({ ok: true }); + } else { + res.status(404).json({ ok: false }); + } +} diff --git a/src/pages/api/discounts/index.ts b/src/pages/api/discounts/index.ts new file mode 100644 index 00000000..c5d768d5 --- /dev/null +++ b/src/pages/api/discounts/index.ts @@ -0,0 +1,81 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from "next"; +import { app } from "@/firebase"; +import { + getFirestore, + collection, + getDocs, + setDoc, + doc, + getDoc, + deleteDoc, +} from "firebase/firestore"; +import { withIronSessionApiRoute } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; +import { Group } from "@/interfaces/user"; +import { Discount, Package } from "@/interfaces/paypal"; +import { v4 } from "uuid"; + +const db = getFirestore(app); + +export default withIronSessionApiRoute(handler, sessionOptions); + +async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method === "GET") await get(req, res); + if (req.method === "POST") await post(req, res); + if (req.method === "DELETE") return del(req, res); +} + +async function get(req: NextApiRequest, res: NextApiResponse) { + if (!req.session.user) { + res.status(401).json({ ok: false }); + return; + } + + const snapshot = await getDocs(collection(db, "discounts")); + + res.status(200).json( + snapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })), + ); +} + +async function post(req: NextApiRequest, res: NextApiResponse) { + if (!req.session.user) { + res.status(401).json({ ok: false }); + return; + } + + if (!["developer", "admin"].includes(req.session.user!.type)) + return res.status(403).json({ + ok: false, + reason: "You do not have permission to create a new discount", + }); + + const body = req.body as Discount; + + await setDoc(doc(db, "discounts", v4()), body); + res.status(200).json({ ok: true }); +} + +async function del(req: NextApiRequest, res: NextApiResponse) { + if (!req.session.user) { + res + .status(401) + .json({ ok: false, reason: "You must be logged in to generate a code!" }); + return; + } + + const discounts = req.query.discount as string[]; + + for (const discount of discounts) { + const snapshot = await getDoc(doc(db, "discounts", discount as string)); + if (!snapshot.exists()) continue; + + await deleteDoc(snapshot.ref); + } + + res.status(200).json({ discounts }); +} diff --git a/src/pages/profile.tsx b/src/pages/profile.tsx index 28d3558d..dbc2ca9d 100644 --- a/src/pages/profile.tsx +++ b/src/pages/profile.tsx @@ -309,7 +309,10 @@ function UserProfile({ user, mutateUser }: Props) {

Edit Profile

-
+ e.preventDefault()} + > {user.type !== "corporate" ? (