Merge branch 'develop'
This commit is contained in:
@@ -5,22 +5,18 @@ import { Assignment } from "@/interfaces/results";
|
|||||||
import {calculateBandScore} from "@/utils/score";
|
import {calculateBandScore} from "@/utils/score";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import {
|
import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs";
|
||||||
BsBook,
|
|
||||||
BsClipboard,
|
|
||||||
BsHeadphones,
|
|
||||||
BsMegaphone,
|
|
||||||
BsPen,
|
|
||||||
} from "react-icons/bs";
|
|
||||||
import {usePDFDownload} from "@/hooks/usePDFDownload";
|
import {usePDFDownload} from "@/hooks/usePDFDownload";
|
||||||
import {useAssignmentArchive} from "@/hooks/useAssignmentArchive";
|
import {useAssignmentArchive} from "@/hooks/useAssignmentArchive";
|
||||||
import {uniqBy} from "lodash";
|
import {uniqBy} from "lodash";
|
||||||
|
import {useAssignmentUnarchive} from "@/hooks/useAssignmentUnarchive";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
allowDownload?: boolean;
|
allowDownload?: boolean;
|
||||||
reload?: Function;
|
reload?: Function;
|
||||||
allowArchive?: boolean;
|
allowArchive?: boolean;
|
||||||
|
allowUnarchive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AssignmentCard({
|
export default function AssignmentCard({
|
||||||
@@ -37,45 +33,35 @@ export default function AssignmentCard({
|
|||||||
allowDownload,
|
allowDownload,
|
||||||
reload,
|
reload,
|
||||||
allowArchive,
|
allowArchive,
|
||||||
|
allowUnarchive,
|
||||||
}: Assignment & Props) {
|
}: Assignment & Props) {
|
||||||
const renderPdfIcon = usePDFDownload("assignments");
|
const renderPdfIcon = usePDFDownload("assignments");
|
||||||
const renderArchiveIcon = useAssignmentArchive(id, reload);
|
const renderArchiveIcon = useAssignmentArchive(id, reload);
|
||||||
|
const renderUnarchiveIcon = useAssignmentUnarchive(id, reload);
|
||||||
|
|
||||||
const calculateAverageModuleScore = (module: Module) => {
|
const calculateAverageModuleScore = (module: Module) => {
|
||||||
const resultModuleBandScores = results.map((r) => {
|
const resultModuleBandScores = results.map((r) => {
|
||||||
const moduleStats = r.stats.filter((s) => s.module === module);
|
const moduleStats = r.stats.filter((s) => s.module === module);
|
||||||
|
|
||||||
const correct = moduleStats.reduce(
|
const correct = moduleStats.reduce((acc, curr) => acc + curr.score.correct, 0);
|
||||||
(acc, curr) => acc + curr.score.correct,
|
const total = moduleStats.reduce((acc, curr) => acc + curr.score.total, 0);
|
||||||
0
|
|
||||||
);
|
|
||||||
const total = moduleStats.reduce(
|
|
||||||
(acc, curr) => acc + curr.score.total,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
return calculateBandScore(correct, total, module, r.type);
|
return calculateBandScore(correct, total, module, r.type);
|
||||||
});
|
});
|
||||||
|
|
||||||
return resultModuleBandScores.length === 0
|
return resultModuleBandScores.length === 0 ? -1 : resultModuleBandScores.reduce((acc, curr) => acc + curr, 0) / results.length;
|
||||||
? -1
|
|
||||||
: resultModuleBandScores.reduce((acc, curr) => acc + curr, 0) /
|
|
||||||
results.length;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className="border-mti-gray-platinum flex h-fit w-[350px] cursor-pointer flex-col gap-6 rounded-xl border bg-white p-4 transition duration-300 ease-in-out hover:drop-shadow"
|
className="border-mti-gray-platinum flex h-fit w-[350px] cursor-pointer flex-col gap-6 rounded-xl border bg-white p-4 transition duration-300 ease-in-out hover:drop-shadow">
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex flex-row justify-between">
|
<div className="flex flex-row justify-between">
|
||||||
<h3 className="text-xl font-semibold">{name}</h3>
|
<h3 className="text-xl font-semibold">{name}</h3>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{allowDownload &&
|
{allowDownload && renderPdfIcon(id, "text-mti-gray-dim", "text-mti-gray-dim")}
|
||||||
renderPdfIcon(id, "text-mti-gray-dim", "text-mti-gray-dim")}
|
{allowArchive && !archived && renderArchiveIcon("text-mti-gray-dim", "text-mti-gray-dim")}
|
||||||
{allowArchive &&
|
{allowUnarchive && archived && renderUnarchiveIcon("text-mti-gray-dim", "text-mti-gray-dim")}
|
||||||
!archived &&
|
|
||||||
renderArchiveIcon("text-mti-gray-dim", "text-mti-gray-dim")}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
@@ -83,11 +69,7 @@ export default function AssignmentCard({
|
|||||||
percentage={(results.length / assignees.length) * 100}
|
percentage={(results.length / assignees.length) * 100}
|
||||||
label={`${results.length}/${assignees.length}`}
|
label={`${results.length}/${assignees.length}`}
|
||||||
className="h-5"
|
className="h-5"
|
||||||
textClassName={
|
textClassName={results.length / assignees.length < 0.5 ? "!text-mti-gray-dim font-light" : "text-white"}
|
||||||
results.length / assignees.length < 0.5
|
|
||||||
? "!text-mti-gray-dim font-light"
|
|
||||||
: "text-white"
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="flex justify-between gap-1">
|
<span className="flex justify-between gap-1">
|
||||||
@@ -105,18 +87,15 @@ export default function AssignmentCard({
|
|||||||
module === "listening" && "bg-ielts-listening",
|
module === "listening" && "bg-ielts-listening",
|
||||||
module === "writing" && "bg-ielts-writing",
|
module === "writing" && "bg-ielts-writing",
|
||||||
module === "speaking" && "bg-ielts-speaking",
|
module === "speaking" && "bg-ielts-speaking",
|
||||||
module === "level" && "bg-ielts-level"
|
module === "level" && "bg-ielts-level",
|
||||||
)}
|
)}>
|
||||||
>
|
|
||||||
{module === "reading" && <BsBook className="h-4 w-4" />}
|
{module === "reading" && <BsBook className="h-4 w-4" />}
|
||||||
{module === "listening" && <BsHeadphones className="h-4 w-4" />}
|
{module === "listening" && <BsHeadphones className="h-4 w-4" />}
|
||||||
{module === "writing" && <BsPen className="h-4 w-4" />}
|
{module === "writing" && <BsPen className="h-4 w-4" />}
|
||||||
{module === "speaking" && <BsMegaphone className="h-4 w-4" />}
|
{module === "speaking" && <BsMegaphone className="h-4 w-4" />}
|
||||||
{module === "level" && <BsClipboard className="h-4 w-4" />}
|
{module === "level" && <BsClipboard className="h-4 w-4" />}
|
||||||
{calculateAverageModuleScore(module) > -1 && (
|
{calculateAverageModuleScore(module) > -1 && (
|
||||||
<span className="text-sm">
|
<span className="text-sm">{calculateAverageModuleScore(module).toFixed(1)}</span>
|
||||||
{calculateAverageModuleScore(module).toFixed(1)}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -151,8 +151,10 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AssignmentsPage = () => {
|
const AssignmentsPage = () => {
|
||||||
const activeFilter = (a: Assignment) => moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment()) && a.assignees.length > a.results.length;
|
const activeFilter = (a: Assignment) =>
|
||||||
|
moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment()) && a.assignees.length > a.results.length;
|
||||||
const pastFilter = (a: Assignment) => (moment(a.endDate).isBefore(moment()) || a.assignees.length === a.results.length) && !a.archived;
|
const pastFilter = (a: Assignment) => (moment(a.endDate).isBefore(moment()) || a.assignees.length === a.results.length) && !a.archived;
|
||||||
|
const archivedFilter = (a: Assignment) => a.archived;
|
||||||
const futureFilter = (a: Assignment) => moment(a.startDate).isAfter(moment());
|
const futureFilter = (a: Assignment) => moment(a.startDate).isAfter(moment());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -234,7 +236,29 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
<h2 className="text-2xl font-semibold">Past Assignments ({assignments.filter(pastFilter).length})</h2>
|
<h2 className="text-2xl font-semibold">Past Assignments ({assignments.filter(pastFilter).length})</h2>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{assignments.filter(pastFilter).map((a) => (
|
{assignments.filter(pastFilter).map((a) => (
|
||||||
<AssignmentCard {...a} onClick={() => setSelectedAssignment(a)} key={a.id} allowDownload reload={reloadAssignments} allowArchive/>
|
<AssignmentCard
|
||||||
|
{...a}
|
||||||
|
onClick={() => setSelectedAssignment(a)}
|
||||||
|
key={a.id}
|
||||||
|
allowDownload
|
||||||
|
reload={reloadAssignments}
|
||||||
|
allowArchive
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section className="flex flex-col gap-4">
|
||||||
|
<h2 className="text-2xl font-semibold">Archived Assignments ({assignments.filter(archivedFilter).length})</h2>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{assignments.filter(archivedFilter).map((a) => (
|
||||||
|
<AssignmentCard
|
||||||
|
{...a}
|
||||||
|
onClick={() => setSelectedAssignment(a)}
|
||||||
|
key={a.id}
|
||||||
|
allowDownload
|
||||||
|
reload={reloadAssignments}
|
||||||
|
allowUnarchive
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -3,10 +3,7 @@ import axios from "axios";
|
|||||||
import {toast} from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
import {BsArchive} from "react-icons/bs";
|
import {BsArchive} from "react-icons/bs";
|
||||||
|
|
||||||
export const useAssignmentArchive = (
|
export const useAssignmentArchive = (assignmentId: string, reload?: Function) => {
|
||||||
assignmentId: string,
|
|
||||||
reload?: Function
|
|
||||||
) => {
|
|
||||||
const [loading, setLoading] = React.useState(false);
|
const [loading, setLoading] = React.useState(false);
|
||||||
const archive = () => {
|
const archive = () => {
|
||||||
// archive assignment
|
// archive assignment
|
||||||
@@ -26,18 +23,18 @@ export const useAssignmentArchive = (
|
|||||||
|
|
||||||
const renderIcon = (downloadClasses: string, loadingClasses: string) => {
|
const renderIcon = (downloadClasses: string, loadingClasses: string) => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return <span className={`${loadingClasses} loading loading-infinity w-6`} />;
|
||||||
<span className={`${loadingClasses} loading loading-infinity w-6`} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<BsArchive
|
<div
|
||||||
className={`${downloadClasses} text-2xl cursor-pointer`}
|
className="tooltip flex items-center justify-center w-fit h-fit"
|
||||||
|
data-tip="Archive assignment"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
archive();
|
archive();
|
||||||
}}
|
}}>
|
||||||
/>
|
<BsArchive className={`${downloadClasses} text-2xl cursor-pointer tooltip`} />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
42
src/hooks/useAssignmentUnarchive.tsx
Normal file
42
src/hooks/useAssignmentUnarchive.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import React from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
import {toast} from "react-toastify";
|
||||||
|
import {BsArchive, BsFileEarmarkCheck, BsFileEarmarkCheckFill} from "react-icons/bs";
|
||||||
|
|
||||||
|
export const useAssignmentUnarchive = (assignmentId: string, reload?: Function) => {
|
||||||
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
const archive = () => {
|
||||||
|
// archive assignment
|
||||||
|
setLoading(true);
|
||||||
|
axios
|
||||||
|
.post(`/api/assignments/${assignmentId}/unarchive`)
|
||||||
|
.then((res) => {
|
||||||
|
toast.success("Assignment unarchived!");
|
||||||
|
if (reload) reload();
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error("Failed to unarchive the assignment!");
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderIcon = (downloadClasses: string, loadingClasses: string) => {
|
||||||
|
if (loading) {
|
||||||
|
return <span className={`${loadingClasses} loading loading-infinity w-6`} />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="tooltip flex items-center justify-center w-fit h-fit"
|
||||||
|
data-tip="Unarchive assignment"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
archive();
|
||||||
|
}}>
|
||||||
|
<BsFileEarmarkCheck className={`${downloadClasses} text-2xl cursor-pointer tooltip`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return renderIcon;
|
||||||
|
};
|
||||||
@@ -6,7 +6,12 @@ import useUser from "@/hooks/useUser";
|
|||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import { Code, User } from "@/interfaces/user";
|
import { Code, User } from "@/interfaces/user";
|
||||||
import { USER_TYPE_LABELS } from "@/resources/user";
|
import { USER_TYPE_LABELS } from "@/resources/user";
|
||||||
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";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@@ -24,7 +29,9 @@ const CreatorCell = ({id, users}: {id: string; users: User[]}) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{(creatorUser?.type === "corporate" ? creatorUser?.corporateInformation?.companyInformation?.name : creatorUser?.name || "N/A") || "N/A"}{" "}
|
{(creatorUser?.type === "corporate"
|
||||||
|
? creatorUser?.corporateInformation?.companyInformation?.name
|
||||||
|
: creatorUser?.name || "N/A") || "N/A"}{" "}
|
||||||
{creatorUser && `(${USER_TYPE_LABELS[creatorUser.type]})`}
|
{creatorUser && `(${USER_TYPE_LABELS[creatorUser.type]})`}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -33,41 +40,62 @@ const CreatorCell = ({id, users}: {id: string; users: User[]}) => {
|
|||||||
export default function CodeList({ user }: { user: User }) {
|
export default function CodeList({ user }: { user: User }) {
|
||||||
const [selectedCodes, setSelectedCodes] = useState<string[]>([]);
|
const [selectedCodes, setSelectedCodes] = useState<string[]>([]);
|
||||||
|
|
||||||
const [filteredCorporate, setFilteredCorporate] = useState<User | undefined>(user?.type === "corporate" ? user : undefined);
|
const [filteredCorporate, setFilteredCorporate] = useState<User | undefined>(
|
||||||
const [filterAvailability, setFilterAvailability] = useState<"in-use" | "unused">();
|
user?.type === "corporate" ? user : undefined,
|
||||||
|
);
|
||||||
|
const [filterAvailability, setFilterAvailability] = useState<
|
||||||
|
"in-use" | "unused"
|
||||||
|
>();
|
||||||
|
|
||||||
const [filteredCodes, setFilteredCodes] = useState<Code[]>([]);
|
const [filteredCodes, setFilteredCodes] = useState<Code[]>([]);
|
||||||
|
|
||||||
const { users } = useUsers();
|
const { users } = useUsers();
|
||||||
const {codes, reload} = useCodes(user?.type === "corporate" ? user?.id : undefined);
|
const { codes, reload } = useCodes(
|
||||||
|
user?.type === "corporate" ? user?.id : undefined,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let result = [...codes];
|
let result = [...codes];
|
||||||
if (filteredCorporate) result = result.filter((x) => x.creator === filteredCorporate.id);
|
if (filteredCorporate)
|
||||||
if (filterAvailability) result = result.filter((x) => (filterAvailability === "in-use" ? !!x.userId : !x.userId));
|
result = result.filter((x) => x.creator === filteredCorporate.id);
|
||||||
|
if (filterAvailability)
|
||||||
|
result = result.filter((x) =>
|
||||||
|
filterAvailability === "in-use" ? !!x.userId : !x.userId,
|
||||||
|
);
|
||||||
|
|
||||||
setFilteredCodes(result);
|
setFilteredCodes(result);
|
||||||
}, [codes, filteredCorporate, filterAvailability]);
|
}, [codes, filteredCorporate, filterAvailability]);
|
||||||
|
|
||||||
const toggleCode = (id: string) => {
|
const toggleCode = (id: string) => {
|
||||||
setSelectedCodes((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));
|
setSelectedCodes((prev) =>
|
||||||
|
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id],
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleAllCodes = (checked: boolean) => {
|
const toggleAllCodes = (checked: boolean) => {
|
||||||
if (checked) return setSelectedCodes(filteredCodes.filter((x) => !x.userId).map((x) => x.code));
|
if (checked)
|
||||||
|
return setSelectedCodes(
|
||||||
|
filteredCodes.filter((x) => !x.userId).map((x) => x.code),
|
||||||
|
);
|
||||||
|
|
||||||
return setSelectedCodes([]);
|
return setSelectedCodes([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteCodes = async (codes: string[]) => {
|
const deleteCodes = async (codes: string[]) => {
|
||||||
if (!confirm(`Are you sure you want to delete these ${codes.length} code(s)?`)) return;
|
if (
|
||||||
|
!confirm(`Are you sure you want to delete these ${codes.length} code(s)?`)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
codes.forEach((code) => params.append("code", code));
|
codes.forEach((code) => params.append("code", code));
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.delete(`/api/code?${params.toString()}`)
|
.delete(`/api/code?${params.toString()}`)
|
||||||
.then(() => toast.success(`Deleted the codes!`))
|
.then(() => {
|
||||||
|
toast.success(`Deleted the codes!`);
|
||||||
|
setSelectedCodes([]);
|
||||||
|
})
|
||||||
.catch((reason) => {
|
.catch((reason) => {
|
||||||
if (reason.response.status === 404) {
|
if (reason.response.status === 404) {
|
||||||
toast.error("Code not found!");
|
toast.error("Code not found!");
|
||||||
@@ -85,7 +113,8 @@ export default function CodeList({user}: {user: User}) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteCode = async (code: Code) => {
|
const deleteCode = async (code: Code) => {
|
||||||
if (!confirm(`Are you sure you want to delete this "${code.code}" code?`)) return;
|
if (!confirm(`Are you sure you want to delete this "${code.code}" code?`))
|
||||||
|
return;
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.delete(`/api/code/${code.code}`)
|
.delete(`/api/code/${code.code}`)
|
||||||
@@ -113,15 +142,21 @@ export default function CodeList({user}: {user: User}) {
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
disabled={filteredCodes.filter((x) => !x.userId).length === 0}
|
disabled={filteredCodes.filter((x) => !x.userId).length === 0}
|
||||||
isChecked={
|
isChecked={
|
||||||
selectedCodes.length === filteredCodes.filter((x) => !x.userId).length && filteredCodes.filter((x) => !x.userId).length > 0
|
selectedCodes.length ===
|
||||||
|
filteredCodes.filter((x) => !x.userId).length &&
|
||||||
|
filteredCodes.filter((x) => !x.userId).length > 0
|
||||||
}
|
}
|
||||||
onChange={(checked) => toggleAllCodes(checked)}>
|
onChange={(checked) => toggleAllCodes(checked)}
|
||||||
|
>
|
||||||
{""}
|
{""}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
),
|
),
|
||||||
cell: (info) =>
|
cell: (info) =>
|
||||||
!info.row.original.userId ? (
|
!info.row.original.userId ? (
|
||||||
<Checkbox isChecked={selectedCodes.includes(info.getValue())} onChange={() => toggleCode(info.getValue())}>
|
<Checkbox
|
||||||
|
isChecked={selectedCodes.includes(info.getValue())}
|
||||||
|
onChange={() => toggleCode(info.getValue())}
|
||||||
|
>
|
||||||
{""}
|
{""}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
) : null,
|
) : null,
|
||||||
@@ -132,7 +167,8 @@ export default function CodeList({user}: {user: User}) {
|
|||||||
}),
|
}),
|
||||||
columnHelper.accessor("creationDate", {
|
columnHelper.accessor("creationDate", {
|
||||||
header: "Creation Date",
|
header: "Creation Date",
|
||||||
cell: (info) => (info.getValue() ? moment(info.getValue()).format("DD/MM/YYYY") : "N/A"),
|
cell: (info) =>
|
||||||
|
info.getValue() ? moment(info.getValue()).format("DD/MM/YYYY") : "N/A",
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("email", {
|
columnHelper.accessor("email", {
|
||||||
header: "Invited E-mail",
|
header: "Invited E-mail",
|
||||||
@@ -162,7 +198,11 @@ export default function CodeList({user}: {user: User}) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
{!row.original.userId && (
|
{!row.original.userId && (
|
||||||
<div data-tip="Delete" className="cursor-pointer tooltip" onClick={() => deleteCode(row.original)}>
|
<div
|
||||||
|
data-tip="Delete"
|
||||||
|
className="cursor-pointer tooltip"
|
||||||
|
onClick={() => deleteCode(row.original)}
|
||||||
|
>
|
||||||
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -192,7 +232,8 @@ export default function CodeList({user}: {user: User}) {
|
|||||||
? {
|
? {
|
||||||
label: `${
|
label: `${
|
||||||
filteredCorporate.type === "corporate"
|
filteredCorporate.type === "corporate"
|
||||||
? filteredCorporate.corporateInformation?.companyInformation?.name || filteredCorporate.name
|
? filteredCorporate.corporateInformation
|
||||||
|
?.companyInformation?.name || filteredCorporate.name
|
||||||
: filteredCorporate.name
|
: filteredCorporate.name
|
||||||
} (${USER_TYPE_LABELS[filteredCorporate.type]})`,
|
} (${USER_TYPE_LABELS[filteredCorporate.type]})`,
|
||||||
value: filteredCorporate.id,
|
value: filteredCorporate.id,
|
||||||
@@ -200,7 +241,9 @@ export default function CodeList({user}: {user: User}) {
|
|||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
options={users
|
options={users
|
||||||
.filter((x) => ["admin", "developer", "corporate"].includes(x.type))
|
.filter((x) =>
|
||||||
|
["admin", "developer", "corporate"].includes(x.type),
|
||||||
|
)
|
||||||
.map((x) => ({
|
.map((x) => ({
|
||||||
label: `${x.type === "corporate" ? x.corporateInformation?.companyInformation?.name || x.name : x.name} (${
|
label: `${x.type === "corporate" ? x.corporateInformation?.companyInformation?.name || x.name : x.name} (${
|
||||||
USER_TYPE_LABELS[x.type]
|
USER_TYPE_LABELS[x.type]
|
||||||
@@ -208,7 +251,11 @@ export default function CodeList({user}: {user: User}) {
|
|||||||
value: x.id,
|
value: x.id,
|
||||||
user: x,
|
user: x,
|
||||||
}))}
|
}))}
|
||||||
onChange={(value) => setFilteredCorporate(value ? users.find((x) => x.id === value?.value) : undefined)}
|
onChange={(value) =>
|
||||||
|
setFilteredCorporate(
|
||||||
|
value ? users.find((x) => x.id === value?.value) : undefined,
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
className="!w-96 !py-1"
|
className="!w-96 !py-1"
|
||||||
@@ -218,7 +265,11 @@ export default function CodeList({user}: {user: User}) {
|
|||||||
{ label: "In Use", value: "in-use" },
|
{ label: "In Use", value: "in-use" },
|
||||||
{ label: "Unused", value: "unused" },
|
{ label: "Unused", value: "unused" },
|
||||||
]}
|
]}
|
||||||
onChange={(value) => setFilterAvailability(value ? (value.value as typeof filterAvailability) : undefined)}
|
onChange={(value) =>
|
||||||
|
setFilterAvailability(
|
||||||
|
value ? (value.value as typeof filterAvailability) : undefined,
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
@@ -228,7 +279,8 @@ export default function CodeList({user}: {user: User}) {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
color="red"
|
color="red"
|
||||||
className="!py-1 px-10"
|
className="!py-1 px-10"
|
||||||
onClick={() => deleteCodes(selectedCodes)}>
|
onClick={() => deleteCodes(selectedCodes)}
|
||||||
|
>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -239,7 +291,12 @@ export default function CodeList({user}: {user: User}) {
|
|||||||
<tr key={headerGroup.id}>
|
<tr key={headerGroup.id}>
|
||||||
{headerGroup.headers.map((header) => (
|
{headerGroup.headers.map((header) => (
|
||||||
<th className="p-4 text-left" key={header.id}>
|
<th className="p-4 text-left" key={header.id}>
|
||||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext(),
|
||||||
|
)}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -247,7 +304,10 @@ export default function CodeList({user}: {user: User}) {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody className="px-2">
|
<tbody className="px-2">
|
||||||
{table.getRowModel().rows.map((row) => (
|
{table.getRowModel().rows.map((row) => (
|
||||||
<tr className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2" key={row.id}>
|
<tr
|
||||||
|
className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2"
|
||||||
|
key={row.id}
|
||||||
|
>
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
<td className="px-4 py-2" key={cell.id}>
|
<td className="px-4 py-2" key={cell.id}>
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
|||||||
33
src/pages/api/assignments/[id]/unarchive.tsx
Normal file
33
src/pages/api/assignments/[id]/unarchive.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import type {NextApiRequest, NextApiResponse} from "next";
|
||||||
|
import {app} from "@/firebase";
|
||||||
|
import {getFirestore, doc, getDoc, setDoc} 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 post(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
// verify if it's a logged user that is trying to archive
|
||||||
|
if (req.session.user) {
|
||||||
|
const {id} = req.query as {id: string};
|
||||||
|
const docSnap = await getDoc(doc(db, "assignments", id));
|
||||||
|
|
||||||
|
if (!docSnap.exists()) {
|
||||||
|
res.status(404).json({ok: false});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await setDoc(docSnap.ref, {archived: false}, {merge: true});
|
||||||
|
res.status(200).json({ok: true});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(401).json({ok: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
if (req.method === "POST") return post(req, res);
|
||||||
|
res.status(404).json({ok: false});
|
||||||
|
}
|
||||||
@@ -1,10 +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 { app } from "@/firebase";
|
import { app } from "@/firebase";
|
||||||
import {getFirestore, setDoc, doc, query, collection, where, getDocs, getDoc, deleteDoc} from "firebase/firestore";
|
import {
|
||||||
|
getFirestore,
|
||||||
|
setDoc,
|
||||||
|
doc,
|
||||||
|
query,
|
||||||
|
collection,
|
||||||
|
where,
|
||||||
|
getDocs,
|
||||||
|
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 {Type} from "@/interfaces/user";
|
import { Code, Type } from "@/interfaces/user";
|
||||||
import { PERMISSIONS } from "@/constants/userPermissions";
|
import { PERMISSIONS } from "@/constants/userPermissions";
|
||||||
import { uuidv4 } from "@firebase/util";
|
import { uuidv4 } from "@firebase/util";
|
||||||
import { prepareMailer, prepareMailOptions } from "@/email";
|
import { prepareMailer, prepareMailOptions } from "@/email";
|
||||||
@@ -23,12 +33,17 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
|
|
||||||
async function get(req: NextApiRequest, res: NextApiResponse) {
|
async function get(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (!req.session.user) {
|
if (!req.session.user) {
|
||||||
res.status(401).json({ok: false, reason: "You must be logged in to generate a code!"});
|
res
|
||||||
|
.status(401)
|
||||||
|
.json({ ok: false, reason: "You must be logged in to generate a code!" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { creator } = req.query as { creator?: string };
|
const { creator } = req.query as { creator?: string };
|
||||||
const q = query(collection(db, "codes"), where("creator", "==", creator || ""));
|
const q = query(
|
||||||
|
collection(db, "codes"),
|
||||||
|
where("creator", "==", creator || ""),
|
||||||
|
);
|
||||||
const snapshot = await getDocs(creator ? q : collection(db, "codes"));
|
const snapshot = await getDocs(creator ? q : collection(db, "codes"));
|
||||||
|
|
||||||
res.status(200).json(snapshot.docs.map((doc) => doc.data()));
|
res.status(200).json(snapshot.docs.map((doc) => doc.data()));
|
||||||
@@ -36,7 +51,9 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
|
|
||||||
async function post(req: NextApiRequest, res: NextApiResponse) {
|
async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (!req.session.user) {
|
if (!req.session.user) {
|
||||||
res.status(401).json({ok: false, reason: "You must be logged in to generate a code!"});
|
res
|
||||||
|
.status(401)
|
||||||
|
.json({ ok: false, reason: "You must be logged in to generate a code!" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,15 +68,23 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
if (!permission.includes(req.session.user.type)) {
|
if (!permission.includes(req.session.user.type)) {
|
||||||
res.status(403).json({
|
res.status(403).json({
|
||||||
ok: false,
|
ok: false,
|
||||||
reason: "Your account type does not have permissions to generate a code for that type of user!",
|
reason:
|
||||||
|
"Your account type does not have permissions to generate a code for that type of user!",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const codesGeneratedByUserSnapshot = await getDocs(
|
||||||
|
query(collection(db, "codes"), where("creator", "==", req.session.user.id)),
|
||||||
|
);
|
||||||
|
const userCodes = codesGeneratedByUserSnapshot.docs.map((x) => ({
|
||||||
|
...x.data(),
|
||||||
|
}));
|
||||||
|
|
||||||
if (req.session.user.type === "corporate") {
|
if (req.session.user.type === "corporate") {
|
||||||
const codesGeneratedByUserSnapshot = await getDocs(query(collection(db, "codes"), where("creator", "==", req.session.user.id)));
|
|
||||||
const totalCodes = codesGeneratedByUserSnapshot.docs.length + codes.length;
|
const totalCodes = codesGeneratedByUserSnapshot.docs.length + codes.length;
|
||||||
const allowedCodes = req.session.user.corporateInformation?.companyInformation.userAmount || 0;
|
const allowedCodes =
|
||||||
|
req.session.user.corporateInformation?.companyInformation.userAmount || 0;
|
||||||
|
|
||||||
if (totalCodes > allowedCodes) {
|
if (totalCodes > allowedCodes) {
|
||||||
res.status(403).json({
|
res.status(403).json({
|
||||||
@@ -74,7 +99,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
|
|
||||||
const codePromises = codes.map(async (code, index) => {
|
const codePromises = codes.map(async (code, index) => {
|
||||||
const codeRef = doc(db, "codes", code);
|
const codeRef = doc(db, "codes", code);
|
||||||
const codeInformation = {
|
let codeInformation = {
|
||||||
type,
|
type,
|
||||||
code,
|
code,
|
||||||
creator: req.session.user!.id,
|
creator: req.session.user!.id,
|
||||||
@@ -84,12 +109,13 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
|
|
||||||
if (infos && infos.length > index) {
|
if (infos && infos.length > index) {
|
||||||
const { email, name, passport_id } = infos[index];
|
const { email, name, passport_id } = infos[index];
|
||||||
|
const previousCode = userCodes.find((x) => x.email === email) as Code;
|
||||||
|
|
||||||
const transport = prepareMailer();
|
const transport = prepareMailer();
|
||||||
const mailOptions = prepareMailOptions(
|
const mailOptions = prepareMailOptions(
|
||||||
{
|
{
|
||||||
type,
|
type,
|
||||||
code,
|
code: previousCode ? previousCode.code : code,
|
||||||
environment: process.env.ENVIRONMENT,
|
environment: process.env.ENVIRONMENT,
|
||||||
},
|
},
|
||||||
[email.toLowerCase().trim()],
|
[email.toLowerCase().trim()],
|
||||||
@@ -99,6 +125,8 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await transport.sendMail(mailOptions);
|
await transport.sendMail(mailOptions);
|
||||||
|
|
||||||
|
if (!previousCode) {
|
||||||
await setDoc(
|
await setDoc(
|
||||||
codeRef,
|
codeRef,
|
||||||
{
|
{
|
||||||
@@ -109,6 +137,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
},
|
},
|
||||||
{ merge: true },
|
{ merge: true },
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -126,7 +155,9 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
|
|
||||||
async function del(req: NextApiRequest, res: NextApiResponse) {
|
async function del(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (!req.session.user) {
|
if (!req.session.user) {
|
||||||
res.status(401).json({ok: false, reason: "You must be logged in to generate a code!"});
|
res
|
||||||
|
.status(401)
|
||||||
|
.json({ ok: false, reason: "You must be logged in to generate a code!" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user