Merge branch 'develop' into feature-report-export
This commit is contained in:
@@ -22,8 +22,8 @@ interface Props {
|
|||||||
|
|
||||||
export default function Diagnostic({onFinish}: Props) {
|
export default function Diagnostic({onFinish}: Props) {
|
||||||
const [focus, setFocus] = useState<"academic" | "general">();
|
const [focus, setFocus] = useState<"academic" | "general">();
|
||||||
const [levels, setLevels] = useState({reading: -1, listening: -1, writing: -1, speaking: -1});
|
const [levels, setLevels] = useState({reading: -1, listening: -1, writing: -1, speaking: -1, level: 0});
|
||||||
const [desiredLevels, setDesiredLevels] = useState({reading: 9, listening: 9, writing: 9, speaking: 9});
|
const [desiredLevels, setDesiredLevels] = useState({reading: 9, listening: 9, writing: 9, speaking: 9, level: 9});
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ export default function Diagnostic({onFinish}: Props) {
|
|||||||
axios
|
axios
|
||||||
.patch("/api/users/update", {
|
.patch("/api/users/update", {
|
||||||
focus,
|
focus,
|
||||||
levels: Object.values(levels).includes(-1) ? {reading: 0, listening: 0, writing: 0, speaking: 0} : levels,
|
levels: Object.values(levels).includes(-1) ? {reading: 0, listening: 0, writing: 0, speaking: 0, level: 0} : levels,
|
||||||
desiredLevels,
|
desiredLevels,
|
||||||
isFirstLogin: false,
|
isFirstLogin: false,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export default function TrueFalse({id, type, prompt, questions, userSolutions, o
|
|||||||
const calculateScore = () => {
|
const calculateScore = () => {
|
||||||
const total = questions.length || 0;
|
const total = questions.length || 0;
|
||||||
const correct = answers.filter(
|
const correct = answers.filter(
|
||||||
(x) => questions.find((y) => x.id.toString() === y.id.toString())?.solution === x.solution.toLowerCase() || false,
|
(x) => questions.find((y) => x.id.toString() === y.id.toString())?.solution?.toLowerCase() === x.solution.toLowerCase() || false,
|
||||||
).length;
|
).length;
|
||||||
const missing = total - answers.filter((x) => questions.find((y) => x.id.toString() === y.id.toString())).length;
|
const missing = total - answers.filter((x) => questions.find((y) => x.id.toString() === y.id.toString())).length;
|
||||||
|
|
||||||
@@ -62,41 +62,37 @@ export default function TrueFalse({id, type, prompt, questions, userSolutions, o
|
|||||||
</div>
|
</div>
|
||||||
<span className="text-sm w-full leading-6">You can click a selected option again to deselect it.</span>
|
<span className="text-sm w-full leading-6">You can click a selected option again to deselect it.</span>
|
||||||
<div className="bg-mti-gray-smoke rounded-xl px-5 py-6 flex flex-col gap-8">
|
<div className="bg-mti-gray-smoke rounded-xl px-5 py-6 flex flex-col gap-8">
|
||||||
{questions.map((question, index) => (
|
{questions.map((question, index) => {
|
||||||
<div key={question.id.toString()} className="flex flex-col gap-4">
|
const id = question.id.toString();
|
||||||
<span>
|
|
||||||
{index + 1}. {question.prompt}
|
return (
|
||||||
</span>
|
<div key={question.id.toString()} className="flex flex-col gap-4">
|
||||||
<div className="flex gap-4">
|
<span>
|
||||||
<Button
|
{index + 1}. {question.prompt}
|
||||||
variant={
|
</span>
|
||||||
answers.find((x) => x.id.toString() === question.id.toString())?.solution === "true" ? "solid" : "outline"
|
<div className="flex gap-4">
|
||||||
}
|
<Button
|
||||||
onClick={() => toggleAnswer("true", question.id.toString())}
|
variant={answers.find((x) => x.id.toString() === id)?.solution === "true" ? "solid" : "outline"}
|
||||||
className="!py-2">
|
onClick={() => toggleAnswer("true", id)}
|
||||||
True
|
className="!py-2">
|
||||||
</Button>
|
True
|
||||||
<Button
|
</Button>
|
||||||
variant={
|
<Button
|
||||||
answers.find((x) => x.id.toString() === question.id.toString())?.solution === "false" ? "solid" : "outline"
|
variant={answers.find((x) => x.id.toString() === id)?.solution === "false" ? "solid" : "outline"}
|
||||||
}
|
onClick={() => toggleAnswer("false", id)}
|
||||||
onClick={() => toggleAnswer("false", question.id.toString())}
|
className="!py-2">
|
||||||
className="!py-2">
|
False
|
||||||
False
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
<Button
|
variant={answers.find((x) => x.id.toString() === id)?.solution === "not_given" ? "solid" : "outline"}
|
||||||
variant={
|
onClick={() => toggleAnswer("not_given", id)}
|
||||||
answers.find((x) => x.id.toString() === question.id.toString())?.solution === "not_given"
|
className="!py-2">
|
||||||
? "solid"
|
Not Given
|
||||||
: "outline"
|
</Button>
|
||||||
}
|
</div>
|
||||||
onClick={() => toggleAnswer("not_given", question.id.toString())}
|
|
||||||
className="!py-2">
|
|
||||||
Not Given
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export default function Writing({
|
|||||||
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (localStorage.getItem("enable_paste")) return;
|
||||||
|
|
||||||
const listener = (e: KeyboardEvent) => {
|
const listener = (e: KeyboardEvent) => {
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === "v") {
|
if ((e.ctrlKey || e.metaKey) && e.key === "v") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -93,8 +95,8 @@ export default function Writing({
|
|||||||
)}
|
)}
|
||||||
<div className="flex flex-col h-full w-full gap-9 mb-20">
|
<div className="flex flex-col h-full w-full gap-9 mb-20">
|
||||||
<div className="flex flex-col w-full gap-7 bg-mti-gray-smoke rounded-xl py-8 pb-12 px-16">
|
<div className="flex flex-col w-full gap-7 bg-mti-gray-smoke rounded-xl py-8 pb-12 px-16">
|
||||||
<span className="whitespace-pre-wrap">{prefix}</span>
|
<span className="whitespace-pre-wrap">{prefix.replaceAll("\\n", "\n")}</span>
|
||||||
<span className="font-semibold whitespace-pre-wrap">{prompt}</span>
|
<span className="font-semibold whitespace-pre-wrap">{prompt.replaceAll("\\n", "\n")}</span>
|
||||||
{attachment && (
|
{attachment && (
|
||||||
<img
|
<img
|
||||||
onClick={() => setIsModalOpen(true)}
|
onClick={() => setIsModalOpen(true)}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {BsArrowRepeat} from "react-icons/bs";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
color?: "rose" | "purple" | "red" | "green";
|
color?: "rose" | "purple" | "red" | "green" | "gray";
|
||||||
variant?: "outline" | "solid";
|
variant?: "outline" | "solid";
|
||||||
className?: string;
|
className?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@@ -39,6 +39,11 @@ export default function Button({
|
|||||||
outline:
|
outline:
|
||||||
"bg-transparent text-mti-red-light border border-mti-red-light hover:bg-mti-red-light disabled:text-mti-red disabled:bg-mti-red-ultralight disabled:border-none selection:bg-mti-red-dark hover:text-white selection:text-white",
|
"bg-transparent text-mti-red-light border border-mti-red-light hover:bg-mti-red-light disabled:text-mti-red disabled:bg-mti-red-ultralight disabled:border-none selection:bg-mti-red-dark hover:text-white selection:text-white",
|
||||||
},
|
},
|
||||||
|
gray: {
|
||||||
|
solid: "bg-mti-gray-davy text-white border border-mti-gray-davy hover:bg-mti-gray-davy disabled:text-mti-gray-davy disabled:bg-mti-gray-davy selection:bg-mti-gray-davy",
|
||||||
|
outline:
|
||||||
|
"bg-transparent text-mti-gray-davy border border-mti-gray-davy hover:bg-mti-gray-davy disabled:text-mti-gray-davy disabled:bg-mti-gray-davy disabled:border-none selection:bg-mti-gray-davy hover:text-white selection:text-white",
|
||||||
|
},
|
||||||
rose: {
|
rose: {
|
||||||
solid: "bg-mti-rose-light text-white border border-mti-rose-light hover:bg-mti-rose disabled:text-mti-rose disabled:bg-mti-rose-ultralight selection:bg-mti-rose-dark",
|
solid: "bg-mti-rose-light text-white border border-mti-rose-light hover:bg-mti-rose disabled:text-mti-rose disabled:bg-mti-rose-ultralight selection:bg-mti-rose-dark",
|
||||||
outline:
|
outline:
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export default function MobileMenu({isOpen, onClose, path, user}: Props) {
|
|||||||
)}>
|
)}>
|
||||||
Record
|
Record
|
||||||
</Link>
|
</Link>
|
||||||
{["admin", "developer", "agent"].includes(user.type) && (
|
{["admin", "developer", "agent", "corporate"].includes(user.type) && (
|
||||||
<Link
|
<Link
|
||||||
href="/payment-record"
|
href="/payment-record"
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
|||||||
@@ -1,156 +1,145 @@
|
|||||||
import React, { ChangeEvent } from "react";
|
import React, {ChangeEvent} from "react";
|
||||||
import { BsUpload, BsDownload, BsTrash, BsArrowRepeat } from "react-icons/bs";
|
import {BsUpload, BsDownload, BsTrash, BsArrowRepeat, BsXCircleFill} from "react-icons/bs";
|
||||||
import { FilesStorage } from "@/interfaces/storage.files";
|
import {FilesStorage} from "@/interfaces/storage.files";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
interface Asset {
|
interface Asset {
|
||||||
file: string | File;
|
file: string | File;
|
||||||
complete: boolean;
|
complete: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PaymentAssetManager = (props: {
|
const PaymentAssetManager = (props: {
|
||||||
asset: string | undefined;
|
asset: string | undefined;
|
||||||
permissions: "read" | "write";
|
permissions: "read" | "write";
|
||||||
type: FilesStorage;
|
type: FilesStorage;
|
||||||
paymentId: string;
|
reload: () => void;
|
||||||
|
paymentId: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { asset, permissions, type, paymentId } = props;
|
const {asset, permissions, type, paymentId} = props;
|
||||||
|
|
||||||
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
||||||
const fileInputReplaceRef = React.useRef<HTMLInputElement>(null);
|
const fileInputReplaceRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const [managingAsset, setManagingAsset] = React.useState<Asset>({
|
const [managingAsset, setManagingAsset] = React.useState<Asset>({
|
||||||
file: asset || "",
|
file: asset || "",
|
||||||
complete: asset ? true : false,
|
complete: asset ? true : false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { file, complete } = managingAsset;
|
const {file, complete} = managingAsset;
|
||||||
|
|
||||||
const deleteAsset = () => {
|
const deleteAsset = () => {
|
||||||
if (confirm("Are you sure you want to delete this document?")) {
|
if (confirm("Are you sure you want to delete this document?")) {
|
||||||
axios
|
axios
|
||||||
.delete(`/api/payments/files/${type}/${paymentId}`)
|
.delete(`/api/payments/files/${type}/${paymentId}`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
console.log("File deleted successfully!");
|
console.log("File deleted successfully!");
|
||||||
setManagingAsset({
|
setManagingAsset({
|
||||||
file: "",
|
file: "",
|
||||||
complete: false,
|
complete: false,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error("File deletion failed");
|
console.error("File deletion failed");
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Error occurred during file deletion:", error);
|
console.error("Error occurred during file deletion:", error);
|
||||||
});
|
})
|
||||||
}
|
.finally(props.reload);
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const renderFileInput = (
|
const renderFileInput = (onChange: any, ref: React.RefObject<HTMLInputElement>) => (
|
||||||
onChange: any,
|
<input type="file" ref={ref} style={{display: "none"}} onChange={onChange} multiple={false} accept="application/pdf" />
|
||||||
ref: React.RefObject<HTMLInputElement>
|
);
|
||||||
) => (
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
ref={ref}
|
|
||||||
style={{ display: "none" }}
|
|
||||||
onChange={onChange}
|
|
||||||
multiple={false}
|
|
||||||
accept="application/pdf"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFileChange = async (e: Event, method: "post" | "patch") => {
|
const handleFileChange = async (e: Event, method: "post" | "patch") => {
|
||||||
const newFile = (e.target as HTMLInputElement).files?.[0];
|
const newFile = (e.target as HTMLInputElement).files?.[0];
|
||||||
if (newFile) {
|
if (newFile) {
|
||||||
setManagingAsset({
|
setManagingAsset({
|
||||||
file: newFile,
|
file: newFile,
|
||||||
complete: false,
|
complete: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", newFile);
|
formData.append("file", newFile);
|
||||||
|
|
||||||
axios[method](`/api/payments/files/${type}/${paymentId}`, formData, {
|
axios[method](`/api/payments/files/${type}/${paymentId}`, formData, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data",
|
"Content-Type": "multipart/form-data",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
console.log("File uploaded successfully!");
|
console.log("File uploaded successfully!");
|
||||||
console.log("Uploaded File URL:", response.data.ref);
|
console.log("Uploaded File URL:", response.data.ref);
|
||||||
// Further actions upon successful upload
|
// Further actions upon successful upload
|
||||||
setManagingAsset({
|
setManagingAsset({
|
||||||
file: response.data.ref,
|
file: response.data.ref,
|
||||||
complete: true,
|
complete: true,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error("File upload failed");
|
console.error("File upload failed");
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Error occurred during file upload:", error);
|
console.error("Error occurred during file upload:", error);
|
||||||
});
|
})
|
||||||
}
|
.finally(props.reload);
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const downloadAsset = () => {
|
const downloadAsset = () => {
|
||||||
axios
|
axios
|
||||||
.get(`/api/payments/files/${type}/${paymentId}`)
|
.get(`/api/payments/files/${type}/${paymentId}`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
console.log("Uploaded File URL:", response.data.url);
|
console.log("Uploaded File URL:", response.data.url);
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.download = response.data.filename;
|
link.download = response.data.filename;
|
||||||
link.href = response.data.url;
|
link.href = response.data.url;
|
||||||
link.click();
|
link.click();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error("Failed to download file");
|
console.error("Failed to download file");
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Error occurred during file upload:", error);
|
console.error("Error occurred during file upload:", error);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (permissions === "read") {
|
if (permissions === "read") {
|
||||||
if (file) return <BsDownload onClick={downloadAsset} />;
|
if (file) return <BsDownload onClick={downloadAsset} />;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
if (complete) {
|
if (complete) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<BsDownload onClick={downloadAsset} />
|
<BsDownload onClick={downloadAsset} />
|
||||||
<BsArrowRepeat onClick={() => fileInputReplaceRef.current?.click()} />
|
<BsArrowRepeat onClick={() => fileInputReplaceRef.current?.click()} />
|
||||||
<BsTrash onClick={deleteAsset} />
|
<BsTrash onClick={deleteAsset} />
|
||||||
{renderFileInput(
|
{renderFileInput((e: Event) => handleFileChange(e, "patch"), fileInputReplaceRef)}
|
||||||
(e: Event) => handleFileChange(e, "patch"),
|
{renderFileInput((e: Event) => handleFileChange(e, "post"), fileInputRef)}
|
||||||
fileInputReplaceRef
|
</>
|
||||||
)}
|
);
|
||||||
{renderFileInput(
|
}
|
||||||
(e: Event) => handleFileChange(e, "post"),
|
|
||||||
fileInputRef
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <span className="loading loading-infinity w-8" />;
|
return <span className="loading loading-infinity w-8" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return permissions === "write" ? (
|
||||||
<>
|
<>
|
||||||
<BsUpload onClick={() => fileInputRef.current?.click()} />
|
<BsUpload onClick={() => fileInputRef.current?.click()} />
|
||||||
{renderFileInput((e: Event) => handleFileChange(e, "post"), fileInputRef)}
|
{renderFileInput((e: Event) => handleFileChange(e, "post"), fileInputRef)}
|
||||||
</>
|
</>
|
||||||
);
|
) : (
|
||||||
|
<BsXCircleFill />
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PaymentAssetManager;
|
export default PaymentAssetManager;
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, u
|
|||||||
)}
|
)}
|
||||||
<Nav disabled={disableNavigation} Icon={BsGraphUp} label="Stats" path={path} keyPath="/stats" isMinimized={isMinimized} />
|
<Nav disabled={disableNavigation} Icon={BsGraphUp} label="Stats" path={path} keyPath="/stats" isMinimized={isMinimized} />
|
||||||
<Nav disabled={disableNavigation} Icon={BsClockHistory} label="Record" path={path} keyPath="/record" isMinimized={isMinimized} />
|
<Nav disabled={disableNavigation} Icon={BsClockHistory} label="Record" path={path} keyPath="/record" isMinimized={isMinimized} />
|
||||||
{["admin", "developer", "agent"].includes(userType || "") && (
|
{["admin", "developer", "agent", "corporate"].includes(userType || "") && (
|
||||||
<Nav
|
<Nav
|
||||||
disabled={disableNavigation}
|
disabled={disableNavigation}
|
||||||
Icon={BsCurrencyDollar}
|
Icon={BsCurrencyDollar}
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ export default function FillBlanksSolutions({id, type, prompt, solutions, text,
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"rounded-full hover:text-white hover:bg-mti-red transition duration-300 ease-in-out my-1 px-5 py-2 text-center text-white bg-mti-red-light",
|
"rounded-full hover:text-white hover:bg-mti-gray-davy transition duration-300 ease-in-out my-1 px-5 py-2 text-center text-white bg-mti-gray-davy",
|
||||||
)}>
|
)}>
|
||||||
{solution.solution}
|
{solution?.solution}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -99,7 +99,7 @@ export default function FillBlanksSolutions({id, type, prompt, solutions, text,
|
|||||||
Correct
|
Correct
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<div className="w-4 h-4 rounded-full bg-mti-red" />
|
<div className="w-4 h-4 rounded-full bg-mti-gray-davy" />
|
||||||
Unanswered
|
Unanswered
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export default function MatchSentencesSolutions({
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"w-8 h-8 rounded-full z-10 text-white",
|
"w-8 h-8 rounded-full z-10 text-white",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
!userSolutions.find((x) => x.question.toString() === id.toString()) && "!bg-mti-red",
|
!userSolutions.find((x) => x.question.toString() === id.toString()) && "!bg-mti-gray-davy",
|
||||||
userSolutions.find((x) => x.question.toString() === id.toString())?.option === solution && "bg-mti-purple",
|
userSolutions.find((x) => x.question.toString() === id.toString())?.option === solution && "bg-mti-purple",
|
||||||
userSolutions.find((x) => x.question.toString() === id.toString())?.option !== solution && "bg-mti-rose",
|
userSolutions.find((x) => x.question.toString() === id.toString())?.option !== solution && "bg-mti-rose",
|
||||||
)}>
|
)}>
|
||||||
@@ -96,7 +96,7 @@ export default function MatchSentencesSolutions({
|
|||||||
<div className="w-4 h-4 rounded-full bg-mti-purple" /> Correct
|
<div className="w-4 h-4 rounded-full bg-mti-purple" /> Correct
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<div className="w-4 h-4 rounded-full bg-mti-red" /> Unanswered
|
<div className="w-4 h-4 rounded-full bg-mti-gray-davy" /> Unanswered
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<div className="w-4 h-4 rounded-full bg-mti-rose" /> Wrong
|
<div className="w-4 h-4 rounded-full bg-mti-rose" /> Wrong
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ function Question({
|
|||||||
}: MultipleChoiceQuestion & {userSolution: string | undefined; onSelectOption?: (option: string) => void; showSolution?: boolean}) {
|
}: MultipleChoiceQuestion & {userSolution: string | undefined; onSelectOption?: (option: string) => void; showSolution?: boolean}) {
|
||||||
const optionColor = (option: string) => {
|
const optionColor = (option: string) => {
|
||||||
if (option === solution && !userSolution) {
|
if (option === solution && !userSolution) {
|
||||||
return "!border-mti-red-light !text-mti-red-light";
|
return "!border-mti-gray-davy !text-mti-gray-davy";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (option === solution) {
|
if (option === solution) {
|
||||||
@@ -114,7 +114,7 @@ export default function MultipleChoice({
|
|||||||
Correct
|
Correct
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<div className="w-4 h-4 rounded-full bg-mti-red" />
|
<div className="w-4 h-4 rounded-full bg-mti-gray-davy" />
|
||||||
Unanswered
|
Unanswered
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
|
|||||||
@@ -80,7 +80,8 @@ export default function Speaking({id, type, title, video_url, text, prompts, use
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{userSolutions[0].evaluation && userSolutions[0].evaluation.perfect_answer ? (
|
{userSolutions[0].evaluation &&
|
||||||
|
(userSolutions[0].evaluation.perfect_answer || userSolutions[0].evaluation.perfect_answer_1) ? (
|
||||||
<Tab.Group>
|
<Tab.Group>
|
||||||
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-speaking/20 p-1">
|
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-speaking/20 p-1">
|
||||||
<Tab
|
<Tab
|
||||||
@@ -112,7 +113,10 @@ export default function Speaking({id, type, title, video_url, text, prompts, use
|
|||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
<Tab.Panel className="w-full bg-ielts-speaking/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
<Tab.Panel className="w-full bg-ielts-speaking/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
||||||
<span className="w-full h-full min-h-fit cursor-text whitespace-pre-wrap">
|
<span className="w-full h-full min-h-fit cursor-text whitespace-pre-wrap">
|
||||||
{userSolutions[0].evaluation!.perfect_answer.replaceAll(/\s{2,}/g, "\n\n")}
|
{userSolutions[0].evaluation!.perfect_answer &&
|
||||||
|
userSolutions[0].evaluation!.perfect_answer.replaceAll(/\s{2,}/g, "\n\n")}
|
||||||
|
{userSolutions[0].evaluation!.perfect_answer_1 &&
|
||||||
|
userSolutions[0].evaluation!.perfect_answer_1.replaceAll(/\s{2,}/g, "\n\n")}
|
||||||
</span>
|
</span>
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
</Tab.Panels>
|
</Tab.Panels>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export default function TrueFalseSolution({prompt, type, id, questions, userSolu
|
|||||||
return "rose";
|
return "rose";
|
||||||
}
|
}
|
||||||
|
|
||||||
return "red";
|
return "gray";
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -67,6 +67,7 @@ export default function TrueFalseSolution({prompt, type, id, questions, userSolu
|
|||||||
{userSolutions &&
|
{userSolutions &&
|
||||||
questions.map((question, index) => {
|
questions.map((question, index) => {
|
||||||
const userSolution = userSolutions.find((x) => x.id === question.id.toString());
|
const userSolution = userSolutions.find((x) => x.id === question.id.toString());
|
||||||
|
const solution = question.solution.toString().toLowerCase() as Solution;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={question.id.toString()} className="flex flex-col gap-4">
|
<div key={question.id.toString()} className="flex flex-col gap-4">
|
||||||
@@ -75,23 +76,23 @@ export default function TrueFalseSolution({prompt, type, id, questions, userSolu
|
|||||||
</span>
|
</span>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<Button
|
<Button
|
||||||
variant={question.solution === "true" || userSolution?.solution === "true" ? "solid" : "outline"}
|
variant={solution === "true" || userSolution?.solution.toLowerCase() === "true" ? "solid" : "outline"}
|
||||||
className="!py-2"
|
className="!py-2"
|
||||||
color={getButtonColor("true", question.solution, userSolution?.solution)}>
|
color={getButtonColor("true", solution, userSolution?.solution.toLowerCase() as Solution)}>
|
||||||
True
|
True
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={question.solution === "false" || userSolution?.solution === "false" ? "solid" : "outline"}
|
variant={solution === "false" || userSolution?.solution.toLowerCase() === "false" ? "solid" : "outline"}
|
||||||
className="!py-2"
|
className="!py-2"
|
||||||
color={getButtonColor("false", question.solution, userSolution?.solution)}>
|
color={getButtonColor("false", solution, userSolution?.solution.toLowerCase() as Solution)}>
|
||||||
False
|
False
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={
|
variant={
|
||||||
question.solution === "not_given" || userSolution?.solution === "not_given" ? "solid" : "outline"
|
solution === "not_given" || userSolution?.solution.toLowerCase() === "not_given" ? "solid" : "outline"
|
||||||
}
|
}
|
||||||
className="!py-2"
|
className="!py-2"
|
||||||
color={getButtonColor("not_given", question.solution, userSolution?.solution)}>
|
color={getButtonColor("not_given", solution, userSolution?.solution.toLowerCase() as Solution)}>
|
||||||
Not Given
|
Not Given
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -105,7 +106,7 @@ export default function TrueFalseSolution({prompt, type, id, questions, userSolu
|
|||||||
Correct
|
Correct
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<div className="w-4 h-4 rounded-full bg-mti-red" />
|
<div className="w-4 h-4 rounded-full bg-mti-gray-davy" />
|
||||||
Unanswered
|
Unanswered
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ function Blank({
|
|||||||
|
|
||||||
const getSolutionStyling = () => {
|
const getSolutionStyling = () => {
|
||||||
if (!userSolution) {
|
if (!userSolution) {
|
||||||
return "bg-mti-red-ultralight text-mti-red-light";
|
return "bg-mti-gray-davy text-mti-gray-davy";
|
||||||
}
|
}
|
||||||
|
|
||||||
return "bg-mti-purple-ultralight text-mti-purple-light";
|
return "bg-mti-purple-ultralight text-mti-purple-light";
|
||||||
@@ -131,7 +131,7 @@ export default function WriteBlanksSolutions({
|
|||||||
Correct
|
Correct
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<div className="w-4 h-4 rounded-full bg-mti-red" />
|
<div className="w-4 h-4 rounded-full bg-mti-gray-davy" />
|
||||||
Unanswered
|
Unanswered
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
|
|||||||
@@ -6,10 +6,31 @@ import Button from "../Low/Button";
|
|||||||
import {Dialog, Tab, Transition} from "@headlessui/react";
|
import {Dialog, Tab, Transition} from "@headlessui/react";
|
||||||
import {writingReverseMarking} from "@/utils/score";
|
import {writingReverseMarking} from "@/utils/score";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import reactStringReplace from "react-string-replace";
|
||||||
|
|
||||||
export default function Writing({id, type, prompt, attachment, userSolutions, onNext, onBack}: WritingExercise & CommonProps) {
|
export default function Writing({id, type, prompt, attachment, userSolutions, onNext, onBack}: WritingExercise & CommonProps) {
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const formatSolution = (solution: string, errors: {correction: string | null; misspelled: string}[]) => {
|
||||||
|
const errorRegex = new RegExp(errors.map((x) => `(${x.misspelled})`).join("|"));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{reactStringReplace(solution, errorRegex, (match) => {
|
||||||
|
const correction = errors.find((x) => x.misspelled === match)?.correction;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-tip={correction ? correction : undefined}
|
||||||
|
className={clsx("text-mti-red-light font-medium underline underline-offset-2", correction && "tooltip")}>
|
||||||
|
{match}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{attachment && (
|
{attachment && (
|
||||||
@@ -67,12 +88,14 @@ export default function Writing({id, type, prompt, attachment, userSolutions, on
|
|||||||
{userSolutions && (
|
{userSolutions && (
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<div className="flex flex-col gap-4 w-full">
|
||||||
<span>Your answer:</span>
|
<span>Your answer:</span>
|
||||||
<textarea
|
<div className="w-full h-full min-h-[320px] cursor-text px-7 py-8 input border-2 border-mti-gray-platinum bg-white rounded-3xl whitespace-pre-wrap">
|
||||||
className="w-full h-full min-h-[320px] cursor-text px-7 py-8 input border-2 border-mti-gray-platinum bg-white rounded-3xl"
|
{userSolutions[0]!.evaluation && userSolutions[0]!.evaluation.misspelled_pairs
|
||||||
contentEditable={false}
|
? formatSolution(
|
||||||
readOnly
|
userSolutions[0]!.solution.replaceAll("\\n", "\n"),
|
||||||
value={userSolutions[0]!.solution}
|
userSolutions[0]!.evaluation.misspelled_pairs,
|
||||||
/>
|
)
|
||||||
|
: userSolutions[0]!.solution.replaceAll("\\n", "\n")}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{userSolutions && userSolutions.length > 0 && userSolutions[0].evaluation && typeof userSolutions[0].evaluation !== "string" && (
|
{userSolutions && userSolutions.length > 0 && userSolutions[0].evaluation && typeof userSolutions[0].evaluation !== "string" && (
|
||||||
@@ -116,7 +139,7 @@ export default function Writing({id, type, prompt, attachment, userSolutions, on
|
|||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
<Tab.Panel className="w-full bg-ielts-writing/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
<Tab.Panel className="w-full bg-ielts-writing/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
||||||
<span className="w-full h-full min-h-fit cursor-text whitespace-pre-wrap">
|
<span className="w-full h-full min-h-fit cursor-text whitespace-pre-wrap">
|
||||||
{userSolutions[0].evaluation!.perfect_answer.replaceAll(/\s{2,}/g, "\n\n")}
|
{userSolutions[0].evaluation!.perfect_answer.replaceAll(/\s{2,}/g, "\n\n").replaceAll("\\n", "\n")}
|
||||||
</span>
|
</span>
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
</Tab.Panels>
|
</Tab.Panels>
|
||||||
|
|||||||
@@ -191,11 +191,12 @@ export default function StudentDashboard({user}: Props) {
|
|||||||
{module === "listening" && <BsHeadphones className="text-ielts-listening w-4 h-4 md:w-5 md:h-5" />}
|
{module === "listening" && <BsHeadphones className="text-ielts-listening w-4 h-4 md:w-5 md:h-5" />}
|
||||||
{module === "writing" && <BsPen className="text-ielts-writing w-4 h-4 md:w-5 md:h-5" />}
|
{module === "writing" && <BsPen className="text-ielts-writing w-4 h-4 md:w-5 md:h-5" />}
|
||||||
{module === "speaking" && <BsMegaphone className="text-ielts-speaking w-4 h-4 md:w-5 md:h-5" />}
|
{module === "speaking" && <BsMegaphone className="text-ielts-speaking w-4 h-4 md:w-5 md:h-5" />}
|
||||||
|
{module === "level" && <BsClipboard className="text-ielts-level w-4 h-4 md:w-5 md:h-5" />}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between w-full">
|
<div className="flex justify-between w-full">
|
||||||
<span className="font-bold md:font-extrabold text-sm">{capitalize(module)}</span>
|
<span className="font-bold md:font-extrabold text-sm">{capitalize(module)}</span>
|
||||||
<span className="text-sm font-normal text-mti-gray-dim">
|
<span className="text-sm font-normal text-mti-gray-dim">
|
||||||
Level {user.levels[module]} / Level {user.desiredLevels[module]}
|
Level {user.levels[module] || 0} / Level {user.desiredLevels[module] || 9}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import Link from "next/link";
|
|||||||
import {useRouter} from "next/router";
|
import {useRouter} from "next/router";
|
||||||
import {Fragment, useEffect, useState} from "react";
|
import {Fragment, useEffect, useState} from "react";
|
||||||
import {BsArrowCounterclockwise, BsBook, BsClipboard, BsEyeFill, BsHeadphones, BsMegaphone, BsPen, BsShareFill} from "react-icons/bs";
|
import {BsArrowCounterclockwise, BsBook, BsClipboard, BsEyeFill, BsHeadphones, BsMegaphone, BsPen, BsShareFill} from "react-icons/bs";
|
||||||
import { LevelScore } from "@/constants/ielts";
|
import {LevelScore} from "@/constants/ielts";
|
||||||
import { getLevelScore } from "@/utils/score";
|
import {getLevelScore} from "@/utils/score";
|
||||||
|
|
||||||
interface Score {
|
interface Score {
|
||||||
module: Module;
|
module: Module;
|
||||||
@@ -71,20 +71,18 @@ export default function Finish({user, scores, modules, isLoading, onViewResults}
|
|||||||
const bandScore: number = calculateBandScore(selectedScore.correct, selectedScore.total, selectedModule, user.focus);
|
const bandScore: number = calculateBandScore(selectedScore.correct, selectedScore.total, selectedModule, user.focus);
|
||||||
|
|
||||||
const showLevel = (level: number) => {
|
const showLevel = (level: number) => {
|
||||||
if(selectedModule === "level") {
|
if (selectedModule === "level") {
|
||||||
const [levelStr, grade] = getLevelScore(level);
|
const [levelStr, grade] = getLevelScore(level);
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center gap-1">
|
<div className="flex flex-col items-center justify-center gap-1">
|
||||||
<span className="text-xl font-bold">{levelStr}</span>
|
<span className="text-xl font-bold">{levelStr}</span>
|
||||||
<span className="text-xl">{grade}</span>
|
<span className="text-xl">{grade}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return <span className="text-3xl font-bold">{level}</span>;
|
return <span className="text-3xl font-bold">{level}</span>;
|
||||||
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -156,14 +154,16 @@ export default function Finish({user, scores, modules, isLoading, onViewResults}
|
|||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="w-fit h-fit absolute top-1/2 -translate-y-1/2 left-1/2 -translate-x-1/2 animate-pulse flex flex-col gap-12 items-center">
|
<div className="w-fit h-fit absolute top-1/2 -translate-y-1/2 left-1/2 -translate-x-1/2 animate-pulse flex flex-col gap-12 items-center">
|
||||||
<span className={clsx("loading loading-infinity w-32", moduleColors[selectedModule].progress)} />
|
<span className={clsx("loading loading-infinity w-32", moduleColors[selectedModule].progress)} />
|
||||||
<span className={clsx("font-bold text-2xl", moduleColors[selectedModule].progress)}>Evaluating your answers...</span>
|
<span className={clsx("font-bold text-2xl text-center", moduleColors[selectedModule].progress)}>
|
||||||
|
Evaluating your answers, please be patient...
|
||||||
|
<br />
|
||||||
|
You can also check it later on your records page!
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
<div className="w-full flex gap-9 mt-32 items-center justify-between mb-20">
|
<div className="w-full flex gap-9 mt-32 items-center justify-between mb-20">
|
||||||
<span className="max-w-3xl">
|
<span className="max-w-3xl">{moduleResultText(selectedModule, bandScore)}</span>
|
||||||
{moduleResultText(selectedModule, bandScore)}
|
|
||||||
</span>
|
|
||||||
<div className="flex gap-9 px-16">
|
<div className="flex gap-9 px-16">
|
||||||
<div
|
<div
|
||||||
className={clsx("radial-progress overflow-hidden", moduleColors[selectedModule].progress)}
|
className={clsx("radial-progress overflow-hidden", moduleColors[selectedModule].progress)}
|
||||||
|
|||||||
48
src/hooks/useListSearch.tsx
Normal file
48
src/hooks/useListSearch.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import {useState, useMemo} from 'react';
|
||||||
|
import Input from "@/components/Low/Input";
|
||||||
|
|
||||||
|
/*fields example = [
|
||||||
|
['id'],
|
||||||
|
['companyInformation', 'companyInformation', 'name']
|
||||||
|
]*/
|
||||||
|
|
||||||
|
|
||||||
|
const getFieldValue = (fields: string[], data: any): string => {
|
||||||
|
if(fields.length === 0) return data;
|
||||||
|
const [key, ...otherFields] = fields;
|
||||||
|
|
||||||
|
if(data[key]) return getFieldValue(otherFields, data[key]);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useListSearch = (fields: string[][], rows: any[]) => {
|
||||||
|
const [text, setText] = useState('');
|
||||||
|
|
||||||
|
const renderSearch = () => (
|
||||||
|
<Input
|
||||||
|
label="Search"
|
||||||
|
type="text"
|
||||||
|
name="search"
|
||||||
|
onChange={setText}
|
||||||
|
placeholder="Enter search text"
|
||||||
|
value={text}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
const updatedRows = useMemo(() => {
|
||||||
|
const searchText = text.toLowerCase();
|
||||||
|
return rows.filter((row) => {
|
||||||
|
return fields.some((fieldsKeys) => {
|
||||||
|
const value = getFieldValue(fieldsKeys, row);
|
||||||
|
if(typeof value === 'string') {
|
||||||
|
return value.toLowerCase().includes(searchText);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, [fields, rows, text])
|
||||||
|
|
||||||
|
return {
|
||||||
|
rows: updatedRows,
|
||||||
|
renderSearch,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ export default function useStats(id?: string) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
axios
|
axios
|
||||||
.get<Stat[]>(!id ? "/api/stats" : `/api/stats/${id}`)
|
.get<Stat[]>(!id ? "/api/stats" : `/api/stats/user/${id}`)
|
||||||
.then((response) => setStats(response.data))
|
.then((response) => setStats(response.data))
|
||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export interface ListeningPart {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UserSolution {
|
export interface UserSolution {
|
||||||
|
id?: string;
|
||||||
solutions: any[];
|
solutions: any[];
|
||||||
module?: Module;
|
module?: Module;
|
||||||
exam?: string;
|
exam?: string;
|
||||||
@@ -91,6 +92,7 @@ export interface Evaluation {
|
|||||||
comment: string;
|
comment: string;
|
||||||
overall: number;
|
overall: number;
|
||||||
task_response: {[key: string]: number};
|
task_response: {[key: string]: number};
|
||||||
|
misspelled_pairs?: {correction: string | null; misspelled: string}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InteractiveSpeakingEvaluation extends Evaluation {
|
interface InteractiveSpeakingEvaluation extends Evaluation {
|
||||||
@@ -101,6 +103,7 @@ interface InteractiveSpeakingEvaluation extends Evaluation {
|
|||||||
|
|
||||||
interface CommonEvaluation extends Evaluation {
|
interface CommonEvaluation extends Evaluation {
|
||||||
perfect_answer?: string;
|
perfect_answer?: string;
|
||||||
|
perfect_answer_1?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WritingExercise {
|
export interface WritingExercise {
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export const EMPLOYMENT_STATUS: {status: EmploymentStatus; label: string}[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export interface Stat {
|
export interface Stat {
|
||||||
|
id: string;
|
||||||
user: string;
|
user: string;
|
||||||
exam: string;
|
exam: string;
|
||||||
exercise: string;
|
exercise: string;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Button from "@/components/Low/Button";
|
|||||||
import {PERMISSIONS} from "@/constants/userPermissions";
|
import {PERMISSIONS} from "@/constants/userPermissions";
|
||||||
import useGroups from "@/hooks/useGroups";
|
import useGroups from "@/hooks/useGroups";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import {Type, User, userTypes} from "@/interfaces/user";
|
import {Type, User, userTypes, CorporateUser} from "@/interfaces/user";
|
||||||
import {Popover, Transition} from "@headlessui/react";
|
import {Popover, Transition} from "@headlessui/react";
|
||||||
import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table";
|
import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
@@ -19,9 +19,15 @@ import UserCard from "@/components/UserCard";
|
|||||||
import {USER_TYPE_LABELS} from "@/resources/user";
|
import {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 { useListSearch } from "@/hooks/useListSearch";
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<User>();
|
const columnHelper = createColumnHelper<User>();
|
||||||
|
const searchFields = [
|
||||||
|
['name'],
|
||||||
|
['email'],
|
||||||
|
['corporateInformation', 'companyInformation', 'name'],
|
||||||
|
];
|
||||||
export default function UserList({user, filters = []}: {user: User; filters?: ((user: User) => boolean)[]}) {
|
export default function UserList({user, filters = []}: {user: User; filters?: ((user: User) => boolean)[]}) {
|
||||||
const [showDemographicInformation, setShowDemographicInformation] = useState(false);
|
const [showDemographicInformation, setShowDemographicInformation] = useState(false);
|
||||||
const [sorter, setSorter] = useState<string>();
|
const [sorter, setSorter] = useState<string>();
|
||||||
@@ -325,6 +331,15 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
) as any,
|
) as any,
|
||||||
cell: (info) => USER_TYPE_LABELS[info.getValue()],
|
cell: (info) => USER_TYPE_LABELS[info.getValue()],
|
||||||
}),
|
}),
|
||||||
|
columnHelper.accessor('corporateInformation.companyInformation.name', {
|
||||||
|
header: (
|
||||||
|
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "companyName"))}>
|
||||||
|
<span>Company Name</span>
|
||||||
|
<SorterArrow name="companyName" />
|
||||||
|
</button>
|
||||||
|
) as any,
|
||||||
|
cell: (info) => getCorporateName(info.row.original),
|
||||||
|
}),
|
||||||
columnHelper.accessor("subscriptionExpirationDate", {
|
columnHelper.accessor("subscriptionExpirationDate", {
|
||||||
header: (
|
header: (
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "expiryDate"))}>
|
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "expiryDate"))}>
|
||||||
@@ -378,6 +393,14 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getCorporateName = (user: User) => {
|
||||||
|
if(isCorporateUser(user)) {
|
||||||
|
return user.corporateInformation?.companyInformation?.name
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
const sortFunction = (a: User, b: User) => {
|
const sortFunction = (a: User, b: User) => {
|
||||||
if (sorter === "name" || sorter === reverseString("name"))
|
if (sorter === "name" || sorter === reverseString("name"))
|
||||||
return sorter === "name" ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
|
return sorter === "name" ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
|
||||||
@@ -445,11 +468,28 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
: b.demographicInformation!.gender.localeCompare(a.demographicInformation!.gender);
|
: b.demographicInformation!.gender.localeCompare(a.demographicInformation!.gender);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(sorter === 'companyName' || sorter === reverseString('companyName')) {
|
||||||
|
const aCorporateName = getCorporateName(a);
|
||||||
|
const bCorporateName = getCorporateName(b);
|
||||||
|
if (!aCorporateName && bCorporateName) return sorter === "companyName" ? -1 : 1;
|
||||||
|
if (aCorporateName && !bCorporateName) return sorter === "companyName" ? 1 : -1;
|
||||||
|
if (!aCorporateName && !bCorporateName) return 0;
|
||||||
|
|
||||||
|
return sorter === "companyName"
|
||||||
|
? aCorporateName.localeCompare(bCorporateName)
|
||||||
|
: bCorporateName.localeCompare(aCorporateName);
|
||||||
|
}
|
||||||
|
|
||||||
return a.id.localeCompare(b.id);
|
return a.id.localeCompare(b.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { rows: filteredRows, renderSearch } = useListSearch(
|
||||||
|
searchFields,
|
||||||
|
displayUsers,
|
||||||
|
)
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: displayUsers,
|
data: filteredRows,
|
||||||
columns: (!showDemographicInformation ? defaultColumns : demographicColumns) as any,
|
columns: (!showDemographicInformation ? defaultColumns : demographicColumns) as any,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
});
|
});
|
||||||
@@ -532,30 +572,33 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
</Modal>
|
</Modal>
|
||||||
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
|
<div className="w-full flex flex-col gap-2">
|
||||||
<thead>
|
{renderSearch()}
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
|
||||||
<tr key={headerGroup.id}>
|
<thead>
|
||||||
{headerGroup.headers.map((header) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<th className="py-4 px-4 text-left" key={header.id}>
|
<tr key={headerGroup.id}>
|
||||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
{headerGroup.headers.map((header) => (
|
||||||
</th>
|
<th className="py-4 px-4 text-left" key={header.id}>
|
||||||
))}
|
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
</tr>
|
</th>
|
||||||
))}
|
))}
|
||||||
</thead>
|
</tr>
|
||||||
<tbody className="px-2">
|
))}
|
||||||
{table.getRowModel().rows.map((row) => (
|
</thead>
|
||||||
<tr className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2" key={row.id}>
|
<tbody className="px-2">
|
||||||
{row.getVisibleCells().map((cell) => (
|
{table.getRowModel().rows.map((row) => (
|
||||||
<td className="px-4 py-2 items-center w-fit" key={cell.id}>
|
<tr className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2" key={row.id}>
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{row.getVisibleCells().map((cell) => (
|
||||||
</td>
|
<td className="px-4 py-2 items-center w-fit" key={cell.id}>
|
||||||
))}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
</tr>
|
</td>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tr>
|
||||||
</table>
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export default function ExamPage({page}: Props) {
|
|||||||
const [showAbandonPopup, setShowAbandonPopup] = useState(false);
|
const [showAbandonPopup, setShowAbandonPopup] = useState(false);
|
||||||
const [avoidRepeated, setAvoidRepeated] = useState(false);
|
const [avoidRepeated, setAvoidRepeated] = useState(false);
|
||||||
const [timeSpent, setTimeSpent] = useState(0);
|
const [timeSpent, setTimeSpent] = useState(0);
|
||||||
|
const [statsAwaitingEvaluation, setStatsAwaitingEvaluation] = useState<string[]>([]);
|
||||||
|
|
||||||
const [exams, setExams] = useExamStore((state) => [state.exams, state.setExams]);
|
const [exams, setExams] = useExamStore((state) => [state.exams, state.setExams]);
|
||||||
const [userSolutions, setUserSolutions] = useExamStore((state) => [state.userSolutions, state.setUserSolutions]);
|
const [userSolutions, setUserSolutions] = useExamStore((state) => [state.userSolutions, state.setUserSolutions]);
|
||||||
@@ -94,6 +95,7 @@ export default function ExamPage({page}: Props) {
|
|||||||
if (selectedModules.length > 0 && exams.length !== 0 && moduleIndex >= selectedModules.length && !hasBeenUploaded && !showSolutions) {
|
if (selectedModules.length > 0 && exams.length !== 0 && moduleIndex >= selectedModules.length && !hasBeenUploaded && !showSolutions) {
|
||||||
const newStats: Stat[] = userSolutions.map((solution) => ({
|
const newStats: Stat[] = userSolutions.map((solution) => ({
|
||||||
...solution,
|
...solution,
|
||||||
|
id: solution.id || uuidv4(),
|
||||||
timeSpent,
|
timeSpent,
|
||||||
session: sessionId,
|
session: sessionId,
|
||||||
exam: solution.exam!,
|
exam: solution.exam!,
|
||||||
@@ -111,6 +113,41 @@ export default function ExamPage({page}: Props) {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [selectedModules, moduleIndex, hasBeenUploaded]);
|
}, [selectedModules, moduleIndex, hasBeenUploaded]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (statsAwaitingEvaluation.length === 0) return setIsEvaluationLoading(false);
|
||||||
|
return setIsEvaluationLoading(true);
|
||||||
|
}, [statsAwaitingEvaluation]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (statsAwaitingEvaluation.length > 0) {
|
||||||
|
statsAwaitingEvaluation.forEach(checkIfStatHasBeenEvaluated);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [statsAwaitingEvaluation]);
|
||||||
|
|
||||||
|
const checkIfStatHasBeenEvaluated = (id: string) => {
|
||||||
|
setTimeout(async () => {
|
||||||
|
const statRequest = await axios.get<Stat>(`/api/stats/${id}`);
|
||||||
|
const stat = statRequest.data;
|
||||||
|
if (stat.solutions.every((x) => x.evaluation !== null)) {
|
||||||
|
const userSolution: UserSolution = {
|
||||||
|
id,
|
||||||
|
exercise: stat.exercise,
|
||||||
|
score: stat.score,
|
||||||
|
solutions: stat.solutions,
|
||||||
|
type: stat.type,
|
||||||
|
exam: stat.exam,
|
||||||
|
module: stat.module,
|
||||||
|
};
|
||||||
|
|
||||||
|
setUserSolutions(userSolutions.map((x) => (x.exercise === userSolution.exercise ? userSolution : x)));
|
||||||
|
return setStatsAwaitingEvaluation((prev) => prev.filter((x) => x !== id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return checkIfStatHasBeenEvaluated(id);
|
||||||
|
}, 5 * 1000);
|
||||||
|
};
|
||||||
|
|
||||||
const updateExamWithUserSolutions = (exam: Exam): Exam => {
|
const updateExamWithUserSolutions = (exam: Exam): Exam => {
|
||||||
if (exam.module === "reading" || exam.module === "listening") {
|
if (exam.module === "reading" || exam.module === "listening") {
|
||||||
const parts = exam.parts.map((p) =>
|
const parts = exam.parts.map((p) =>
|
||||||
@@ -137,20 +174,19 @@ export default function ExamPage({page}: Props) {
|
|||||||
|
|
||||||
Promise.all(
|
Promise.all(
|
||||||
exam.exercises.map(async (exercise) => {
|
exam.exercises.map(async (exercise) => {
|
||||||
if (exercise.type === "writing") {
|
const evaluationID = uuidv4();
|
||||||
return await evaluateWritingAnswer(exercise, solutions.find((x) => x.exercise === exercise.id)!);
|
if (exercise.type === "writing")
|
||||||
}
|
return await evaluateWritingAnswer(exercise, solutions.find((x) => x.exercise === exercise.id)!, evaluationID);
|
||||||
|
|
||||||
if (exercise.type === "interactiveSpeaking" || exercise.type === "speaking") {
|
if (exercise.type === "interactiveSpeaking" || exercise.type === "speaking")
|
||||||
return await evaluateSpeakingAnswer(exercise, solutions.find((x) => x.exercise === exercise.id)!);
|
return await evaluateSpeakingAnswer(exercise, solutions.find((x) => x.exercise === exercise.id)!, evaluationID);
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.then((responses) => {
|
.then((responses) => {
|
||||||
|
setStatsAwaitingEvaluation((prev) => [...prev, ...responses.filter((x) => !!x).map((r) => (r as any).id)]);
|
||||||
setUserSolutions([...userSolutions, ...responses.filter((x) => !!x)] as any);
|
setUserSolutions([...userSolutions, ...responses.filter((x) => !!x)] as any);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsEvaluationLoading(false);
|
|
||||||
setHasBeenUploaded(false);
|
setHasBeenUploaded(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,21 +4,22 @@ import Input from "@/components/Low/Input";
|
|||||||
import {User} from "@/interfaces/user";
|
import {User} from "@/interfaces/user";
|
||||||
import {sendEmailVerification} from "@/utils/email";
|
import {sendEmailVerification} from "@/utils/email";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {toast} from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
import {KeyedMutator} from "swr";
|
import {KeyedMutator} from "swr";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
queryCode?: string;
|
queryCode?: string;
|
||||||
|
defaultEmail?: string;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
setIsLoading: (isLoading: boolean) => void;
|
setIsLoading: (isLoading: boolean) => void;
|
||||||
mutateUser: KeyedMutator<User>;
|
mutateUser: KeyedMutator<User>;
|
||||||
sendEmailVerification: typeof sendEmailVerification;
|
sendEmailVerification: typeof sendEmailVerification;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RegisterIndividual({queryCode, isLoading, setIsLoading, mutateUser, sendEmailVerification}: Props) {
|
export default function RegisterIndividual({queryCode, defaultEmail, isLoading, setIsLoading, mutateUser, sendEmailVerification}: Props) {
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState(defaultEmail || "");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [confirmPassword, setConfirmPassword] = useState("");
|
const [confirmPassword, setConfirmPassword] = useState("");
|
||||||
const [code, setCode] = useState(queryCode || "");
|
const [code, setCode] = useState(queryCode || "");
|
||||||
@@ -73,7 +74,15 @@ export default function RegisterIndividual({queryCode, isLoading, setIsLoading,
|
|||||||
return (
|
return (
|
||||||
<form className="flex flex-col items-center gap-6 w-full" onSubmit={register}>
|
<form className="flex flex-col items-center gap-6 w-full" onSubmit={register}>
|
||||||
<Input type="text" name="name" onChange={(e) => setName(e)} placeholder="Enter your name" defaultValue={name} required />
|
<Input type="text" name="name" onChange={(e) => setName(e)} placeholder="Enter your name" defaultValue={name} required />
|
||||||
<Input type="email" name="email" onChange={(e) => setEmail(e)} placeholder="Enter email address" defaultValue={email} required />
|
<Input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
onChange={(e) => setEmail(e)}
|
||||||
|
placeholder="Enter email address"
|
||||||
|
value={email}
|
||||||
|
disabled={!!defaultEmail}
|
||||||
|
required
|
||||||
|
/>
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
name="password"
|
name="password"
|
||||||
|
|||||||
23
src/pages/api/code/[id].ts
Normal file
23
src/pages/api/code/[id].ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// 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, query, where, setDoc, doc, getDoc, deleteDoc} from "firebase/firestore";
|
||||||
|
import {withIronSessionApiRoute} from "iron-session/next";
|
||||||
|
import {sessionOptions} from "@/lib/session";
|
||||||
|
import {uuidv4} from "@firebase/util";
|
||||||
|
|
||||||
|
const db = getFirestore(app);
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
if (req.method === "GET") return GET(req, res);
|
||||||
|
|
||||||
|
res.status(404).json({ok: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function GET(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const {id} = req.query;
|
||||||
|
|
||||||
|
const snapshot = await getDoc(doc(db, "codes", id as string));
|
||||||
|
|
||||||
|
res.status(200).json({...snapshot.data(), id: snapshot.id});
|
||||||
|
}
|
||||||
@@ -48,6 +48,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
await setDoc(codeRef, {type, code, creator: req.session.user!.id, expiryDate});
|
await setDoc(codeRef, {type, code, creator: req.session.user!.id, expiryDate});
|
||||||
|
|
||||||
if (emails && emails.length > index) {
|
if (emails && emails.length > index) {
|
||||||
|
await setDoc(codeRef, {email: emails[index]}, {merge: true});
|
||||||
|
|
||||||
const transport = prepareMailer();
|
const transport = prepareMailer();
|
||||||
const mailOptions = prepareMailOptions(
|
const mailOptions = prepareMailOptions(
|
||||||
{
|
{
|
||||||
@@ -2,12 +2,16 @@
|
|||||||
import type {NextApiRequest, NextApiResponse} from "next";
|
import type {NextApiRequest, NextApiResponse} from "next";
|
||||||
import {withIronSessionApiRoute} from "iron-session/next";
|
import {withIronSessionApiRoute} from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import axios from "axios";
|
import axios, {AxiosResponse} from "axios";
|
||||||
import formidable from "formidable-serverless";
|
import formidable from "formidable-serverless";
|
||||||
import {ref, uploadBytes} from "firebase/storage";
|
import {ref, uploadBytes} from "firebase/storage";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import {storage} from "@/firebase";
|
import {app, storage} from "@/firebase";
|
||||||
|
import {doc, getDoc, getFirestore, setDoc} from "firebase/firestore";
|
||||||
|
import {Stat} from "@/interfaces/user";
|
||||||
|
import {speakingReverseMarking} from "@/utils/score";
|
||||||
|
|
||||||
|
const db = getFirestore(app);
|
||||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
@@ -36,20 +40,41 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const backendRequest = await axios.post(
|
res.status(200).json(null);
|
||||||
`${process.env.BACKEND_URL}/speaking_task_3`,
|
|
||||||
{answers: uploadingAudios},
|
console.log("🌱 - Still processing");
|
||||||
|
const backendRequest = await evaluate({answers: uploadingAudios});
|
||||||
|
console.log("🌱 - Process complete");
|
||||||
|
|
||||||
|
const correspondingStat = (await getDoc(doc(db, "stats", fields.id))).data() as Stat;
|
||||||
|
const solutions = correspondingStat.solutions.map((x) => ({...x, evaluation: backendRequest.data, solution: uploadingAudios}));
|
||||||
|
await setDoc(
|
||||||
|
doc(db, "stats", fields.id),
|
||||||
{
|
{
|
||||||
headers: {
|
solutions,
|
||||||
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
|
score: {
|
||||||
|
correct: speakingReverseMarking[backendRequest.data.overall],
|
||||||
|
missing: 0,
|
||||||
|
total: 100,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{merge: true},
|
||||||
);
|
);
|
||||||
|
console.log("🌱 - Updated the DB");
|
||||||
res.status(200).json({...backendRequest.data, answer: uploadingAudios});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function evaluate(body: {answers: object[]}): Promise<AxiosResponse> {
|
||||||
|
const backendRequest = await axios.post(`${process.env.BACKEND_URL}/speaking_task_3`, body, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof backendRequest.data === "string") return evaluate(body);
|
||||||
|
return backendRequest;
|
||||||
|
}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
api: {
|
api: {
|
||||||
bodyParser: false,
|
bodyParser: false,
|
||||||
|
|||||||
@@ -2,12 +2,16 @@
|
|||||||
import type {NextApiRequest, NextApiResponse} from "next";
|
import type {NextApiRequest, NextApiResponse} from "next";
|
||||||
import {withIronSessionApiRoute} from "iron-session/next";
|
import {withIronSessionApiRoute} from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import axios from "axios";
|
import axios, {AxiosResponse} from "axios";
|
||||||
import formidable from "formidable-serverless";
|
import formidable from "formidable-serverless";
|
||||||
import {ref, uploadBytes} from "firebase/storage";
|
import {ref, uploadBytes} from "firebase/storage";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import {storage} from "@/firebase";
|
import {app, storage} from "@/firebase";
|
||||||
|
import {doc, getDoc, getFirestore, setDoc} from "firebase/firestore";
|
||||||
|
import {Stat} from "@/interfaces/user";
|
||||||
|
import {speakingReverseMarking} from "@/utils/score";
|
||||||
|
|
||||||
|
const db = getFirestore(app);
|
||||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
@@ -26,21 +30,46 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const binary = fs.readFileSync((audioFile as any).path).buffer;
|
const binary = fs.readFileSync((audioFile as any).path).buffer;
|
||||||
const snapshot = await uploadBytes(audioFileRef, binary);
|
const snapshot = await uploadBytes(audioFileRef, binary);
|
||||||
|
|
||||||
const backendRequest = await axios.post(
|
res.status(200).json(null);
|
||||||
`${process.env.BACKEND_URL}/speaking_task_3`,
|
|
||||||
{answers: [{question: fields.question, answer: snapshot.metadata.fullPath}]},
|
console.log("🌱 - Still processing");
|
||||||
|
const backendRequest = await evaluate({answers: [{question: fields.question, answer: snapshot.metadata.fullPath}]});
|
||||||
|
fs.rmSync((audioFile as any).path);
|
||||||
|
console.log("🌱 - Process complete");
|
||||||
|
|
||||||
|
const correspondingStat = (await getDoc(doc(db, "stats", fields.id))).data() as Stat;
|
||||||
|
const solutions = correspondingStat.solutions.map((x) => ({
|
||||||
|
...x,
|
||||||
|
evaluation: backendRequest.data,
|
||||||
|
solution: snapshot.metadata.fullPath,
|
||||||
|
}));
|
||||||
|
await setDoc(
|
||||||
|
doc(db, "stats", fields.id),
|
||||||
{
|
{
|
||||||
headers: {
|
solutions,
|
||||||
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
|
score: {
|
||||||
|
correct: speakingReverseMarking[backendRequest.data.overall],
|
||||||
|
total: 100,
|
||||||
|
missing: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{merge: true},
|
||||||
);
|
);
|
||||||
|
console.log("🌱 - Updated the DB");
|
||||||
fs.rmSync((audioFile as any).path);
|
|
||||||
res.status(200).json({...backendRequest.data, fullPath: snapshot.metadata.fullPath});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function evaluate(body: {answers: object[]}): Promise<AxiosResponse> {
|
||||||
|
const backendRequest = await axios.post(`${process.env.BACKEND_URL}/speaking_task_3`, body, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof backendRequest.data === "string") return evaluate(body);
|
||||||
|
return backendRequest;
|
||||||
|
}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
api: {
|
api: {
|
||||||
bodyParser: false,
|
bodyParser: false,
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
import type {NextApiRequest, NextApiResponse} from "next";
|
import type {NextApiRequest, NextApiResponse} from "next";
|
||||||
import {getFirestore, doc, getDoc} from "firebase/firestore";
|
import {getFirestore, doc, getDoc, setDoc} from "firebase/firestore";
|
||||||
import {withIronSessionApiRoute} from "iron-session/next";
|
import {withIronSessionApiRoute} from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import axios from "axios";
|
import axios, {AxiosResponse} from "axios";
|
||||||
|
import {app} from "@/firebase";
|
||||||
|
import {Stat} from "@/interfaces/user";
|
||||||
|
import {writingReverseMarking} from "@/utils/score";
|
||||||
|
|
||||||
interface Body {
|
interface Body {
|
||||||
question: string;
|
question: string;
|
||||||
answer: string;
|
answer: string;
|
||||||
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const db = getFirestore(app);
|
||||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
@@ -18,11 +23,36 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const backendRequest = await axios.post(`${process.env.BACKEND_URL}/writing_task2`, req.body as Body, {
|
res.status(200).json(null);
|
||||||
|
|
||||||
|
console.log("🌱 - Still processing");
|
||||||
|
const backendRequest = await evaluate(req.body as Body);
|
||||||
|
console.log("🌱 - Process complete");
|
||||||
|
|
||||||
|
const correspondingStat = (await getDoc(doc(db, "stats", req.body.id))).data() as Stat;
|
||||||
|
const solutions = correspondingStat.solutions.map((x) => ({...x, evaluation: backendRequest.data}));
|
||||||
|
await setDoc(
|
||||||
|
doc(db, "stats", (req.body as Body).id),
|
||||||
|
{
|
||||||
|
solutions,
|
||||||
|
score: {
|
||||||
|
correct: writingReverseMarking[backendRequest.data.overall],
|
||||||
|
total: 100,
|
||||||
|
missing: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{merge: true},
|
||||||
|
);
|
||||||
|
console.log("🌱 - Updated the DB");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function evaluate(body: Body): Promise<AxiosResponse> {
|
||||||
|
const backendRequest = await axios.post(`${process.env.BACKEND_URL}/writing_task2`, body as Body, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
|
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(backendRequest.status).json(backendRequest.data);
|
if (typeof backendRequest.data === "string") return evaluate(body);
|
||||||
|
return backendRequest;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
import type {NextApiRequest, NextApiResponse} from "next";
|
import type {NextApiRequest, NextApiResponse} from "next";
|
||||||
import {app} from "@/firebase";
|
import {app, storage} from "@/firebase";
|
||||||
import {getFirestore, collection, getDocs, getDoc, doc, deleteDoc, setDoc} from "firebase/firestore";
|
import {getFirestore, collection, getDocs, getDoc, doc, deleteDoc, setDoc} from "firebase/firestore";
|
||||||
import {withIronSessionApiRoute} from "iron-session/next";
|
import {withIronSessionApiRoute} from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import {Group} from "@/interfaces/user";
|
import {Group} from "@/interfaces/user";
|
||||||
|
import {Payment} from "@/interfaces/paypal";
|
||||||
|
import {deleteObject, ref} from "firebase/storage";
|
||||||
|
|
||||||
const db = getFirestore(app);
|
const db = getFirestore(app);
|
||||||
|
|
||||||
@@ -44,11 +46,14 @@ async function del(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const {id} = req.query as {id: string};
|
const {id} = req.query as {id: string};
|
||||||
|
|
||||||
const snapshot = await getDoc(doc(db, "payments", id));
|
const snapshot = await getDoc(doc(db, "payments", id));
|
||||||
|
const data = snapshot.data() as Payment;
|
||||||
|
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
if (user.type === "admin" || user.type === "developer") {
|
if (user.type === "admin" || user.type === "developer") {
|
||||||
await deleteDoc(snapshot.ref);
|
if (data.commissionTransfer) await deleteObject(ref(storage, data.commissionTransfer));
|
||||||
|
if (data.corporateTransfer) await deleteObject(ref(storage, data.corporateTransfer));
|
||||||
|
|
||||||
|
await deleteDoc(snapshot.ref);
|
||||||
res.status(200).json({ok: true});
|
res.status(200).json({ok: true});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,194 +1,180 @@
|
|||||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type {NextApiRequest, NextApiResponse} from "next";
|
||||||
import { app, storage } from "@/firebase";
|
import {app, storage} from "@/firebase";
|
||||||
import {
|
import {getFirestore, getDoc, doc, updateDoc, deleteField, setDoc} from "firebase/firestore";
|
||||||
getFirestore,
|
import {withIronSessionApiRoute} from "iron-session/next";
|
||||||
getDoc,
|
import {sessionOptions} from "@/lib/session";
|
||||||
doc,
|
import {FilesStorage} from "@/interfaces/storage.files";
|
||||||
updateDoc,
|
|
||||||
deleteField,
|
|
||||||
} from "firebase/firestore";
|
|
||||||
import { withIronSessionApiRoute } from "iron-session/next";
|
|
||||||
import { sessionOptions } from "@/lib/session";
|
|
||||||
import { FilesStorage } from "@/interfaces/storage.files";
|
|
||||||
|
|
||||||
import { Payment } from "@/interfaces/paypal";
|
import {Payment} from "@/interfaces/paypal";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import {
|
import {ref, uploadBytes, deleteObject, getDownloadURL} from "firebase/storage";
|
||||||
ref,
|
|
||||||
uploadBytes,
|
|
||||||
deleteObject,
|
|
||||||
getDownloadURL,
|
|
||||||
} from "firebase/storage";
|
|
||||||
import formidable from "formidable-serverless";
|
import formidable from "formidable-serverless";
|
||||||
|
|
||||||
const db = getFirestore(app);
|
const db = getFirestore(app);
|
||||||
|
|
||||||
const getPaymentField = (type: FilesStorage) => {
|
const getPaymentField = (type: FilesStorage) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "commission":
|
case "commission":
|
||||||
return "commissionTransfer";
|
return "commissionTransfer";
|
||||||
case "corporate":
|
case "corporate":
|
||||||
return "corporateTransfer";
|
return "corporateTransfer";
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (
|
const handleDelete = async (paymentId: string, paymentField: "commissionTransfer" | "corporateTransfer") => {
|
||||||
paymentId: string,
|
const paymentRef = doc(db, "payments", paymentId);
|
||||||
paymentField: "commissionTransfer" | "corporateTransfer"
|
const paymentDoc = await getDoc(paymentRef);
|
||||||
) => {
|
const {[paymentField]: paymentFieldPath} = paymentDoc.data() as Payment;
|
||||||
const paymentRef = doc(db, "payments", paymentId);
|
// Create a reference to the file to delete
|
||||||
const paymentDoc = await getDoc(paymentRef);
|
const documentRef = ref(storage, paymentFieldPath);
|
||||||
const { [paymentField]: paymentFieldPath } = paymentDoc.data() as Payment;
|
await deleteObject(documentRef);
|
||||||
// Create a reference to the file to delete
|
await updateDoc(paymentRef, {
|
||||||
const documentRef = ref(storage, paymentFieldPath);
|
[paymentField]: deleteField(),
|
||||||
await deleteObject(documentRef);
|
isPaid: false,
|
||||||
await updateDoc(paymentRef, {
|
});
|
||||||
[paymentField]: deleteField(),
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpload = async (
|
const handleUpload = async (req: NextApiRequest, paymentId: string, paymentField: "commissionTransfer" | "corporateTransfer") =>
|
||||||
req: NextApiRequest,
|
new Promise((resolve, reject) => {
|
||||||
paymentId: string,
|
const form = formidable({keepExtensions: true});
|
||||||
paymentField: "commissionTransfer" | "corporateTransfer"
|
form.parse(req, async (err: any, fields: any, files: any) => {
|
||||||
) =>
|
if (err) {
|
||||||
new Promise((resolve, reject) => {
|
reject(err);
|
||||||
const form = formidable({ keepExtensions: true });
|
return;
|
||||||
form.parse(req, async (err: any, fields: any, files: any) => {
|
}
|
||||||
if (err) {
|
try {
|
||||||
reject(err);
|
const {file} = files;
|
||||||
return;
|
const fileName = Date.now() + "-" + file.name;
|
||||||
}
|
const fileRef = ref(storage, fileName);
|
||||||
try {
|
|
||||||
const { file } = files;
|
|
||||||
const fileName = Date.now() + "-" + file.name;
|
|
||||||
const fileRef = ref(storage, fileName);
|
|
||||||
|
|
||||||
const binary = fs.readFileSync(file.path).buffer;
|
const binary = fs.readFileSync(file.path).buffer;
|
||||||
const snapshot = await uploadBytes(fileRef, binary);
|
const snapshot = await uploadBytes(fileRef, binary);
|
||||||
fs.rmSync(file.path);
|
fs.rmSync(file.path);
|
||||||
|
|
||||||
const paymentRef = doc(db, "payments", paymentId);
|
const paymentRef = doc(db, "payments", paymentId);
|
||||||
|
|
||||||
await updateDoc(paymentRef, {
|
await updateDoc(paymentRef, {
|
||||||
[paymentField]: snapshot.ref.fullPath,
|
[paymentField]: snapshot.ref.fullPath,
|
||||||
});
|
});
|
||||||
resolve(snapshot.ref.fullPath);
|
resolve(snapshot.ref.fullPath);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (!req.session.user) {
|
if (!req.session.user) {
|
||||||
res.status(401).json({ ok: false });
|
res.status(401).json({ok: false});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === "GET") return await get(req, res);
|
if (req.method === "GET") return await get(req, res);
|
||||||
if (req.method === "POST") return await post(req, res);
|
if (req.method === "POST") return await post(req, res);
|
||||||
if (req.method === "DELETE") return await del(req, res);
|
if (req.method === "DELETE") return await del(req, res);
|
||||||
if (req.method === "PATCH") return await patch(req, res);
|
if (req.method === "PATCH") return await patch(req, res);
|
||||||
|
|
||||||
res.status(404).json(undefined);
|
res.status(404).json(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function get(req: NextApiRequest, res: NextApiResponse) {
|
async function get(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const { type, paymentId } = req.query as {
|
const {type, paymentId} = req.query as {
|
||||||
type: FilesStorage;
|
type: FilesStorage;
|
||||||
paymentId: string;
|
paymentId: string;
|
||||||
};
|
};
|
||||||
const paymentField = getPaymentField(type);
|
const paymentField = getPaymentField(type);
|
||||||
|
|
||||||
if (paymentField === null) {
|
if (paymentField === null) {
|
||||||
res.status(500).json({ error: "Failed to identify payment field" });
|
res.status(500).json({error: "Failed to identify payment field"});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const paymentRef = doc(db, "payments", paymentId);
|
const paymentRef = doc(db, "payments", paymentId);
|
||||||
const { [paymentField]: paymentFieldPath } = (
|
const {[paymentField]: paymentFieldPath} = (await getDoc(paymentRef)).data() as Payment;
|
||||||
await getDoc(paymentRef)
|
|
||||||
).data() as Payment;
|
|
||||||
|
|
||||||
// Create a reference to the file to delete
|
// Create a reference to the file to delete
|
||||||
const documentRef = ref(storage, paymentFieldPath);
|
const documentRef = ref(storage, paymentFieldPath);
|
||||||
const url = await getDownloadURL(documentRef);
|
const url = await getDownloadURL(documentRef);
|
||||||
res.status(200).json({ url, name: documentRef.name });
|
res.status(200).json({url, name: documentRef.name});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function post(req: NextApiRequest, res: NextApiResponse) {
|
async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const { type, paymentId } = req.query as {
|
const {type, paymentId} = req.query as {
|
||||||
type: FilesStorage;
|
type: FilesStorage;
|
||||||
paymentId: string;
|
paymentId: string;
|
||||||
};
|
};
|
||||||
const paymentField = getPaymentField(type);
|
const paymentField = getPaymentField(type);
|
||||||
|
|
||||||
if (paymentField === null) {
|
if (paymentField === null) {
|
||||||
res.status(500).json({ error: "Failed to identify payment field" });
|
res.status(500).json({error: "Failed to identify payment field"});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ref = await handleUpload(req, paymentId, paymentField);
|
const ref = await handleUpload(req, paymentId, paymentField);
|
||||||
res.status(200).json({ ref });
|
|
||||||
} catch (error) {
|
const updatedDoc = (await getDoc(doc(db, "payments", paymentId))).data() as Payment;
|
||||||
res.status(500).json({ error });
|
if (updatedDoc.commissionTransfer && updatedDoc.corporateTransfer) {
|
||||||
}
|
await setDoc(doc(db, "payments", paymentId), {isPaid: true}, {merge: true});
|
||||||
|
}
|
||||||
|
res.status(200).json({ref});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({error});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function del(req: NextApiRequest, res: NextApiResponse) {
|
async function del(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const { type, paymentId } = req.query as {
|
const {type, paymentId} = req.query as {
|
||||||
type: FilesStorage;
|
type: FilesStorage;
|
||||||
paymentId: string;
|
paymentId: string;
|
||||||
};
|
};
|
||||||
const paymentField = getPaymentField(type);
|
const paymentField = getPaymentField(type);
|
||||||
if (paymentField === null) {
|
if (paymentField === null) {
|
||||||
res.status(500).json({ error: "Failed to identify payment field" });
|
res.status(500).json({error: "Failed to identify payment field"});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await handleDelete(paymentId, paymentField);
|
await handleDelete(paymentId, paymentField);
|
||||||
res.status(200).json({ ok: true });
|
res.status(200).json({ok: true});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(500).json({ error: "Failed to delete file" });
|
res.status(500).json({error: "Failed to delete file"});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function patch(req: NextApiRequest, res: NextApiResponse) {
|
async function patch(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const { type, paymentId } = req.query as {
|
const {type, paymentId} = req.query as {
|
||||||
type: FilesStorage;
|
type: FilesStorage;
|
||||||
paymentId: string;
|
paymentId: string;
|
||||||
};
|
};
|
||||||
const paymentField = getPaymentField(type);
|
const paymentField = getPaymentField(type);
|
||||||
if (paymentField === null) {
|
if (paymentField === null) {
|
||||||
res.status(500).json({ error: "Failed to identify payment field" });
|
res.status(500).json({error: "Failed to identify payment field"});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await handleDelete(paymentId, paymentField);
|
await handleDelete(paymentId, paymentField);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(500).json({ error: "Failed to delete file" });
|
res.status(500).json({error: "Failed to delete file"});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ref = await handleUpload(req, paymentId, paymentField);
|
const ref = await handleUpload(req, paymentId, paymentField);
|
||||||
res.status(200).json({ ref });
|
res.status(200).json({ref});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ error: "Failed to upload file" });
|
res.status(500).json({error: "Failed to upload file"});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
api: {
|
api: {
|
||||||
bodyParser: false,
|
bodyParser: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
23
src/pages/api/stats/[id].ts
Normal file
23
src/pages/api/stats/[id].ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// 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, query, where, setDoc, doc, getDoc, deleteDoc} from "firebase/firestore";
|
||||||
|
import {withIronSessionApiRoute} from "iron-session/next";
|
||||||
|
import {sessionOptions} from "@/lib/session";
|
||||||
|
import {uuidv4} from "@firebase/util";
|
||||||
|
|
||||||
|
const db = getFirestore(app);
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
if (req.method === "GET") return GET(req, res);
|
||||||
|
|
||||||
|
res.status(404).json({ok: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function GET(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const {id} = req.query;
|
||||||
|
|
||||||
|
const snapshot = await getDoc(doc(db, "stats", id as string));
|
||||||
|
|
||||||
|
res.status(200).json({...snapshot.data(), id: snapshot.id});
|
||||||
|
}
|
||||||
@@ -1,29 +1,23 @@
|
|||||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
import type {NextApiRequest, NextApiResponse} from "next";
|
import type {NextApiRequest, NextApiResponse} from "next";
|
||||||
import {app} from "@/firebase";
|
import {app} from "@/firebase";
|
||||||
import {getFirestore, collection, getDocs, query, where, doc, setDoc, addDoc} from "firebase/firestore";
|
import {getFirestore, collection, getDocs, query, where, setDoc, doc, getDoc, deleteDoc} from "firebase/firestore";
|
||||||
import {withIronSessionApiRoute} from "iron-session/next";
|
import {withIronSessionApiRoute} from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
|
import {uuidv4} from "@firebase/util";
|
||||||
|
|
||||||
const db = getFirestore(app);
|
const db = getFirestore(app);
|
||||||
|
|
||||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
if (req.method === "GET") return GET(req, res);
|
||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
res.status(404).json({ok: false});
|
||||||
if (!req.session.user) {
|
}
|
||||||
res.status(401).json({ok: false});
|
|
||||||
return;
|
async function GET(req: NextApiRequest, res: NextApiResponse) {
|
||||||
}
|
const {id} = req.query;
|
||||||
|
|
||||||
const {id: user} = req.query;
|
const snapshot = await getDoc(doc(db, "stats", id as string));
|
||||||
const q = query(collection(db, "stats"), where("user", "==", user));
|
|
||||||
|
res.status(200).json({...snapshot.data(), id: snapshot.id});
|
||||||
const snapshot = await getDocs(q);
|
|
||||||
|
|
||||||
res.status(200).json(
|
|
||||||
snapshot.docs.map((doc) => ({
|
|
||||||
id: doc.id,
|
|
||||||
...doc.data(),
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const stats = req.body as Stat[];
|
const stats = req.body as Stat[];
|
||||||
await stats.forEach(async (stat) => await addDoc(collection(db, "stats"), stat));
|
await stats.forEach(async (stat) => await setDoc(doc(db, "stats", stat.id), stat));
|
||||||
|
|
||||||
const groupedStatsByAssignment = groupBy(
|
const groupedStatsByAssignment = groupBy(
|
||||||
stats.filter((x) => !!x.assignment),
|
stats.filter((x) => !!x.assignment),
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ async function update(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
|
|
||||||
const q = query(collection(db, "stats"), where("user", "==", req.session.user.id));
|
const q = query(collection(db, "stats"), where("user", "==", req.session.user.id));
|
||||||
const stats = (await getDocs(q)).docs.map((doc) => ({
|
const stats = (await getDocs(q)).docs.map((doc) => ({
|
||||||
id: doc.id,
|
|
||||||
...(doc.data() as Stat),
|
...(doc.data() as Stat),
|
||||||
|
id: doc.id,
|
||||||
})) as Stat[];
|
})) as Stat[];
|
||||||
|
|
||||||
const groupedStats = groupBySession(stats);
|
const groupedStats = groupBySession(stats);
|
||||||
|
|||||||
29
src/pages/api/stats/user/[user].ts
Normal file
29
src/pages/api/stats/user/[user].ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// 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, query, where, doc, setDoc, addDoc} from "firebase/firestore";
|
||||||
|
import {withIronSessionApiRoute} from "iron-session/next";
|
||||||
|
import {sessionOptions} from "@/lib/session";
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {user} = req.query;
|
||||||
|
const q = query(collection(db, "stats"), where("user", "==", user));
|
||||||
|
|
||||||
|
const snapshot = await getDocs(q);
|
||||||
|
|
||||||
|
res.status(200).json(
|
||||||
|
snapshot.docs.map((doc) => ({
|
||||||
|
id: doc.id,
|
||||||
|
...doc.data(),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,14 +4,14 @@ import {app, storage} from "@/firebase";
|
|||||||
import {getFirestore, collection, getDocs, getDoc, doc, setDoc, query, where} from "firebase/firestore";
|
import {getFirestore, collection, getDocs, getDoc, doc, setDoc, query, where} from "firebase/firestore";
|
||||||
import {withIronSessionApiRoute} from "iron-session/next";
|
import {withIronSessionApiRoute} from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import {User} from "@/interfaces/user";
|
import {Group, User} from "@/interfaces/user";
|
||||||
import {getDownloadURL, getStorage, ref, uploadBytes} from "firebase/storage";
|
import {getDownloadURL, getStorage, ref, uploadBytes} from "firebase/storage";
|
||||||
import {getAuth, signInWithEmailAndPassword, updateEmail, updatePassword} from "firebase/auth";
|
import {getAuth, signInWithEmailAndPassword, updateEmail, updatePassword} from "firebase/auth";
|
||||||
import {errorMessages} from "@/constants/errors";
|
import {errorMessages} from "@/constants/errors";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import ShortUniqueId from "short-unique-id";
|
import ShortUniqueId from "short-unique-id";
|
||||||
import {Payment} from "@/interfaces/paypal";
|
import {Payment} from "@/interfaces/paypal";
|
||||||
import { toFixedNumber } from "@/utils/number";
|
import {toFixedNumber} from "@/utils/number";
|
||||||
const db = getFirestore(app);
|
const db = getFirestore(app);
|
||||||
const auth = getAuth(app);
|
const auth = getAuth(app);
|
||||||
|
|
||||||
@@ -22,10 +22,10 @@ export default withIronSessionApiRoute(handler, sessionOptions);
|
|||||||
// but if it is not inserted as a string, some UI components will not work (Invalid Date)
|
// but if it is not inserted as a string, some UI components will not work (Invalid Date)
|
||||||
const addPaymentRecord = async (data: any) => {
|
const addPaymentRecord = async (data: any) => {
|
||||||
await setDoc(doc(db, "payments", data.id), data);
|
await setDoc(doc(db, "payments", data.id), data);
|
||||||
}
|
};
|
||||||
const managePaymentRecords = async (user: User, userId: string | undefined): Promise<boolean> => {
|
const managePaymentRecords = async (user: User, userId: string | undefined): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
if(user.type === 'corporate' && userId) {
|
if (user.type === "corporate" && userId) {
|
||||||
const shortUID = new ShortUniqueId();
|
const shortUID = new ShortUniqueId();
|
||||||
const data: Payment = {
|
const data: Payment = {
|
||||||
id: shortUID.randomUUID(8),
|
id: shortUID.randomUUID(8),
|
||||||
@@ -40,32 +40,33 @@ const managePaymentRecords = async (user: User, userId: string | undefined): Pro
|
|||||||
};
|
};
|
||||||
|
|
||||||
const corporatePayments = await getDocs(query(collection(db, "payments"), where("corporate", "==", userId)));
|
const corporatePayments = await getDocs(query(collection(db, "payments"), where("corporate", "==", userId)));
|
||||||
if(corporatePayments.docs.length === 0) {
|
if (corporatePayments.docs.length === 0) {
|
||||||
await addPaymentRecord(data);
|
await addPaymentRecord(data);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasPaymentPaidAndExpiring = corporatePayments.docs.filter((doc) => {
|
const hasPaymentPaidAndExpiring = corporatePayments.docs.filter((doc) => {
|
||||||
const data = doc.data();
|
const data = doc.data();
|
||||||
return data.isPaid
|
return (
|
||||||
&& moment().isAfter(moment(user.subscriptionExpirationDate).subtract(30, "days"))
|
data.isPaid &&
|
||||||
&& moment().isBefore(moment(user.subscriptionExpirationDate));
|
moment().isAfter(moment(user.subscriptionExpirationDate).subtract(30, "days")) &&
|
||||||
|
moment().isBefore(moment(user.subscriptionExpirationDate))
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if(hasPaymentPaidAndExpiring.length > 0) {
|
if (hasPaymentPaidAndExpiring.length > 0) {
|
||||||
await addPaymentRecord(data);
|
await addPaymentRecord(data);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
// if this process fails it should not stop the rest of the process
|
// if this process fails it should not stop the rest of the process
|
||||||
console.log(e);
|
console.log(e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
}
|
|
||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (!req.session.user) {
|
if (!req.session.user) {
|
||||||
@@ -108,6 +109,23 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
try {
|
try {
|
||||||
const credential = await signInWithEmailAndPassword(auth, req.session.user.email, updatedUser.password);
|
const credential = await signInWithEmailAndPassword(auth, req.session.user.email, updatedUser.password);
|
||||||
await updateEmail(credential.user, updatedUser.email);
|
await updateEmail(credential.user, updatedUser.email);
|
||||||
|
|
||||||
|
if (req.session.user.type === "student") {
|
||||||
|
const corporateAdmins = ((await getDocs(collection(db, "users"))).docs.map((x) => ({...x.data(), id: x.id})) as User[])
|
||||||
|
.filter((x) => x.type === "corporate")
|
||||||
|
.map((x) => x.id);
|
||||||
|
const groups = ((await getDocs(collection(db, "groups"))).docs.map((x) => ({...x.data(), id: x.id})) as Group[]).filter(
|
||||||
|
(x) => x.participants.includes(req.session.user!.id) && corporateAdmins.includes(x.admin),
|
||||||
|
);
|
||||||
|
|
||||||
|
groups.forEach(async (group) => {
|
||||||
|
await setDoc(
|
||||||
|
doc(db, "groups", group.id),
|
||||||
|
{participants: group.participants.filter((x) => x !== req.session.user!.id)},
|
||||||
|
{merge: true},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
res.status(400).json({error: "E002", message: errorMessages.E002});
|
res.status(400).json({error: "E002", message: errorMessages.E002});
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import Layout from "@/components/High/Layout";
|
|||||||
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
||||||
import usePayments from "@/hooks/usePayments";
|
import usePayments from "@/hooks/usePayments";
|
||||||
import {Payment} from "@/interfaces/paypal";
|
import {Payment} from "@/interfaces/paypal";
|
||||||
import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table";
|
import {CellContext, createColumnHelper, flexRender, getCoreRowModel, HeaderGroup, 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";
|
||||||
@@ -62,28 +62,21 @@ const columnHelper = createColumnHelper<Payment>();
|
|||||||
|
|
||||||
const PaymentCreator = ({onClose, reload, showComission = false}: {onClose: () => void; reload: () => void; showComission: boolean}) => {
|
const PaymentCreator = ({onClose, reload, showComission = false}: {onClose: () => void; reload: () => void; showComission: boolean}) => {
|
||||||
const [corporate, setCorporate] = useState<CorporateUser>();
|
const [corporate, setCorporate] = useState<CorporateUser>();
|
||||||
const [price, setPrice] = useState<number>(0);
|
|
||||||
const [currency, setCurrency] = useState<string>("EUR");
|
|
||||||
const [commission, setCommission] = useState<number>(0);
|
|
||||||
const [referralAgent, setReferralAgent] = useState<AgentUser>();
|
|
||||||
const [date, setDate] = useState<Date>(new Date());
|
const [date, setDate] = useState<Date>(new Date());
|
||||||
|
|
||||||
const {users} = useUsers();
|
const {users} = useUsers();
|
||||||
|
|
||||||
useEffect(() => {
|
const price = corporate?.corporateInformation?.payment?.value || 0;
|
||||||
if (!corporate) return setReferralAgent(undefined);
|
const commission = corporate?.corporateInformation?.payment?.commission || 0;
|
||||||
if (!corporate.corporateInformation?.referralAgent) return setReferralAgent(undefined);
|
const currency = corporate?.corporateInformation?.payment?.currency || "EUR";
|
||||||
|
|
||||||
const referralAgent = users.find((u) => u.id === corporate.corporateInformation.referralAgent);
|
const referralAgent = useMemo(() => {
|
||||||
setReferralAgent(referralAgent as AgentUser | undefined);
|
if (corporate?.corporateInformation?.referralAgent) {
|
||||||
}, [corporate, users]);
|
return users.find((u) => u.id === corporate.corporateInformation.referralAgent);
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
return undefined;
|
||||||
const payment = corporate?.corporateInformation?.payment;
|
}, [corporate?.corporateInformation?.referralAgent, users]);
|
||||||
|
|
||||||
setPrice(payment?.value || 0);
|
|
||||||
setCurrency(payment?.currency || "EUR");
|
|
||||||
}, [corporate]);
|
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
axios
|
axios
|
||||||
@@ -91,7 +84,7 @@ const PaymentCreator = ({onClose, reload, showComission = false}: {onClose: () =
|
|||||||
corporate: corporate?.id,
|
corporate: corporate?.id,
|
||||||
agent: referralAgent?.id,
|
agent: referralAgent?.id,
|
||||||
agentCommission: commission,
|
agentCommission: commission,
|
||||||
agentValue: toFixedNumber((commission / 100) * price, 2),
|
agentValue: toFixedNumber((commission! / 100) * price!, 2),
|
||||||
currency,
|
currency,
|
||||||
value: price,
|
value: price,
|
||||||
isPaid: false,
|
isPaid: false,
|
||||||
@@ -144,18 +137,12 @@ const PaymentCreator = ({onClose, reload, showComission = false}: {onClose: () =
|
|||||||
<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">Price *</label>
|
<label className="font-normal text-base text-mti-gray-dim">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={(e) => setPrice(e ? parseInt(e) : 0)}
|
|
||||||
type="number"
|
|
||||||
value={price}
|
|
||||||
className="col-span-3"
|
|
||||||
/>
|
|
||||||
<Select
|
<Select
|
||||||
className="px-4 col-span-2 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 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}) => ({value: currency, label}))}
|
options={CURRENCIES.map(({label, currency}) => ({value: currency, label}))}
|
||||||
defaultValue={{value: currency || "EUR", label: CURRENCIES.find((c) => c.currency === currency)?.label || "Euro"}}
|
defaultValue={{value: currency || "EUR", label: CURRENCIES.find((c) => c.currency === currency)?.label || "Euro"}}
|
||||||
onChange={(value) => setCurrency(value?.value || "EUR")}
|
onChange={() => {}}
|
||||||
value={{value: currency || "EUR", label: CURRENCIES.find((c) => c.currency === currency)?.label || "Euro"}}
|
value={{value: currency || "EUR", label: CURRENCIES.find((c) => c.currency === currency)?.label || "Euro"}}
|
||||||
styles={{
|
styles={{
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
@@ -173,6 +160,7 @@ const PaymentCreator = ({onClose, reload, showComission = false}: {onClose: () =
|
|||||||
color: state.isFocused ? "black" : styles.color,
|
color: state.isFocused ? "black" : styles.color,
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
|
isDisabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -180,13 +168,13 @@ const PaymentCreator = ({onClose, reload, showComission = false}: {onClose: () =
|
|||||||
<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">Commission *</label>
|
<label className="font-normal text-base text-mti-gray-dim">Commission *</label>
|
||||||
<Input name="commission" onChange={(e) => setCommission(e ? parseInt(e) : 0)} type="number" defaultValue={0} />
|
<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">Commission Value*</label>
|
<label className="font-normal text-base text-mti-gray-dim">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}`}
|
||||||
onChange={() => null}
|
onChange={() => null}
|
||||||
type="text"
|
type="text"
|
||||||
defaultValue={0}
|
defaultValue={0}
|
||||||
@@ -237,17 +225,43 @@ const PaymentCreator = ({onClose, reload, showComission = false}: {onClose: () =
|
|||||||
};
|
};
|
||||||
|
|
||||||
const IS_PAID_OPTIONS = [
|
const IS_PAID_OPTIONS = [
|
||||||
{
|
{
|
||||||
value: null,
|
value: null,
|
||||||
label: 'All',
|
label: "All",
|
||||||
}, {
|
},
|
||||||
value: false,
|
{
|
||||||
label: 'Unpaid',
|
value: false,
|
||||||
}, {
|
label: "Unpaid",
|
||||||
value: true,
|
},
|
||||||
label: 'Paid',
|
{
|
||||||
},
|
value: true,
|
||||||
|
label: "Paid",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const IS_FILE_SUBMITTED_OPTIONS = [
|
||||||
|
{
|
||||||
|
value: null,
|
||||||
|
label: "All",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: false,
|
||||||
|
label: "Submitted",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
label: "Not Submitted",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const CSV_WHITELISTED_KEYS = ["corporateId", "corporate", "date", "amount", "agent", "agentCommission", "agentValue", "isPaid"];
|
||||||
|
|
||||||
|
interface SimpleCSVColumn {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
export default function PaymentRecord() {
|
export default function PaymentRecord() {
|
||||||
const [selectedCorporateUser, setSelectedCorporateUser] = useState<User>();
|
const [selectedCorporateUser, setSelectedCorporateUser] = useState<User>();
|
||||||
const [selectedAgentUser, setSelectedAgentUser] = useState<User>();
|
const [selectedAgentUser, setSelectedAgentUser] = useState<User>();
|
||||||
@@ -262,8 +276,10 @@ export default function PaymentRecord() {
|
|||||||
const {users, reload: reloadUsers} = useUsers();
|
const {users, reload: reloadUsers} = useUsers();
|
||||||
const {payments: originalPayments, reload: reloadPayment} = usePayments();
|
const {payments: originalPayments, reload: reloadPayment} = usePayments();
|
||||||
const [startDate, setStartDate] = useState<Date | null>(moment("01/01/2023").toDate());
|
const [startDate, setStartDate] = useState<Date | null>(moment("01/01/2023").toDate());
|
||||||
const [endDate, setEndDate] = useState<Date | null>(moment().endOf('day').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>(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();
|
||||||
@@ -298,8 +314,6 @@ export default function PaymentRecord() {
|
|||||||
]);
|
]);
|
||||||
}, [agent]);
|
}, [agent]);
|
||||||
|
|
||||||
useEffect(() => console.log(filters), [filters]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFilters((prev) => [
|
setFilters((prev) => [
|
||||||
...prev.filter((x) => x.id !== "corporate-filter"),
|
...prev.filter((x) => x.id !== "corporate-filter"),
|
||||||
@@ -310,10 +324,33 @@ 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' ? [] : [{id: "paid", filter: (p: Payment) => p.isPaid === paid}]),
|
...(typeof paid !== "boolean" ? [] : [{id: "paid", filter: (p: Payment) => p.isPaid === paid}]),
|
||||||
])
|
]);
|
||||||
}, [paid]);
|
}, [paid]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFilters((prev) => [
|
||||||
|
...prev.filter((x) => x.id !== "commissionTransfer"),
|
||||||
|
...(typeof commissionTransfer !== "boolean"
|
||||||
|
? []
|
||||||
|
: [{id: "commissionTransfer", filter: (p: Payment) => !p.commissionTransfer === commissionTransfer}]),
|
||||||
|
]);
|
||||||
|
}, [commissionTransfer]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFilters((prev) => [
|
||||||
|
...prev.filter((x) => x.id !== "corporateTransfer"),
|
||||||
|
...(typeof corporateTransfer !== "boolean"
|
||||||
|
? []
|
||||||
|
: [{id: "corporateTransfer", filter: (p: Payment) => !p.corporateTransfer === corporateTransfer}]),
|
||||||
|
]);
|
||||||
|
}, [corporateTransfer]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user && user.type === "corporate") return setCorporate(user);
|
||||||
|
if (user && user.type === "agent") return setAgent(user);
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
const updatePayment = (payment: Payment, key: string, value: any) => {
|
const updatePayment = (payment: Payment, key: string, value: any) => {
|
||||||
axios
|
axios
|
||||||
.patch(`api/payments/${payment.id}`, {...payment, [key]: value})
|
.patch(`api/payments/${payment.id}`, {...payment, [key]: value})
|
||||||
@@ -355,6 +392,7 @@ export default function PaymentRecord() {
|
|||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
<div className={containerClassName}>
|
<div className={containerClassName}>
|
||||||
<PaymentAssetManager
|
<PaymentAssetManager
|
||||||
|
reload={reload}
|
||||||
permissions={info.row.original.isPaid ? "read" : "write"}
|
permissions={info.row.original.isPaid ? "read" : "write"}
|
||||||
asset={info.row.original.corporateTransfer}
|
asset={info.row.original.corporateTransfer}
|
||||||
paymentId={info.row.original.id}
|
paymentId={info.row.original.id}
|
||||||
@@ -372,6 +410,7 @@ export default function PaymentRecord() {
|
|||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
<div className={containerClassName}>
|
<div className={containerClassName}>
|
||||||
<PaymentAssetManager
|
<PaymentAssetManager
|
||||||
|
reload={reload}
|
||||||
permissions="read"
|
permissions="read"
|
||||||
asset={info.row.original.commissionTransfer}
|
asset={info.row.original.commissionTransfer}
|
||||||
paymentId={info.row.original.id}
|
paymentId={info.row.original.id}
|
||||||
@@ -389,6 +428,7 @@ export default function PaymentRecord() {
|
|||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
<div className={containerClassName}>
|
<div className={containerClassName}>
|
||||||
<PaymentAssetManager
|
<PaymentAssetManager
|
||||||
|
reload={reload}
|
||||||
permissions="read"
|
permissions="read"
|
||||||
asset={info.row.original.corporateTransfer}
|
asset={info.row.original.corporateTransfer}
|
||||||
paymentId={info.row.original.id}
|
paymentId={info.row.original.id}
|
||||||
@@ -403,6 +443,7 @@ export default function PaymentRecord() {
|
|||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
<div className={containerClassName}>
|
<div className={containerClassName}>
|
||||||
<PaymentAssetManager
|
<PaymentAssetManager
|
||||||
|
reload={reload}
|
||||||
permissions={info.row.original.isPaid ? "read" : "write"}
|
permissions={info.row.original.isPaid ? "read" : "write"}
|
||||||
asset={info.row.original.commissionTransfer}
|
asset={info.row.original.commissionTransfer}
|
||||||
paymentId={info.row.original.id}
|
paymentId={info.row.original.id}
|
||||||
@@ -420,6 +461,7 @@ export default function PaymentRecord() {
|
|||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
<div className={containerClassName}>
|
<div className={containerClassName}>
|
||||||
<PaymentAssetManager
|
<PaymentAssetManager
|
||||||
|
reload={reload}
|
||||||
permissions="write"
|
permissions="write"
|
||||||
asset={info.row.original.corporateTransfer}
|
asset={info.row.original.corporateTransfer}
|
||||||
paymentId={info.row.original.id}
|
paymentId={info.row.original.id}
|
||||||
@@ -434,6 +476,7 @@ export default function PaymentRecord() {
|
|||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
<div className={containerClassName}>
|
<div className={containerClassName}>
|
||||||
<PaymentAssetManager
|
<PaymentAssetManager
|
||||||
|
reload={reload}
|
||||||
permissions="write"
|
permissions="write"
|
||||||
asset={info.row.original.commissionTransfer}
|
asset={info.row.original.commissionTransfer}
|
||||||
paymentId={info.row.original.id}
|
paymentId={info.row.original.id}
|
||||||
@@ -450,24 +493,114 @@ export default function PaymentRecord() {
|
|||||||
|
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const columHelperValue = (key: string, info: any) => {
|
||||||
|
switch (key) {
|
||||||
|
case "agentCommission": {
|
||||||
|
const value = info.getValue();
|
||||||
|
return {value: `${value}%`};
|
||||||
|
}
|
||||||
|
case "agent": {
|
||||||
|
const user = users.find((x) => x.id === info.row.original.agent) as AgentUser;
|
||||||
|
return {
|
||||||
|
value: user?.name,
|
||||||
|
user,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "agentValue":
|
||||||
|
case "amount": {
|
||||||
|
const value = info.getValue();
|
||||||
|
const numberValue = toFixedNumber(value, 2);
|
||||||
|
return {value: numberValue};
|
||||||
|
}
|
||||||
|
case "date": {
|
||||||
|
const value = info.getValue();
|
||||||
|
return {value: moment(value).format("DD/MM/YYYY")};
|
||||||
|
}
|
||||||
|
case "corporate": {
|
||||||
|
const specificValue = info.row.original.corporate;
|
||||||
|
const user = users.find((x) => x.id === specificValue) as CorporateUser;
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
value: user?.corporateInformation.companyInformation.name || user?.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "currency": {
|
||||||
|
return {
|
||||||
|
value: info.row.original.currency,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "isPaid":
|
||||||
|
case "corporateId":
|
||||||
|
default: {
|
||||||
|
const value = info.getValue();
|
||||||
|
return {value};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const hiddenToCorporateColumns = () => {
|
||||||
|
if (user && user.type !== "corporate")
|
||||||
|
return [
|
||||||
|
columnHelper.accessor("agent", {
|
||||||
|
header: "Country Manager",
|
||||||
|
id: "agent",
|
||||||
|
cell: (info) => {
|
||||||
|
const {user, value} = columHelperValue(info.column.id, info);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer",
|
||||||
|
)}
|
||||||
|
onClick={() => setSelectedAgentUser(user)}>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("agentCommission", {
|
||||||
|
header: "Commission",
|
||||||
|
id: "agentCommission",
|
||||||
|
cell: (info) => {
|
||||||
|
const {value} = columHelperValue(info.column.id, info);
|
||||||
|
return <>{value}</>;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("agentValue", {
|
||||||
|
header: "Commission Value",
|
||||||
|
id: "agentValue",
|
||||||
|
cell: (info) => {
|
||||||
|
const {value} = columHelperValue(info.column.id, info);
|
||||||
|
const currency = CURRENCIES.find((x) => x.currency === info.row.original.currency)?.label;
|
||||||
|
const finalValue = `${value} ${currency}`;
|
||||||
|
return <span>{finalValue}</span>;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
const defaultColumns = [
|
const defaultColumns = [
|
||||||
columnHelper.accessor("id", {
|
columnHelper.accessor("corporate", {
|
||||||
header: "ID",
|
header: "Corporate ID",
|
||||||
id: "id",
|
id: "corporateId",
|
||||||
cell: (info) => info.getValue(),
|
cell: (info) => {
|
||||||
|
const {value} = columHelperValue(info.column.id, info);
|
||||||
|
return value;
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("corporate", {
|
columnHelper.accessor("corporate", {
|
||||||
header: "Corporate",
|
header: "Corporate",
|
||||||
id: "corporate",
|
id: "corporate",
|
||||||
cell: (info) => {
|
cell: (info) => {
|
||||||
const user = users.find((x) => x.id === info.row.original.corporate) as CorporateUser;
|
const {user, value} = columHelperValue(info.column.id, info);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
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)}>
|
||||||
{user?.corporateInformation.companyInformation.name || user?.name}
|
{value}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -475,52 +608,43 @@ export default function PaymentRecord() {
|
|||||||
columnHelper.accessor("date", {
|
columnHelper.accessor("date", {
|
||||||
header: "Date",
|
header: "Date",
|
||||||
id: "date",
|
id: "date",
|
||||||
cell: (info) => <span>{moment(info.getValue()).format("DD/MM/YYYY")}</span>,
|
cell: (info) => {
|
||||||
|
const {value} = columHelperValue(info.column.id, info);
|
||||||
|
return <span>{value}</span>;
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("value", {
|
columnHelper.accessor("value", {
|
||||||
header: "Amount",
|
header: "Amount",
|
||||||
id: "amount",
|
id: "amount",
|
||||||
cell: (info) => (
|
cell: (info) => {
|
||||||
<span>
|
const {value} = columHelperValue(info.column.id, info);
|
||||||
{toFixedNumber(info.getValue(), 2)} {CURRENCIES.find((x) => x.currency === info.row.original.currency)?.label}
|
const currency = CURRENCIES.find((x) => x.currency === info.row.original.currency)?.label;
|
||||||
</span>
|
const finalValue = `${value} ${currency}`;
|
||||||
),
|
return <span>{finalValue}</span>;
|
||||||
}),
|
},
|
||||||
columnHelper.accessor("agent", {
|
|
||||||
header: "Country Manager",
|
|
||||||
id: "agent",
|
|
||||||
cell: (info) => (
|
|
||||||
<div
|
|
||||||
className={clsx("underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer")}
|
|
||||||
onClick={() => setSelectedAgentUser(users.find((x) => x.id === info.row.original.agent))}>
|
|
||||||
{(users.find((x) => x.id === info.row.original.agent) as AgentUser)?.name}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("agentCommission", {
|
|
||||||
header: "Commission",
|
|
||||||
id: "agentCommission",
|
|
||||||
cell: (info) => <>{info.getValue()}%</>,
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("agentValue", {
|
|
||||||
header: "Commission Value",
|
|
||||||
id: "agentValue",
|
|
||||||
cell: (info) => (
|
|
||||||
<span>
|
|
||||||
{toFixedNumber(info.getValue(), 2)} {CURRENCIES.find((x) => x.currency === info.row.original.currency)?.label}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
|
...hiddenToCorporateColumns(),
|
||||||
columnHelper.accessor("isPaid", {
|
columnHelper.accessor("isPaid", {
|
||||||
header: "Paid",
|
header: "Paid",
|
||||||
id: "isPaid",
|
id: "isPaid",
|
||||||
cell: (info) => (
|
cell: (info) => {
|
||||||
<Checkbox
|
const {value} = columHelperValue(info.column.id, info);
|
||||||
isChecked={info.getValue()}
|
|
||||||
onChange={(e) => (user?.type !== "agent" ? updatePayment(info.row.original, "isPaid", e) : null)}>
|
return (
|
||||||
<span></span>
|
<Checkbox
|
||||||
</Checkbox>
|
isChecked={value}
|
||||||
),
|
onChange={(e) => {
|
||||||
|
if (user?.type === agent || user?.type === "corporate" || value) return null;
|
||||||
|
if (!info.row.original.commissionTransfer || !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);
|
||||||
|
}}>
|
||||||
|
<span></span>
|
||||||
|
</Checkbox>
|
||||||
|
);
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
...getFileAssetsColumns(),
|
...getFileAssetsColumns(),
|
||||||
{
|
{
|
||||||
@@ -547,8 +671,8 @@ export default function PaymentRecord() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const getUserModal = () => {
|
const getUserModal = () => {
|
||||||
if(user) {
|
if (user) {
|
||||||
if(selectedCorporateUser) {
|
if (selectedCorporateUser) {
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={!!selectedCorporateUser} onClose={() => setSelectedCorporateUser(undefined)}>
|
<Modal isOpen={!!selectedCorporateUser} onClose={() => setSelectedCorporateUser(undefined)}>
|
||||||
<>
|
<>
|
||||||
@@ -570,7 +694,7 @@ export default function PaymentRecord() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(selectedAgentUser) {
|
if (selectedAgentUser) {
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={!!selectedAgentUser} onClose={() => setSelectedAgentUser(undefined)}>
|
<Modal isOpen={!!selectedAgentUser} onClose={() => setSelectedAgentUser(undefined)}>
|
||||||
<>
|
<>
|
||||||
@@ -593,8 +717,48 @@ export default function PaymentRecord() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const getCSVData = () => {
|
||||||
|
const columns = table.getHeaderGroups().reduce((accm: SimpleCSVColumn[], group: HeaderGroup<Payment>) => {
|
||||||
|
const whitelistedColumns = group.headers.filter((header) => CSV_WHITELISTED_KEYS.includes(header.id));
|
||||||
|
|
||||||
|
const data = whitelistedColumns.map((data) => ({
|
||||||
|
key: data.column.columnDef.id,
|
||||||
|
label: data.column.columnDef.header,
|
||||||
|
})) as SimpleCSVColumn[];
|
||||||
|
|
||||||
|
return [...accm, ...data];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const {rows} = table.getRowModel();
|
||||||
|
|
||||||
|
const finalColumns = [
|
||||||
|
...columns,
|
||||||
|
{
|
||||||
|
key: "currency",
|
||||||
|
label: "Currency",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
columns: finalColumns,
|
||||||
|
rows: rows.map((row) => {
|
||||||
|
return finalColumns.reduce((accm, {key}) => {
|
||||||
|
const {value} = columHelperValue(key, {
|
||||||
|
row,
|
||||||
|
getValue: () => row.getValue(key),
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...accm,
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
}, {});
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const {rows: csvRows, columns: csvColumns} = getCSVData();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@@ -623,15 +787,7 @@ export default function PaymentRecord() {
|
|||||||
{(user.type === "developer" || user.type === "admin") && (
|
{(user.type === "developer" || user.type === "admin") && (
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<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={displayPayments}
|
|
||||||
headers={defaultColumns
|
|
||||||
.filter((e) => e.header)
|
|
||||||
.map((e) => ({
|
|
||||||
label: e.header?.toString() || "",
|
|
||||||
key: e.id || "",
|
|
||||||
}))}
|
|
||||||
filename="payment-records.csv">
|
|
||||||
Download CSV
|
Download CSV
|
||||||
</CSVLink>
|
</CSVLink>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -641,17 +797,30 @@ export default function PaymentRecord() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-8 w-full">
|
<div 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">Corporate account *</label>
|
<label className="font-normal text-base text-mti-gray-dim">Corporate account *</label>
|
||||||
<Select
|
<Select
|
||||||
isClearable
|
isClearable={user.type !== "corporate"}
|
||||||
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={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",
|
||||||
|
user.type === "corporate" && "!bg-mti-gray-platinum/40 !text-mti-gray-dim !cursor-not-allowed",
|
||||||
|
)}
|
||||||
options={(users.filter((u) => u.type === "corporate") as CorporateUser[]).map((user) => ({
|
options={(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={
|
||||||
|
user.type === "corporate"
|
||||||
|
? {
|
||||||
|
value: user.id,
|
||||||
|
meta: user,
|
||||||
|
label: `${user.corporateInformation.companyInformation.name || user.name} - ${user.email}`,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
isDisabled={user.type === "corporate"}
|
||||||
onChange={(value) => setCorporate((value as any)?.meta ?? undefined)}
|
onChange={(value) => setCorporate((value as any)?.meta ?? undefined)}
|
||||||
styles={{
|
styles={{
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
@@ -671,40 +840,42 @@ export default function PaymentRecord() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3 w-full">
|
{user.type !== "corporate" && (
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Country manager *</label>
|
<div className="flex flex-col gap-3 w-full">
|
||||||
<Select
|
<label className="font-normal text-base text-mti-gray-dim">Country manager *</label>
|
||||||
isClearable
|
<Select
|
||||||
isDisabled={user.type === "agent"}
|
isClearable
|
||||||
className={clsx(
|
isDisabled={user.type === "agent"}
|
||||||
"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",
|
className={clsx(
|
||||||
user.type === "agent" ? "bg-mti-gray-platinum/40" : "bg-white",
|
"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" ? "bg-mti-gray-platinum/40" : "bg-white",
|
||||||
options={(users.filter((u) => u.type === "agent") as AgentUser[]).map((user) => ({
|
)}
|
||||||
value: user.id,
|
options={(users.filter((u) => u.type === "agent") as AgentUser[]).map((user) => ({
|
||||||
meta: user,
|
value: user.id,
|
||||||
label: `${user.name} - ${user.email}`,
|
meta: user,
|
||||||
}))}
|
label: `${user.name} - ${user.email}`,
|
||||||
value={agent ? {value: agent?.id, label: `${agent.name} - ${agent.email}`} : undefined}
|
}))}
|
||||||
onChange={(value) => setAgent(value !== null ? (value as any).meta : undefined)}
|
value={agent ? {value: agent?.id, label: `${agent.name} - ${agent.email}`} : undefined}
|
||||||
styles={{
|
onChange={(value) => setAgent(value !== null ? (value as any).meta : undefined)}
|
||||||
control: (styles) => ({
|
styles={{
|
||||||
...styles,
|
control: (styles) => ({
|
||||||
paddingLeft: "4px",
|
...styles,
|
||||||
border: "none",
|
paddingLeft: "4px",
|
||||||
outline: "none",
|
border: "none",
|
||||||
":focus": {
|
|
||||||
outline: "none",
|
outline: "none",
|
||||||
},
|
":focus": {
|
||||||
}),
|
outline: "none",
|
||||||
option: (styles, state) => ({
|
},
|
||||||
...styles,
|
}),
|
||||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
option: (styles, state) => ({
|
||||||
color: state.isFocused ? "black" : styles.color,
|
...styles,
|
||||||
}),
|
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||||
}}
|
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">Paid</label>
|
<label className="font-normal text-base text-mti-gray-dim">Paid</label>
|
||||||
<Select
|
<Select
|
||||||
@@ -716,9 +887,8 @@ export default function PaymentRecord() {
|
|||||||
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)}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
if(value) {
|
if (value) return setPaid(value.value);
|
||||||
setPaid(value.value);
|
setPaid(null);
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
styles={{
|
styles={{
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
@@ -742,7 +912,7 @@ export default function PaymentRecord() {
|
|||||||
<label className="font-normal text-base text-mti-gray-dim">Date</label>
|
<label className="font-normal text-base text-mti-gray-dim">Date</label>
|
||||||
<ReactDatePicker
|
<ReactDatePicker
|
||||||
dateFormat="dd/MM/yyyy"
|
dateFormat="dd/MM/yyyy"
|
||||||
className="border border-mti-gray-dim/40 px-4 py-1.5 rounded-lg text-center w-[256px]"
|
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"
|
||||||
selected={startDate}
|
selected={startDate}
|
||||||
startDate={startDate}
|
startDate={startDate}
|
||||||
endDate={endDate}
|
endDate={endDate}
|
||||||
@@ -751,16 +921,80 @@ export default function PaymentRecord() {
|
|||||||
filterDate={(date: Date) => moment(date).isSameOrBefore(moment(new Date()))}
|
filterDate={(date: Date) => moment(date).isSameOrBefore(moment(new Date()))}
|
||||||
onChange={([initialDate, finalDate]: [Date, Date]) => {
|
onChange={([initialDate, finalDate]: [Date, Date]) => {
|
||||||
setStartDate(initialDate ?? moment("01/01/2023").toDate());
|
setStartDate(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
|
||||||
setEndDate(moment(finalDate).endOf('day').toDate());
|
setEndDate(moment(finalDate).endOf("day").toDate());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setEndDate(null);
|
setEndDate(null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{user.type !== "corporate" && (
|
||||||
|
<div className="flex flex-col gap-3 w-full">
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">Commission transfer</label>
|
||||||
|
<Select
|
||||||
|
isClearable
|
||||||
|
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",
|
||||||
|
)}
|
||||||
|
options={IS_FILE_SUBMITTED_OPTIONS}
|
||||||
|
value={IS_FILE_SUBMITTED_OPTIONS.find((e) => e.value === commissionTransfer)}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (value) return setCommissionTransfer(value.value);
|
||||||
|
setCommissionTransfer(null);
|
||||||
|
}}
|
||||||
|
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-full">
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">Corporate transfer</label>
|
||||||
|
<Select
|
||||||
|
isClearable
|
||||||
|
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",
|
||||||
|
)}
|
||||||
|
options={IS_FILE_SUBMITTED_OPTIONS}
|
||||||
|
value={IS_FILE_SUBMITTED_OPTIONS.find((e) => e.value === corporateTransfer)}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (value) return setCorporateTransfer(value.value);
|
||||||
|
setCorporateTransfer(null);
|
||||||
|
}}
|
||||||
|
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>
|
</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>
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
|||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import {BsCamera, BsCameraFill} from "react-icons/bs";
|
import {BsCamera, BsCameraFill} from "react-icons/bs";
|
||||||
import {USER_TYPE_LABELS} from "@/resources/user";
|
import {USER_TYPE_LABELS} from "@/resources/user";
|
||||||
|
import useGroups from "@/hooks/useGroups";
|
||||||
|
import useUsers from "@/hooks/useUsers";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
@@ -50,27 +52,36 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
|||||||
};
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
export default function Home() {
|
interface Props {
|
||||||
const [bio, setBio] = useState("");
|
user: User;
|
||||||
const [name, setName] = useState("");
|
mutateUser: Function;
|
||||||
const [email, setEmail] = useState("");
|
}
|
||||||
|
|
||||||
|
function UserProfile({user, mutateUser}: Props) {
|
||||||
|
const [bio, setBio] = useState(user.bio || "");
|
||||||
|
const [name, setName] = useState(user.name || "");
|
||||||
|
const [email, setEmail] = useState(user.email || "");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [newPassword, setNewPassword] = useState("");
|
const [newPassword, setNewPassword] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [profilePicture, setProfilePicture] = useState("");
|
const [profilePicture, setProfilePicture] = useState(user.profilePicture);
|
||||||
|
|
||||||
const [country, setCountry] = useState<string>();
|
const [country, setCountry] = useState<string>(user.demographicInformation?.country || "");
|
||||||
const [phone, setPhone] = useState<string>();
|
const [phone, setPhone] = useState<string>(user.demographicInformation?.phone || "");
|
||||||
const [gender, setGender] = useState<Gender>();
|
const [gender, setGender] = useState<Gender | undefined>(user.demographicInformation?.gender || undefined);
|
||||||
const [employment, setEmployment] = useState<EmploymentStatus>();
|
const [employment, setEmployment] = useState<EmploymentStatus | undefined>(
|
||||||
const [position, setPosition] = useState<string>();
|
user.type === "corporate" ? undefined : user.demographicInformation?.employment,
|
||||||
const [companyName, setCompanyName] = useState<string>("");
|
);
|
||||||
const [commercialRegistration, setCommercialRegistration] = useState<string>("");
|
const [position, setPosition] = useState<string | undefined>(user.type === "corporate" ? user.demographicInformation?.position : undefined);
|
||||||
|
const [companyName, setCompanyName] = useState<string | undefined>(user.type === "agent" ? user.agentInformation?.companyName : undefined);
|
||||||
|
const [commercialRegistration, setCommercialRegistration] = useState<string | undefined>(
|
||||||
|
user.type === "agent" ? user.agentInformation?.commercialRegistration : undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
const {groups} = useGroups();
|
||||||
|
const {users} = useUsers();
|
||||||
|
|
||||||
const profilePictureInput = useRef(null);
|
const profilePictureInput = useRef(null);
|
||||||
|
|
||||||
const {user, mutateUser} = useUser({redirectTo: "/login"});
|
|
||||||
|
|
||||||
const expirationDateColor = (date: Date) => {
|
const expirationDateColor = (date: Date) => {
|
||||||
const momentDate = moment(date);
|
const momentDate = moment(date);
|
||||||
const today = moment(new Date());
|
const today = moment(new Date());
|
||||||
@@ -80,24 +91,6 @@ export default function Home() {
|
|||||||
if (today.add(7, "days").isAfter(momentDate)) return "!bg-mti-orange-ultralight border-mti-orange-light";
|
if (today.add(7, "days").isAfter(momentDate)) return "!bg-mti-orange-ultralight border-mti-orange-light";
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (user) {
|
|
||||||
setName(user.name);
|
|
||||||
setEmail(user.email);
|
|
||||||
setBio(user.bio);
|
|
||||||
setProfilePicture(user.profilePicture);
|
|
||||||
setCountry(user.demographicInformation?.country);
|
|
||||||
setPhone(user.demographicInformation?.phone);
|
|
||||||
setGender(user.demographicInformation?.gender);
|
|
||||||
setEmployment(user.type === "corporate" ? undefined : user.demographicInformation?.employment);
|
|
||||||
setPosition(user.type === "corporate" ? user.demographicInformation?.position : undefined);
|
|
||||||
if(user.type === 'agent') {
|
|
||||||
setCompanyName(user.agentInformation?.companyName)
|
|
||||||
setCommercialRegistration(user.agentInformation?.commercialRegistration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [user]);
|
|
||||||
|
|
||||||
const convertBase64 = (file: File) => {
|
const convertBase64 = (file: File) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const fileReader = new FileReader();
|
const fileReader = new FileReader();
|
||||||
@@ -133,6 +126,19 @@ export default function Home() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (email !== user?.email) {
|
||||||
|
const userAdmins = groups.filter((x) => x.participants.includes(user.id)).map((x) => x.admin);
|
||||||
|
const message =
|
||||||
|
users.filter((x) => userAdmins.includes(x.id) && x.type === "corporate").length > 0
|
||||||
|
? "If you change your e-mail address, you will lose all benefits from your university/institute. Are you sure you want to continue?"
|
||||||
|
: "Are you sure you want to update your e-mail address?";
|
||||||
|
|
||||||
|
if (!confirm(message)) {
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const request = await axios.post("/api/users/update", {
|
const request = await axios.post("/api/users/update", {
|
||||||
bio,
|
bio,
|
||||||
name,
|
name,
|
||||||
@@ -159,6 +165,257 @@ export default function Home() {
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout user={user}>
|
||||||
|
<section className="w-full flex flex-col gap-4 md:gap-8 px-4 py-8">
|
||||||
|
<h1 className="text-4xl font-bold mb-6 md:hidden">Edit Profile</h1>
|
||||||
|
<div className="flex -md:flex-col-reverse -md:items-center w-full justify-between">
|
||||||
|
<div className="flex flex-col gap-8 w-full md:w-2/3">
|
||||||
|
<h1 className="text-4xl font-bold mb-6 -md:hidden">Edit Profile</h1>
|
||||||
|
<form className="flex flex-col items-center gap-6 w-full">
|
||||||
|
<div className="flex flex-col md:flex-row gap-8 w-full">
|
||||||
|
<Input
|
||||||
|
label="Name"
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
onChange={(e) => setName(e)}
|
||||||
|
placeholder="Enter your name"
|
||||||
|
defaultValue={name}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="E-mail Address"
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
onChange={(e) => setEmail(e)}
|
||||||
|
placeholder="Enter email address"
|
||||||
|
defaultValue={email}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col md:flex-row gap-8 w-full">
|
||||||
|
<Input
|
||||||
|
label="Current Password"
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
onChange={(e) => setPassword(e)}
|
||||||
|
placeholder="Enter your password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="New Password"
|
||||||
|
type="password"
|
||||||
|
name="newPassword"
|
||||||
|
onChange={(e) => setNewPassword(e)}
|
||||||
|
placeholder="Enter your new password (optional)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{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={() => null}
|
||||||
|
placeholder="Enter corporate name"
|
||||||
|
defaultValue={companyName}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Commercial Registration"
|
||||||
|
type="text"
|
||||||
|
name="commercialRegistration"
|
||||||
|
onChange={() => null}
|
||||||
|
placeholder="Enter commercial registration"
|
||||||
|
defaultValue={commercialRegistration}
|
||||||
|
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 value={country} onChange={setCountry} />
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="tel"
|
||||||
|
name="phone"
|
||||||
|
label="Phone number"
|
||||||
|
onChange={(e) => setPhone(e)}
|
||||||
|
placeholder="Enter phone number"
|
||||||
|
defaultValue={phone}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col md:flex-row gap-8 w-full">
|
||||||
|
{user.type === "corporate" && (
|
||||||
|
<Input
|
||||||
|
name="position"
|
||||||
|
onChange={setPosition}
|
||||||
|
defaultValue={position}
|
||||||
|
type="text"
|
||||||
|
label="Position"
|
||||||
|
placeholder="CEO, Head of Marketing..."
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{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={employment}
|
||||||
|
onChange={setEmployment}
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
<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={gender} onChange={setGender} 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">
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">Expiry Date (click to purchase)</label>
|
||||||
|
<Link
|
||||||
|
href="/payment"
|
||||||
|
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",
|
||||||
|
!user.subscriptionExpirationDate
|
||||||
|
? "!bg-mti-green-ultralight !border-mti-green-light"
|
||||||
|
: expirationDateColor(user.subscriptionExpirationDate),
|
||||||
|
"bg-white border-mti-gray-platinum",
|
||||||
|
)}>
|
||||||
|
{!user.subscriptionExpirationDate && "Unlimited"}
|
||||||
|
{user.subscriptionExpirationDate && moment(user.subscriptionExpirationDate).format("DD/MM/YYYY")}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-6 w-48">
|
||||||
|
<div
|
||||||
|
className="flex flex-col gap-3 items-center h-fit cursor-pointer group"
|
||||||
|
onClick={() => (profilePictureInput.current as any)?.click()}>
|
||||||
|
<div className="relative overflow-hidden h-48 w-48 rounded-full">
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"absolute top-0 left-0 bg-mti-purple-light/60 w-full h-full z-20 flex items-center justify-center opacity-0 group-hover:opacity-100",
|
||||||
|
"transition ease-in-out duration-300",
|
||||||
|
)}>
|
||||||
|
<BsCamera className="text-6xl text-mti-purple-ultralight/80" />
|
||||||
|
</div>
|
||||||
|
<img src={profilePicture} alt={user.name} className="aspect-square drop-shadow-xl self-end object-cover" />
|
||||||
|
</div>
|
||||||
|
<input type="file" className="hidden" onChange={uploadProfilePicture} accept="image/*" ref={profilePictureInput} />
|
||||||
|
<span
|
||||||
|
onClick={() => (profilePictureInput.current as any)?.click()}
|
||||||
|
className="cursor-pointer text-mti-purple-light text-sm">
|
||||||
|
Change picture
|
||||||
|
</span>
|
||||||
|
<h6 className="font-normal text-base text-mti-gray-taupe">{USER_TYPE_LABELS[user.type]}</h6>
|
||||||
|
</div>
|
||||||
|
{user.type === "agent" && (
|
||||||
|
<div className="flag items-center h-fit">
|
||||||
|
<img
|
||||||
|
alt={user.demographicInformation?.country.toLowerCase() + "_flag"}
|
||||||
|
src={`https://flagcdn.com/w320/${user.demographicInformation?.country.toLowerCase()}.png`}
|
||||||
|
width="320"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4 mt-8 mb-20">
|
||||||
|
<span className="text-lg font-bold">Bio</span>
|
||||||
|
<textarea
|
||||||
|
className="w-full h-full min-h-[148px] cursor-text px-7 py-8 input border-2 border-mti-gray-platinum bg-white rounded-3xl"
|
||||||
|
onChange={(e) => setBio(e.target.value)}
|
||||||
|
defaultValue={bio}
|
||||||
|
placeholder="Write your text here..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
||||||
|
<Link href="/" className="max-w-[200px] self-end w-full">
|
||||||
|
<Button color="purple" variant="outline" className="max-w-[200px] self-end w-full">
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Button color="purple" className="max-w-[200px] self-end w-full" onClick={updateUser} disabled={isLoading}>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const {user, mutateUser} = useUser({redirectTo: "/login"});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@@ -171,252 +428,7 @@ export default function Home() {
|
|||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
{user && (
|
{user && <UserProfile user={user} mutateUser={mutateUser} />}
|
||||||
<Layout user={user}>
|
|
||||||
<section className="w-full flex flex-col gap-4 md:gap-8 px-4 py-8">
|
|
||||||
<h1 className="text-4xl font-bold mb-6 md:hidden">Edit Profile</h1>
|
|
||||||
<div className="flex -md:flex-col-reverse -md:items-center w-full justify-between">
|
|
||||||
<div className="flex flex-col gap-8 w-full md:w-2/3">
|
|
||||||
<h1 className="text-4xl font-bold mb-6 -md:hidden">Edit Profile</h1>
|
|
||||||
<form className="flex flex-col items-center gap-6 w-full">
|
|
||||||
<div className="flex flex-col md:flex-row gap-8 w-full">
|
|
||||||
<Input
|
|
||||||
label="Name"
|
|
||||||
type="text"
|
|
||||||
name="name"
|
|
||||||
onChange={(e) => setName(e)}
|
|
||||||
placeholder="Enter your name"
|
|
||||||
defaultValue={name}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label="E-mail Address"
|
|
||||||
type="email"
|
|
||||||
name="email"
|
|
||||||
onChange={(e) => setEmail(e)}
|
|
||||||
placeholder="Enter email address"
|
|
||||||
defaultValue={email}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col md:flex-row gap-8 w-full">
|
|
||||||
<Input
|
|
||||||
label="Old Password"
|
|
||||||
type="password"
|
|
||||||
name="password"
|
|
||||||
onChange={(e) => setPassword(e)}
|
|
||||||
placeholder="Enter your password"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label="New Password"
|
|
||||||
type="password"
|
|
||||||
name="newPassword"
|
|
||||||
onChange={(e) => setNewPassword(e)}
|
|
||||||
placeholder="Enter your new password (optional)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{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={() => null}
|
|
||||||
placeholder="Enter corporate name"
|
|
||||||
defaultValue={companyName}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label="Commercial Registration"
|
|
||||||
type="text"
|
|
||||||
name="commercialRegistration"
|
|
||||||
onChange={() => null}
|
|
||||||
placeholder="Enter commercial registration"
|
|
||||||
defaultValue={commercialRegistration}
|
|
||||||
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 value={country} onChange={setCountry} />
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
type="tel"
|
|
||||||
name="phone"
|
|
||||||
label="Phone number"
|
|
||||||
onChange={(e) => setPhone(e)}
|
|
||||||
placeholder="Enter phone number"
|
|
||||||
defaultValue={phone}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col md:flex-row gap-8 w-full">
|
|
||||||
{user.type === "corporate" && (
|
|
||||||
<Input
|
|
||||||
name="position"
|
|
||||||
onChange={setPosition}
|
|
||||||
defaultValue={position}
|
|
||||||
type="text"
|
|
||||||
label="Position"
|
|
||||||
placeholder="CEO, Head of Marketing..."
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{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={employment}
|
|
||||||
onChange={setEmployment}
|
|
||||||
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>
|
|
||||||
)}
|
|
||||||
<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={gender} onChange={setGender} 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">
|
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Expiry Date (click to purchase)</label>
|
|
||||||
<Link
|
|
||||||
href="/payment"
|
|
||||||
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",
|
|
||||||
!user.subscriptionExpirationDate
|
|
||||||
? "!bg-mti-green-ultralight !border-mti-green-light"
|
|
||||||
: expirationDateColor(user.subscriptionExpirationDate),
|
|
||||||
"bg-white border-mti-gray-platinum",
|
|
||||||
)}>
|
|
||||||
{!user.subscriptionExpirationDate && "Unlimited"}
|
|
||||||
{user.subscriptionExpirationDate && moment(user.subscriptionExpirationDate).format("DD/MM/YYYY")}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-6 w-48">
|
|
||||||
<div
|
|
||||||
className="flex flex-col gap-3 items-center h-fit cursor-pointer group"
|
|
||||||
onClick={() => (profilePictureInput.current as any)?.click()}>
|
|
||||||
<div className="relative overflow-hidden h-48 w-48 rounded-full">
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
"absolute top-0 left-0 bg-mti-purple-light/60 w-full h-full z-20 flex items-center justify-center opacity-0 group-hover:opacity-100",
|
|
||||||
"transition ease-in-out duration-300",
|
|
||||||
)}>
|
|
||||||
<BsCamera className="text-6xl text-mti-purple-ultralight/80" />
|
|
||||||
</div>
|
|
||||||
<img src={profilePicture} alt={user.name} className="aspect-square drop-shadow-xl self-end object-cover" />
|
|
||||||
</div>
|
|
||||||
<input type="file" className="hidden" onChange={uploadProfilePicture} accept="image/*" ref={profilePictureInput} />
|
|
||||||
<span
|
|
||||||
onClick={() => (profilePictureInput.current as any)?.click()}
|
|
||||||
className="cursor-pointer text-mti-purple-light text-sm">
|
|
||||||
Change picture
|
|
||||||
</span>
|
|
||||||
<h6 className="font-normal text-base text-mti-gray-taupe">{USER_TYPE_LABELS[user.type]}</h6>
|
|
||||||
</div>
|
|
||||||
{user.type === 'agent' && (
|
|
||||||
<div className="flag items-center h-fit">
|
|
||||||
<img
|
|
||||||
alt={user.demographicInformation?.country.toLowerCase() + '_flag'}
|
|
||||||
src={`https://flagcdn.com/w320/${user.demographicInformation?.country.toLowerCase()}.png`}
|
|
||||||
width="320"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4 mt-8 mb-20">
|
|
||||||
<span className="text-lg font-bold">Bio</span>
|
|
||||||
<textarea
|
|
||||||
className="w-full h-full min-h-[148px] cursor-text px-7 py-8 input border-2 border-mti-gray-platinum bg-white rounded-3xl"
|
|
||||||
onChange={(e) => setBio(e.target.value)}
|
|
||||||
defaultValue={bio}
|
|
||||||
placeholder="Write your text here..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
|
||||||
<Link href="/" className="max-w-[200px] self-end w-full">
|
|
||||||
<Button color="purple" variant="outline" className="max-w-[200px] self-end w-full">
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<Button color="purple" className="max-w-[200px] self-end w-full" onClick={updateUser} disabled={isLoading}>
|
|
||||||
Save Changes
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Layout>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import {ToastContainer} from "react-toastify";
|
import {ToastContainer} from "react-toastify";
|
||||||
import {useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import useUser from "@/hooks/useUser";
|
import useUser from "@/hooks/useUser";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -11,6 +11,7 @@ import RegisterCorporate from "./(register)/RegisterCorporate";
|
|||||||
import EmailVerification from "./(auth)/EmailVerification";
|
import EmailVerification from "./(auth)/EmailVerification";
|
||||||
import {sendEmailVerification} from "@/utils/email";
|
import {sendEmailVerification} from "@/utils/email";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
export const getServerSideProps = (context: any) => {
|
export const getServerSideProps = (context: any) => {
|
||||||
const {code} = context.query;
|
const {code} = context.query;
|
||||||
@@ -21,8 +22,17 @@ export const getServerSideProps = (context: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function Register({code: queryCode}: {code: string}) {
|
export default function Register({code: queryCode}: {code: string}) {
|
||||||
|
const [defaultEmail, setDefaultEmail] = useState<string>();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (queryCode) {
|
||||||
|
(async () => {
|
||||||
|
axios.get<{email?: string}>(`/api/code/${queryCode}`).then((result) => setDefaultEmail(result.data.email));
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}, [queryCode]);
|
||||||
|
|
||||||
const {user, mutateUser} = useUser({
|
const {user, mutateUser} = useUser({
|
||||||
redirectTo: "/",
|
redirectTo: "/",
|
||||||
redirectIfFound: true,
|
redirectIfFound: true,
|
||||||
@@ -79,11 +89,13 @@ export default function Register({code: queryCode}: {code: string}) {
|
|||||||
<Tab.Panels>
|
<Tab.Panels>
|
||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
<RegisterIndividual
|
<RegisterIndividual
|
||||||
|
key={defaultEmail || "individual"}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
setIsLoading={setIsLoading}
|
setIsLoading={setIsLoading}
|
||||||
mutateUser={mutateUser}
|
mutateUser={mutateUser}
|
||||||
sendEmailVerification={sendEmailVerification}
|
sendEmailVerification={sendEmailVerification}
|
||||||
queryCode={queryCode}
|
queryCode={queryCode}
|
||||||
|
defaultEmail={defaultEmail}
|
||||||
/>
|
/>
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {Type} from "@/interfaces/user";
|
import {Type, User, CorporateUser} 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",
|
||||||
@@ -8,3 +8,7 @@ export const USER_TYPE_LABELS: {[key in Type]: string} = {
|
|||||||
admin: "Admin",
|
admin: "Admin",
|
||||||
developer: "Developer",
|
developer: "Developer",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function isCorporateUser(user: User): user is CorporateUser {
|
||||||
|
return (user as CorporateUser).corporateInformation !== undefined;
|
||||||
|
}
|
||||||
@@ -11,17 +11,19 @@ import {
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {speakingReverseMarking, writingReverseMarking} from "./score";
|
import {speakingReverseMarking, writingReverseMarking} from "./score";
|
||||||
|
|
||||||
export const evaluateWritingAnswer = async (exercise: WritingExercise, solution: UserSolution) => {
|
export const evaluateWritingAnswer = async (exercise: WritingExercise, solution: UserSolution, id: string): Promise<object | undefined> => {
|
||||||
const response = await axios.post<Evaluation>("/api/evaluate/writing", {
|
const response = await axios.post<Evaluation>("/api/evaluate/writing", {
|
||||||
question: `${exercise.prompt} ${exercise.attachment ? exercise.attachment.description : ""}`.replaceAll("\n", ""),
|
question: `${exercise.prompt} ${exercise.attachment ? exercise.attachment.description : ""}`.replaceAll("\n", ""),
|
||||||
answer: solution.solutions[0].solution.trim().replaceAll("\n", " "),
|
answer: solution.solutions[0].solution.trim().replaceAll("\n", " "),
|
||||||
|
id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
return {
|
return {
|
||||||
...solution,
|
...solution,
|
||||||
|
id,
|
||||||
score: {
|
score: {
|
||||||
correct: writingReverseMarking[response.data.overall] || 0,
|
correct: response.data ? writingReverseMarking[response.data.overall] : 0,
|
||||||
missing: 0,
|
missing: 0,
|
||||||
total: 100,
|
total: 100,
|
||||||
},
|
},
|
||||||
@@ -32,12 +34,12 @@ export const evaluateWritingAnswer = async (exercise: WritingExercise, solution:
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const evaluateSpeakingAnswer = async (exercise: SpeakingExercise | InteractiveSpeakingExercise, solution: UserSolution) => {
|
export const evaluateSpeakingAnswer = async (exercise: SpeakingExercise | InteractiveSpeakingExercise, solution: UserSolution, id: string) => {
|
||||||
switch (exercise?.type) {
|
switch (exercise?.type) {
|
||||||
case "speaking":
|
case "speaking":
|
||||||
return await evaluateSpeakingExercise(exercise, exercise.id, solution);
|
return {...(await evaluateSpeakingExercise(exercise, exercise.id, solution, id)), id};
|
||||||
case "interactiveSpeaking":
|
case "interactiveSpeaking":
|
||||||
return await evaluateInteractiveSpeakingExercise(exercise.id, solution);
|
return {...(await evaluateInteractiveSpeakingExercise(exercise.id, solution, id)), id};
|
||||||
default:
|
default:
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -48,7 +50,7 @@ const downloadBlob = async (url: string): Promise<Buffer> => {
|
|||||||
return Buffer.from(blobResponse.data, "binary");
|
return Buffer.from(blobResponse.data, "binary");
|
||||||
};
|
};
|
||||||
|
|
||||||
const evaluateSpeakingExercise = async (exercise: SpeakingExercise, exerciseId: string, solution: UserSolution) => {
|
const evaluateSpeakingExercise = async (exercise: SpeakingExercise, exerciseId: string, solution: UserSolution, id: string) => {
|
||||||
const audioBlob = await downloadBlob(solution.solutions[0].solution.trim());
|
const audioBlob = await downloadBlob(solution.solutions[0].solution.trim());
|
||||||
const audioFile = new File([audioBlob], "audio.wav", {type: "audio/wav"});
|
const audioFile = new File([audioBlob], "audio.wav", {type: "audio/wav"});
|
||||||
|
|
||||||
@@ -58,6 +60,7 @@ const evaluateSpeakingExercise = async (exercise: SpeakingExercise, exerciseId:
|
|||||||
const evaluationQuestion =
|
const evaluationQuestion =
|
||||||
`${exercise.text.replaceAll("\n", "")}` + (exercise.prompts.length > 0 ? `You should talk about: ${exercise.prompts.join(", ")}` : "");
|
`${exercise.text.replaceAll("\n", "")}` + (exercise.prompts.length > 0 ? `You should talk about: ${exercise.prompts.join(", ")}` : "");
|
||||||
formData.append("question", evaluationQuestion);
|
formData.append("question", evaluationQuestion);
|
||||||
|
formData.append("id", id);
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -71,18 +74,18 @@ const evaluateSpeakingExercise = async (exercise: SpeakingExercise, exerciseId:
|
|||||||
return {
|
return {
|
||||||
...solution,
|
...solution,
|
||||||
score: {
|
score: {
|
||||||
correct: speakingReverseMarking[response.data.overall] || 0,
|
correct: response.data ? speakingReverseMarking[response.data.overall] : 0,
|
||||||
missing: 0,
|
missing: 0,
|
||||||
total: 100,
|
total: 100,
|
||||||
},
|
},
|
||||||
solutions: [{id: exerciseId, solution: response.data.fullPath, evaluation: response.data}],
|
solutions: [{id: exerciseId, solution: response.data ? response.data.fullPath : null, evaluation: response.data}],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const evaluateInteractiveSpeakingExercise = async (exerciseId: string, solution: UserSolution) => {
|
const evaluateInteractiveSpeakingExercise = async (exerciseId: string, solution: UserSolution, id: string) => {
|
||||||
const promiseParts = solution.solutions.map(async (x: {prompt: string; blob: string}) => ({
|
const promiseParts = solution.solutions.map(async (x: {prompt: string; blob: string}) => ({
|
||||||
question: x.prompt,
|
question: x.prompt,
|
||||||
answer: await downloadBlob(x.blob),
|
answer: await downloadBlob(x.blob),
|
||||||
@@ -98,6 +101,7 @@ const evaluateInteractiveSpeakingExercise = async (exerciseId: string, solution:
|
|||||||
formData.append(`question_${seed}`, question);
|
formData.append(`question_${seed}`, question);
|
||||||
formData.append(`answer_${seed}`, audioFile, `${seed}.wav`);
|
formData.append(`answer_${seed}`, audioFile, `${seed}.wav`);
|
||||||
});
|
});
|
||||||
|
formData.append("id", id);
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -111,11 +115,11 @@ const evaluateInteractiveSpeakingExercise = async (exerciseId: string, solution:
|
|||||||
return {
|
return {
|
||||||
...solution,
|
...solution,
|
||||||
score: {
|
score: {
|
||||||
correct: speakingReverseMarking[response.data.overall] || 0,
|
correct: response.data ? speakingReverseMarking[response.data.overall] : 0,
|
||||||
missing: 0,
|
missing: 0,
|
||||||
total: 100,
|
total: 100,
|
||||||
},
|
},
|
||||||
solutions: [{id: exerciseId, solution: response.data.answer, evaluation: response.data}],
|
solutions: [{id: exerciseId, solution: response.data ? response.data.answer : null, evaluation: response.data}],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user