Merged from develop
This commit is contained in:
22
src/hooks/useDiscounts.tsx
Normal file
22
src/hooks/useDiscounts.tsx
Normal file
@@ -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<Discount[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isError, setIsError] = useState(false);
|
||||||
|
|
||||||
|
const getData = () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
axios
|
||||||
|
.get<Discount[]>("/api/discounts")
|
||||||
|
.then((response) => setDiscounts(response.data))
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(getData, [creator]);
|
||||||
|
|
||||||
|
return { discounts, isLoading, isError, reload: getData };
|
||||||
|
}
|
||||||
@@ -20,6 +20,12 @@ export interface Package {
|
|||||||
price: number;
|
price: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Discount {
|
||||||
|
id: string;
|
||||||
|
percentage: number;
|
||||||
|
domain: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type DurationUnit = "weeks" | "days" | "months" | "years";
|
export type DurationUnit = "weeks" | "days" | "months" | "years";
|
||||||
|
|
||||||
export interface Payment {
|
export interface Payment {
|
||||||
@@ -36,7 +42,6 @@ export interface Payment {
|
|||||||
commissionTransfer?: string;
|
commissionTransfer?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface PaypalPayment {
|
export interface PaypalPayment {
|
||||||
orderId: string;
|
orderId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
|||||||
342
src/pages/(admin)/Lists/DiscountList.tsx
Normal file
342
src/pages/(admin)/Lists/DiscountList.tsx
Normal file
@@ -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<Discount>();
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="flex flex-col gap-8 py-8">
|
||||||
|
<div className="w-full grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">
|
||||||
|
Domain *
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-4 items-center">
|
||||||
|
<Input
|
||||||
|
defaultValue={domain}
|
||||||
|
placeholder="encoach.com"
|
||||||
|
name="domain"
|
||||||
|
type="text"
|
||||||
|
onChange={(e) => setDomain(e.replaceAll("@", ""))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">
|
||||||
|
Percentage (in %) *
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-4 items-center">
|
||||||
|
<Input
|
||||||
|
defaultValue={percentage}
|
||||||
|
placeholder="20"
|
||||||
|
name="percentage"
|
||||||
|
type="number"
|
||||||
|
onChange={(e) => setPercentage(parseFloat(e))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full justify-end items-center gap-8 mt-8">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="red"
|
||||||
|
className="w-full max-w-[200px]"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="w-full max-w-[200px]"
|
||||||
|
onClick={submit}
|
||||||
|
disabled={!percentage || !domain}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DiscountList({ user }: { user: User }) {
|
||||||
|
const [selectedDiscounts, setSelectedDiscounts] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
|
const [editingDiscount, setEditingDiscount] = useState<Discount>();
|
||||||
|
|
||||||
|
const [filteredDiscounts, setFilteredDiscounts] = useState<Discount[]>([]);
|
||||||
|
|
||||||
|
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: () => (
|
||||||
|
<Checkbox
|
||||||
|
disabled={filteredDiscounts.length === 0}
|
||||||
|
isChecked={
|
||||||
|
selectedDiscounts.length === filteredDiscounts.length &&
|
||||||
|
filteredDiscounts.length > 0
|
||||||
|
}
|
||||||
|
onChange={(checked) => toggleAllDiscounts(checked)}
|
||||||
|
>
|
||||||
|
{""}
|
||||||
|
</Checkbox>
|
||||||
|
),
|
||||||
|
cell: (info) => (
|
||||||
|
<Checkbox
|
||||||
|
isChecked={selectedDiscounts.includes(info.getValue())}
|
||||||
|
onChange={() => toggleDiscount(info.getValue())}
|
||||||
|
>
|
||||||
|
{""}
|
||||||
|
</Checkbox>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
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 (
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<div
|
||||||
|
data-tip="Delete"
|
||||||
|
className="cursor-pointer tooltip"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingDiscount(row.original);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BsPencil className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
data-tip="Delete"
|
||||||
|
className="cursor-pointer tooltip"
|
||||||
|
onClick={() => deleteDiscount(row.original)}
|
||||||
|
>
|
||||||
|
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: filteredDiscounts,
|
||||||
|
columns: defaultColumns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setIsCreating(false);
|
||||||
|
setEditingDiscount(undefined);
|
||||||
|
reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
isOpen={isCreating || !!editingDiscount}
|
||||||
|
onClose={closeModal}
|
||||||
|
title={
|
||||||
|
editingDiscount ? `Editing ${editingDiscount.id}` : "New Discount"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DiscountCreator onClose={closeModal} discount={editingDiscount} />
|
||||||
|
</Modal>
|
||||||
|
<div className="flex items-center justify-end pb-4 pt-1">
|
||||||
|
<div className="flex gap-4 items-center">
|
||||||
|
<span>{selectedDiscounts.length} code(s) selected</span>
|
||||||
|
<Button
|
||||||
|
disabled={selectedDiscounts.length === 0}
|
||||||
|
variant="outline"
|
||||||
|
color="red"
|
||||||
|
className="!py-1 px-10"
|
||||||
|
onClick={() => deleteDiscounts(selectedDiscounts)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
|
||||||
|
<thead>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<tr key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => (
|
||||||
|
<th className="p-4 text-left" key={header.id}>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext(),
|
||||||
|
)}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</thead>
|
||||||
|
<tbody className="px-2">
|
||||||
|
{table.getRowModel().rows.map((row) => (
|
||||||
|
<tr
|
||||||
|
className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2"
|
||||||
|
key={row.id}
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<td className="px-4 py-2" key={cell.id}>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsCreating(true)}
|
||||||
|
className="w-full py-2 bg-mti-purple-light hover:bg-mti-purple transition ease-in-out duration-300 text-white"
|
||||||
|
>
|
||||||
|
New Discount
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import {User} from "@/interfaces/user";
|
|||||||
import { Tab } from "@headlessui/react";
|
import { Tab } from "@headlessui/react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import CodeList from "./CodeList";
|
import CodeList from "./CodeList";
|
||||||
|
import DiscountList from "./DiscountList";
|
||||||
import ExamList from "./ExamList";
|
import ExamList from "./ExamList";
|
||||||
import GroupList from "./GroupList";
|
import GroupList from "./GroupList";
|
||||||
import PackageList from "./PackageList";
|
import PackageList from "./PackageList";
|
||||||
@@ -17,9 +18,12 @@ export default function Lists({user}: {user: User}) {
|
|||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light",
|
"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",
|
"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",
|
"transition duration-300 ease-in-out",
|
||||||
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
|
selected
|
||||||
|
? "bg-white shadow"
|
||||||
|
: "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
|
||||||
)
|
)
|
||||||
}>
|
}
|
||||||
|
>
|
||||||
User List
|
User List
|
||||||
</Tab>
|
</Tab>
|
||||||
{user?.type === "developer" && (
|
{user?.type === "developer" && (
|
||||||
@@ -29,9 +33,12 @@ export default function Lists({user}: {user: User}) {
|
|||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light",
|
"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",
|
"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",
|
"transition duration-300 ease-in-out",
|
||||||
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
|
selected
|
||||||
|
? "bg-white shadow"
|
||||||
|
: "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
|
||||||
)
|
)
|
||||||
}>
|
}
|
||||||
|
>
|
||||||
Exam List
|
Exam List
|
||||||
</Tab>
|
</Tab>
|
||||||
)}
|
)}
|
||||||
@@ -41,9 +48,12 @@ export default function Lists({user}: {user: User}) {
|
|||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light",
|
"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",
|
"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",
|
"transition duration-300 ease-in-out",
|
||||||
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
|
selected
|
||||||
|
? "bg-white shadow"
|
||||||
|
: "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
|
||||||
)
|
)
|
||||||
}>
|
}
|
||||||
|
>
|
||||||
Group List
|
Group List
|
||||||
</Tab>
|
</Tab>
|
||||||
{user && ["developer", "admin", "corporate"].includes(user.type) && (
|
{user && ["developer", "admin", "corporate"].includes(user.type) && (
|
||||||
@@ -53,9 +63,12 @@ export default function Lists({user}: {user: User}) {
|
|||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light",
|
"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",
|
"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",
|
"transition duration-300 ease-in-out",
|
||||||
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
|
selected
|
||||||
|
? "bg-white shadow"
|
||||||
|
: "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
|
||||||
)
|
)
|
||||||
}>
|
}
|
||||||
|
>
|
||||||
Code List
|
Code List
|
||||||
</Tab>
|
</Tab>
|
||||||
)}
|
)}
|
||||||
@@ -66,12 +79,31 @@ export default function Lists({user}: {user: User}) {
|
|||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light",
|
"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",
|
"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",
|
"transition duration-300 ease-in-out",
|
||||||
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
|
selected
|
||||||
|
? "bg-white shadow"
|
||||||
|
: "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
|
||||||
)
|
)
|
||||||
}>
|
}
|
||||||
|
>
|
||||||
Package List
|
Package List
|
||||||
</Tab>
|
</Tab>
|
||||||
)}
|
)}
|
||||||
|
{user && ["developer", "admin"].includes(user.type) && (
|
||||||
|
<Tab
|
||||||
|
className={({ selected }) =>
|
||||||
|
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
|
||||||
|
</Tab>
|
||||||
|
)}
|
||||||
</Tab.List>
|
</Tab.List>
|
||||||
<Tab.Panels className="mt-2">
|
<Tab.Panels className="mt-2">
|
||||||
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide">
|
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide">
|
||||||
@@ -95,6 +127,11 @@ export default function Lists({user}: {user: User}) {
|
|||||||
<PackageList user={user} />
|
<PackageList user={user} />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
)}
|
)}
|
||||||
|
{user && ["developer", "admin"].includes(user.type) && (
|
||||||
|
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide">
|
||||||
|
<DiscountList user={user} />
|
||||||
|
</Tab.Panel>
|
||||||
|
)}
|
||||||
</Tab.Panels>
|
</Tab.Panels>
|
||||||
</Tab.Group>
|
</Tab.Group>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import useUsers from "@/hooks/useUsers";
|
|||||||
import { User } from "@/interfaces/user";
|
import { User } from "@/interfaces/user";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { capitalize } from "lodash";
|
import { capitalize } from "lodash";
|
||||||
import {useState} from "react";
|
import { useEffect, useState } from "react";
|
||||||
import getSymbolFromCurrency from "currency-symbol-map";
|
import getSymbolFromCurrency from "currency-symbol-map";
|
||||||
import useInvites from "@/hooks/useInvites";
|
import useInvites from "@/hooks/useInvites";
|
||||||
import { BsArrowRepeat } from "react-icons/bs";
|
import { BsArrowRepeat } from "react-icons/bs";
|
||||||
@@ -16,7 +16,7 @@ import {useRouter} from "next/router";
|
|||||||
import { PayPalScriptProvider } from "@paypal/react-paypal-js";
|
import { PayPalScriptProvider } from "@paypal/react-paypal-js";
|
||||||
import { usePaypalTracking } from "@/hooks/usePaypalTracking";
|
import { usePaypalTracking } from "@/hooks/usePaypalTracking";
|
||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
|
import useDiscounts from "@/hooks/useDiscounts";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
@@ -25,17 +25,42 @@ interface Props {
|
|||||||
reload: () => void;
|
reload: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PaymentDue({user, hasExpired = false, clientID, reload}: Props) {
|
export default function PaymentDue({
|
||||||
|
user,
|
||||||
|
hasExpired = false,
|
||||||
|
clientID,
|
||||||
|
reload,
|
||||||
|
}: Props) {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [appliedDiscount, setAppliedDiscount] = useState(0);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { packages } = usePackages();
|
const { packages } = usePackages();
|
||||||
|
const { discounts } = useDiscounts();
|
||||||
const { users } = useUsers();
|
const { users } = useUsers();
|
||||||
const { groups } = useGroups();
|
const { groups } = useGroups();
|
||||||
const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user?.id});
|
const {
|
||||||
|
invites,
|
||||||
|
isLoading: isInvitesLoading,
|
||||||
|
reload: reloadInvites,
|
||||||
|
} = useInvites({ to: user?.id });
|
||||||
const trackingId = usePaypalTracking();
|
const trackingId = usePaypalTracking();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const userDiscounts = discounts.filter((x) =>
|
||||||
|
user.email.endsWith(`@${x.domain}`),
|
||||||
|
);
|
||||||
|
if (userDiscounts.length === 0) return;
|
||||||
|
|
||||||
|
const biggestDiscount = [...userDiscounts]
|
||||||
|
.sort((a, b) => b.percentage - a.percentage)
|
||||||
|
.shift();
|
||||||
|
if (!biggestDiscount) return;
|
||||||
|
|
||||||
|
setAppliedDiscount(biggestDiscount.percentage);
|
||||||
|
}, [discounts, user]);
|
||||||
|
|
||||||
const isIndividual = () => {
|
const isIndividual = () => {
|
||||||
if (user?.type === "developer") return true;
|
if (user?.type === "developer") return true;
|
||||||
if (user?.type !== "student") return false;
|
if (user?.type !== "student") return false;
|
||||||
@@ -43,7 +68,9 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}:
|
|||||||
|
|
||||||
if (userGroups.length === 0) return true;
|
if (userGroups.length === 0) return true;
|
||||||
|
|
||||||
const userGroupsAdminTypes = userGroups.map((g) => users?.find((u) => u.id === g.admin)?.type).filter((t) => !!t);
|
const userGroupsAdminTypes = userGroups
|
||||||
|
.map((g) => users?.find((u) => u.id === g.admin)?.type)
|
||||||
|
.filter((t) => !!t);
|
||||||
return userGroupsAdminTypes.every((t) => t !== "corporate");
|
return userGroupsAdminTypes.every((t) => t !== "corporate");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -54,7 +81,9 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}:
|
|||||||
<div className="absolute left-0 top-0 z-[999] h-screen w-screen overflow-hidden bg-black/60">
|
<div className="absolute left-0 top-0 z-[999] h-screen w-screen overflow-hidden bg-black/60">
|
||||||
<div className="absolute left-1/2 top-1/2 flex h-fit w-fit -translate-x-1/2 -translate-y-1/2 animate-pulse flex-col items-center gap-8 text-white">
|
<div className="absolute left-1/2 top-1/2 flex h-fit w-fit -translate-x-1/2 -translate-y-1/2 animate-pulse flex-col items-center gap-8 text-white">
|
||||||
<span className={clsx("loading loading-infinity w-48")} />
|
<span className={clsx("loading loading-infinity w-48")} />
|
||||||
<span className={clsx("text-2xl font-bold")}>Completing your payment...</span>
|
<span className={clsx("text-2xl font-bold")}>
|
||||||
|
Completing your payment...
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -65,9 +94,17 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}:
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div
|
<div
|
||||||
onClick={reloadInvites}
|
onClick={reloadInvites}
|
||||||
className="text-mti-purple-light hover:text-mti-purple-dark flex cursor-pointer items-center gap-2 transition duration-300 ease-in-out">
|
className="text-mti-purple-light hover:text-mti-purple-dark flex cursor-pointer items-center gap-2 transition duration-300 ease-in-out"
|
||||||
<span className="text-mti-black text-lg font-bold">Invites</span>
|
>
|
||||||
<BsArrowRepeat className={clsx("text-xl", isInvitesLoading && "animate-spin")} />
|
<span className="text-mti-black text-lg font-bold">
|
||||||
|
Invites
|
||||||
|
</span>
|
||||||
|
<BsArrowRepeat
|
||||||
|
className={clsx(
|
||||||
|
"text-xl",
|
||||||
|
isInvitesLoading && "animate-spin",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll">
|
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll">
|
||||||
@@ -87,11 +124,16 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}:
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex w-full flex-col items-center justify-center gap-4 text-center">
|
<div className="flex w-full flex-col items-center justify-center gap-4 text-center">
|
||||||
{hasExpired && <span className="text-lg font-bold">You do not have time credits for your account type!</span>}
|
{hasExpired && (
|
||||||
|
<span className="text-lg font-bold">
|
||||||
|
You do not have time credits for your account type!
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{isIndividual() && (
|
{isIndividual() && (
|
||||||
<div className="scrollbar-hide flex w-full flex-col items-center gap-12 overflow-x-scroll">
|
<div className="scrollbar-hide flex w-full flex-col items-center gap-12 overflow-x-scroll">
|
||||||
<span className="max-w-lg">
|
<span className="max-w-lg">
|
||||||
To add to your use of EnCoach, please purchase one of the time packages available below:
|
To add to your use of EnCoach, please purchase one of the time
|
||||||
|
packages available below:
|
||||||
</span>
|
</span>
|
||||||
<div className="flex w-full flex-wrap justify-center gap-8">
|
<div className="flex w-full flex-wrap justify-center gap-8">
|
||||||
<PayPalScriptProvider
|
<PayPalScriptProvider
|
||||||
@@ -100,40 +142,84 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}:
|
|||||||
currency: "USD",
|
currency: "USD",
|
||||||
intent: "capture",
|
intent: "capture",
|
||||||
commit: true,
|
commit: true,
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
{packages.map((p) => (
|
{packages.map((p) => (
|
||||||
<div key={p.id} className={clsx("flex flex-col items-start gap-6 rounded-xl bg-white p-4")}>
|
<div
|
||||||
|
key={p.id}
|
||||||
|
className={clsx(
|
||||||
|
"flex flex-col items-start gap-6 rounded-xl bg-white p-4",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="mb-2 flex flex-col items-start">
|
<div className="mb-2 flex flex-col items-start">
|
||||||
<img src="/logo_title.png" alt="EnCoach's Logo" className="w-32" />
|
<img
|
||||||
|
src="/logo_title.png"
|
||||||
|
alt="EnCoach's Logo"
|
||||||
|
className="w-32"
|
||||||
|
/>
|
||||||
<span className="text-xl font-semibold">
|
<span className="text-xl font-semibold">
|
||||||
EnCoach - {p.duration}{" "}
|
EnCoach - {p.duration}{" "}
|
||||||
{capitalize(
|
{capitalize(
|
||||||
p.duration === 1 ? p.duration_unit.slice(0, p.duration_unit.length - 1) : p.duration_unit,
|
p.duration === 1
|
||||||
|
? p.duration_unit.slice(
|
||||||
|
0,
|
||||||
|
p.duration_unit.length - 1,
|
||||||
|
)
|
||||||
|
: p.duration_unit,
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-col items-start gap-2">
|
<div className="flex w-full flex-col items-start gap-2">
|
||||||
|
{!appliedDiscount && (
|
||||||
<span className="text-2xl">
|
<span className="text-2xl">
|
||||||
{p.price}
|
{p.price}
|
||||||
{getSymbolFromCurrency(p.currency)}
|
{getSymbolFromCurrency(p.currency)}
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
|
{appliedDiscount && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-2xl line-through">
|
||||||
|
{p.price}
|
||||||
|
{getSymbolFromCurrency(p.currency)}
|
||||||
|
</span>
|
||||||
|
<span className="text-2xl text-mti-red-light">
|
||||||
|
{(
|
||||||
|
p.price -
|
||||||
|
p.price * (appliedDiscount / 100)
|
||||||
|
).toFixed(2)}
|
||||||
|
{getSymbolFromCurrency(p.currency)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<PayPalPayment
|
<PayPalPayment
|
||||||
key={clientID}
|
key={clientID}
|
||||||
{...p}
|
|
||||||
clientID={clientID}
|
clientID={clientID}
|
||||||
setIsLoading={setIsLoading}
|
setIsLoading={setIsLoading}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
setTimeout(reload, 500);
|
setTimeout(reload, 500);
|
||||||
}}
|
}}
|
||||||
trackingId={trackingId}
|
trackingId={trackingId}
|
||||||
|
currency={p.currency}
|
||||||
|
duration={p.duration}
|
||||||
|
duration_unit={p.duration_unit}
|
||||||
|
price={
|
||||||
|
+(
|
||||||
|
p.price -
|
||||||
|
p.price * (appliedDiscount / 100)
|
||||||
|
).toFixed(2)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-start gap-1">
|
<div className="flex flex-col items-start gap-1">
|
||||||
<span>This includes:</span>
|
<span>This includes:</span>
|
||||||
<ul className="flex flex-col items-start text-sm">
|
<ul className="flex flex-col items-start text-sm">
|
||||||
<li>- Train your abilities for the IELTS exam</li>
|
<li>- Train your abilities for the IELTS exam</li>
|
||||||
<li>- Gain insights into your weaknesses and strengths</li>
|
<li>
|
||||||
<li>- Allow yourself to correctly prepare for the exam</li>
|
- Gain insights into your weaknesses and strengths
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
- Allow yourself to correctly prepare for the exam
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -142,20 +228,36 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}:
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isIndividual() && user.type === "corporate" && user?.corporateInformation.payment && (
|
{!isIndividual() &&
|
||||||
|
user.type === "corporate" &&
|
||||||
|
user?.corporateInformation.payment && (
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<span className="max-w-lg">
|
<span className="max-w-lg">
|
||||||
To add to your use of EnCoach and that of your students and teachers, please pay your designated package below:
|
To add to your use of EnCoach and that of your students and
|
||||||
|
teachers, please pay your designated package below:
|
||||||
</span>
|
</span>
|
||||||
<div className={clsx("flex flex-col items-start gap-6 rounded-xl bg-white p-4")}>
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"flex flex-col items-start gap-6 rounded-xl bg-white p-4",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="mb-2 flex flex-col items-start">
|
<div className="mb-2 flex flex-col items-start">
|
||||||
<img src="/logo_title.png" alt="EnCoach's Logo" className="w-32" />
|
<img
|
||||||
<span className="text-xl font-semibold">EnCoach - {user.corporateInformation?.monthlyDuration} Months</span>
|
src="/logo_title.png"
|
||||||
|
alt="EnCoach's Logo"
|
||||||
|
className="w-32"
|
||||||
|
/>
|
||||||
|
<span className="text-xl font-semibold">
|
||||||
|
EnCoach - {user.corporateInformation?.monthlyDuration}{" "}
|
||||||
|
Months
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-col items-start gap-2">
|
<div className="flex w-full flex-col items-start gap-2">
|
||||||
<span className="text-2xl">
|
<span className="text-2xl">
|
||||||
{user.corporateInformation.payment.value}
|
{user.corporateInformation.payment.value}
|
||||||
{getSymbolFromCurrency(user.corporateInformation.payment.currency)}
|
{getSymbolFromCurrency(
|
||||||
|
user.corporateInformation.payment.currency,
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<PayPalPayment
|
<PayPalPayment
|
||||||
key={clientID}
|
key={clientID}
|
||||||
@@ -177,11 +279,18 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}:
|
|||||||
<span>This includes:</span>
|
<span>This includes:</span>
|
||||||
<ul className="flex flex-col items-start text-sm">
|
<ul className="flex flex-col items-start text-sm">
|
||||||
<li>
|
<li>
|
||||||
- Allow a total of {user.corporateInformation.companyInformation.userAmount} students and teachers to
|
- Allow a total of{" "}
|
||||||
use EnCoach
|
{
|
||||||
|
user.corporateInformation.companyInformation
|
||||||
|
.userAmount
|
||||||
|
}{" "}
|
||||||
|
students and teachers to use EnCoach
|
||||||
</li>
|
</li>
|
||||||
<li>- Train their abilities for the IELTS exam</li>
|
<li>- Train their abilities for the IELTS exam</li>
|
||||||
<li>- Gain insights into your students' weaknesses and strengths</li>
|
<li>
|
||||||
|
- Gain insights into your students' weaknesses
|
||||||
|
and strengths
|
||||||
|
</li>
|
||||||
<li>- Allow them to correctly prepare for the exam</li>
|
<li>- Allow them to correctly prepare for the exam</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -191,22 +300,27 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}:
|
|||||||
{!isIndividual() && user.type !== "corporate" && (
|
{!isIndividual() && user.type !== "corporate" && (
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<span className="max-w-lg">
|
<span className="max-w-lg">
|
||||||
You are not the person in charge of your time credits, please contact your administrator about this situation.
|
You are not the person in charge of your time credits, please
|
||||||
|
contact your administrator about this situation.
|
||||||
</span>
|
</span>
|
||||||
<span className="max-w-lg">
|
<span className="max-w-lg">
|
||||||
If you believe this to be a mistake, please contact the platform's administration, thank you for your
|
If you believe this to be a mistake, please contact the
|
||||||
patience.
|
platform's administration, thank you for your patience.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isIndividual() && user.type === "corporate" && !user.corporateInformation.payment && (
|
{!isIndividual() &&
|
||||||
|
user.type === "corporate" &&
|
||||||
|
!user.corporateInformation.payment && (
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<span className="max-w-lg">
|
<span className="max-w-lg">
|
||||||
An admin nor your agent have yet set the price intended to your requirements in terms of the amount of users you
|
An admin nor your agent have yet set the price intended to
|
||||||
desire and your expected monthly duration.
|
your requirements in terms of the amount of users you desire
|
||||||
|
and your expected monthly duration.
|
||||||
</span>
|
</span>
|
||||||
<span className="max-w-lg">
|
<span className="max-w-lg">
|
||||||
Please try again later or contact your agent or an admin, thank you for your patience.
|
Please try again later or contact your agent or an admin,
|
||||||
|
thank you for your patience.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
94
src/pages/api/discounts/[id].ts
Normal file
94
src/pages/api/discounts/[id].ts
Normal file
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
81
src/pages/api/discounts/index.ts
Normal file
81
src/pages/api/discounts/index.ts
Normal file
@@ -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 });
|
||||||
|
}
|
||||||
@@ -309,7 +309,10 @@ function UserProfile({ user, mutateUser }: Props) {
|
|||||||
<div className="flex -md:flex-col-reverse -md:items-center w-full justify-between">
|
<div className="flex -md:flex-col-reverse -md:items-center w-full justify-between">
|
||||||
<div className="flex flex-col gap-8 w-full md:w-2/3">
|
<div className="flex flex-col gap-8 w-full md:w-2/3">
|
||||||
<h1 className="text-4xl font-bold mb-6 -md:hidden">Edit Profile</h1>
|
<h1 className="text-4xl font-bold mb-6 -md:hidden">Edit Profile</h1>
|
||||||
<form className="flex flex-col items-center gap-6 w-full">
|
<form
|
||||||
|
className="flex flex-col items-center gap-6 w-full"
|
||||||
|
onSubmit={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
<DoubleColumnRow>
|
<DoubleColumnRow>
|
||||||
{user.type !== "corporate" ? (
|
{user.type !== "corporate" ? (
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
Reference in New Issue
Block a user