diff --git a/client/src/pages/Ranking.tsx b/client/src/pages/Ranking.tsx index 3bfb427..708804f 100644 --- a/client/src/pages/Ranking.tsx +++ b/client/src/pages/Ranking.tsx @@ -1,403 +1,489 @@ -import { useEffect, useState } from 'react'; -import config from '../config'; -import instance from '../security/http'; -import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, SortDescriptor, Button, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from '@nextui-org/react'; -import { EmailIcon, ImageIcon, TrashDeleteIcon } from '../icons'; -import NextUIFormikSelect2 from '../components/NextUIFormikSelect2'; +import { useEffect, useState } from "react"; +import config from "../config"; +import instance from "../security/http"; +import { + Table, + TableHeader, + TableColumn, + TableBody, + TableRow, + TableCell, + SortDescriptor, + Button, + Modal, + ModalContent, + ModalHeader, + ModalBody, + ModalFooter, +} from "@nextui-org/react"; +import { EmailIcon, ImageIcon, TrashDeleteIcon } from "../icons"; +import NextUIFormikSelect2 from "../components/NextUIFormikSelect2"; interface User { - id: string; - firstName: string; - lastName: string; - email: string; - townCouncil: string; + id: string; + firstName: string; + lastName: string; + email: string; + townCouncil: string; } interface FormData { - id: string; - electricalBill: number; - waterBill: number; - totalBill: number; - noOfDependents: number; - avgBill: number; - billPicture: string; - userId: string; + id: string; + electricalBill: number; + waterBill: number; + totalBill: number; + noOfDependents: number; + avgBill: number; + billPicture: string; + userId: string; } interface FormDataWithUser extends FormData { - userName: string; - userEmail: string; - userTownCouncil: string; + userName: string; + userEmail: string; + userTownCouncil: string; } interface Voucher { - id: string; - brandLogo: string | null; - brand: string; - description: string; - expirationDate: Date; - quantityAvailable: number; - code: string; + id: string; + brandLogo: string | null; + brand: string; + description: string; + expirationDate: Date; + quantityAvailable: number; + code: string; } interface UserVoucher { - id: string; // UUID type - userId: string; - voucherId: string; + id: string; // UUID type + userId: string; + voucherId: string; } export default function Ranking() { - const [originalFormData, setOriginalFormData] = useState([]); - const [filteredFormData, setFilteredFormData] = useState([]); - const [userData, setUserData] = useState([]); - const [sortDescriptor, setSortDescriptor] = useState({ - column: "avgBill", - direction: "ascending", - }); - const [isEmailModalOpen, setIsEmailModalOpen] = useState(false); - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [selectedUser, setSelectedUser] = useState<{ email: string, name: string }>({ email: "", name: "" }); - const [selectedFormId, setSelectedFormId] = useState(null); // Changed to string to match UUID type - const [townCouncils, setTownCouncils] = useState([]); - const [selectedTownCouncil, setSelectedTownCouncil] = useState(""); - const [top3Users, setTop3Users] = useState([]); + const [originalFormData, setOriginalFormData] = useState([]); + const [filteredFormData, setFilteredFormData] = useState([]); + const [userData, setUserData] = useState([]); + const [sortDescriptor, setSortDescriptor] = useState({ + column: "avgBill", + direction: "ascending", + }); + const [isEmailModalOpen, setIsEmailModalOpen] = useState(false); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [selectedUser, setSelectedUser] = useState<{ + email: string; + name: string; + }>({ email: "", name: "" }); + const [selectedFormId, setSelectedFormId] = useState(null); // Changed to string to match UUID type + const [townCouncils, setTownCouncils] = useState([]); + const [selectedTownCouncil, setSelectedTownCouncil] = useState(""); + const [top3Users, setTop3Users] = useState([]); + + // Fetch data on component mount + useEffect(() => { + const fetchData = async () => { + try { + const formResponse = await instance.get( + `${config.serverAddress}/hbcform` + ); + const processedFormData = formResponse.data.map((data) => ({ + ...data, + electricalBill: Number(data.electricalBill), + waterBill: Number(data.waterBill), + totalBill: Number(data.totalBill), + avgBill: Number(data.avgBill), + })); + setOriginalFormData(processedFormData); + setFilteredFormData(processedFormData); + + const userResponse = await instance.get( + `${config.serverAddress}/users/all` + ); + setUserData(userResponse.data); + + 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:", error); + } + }; + + fetchData(); + }, []); + + // Filter form data based on selected town council + useEffect(() => { + const filtered = originalFormData.filter((data) => { + const user = userData.find((user) => user.id === data.userId); + return selectedTownCouncil + ? user?.townCouncil === selectedTownCouncil + : true; + }); + setFilteredFormData(filtered); + }, [selectedTownCouncil, originalFormData, userData]); + + // Compute top 3 users for each town council + useEffect(() => { + const townCouncilTopUsers: Record = {}; + + filteredFormData.forEach((data) => { + const user = userData.find((user) => user.id === data.userId); + 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", + }; + + if (!townCouncilTopUsers[formDataWithUser.userTownCouncil]) { + townCouncilTopUsers[formDataWithUser.userTownCouncil] = []; + } + + townCouncilTopUsers[formDataWithUser.userTownCouncil].push( + formDataWithUser + ); + }); + + // Sort each town council's users by avgBill and pick the top 3 + const topUsersByTownCouncil: FormDataWithUser[] = []; + + 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 + const sortFormData = (list: FormData[], descriptor: SortDescriptor) => { + const { column } = descriptor; + + if (column === "avgBill") { + return [...list].sort((a, b) => a.avgBill - b.avgBill); + } + + return list; + }; + + const sortedFormData = sortFormData(filteredFormData, sortDescriptor); + + const combinedData: FormDataWithUser[] = sortedFormData.map((data) => { + const user = userData.find((user) => user.id === data.userId); + return { + ...data, + userName: user ? `${user.firstName} ${user.lastName}` : "Unknown User", + userEmail: user ? user.email : "Unknown Email", + userTownCouncil: user ? user.townCouncil : "Unknown Town Council", + }; + }); + + const [isImageModalOpen, setIsImageModalOpen] = useState(false); + const [modalImageUrl, setModalImageUrl] = useState(null); + + const handleImageClick = (imageUrl: string) => { + setModalImageUrl(imageUrl); + setIsImageModalOpen(true); + }; + + const handleEmailClick = (email: string, name: string) => { + setSelectedUser({ email, name }); + setIsEmailModalOpen(true); + }; + + const sendEmail = async () => { + try { + const response = await instance.post( + `${config.serverAddress}/hbcform/send-homebill-contest-email`, + { + email: selectedUser.email, + name: selectedUser.name, + } + ); + console.log(response.data.message); + setIsEmailModalOpen(false); + } catch (error) { + console.error("Failed to send email:", error); + } + }; + + const handleDeleteClick = (id: string) => { + setSelectedFormId(id); + setIsDeleteModalOpen(true); + }; + + const deleteForm = async () => { + if (selectedFormId === null) return; + try { + await instance.delete( + `${config.serverAddress}/hbcform/${selectedFormId}` + ); + setOriginalFormData( + originalFormData.filter((data) => data.id !== selectedFormId) + ); + setFilteredFormData( + filteredFormData.filter((data) => data.id !== selectedFormId) + ); + setSelectedFormId(null); + setIsDeleteModalOpen(false); + } catch (error) { + console.error("Failed to delete form entry:", error); + } + }; + + const fetchVouchers = async (): Promise => { + try { + const response = await instance.get(`${config.serverAddress}/vouchers`); + return response.data; + } catch (error) { + console.error("Failed to fetch vouchers:", error); + return []; + } + }; + + const assignVouchersToUsers = async (topUsers: FormDataWithUser[]) => { + try { + const vouchers = await fetchVouchers(); + if (vouchers.length === 0) { + console.warn("No vouchers available"); + return; + } + + const getRandomVouchers = (count: number): Voucher[] => { + const shuffled = vouchers.sort(() => 0.5 - Math.random()); + return shuffled.slice(0, count); + }; + + const townCouncilGroups: Record = {}; + topUsers.forEach((user) => { + if (!townCouncilGroups[user.userTownCouncil]) { + townCouncilGroups[user.userTownCouncil] = []; + } + townCouncilGroups[user.userTownCouncil].push(user); + }); + + for (const [townCouncil, users] of Object.entries(townCouncilGroups)) { + 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; + } else if (i === 1) { + voucherCount = 2; + } else if (i === 2) { + voucherCount = 1; + } + + const vouchersToAssign = getRandomVouchers(voucherCount); + + for (const voucher of vouchersToAssign) { + await instance.post(`${config.serverAddress}/user-vouchers`, { + userId: user.userId, + voucherId: voucher.id, + }); - // Fetch data on component mount - useEffect(() => { - const fetchData = async () => { try { - const formResponse = await instance.get(`${config.serverAddress}/hbcform`); - const processedFormData = formResponse.data.map((data) => ({ - ...data, - electricalBill: Number(data.electricalBill), - waterBill: Number(data.waterBill), - totalBill: Number(data.totalBill), - avgBill: Number(data.avgBill), - })); - setOriginalFormData(processedFormData); - setFilteredFormData(processedFormData); + const voucherId = voucher.id; + const response = await instance.put( + `${config.serverAddress}/vouchers/update-quantity/${voucherId}`, + { + quantityToSubtract: 1, + } + ); - const userResponse = await instance.get(`${config.serverAddress}/users/all`); - setUserData(userResponse.data); - - const townCouncilsResponse = await instance.get(`${config.serverAddress}/users/town-councils-metadata`); - setTownCouncils(JSON.parse(townCouncilsResponse.data).townCouncils); + if (response.status !== 200) { + console.error( + `Failed to update voucher quantity for voucherId: ${voucher.id}` + ); + } } catch (error) { - console.log("Failed to fetch data:", error); + console.error( + `Error updating voucher quantity for voucherId: ${voucher.id}`, + error + ); + throw new Error( + `Failed to update voucher quantity for voucherId: ${voucher.id}` + ); } - }; - - fetchData(); - }, []); - - // Filter form data based on selected town council - useEffect(() => { - const filtered = originalFormData.filter((data) => { - const user = userData.find((user) => user.id === data.userId); - return selectedTownCouncil ? user?.townCouncil === selectedTownCouncil : true; - }); - setFilteredFormData(filtered); - }, [selectedTownCouncil, originalFormData, userData]); - - // Compute top 3 users for each town council - useEffect(() => { - const townCouncilTopUsers: Record = {}; - - filteredFormData.forEach((data) => { - const user = userData.find((user) => user.id === data.userId); - 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", - }; - - if (!townCouncilTopUsers[formDataWithUser.userTownCouncil]) { - townCouncilTopUsers[formDataWithUser.userTownCouncil] = []; - } - - townCouncilTopUsers[formDataWithUser.userTownCouncil].push(formDataWithUser); - }); - - // Sort each town council's users by avgBill and pick the top 3 - const topUsersByTownCouncil: FormDataWithUser[] = []; - - 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 - const sortFormData = (list: FormData[], descriptor: SortDescriptor) => { - const { column } = descriptor; - - if (column === "avgBill") { - return [...list].sort((a, b) => a.avgBill - b.avgBill); + } } + } + console.log("Vouchers assigned successfully"); + } catch (error) { + console.error("Failed to assign vouchers:", error); + } + }; - return list; - }; + const handleGiveVouchers = async () => { + try { + await assignVouchersToUsers(top3Users); + console.log("Vouchers assigned successfully"); + } catch (error) { + console.error("Failed to give vouchers:", error); + } + }; - const sortedFormData = sortFormData(filteredFormData, sortDescriptor); + const handleTownCouncilChange = (value: string) => { + setSelectedTownCouncil(value); + }; - const combinedData: FormDataWithUser[] = sortedFormData.map((data) => { - const user = userData.find((user) => user.id === data.userId); - return { - ...data, - userName: user ? `${user.firstName} ${user.lastName}` : "Unknown User", - userEmail: user ? user.email : "Unknown Email", - userTownCouncil: user ? user.townCouncil : "Unknown Town Council", - }; - }); + const options = townCouncils.map((townCouncil) => ({ + key: townCouncil, // Use key instead of value + label: townCouncil, + })); - const [isImageModalOpen, setIsImageModalOpen] = useState(false); - const [modalImageUrl, setModalImageUrl] = useState(null); - - const handleImageClick = (imageUrl: string) => { - setModalImageUrl(imageUrl); - setIsImageModalOpen(true); - }; - - const handleEmailClick = (email: string, name: string) => { - setSelectedUser({ email, name }); - setIsEmailModalOpen(true); - }; - - const sendEmail = async () => { - try { - const response = await instance.post(`${config.serverAddress}/hbcform/send-homebill-contest-email`, { - email: selectedUser.email, - name: selectedUser.name, - }); - console.log(response.data.message); - setIsEmailModalOpen(false); - } catch (error) { - console.error("Failed to send email:", error); - } - }; - - const handleDeleteClick = (id: string) => { - setSelectedFormId(id); - setIsDeleteModalOpen(true); - }; - - const deleteForm = async () => { - if (selectedFormId === null) return; - try { - await instance.delete(`${config.serverAddress}/hbcform/${selectedFormId}`); - setOriginalFormData(originalFormData.filter((data) => data.id !== selectedFormId)); - setFilteredFormData(filteredFormData.filter((data) => data.id !== selectedFormId)); - setSelectedFormId(null); - setIsDeleteModalOpen(false); - } catch (error) { - console.error("Failed to delete form entry:", error); - } - }; - - const fetchVouchers = async (): Promise => { - try { - const response = await instance.get(`${config.serverAddress}/vouchers`); - return response.data; - } catch (error) { - console.error("Failed to fetch vouchers:", error); - return []; - } - }; - - const assignVouchersToUsers = async (topUsers: FormDataWithUser[]) => { - try { - const vouchers = await fetchVouchers(); - if (vouchers.length === 0) { - console.warn("No vouchers available"); - return; - } - - const getRandomVouchers = (count: number): Voucher[] => { - const shuffled = vouchers.sort(() => 0.5 - Math.random()); - return shuffled.slice(0, count); - }; - - const townCouncilGroups: Record = {}; - topUsers.forEach(user => { - if (!townCouncilGroups[user.userTownCouncil]) { - townCouncilGroups[user.userTownCouncil] = []; - } - townCouncilGroups[user.userTownCouncil].push(user); - }); - - for (const [townCouncil, users] of Object.entries(townCouncilGroups)) { - 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; - } else if (i === 1) { - voucherCount = 2; - } else if (i === 2) { - voucherCount = 1; - } - - const vouchersToAssign = getRandomVouchers(voucherCount); - - for (const voucher of vouchersToAssign) { - await instance.post(`${config.serverAddress}/user-vouchers`, { - userId: user.userId, - voucherId: voucher.id, - }); - - try { - const voucherId = voucher.id; - const response = await instance.put(`${config.serverAddress}/vouchers/update-quantity/${voucherId}`, { - quantityToSubtract: 1 - }); - - if (response.status !== 200) { - console.error(`Failed to update voucher quantity for voucherId: ${voucher.id}`); - } - } catch (error) { - console.error(`Error updating voucher quantity for voucherId: ${voucher.id}`, error); - throw new Error(`Failed to update voucher quantity for voucherId: ${voucher.id}`); - } - } - } - } - console.log("Vouchers assigned successfully"); - } catch (error) { - console.error("Failed to assign vouchers:", error); - } - }; - - - const handleGiveVouchers = async () => { - try { - await assignVouchersToUsers(top3Users); - console.log("Vouchers assigned successfully"); - } catch (error) { - console.error("Failed to give vouchers:", error); - } - }; - - const handleTownCouncilChange = (value: string) => { - setSelectedTownCouncil(value); - }; - - const options = townCouncils.map((townCouncil) => ({ - key: townCouncil, // Use key instead of value - label: townCouncil, - })); - - return ( -
-
-
-

Home Bill Contest Form Data

-
-
-
- -
-
- -
- -
-
- - - - Rank - User Name - Total Bill - Dependents - - Average Bill - - Bill Picture - Actions - - - - {combinedData.map((data, index) => ( - - {index + 1} - {data.userName} - ${data.totalBill.toFixed(2)} - {data.noOfDependents} - ${data.avgBill.toFixed(2)} - - {data.billPicture ? ( - - ) : ( - - )} - - - - - - - ))} - -
- - {/* Email Modal */} - setIsEmailModalOpen(false)}> - - Send Email - -

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

