507 lines
18 KiB
TypeScript
507 lines
18 KiB
TypeScript
import useStats from "@/hooks/useStats";
|
|
import {EMPLOYMENT_STATUS, User} from "@/interfaces/user";
|
|
import {groupBySession, averageScore} from "@/utils/stats";
|
|
import {RadioGroup} from "@headlessui/react";
|
|
import axios from "axios";
|
|
import clsx from "clsx";
|
|
import moment from "moment";
|
|
import {Divider} from "primereact/divider";
|
|
import {useEffect, useState} from "react";
|
|
import ReactDatePicker from "react-datepicker";
|
|
import {BsFileEarmarkText, BsPencil, BsStar} from "react-icons/bs";
|
|
import {toast} from "react-toastify";
|
|
import Button from "./Low/Button";
|
|
import Checkbox from "./Low/Checkbox";
|
|
import CountrySelect from "./Low/CountrySelect";
|
|
import Input from "./Low/Input";
|
|
import ProfileSummary from "./ProfileSummary";
|
|
import Select from "react-select";
|
|
import useUsers from "@/hooks/useUsers";
|
|
import {USER_TYPE_LABELS} from "@/resources/user";
|
|
import {CURRENCIES} from "@/resources/paypal";
|
|
|
|
const expirationDateColor = (date: Date) => {
|
|
const momentDate = moment(date);
|
|
const today = moment(new Date());
|
|
|
|
if (today.add(1, "days").isAfter(momentDate)) return "!bg-mti-red-ultralight border-mti-red-light";
|
|
if (today.add(3, "days").isAfter(momentDate)) return "!bg-mti-rose-ultralight border-mti-rose-light";
|
|
if (today.add(7, "days").isAfter(momentDate)) return "!bg-mti-orange-ultralight border-mti-orange-light";
|
|
};
|
|
|
|
interface Props {
|
|
user: User;
|
|
loggedInUser: User;
|
|
onClose: (reload?: boolean) => void;
|
|
onViewStudents?: () => void;
|
|
onViewTeachers?: () => void;
|
|
onViewCorporate?: () => void;
|
|
}
|
|
|
|
const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, onViewCorporate}: Props) => {
|
|
const [expiryDate, setExpiryDate] = useState<Date | null | undefined>(user.subscriptionExpirationDate);
|
|
const [type, setType] = useState(user.type);
|
|
const [status, setStatus] = useState(user.status);
|
|
const [referralAgentLabel, setReferralAgentLabel] = useState<string>();
|
|
const [position, setPosition] = useState<string | undefined>(user.type === "corporate" ? user.demographicInformation?.position : undefined);
|
|
|
|
const [referralAgent, setReferralAgent] = useState(user.type === "corporate" ? user.corporateInformation?.referralAgent : undefined);
|
|
const [companyName, setCompanyName] = useState(
|
|
user.type === "corporate"
|
|
? user.corporateInformation?.companyInformation.name
|
|
: user.type === "agent"
|
|
? user.agentInformation.companyName
|
|
: undefined,
|
|
);
|
|
const [commercialRegistration, setCommercialRegistration] = useState(
|
|
user.type === "agent" ? user.agentInformation.commercialRegistration : undefined,
|
|
);
|
|
const [userAmount, setUserAmount] = useState(user.type === "corporate" ? user.corporateInformation?.companyInformation.userAmount : undefined);
|
|
const [paymentValue, setPaymentValue] = useState(user.type === "corporate" ? user.corporateInformation?.payment?.value : undefined);
|
|
const [paymentCurrency, setPaymentCurrency] = useState(user.type === "corporate" ? user.corporateInformation?.payment?.currency : "EUR");
|
|
const [monthlyDuration, setMonthlyDuration] = useState(user.type === "corporate" ? user.corporateInformation?.monthlyDuration : undefined);
|
|
const [commissionValue, setCommission] = useState(user.type === "corporate" ? user.corporateInformation?.payment?.commission : undefined);
|
|
const {stats} = useStats(user.id);
|
|
const {users} = useUsers();
|
|
|
|
useEffect(() => {
|
|
if (users && users.length > 0) {
|
|
if (!referralAgent) {
|
|
setReferralAgentLabel("No manager");
|
|
return;
|
|
}
|
|
|
|
const agent = users.find((x) => x.id === referralAgent);
|
|
setReferralAgentLabel(`${agent?.name} - ${agent?.email}`);
|
|
}
|
|
}, [users, referralAgent]);
|
|
|
|
const updateUser = () => {
|
|
if (user.type === "corporate" && (!paymentValue || paymentValue < 0))
|
|
return toast.error("Please set a price for the user's package before updating!");
|
|
if (!confirm(`Are you sure you want to update ${user.name}'s account?`)) return;
|
|
|
|
axios
|
|
.post<{user?: User; ok?: boolean}>(`/api/users/update?id=${user.id}`, {
|
|
...user,
|
|
subscriptionExpirationDate: expiryDate,
|
|
type,
|
|
status,
|
|
agentInformation:
|
|
type === "agent"
|
|
? {
|
|
name: companyName,
|
|
commercialRegistration,
|
|
}
|
|
: undefined,
|
|
corporateInformation:
|
|
type === "corporate"
|
|
? {
|
|
referralAgent,
|
|
monthlyDuration,
|
|
companyInformation: {
|
|
name: companyName,
|
|
userAmount,
|
|
},
|
|
payment: {
|
|
value: paymentValue,
|
|
currency: paymentCurrency,
|
|
...(referralAgent === "" ? {} : {commission: commissionValue}),
|
|
},
|
|
}
|
|
: undefined,
|
|
})
|
|
.then(() => {
|
|
toast.success("User updated successfully!");
|
|
onClose(true);
|
|
})
|
|
.catch(() => {
|
|
toast.error("Something went wrong!", {toastId: "update-error"});
|
|
});
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<ProfileSummary
|
|
user={user}
|
|
items={[
|
|
{
|
|
icon: <BsFileEarmarkText className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />,
|
|
value: Object.keys(groupBySession(stats)).length,
|
|
label: "Exams",
|
|
},
|
|
{
|
|
icon: <BsPencil className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />,
|
|
value: stats.length,
|
|
label: "Exercises",
|
|
},
|
|
{
|
|
icon: <BsStar className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />,
|
|
value: `${stats.length > 0 ? averageScore(stats) : 0}%`,
|
|
label: "Average Score",
|
|
},
|
|
]}
|
|
/>
|
|
|
|
{user.type === "agent" && (
|
|
<>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 w-full">
|
|
<Input
|
|
label="Corporate Name"
|
|
type="text"
|
|
name="companyName"
|
|
onChange={setCompanyName}
|
|
placeholder="Enter corporate name"
|
|
defaultValue={companyName}
|
|
required
|
|
/>
|
|
<Input
|
|
label="Commercial Registration"
|
|
type="text"
|
|
name="commercialRegistration"
|
|
onChange={setCommercialRegistration}
|
|
placeholder="Enter commercial registration"
|
|
defaultValue={commercialRegistration}
|
|
required
|
|
/>
|
|
</div>
|
|
<Divider className="w-full !m-0" />
|
|
</>
|
|
)}
|
|
{user.type === "corporate" && (
|
|
<>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 w-full">
|
|
<Input
|
|
label="Corporate Name"
|
|
type="text"
|
|
name="companyName"
|
|
onChange={setCompanyName}
|
|
placeholder="Enter corporate name"
|
|
defaultValue={companyName}
|
|
/>
|
|
<Input
|
|
label="Number of Users"
|
|
type="number"
|
|
name="userAmount"
|
|
onChange={(e) => setUserAmount(e ? parseInt(e) : undefined)}
|
|
placeholder="Enter number of users"
|
|
defaultValue={userAmount}
|
|
/>
|
|
<Input
|
|
label="Monthly Duration"
|
|
type="number"
|
|
name="monthlyDuration"
|
|
onChange={(e) => setMonthlyDuration(e ? parseInt(e) : undefined)}
|
|
placeholder="Enter monthly duration"
|
|
defaultValue={monthlyDuration}
|
|
/>
|
|
<div className="flex flex-col gap-3 w-full lg:col-span-2">
|
|
<label className="font-normal text-base text-mti-gray-dim">Pricing</label>
|
|
<div className="w-full grid grid-cols-5 gap-2">
|
|
<Input
|
|
name="paymentValue"
|
|
onChange={(e) => setPaymentValue(e ? parseInt(e) : undefined)}
|
|
type="number"
|
|
defaultValue={paymentValue || 0}
|
|
className="col-span-3"
|
|
/>
|
|
<select
|
|
defaultValue={paymentCurrency}
|
|
onChange={(e) => setPaymentCurrency(e.target.value)}
|
|
className="p-6 col-span-2 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer bg-white">
|
|
{CURRENCIES.map(({label, currency}) => (
|
|
<option value={currency} key={currency}>
|
|
{label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-3 w-full">
|
|
<div className="flex flex-col gap-3 w-8/12">
|
|
<label className="font-normal text-base text-mti-gray-dim">Country Manager</label>
|
|
{referralAgentLabel && (
|
|
<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"
|
|
options={[
|
|
{value: "", label: "No referral"},
|
|
...users.filter((u) => u.type === "agent").map((x) => ({value: x.id, label: `${x.name} - ${x.email}`})),
|
|
]}
|
|
defaultValue={{
|
|
value: referralAgent,
|
|
label: referralAgentLabel,
|
|
}}
|
|
onChange={(value) => setReferralAgent(value?.value)}
|
|
styles={{
|
|
control: (styles) => ({
|
|
...styles,
|
|
paddingLeft: "4px",
|
|
border: "none",
|
|
outline: "none",
|
|
":focus": {
|
|
outline: "none",
|
|
},
|
|
}),
|
|
option: (styles, state) => ({
|
|
...styles,
|
|
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
|
color: state.isFocused ? "black" : styles.color,
|
|
}),
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
<div className="flex flex-col gap-3 w-4/12">
|
|
{referralAgent !== "" ? (
|
|
<>
|
|
<label className="font-normal text-base text-mti-gray-dim">Commission</label>
|
|
<Input
|
|
name="commissionValue"
|
|
onChange={(e) => setCommission(e ? parseInt(e) : undefined)}
|
|
type="number"
|
|
defaultValue={commissionValue || 0}
|
|
className="col-span-3"
|
|
/>
|
|
</>
|
|
) : (
|
|
<div />
|
|
)}
|
|
</div>
|
|
</div>
|
|
<Divider className="w-full !m-0" />
|
|
</>
|
|
)}
|
|
<section className="flex flex-col gap-4 justify-between">
|
|
<div className="flex flex-col md:flex-row gap-8 w-full">
|
|
<Input
|
|
label="Name"
|
|
type="text"
|
|
name="name"
|
|
onChange={() => null}
|
|
placeholder="Enter your name"
|
|
defaultValue={user.name}
|
|
disabled
|
|
/>
|
|
<Input
|
|
label="E-mail Address"
|
|
type="email"
|
|
name="email"
|
|
onChange={() => null}
|
|
placeholder="Enter email address"
|
|
defaultValue={user.email}
|
|
disabled
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex flex-col md:flex-row gap-8 w-full">
|
|
<div className="flex flex-col gap-3 w-full">
|
|
<label className="font-normal text-base text-mti-gray-dim">Country</label>
|
|
<CountrySelect disabled value={user.demographicInformation?.country} />
|
|
</div>
|
|
<Input
|
|
type="tel"
|
|
name="phone"
|
|
label="Phone number"
|
|
onChange={() => null}
|
|
placeholder="Enter phone number"
|
|
defaultValue={user.demographicInformation?.phone}
|
|
disabled
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex flex-col md:flex-row gap-8 w-full">
|
|
{user.type !== "corporate" && (
|
|
<div className="relative flex flex-col gap-3 w-full">
|
|
<label className="font-normal text-base text-mti-gray-dim">Employment Status</label>
|
|
<RadioGroup
|
|
value={user.demographicInformation?.employment}
|
|
className="grid grid-cols-2 items-center gap-4 place-items-center">
|
|
{EMPLOYMENT_STATUS.map(({status, label}) => (
|
|
<RadioGroup.Option value={status} key={status}>
|
|
{({checked}) => (
|
|
<span
|
|
className={clsx(
|
|
"px-6 py-4 w-40 md:w-48 flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
|
"transition duration-300 ease-in-out",
|
|
!checked
|
|
? "bg-white border-mti-gray-platinum"
|
|
: "bg-mti-purple-light border-mti-purple-dark text-white",
|
|
)}>
|
|
{label}
|
|
</span>
|
|
)}
|
|
</RadioGroup.Option>
|
|
))}
|
|
</RadioGroup>
|
|
</div>
|
|
)}
|
|
{user.type === "corporate" && (
|
|
<Input
|
|
name="position"
|
|
onChange={setPosition}
|
|
type="text"
|
|
label="Position"
|
|
defaultValue={position}
|
|
placeholder="CEO, Head of Marketing..."
|
|
disabled
|
|
required
|
|
/>
|
|
)}
|
|
<div className="flex flex-col gap-8 w-full">
|
|
<div className="relative flex flex-col gap-3 w-full">
|
|
<label className="font-normal text-base text-mti-gray-dim">Gender</label>
|
|
<RadioGroup value={user.demographicInformation?.gender} className="flex flex-row gap-4 justify-between">
|
|
<RadioGroup.Option value="male">
|
|
{({checked}) => (
|
|
<span
|
|
className={clsx(
|
|
"px-6 py-4 w-28 flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
|
"transition duration-300 ease-in-out",
|
|
!checked
|
|
? "bg-white border-mti-gray-platinum"
|
|
: "bg-mti-purple-light border-mti-purple-dark text-white",
|
|
)}>
|
|
Male
|
|
</span>
|
|
)}
|
|
</RadioGroup.Option>
|
|
<RadioGroup.Option value="female">
|
|
{({checked}) => (
|
|
<span
|
|
className={clsx(
|
|
"px-6 py-4 w-28 flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
|
"transition duration-300 ease-in-out",
|
|
!checked
|
|
? "bg-white border-mti-gray-platinum"
|
|
: "bg-mti-purple-light border-mti-purple-dark text-white",
|
|
)}>
|
|
Female
|
|
</span>
|
|
)}
|
|
</RadioGroup.Option>
|
|
<RadioGroup.Option value="other">
|
|
{({checked}) => (
|
|
<span
|
|
className={clsx(
|
|
"px-6 py-4 w-28 flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
|
"transition duration-300 ease-in-out",
|
|
!checked
|
|
? "bg-white border-mti-gray-platinum"
|
|
: "bg-mti-purple-light border-mti-purple-dark text-white",
|
|
)}>
|
|
Other
|
|
</span>
|
|
)}
|
|
</RadioGroup.Option>
|
|
</RadioGroup>
|
|
</div>
|
|
<div className="flex flex-col gap-3">
|
|
<div className="flex justify-between items-center">
|
|
<label className="font-normal text-base text-mti-gray-dim">Expiry Date</label>
|
|
<Checkbox
|
|
isChecked={!!expiryDate}
|
|
onChange={(checked) => setExpiryDate(checked ? user.subscriptionExpirationDate || new Date() : null)}>
|
|
Enabled
|
|
</Checkbox>
|
|
</div>
|
|
{!expiryDate && (
|
|
<div
|
|
className={clsx(
|
|
"p-6 w-full flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
|
"transition duration-300 ease-in-out",
|
|
!expiryDate ? "!bg-mti-green-ultralight !border-mti-green-light" : expirationDateColor(expiryDate),
|
|
"bg-white border-mti-gray-platinum",
|
|
)}>
|
|
{!expiryDate && "Unlimited"}
|
|
{expiryDate && moment(expiryDate).format("DD/MM/YYYY")}
|
|
</div>
|
|
)}
|
|
{expiryDate && (
|
|
<ReactDatePicker
|
|
className={clsx(
|
|
"p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
|
"hover:border-mti-purple tooltip",
|
|
expirationDateColor(expiryDate),
|
|
"transition duration-300 ease-in-out",
|
|
)}
|
|
filterDate={(date) =>
|
|
moment(date).isAfter(new Date()) &&
|
|
(loggedInUser.subscriptionExpirationDate
|
|
? moment(date).isBefore(moment(loggedInUser.subscriptionExpirationDate))
|
|
: true)
|
|
}
|
|
dateFormat="dd/MM/yyyy"
|
|
selected={moment(expiryDate).toDate()}
|
|
onChange={(date) => setExpiryDate(date)}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{(loggedInUser.type === "developer" || loggedInUser.type === "admin") && (
|
|
<>
|
|
<Divider className="w-full !m-0" />
|
|
<div className="flex flex-col md:flex-row gap-8 w-full">
|
|
<div className="flex flex-col gap-3 w-full">
|
|
<label className="font-normal text-base text-mti-gray-dim">Status</label>
|
|
<select
|
|
defaultValue={user.status}
|
|
onChange={(e) => setStatus(e.target.value as typeof user.status)}
|
|
className="p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer bg-white">
|
|
<option value="active">Active</option>
|
|
<option value="disabled">Disabled</option>
|
|
<option value="paymentDue">Payment Due</option>
|
|
</select>
|
|
</div>
|
|
<div className="flex flex-col gap-3 w-full">
|
|
<label className="font-normal text-base text-mti-gray-dim">Type</label>
|
|
<select
|
|
defaultValue={user.type}
|
|
onChange={(e) => setType(e.target.value as typeof user.type)}
|
|
className="p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer bg-white">
|
|
{Object.keys(USER_TYPE_LABELS).map((type) => (
|
|
<option key={type} value={type}>
|
|
{USER_TYPE_LABELS[type as keyof typeof USER_TYPE_LABELS]}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</section>
|
|
|
|
<div className="flex gap-4 justify-between mt-4 w-full">
|
|
<div className="self-start flex gap-4 justify-start items-center w-full">
|
|
{onViewCorporate && (
|
|
<Button className="w-full max-w-[200px]" variant="outline" color="rose" onClick={onViewCorporate}>
|
|
View Corporate
|
|
</Button>
|
|
)}
|
|
{onViewStudents && (
|
|
<Button className="w-full max-w-[200px]" variant="outline" color="rose" onClick={onViewStudents}>
|
|
View Students
|
|
</Button>
|
|
)}
|
|
{onViewTeachers && (
|
|
<Button className="w-full max-w-[200px]" variant="outline" color="rose" onClick={onViewTeachers}>
|
|
View Teachers
|
|
</Button>
|
|
)}
|
|
</div>
|
|
<div className="self-end flex gap-4 w-full justify-end">
|
|
<Button className="w-full max-w-[200px]" variant="outline" onClick={onClose}>
|
|
Close
|
|
</Button>
|
|
<Button onClick={updateUser} className="w-full max-w-[200px]">
|
|
Update
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default UserCard;
|