Scaffoled Users Management Screen

This commit is contained in:
2024-07-27 02:04:26 +08:00
parent 4f806b56f6
commit a6a2c5834f
4 changed files with 296 additions and 2 deletions

View File

@@ -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="#"
/>

View File

@@ -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>
);
};

View 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>
);
}

View File

@@ -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);