-
- - - - -
-
- - {/* Delete Modal */} - setIsDeleteModalOpen(false)}> - - Delete Entry - -

Are you sure you want to delete this entry?

-
- - - - -
-
- - {/* Image Modal */} - setIsImageModalOpen(false)}> - - - {modalImageUrl && Bill Image} - - - + return ( +
+
+
+

Home Bill Contest Form Data

- ); +
+
+ +
+
+ +
+
+
+ + + + Rank + User Name + Total Bill + Dependents + Average Bill + Bill Picture + Actions + + + + {combinedData.map((data, index) => ( + + {index + 1} + {data.userName} + ${data.totalBill.toFixed(2)} + {data.noOfDependents} + ${data.avgBill.toFixed(2)} + + + + + + + + + ))} + +
+ + {/* Email Modal */} + setIsEmailModalOpen(false)} + > + + Send Email + +

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

+
+ + + + +
+
+ + {/* Delete Modal */} + setIsDeleteModalOpen(false)} + > + + Delete Entry + +

Are you sure you want to delete this entry?

+
+ + + + +
+
+ + {/* Image Modal */} + setIsImageModalOpen(false)} + > + + + {modalImageUrl && Bill Image} + + + +
+ ); } diff --git a/server/routes/hbcform.js b/server/routes/hbcform.js index b1f4a2d..d59159b 100644 --- a/server/routes/hbcform.js +++ b/server/routes/hbcform.js @@ -116,6 +116,7 @@ router.get("/", async (req, res) => { let list = await HBCform.findAll({ where: condition, order: [["createdAt", "ASC"]], + attributes: { exclude: ["billPicture"] }, }); res.json(list); });