Scaffoled Users Management Screen
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
ChatBubbleOvalLeftIcon,
|
||||
ChevronLeftIcon,
|
||||
ArrowRightStartOnRectangleIcon,
|
||||
UsersIcon,
|
||||
} from "../icons";
|
||||
import EcoconnectFullLogo from "./EcoconnectFullLogo";
|
||||
import { retrieveUserInformation } from "../security/users";
|
||||
@@ -192,7 +193,12 @@ export default function AdministratorNavigationPanel() {
|
||||
<div>
|
||||
<p className="text-sm font-bold opacity-50 pb-2">Users</p>
|
||||
<AdministratorNavigationPanelNavigationButton
|
||||
text="User Feedbacks"
|
||||
text="Manage Users"
|
||||
icon={<UsersIcon />}
|
||||
onClickRef="users-management"
|
||||
/>
|
||||
<AdministratorNavigationPanelNavigationButton
|
||||
text="Feedbacks"
|
||||
icon={<ChatBubbleOvalLeftIcon />}
|
||||
onClickRef="#"
|
||||
/>
|
||||
|
||||
@@ -443,4 +443,80 @@ export const TrashIcon = () => {
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export const UsersIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="size-6"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const ClipboardDocumentIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="size-6"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M8.25 7.5V6.108c0-1.135.845-2.098 1.976-2.192.373-.03.748-.057 1.123-.08M15.75 18H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 0 0-1.123-.08M15.75 18.75v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5A3.375 3.375 0 0 0 6.375 7.5H5.25m11.9-3.664A2.251 2.251 0 0 0 15 2.25h-1.5a2.251 2.251 0 0 0-2.15 1.586m5.8 0c.065.21.1.433.1.664v.75h-6V4.5c0-.231.035-.454.1-.664M6.75 7.5H4.875c-.621 0-1.125.504-1.125 1.125v12c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V16.5a9 9 0 0 0-9-9Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const ArchiveBoxIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="size-6"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="m20.25 7.5-.625 10.632a2.25 2.25 0 0 1-2.247 2.118H6.622a2.25 2.25 0 0 1-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const LifebuoyIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="size-6"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M16.712 4.33a9.027 9.027 0 0 1 1.652 1.306c.51.51.944 1.064 1.306 1.652M16.712 4.33l-3.448 4.138m3.448-4.138a9.014 9.014 0 0 0-9.424 0M19.67 7.288l-4.138 3.448m4.138-3.448a9.014 9.014 0 0 1 0 9.424m-4.138-5.976a3.736 3.736 0 0 0-.88-1.388 3.737 3.737 0 0 0-1.388-.88m2.268 2.268a3.765 3.765 0 0 1 0 2.528m-2.268-4.796a3.765 3.765 0 0 0-2.528 0m4.796 4.796c-.181.506-.475.982-.88 1.388a3.736 3.736 0 0 1-1.388.88m2.268-2.268 4.138 3.448m0 0a9.027 9.027 0 0 1-1.306 1.652c-.51.51-1.064.944-1.652 1.306m0 0-3.448-4.138m3.448 4.138a9.014 9.014 0 0 1-9.424 0m5.976-4.138a3.765 3.765 0 0 1-2.528 0m0 0a3.736 3.736 0 0 1-1.388-.88 3.737 3.737 0 0 1-.88-1.388m2.268 2.268L7.288 19.67m0 0a9.024 9.024 0 0 1-1.652-1.306 9.027 9.027 0 0 1-1.306-1.652m0 0 4.138-3.448M4.33 16.712a9.014 9.014 0 0 1 0-9.424m4.138 5.976a3.765 3.765 0 0 1 0-2.528m0 0c.181-.506.475-.982.88-1.388a3.736 3.736 0 0 1 1.388-.88m-2.268 2.268L4.33 7.288m6.406 1.18L7.288 4.33m0 0a9.024 9.024 0 0 0-1.652 1.306A9.025 9.025 0 0 0 4.33 7.288"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
185
client/src/pages/UsersManagement.tsx
Normal file
185
client/src/pages/UsersManagement.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import {
|
||||
getKeyValue,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableColumn,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Button,
|
||||
Tooltip,
|
||||
} from "@nextui-org/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import instance from "../security/http";
|
||||
import config from "../config";
|
||||
import { popErrorToast, popToast } from "../utilities";
|
||||
import { ArchiveBoxIcon, ClipboardDocumentIcon, LifebuoyIcon } from "../icons";
|
||||
|
||||
export default function UsersManagement() {
|
||||
const [userInformationlist, setUserInformationList] = useState<any>([]);
|
||||
const columns = [
|
||||
{
|
||||
key: "firstName",
|
||||
label: "FISRT NAME",
|
||||
},
|
||||
{
|
||||
key: "lastName",
|
||||
label: "LAST NAME",
|
||||
},
|
||||
{
|
||||
key: "email",
|
||||
label: "EMAIL ADDRESS",
|
||||
},
|
||||
{
|
||||
key: "phoneNumber",
|
||||
label: "TELEPHONE",
|
||||
},
|
||||
{
|
||||
key: "accountType",
|
||||
label: "ACCOUNT TYPE",
|
||||
},
|
||||
{
|
||||
key: "isArchived",
|
||||
label: "STATUS",
|
||||
},
|
||||
{
|
||||
key: "actions",
|
||||
label: "ACTIONS",
|
||||
},
|
||||
];
|
||||
|
||||
const populateUserInformationList = () => {
|
||||
instance
|
||||
.get(`${config.serverAddress}/users/all`)
|
||||
.then((response) => {
|
||||
setUserInformationList(response.data);
|
||||
console.log(userInformationlist);
|
||||
})
|
||||
.catch((error) => {
|
||||
popErrorToast(error);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
populateUserInformationList();
|
||||
}, []);
|
||||
|
||||
const handleCopyID = (userId: string, firstName: string) => {
|
||||
navigator.clipboard.writeText(userId);
|
||||
popToast(firstName + "'s User ID has been copied!", 1);
|
||||
};
|
||||
|
||||
const handleArchiveToggle = (userId: string, isArchived: boolean) => {
|
||||
instance
|
||||
.put(
|
||||
`${config.serverAddress}/users/${
|
||||
isArchived ? "unarchive" : "archive"
|
||||
}/${userId}`
|
||||
)
|
||||
.then(() => {
|
||||
window.location.reload();
|
||||
})
|
||||
.catch((error) => {
|
||||
popErrorToast(error);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{userInformationlist && (
|
||||
<div className="flex flex-col gap-8 p-8">
|
||||
<p className="text-4xl font-bold">Users Onboard</p>
|
||||
<Table aria-label="User Information Table">
|
||||
<TableHeader columns={columns}>
|
||||
{(column) => (
|
||||
<TableColumn key={column.key}>{column.label}</TableColumn>
|
||||
)}
|
||||
</TableHeader>
|
||||
<TableBody items={userInformationlist}>
|
||||
{(userEntry: any) => (
|
||||
<TableRow key={userEntry.id}>
|
||||
{(columnKey) => (
|
||||
<TableCell
|
||||
className={
|
||||
columnKey == "accountType" &&
|
||||
getKeyValue(userEntry, columnKey) == 2
|
||||
? "text-primary-500 font-semibold"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{columnKey === "accountType" ? (
|
||||
(() => {
|
||||
const accountType = getKeyValue(userEntry, columnKey);
|
||||
switch (accountType) {
|
||||
case 0:
|
||||
return "User";
|
||||
case 1:
|
||||
return "Karang Guni";
|
||||
case 2:
|
||||
return "Admin";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
})()
|
||||
) : columnKey === "actions" ? (
|
||||
<div className="flex gap-2">
|
||||
<Tooltip content="Copy ID">
|
||||
<Button
|
||||
variant="light"
|
||||
isIconOnly
|
||||
onClick={() =>
|
||||
handleCopyID(userEntry.id, userEntry.firstName)
|
||||
}
|
||||
>
|
||||
<ClipboardDocumentIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={
|
||||
userEntry.isArchived ? "Unarchive" : "Archive"
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="light"
|
||||
isIconOnly
|
||||
onClick={() =>
|
||||
handleArchiveToggle(
|
||||
userEntry.id,
|
||||
userEntry.isArchived
|
||||
)
|
||||
}
|
||||
>
|
||||
{userEntry.isArchived ? (
|
||||
<div className="text-green-600">
|
||||
<LifebuoyIcon />
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-red-500">
|
||||
<ArchiveBoxIcon />
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : columnKey == "isArchived" ? (
|
||||
getKeyValue(userEntry, columnKey) ? (
|
||||
"Archived"
|
||||
) : (
|
||||
"Active"
|
||||
)
|
||||
) : (
|
||||
<p className={userEntry.isArchived ? "opacity-50" : ""}>
|
||||
{getKeyValue(userEntry, columnKey)}
|
||||
</p>
|
||||
)}
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -79,6 +79,7 @@ router.get("/all", async (req, res) => {
|
||||
let list = await User.findAll({
|
||||
where: condition,
|
||||
order: [["createdAt", "DESC"]],
|
||||
attributes: { exclude: ["password", "profilePicture"] },
|
||||
});
|
||||
res.json(list);
|
||||
});
|
||||
@@ -97,6 +98,8 @@ router.get("/individual/:id", validateToken, async (req, res) => {
|
||||
message: `ERR_ACC_IS_ARCHIVED`,
|
||||
});
|
||||
} else {
|
||||
user.password = undefined;
|
||||
user.profilePicture = undefined;
|
||||
res.json(user);
|
||||
}
|
||||
});
|
||||
@@ -223,6 +226,30 @@ router.put("/archive/:id", validateToken, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.put("/unarchive/:id", validateToken, async (req, res) => {
|
||||
let id = req.params.id;
|
||||
let user = await User.findByPk(id);
|
||||
|
||||
if (!user) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await User.update(
|
||||
{ isArchived: false },
|
||||
{
|
||||
where: { id: id },
|
||||
}
|
||||
);
|
||||
res.json({
|
||||
message: "User archived successfully.",
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(400).json({ errors: err.errors });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/profile-image/:id", async (req, res) => {
|
||||
let id = req.params.id;
|
||||
let user = await User.findByPk(id);
|
||||
|
||||
Reference in New Issue
Block a user