diff --git a/src/interfaces/paypal.ts b/src/interfaces/paypal.ts index d266b097..b1b3e8b0 100644 --- a/src/interfaces/paypal.ts +++ b/src/interfaces/paypal.ts @@ -1,55 +1,56 @@ 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; + id: string; + percentage: number; + domain: string; + validUntil?: Date; } 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; + 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 index 46bdf08c..f0478e7f 100644 --- a/src/pages/(admin)/Lists/DiscountList.tsx +++ b/src/pages/(admin)/Lists/DiscountList.tsx @@ -7,336 +7,301 @@ 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 {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 clsx from "clsx"; import moment from "moment"; -import { useEffect, useState } from "react"; -import { BsPencil, BsTrash } from "react-icons/bs"; -import { toast } from "react-toastify"; +import {useEffect, useState} from "react"; +import ReactDatePicker from "react-datepicker"; +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 DiscountCreator = ({discount, onClose}: {discount?: Discount; onClose: () => void}) => { + const [percentage, setPercentage] = useState(discount?.percentage); + const [domain, setDomain] = useState(discount?.domain); + const [validUntil, setValidUntil] = useState(discount?.validUntil); - const submit = async () => { - const body = { percentage, domain }; + const submit = async () => { + const body = {percentage, domain, validUntil: validUntil?.toISOString() || undefined}; - 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!"); - }); - } + 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 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))} - /> -
-
-
-
- - -
-
- ); + return ( +
+
+
+ +
+ setDomain(e.replaceAll("@", ""))} + /> +
+
+
+ +
+ setPercentage(parseFloat(e))} + /> +
+
+
+ +
+ moment(date).isAfter(new Date())} + dateFormat="dd/MM/yyyy" + selected={validUntil} + onChange={(date) => setValidUntil(date ? moment(date).endOf("day").toDate() : undefined)} + /> +
+
+
+
+ + +
+
+ ); }; -export default function DiscountList({ user }: { user: User }) { - const [selectedDiscounts, setSelectedDiscounts] = useState([]); +export default function DiscountList({user}: {user: User}) { + const [selectedDiscounts, setSelectedDiscounts] = useState([]); - const [isCreating, setIsCreating] = useState(false); - const [editingDiscount, setEditingDiscount] = useState(); + const [isCreating, setIsCreating] = useState(false); + const [editingDiscount, setEditingDiscount] = useState(); - const [filteredDiscounts, setFilteredDiscounts] = useState([]); + const [filteredDiscounts, setFilteredDiscounts] = useState([]); - const { users } = useUsers(); - const { discounts, reload } = useDiscounts(); + const {users} = useUsers(); + const {discounts, reload} = useDiscounts(); - useEffect(() => { - setFilteredDiscounts(discounts); - }, [discounts]); + useEffect(() => { + setFilteredDiscounts(discounts); + }, [discounts]); - const toggleDiscount = (id: string) => { - setSelectedDiscounts((prev) => - prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id], - ); - }; + 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)); + const toggleAllDiscounts = (checked: boolean) => { + if (checked) return setSelectedDiscounts(filteredDiscounts.map((x) => x.id)); - return setSelectedDiscounts([]); - }; + return setSelectedDiscounts([]); + }; - const deleteDiscounts = async (discounts: string[]) => { - if ( - !confirm( - `Are you sure you want to delete these ${discounts.length} discount(s)?`, - ) - ) - return; + 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)); + 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; - } + 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; - } + 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); - }; + 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; + 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; - } + 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; - } + 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); - }; + 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 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()}%`, + }), + columnHelper.accessor("validUntil", { + header: "Valid Until", + cell: (info) => (info.getValue() ? moment(info.getValue()).format("DD/MM/YYYY") : ""), + }), + { + 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 table = useReactTable({ + data: filteredDiscounts, + columns: defaultColumns, + getCoreRowModel: getCoreRowModel(), + }); - const closeModal = () => { - setIsCreating(false); - setEditingDiscount(undefined); - reload(); - }; + 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())} -
- - - ); + 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/(status)/PaymentDue.tsx b/src/pages/(status)/PaymentDue.tsx index 23f1342a..1285d041 100644 --- a/src/pages/(status)/PaymentDue.tsx +++ b/src/pages/(status)/PaymentDue.tsx @@ -14,6 +14,7 @@ import {useRouter} from "next/router"; import {ToastContainer} from "react-toastify"; import useDiscounts from "@/hooks/useDiscounts"; import PaymobPayment from "@/components/PaymobPayment"; +import moment from "moment"; interface Props { user: User; @@ -39,7 +40,7 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}: if (userDiscounts.length === 0) return; const biggestDiscount = [...userDiscounts].sort((a, b) => b.percentage - a.percentage).shift(); - if (!biggestDiscount) return; + if (!biggestDiscount || (biggestDiscount.validUntil && moment(biggestDiscount.validUntil).isBefore(moment()))) return; setAppliedDiscount(biggestDiscount.percentage); }, [discounts, user]);