368 lines
12 KiB
TypeScript
368 lines
12 KiB
TypeScript
import Button from "@/components/Low/Button";
|
|
import Checkbox from "@/components/Low/Checkbox";
|
|
import Select from "@/components/Low/Select";
|
|
import useCodes from "@/hooks/useCodes";
|
|
import useUser from "@/hooks/useUser";
|
|
import useUsers from "@/hooks/useUsers";
|
|
import { Code, User } from "@/interfaces/user";
|
|
import { USER_TYPE_LABELS } from "@/resources/user";
|
|
import {
|
|
createColumnHelper,
|
|
flexRender,
|
|
getCoreRowModel,
|
|
useReactTable,
|
|
} from "@tanstack/react-table";
|
|
import axios from "axios";
|
|
import moment from "moment";
|
|
import { useEffect, useState, useMemo } from "react";
|
|
import { BsTrash } from "react-icons/bs";
|
|
import { toast } from "react-toastify";
|
|
import ReactDatePicker from "react-datepicker";
|
|
import clsx from "clsx";
|
|
import { checkAccess } from "@/utils/permissions";
|
|
|
|
const columnHelper = createColumnHelper<Code>();
|
|
|
|
const CreatorCell = ({ id, users }: { id: string; users: User[] }) => {
|
|
const [creatorUser, setCreatorUser] = useState<User>();
|
|
|
|
useEffect(() => {
|
|
setCreatorUser(users.find((x) => x.id === id));
|
|
}, [id, users]);
|
|
|
|
return (
|
|
<>
|
|
{(creatorUser?.type === "corporate"
|
|
? creatorUser?.corporateInformation?.companyInformation?.name
|
|
: creatorUser?.name || "N/A") || "N/A"}{" "}
|
|
{creatorUser && `(${USER_TYPE_LABELS[creatorUser.type]})`}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default function CodeList({ user }: { user: User }) {
|
|
const [selectedCodes, setSelectedCodes] = useState<string[]>([]);
|
|
|
|
const [filteredCorporate, setFilteredCorporate] = useState<User | undefined>(
|
|
user?.type === "corporate" ? user : undefined
|
|
);
|
|
const [filterAvailability, setFilterAvailability] = useState<
|
|
"in-use" | "unused"
|
|
>();
|
|
|
|
// const [filteredCodes, setFilteredCodes] = useState<Code[]>([]);
|
|
|
|
const { users } = useUsers();
|
|
const { codes, reload } = useCodes(
|
|
user?.type === "corporate" ? user?.id : undefined
|
|
);
|
|
|
|
const [startDate, setStartDate] = useState<Date | null>(moment("01/01/2023").toDate());
|
|
const [endDate, setEndDate] = useState<Date | null>(moment().endOf("day").toDate());
|
|
const filteredCodes = useMemo(() => {
|
|
return codes.filter((x) => {
|
|
// TODO: if the expiry date is missing, it does not make sense to filter by date
|
|
// so we need to find a way to handle this edge case
|
|
if(startDate && endDate && x.expiryDate) {
|
|
const date = moment(x.expiryDate);
|
|
if(date.isBefore(startDate) || date.isAfter(endDate)) {
|
|
return false;
|
|
}
|
|
}
|
|
if (filteredCorporate && x.creator !== filteredCorporate.id) return false;
|
|
if (filterAvailability) {
|
|
if (filterAvailability === "in-use" && !x.userId) return false;
|
|
if (filterAvailability === "unused" && x.userId) return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}, [codes, startDate, endDate, filteredCorporate, filterAvailability]);
|
|
|
|
const toggleCode = (id: string) => {
|
|
setSelectedCodes((prev) =>
|
|
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
|
|
);
|
|
};
|
|
|
|
const toggleAllCodes = (checked: boolean) => {
|
|
if (checked)
|
|
return setSelectedCodes(
|
|
filteredCodes.filter((x) => !x.userId).map((x) => x.code)
|
|
);
|
|
|
|
return setSelectedCodes([]);
|
|
};
|
|
|
|
const deleteCodes = async (codes: string[]) => {
|
|
if (
|
|
!confirm(`Are you sure you want to delete these ${codes.length} code(s)?`)
|
|
)
|
|
return;
|
|
|
|
const params = new URLSearchParams();
|
|
codes.forEach((code) => params.append("code", code));
|
|
|
|
axios
|
|
.delete(`/api/code?${params.toString()}`)
|
|
.then(() => {
|
|
toast.success(`Deleted the codes!`);
|
|
setSelectedCodes([]);
|
|
})
|
|
.catch((reason) => {
|
|
if (reason.response.status === 404) {
|
|
toast.error("Code not found!");
|
|
return;
|
|
}
|
|
|
|
if (reason.response.status === 403) {
|
|
toast.error("You do not have permission to delete this code!");
|
|
return;
|
|
}
|
|
|
|
toast.error("Something went wrong, please try again later.");
|
|
})
|
|
.finally(reload);
|
|
};
|
|
|
|
const deleteCode = async (code: Code) => {
|
|
if (!confirm(`Are you sure you want to delete this "${code.code}" code?`))
|
|
return;
|
|
|
|
axios
|
|
.delete(`/api/code/${code.code}`)
|
|
.then(() => toast.success(`Deleted the "${code.code}" exam`))
|
|
.catch((reason) => {
|
|
if (reason.response.status === 404) {
|
|
toast.error("Code not found!");
|
|
return;
|
|
}
|
|
|
|
if (reason.response.status === 403) {
|
|
toast.error("You do not have permission to delete this code!");
|
|
return;
|
|
}
|
|
|
|
toast.error("Something went wrong, please try again later.");
|
|
})
|
|
.finally(reload);
|
|
};
|
|
|
|
const allowedToDelete = checkAccess(
|
|
user,
|
|
["developer", "admin", "corporate", "mastercorporate"],
|
|
"deleteCodes"
|
|
);
|
|
|
|
const defaultColumns = [
|
|
columnHelper.accessor("code", {
|
|
id: "codeCheckbox",
|
|
header: () => (
|
|
<Checkbox
|
|
disabled={filteredCodes.filter((x) => !x.userId).length === 0}
|
|
isChecked={
|
|
selectedCodes.length ===
|
|
filteredCodes.filter((x) => !x.userId).length &&
|
|
filteredCodes.filter((x) => !x.userId).length > 0
|
|
}
|
|
onChange={(checked) => toggleAllCodes(checked)}
|
|
>
|
|
{""}
|
|
</Checkbox>
|
|
),
|
|
cell: (info) =>
|
|
!info.row.original.userId ? (
|
|
<Checkbox
|
|
isChecked={selectedCodes.includes(info.getValue())}
|
|
onChange={() => toggleCode(info.getValue())}
|
|
>
|
|
{""}
|
|
</Checkbox>
|
|
) : null,
|
|
}),
|
|
columnHelper.accessor("code", {
|
|
header: "Code",
|
|
cell: (info) => info.getValue(),
|
|
}),
|
|
columnHelper.accessor("creationDate", {
|
|
header: "Creation Date",
|
|
cell: (info) =>
|
|
info.getValue() ? moment(info.getValue()).format("DD/MM/YYYY") : "N/A",
|
|
}),
|
|
columnHelper.accessor("email", {
|
|
header: "Invited E-mail",
|
|
cell: (info) => info.getValue() || "N/A",
|
|
}),
|
|
columnHelper.accessor("creator", {
|
|
header: "Creator",
|
|
cell: (info) => <CreatorCell id={info.getValue()} users={users} />,
|
|
}),
|
|
columnHelper.accessor("userId", {
|
|
header: "Availability",
|
|
cell: (info) =>
|
|
info.getValue() ? (
|
|
<span className="flex gap-1 items-center text-mti-green">
|
|
<div className="w-2 h-2 rounded-full bg-mti-green" /> In Use
|
|
</span>
|
|
) : (
|
|
<span className="flex gap-1 items-center text-mti-red">
|
|
<div className="w-2 h-2 rounded-full bg-mti-red" /> Unused
|
|
</span>
|
|
),
|
|
}),
|
|
{
|
|
header: "",
|
|
id: "actions",
|
|
cell: ({ row }: { row: { original: Code } }) => {
|
|
return (
|
|
<div className="flex gap-4">
|
|
{allowedToDelete && !row.original.userId && (
|
|
<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" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
];
|
|
|
|
const table = useReactTable({
|
|
data: filteredCodes,
|
|
columns: defaultColumns,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
});
|
|
|
|
return (
|
|
<>
|
|
<div className="flex items-center justify-between pb-4 pt-1">
|
|
<div className="flex items-center gap-4">
|
|
<Select
|
|
className="!w-96 !py-1"
|
|
disabled={user?.type === "corporate"}
|
|
isClearable
|
|
placeholder="Corporate"
|
|
value={
|
|
filteredCorporate
|
|
? {
|
|
label: `${
|
|
filteredCorporate.type === "corporate"
|
|
? filteredCorporate.corporateInformation
|
|
?.companyInformation?.name || filteredCorporate.name
|
|
: filteredCorporate.name
|
|
} (${USER_TYPE_LABELS[filteredCorporate.type]})`,
|
|
value: filteredCorporate.id,
|
|
}
|
|
: null
|
|
}
|
|
options={users
|
|
.filter((x) =>
|
|
["admin", "developer", "corporate"].includes(x.type)
|
|
)
|
|
.map((x) => ({
|
|
label: `${
|
|
x.type === "corporate"
|
|
? x.corporateInformation?.companyInformation?.name || x.name
|
|
: x.name
|
|
} (${USER_TYPE_LABELS[x.type]})`,
|
|
value: x.id,
|
|
user: x,
|
|
}))}
|
|
onChange={(value) =>
|
|
setFilteredCorporate(
|
|
value ? users.find((x) => x.id === value?.value) : undefined
|
|
)
|
|
}
|
|
/>
|
|
<Select
|
|
className="!w-96 !py-1"
|
|
placeholder="Availability"
|
|
isClearable
|
|
options={[
|
|
{ label: "In Use", value: "in-use" },
|
|
{ label: "Unused", value: "unused" },
|
|
]}
|
|
onChange={(value) =>
|
|
setFilterAvailability(
|
|
value ? (value.value as typeof filterAvailability) : undefined
|
|
)
|
|
}
|
|
/>
|
|
<ReactDatePicker
|
|
dateFormat="dd/MM/yyyy"
|
|
className="px-4 py-6 w-full text-sm text-center font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed rounded-full border border-mti-gray-platinum focus:outline-none"
|
|
selected={startDate}
|
|
startDate={startDate}
|
|
endDate={endDate}
|
|
selectsRange
|
|
showMonthDropdown
|
|
filterDate={(date: Date) =>
|
|
moment(date).isSameOrBefore(moment(new Date()))
|
|
}
|
|
onChange={([initialDate, finalDate]: [Date, Date]) => {
|
|
setStartDate(initialDate ?? moment("01/01/2023").toDate());
|
|
if (finalDate) {
|
|
// basicly selecting a final day works as if I'm selecting the first
|
|
// minute of that day. this way it covers the whole day
|
|
setEndDate(moment(finalDate).endOf("day").toDate());
|
|
return;
|
|
}
|
|
setEndDate(null);
|
|
}}
|
|
/>
|
|
</div>
|
|
{allowedToDelete && (
|
|
<div className="flex gap-4 items-center">
|
|
<span>{selectedCodes.length} code(s) selected</span>
|
|
<Button
|
|
disabled={selectedCodes.length === 0}
|
|
variant="outline"
|
|
color="red"
|
|
className="!py-1 px-10"
|
|
onClick={() => deleteCodes(selectedCodes)}
|
|
>
|
|
Delete
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
|
|
<thead>
|
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
<tr key={headerGroup.id}>
|
|
{headerGroup.headers.map((header) => (
|
|
<th className="p-4 text-left" key={header.id}>
|
|
{header.isPlaceholder
|
|
? null
|
|
: flexRender(
|
|
header.column.columnDef.header,
|
|
header.getContext()
|
|
)}
|
|
</th>
|
|
))}
|
|
</tr>
|
|
))}
|
|
</thead>
|
|
<tbody className="px-2">
|
|
{table.getRowModel().rows.map((row) => (
|
|
<tr
|
|
className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2"
|
|
key={row.id}
|
|
>
|
|
{row.getVisibleCells().map((cell) => (
|
|
<td className="px-4 py-2" key={cell.id}>
|
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
</td>
|
|
))}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</>
|
|
);
|
|
}
|