Claim voucher function

This commit is contained in:
ZacTohZY
2024-08-12 19:39:37 +08:00
parent 46b96542ee
commit 4429f48c40
4 changed files with 387 additions and 223 deletions

View File

@@ -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 { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import instance from "../security/http"; import instance from "../security/http";
import config from "../config"; import config from "../config";
import { InfoIcon, TrophyIcon } from "../icons"; import { InfoIcon, TrophyIcon } from "../icons";
import NextUIFormikSelect2 from "../components/NextUIFormikSelect2"; // Import the component
interface User { interface User {
id: string; id: string;
@@ -45,15 +53,21 @@ export default function HBContestPage() {
const fetchData = async () => { const fetchData = async () => {
try { try {
// Fetch form data // Fetch form data
const formDataResponse = await instance.get<CombinedData[]>(`${config.serverAddress}/hbcform`); const formDataResponse = await instance.get<CombinedData[]>(
`${config.serverAddress}/hbcform`
);
const processedFormData = formDataResponse.data; const processedFormData = formDataResponse.data;
// Fetch user information // Fetch user information
const userInfoResponse = await instance.get<User[]>(`${config.serverAddress}/users/all`); const userInfoResponse = await instance.get<User[]>(
`${config.serverAddress}/users/all`
);
// Combine form data with user information // Combine form data with user information
const combined = processedFormData.map((form) => { 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 { return {
userId: user ? user.id : "Unknown User", userId: user ? user.id : "Unknown User",
formId: form.userId, formId: form.userId,
@@ -70,7 +84,9 @@ export default function HBContestPage() {
setFilteredData(combined); setFilteredData(combined);
// Fetch town councils // 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); setTownCouncils(JSON.parse(townCouncilsResponse.data).townCouncils);
} catch (error) { } catch (error) {
console.log("Failed to fetch data"); console.log("Failed to fetch data");
@@ -91,18 +107,32 @@ export default function HBContestPage() {
const topUser = filteredData.length > 0 ? filteredData[0] : null; const topUser = filteredData.length > 0 ? filteredData[0] : null;
const top10Users = filteredData.slice(1, 10); 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 ( return (
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
<div className="h-[700px] flex flex-col items-center justify-center overflow-y-auto p-4"> <div className="h-[700px] flex flex-col items-center justify-center overflow-y-auto p-4">
<section className="bg-red-50 dark:bg-primary-950 border border-primary-100 p-10 rounded-xl w-full max-w-2xl flex flex-col items-center"> <section className="bg-red-50 dark:bg-primary-950 border border-primary-100 p-10 rounded-xl w-full max-w-2xl flex flex-col items-center">
<div className="w-full flex justify-end"> <div className="w-full flex justify-end">
<Tooltip content="Information"> <Tooltip content="Information">
<Button isIconOnly variant="light" onPress={() => setIsInfoModalOpen(true)}> <Button
isIconOnly
variant="light"
onPress={() => setIsInfoModalOpen(true)}
>
<InfoIcon /> <InfoIcon />
</Button> </Button>
</Tooltip> </Tooltip>
<Tooltip content="Leaderboard"> <Tooltip content="Leaderboard">
<Button isIconOnly variant="light" onPress={() => setIsModalOpen(true)}> <Button
isIconOnly
variant="light"
onPress={() => setIsModalOpen(true)}
>
<TrophyIcon /> <TrophyIcon />
</Button> </Button>
</Tooltip> </Tooltip>
@@ -118,7 +148,8 @@ export default function HBContestPage() {
This contest is to encourage residents to reduce the use of This contest is to encourage residents to reduce the use of
electricity and water usage. This contest would be won by the electricity and water usage. This contest would be won by the
person with the lowest overall bill average. Join us in this 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.{" "}
</p> </p>
</div> </div>
<div> <div>
@@ -132,9 +163,15 @@ export default function HBContestPage() {
random vouchers. random vouchers.
</p> </p>
<br></br> <br></br>
<p className="text-gray-700 dark:text-gray-300 font-bold">1st Place &rarr; 3 vouchers</p> <p className="text-gray-700 dark:text-gray-300 font-bold">
<p className="text-gray-700 dark:text-gray-300 font-bold">2nd Place &rarr; 2 vouchers</p> 1st Place &rarr; 3 vouchers
<p className="text-gray-700 dark:text-gray-300 font-bold">3rd Place &rarr; 1 voucher</p> </p>
<p className="text-gray-700 dark:text-gray-300 font-bold">
2nd Place &rarr; 2 vouchers
</p>
<p className="text-gray-700 dark:text-gray-300 font-bold">
3rd Place &rarr; 1 voucher
</p>
</div> </div>
<div className="flex justify-end"> <div className="flex justify-end">
<Button <Button
@@ -149,39 +186,46 @@ export default function HBContestPage() {
</section> </section>
</div> </div>
<Modal isOpen={isModalOpen} onOpenChange={setIsModalOpen} isDismissable={false} isKeyboardDismissDisabled={true}> <Modal
isOpen={isModalOpen}
onOpenChange={setIsModalOpen}
isDismissable={false}
isKeyboardDismissDisabled={true}
>
<ModalContent className="w-full max-w-[400px] relative"> <ModalContent className="w-full max-w-[400px] relative">
<ModalHeader className="flex justify-center items-center font-bold text-2xl text-red-900"> <ModalHeader className="flex">
<span>Leaderboard</span> <p className="text-3xl font-bold text-red-900">Leaderboard</p>
</ModalHeader> </ModalHeader>
<ModalBody className="pb-8"> <ModalBody className="pb-8">
<div className="mb-4"> <div className="mb-4">
{townCouncils.length > 0 && ( {townCouncilOptions.length > 0 && (
<select <NextUIFormikSelect2
value={selectedTownCouncil} label="Select Town Council"
onChange={(e) => setSelectedTownCouncil(e.target.value)} placeholder="Select a town council"
> options={[
<option value="">All locations</option> { key: "", label: "All locations" },
{townCouncils.map((townCouncil) => ( ...townCouncilOptions,
<option key={townCouncil} value={townCouncil}> ]}
{townCouncil} onChange={setSelectedTownCouncil} // Update selected town council
</option> />
))}
</select>
)} )}
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{topUser && ( {topUser && (
<div className="p-4 border rounded-md bg-red-100 dark:bg-primary-950 flex items-center"> <div className="p-4 border rounded-md bg-red-100 dark:bg-primary-950 flex items-center">
<TrophyIcon /> <TrophyIcon />
<p className="text-lg flex-1 text-center font-bold">{topUser.name}</p> <p className="text-lg flex-1 text-center font-bold">
{topUser.name}
</p>
</div> </div>
)} )}
</div> </div>
<div className="grid grid-rows-1 md:grid-rows-2 gap-2"> <div className="grid grid-rows-1 md:grid-rows-2 gap-2">
{top10Users.map((user, index) => ( {top10Users.map((user, index) => (
<div key={user.userId} className="p-4 border rounded-md bg-red-100 dark:bg-primary-950 flex items-center"> <div
key={user.userId}
className="p-4 border rounded-md bg-red-100 dark:bg-primary-950 flex items-center"
>
<span className="text-xl font-bold w-8">{index + 2}</span> <span className="text-xl font-bold w-8">{index + 2}</span>
<span className="flex-1 text-center">{user.name}</span> <span className="flex-1 text-center">{user.name}</span>
</div> </div>
@@ -200,7 +244,7 @@ export default function HBContestPage() {
> >
<ModalContent className="w-full max-w-[400px]"> <ModalContent className="w-full max-w-[400px]">
<ModalHeader className="flex justify-between items-center font-bold text-2xl text-red-900"> <ModalHeader className="flex justify-between items-center font-bold text-2xl text-red-900">
Information <p className="text-3xl font-bold text-red-900">Information</p>
</ModalHeader> </ModalHeader>
<ModalBody className="pb-8"> <ModalBody className="pb-8">
<div className="space-y-4 text-gray-700 dark:text-gray-300"> <div className="space-y-4 text-gray-700 dark:text-gray-300">

View File

@@ -99,29 +99,35 @@ export default function Ranking() {
setFilteredFormData(filtered); setFilteredFormData(filtered);
}, [selectedTownCouncil, originalFormData, userData]); }, [selectedTownCouncil, originalFormData, userData]);
// Compute top 3 users based on average bill // Compute top 3 users for each town council
useEffect(() => { useEffect(() => {
const combinedData: FormDataWithUser[] = filteredFormData.map((data) => { const townCouncilTopUsers: Record<string, FormDataWithUser[]> = {};
filteredFormData.forEach((data) => {
const user = userData.find((user) => user.id === data.userId); const user = userData.find((user) => user.id === data.userId);
return { const formDataWithUser: FormDataWithUser = {
...data, ...data,
userName: user ? `${user.firstName} ${user.lastName}` : "Unknown User", userName: user ? `${user.firstName} ${user.lastName}` : "Unknown User",
userEmail: user ? user.email : "Unknown Email", userEmail: user ? user.email : "Unknown Email",
userTownCouncil: user ? user.townCouncil : "Unknown Town Council", userTownCouncil: user ? user.townCouncil : "Unknown Town Council",
}; };
});
const townCouncilTopUsers: Record<string, FormDataWithUser> = {}; if (!townCouncilTopUsers[formDataWithUser.userTownCouncil]) {
combinedData.forEach((data) => { townCouncilTopUsers[formDataWithUser.userTownCouncil] = [];
if (!townCouncilTopUsers[data.userTownCouncil] || data.avgBill > townCouncilTopUsers[data.userTownCouncil].avgBill) {
townCouncilTopUsers[data.userTownCouncil] = data;
} }
townCouncilTopUsers[formDataWithUser.userTownCouncil].push(formDataWithUser);
}); });
const topUsers = Object.values(townCouncilTopUsers); // Sort each town council's users by avgBill and pick the top 3
const sortedTopUsers = topUsers.sort((a, b) => b.avgBill - a.avgBill).slice(0, 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]); }, [filteredFormData, userData]);
// Sort form data based on descriptor // Sort form data based on descriptor
@@ -209,15 +215,46 @@ export default function Ranking() {
return; 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) { // Group users by town council
const voucher = randomVoucher(vouchers); const townCouncilGroups: Record<string, FormDataWithUser[]> = {};
if (voucher) { topUsers.forEach(user => {
await instance.post(`${config.serverAddress}/user-vouchers`, { if (!townCouncilGroups[user.userTownCouncil]) {
userId: user.userId, townCouncilGroups[user.userTownCouncil] = [];
voucherId: voucher.id, }
}); 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) { } catch (error) {
@@ -225,6 +262,8 @@ export default function Ranking() {
} }
}; };
const handleGiveVouchers = async () => { const handleGiveVouchers = async () => {
try { try {
await assignVouchersToUsers(top3Users); await assignVouchersToUsers(top3Users);
@@ -238,6 +277,12 @@ export default function Ranking() {
setSelectedTownCouncil(value); setSelectedTownCouncil(value);
}; };
const options = townCouncils.map((townCouncil) => ({
key: townCouncil, // Use key instead of value
label: townCouncil,
}));
return ( return (
<div className="flex flex-col gap-8 p-8"> <div className="flex flex-col gap-8 p-8">
<div className="flex justify-between items-center gap-5"> <div className="flex justify-between items-center gap-5">
@@ -246,129 +291,108 @@ export default function Ranking() {
</div> </div>
<div className="flex flex-row gap-4 "> <div className="flex flex-row gap-4 ">
<div className="w-[200px]"> <div className="w-[200px]">
{townCouncils.length > 0 && ( <NextUIFormikSelect2
<NextUIFormikSelect2 label="Town Council"
label="Town council" placeholder="Select a Town Council"
placeholder="Choose towncouncil" options={options}
options={townCouncils.map((townCouncil) => ({ onChange={handleTownCouncilChange}
key: townCouncil, />
label: townCouncil,
}))}
onChange={handleTownCouncilChange}
/>
)}
</div> </div>
<div className="w-[130px]"> <div className="w-[130px]">
{top3Users.length > 0 && ( <Button
<Button color="primary" onPress={handleGiveVouchers} className="w-full"> color="primary"
Give Vouchers onPress={handleGiveVouchers}
</Button> className="w-full"
)} isDisabled={!!selectedTownCouncil}
>
Give Vouchers
</Button>
</div> </div>
</div> </div>
</div> </div>
<div> <Table aria-label="Form Data Table">
<Table aria-label="Form Data Table"> <TableHeader>
<TableHeader> <TableColumn>Rank</TableColumn>
<TableColumn>User Name</TableColumn> <TableColumn>User Name</TableColumn>
<TableColumn>Electrical Bill</TableColumn> <TableColumn>Electrical Bill</TableColumn>
<TableColumn>Water Bill</TableColumn> <TableColumn>Water Bill</TableColumn>
<TableColumn>Total Bill</TableColumn> <TableColumn>Total Bill</TableColumn>
<TableColumn>Dependents</TableColumn> <TableColumn>Dependents</TableColumn>
<TableColumn> <TableColumn>
Average Bill Average Bill
</TableColumn> </TableColumn>
<TableColumn>Bill Picture</TableColumn> <TableColumn>Bill Picture</TableColumn>
<TableColumn>Actions</TableColumn> <TableColumn>Actions</TableColumn>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{combinedData.map((data) => ( {combinedData.map((data, index) => (
<TableRow key={data.id}> <TableRow key={data.id}>
<TableCell>{data.userName}</TableCell> <TableCell>{index + 1}</TableCell>
<TableCell>${data.electricalBill.toFixed(2)}</TableCell> <TableCell>{data.userName}</TableCell>
<TableCell>${data.waterBill.toFixed(2)}</TableCell> <TableCell>${data.electricalBill.toFixed(2)}</TableCell>
<TableCell>${data.totalBill.toFixed(2)}</TableCell> <TableCell>${data.waterBill.toFixed(2)}</TableCell>
<TableCell>{data.noOfDependents}</TableCell> <TableCell>${data.totalBill.toFixed(2)}</TableCell>
<TableCell>${data.avgBill.toFixed(2)}</TableCell> <TableCell>{data.noOfDependents}</TableCell>
<TableCell> <TableCell>${data.avgBill.toFixed(2)}</TableCell>
{data.billPicture ? ( <TableCell>
<Button isIconOnly variant="light" onPress={() => handleImageClick(`${config.serverAddress}/hbcform/billPicture/${data.id}`)}> {data.billPicture ? (
<ImageIcon /> <Button isIconOnly variant="light" onPress={() => handleImageClick(`${config.serverAddress}/hbcform/billPicture/${data.id}`)}>
</Button> <ImageIcon />
) : ( </Button>
<Button isIconOnly variant="light"> ) : (
<ImageIcon /> <Button isIconOnly variant="light">
</Button> <ImageIcon />
)} </Button>
</TableCell> )}
</TableCell>
<TableCell className="flex flex-row">
<Button isIconOnly variant="light" className="text-blue-500" onClick={() => handleEmailClick(data.userEmail, data.userName)}><EmailIcon /></Button>
<Button isIconOnly variant="light" color="danger" onClick={() => handleDeleteClick(data.id)}><TrashDeleteIcon /></Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<TableCell className="flex flex-row"> {/* Email Modal */}
<Button isIconOnly variant="light" className="text-blue-500" onClick={() => handleEmailClick(data.userEmail, data.userName)}><EmailIcon /></Button> <Modal isOpen={isEmailModalOpen} onClose={() => setIsEmailModalOpen(false)}>
<Button isIconOnly variant="light" color="danger" onClick={() => handleDeleteClick(data.id)}><TrashDeleteIcon /></Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{/* Email Confirmation Modal */}
<Modal isOpen={isEmailModalOpen} onOpenChange={setIsEmailModalOpen} isDismissable={false} isKeyboardDismissDisabled={true}>
<ModalContent> <ModalContent>
{(onClose) => ( <ModalHeader>Send Email</ModalHeader>
<> <ModalBody>
<ModalHeader className="flex flex-col gap-1">Send Email</ModalHeader> <p>Are you sure you want to send an email to {selectedUser.name} ({selectedUser.email})?</p>
<ModalBody> </ModalBody>
<p>Are you sure you want to send an email to {selectedUser.name} ({selectedUser.email})?</p> <ModalFooter>
</ModalBody> <Button color="primary" onClick={sendEmail}>Send</Button>
<ModalFooter> <Button color="secondary" onClick={() => setIsEmailModalOpen(false)}>Cancel</Button>
<Button color="danger" variant="light" onPress={onClose}> </ModalFooter>
Close
</Button>
<Button color="primary" onPress={() => { sendEmail(); onClose(); }}>
Send
</Button>
</ModalFooter>
</>
)}
</ModalContent> </ModalContent>
</Modal> </Modal>
{/* Delete Confirmation Modal */}
<Modal isOpen={isDeleteModalOpen} onOpenChange={setIsDeleteModalOpen} isDismissable={false} isKeyboardDismissDisabled={true}> {/* Delete Modal */}
<Modal isOpen={isDeleteModalOpen} onClose={() => setIsDeleteModalOpen(false)}>
<ModalContent> <ModalContent>
{(onClose) => ( <ModalHeader>Delete Entry</ModalHeader>
<> <ModalBody>
<ModalHeader className="flex flex-col gap-1">Delete Entry</ModalHeader> <p>Are you sure you want to delete this entry?</p>
<ModalBody> </ModalBody>
<p>Are you sure you want to delete this form entry?</p> <ModalFooter>
</ModalBody> <Button color="danger" onClick={deleteForm}>Delete</Button>
<ModalFooter> <Button color="secondary" onClick={() => setIsDeleteModalOpen(false)}>Cancel</Button>
<Button color="danger" variant="light" onPress={onClose}> </ModalFooter>
Close
</Button>
<Button color="danger" onPress={() => { deleteForm(); onClose(); }}>
Delete
</Button>
</ModalFooter>
</>
)}
</ModalContent> </ModalContent>
</Modal> </Modal>
{/* Open Image Modal */}
<Modal {/* Image Modal */}
isOpen={isImageModalOpen} <Modal isOpen={isImageModalOpen} onClose={() => setIsImageModalOpen(false)}>
onOpenChange={setIsImageModalOpen}
isDismissable={true}
>
<ModalContent> <ModalContent>
<ModalBody> <ModalBody>
{modalImageUrl && ( {modalImageUrl && <img src={modalImageUrl} alt="Bill Image" />}
<img src={modalImageUrl} alt="Bill Picture" style={{ width: '100%', height: 'auto' }} />
)}
</ModalBody> </ModalBody>
</ModalContent> </ModalContent>
</Modal> </Modal>
</div > </div>
); );
} }

View File

@@ -1,9 +1,19 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from "react";
import { useNavigate } from 'react-router-dom'; import { useNavigate } from "react-router-dom";
import instance from '../security/http'; import instance from "../security/http";
import config from '../config'; import config from "../config";
import { retrieveUserInformation } from '../security/users'; import { retrieveUserInformation } from "../security/users";
import { Card, CardHeader, CardBody, CardFooter, Divider, Image } from '@nextui-org/react'; import {
Card,
CardBody,
Image,
Button,
Modal,
ModalContent,
ModalHeader,
ModalBody,
} from "@nextui-org/react";
import { VoucherIcon } from "../icons";
interface Voucher { interface Voucher {
id: string; id: string;
@@ -30,6 +40,10 @@ export default function UserVoucherPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const [userInformation, setUserInformation] = useState<any>(null); const [userInformation, setUserInformation] = useState<any>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedVoucher, setSelectedVoucher] = useState<Voucher | null>(null);
const [selectedUserVoucherId, setSelectedUserVoucherId] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
const fetchUserInformation = async () => { const fetchUserInformation = async () => {
try { try {
@@ -47,12 +61,12 @@ export default function UserVoucherPage() {
const fetchUserVouchers = async () => { const fetchUserVouchers = async () => {
try { try {
if (userInformation && userInformation.id) { if (userInformation && userInformation.id) {
// Fetch user vouchers const response = await instance.get(
const response = await instance.get(`${config.serverAddress}/user-vouchers/user/${userInformation.id}`); `${config.serverAddress}/user-vouchers/user/${userInformation.id}`
);
const fetchedUserVouchers = response.data.userVouchers; const fetchedUserVouchers = response.data.userVouchers;
setUserVouchers(fetchedUserVouchers); setUserVouchers(fetchedUserVouchers);
// Fetch voucher details
const voucherIds = response.data.voucherIds; const voucherIds = response.data.voucherIds;
const voucherDetailsPromises = voucherIds.map((voucherId: string) => const voucherDetailsPromises = voucherIds.map((voucherId: string) =>
instance.get(`${config.serverAddress}/vouchers/${voucherId}`) instance.get(`${config.serverAddress}/vouchers/${voucherId}`)
@@ -60,26 +74,32 @@ export default function UserVoucherPage() {
const voucherResponses = await Promise.all(voucherDetailsPromises); const voucherResponses = await Promise.all(voucherDetailsPromises);
const voucherMap = new Map<string, Voucher>(); const voucherMap = new Map<string, Voucher>();
voucherResponses.forEach(response => { voucherResponses.forEach((response) => {
const voucher = response.data; const voucher = response.data;
voucherMap.set(voucher.id, voucher); voucherMap.set(voucher.id, voucher);
// Fetch brand logos
if (voucher.brandLogo) { if (voucher.brandLogo) {
instance instance
.get(`${config.serverAddress}/vouchers/brandLogo/${voucher.id}`, { responseType: 'blob' }) .get(`${config.serverAddress}/vouchers/brandLogo/${voucher.id}`, {
responseType: "blob",
})
.then((res) => { .then((res) => {
const url = URL.createObjectURL(res.data); 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); setVouchers(voucherMap);
} }
} catch (error) { } catch (error) {
setError('Failed to fetch vouchers'); setError("Failed to fetch vouchers");
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -90,64 +110,123 @@ export default function UserVoucherPage() {
} }
}, [userInformation]); }, [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 ( return (
<div> <div className="flex justify-center mt-10">
{loading ? ( <div className="flex flex-col">
<p>Loading...</p> <p className="text-4xl font-bold mb-4">Vouchers</p>
) : error ? ( <div className=" bg-red-50 dark:bg-primary-950 border border-primary-100 p-10 rounded-xl ">
<p>{error}</p> {loading ? (
) : userVouchers.length === 0 ? ( <p>Loading...</p>
<p>You have no vouchers currently.</p> ) : error ? (
) : ( <p>{error}</p>
<div> ) : userVouchers.length === 0 ? (
<h2>Your Vouchers</h2> <p>You have no vouchers currently.</p>
<div className="flex flex-wrap gap-4"> ) : (
{userVouchers.map((userVoucher) => { <div className="flex flex-wrap gap-4">
const voucher = vouchers.get(userVoucher.voucherId); {userVouchers.map((userVoucher) => {
return ( const voucher = vouchers.get(userVoucher.voucherId);
<Card key={userVoucher.id} className="max-w-xs"> return (
<CardHeader> <Card key={userVoucher.id} className="max-w-[500px]">
{voucher ? ( <CardBody className="flex flex-row items-center gap-5">
<Image <Image
alt={voucher.brand} alt={voucher?.brand || "No image available"}
height={100} height={90}
width={100} width={90}
src={brandLogoUrls[voucher.id] || '/default-logo.png'} src={
style={{ objectFit: 'cover' }} // Use style prop for objectFit brandLogoUrls[voucher?.id || ""] || "/default-logo.png"
}
style={{ objectFit: "cover" }}
className="flex-shrink-0 mr-4"
/> />
) : ( <div className="flex flex-col">
<Image {voucher ? (
alt="No image available" <>
height={100} <p className="font-bold">{voucher.brand}</p>
width={100} <p>{voucher.description}</p>
src='/default-logo.png' <p>
style={{ objectFit: 'cover' }} // Use style prop for objectFit Expires on:{" "}
/> {new Date(
)} voucher.expirationDate
</CardHeader> ).toLocaleDateString()}
<Divider /> </p>
<CardBody> </>
{voucher ? ( ) : (
<> <p>Voucher details are unavailable.</p>
<p>{voucher.brand}</p> )}
<p>{voucher.description}</p> </div>
<p>Code: {voucher.code}</p> <div>
<p>Expires on: {new Date(voucher.expirationDate).toLocaleDateString()}</p> <Button
</> isIconOnly
) : ( variant="light"
<p>Voucher details are unavailable.</p> onClick={() => voucher && handleVoucherButtonClick(voucher, userVoucher.id)}
)} >
</CardBody> <VoucherIcon />
<Divider /> </Button>
<CardFooter> </div>
{/* Add any additional footer content here */} </CardBody>
</CardFooter> </Card>
</Card> );
); })}
})} </div>
</div> )}
</div> </div>
)} </div>
{/* Modal for using voucher */}
<Modal isOpen={isModalOpen} onOpenChange={setIsModalOpen} isDismissable={true}>
<ModalContent className="w-full max-w-[400px]">
<ModalHeader className="flex justify-between items-center font-bold text-2xl text-red-900">
<p className="text-3xl font-bold text-red-900">Use Voucher</p>
</ModalHeader>
<ModalBody className="pb-8">
<div className="space-y-4 text-gray-700 dark:text-gray-300">
<p className="font-semibold">Do you want to use this voucher?</p>
{selectedVoucher && (
<>
<p className="font-semibold">Brand: {selectedVoucher.brand}</p>
<p className="font-semibold">Code: {selectedVoucher.code}</p>
</>
)}
</div>
</ModalBody>
<div className="flex justify-end p-4 gap-4">
<Button color="danger" variant="light" onClick={handleCancel}>Cancel</Button>
<Button color="danger" onClick={handleConfirm}>Yes</Button>
</div>
</ModalContent>
</Modal>
</div> </div>
); );
} }

View File

@@ -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; module.exports = router;