From 4429f48c4032a62b1bc68a89237b34a3475f2ed8 Mon Sep 17 00:00:00 2001 From: ZacTohZY Date: Mon, 12 Aug 2024 19:39:37 +0800 Subject: [PATCH] Claim voucher function --- client/src/pages/HBContestPage.tsx | 104 +++++++--- client/src/pages/Ranking.tsx | 272 ++++++++++++++------------ client/src/pages/UserVouchersPage.tsx | 217 +++++++++++++------- server/routes/uservoucher.js | 17 ++ 4 files changed, 387 insertions(+), 223 deletions(-) diff --git a/client/src/pages/HBContestPage.tsx b/client/src/pages/HBContestPage.tsx index 2f4cb2e..b4ab71d 100644 --- a/client/src/pages/HBContestPage.tsx +++ b/client/src/pages/HBContestPage.tsx @@ -1,9 +1,17 @@ -import { Button, Tooltip, Modal, ModalContent, ModalHeader, ModalBody } from "@nextui-org/react"; +import { + Button, + Tooltip, + Modal, + ModalContent, + ModalHeader, + ModalBody, +} from "@nextui-org/react"; import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import instance from "../security/http"; import config from "../config"; import { InfoIcon, TrophyIcon } from "../icons"; +import NextUIFormikSelect2 from "../components/NextUIFormikSelect2"; // Import the component interface User { id: string; @@ -45,15 +53,21 @@ export default function HBContestPage() { const fetchData = async () => { try { // Fetch form data - const formDataResponse = await instance.get(`${config.serverAddress}/hbcform`); + const formDataResponse = await instance.get( + `${config.serverAddress}/hbcform` + ); const processedFormData = formDataResponse.data; // Fetch user information - const userInfoResponse = await instance.get(`${config.serverAddress}/users/all`); + const userInfoResponse = await instance.get( + `${config.serverAddress}/users/all` + ); // Combine form data with user information const combined = processedFormData.map((form) => { - const user = userInfoResponse.data.find((user) => user.id === form.userId); + const user = userInfoResponse.data.find( + (user) => user.id === form.userId + ); return { userId: user ? user.id : "Unknown User", formId: form.userId, @@ -70,7 +84,9 @@ export default function HBContestPage() { setFilteredData(combined); // Fetch town councils - const townCouncilsResponse = await instance.get(`${config.serverAddress}/users/town-councils-metadata`); + const townCouncilsResponse = await instance.get( + `${config.serverAddress}/users/town-councils-metadata` + ); setTownCouncils(JSON.parse(townCouncilsResponse.data).townCouncils); } catch (error) { console.log("Failed to fetch data"); @@ -91,18 +107,32 @@ export default function HBContestPage() { const topUser = filteredData.length > 0 ? filteredData[0] : null; const top10Users = filteredData.slice(1, 10); + // Convert town councils to options for the NextUIFormikSelect2 component + const townCouncilOptions = townCouncils.map((townCouncil) => ({ + key: townCouncil, + label: townCouncil, + })); + return (
- - @@ -118,7 +148,8 @@ export default function HBContestPage() { This contest is to encourage residents to reduce the use of electricity and water usage. This contest would be won by the person with the lowest overall bill average. Join us in this - important effort to create a more sustainable future for everyone.{" "} + important effort to create a more sustainable future for + everyone.{" "}

@@ -132,9 +163,15 @@ export default function HBContestPage() { random vouchers.



-

1st Place → 3 vouchers

-

2nd Place → 2 vouchers

-

3rd Place → 1 voucher

+

+ 1st Place → 3 vouchers +

+

+ 2nd Place → 2 vouchers +

+

+ 3rd Place → 1 voucher +

- + - - Leaderboard + +

Leaderboard

- {townCouncils.length > 0 && ( - + {townCouncilOptions.length > 0 && ( + )} -
{topUser && (
-

{topUser.name}

+

+ {topUser.name} +

)}
{top10Users.map((user, index) => ( -
+
{index + 2} {user.name}
@@ -200,7 +244,7 @@ export default function HBContestPage() { > - Information +

Information

diff --git a/client/src/pages/Ranking.tsx b/client/src/pages/Ranking.tsx index 80b9454..d86e64e 100644 --- a/client/src/pages/Ranking.tsx +++ b/client/src/pages/Ranking.tsx @@ -99,29 +99,35 @@ export default function Ranking() { setFilteredFormData(filtered); }, [selectedTownCouncil, originalFormData, userData]); - // Compute top 3 users based on average bill + // Compute top 3 users for each town council useEffect(() => { - const combinedData: FormDataWithUser[] = filteredFormData.map((data) => { + const townCouncilTopUsers: Record = {}; + + filteredFormData.forEach((data) => { const user = userData.find((user) => user.id === data.userId); - return { + const formDataWithUser: FormDataWithUser = { ...data, userName: user ? `${user.firstName} ${user.lastName}` : "Unknown User", userEmail: user ? user.email : "Unknown Email", userTownCouncil: user ? user.townCouncil : "Unknown Town Council", }; - }); - const townCouncilTopUsers: Record = {}; - combinedData.forEach((data) => { - if (!townCouncilTopUsers[data.userTownCouncil] || data.avgBill > townCouncilTopUsers[data.userTownCouncil].avgBill) { - townCouncilTopUsers[data.userTownCouncil] = data; + if (!townCouncilTopUsers[formDataWithUser.userTownCouncil]) { + townCouncilTopUsers[formDataWithUser.userTownCouncil] = []; } + + townCouncilTopUsers[formDataWithUser.userTownCouncil].push(formDataWithUser); }); - const topUsers = Object.values(townCouncilTopUsers); - const sortedTopUsers = topUsers.sort((a, b) => b.avgBill - a.avgBill).slice(0, 3); + // Sort each town council's users by avgBill and pick the top 3 + const topUsersByTownCouncil: FormDataWithUser[] = []; - setTop3Users(sortedTopUsers); + Object.values(townCouncilTopUsers).forEach((users) => { + const top3 = users.sort((a, b) => b.avgBill - a.avgBill).slice(0, 3); + topUsersByTownCouncil.push(...top3); + }); + + setTop3Users(topUsersByTownCouncil); }, [filteredFormData, userData]); // Sort form data based on descriptor @@ -209,15 +215,46 @@ export default function Ranking() { return; } - const randomVoucher = (vouchers: Voucher[]) => vouchers[Math.floor(Math.random() * vouchers.length)]; + // Function to get a random voucher from the available vouchers + const getRandomVouchers = (count: number): Voucher[] => { + const shuffled = vouchers.sort(() => 0.5 - Math.random()); + return shuffled.slice(0, count); + }; - for (const user of topUsers) { - const voucher = randomVoucher(vouchers); - if (voucher) { - await instance.post(`${config.serverAddress}/user-vouchers`, { - userId: user.userId, - voucherId: voucher.id, - }); + // Group users by town council + const townCouncilGroups: Record = {}; + topUsers.forEach(user => { + if (!townCouncilGroups[user.userTownCouncil]) { + townCouncilGroups[user.userTownCouncil] = []; + } + townCouncilGroups[user.userTownCouncil].push(user); + }); + + // Iterate over each town council and assign vouchers + for (const [townCouncil, users] of Object.entries(townCouncilGroups)) { + // Sort users by avgBill and pick top 3 + const top3 = users.sort((a, b) => b.avgBill - a.avgBill).slice(0, 3); + + for (let i = 0; i < top3.length; i++) { + const user = top3[i]; + let voucherCount = 0; + + if (i === 0) { + voucherCount = 3; // Top 1 gets 3 vouchers + } else if (i === 1) { + voucherCount = 2; // Top 2 gets 2 vouchers + } else if (i === 2) { + voucherCount = 1; // Top 3 gets 1 voucher + } + + const vouchersToAssign = getRandomVouchers(voucherCount); + + for (const voucher of vouchersToAssign) { + await instance.post(`${config.serverAddress}/user-vouchers`, { + userId: user.userId, + voucherId: voucher.id, + }); + } } } } catch (error) { @@ -225,6 +262,8 @@ export default function Ranking() { } }; + + const handleGiveVouchers = async () => { try { await assignVouchersToUsers(top3Users); @@ -238,6 +277,12 @@ export default function Ranking() { setSelectedTownCouncil(value); }; + const options = townCouncils.map((townCouncil) => ({ + key: townCouncil, // Use key instead of value + label: townCouncil, + })); + + return (
@@ -246,129 +291,108 @@ export default function Ranking() {
- {townCouncils.length > 0 && ( - ({ - key: townCouncil, - label: townCouncil, - }))} - onChange={handleTownCouncilChange} - /> - )} +
- {top3Users.length > 0 && ( - - )} +
+
-
- - - User Name - Electrical Bill - Water Bill - Total Bill - Dependents - - Average Bill - - Bill Picture - Actions - +
+ + Rank + User Name + Electrical Bill + Water Bill + Total Bill + Dependents + + Average Bill + + Bill Picture + Actions + - - {combinedData.map((data) => ( - - {data.userName} - ${data.electricalBill.toFixed(2)} - ${data.waterBill.toFixed(2)} - ${data.totalBill.toFixed(2)} - {data.noOfDependents} - ${data.avgBill.toFixed(2)} - - {data.billPicture ? ( - - ) : ( - - )} - + + {combinedData.map((data, index) => ( + + {index + 1} + {data.userName} + ${data.electricalBill.toFixed(2)} + ${data.waterBill.toFixed(2)} + ${data.totalBill.toFixed(2)} + {data.noOfDependents} + ${data.avgBill.toFixed(2)} + + {data.billPicture ? ( + + ) : ( + + )} + + + + + + + ))} + +
- - - - - - ))} - - -
- {/* Email Confirmation Modal */} - + {/* Email Modal */} + setIsEmailModalOpen(false)}> - {(onClose) => ( - <> - Send Email - -

Are you sure you want to send an email to {selectedUser.name} ({selectedUser.email})?

-
- - - - - - )} + Send Email + +

Are you sure you want to send an email to {selectedUser.name} ({selectedUser.email})?

+
+ + + +
- {/* Delete Confirmation Modal */} - + + {/* Delete Modal */} + setIsDeleteModalOpen(false)}> - {(onClose) => ( - <> - Delete Entry - -

Are you sure you want to delete this form entry?

-
- - - - - - )} + Delete Entry + +

Are you sure you want to delete this entry?

+
+ + + +
- {/* Open Image Modal */} - + + {/* Image Modal */} + setIsImageModalOpen(false)}> - {modalImageUrl && ( - Bill Picture - )} + {modalImageUrl && Bill Image} -
+
); } diff --git a/client/src/pages/UserVouchersPage.tsx b/client/src/pages/UserVouchersPage.tsx index 6f1c8ba..287bd46 100644 --- a/client/src/pages/UserVouchersPage.tsx +++ b/client/src/pages/UserVouchersPage.tsx @@ -1,9 +1,19 @@ -import { useEffect, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import instance from '../security/http'; -import config from '../config'; -import { retrieveUserInformation } from '../security/users'; -import { Card, CardHeader, CardBody, CardFooter, Divider, Image } from '@nextui-org/react'; +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import instance from "../security/http"; +import config from "../config"; +import { retrieveUserInformation } from "../security/users"; +import { + Card, + CardBody, + Image, + Button, + Modal, + ModalContent, + ModalHeader, + ModalBody, +} from "@nextui-org/react"; +import { VoucherIcon } from "../icons"; interface Voucher { id: string; @@ -30,6 +40,10 @@ export default function UserVoucherPage() { const navigate = useNavigate(); const [userInformation, setUserInformation] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [selectedVoucher, setSelectedVoucher] = useState(null); + const [selectedUserVoucherId, setSelectedUserVoucherId] = useState(null); + useEffect(() => { const fetchUserInformation = async () => { try { @@ -47,12 +61,12 @@ export default function UserVoucherPage() { const fetchUserVouchers = async () => { try { if (userInformation && userInformation.id) { - // Fetch user vouchers - const response = await instance.get(`${config.serverAddress}/user-vouchers/user/${userInformation.id}`); + const response = await instance.get( + `${config.serverAddress}/user-vouchers/user/${userInformation.id}` + ); const fetchedUserVouchers = response.data.userVouchers; setUserVouchers(fetchedUserVouchers); - // Fetch voucher details const voucherIds = response.data.voucherIds; const voucherDetailsPromises = voucherIds.map((voucherId: string) => instance.get(`${config.serverAddress}/vouchers/${voucherId}`) @@ -60,26 +74,32 @@ export default function UserVoucherPage() { const voucherResponses = await Promise.all(voucherDetailsPromises); const voucherMap = new Map(); - voucherResponses.forEach(response => { + voucherResponses.forEach((response) => { const voucher = response.data; voucherMap.set(voucher.id, voucher); - // Fetch brand logos if (voucher.brandLogo) { instance - .get(`${config.serverAddress}/vouchers/brandLogo/${voucher.id}`, { responseType: 'blob' }) + .get(`${config.serverAddress}/vouchers/brandLogo/${voucher.id}`, { + responseType: "blob", + }) .then((res) => { const url = URL.createObjectURL(res.data); - setBrandLogoUrls(prev => ({ ...prev, [voucher.id]: url })); + setBrandLogoUrls((prev) => ({ ...prev, [voucher.id]: url })); }) - .catch(err => console.error(`Error fetching brand logo for voucher ${voucher.id}:`, err)); + .catch((err) => + console.error( + `Error fetching brand logo for voucher ${voucher.id}:`, + err + ) + ); } }); setVouchers(voucherMap); } } catch (error) { - setError('Failed to fetch vouchers'); + setError("Failed to fetch vouchers"); } finally { setLoading(false); } @@ -90,64 +110,123 @@ export default function UserVoucherPage() { } }, [userInformation]); + const handleVoucherButtonClick = (voucher: Voucher, userVoucherId: string) => { + setSelectedVoucher(voucher); + setSelectedUserVoucherId(userVoucherId); + setIsModalOpen(true); + }; + + const handleConfirm = async () => { + if (selectedUserVoucherId) { + try { + // DELETE request to remove the voucher from the user-vouchers + await instance.delete( + `${config.serverAddress}/user-vouchers/${selectedUserVoucherId}` + ); + + // Update state to remove the deleted voucher + setUserVouchers((prev) => + prev.filter((userVoucher) => userVoucher.id !== selectedUserVoucherId) + ); + setSelectedVoucher(null); + setSelectedUserVoucherId(null); + setIsModalOpen(false); + } catch (error) { + console.error("Failed to delete voucher", error); + setError("Failed to delete voucher"); + } + } + }; + + const handleCancel = () => { + setIsModalOpen(false); + }; + return ( -
- {loading ? ( -

Loading...

- ) : error ? ( -

{error}

- ) : userVouchers.length === 0 ? ( -

You have no vouchers currently.

- ) : ( -
-

Your Vouchers

-
- {userVouchers.map((userVoucher) => { - const voucher = vouchers.get(userVoucher.voucherId); - return ( - - - {voucher ? ( +
+
+

Vouchers

+
+ {loading ? ( +

Loading...

+ ) : error ? ( +

{error}

+ ) : userVouchers.length === 0 ? ( +

You have no vouchers currently.

+ ) : ( +
+ {userVouchers.map((userVoucher) => { + const voucher = vouchers.get(userVoucher.voucherId); + return ( + + {voucher.brand} - ) : ( - No image available - )} - - - - {voucher ? ( - <> -

{voucher.brand}

-

{voucher.description}

-

Code: {voucher.code}

-

Expires on: {new Date(voucher.expirationDate).toLocaleDateString()}

- - ) : ( -

Voucher details are unavailable.

- )} -
- - - {/* Add any additional footer content here */} - -
- ); - })} -
+
+ {voucher ? ( + <> +

{voucher.brand}

+

{voucher.description}

+

+ Expires on:{" "} + {new Date( + voucher.expirationDate + ).toLocaleDateString()} +

+ + ) : ( +

Voucher details are unavailable.

+ )} +
+
+ +
+ + + ); + })} +
+ )}
- )} +
+ + {/* Modal for using voucher */} + + + +

Use Voucher

+
+ +
+

Do you want to use this voucher?

+ {selectedVoucher && ( + <> +

Brand: {selectedVoucher.brand}

+

Code: {selectedVoucher.code}

+ + )} +
+
+
+ + +
+
+
); } diff --git a/server/routes/uservoucher.js b/server/routes/uservoucher.js index 692626e..0d4b40c 100644 --- a/server/routes/uservoucher.js +++ b/server/routes/uservoucher.js @@ -50,5 +50,22 @@ router.get('/user/:userId', async (req, res) => { } }); +router.delete('/:id', async (req, res) => { + const id = req.params.id; + + try { + const userVoucher = await UserVoucher.findByPk(id); + + if (!userVoucher) { + return res.status(404).json({ error: 'UserVoucher not found' }); + } + + await userVoucher.destroy(); + res.status(204).send(); // No Content response + } catch (error) { + res.status(500).json({ error: 'Failed to delete UserVoucher' }); + } +}); + module.exports = router;