Merge branch 'develop'

This commit is contained in:
Tiago Ribeiro
2024-05-20 21:09:43 +01:00
14 changed files with 1787 additions and 1753 deletions

View File

@@ -86,7 +86,7 @@ export default function Navbar({user, path, navDisabled = false, focusMode = fal
{showExpirationDate() && ( {showExpirationDate() && (
<Link <Link
href={user.subscriptionExpirationDate && disablePaymentPage ? "/payment" : ""} href={!!user.subscriptionExpirationDate && !disablePaymentPage ? "/payment" : ""}
data-tip="Expiry date" data-tip="Expiry date"
className={clsx( className={clsx(
"flex w-fit cursor-pointer justify-center rounded-full border px-6 py-2 text-sm font-normal focus:outline-none", "flex w-fit cursor-pointer justify-center rounded-full border px-6 py-2 text-sm font-normal focus:outline-none",

View File

@@ -0,0 +1,77 @@
import {PaymentIntention} from "@/interfaces/paymob";
import {DurationUnit} from "@/interfaces/paypal";
import {User} from "@/interfaces/user";
import axios from "axios";
import {useRouter} from "next/router";
import {useState} from "react";
import Button from "./Low/Button";
import Input from "./Low/Input";
import Modal from "./Modal";
interface Props {
user: User;
currency: string;
price: number;
setIsPaymentLoading: (v: boolean) => void;
duration: number;
duration_unit: DurationUnit;
onSuccess: (duration: number, duration_unit: DurationUnit) => void;
}
export default function PaymobPayment({user, price, setIsPaymentLoading, currency, duration, duration_unit, onSuccess}: Props) {
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
const handleCardPayment = async () => {
try {
setIsPaymentLoading(true);
const paymentIntention: PaymentIntention = {
amount: price * 1000,
currency: "OMR",
items: [],
payment_methods: [],
customer: {
email: user.email,
first_name: user.name.split(" ")[0],
last_name: [...user.name.split(" ")].pop() || "N/A",
extras: {
re: user.id,
},
},
billing_data: {
apartment: "N/A",
building: "N/A",
country: user.demographicInformation?.country || "N/A",
email: user.email,
first_name: user.name.split(" ")[0],
last_name: [...user.name.split(" ")].pop() || "N/A",
floor: "N/A",
phone_number: user.demographicInformation?.phone || "N/A",
state: "N/A",
street: "N/A",
},
extras: {
userID: user.id,
duration,
duration_unit,
},
};
const response = await axios.post<{iframeURL: string}>(`/api/paymob`, paymentIntention);
router.push(response.data.iframeURL);
} catch (error) {
console.error("Error starting card payment process:", error);
}
};
return (
<>
<Button isLoading={isLoading} onClick={handleCardPayment}>
Select
</Button>
</>
);
}

View File

@@ -1,4 +1,4 @@
import {useState, useMemo} from 'react'; import {useState, useMemo} from "react";
import Input from "@/components/Low/Input"; import Input from "@/components/Low/Input";
/*fields example = [ /*fields example = [
@@ -6,43 +6,33 @@ import Input from "@/components/Low/Input";
['companyInformation', 'companyInformation', 'name'] ['companyInformation', 'companyInformation', 'name']
]*/ ]*/
const getFieldValue = (fields: string[], data: any): string => { const getFieldValue = (fields: string[], data: any): string => {
if (fields.length === 0) return data; if (fields.length === 0) return data;
const [key, ...otherFields] = fields; const [key, ...otherFields] = fields;
if (data[key]) return getFieldValue(otherFields, data[key]); if (data[key]) return getFieldValue(otherFields, data[key]);
return data; return data;
} };
export const useListSearch = (fields: string[][], rows: any[]) => { export function useListSearch<T>(fields: string[][], rows: T[]) {
const [text, setText] = useState(''); const [text, setText] = useState("");
const renderSearch = () => ( const renderSearch = () => <Input label="Search" type="text" name="search" onChange={setText} placeholder="Enter search text" value={text} />;
<Input
label="Search"
type="text"
name="search"
onChange={setText}
placeholder="Enter search text"
value={text}
/>
)
const updatedRows = useMemo(() => { const updatedRows = useMemo(() => {
const searchText = text.toLowerCase(); const searchText = text.toLowerCase();
return rows.filter((row) => { return rows.filter((row) => {
return fields.some((fieldsKeys) => { return fields.some((fieldsKeys) => {
const value = getFieldValue(fieldsKeys, row); const value = getFieldValue(fieldsKeys, row);
if(typeof value === 'string') { if (typeof value === "string") {
return value.toLowerCase().includes(searchText); return value.toLowerCase().includes(searchText);
} }
}) });
}) });
}, [fields, rows, text]) }, [fields, rows, text]);
return { return {
rows: updatedRows, rows: updatedRows,
renderSearch, renderSearch,
} };
} }

118
src/interfaces/paymob.ts Normal file
View File

@@ -0,0 +1,118 @@
export interface PaymentIntention {
amount: number;
currency: string;
payment_methods: number[];
items: any[];
billing_data: BillingData;
customer: Customer;
extras: IntentionExtras;
}
interface BillingData {
apartment: string;
first_name: string;
last_name: string;
street: string;
building: string;
phone_number: string;
country: string;
email: string;
floor: string;
state: string;
}
interface Customer {
first_name: string;
last_name: string;
email: string;
extras: IntentionExtras;
}
type IntentionExtras = {[key: string]: string | number};
export interface IntentionResult {
payment_keys: PaymentKeysItem[];
id: string;
intention_detail: IntentionDetail;
client_secret: string;
payment_methods: PaymentMethodsItem[];
special_reference: null;
extras: Extras;
confirmed: boolean;
status: string;
created: string;
card_detail: null;
object: string;
}
interface PaymentKeysItem {
integration: number;
key: string;
gateway_type: string;
iframe_id: null;
}
interface IntentionDetail {
amount: number;
items: ItemsItem[];
currency: string;
}
interface ItemsItem {
name: string;
amount: number;
description: string;
quantity: number;
}
interface PaymentMethodsItem {
integration_id: number;
alias: null;
name: null;
method_type: string;
currency: string;
live: boolean;
use_cvc_with_moto: boolean;
}
interface Extras {
creation_extras: IntentionExtras;
confirmation_extras: null;
}
export interface TransactionResult {
paymob_request_id: null;
intention: IntentionResult;
hmac: string;
transaction: Transaction;
}
interface Transaction {
amount_cents: number;
created_at: string;
currency: string;
error_occured: boolean;
has_parent_transaction: boolean;
id: number;
integration_id: number;
is_3d_secure: boolean;
is_auth: boolean;
is_capture: boolean;
is_refunded: boolean;
is_standalone_payment: boolean;
is_voided: boolean;
order: Order;
owner: number;
pending: boolean;
source_data: Source_data;
success: boolean;
receipt: string;
}
interface Order {
id: number;
}
interface Source_data {
pan: string;
sub_type: string;
type: string;
}

View File

@@ -40,7 +40,7 @@ function PackageCreator({pack, onClose}: {pack?: Package; onClose: () => void})
const [unit, setUnit] = useState<DurationUnit>(pack?.duration_unit || "months"); const [unit, setUnit] = useState<DurationUnit>(pack?.duration_unit || "months");
const [price, setPrice] = useState(pack?.price || 0); const [price, setPrice] = useState(pack?.price || 0);
const [currency, setCurrency] = useState<string>(pack?.currency || "EUR"); const [currency, setCurrency] = useState<string>(pack?.currency || "OMR");
const submit = () => { const submit = () => {
(pack ? axios.patch : axios.post)(pack ? `/api/packages/${pack.id}` : "/api/packages", { (pack ? axios.patch : axios.post)(pack ? `/api/packages/${pack.id}` : "/api/packages", {

View File

@@ -16,49 +16,25 @@ import {countries, TCountries} from "countries-list";
import countryCodes from "country-codes-list"; import countryCodes from "country-codes-list";
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
import UserCard from "@/components/UserCard"; import UserCard from "@/components/UserCard";
import {isAgentUser, USER_TYPE_LABELS} from "@/resources/user"; import {getUserCompanyName, isAgentUser, USER_TYPE_LABELS} from "@/resources/user";
import useFilterStore from "@/stores/listFilterStore"; import useFilterStore from "@/stores/listFilterStore";
import {useRouter} from "next/router"; import {useRouter} from "next/router";
import {isCorporateUser} from "@/resources/user"; import {isCorporateUser} from "@/resources/user";
import {useListSearch} from "@/hooks/useListSearch"; import {useListSearch} from "@/hooks/useListSearch";
import {getUserCorporate} from "@/utils/groups"; import {getUserCorporate} from "@/utils/groups";
import {asyncSorter} from "@/utils"; import {asyncSorter} from "@/utils";
import {exportListToExcel, UserListRow} from "@/utils/users";
const columnHelper = createColumnHelper<User>(); const columnHelper = createColumnHelper<User>();
const searchFields = [["name"], ["email"], ["corporateInformation", "companyInformation", "name"]]; const searchFields = [["name"], ["email"], ["corporateInformation", "companyInformation", "name"]];
const getCompanyName = async (user: User) => { const CompanyNameCell = ({users, user, groups}: {user: User; users: User[]; groups: Group[]}) => {
if (isCorporateUser(user)) {
return user.corporateInformation?.companyInformation?.name;
}
if (isAgentUser(user)) {
return user.agentInformation.companyName;
}
if (user.type === "teacher" || user.type === "student") {
const userCorporate = await getUserCorporate(user.id);
return userCorporate?.corporateInformation?.companyInformation.name || "";
}
return "";
};
const CompanyNameCell = ({users, user, groups}: {user: User, users: User[], groups: Group[]}) => {
const [companyName, setCompanyName] = useState(""); const [companyName, setCompanyName] = useState("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
useEffect(() => { useEffect(() => {
if (isCorporateUser(user)) return setCompanyName(user.corporateInformation?.companyInformation?.name || user.name) const name = getUserCompanyName(user, users, groups);
if (isAgentUser(user)) return setCompanyName(user.agentInformation?.companyName || user.name) setCompanyName(name);
const belongingGroups = groups.filter((x) => x.participants.includes(user.id))
const belongingGroupsAdmins = belongingGroups.map((x) => users.find((u) => u.id === x.admin)).filter((x) => !!x && isCorporateUser(x))
if (belongingGroupsAdmins.length === 0) return setCompanyName("")
const admin = (belongingGroupsAdmins[0] as CorporateUser)
setCompanyName(admin.corporateInformation?.companyInformation.name || admin.name)
}, [user, users, groups]); }, [user, users, groups]);
return isLoading ? <span className="animate-pulse">Loading...</span> : <>{companyName}</>; return isLoading ? <span className="animate-pulse">Loading...</span> : <>{companyName}</>;
@@ -501,8 +477,8 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
} }
if (sorter === "companyName" || sorter === reverseString("companyName")) { if (sorter === "companyName" || sorter === reverseString("companyName")) {
const aCorporateName = await getCompanyName(a); const aCorporateName = getUserCompanyName(a, users, groups);
const bCorporateName = await getCompanyName(b); const bCorporateName = getUserCompanyName(b, users, groups);
if (!aCorporateName && bCorporateName) return sorter === "companyName" ? -1 : 1; if (!aCorporateName && bCorporateName) return sorter === "companyName" ? -1 : 1;
if (aCorporateName && !bCorporateName) return sorter === "companyName" ? 1 : -1; if (aCorporateName && !bCorporateName) return sorter === "companyName" ? 1 : -1;
if (!aCorporateName && !bCorporateName) return 0; if (!aCorporateName && !bCorporateName) return 0;
@@ -513,7 +489,7 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
return a.id.localeCompare(b.id); return a.id.localeCompare(b.id);
}; };
const {rows: filteredRows, renderSearch} = useListSearch(searchFields, displayUsers); const {rows: filteredRows, renderSearch} = useListSearch<User>(searchFields, displayUsers);
const table = useReactTable({ const table = useReactTable({
data: filteredRows, data: filteredRows,
@@ -521,6 +497,18 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
}); });
const downloadExcel = () => {
const csv = exportListToExcel(filteredRows, users, groups);
const element = document.createElement("a");
const file = new Blob([csv], {type: "text/plain"});
element.href = URL.createObjectURL(file);
element.download = "users.xlsx";
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
};
return ( return (
<div className="w-full"> <div className="w-full">
<Modal isOpen={!!selectedUser} onClose={() => setSelectedUser(undefined)}> <Modal isOpen={!!selectedUser} onClose={() => setSelectedUser(undefined)}>
@@ -600,7 +588,12 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
</> </>
</Modal> </Modal>
<div className="w-full flex flex-col gap-2"> <div className="w-full flex flex-col gap-2">
<div className="w-full flex gap-2 items-end">
{renderSearch()} {renderSearch()}
<Button className="w-full max-w-[200px] mb-1" variant="outline" onClick={downloadExcel}>
Download List
</Button>
</div>
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full"> <table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
<thead> <thead>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (

View File

@@ -1,6 +1,5 @@
/* eslint-disable @next/next/no-img-element */ /* eslint-disable @next/next/no-img-element */
import Layout from "@/components/High/Layout"; import Layout from "@/components/High/Layout";
import PayPalPayment from "@/components/PayPalPayment";
import useGroups from "@/hooks/useGroups"; import useGroups from "@/hooks/useGroups";
import usePackages from "@/hooks/usePackages"; import usePackages from "@/hooks/usePackages";
import useUsers from "@/hooks/useUsers"; import useUsers from "@/hooks/useUsers";
@@ -8,15 +7,13 @@ import { User } from "@/interfaces/user";
import clsx from "clsx"; import clsx from "clsx";
import {capitalize} from "lodash"; import {capitalize} from "lodash";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
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";
import InviteCard from "@/components/Medium/InviteCard"; import InviteCard from "@/components/Medium/InviteCard";
import {useRouter} from "next/router"; 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"; import useDiscounts from "@/hooks/useDiscounts";
import PaymobPayment from "@/components/PaymobPayment";
interface Props { interface Props {
user: User; user: User;
@@ -25,12 +22,7 @@ interface Props {
reload: () => void; reload: () => void;
} }
export default function PaymentDue({ export default function PaymentDue({user, hasExpired = false, clientID, reload}: Props) {
user,
hasExpired = false,
clientID,
reload,
}: Props) {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [appliedDiscount, setAppliedDiscount] = useState(0); const [appliedDiscount, setAppliedDiscount] = useState(0);
@@ -40,22 +32,13 @@ export default function PaymentDue({
const {discounts} = useDiscounts(); const {discounts} = useDiscounts();
const {users} = useUsers(); const {users} = useUsers();
const {groups} = useGroups(); const {groups} = useGroups();
const { const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user?.id});
invites,
isLoading: isInvitesLoading,
reload: reloadInvites,
} = useInvites({ to: user?.id });
const trackingId = usePaypalTracking();
useEffect(() => { useEffect(() => {
const userDiscounts = discounts.filter((x) => const userDiscounts = discounts.filter((x) => user.email.endsWith(`@${x.domain}`));
user.email.endsWith(`@${x.domain}`),
);
if (userDiscounts.length === 0) return; if (userDiscounts.length === 0) return;
const biggestDiscount = [...userDiscounts] const biggestDiscount = [...userDiscounts].sort((a, b) => b.percentage - a.percentage).shift();
.sort((a, b) => b.percentage - a.percentage)
.shift();
if (!biggestDiscount) return; if (!biggestDiscount) return;
setAppliedDiscount(biggestDiscount.percentage); setAppliedDiscount(biggestDiscount.percentage);
@@ -68,9 +51,7 @@ export default function PaymentDue({
if (userGroups.length === 0) return true; if (userGroups.length === 0) return true;
const userGroupsAdminTypes = userGroups const userGroupsAdminTypes = userGroups.map((g) => users?.find((u) => u.id === g.admin)?.type).filter((t) => !!t);
.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");
}; };
@@ -79,11 +60,15 @@ export default function PaymentDue({
<ToastContainer /> <ToastContainer />
{isLoading && ( {isLoading && (
<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 flex-col items-center gap-8 text-white">
<span className={clsx("loading loading-infinity w-48")} /> <span className={clsx("loading loading-infinity w-48 animate-pulse")} />
<span className={clsx("text-2xl font-bold")}> <span className={clsx("text-2xl font-bold animate-pulse")}>Completing your payment...</span>
Completing your payment... <span>If you canceled your payment or it failed, please click the button below to restart</span>
</span> <button
onClick={() => setIsLoading(false)}
className="border border-white rounded-full px-4 py-2 hover:bg-white/80 hover:text-black cursor-pointer transition ease-in-out duration-300">
Cancel Payment
</button>
</div> </div>
</div> </div>
)} )}
@@ -94,17 +79,9 @@ export default function PaymentDue({
<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>
<span className="text-mti-black text-lg font-bold"> <BsArrowRepeat className={clsx("text-xl", isInvitesLoading && "animate-spin")} />
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">
@@ -124,145 +101,84 @@ export default function PaymentDue({
)} )}
<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 && ( {hasExpired && <span className="text-lg font-bold">You do not have time credits for your account type!</span>}
<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 To add to your use of EnCoach, please purchase one of the time packages available below:
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
options={{
clientId: clientID,
currency: "USD",
intent: "capture",
commit: true,
}}
>
{packages.map((p) => ( {packages.map((p) => (
<div <div key={p.id} className={clsx("flex flex-col items-start gap-6 rounded-xl bg-white p-4")}>
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 <img src="/logo_title.png" alt="EnCoach's Logo" className="w-32" />
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 === 1 ? p.duration_unit.slice(0, p.duration_unit.length - 1) : p.duration_unit,
? 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 && ( {!appliedDiscount && (
<span className="text-2xl"> <span className="text-2xl">
{p.price} {p.price} {p.currency}
{getSymbolFromCurrency(p.currency)}
</span> </span>
)} )}
{appliedDiscount && ( {appliedDiscount && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-2xl line-through"> <span className="text-2xl line-through">
{p.price} {p.price} {p.currency}
{getSymbolFromCurrency(p.currency)}
</span> </span>
<span className="text-2xl text-mti-red-light"> <span className="text-2xl text-mti-red-light">
{( {(p.price - p.price * (appliedDiscount / 100)).toFixed(2)} {p.currency}
p.price -
p.price * (appliedDiscount / 100)
).toFixed(2)}
{getSymbolFromCurrency(p.currency)}
</span> </span>
</div> </div>
)} )}
<PayPalPayment <PaymobPayment
key={clientID} key={clientID}
clientID={clientID} user={user}
setIsLoading={setIsLoading} setIsPaymentLoading={setIsLoading}
onSuccess={() => { onSuccess={() => {
setTimeout(reload, 500); setTimeout(reload, 500);
}} }}
trackingId={trackingId}
currency={p.currency} currency={p.currency}
duration={p.duration} duration={p.duration}
duration_unit={p.duration_unit} duration_unit={p.duration_unit}
price={ price={+(p.price - p.price * (appliedDiscount / 100)).toFixed(2)}
+(
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> <li>- Gain insights into your weaknesses and strengths</li>
- Gain insights into your weaknesses and strengths <li>- Allow yourself to correctly prepare for the exam</li>
</li>
<li>
- Allow yourself to correctly prepare for the exam
</li>
</ul> </ul>
</div> </div>
</div> </div>
))} ))}
</PayPalScriptProvider>
</div> </div>
</div> </div>
)} )}
{!isIndividual() && {!isIndividual() && user.type === "corporate" && user?.corporateInformation.payment && (
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 To add to your use of EnCoach and that of your students and teachers, please pay your designated package below:
teachers, please pay your designated package below:
</span> </span>
<div <div className={clsx("flex flex-col items-start gap-6 rounded-xl bg-white p-4")}>
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 <img src="/logo_title.png" alt="EnCoach's Logo" className="w-32" />
src="/logo_title.png" <span className="text-xl font-semibold">EnCoach - {user.corporateInformation?.monthlyDuration} Months</span>
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} {user.corporateInformation.payment.currency}
{getSymbolFromCurrency(
user.corporateInformation.payment.currency,
)}
</span> </span>
<PayPalPayment <PaymobPayment
key={clientID} key={clientID}
clientID={clientID} user={user}
setIsLoading={setIsLoading} setIsPaymentLoading={setIsLoading}
currency={user.corporateInformation.payment.currency} currency={user.corporateInformation.payment.currency}
price={user.corporateInformation.payment.value} price={user.corporateInformation.payment.value}
duration={user.corporateInformation.monthlyDuration} duration={user.corporateInformation.monthlyDuration}
@@ -271,26 +187,17 @@ export default function PaymentDue({
setIsLoading(false); setIsLoading(false);
setTimeout(reload, 500); setTimeout(reload, 500);
}} }}
loadScript
trackingId={trackingId}
/> />
</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> <li>
- Allow a total of{" "} - Allow a total of {user.corporateInformation.companyInformation.userAmount} students and teachers to
{ 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> <li>- Gain insights into your students&apos; weaknesses and strengths</li>
- Gain insights into your students&apos; 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>
@@ -300,27 +207,22 @@ export default function PaymentDue({
{!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 You are not the person in charge of your time credits, please contact your administrator about this situation.
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 If you believe this to be a mistake, please contact the platform&apos;s administration, thank you for your
platform&apos;s administration, thank you for your patience. patience.
</span> </span>
</div> </div>
)} )}
{!isIndividual() && {!isIndividual() && user.type === "corporate" && !user.corporateInformation.payment && (
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 An admin nor your agent have yet set the price intended to your requirements in terms of the amount of users you
your requirements in terms of the amount of users you desire desire and your expected monthly duration.
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, Please try again later or contact your agent or an admin, thank you for your patience.
thank you for your patience.
</span> </span>
</div> </div>
)} )}

View File

@@ -0,0 +1,52 @@
// 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} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {Group} from "@/interfaces/user";
import {Payment} from "@/interfaces/paypal";
import {v4} from "uuid";
import ShortUniqueId from "short-unique-id";
import axios from "axios";
import {IntentionResult, PaymentIntention} from "@/interfaces/paymob";
const db = getFirestore(app);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
if (req.method === "GET") await get(req, res);
if (req.method === "POST") await post(req, res);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
const snapshot = await getDocs(collection(db, "payments"));
res.status(200).json(
snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
})),
);
}
async function post(req: NextApiRequest, res: NextApiResponse) {
const intention = req.body as PaymentIntention;
const response = await axios.post<IntentionResult>(
"https://oman.paymob.com/v1/intention/",
{...intention, payment_methods: [parseInt(process.env.PAYMOB_INTEGRATION_ID || "0")], items: []},
{headers: {Authorization: `Token ${process.env.PAYMOB_SECRET_KEY}`}},
);
const intentionResult = response.data;
res.status(200).json({
iframeURL: `https://oman.paymob.com/unifiedcheckout/?publicKey=${process.env.PAYMOB_PUBLIC_KEY}&clientSecret=${intentionResult.client_secret}`,
});
}

View File

@@ -0,0 +1,95 @@
// 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, query, where} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {Group, User} from "@/interfaces/user";
import {DurationUnit, Package, Payment} from "@/interfaces/paypal";
import {v4} from "uuid";
import ShortUniqueId from "short-unique-id";
import axios from "axios";
import {IntentionResult, PaymentIntention, TransactionResult} from "@/interfaces/paymob";
import moment from "moment";
const db = getFirestore(app);
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") await post(req, res);
}
async function post(req: NextApiRequest, res: NextApiResponse) {
const transactionResult = req.body as TransactionResult;
const authToken = await authenticatePaymob();
if (!checkTransaction(authToken, transactionResult.transaction.order.id)) return res.status(404).json({ok: false});
if (!transactionResult.transaction.success) return res.status(200).json({ok: false});
const {userID, duration, duration_unit} = transactionResult.intention.extras.creation_extras as {
userID: string;
duration: number;
duration_unit: DurationUnit;
};
const userSnapshot = await getDoc(doc(db, "users", userID as string));
if (!userSnapshot.exists() || !duration || !duration_unit) return res.status(404).json({ok: false});
const user = {...userSnapshot.data(), id: userSnapshot.id} as User;
const subscriptionExpirationDate = user.subscriptionExpirationDate;
if (!subscriptionExpirationDate) return res.status(200).json({ok: false});
const initialDate = moment(subscriptionExpirationDate).isAfter(moment()) ? moment(subscriptionExpirationDate) : moment();
const updatedSubscriptionExpirationDate = moment(initialDate).add(duration, duration_unit).toISOString();
await setDoc(userSnapshot.ref, {subscriptionExpirationDate: updatedSubscriptionExpirationDate}, {merge: true});
await setDoc(doc(db, "paypalpayments", v4()), {
createdAt: new Date().toISOString(),
currency: transactionResult.transaction.currency,
orderId: transactionResult.transaction.id,
status: "COMPLETED",
subscriptionDuration: duration,
subscriptionDurationUnit: duration_unit,
subscriptionExpirationDate: updatedSubscriptionExpirationDate,
userId: userID,
value: transactionResult.transaction.amount_cents / 1000,
});
if (user.type === "corporate") {
const groupsSnapshot = await getDocs(query(collection(db, "groups"), where("admin", "==", user.id)));
const groups = groupsSnapshot.docs.map((g) => ({...g.data(), id: g.id})) as Group[];
const participants = (await Promise.all(
groups.flatMap((x) => x.participants).map(async (x) => ({...(await getDoc(doc(db, "users", x))).data(), id: x})),
)) as User[];
const sameExpiryDateParticipants = participants.filter((x) => x.subscriptionExpirationDate === subscriptionExpirationDate);
for (const participant of sameExpiryDateParticipants) {
await setDoc(doc(db, "users", participant.id), {subscriptionExpirationDate: updatedSubscriptionExpirationDate}, {merge: true});
}
}
res.status(200).json({
ok: true,
});
}
const authenticatePaymob = async () => {
const response = await axios.post<{token: string}>(
"https://oman.paymob.com/api/auth/tokens",
{
api_key: process.env.PAYMOB_API_KEY,
},
{headers: {Authorization: `Bearer ${process.env.PAYMOB_SECRET_KEY}`}},
);
return response.data.token;
};
const checkTransaction = async (token: string, orderID: number) => {
const response = await axios.post("https://oman.paymob.com/api/ecommerce/orders/transaction_inquiry", {auth_token: token, order_id: orderID});
return response.status === 200;
};

View File

@@ -9,15 +9,7 @@ import { shouldRedirectHome } from "@/utils/navigation.disabled";
import usePayments from "@/hooks/usePayments"; import usePayments from "@/hooks/usePayments";
import usePaypalPayments from "@/hooks/usePaypalPayments"; import usePaypalPayments from "@/hooks/usePaypalPayments";
import {Payment, PaypalPayment} from "@/interfaces/paypal"; import {Payment, PaypalPayment} from "@/interfaces/paypal";
import { import {CellContext, createColumnHelper, flexRender, getCoreRowModel, HeaderGroup, Table, useReactTable} from "@tanstack/react-table";
CellContext,
createColumnHelper,
flexRender,
getCoreRowModel,
HeaderGroup,
Table,
useReactTable,
} from "@tanstack/react-table";
import {CURRENCIES} from "@/resources/paypal"; import {CURRENCIES} from "@/resources/paypal";
import {BsTrash} from "react-icons/bs"; import {BsTrash} from "react-icons/bs";
import axios from "axios"; import axios from "axios";
@@ -50,10 +42,7 @@ export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
}; };
} }
if ( if (shouldRedirectHome(user) || !["admin", "developer", "agent", "corporate"].includes(user.type)) {
shouldRedirectHome(user) ||
!["admin", "developer", "agent", "corporate"].includes(user.type)
) {
return { return {
redirect: { redirect: {
destination: "/", destination: "/",
@@ -70,15 +59,7 @@ export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
const columnHelper = createColumnHelper<Payment>(); const columnHelper = createColumnHelper<Payment>();
const paypalColumnHelper = createColumnHelper<PaypalPaymentWithUserData>(); const paypalColumnHelper = createColumnHelper<PaypalPaymentWithUserData>();
const PaymentCreator = ({ const PaymentCreator = ({onClose, reload, showComission = false}: {onClose: () => void; reload: () => void; showComission: boolean}) => {
onClose,
reload,
showComission = false,
}: {
onClose: () => void;
reload: () => void;
showComission: boolean;
}) => {
const [corporate, setCorporate] = useState<CorporateUser>(); const [corporate, setCorporate] = useState<CorporateUser>();
const [date, setDate] = useState<Date>(new Date()); const [date, setDate] = useState<Date>(new Date());
@@ -90,9 +71,7 @@ const PaymentCreator = ({
const referralAgent = useMemo(() => { const referralAgent = useMemo(() => {
if (corporate?.corporateInformation?.referralAgent) { if (corporate?.corporateInformation?.referralAgent) {
return users.find( return users.find((u) => u.id === corporate.corporateInformation.referralAgent);
(u) => u.id === corporate.corporateInformation.referralAgent,
);
} }
return undefined; return undefined;
@@ -125,22 +104,16 @@ const PaymentCreator = ({
<h1 className="text-2xl font-semibold">New Payment</h1> <h1 className="text-2xl font-semibold">New Payment</h1>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<label className="font-normal text-base text-mti-gray-dim"> <label className="font-normal text-base text-mti-gray-dim">Corporate account *</label>
Corporate account *
</label>
<Select <Select
className="px-4 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed bg-white rounded-full border border-mti-gray-platinum focus:outline-none" className="px-4 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed bg-white rounded-full border border-mti-gray-platinum focus:outline-none"
options={( options={(users.filter((u) => u.type === "corporate") as CorporateUser[]).map((user) => ({
users.filter((u) => u.type === "corporate") as CorporateUser[]
).map((user) => ({
value: user.id, value: user.id,
meta: user, meta: user,
label: `${user.corporateInformation?.companyInformation?.name || user.name} - ${user.email}`, label: `${user.corporateInformation?.companyInformation?.name || user.name} - ${user.email}`,
}))} }))}
defaultValue={{value: "undefined", label: "Select an account"}} defaultValue={{value: "undefined", label: "Select an account"}}
onChange={(value) => onChange={(value) => setCorporate((value as any)?.meta ?? undefined)}
setCorporate((value as any)?.meta ?? undefined)
}
menuPortalTarget={document?.body} menuPortalTarget={document?.body}
styles={{ styles={{
menuPortal: (base) => ({...base, zIndex: 9999}), menuPortal: (base) => ({...base, zIndex: 9999}),
@@ -155,11 +128,7 @@ const PaymentCreator = ({
}), }),
option: (styles, state) => ({ option: (styles, state) => ({
...styles, ...styles,
backgroundColor: state.isFocused backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
? "#D5D9F0"
: state.isSelected
? "#7872BF"
: "white",
color: state.isFocused ? "black" : styles.color, color: state.isFocused ? "black" : styles.color,
}), }),
}} }}
@@ -167,19 +136,9 @@ const PaymentCreator = ({
</div> </div>
<div className="flex flex-col gap-3 w-full"> <div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim"> <label className="font-normal text-base text-mti-gray-dim">Price *</label>
Price *
</label>
<div className="w-full grid grid-cols-5 gap-2"> <div className="w-full grid grid-cols-5 gap-2">
<Input <Input name="paymentValue" onChange={() => {}} type="number" value={price} defaultValue={0} className="col-span-3" disabled />
name="paymentValue"
onChange={() => {}}
type="number"
value={price}
defaultValue={0}
className="col-span-3"
disabled
/>
<Select <Select
className="px-4 col-span-2 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool bg-mti-gray-platinum/40 text-mti-gray-dim cursor-not-allowed rounded-full border border-mti-gray-platinum focus:outline-none" className="px-4 col-span-2 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool bg-mti-gray-platinum/40 text-mti-gray-dim cursor-not-allowed rounded-full border border-mti-gray-platinum focus:outline-none"
options={CURRENCIES.map(({label, currency}) => ({ options={CURRENCIES.map(({label, currency}) => ({
@@ -188,16 +147,12 @@ const PaymentCreator = ({
}))} }))}
defaultValue={{ defaultValue={{
value: currency || "EUR", value: currency || "EUR",
label: label: CURRENCIES.find((c) => c.currency === currency)?.label || "Euro",
CURRENCIES.find((c) => c.currency === currency)?.label ||
"Euro",
}} }}
onChange={() => {}} onChange={() => {}}
value={{ value={{
value: currency || "EUR", value: currency || "EUR",
label: label: CURRENCIES.find((c) => c.currency === currency)?.label || "Euro",
CURRENCIES.find((c) => c.currency === currency)?.label ||
"Euro",
}} }}
menuPortalTarget={document?.body} menuPortalTarget={document?.body}
styles={{ styles={{
@@ -213,11 +168,7 @@ const PaymentCreator = ({
}), }),
option: (styles, state) => ({ option: (styles, state) => ({
...styles, ...styles,
backgroundColor: state.isFocused backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
? "#D5D9F0"
: state.isSelected
? "#7872BF"
: "white",
color: state.isFocused ? "black" : styles.color, color: state.isFocused ? "black" : styles.color,
}), }),
}} }}
@@ -228,22 +179,11 @@ const PaymentCreator = ({
{showComission && ( {showComission && (
<div className="flex gap-4 w-full"> <div className="flex gap-4 w-full">
<div className="flex flex-col w-full gap-3"> <div className="flex flex-col w-full gap-3">
<label className="font-normal text-base text-mti-gray-dim"> <label className="font-normal text-base text-mti-gray-dim">Commission *</label>
Commission * <Input name="commission" onChange={() => {}} type="number" defaultValue={0} value={commission} disabled />
</label>
<Input
name="commission"
onChange={() => {}}
type="number"
defaultValue={0}
value={commission}
disabled
/>
</div> </div>
<div className="flex flex-col w-full gap-3"> <div className="flex flex-col w-full gap-3">
<label className="font-normal text-base text-mti-gray-dim"> <label className="font-normal text-base text-mti-gray-dim">Commission Value*</label>
Commission Value*
</label>
<Input <Input
name="commissionValue" name="commissionValue"
value={`${(commission! / 100) * price!} ${CURRENCIES.find((c) => c.currency === currency)?.label}`} value={`${(commission! / 100) * price!} ${CURRENCIES.find((c) => c.currency === currency)?.label}`}
@@ -258,9 +198,7 @@ const PaymentCreator = ({
<div className="flex gap-4"> <div className="flex gap-4">
<div className="flex flex-col w-full gap-3"> <div className="flex flex-col w-full gap-3">
<label className="font-normal text-base text-mti-gray-dim"> <label className="font-normal text-base text-mti-gray-dim">Date *</label>
Date *
</label>
<ReactDatePicker <ReactDatePicker
className={clsx( className={clsx(
"p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer", "p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
@@ -274,16 +212,10 @@ const PaymentCreator = ({
</div> </div>
<div className="flex flex-col w-full gap-3"> <div className="flex flex-col w-full gap-3">
<label className="font-normal text-base text-mti-gray-dim"> <label className="font-normal text-base text-mti-gray-dim">Country Manager *</label>
Country Manager *
</label>
<Input <Input
name="referralAgent" name="referralAgent"
value={ value={referralAgent ? `${referralAgent.name} - ${referralAgent.email}` : "No country manager"}
referralAgent
? `${referralAgent.name} - ${referralAgent.email}`
: "No country manager"
}
onChange={() => null} onChange={() => null}
type="text" type="text"
defaultValue={"No country manager"} defaultValue={"No country manager"}
@@ -292,19 +224,10 @@ const PaymentCreator = ({
</div> </div>
</div> </div>
<div className="flex w-full justify-end items-center gap-8 mt-4"> <div className="flex w-full justify-end items-center gap-8 mt-4">
<Button <Button variant="outline" color="red" className="w-full max-w-[200px]" onClick={onClose}>
variant="outline"
color="red"
className="w-full max-w-[200px]"
onClick={onClose}
>
Cancel Cancel
</Button> </Button>
<Button <Button className="w-full max-w-[200px]" onClick={submit} disabled={!corporate || !price}>
className="w-full max-w-[200px]"
onClick={submit}
disabled={!corporate || !price}
>
Submit Submit
</Button> </Button>
</div> </div>
@@ -343,26 +266,9 @@ const IS_FILE_SUBMITTED_OPTIONS = [
}, },
]; ];
const CSV_PAYMENTS_WHITELISTED_KEYS = [ const CSV_PAYMENTS_WHITELISTED_KEYS = ["corporateId", "corporate", "date", "amount", "agent", "agentCommission", "agentValue", "isPaid"];
"corporateId",
"corporate",
"date",
"amount",
"agent",
"agentCommission",
"agentValue",
"isPaid",
];
const CSV_PAYPAL_WHITELISTED_KEYS = [ const CSV_PAYPAL_WHITELISTED_KEYS = ["orderId", "status", "name", "email", "value", "createdAt", "subscriptionExpirationDate"];
"orderId",
"status",
"name",
"email",
"value",
"createdAt",
"subscriptionExpirationDate",
];
interface SimpleCSVColumn { interface SimpleCSVColumn {
key: string; key: string;
@@ -380,9 +286,7 @@ export default function PaymentRecord() {
const [selectedCorporateUser, setSelectedCorporateUser] = useState<User>(); const [selectedCorporateUser, setSelectedCorporateUser] = useState<User>();
const [selectedAgentUser, setSelectedAgentUser] = useState<User>(); const [selectedAgentUser, setSelectedAgentUser] = useState<User>();
const [isCreatingPayment, setIsCreatingPayment] = useState(false); const [isCreatingPayment, setIsCreatingPayment] = useState(false);
const [filters, setFilters] = useState< const [filters, setFilters] = useState<{filter: (p: Payment) => boolean; id: string}[]>([]);
{ filter: (p: Payment) => boolean; id: string }[]
>([]);
const [displayPayments, setDisplayPayments] = useState<Payment[]>([]); const [displayPayments, setDisplayPayments] = useState<Payment[]>([]);
const [corporate, setCorporate] = useState<User>(); const [corporate, setCorporate] = useState<User>();
@@ -391,21 +295,12 @@ export default function PaymentRecord() {
const {user} = useUser({redirectTo: "/login"}); const {user} = useUser({redirectTo: "/login"});
const {users, reload: reloadUsers} = useUsers(); const {users, reload: reloadUsers} = useUsers();
const {payments: originalPayments, reload: reloadPayment} = usePayments(); const {payments: originalPayments, reload: reloadPayment} = usePayments();
const { payments: paypalPayments, reload: reloadPaypalPayment } = const {payments: paypalPayments, reload: reloadPaypalPayment} = usePaypalPayments();
usePaypalPayments(); const [startDate, setStartDate] = useState<Date | null>(moment("01/01/2023").toDate());
const [startDate, setStartDate] = useState<Date | null>( const [endDate, setEndDate] = useState<Date | null>(moment().endOf("day").toDate());
moment("01/01/2023").toDate(),
);
const [endDate, setEndDate] = useState<Date | null>(
moment().endOf("day").toDate(),
);
const [paid, setPaid] = useState<Boolean | null>(IS_PAID_OPTIONS[0].value); const [paid, setPaid] = useState<Boolean | null>(IS_PAID_OPTIONS[0].value);
const [commissionTransfer, setCommissionTransfer] = useState<Boolean | null>( const [commissionTransfer, setCommissionTransfer] = useState<Boolean | null>(IS_FILE_SUBMITTED_OPTIONS[0].value);
IS_FILE_SUBMITTED_OPTIONS[0].value, const [corporateTransfer, setCorporateTransfer] = useState<Boolean | null>(IS_FILE_SUBMITTED_OPTIONS[0].value);
);
const [corporateTransfer, setCorporateTransfer] = useState<Boolean | null>(
IS_FILE_SUBMITTED_OPTIONS[0].value,
);
const reload = () => { const reload = () => {
reloadUsers(); reloadUsers();
reloadPayment(); reloadPayment();
@@ -466,9 +361,7 @@ export default function PaymentRecord() {
useEffect(() => { useEffect(() => {
setFilters((prev) => [ setFilters((prev) => [
...prev.filter((x) => x.id !== "paid"), ...prev.filter((x) => x.id !== "paid"),
...(typeof paid !== "boolean" ...(typeof paid !== "boolean" ? [] : [{id: "paid", filter: (p: Payment) => p.isPaid === paid}]),
? []
: [{ id: "paid", filter: (p: Payment) => p.isPaid === paid }]),
]); ]);
}, [paid]); }, [paid]);
@@ -480,8 +373,7 @@ export default function PaymentRecord() {
: [ : [
{ {
id: "commissionTransfer", id: "commissionTransfer",
filter: (p: Payment) => filter: (p: Payment) => !p.commissionTransfer === commissionTransfer,
!p.commissionTransfer === commissionTransfer,
}, },
]), ]),
]); ]);
@@ -495,8 +387,7 @@ export default function PaymentRecord() {
: [ : [
{ {
id: "corporateTransfer", id: "corporateTransfer",
filter: (p: Payment) => filter: (p: Payment) => !p.corporateTransfer === corporateTransfer,
!p.corporateTransfer === corporateTransfer,
}, },
]), ]),
]); ]);
@@ -527,9 +418,7 @@ export default function PaymentRecord() {
} }
if (reason.response.status === 403) { if (reason.response.status === 403) {
toast.error( toast.error("You do not have permission to delete an approved payment record!");
"You do not have permission to delete an approved payment record!",
);
return; return;
} }
@@ -540,8 +429,7 @@ export default function PaymentRecord() {
const getFileAssetsColumns = () => { const getFileAssetsColumns = () => {
if (user) { if (user) {
const containerClassName = const containerClassName = "flex gap-2 text-mti-purple-light hover:text-mti-purple-dark ease-in-out duration-300 cursor-pointer";
"flex gap-2 text-mti-purple-light hover:text-mti-purple-dark ease-in-out duration-300 cursor-pointer";
switch (user.type) { switch (user.type) {
case "corporate": case "corporate":
return [ return [
@@ -660,9 +548,7 @@ export default function PaymentRecord() {
return {value: `${value}%`}; return {value: `${value}%`};
} }
case "agent": { case "agent": {
const user = users.find( const user = users.find((x) => x.id === info.row.original.agent) as AgentUser;
(x) => x.id === info.row.original.agent,
) as AgentUser;
return { return {
value: user?.name, value: user?.name,
user, user,
@@ -683,8 +569,7 @@ export default function PaymentRecord() {
const user = users.find((x) => x.id === specificValue) as CorporateUser; const user = users.find((x) => x.id === specificValue) as CorporateUser;
return { return {
user, user,
value: value: user?.corporateInformation.companyInformation.name || user?.name,
user?.corporateInformation.companyInformation.name || user?.name,
}; };
} }
case "currency": { case "currency": {
@@ -714,8 +599,7 @@ export default function PaymentRecord() {
className={clsx( className={clsx(
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer", "underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer",
)} )}
onClick={() => setSelectedAgentUser(user)} onClick={() => setSelectedAgentUser(user)}>
>
{value} {value}
</div> </div>
); );
@@ -734,10 +618,7 @@ export default function PaymentRecord() {
id: "agentValue", id: "agentValue",
cell: (info) => { cell: (info) => {
const {value} = columHelperValue(info.column.id, info); const {value} = columHelperValue(info.column.id, info);
const currency = CURRENCIES.find( const finalValue = `${value} ${info.row.original.currency}`;
(x) => x.currency === info.row.original.currency,
)?.label;
const finalValue = `${value} ${currency}`;
return <span>{finalValue}</span>; return <span>{finalValue}</span>;
}, },
}), }),
@@ -764,8 +645,7 @@ export default function PaymentRecord() {
className={clsx( className={clsx(
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer", "underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer",
)} )}
onClick={() => setSelectedCorporateUser(user)} onClick={() => setSelectedCorporateUser(user)}>
>
{value} {value}
</div> </div>
); );
@@ -784,9 +664,7 @@ export default function PaymentRecord() {
id: "amount", id: "amount",
cell: (info) => { cell: (info) => {
const {value} = columHelperValue(info.column.id, info); const {value} = columHelperValue(info.column.id, info);
const currency = CURRENCIES.find( const currency = CURRENCIES.find((x) => x.currency === info.row.original.currency)?.label;
(x) => x.currency === info.row.original.currency,
)?.label;
const finalValue = `${value} ${currency}`; const finalValue = `${value} ${currency}`;
return <span>{finalValue}</span>; return <span>{finalValue}</span>;
}, },
@@ -802,23 +680,13 @@ export default function PaymentRecord() {
<Checkbox <Checkbox
isChecked={value} isChecked={value}
onChange={(e) => { onChange={(e) => {
if (user?.type === agent || user?.type === "corporate" || value) if (user?.type === agent || user?.type === "corporate" || value) return null;
return null; if (!info.row.original.commissionTransfer || !info.row.original.corporateTransfer)
if ( return alert("All files need to be uploaded to consider it paid!");
!info.row.original.commissionTransfer || if (!confirm(`Are you sure you want to consider this payment paid?`)) return null;
!info.row.original.corporateTransfer
)
return alert(
"All files need to be uploaded to consider it paid!",
);
if (
!confirm(`Are you sure you want to consider this payment paid?`)
)
return null;
return updatePayment(info.row.original, "isPaid", e); return updatePayment(info.row.original, "isPaid", e);
}} }}>
>
<span></span> <span></span>
</Checkbox> </Checkbox>
); );
@@ -832,11 +700,7 @@ export default function PaymentRecord() {
return ( return (
<div className="flex gap-4"> <div className="flex gap-4">
{user?.type !== "agent" && ( {user?.type !== "agent" && (
<div <div data-tip="Delete" className="cursor-pointer tooltip" onClick={() => deletePayment(row.original.id)}>
data-tip="Delete"
className="cursor-pointer tooltip"
onClick={() => deletePayment(row.original.id)}
>
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" /> <BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
</div> </div>
)} )}
@@ -899,10 +763,7 @@ export default function PaymentRecord() {
id: "value", id: "value",
cell: (info) => { cell: (info) => {
const {value} = columHelperValue(info.column.id, info); const {value} = columHelperValue(info.column.id, info);
const currency = CURRENCIES.find( const finalValue = `${value} ${info.row.original.currency}`;
(x) => x.currency === info.row.original.currency,
)?.label;
const finalValue = `${value} ${currency}`;
return <span>{finalValue}</span>; return <span>{finalValue}</span>;
}, },
}), }),
@@ -924,10 +785,7 @@ export default function PaymentRecord() {
}), }),
]; ];
const { rows: filteredRows, renderSearch } = useListSearch( const {rows: filteredRows, renderSearch} = useListSearch(paypalFilterRows, updatedPaypalPayments);
paypalFilterRows,
updatedPaypalPayments,
);
const paypalTable = useReactTable({ const paypalTable = useReactTable({
data: filteredRows, data: filteredRows,
@@ -938,10 +796,7 @@ export default function PaymentRecord() {
if (user) { if (user) {
if (selectedCorporateUser) { if (selectedCorporateUser) {
return ( return (
<Modal <Modal isOpen={!!selectedCorporateUser} onClose={() => setSelectedCorporateUser(undefined)}>
isOpen={!!selectedCorporateUser}
onClose={() => setSelectedCorporateUser(undefined)}
>
<> <>
{selectedCorporateUser && ( {selectedCorporateUser && (
<div className="w-full flex flex-col gap-8"> <div className="w-full flex flex-col gap-8">
@@ -964,10 +819,7 @@ export default function PaymentRecord() {
if (selectedAgentUser) { if (selectedAgentUser) {
return ( return (
<Modal <Modal isOpen={!!selectedAgentUser} onClose={() => setSelectedAgentUser(undefined)}>
isOpen={!!selectedAgentUser}
onClose={() => setSelectedAgentUser(undefined)}
>
<> <>
{selectedAgentUser && ( {selectedAgentUser && (
<div className="w-full flex flex-col gap-8"> <div className="w-full flex flex-col gap-8">
@@ -992,17 +844,11 @@ export default function PaymentRecord() {
const getCSVData = () => { const getCSVData = () => {
const tables = [table, paypalTable]; const tables = [table, paypalTable];
const whitelists = [ const whitelists = [CSV_PAYMENTS_WHITELISTED_KEYS, CSV_PAYPAL_WHITELISTED_KEYS];
CSV_PAYMENTS_WHITELISTED_KEYS,
CSV_PAYPAL_WHITELISTED_KEYS,
];
const currentTable = tables[selectedIndex]; const currentTable = tables[selectedIndex];
const whitelist = whitelists[selectedIndex]; const whitelist = whitelists[selectedIndex];
const columns = (currentTable.getHeaderGroups() as any[]).reduce( const columns = (currentTable.getHeaderGroups() as any[]).reduce((accm: any[], group: any) => {
(accm: any[], group: any) => { const whitelistedColumns = group.headers.filter((header: any) => whitelist.includes(header.id));
const whitelistedColumns = group.headers.filter((header: any) =>
whitelist.includes(header.id),
);
const data = whitelistedColumns.map((data: any) => ({ const data = whitelistedColumns.map((data: any) => ({
key: data.column.columnDef.id, key: data.column.columnDef.id,
@@ -1010,9 +856,7 @@ export default function PaymentRecord() {
})) as SimpleCSVColumn[]; })) as SimpleCSVColumn[];
return [...accm, ...data]; return [...accm, ...data];
}, }, []);
[],
);
const {rows} = currentTable.getRowModel(); const {rows} = currentTable.getRowModel();
@@ -1050,12 +894,7 @@ export default function PaymentRecord() {
<tr key={headerGroup.id}> <tr key={headerGroup.id}>
{headerGroup.headers.map((header) => ( {headerGroup.headers.map((header) => (
<th className="p-4 text-left" key={header.id}> <th className="p-4 text-left" key={header.id}>
{header.isPlaceholder {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</th> </th>
))} ))}
</tr> </tr>
@@ -1063,10 +902,7 @@ export default function PaymentRecord() {
</thead> </thead>
<tbody className="px-2"> <tbody className="px-2">
{table.getRowModel().rows.map((row) => ( {table.getRowModel().rows.map((row) => (
<tr <tr className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2" key={row.id}>
className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2"
key={row.id}
>
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<td className="px-4 py-2" key={cell.id}> <td className="px-4 py-2" key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())} {flexRender(cell.column.columnDef.cell, cell.getContext())}
@@ -1093,10 +929,7 @@ export default function PaymentRecord() {
{user && ( {user && (
<Layout user={user} className="gap-6"> <Layout user={user} className="gap-6">
{getUserModal()} {getUserModal()}
<Modal <Modal isOpen={isCreatingPayment} onClose={() => setIsCreatingPayment(false)}>
isOpen={isCreatingPayment}
onClose={() => setIsCreatingPayment(false)}
>
<PaymentCreator <PaymentCreator
onClose={() => setIsCreatingPayment(false)} onClose={() => setIsCreatingPayment(false)}
reload={reload} reload={reload}
@@ -1107,26 +940,15 @@ export default function PaymentRecord() {
<div className="w-full flex flex-end justify-between p-2"> <div className="w-full flex flex-end justify-between p-2">
<h1 className="text-2xl font-semibold">Payment Record</h1> <h1 className="text-2xl font-semibold">Payment Record</h1>
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
{(user.type === "developer" || {(user.type === "developer" || user.type === "admin" || user.type === "agent" || user.type === "corporate") && (
user.type === "admin" ||
user.type === "agent" ||
user.type === "corporate") && (
<Button className="max-w-[200px]" variant="outline"> <Button className="max-w-[200px]" variant="outline">
<CSVLink <CSVLink data={csvRows} headers={csvColumns} filename="payment-records.csv">
data={csvRows}
headers={csvColumns}
filename="payment-records.csv"
>
Download CSV Download CSV
</CSVLink> </CSVLink>
</Button> </Button>
)} )}
{(user.type === "developer" || user.type === "admin") && ( {(user.type === "developer" || user.type === "admin") && (
<Button <Button className="max-w-[200px]" variant="outline" onClick={() => setIsCreatingPayment(true)}>
className="max-w-[200px]"
variant="outline"
onClick={() => setIsCreatingPayment(true)}
>
New Payment New Payment
</Button> </Button>
)} )}
@@ -1140,12 +962,9 @@ export default function PaymentRecord() {
"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 selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
? "bg-white shadow"
: "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
) )
} }>
>
Payments Payments
</Tab> </Tab>
{["admin", "developer"].includes(user.type) && ( {["admin", "developer"].includes(user.type) && (
@@ -1155,40 +974,25 @@ export default function PaymentRecord() {
"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 selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
? "bg-white shadow"
: "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
) )
} }>
> Paymob
Paypal
</Tab> </Tab>
)} )}
</Tab.List> </Tab.List>
<Tab.Panels> <Tab.Panels>
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide gap-8 flex flex-col"> <Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide gap-8 flex flex-col">
<div <div className={clsx("grid grid-cols-1 md:grid-cols-2 gap-8 w-full", user.type !== "corporate" && "lg:grid-cols-3")}>
className={clsx(
"grid grid-cols-1 md:grid-cols-2 gap-8 w-full",
user.type !== "corporate" && "lg:grid-cols-3",
)}
>
<div className="flex flex-col gap-3 w-full"> <div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim"> <label className="font-normal text-base text-mti-gray-dim">Corporate account *</label>
Corporate account *
</label>
<Select <Select
isClearable={user.type !== "corporate"} isClearable={user.type !== "corporate"}
className={clsx( className={clsx(
"px-4 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool bg-white rounded-full border border-mti-gray-platinum focus:outline-none", "px-4 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool bg-white rounded-full border border-mti-gray-platinum focus:outline-none",
user.type === "corporate" && user.type === "corporate" && "!bg-mti-gray-platinum/40 !text-mti-gray-dim !cursor-not-allowed",
"!bg-mti-gray-platinum/40 !text-mti-gray-dim !cursor-not-allowed",
)} )}
options={( options={(users.filter((u) => u.type === "corporate") as CorporateUser[]).map((user) => ({
users.filter(
(u) => u.type === "corporate",
) as CorporateUser[]
).map((user) => ({
value: user.id, value: user.id,
meta: user, meta: user,
label: `${user.corporateInformation?.companyInformation?.name || user.name} - ${user.email}`, label: `${user.corporateInformation?.companyInformation?.name || user.name} - ${user.email}`,
@@ -1205,9 +1009,7 @@ export default function PaymentRecord() {
: undefined : undefined
} }
isDisabled={user.type === "corporate"} isDisabled={user.type === "corporate"}
onChange={(value) => onChange={(value) => setCorporate((value as any)?.meta ?? undefined)}
setCorporate((value as any)?.meta ?? undefined)
}
menuPortalTarget={document?.body} menuPortalTarget={document?.body}
styles={{ styles={{
menuPortal: (base) => ({...base, zIndex: 9999}), menuPortal: (base) => ({...base, zIndex: 9999}),
@@ -1222,11 +1024,7 @@ export default function PaymentRecord() {
}), }),
option: (styles, state) => ({ option: (styles, state) => ({
...styles, ...styles,
backgroundColor: state.isFocused backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
? "#D5D9F0"
: state.isSelected
? "#7872BF"
: "white",
color: state.isFocused ? "black" : styles.color, color: state.isFocused ? "black" : styles.color,
}), }),
}} }}
@@ -1234,21 +1032,15 @@ export default function PaymentRecord() {
</div> </div>
{user.type !== "corporate" && ( {user.type !== "corporate" && (
<div className="flex flex-col gap-3 w-full"> <div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim"> <label className="font-normal text-base text-mti-gray-dim">Country manager *</label>
Country manager *
</label>
<Select <Select
isClearable isClearable
isDisabled={user.type === "agent"} isDisabled={user.type === "agent"}
className={clsx( className={clsx(
"px-4 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed rounded-full border border-mti-gray-platinum focus:outline-none", "px-4 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed rounded-full border border-mti-gray-platinum focus:outline-none",
user.type === "agent" user.type === "agent" ? "bg-mti-gray-platinum/40" : "bg-white",
? "bg-mti-gray-platinum/40"
: "bg-white",
)} )}
options={( options={(users.filter((u) => u.type === "agent") as AgentUser[]).map((user) => ({
users.filter((u) => u.type === "agent") as AgentUser[]
).map((user) => ({
value: user.id, value: user.id,
meta: user, meta: user,
label: `${user.name} - ${user.email}`, label: `${user.name} - ${user.email}`,
@@ -1261,11 +1053,7 @@ export default function PaymentRecord() {
} }
: undefined : undefined
} }
onChange={(value) => onChange={(value) => setAgent(value !== null ? (value as any).meta : undefined)}
setAgent(
value !== null ? (value as any).meta : undefined,
)
}
menuPortalTarget={document?.body} menuPortalTarget={document?.body}
styles={{ styles={{
menuPortal: (base) => ({...base, zIndex: 9999}), menuPortal: (base) => ({...base, zIndex: 9999}),
@@ -1280,11 +1068,7 @@ export default function PaymentRecord() {
}), }),
option: (styles, state) => ({ option: (styles, state) => ({
...styles, ...styles,
backgroundColor: state.isFocused backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
? "#D5D9F0"
: state.isSelected
? "#7872BF"
: "white",
color: state.isFocused ? "black" : styles.color, color: state.isFocused ? "black" : styles.color,
}), }),
}} }}
@@ -1292,16 +1076,12 @@ export default function PaymentRecord() {
</div> </div>
)} )}
<div className="flex flex-col gap-3 w-full"> <div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim"> <label className="font-normal text-base text-mti-gray-dim">Paid</label>
Paid
</label>
<Select <Select
isClearable isClearable
className={clsx( className={clsx(
"px-4 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed rounded-full border border-mti-gray-platinum focus:outline-none", "px-4 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed rounded-full border border-mti-gray-platinum focus:outline-none",
user.type === "agent" user.type === "agent" ? "bg-mti-gray-platinum/40" : "bg-white",
? "bg-mti-gray-platinum/40"
: "bg-white",
)} )}
options={IS_PAID_OPTIONS} options={IS_PAID_OPTIONS}
value={IS_PAID_OPTIONS.find((e) => e.value === paid)} value={IS_PAID_OPTIONS.find((e) => e.value === paid)}
@@ -1323,20 +1103,14 @@ export default function PaymentRecord() {
}), }),
option: (styles, state) => ({ option: (styles, state) => ({
...styles, ...styles,
backgroundColor: state.isFocused backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
? "#D5D9F0"
: state.isSelected
? "#7872BF"
: "white",
color: state.isFocused ? "black" : styles.color, color: state.isFocused ? "black" : styles.color,
}), }),
}} }}
/> />
</div> </div>
<div className="flex flex-col gap-3 w-full"> <div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim"> <label className="font-normal text-base text-mti-gray-dim">Date</label>
Date
</label>
<ReactDatePicker <ReactDatePicker
dateFormat="dd/MM/yyyy" dateFormat="dd/MM/yyyy"
className="px-4 py-6 w-full text-sm text-center font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed rounded-full border border-mti-gray-platinum focus:outline-none" className="px-4 py-6 w-full text-sm text-center font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed rounded-full border border-mti-gray-platinum focus:outline-none"
@@ -1345,13 +1119,9 @@ export default function PaymentRecord() {
endDate={endDate} endDate={endDate}
selectsRange selectsRange
showMonthDropdown showMonthDropdown
filterDate={(date: Date) => filterDate={(date: Date) => moment(date).isSameOrBefore(moment(new Date()))}
moment(date).isSameOrBefore(moment(new Date()))
}
onChange={([initialDate, finalDate]: [Date, Date]) => { onChange={([initialDate, finalDate]: [Date, Date]) => {
setStartDate( setStartDate(initialDate ?? moment("01/01/2023").toDate());
initialDate ?? moment("01/01/2023").toDate(),
);
if (finalDate) { if (finalDate) {
// basicly selecting a final day works as if I'm selecting the first // basicly selecting a final day works as if I'm selecting the first
// minute of that day. this way it covers the whole day // minute of that day. this way it covers the whole day
@@ -1364,18 +1134,14 @@ export default function PaymentRecord() {
</div> </div>
{user.type !== "corporate" && ( {user.type !== "corporate" && (
<div className="flex flex-col gap-3 w-full"> <div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim"> <label className="font-normal text-base text-mti-gray-dim">Commission transfer</label>
Commission transfer
</label>
<Select <Select
isClearable isClearable
className={clsx( className={clsx(
"px-4 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed rounded-full border border-mti-gray-platinum focus:outline-none", "px-4 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed rounded-full border border-mti-gray-platinum focus:outline-none",
)} )}
options={IS_FILE_SUBMITTED_OPTIONS} options={IS_FILE_SUBMITTED_OPTIONS}
value={IS_FILE_SUBMITTED_OPTIONS.find( value={IS_FILE_SUBMITTED_OPTIONS.find((e) => e.value === commissionTransfer)}
(e) => e.value === commissionTransfer,
)}
onChange={(value) => { onChange={(value) => {
if (value) return setCommissionTransfer(value.value); if (value) return setCommissionTransfer(value.value);
setCommissionTransfer(null); setCommissionTransfer(null);
@@ -1394,11 +1160,7 @@ export default function PaymentRecord() {
}), }),
option: (styles, state) => ({ option: (styles, state) => ({
...styles, ...styles,
backgroundColor: state.isFocused backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
? "#D5D9F0"
: state.isSelected
? "#7872BF"
: "white",
color: state.isFocused ? "black" : styles.color, color: state.isFocused ? "black" : styles.color,
}), }),
}} }}
@@ -1406,18 +1168,14 @@ export default function PaymentRecord() {
</div> </div>
)} )}
<div className="flex flex-col gap-3 w-full"> <div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim"> <label className="font-normal text-base text-mti-gray-dim">Corporate transfer</label>
Corporate transfer
</label>
<Select <Select
isClearable isClearable
className={clsx( className={clsx(
"px-4 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed rounded-full border border-mti-gray-platinum focus:outline-none", "px-4 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed rounded-full border border-mti-gray-platinum focus:outline-none",
)} )}
options={IS_FILE_SUBMITTED_OPTIONS} options={IS_FILE_SUBMITTED_OPTIONS}
value={IS_FILE_SUBMITTED_OPTIONS.find( value={IS_FILE_SUBMITTED_OPTIONS.find((e) => e.value === corporateTransfer)}
(e) => e.value === corporateTransfer,
)}
onChange={(value) => { onChange={(value) => {
if (value) return setCorporateTransfer(value.value); if (value) return setCorporateTransfer(value.value);
setCorporateTransfer(null); setCorporateTransfer(null);
@@ -1436,11 +1194,7 @@ export default function PaymentRecord() {
}), }),
option: (styles, state) => ({ option: (styles, state) => ({
...styles, ...styles,
backgroundColor: state.isFocused backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
? "#D5D9F0"
: state.isSelected
? "#7872BF"
: "white",
color: state.isFocused ? "black" : styles.color, color: state.isFocused ? "black" : styles.color,
}), }),
}} }}

View File

@@ -95,4 +95,8 @@ export const CURRENCIES: {label: string; currency: string}[] = [
label: "United States dollar", label: "United States dollar",
currency: "USD", currency: "USD",
}, },
{
label: "Omani rial",
currency: "OMR",
},
]; ];

View File

@@ -1,4 +1,4 @@
import {Type, User, CorporateUser, AgentUser} from "@/interfaces/user"; import {Type, User, CorporateUser, AgentUser, Group} from "@/interfaces/user";
export const USER_TYPE_LABELS: {[key in Type]: string} = { export const USER_TYPE_LABELS: {[key in Type]: string} = {
student: "Student", student: "Student",
@@ -16,3 +16,16 @@ export function isCorporateUser(user: User): user is CorporateUser {
export function isAgentUser(user: User): user is AgentUser { export function isAgentUser(user: User): user is AgentUser {
return (user as AgentUser).agentInformation !== undefined; return (user as AgentUser).agentInformation !== undefined;
} }
export function getUserCompanyName(user: User, users: User[], groups: Group[]) {
if (isCorporateUser(user)) return user.corporateInformation?.companyInformation?.name || user.name;
if (isAgentUser(user)) return user.agentInformation?.companyName || user.name;
const belongingGroups = groups.filter((x) => x.participants.includes(user.id));
const belongingGroupsAdmins = belongingGroups.map((x) => users.find((u) => u.id === x.admin)).filter((x) => !!x && isCorporateUser(x));
if (belongingGroupsAdmins.length === 0) return "";
const admin = belongingGroupsAdmins[0] as CorporateUser;
return admin.corporateInformation?.companyInformation.name || admin.name;
}

36
src/utils/users.ts Normal file
View File

@@ -0,0 +1,36 @@
import {Group, User} from "@/interfaces/user";
import {getUserCompanyName, USER_TYPE_LABELS} from "@/resources/user";
import {capitalize} from "lodash";
import moment from "moment";
export interface UserListRow {
name: string;
email: string;
type: string;
companyName: string;
expiryDate: string;
verified: string;
country: string;
phone: string;
employmentPosition: string;
gender: string;
}
export const exportListToExcel = (rowUsers: User[], users: User[], groups: Group[]) => {
const rows: UserListRow[] = rowUsers.map((user) => ({
name: user.name,
email: user.email,
type: USER_TYPE_LABELS[user.type],
companyName: getUserCompanyName(user, users, groups),
expiryDate: user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/YYYY") : "Unlimited",
country: user.demographicInformation?.country || "N/A",
phone: user.demographicInformation?.phone || "N/A",
employmentPosition: (user.type === "corporate" ? user.demographicInformation?.position : user.demographicInformation?.employment) || "N/A",
gender: user.demographicInformation?.gender ? capitalize(user.demographicInformation.gender) : "N/A",
verified: user.isVerified?.toString() || "FALSE",
}));
const header = "Name,Email,Type,Company Name,Expiry Date,Country,Phone,Employment/Position,Gender,Verification";
const rowsString = rows.map((x) => Object.values(x).join(",")).join("\n");
return `${header}\n${rowsString}`;
};