Claim voucher function
This commit is contained in:
@@ -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<CombinedData[]>(`${config.serverAddress}/hbcform`);
|
||||
const formDataResponse = await instance.get<CombinedData[]>(
|
||||
`${config.serverAddress}/hbcform`
|
||||
);
|
||||
const processedFormData = formDataResponse.data;
|
||||
|
||||
// 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
|
||||
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 (
|
||||
<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">
|
||||
<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">
|
||||
<Tooltip content="Information">
|
||||
<Button isIconOnly variant="light" onPress={() => setIsInfoModalOpen(true)}>
|
||||
<Button
|
||||
isIconOnly
|
||||
variant="light"
|
||||
onPress={() => setIsInfoModalOpen(true)}
|
||||
>
|
||||
<InfoIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip content="Leaderboard">
|
||||
<Button isIconOnly variant="light" onPress={() => setIsModalOpen(true)}>
|
||||
<Button
|
||||
isIconOnly
|
||||
variant="light"
|
||||
onPress={() => setIsModalOpen(true)}
|
||||
>
|
||||
<TrophyIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
@@ -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.{" "}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
@@ -132,9 +163,15 @@ export default function HBContestPage() {
|
||||
random vouchers.
|
||||
</p>
|
||||
<br></br>
|
||||
<p className="text-gray-700 dark:text-gray-300 font-bold">1st Place → 3 vouchers</p>
|
||||
<p className="text-gray-700 dark:text-gray-300 font-bold">2nd Place → 2 vouchers</p>
|
||||
<p className="text-gray-700 dark:text-gray-300 font-bold">3rd Place → 1 voucher</p>
|
||||
<p className="text-gray-700 dark:text-gray-300 font-bold">
|
||||
1st Place → 3 vouchers
|
||||
</p>
|
||||
<p className="text-gray-700 dark:text-gray-300 font-bold">
|
||||
2nd Place → 2 vouchers
|
||||
</p>
|
||||
<p className="text-gray-700 dark:text-gray-300 font-bold">
|
||||
3rd Place → 1 voucher
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
@@ -149,39 +186,46 @@ export default function HBContestPage() {
|
||||
</section>
|
||||
</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">
|
||||
<ModalHeader className="flex justify-center items-center font-bold text-2xl text-red-900">
|
||||
<span>Leaderboard</span>
|
||||
<ModalHeader className="flex">
|
||||
<p className="text-3xl font-bold text-red-900">Leaderboard</p>
|
||||
</ModalHeader>
|
||||
<ModalBody className="pb-8">
|
||||
<div className="mb-4">
|
||||
{townCouncils.length > 0 && (
|
||||
<select
|
||||
value={selectedTownCouncil}
|
||||
onChange={(e) => setSelectedTownCouncil(e.target.value)}
|
||||
>
|
||||
<option value="">All locations</option>
|
||||
{townCouncils.map((townCouncil) => (
|
||||
<option key={townCouncil} value={townCouncil}>
|
||||
{townCouncil}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{townCouncilOptions.length > 0 && (
|
||||
<NextUIFormikSelect2
|
||||
label="Select Town Council"
|
||||
placeholder="Select a town council"
|
||||
options={[
|
||||
{ key: "", label: "All locations" },
|
||||
...townCouncilOptions,
|
||||
]}
|
||||
onChange={setSelectedTownCouncil} // Update selected town council
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{topUser && (
|
||||
<div className="p-4 border rounded-md bg-red-100 dark:bg-primary-950 flex items-center">
|
||||
<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 className="grid grid-rows-1 md:grid-rows-2 gap-2">
|
||||
{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="flex-1 text-center">{user.name}</span>
|
||||
</div>
|
||||
@@ -200,7 +244,7 @@ export default function HBContestPage() {
|
||||
>
|
||||
<ModalContent className="w-full max-w-[400px]">
|
||||
<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>
|
||||
<ModalBody className="pb-8">
|
||||
<div className="space-y-4 text-gray-700 dark:text-gray-300">
|
||||
|
||||
@@ -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<string, FormDataWithUser[]> = {};
|
||||
|
||||
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<string, FormDataWithUser> = {};
|
||||
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<string, FormDataWithUser[]> = {};
|
||||
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 (
|
||||
<div className="flex flex-col gap-8 p-8">
|
||||
<div className="flex justify-between items-center gap-5">
|
||||
@@ -246,129 +291,108 @@ export default function Ranking() {
|
||||
</div>
|
||||
<div className="flex flex-row gap-4 ">
|
||||
<div className="w-[200px]">
|
||||
{townCouncils.length > 0 && (
|
||||
<NextUIFormikSelect2
|
||||
label="Town council"
|
||||
placeholder="Choose towncouncil"
|
||||
options={townCouncils.map((townCouncil) => ({
|
||||
key: townCouncil,
|
||||
label: townCouncil,
|
||||
}))}
|
||||
onChange={handleTownCouncilChange}
|
||||
/>
|
||||
)}
|
||||
<NextUIFormikSelect2
|
||||
label="Town Council"
|
||||
placeholder="Select a Town Council"
|
||||
options={options}
|
||||
onChange={handleTownCouncilChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-[130px]">
|
||||
{top3Users.length > 0 && (
|
||||
<Button color="primary" onPress={handleGiveVouchers} className="w-full">
|
||||
Give Vouchers
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
color="primary"
|
||||
onPress={handleGiveVouchers}
|
||||
className="w-full"
|
||||
isDisabled={!!selectedTownCouncil}
|
||||
>
|
||||
Give Vouchers
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Table aria-label="Form Data Table">
|
||||
<TableHeader>
|
||||
<TableColumn>User Name</TableColumn>
|
||||
<TableColumn>Electrical Bill</TableColumn>
|
||||
<TableColumn>Water Bill</TableColumn>
|
||||
<TableColumn>Total Bill</TableColumn>
|
||||
<TableColumn>Dependents</TableColumn>
|
||||
<TableColumn>
|
||||
Average Bill
|
||||
</TableColumn>
|
||||
<TableColumn>Bill Picture</TableColumn>
|
||||
<TableColumn>Actions</TableColumn>
|
||||
</TableHeader>
|
||||
<Table aria-label="Form Data Table">
|
||||
<TableHeader>
|
||||
<TableColumn>Rank</TableColumn>
|
||||
<TableColumn>User Name</TableColumn>
|
||||
<TableColumn>Electrical Bill</TableColumn>
|
||||
<TableColumn>Water Bill</TableColumn>
|
||||
<TableColumn>Total Bill</TableColumn>
|
||||
<TableColumn>Dependents</TableColumn>
|
||||
<TableColumn>
|
||||
Average Bill
|
||||
</TableColumn>
|
||||
<TableColumn>Bill Picture</TableColumn>
|
||||
<TableColumn>Actions</TableColumn>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{combinedData.map((data) => (
|
||||
<TableRow key={data.id}>
|
||||
<TableCell>{data.userName}</TableCell>
|
||||
<TableCell>${data.electricalBill.toFixed(2)}</TableCell>
|
||||
<TableCell>${data.waterBill.toFixed(2)}</TableCell>
|
||||
<TableCell>${data.totalBill.toFixed(2)}</TableCell>
|
||||
<TableCell>{data.noOfDependents}</TableCell>
|
||||
<TableCell>${data.avgBill.toFixed(2)}</TableCell>
|
||||
<TableCell>
|
||||
{data.billPicture ? (
|
||||
<Button isIconOnly variant="light" onPress={() => handleImageClick(`${config.serverAddress}/hbcform/billPicture/${data.id}`)}>
|
||||
<ImageIcon />
|
||||
</Button>
|
||||
) : (
|
||||
<Button isIconOnly variant="light">
|
||||
<ImageIcon />
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableBody>
|
||||
{combinedData.map((data, index) => (
|
||||
<TableRow key={data.id}>
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
<TableCell>{data.userName}</TableCell>
|
||||
<TableCell>${data.electricalBill.toFixed(2)}</TableCell>
|
||||
<TableCell>${data.waterBill.toFixed(2)}</TableCell>
|
||||
<TableCell>${data.totalBill.toFixed(2)}</TableCell>
|
||||
<TableCell>{data.noOfDependents}</TableCell>
|
||||
<TableCell>${data.avgBill.toFixed(2)}</TableCell>
|
||||
<TableCell>
|
||||
{data.billPicture ? (
|
||||
<Button isIconOnly variant="light" onPress={() => handleImageClick(`${config.serverAddress}/hbcform/billPicture/${data.id}`)}>
|
||||
<ImageIcon />
|
||||
</Button>
|
||||
) : (
|
||||
<Button isIconOnly variant="light">
|
||||
<ImageIcon />
|
||||
</Button>
|
||||
)}
|
||||
</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">
|
||||
<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>
|
||||
</div>
|
||||
{/* Email Confirmation Modal */}
|
||||
<Modal isOpen={isEmailModalOpen} onOpenChange={setIsEmailModalOpen} isDismissable={false} isKeyboardDismissDisabled={true}>
|
||||
{/* Email Modal */}
|
||||
<Modal isOpen={isEmailModalOpen} onClose={() => setIsEmailModalOpen(false)}>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader className="flex flex-col gap-1">Send Email</ModalHeader>
|
||||
<ModalBody>
|
||||
<p>Are you sure you want to send an email to {selectedUser.name} ({selectedUser.email})?</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="danger" variant="light" onPress={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
<Button color="primary" onPress={() => { sendEmail(); onClose(); }}>
|
||||
Send
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
<ModalHeader>Send Email</ModalHeader>
|
||||
<ModalBody>
|
||||
<p>Are you sure you want to send an email to {selectedUser.name} ({selectedUser.email})?</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary" onClick={sendEmail}>Send</Button>
|
||||
<Button color="secondary" onClick={() => setIsEmailModalOpen(false)}>Cancel</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{/* Delete Confirmation Modal */}
|
||||
<Modal isOpen={isDeleteModalOpen} onOpenChange={setIsDeleteModalOpen} isDismissable={false} isKeyboardDismissDisabled={true}>
|
||||
|
||||
{/* Delete Modal */}
|
||||
<Modal isOpen={isDeleteModalOpen} onClose={() => setIsDeleteModalOpen(false)}>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader className="flex flex-col gap-1">Delete Entry</ModalHeader>
|
||||
<ModalBody>
|
||||
<p>Are you sure you want to delete this form entry?</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="danger" variant="light" onPress={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
<Button color="danger" onPress={() => { deleteForm(); onClose(); }}>
|
||||
Delete
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
<ModalHeader>Delete Entry</ModalHeader>
|
||||
<ModalBody>
|
||||
<p>Are you sure you want to delete this entry?</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="danger" onClick={deleteForm}>Delete</Button>
|
||||
<Button color="secondary" onClick={() => setIsDeleteModalOpen(false)}>Cancel</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{/* Open Image Modal */}
|
||||
<Modal
|
||||
isOpen={isImageModalOpen}
|
||||
onOpenChange={setIsImageModalOpen}
|
||||
isDismissable={true}
|
||||
>
|
||||
|
||||
{/* Image Modal */}
|
||||
<Modal isOpen={isImageModalOpen} onClose={() => setIsImageModalOpen(false)}>
|
||||
<ModalContent>
|
||||
<ModalBody>
|
||||
{modalImageUrl && (
|
||||
<img src={modalImageUrl} alt="Bill Picture" style={{ width: '100%', height: 'auto' }} />
|
||||
)}
|
||||
{modalImageUrl && <img src={modalImageUrl} alt="Bill Image" />}
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div >
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<any>(null);
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [selectedVoucher, setSelectedVoucher] = useState<Voucher | null>(null);
|
||||
const [selectedUserVoucherId, setSelectedUserVoucherId] = useState<string | null>(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<string, Voucher>();
|
||||
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 (
|
||||
<div>
|
||||
{loading ? (
|
||||
<p>Loading...</p>
|
||||
) : error ? (
|
||||
<p>{error}</p>
|
||||
) : userVouchers.length === 0 ? (
|
||||
<p>You have no vouchers currently.</p>
|
||||
) : (
|
||||
<div>
|
||||
<h2>Your Vouchers</h2>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{userVouchers.map((userVoucher) => {
|
||||
const voucher = vouchers.get(userVoucher.voucherId);
|
||||
return (
|
||||
<Card key={userVoucher.id} className="max-w-xs">
|
||||
<CardHeader>
|
||||
{voucher ? (
|
||||
<div className="flex justify-center mt-10">
|
||||
<div className="flex flex-col">
|
||||
<p className="text-4xl font-bold mb-4">Vouchers</p>
|
||||
<div className=" bg-red-50 dark:bg-primary-950 border border-primary-100 p-10 rounded-xl ">
|
||||
{loading ? (
|
||||
<p>Loading...</p>
|
||||
) : error ? (
|
||||
<p>{error}</p>
|
||||
) : userVouchers.length === 0 ? (
|
||||
<p>You have no vouchers currently.</p>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{userVouchers.map((userVoucher) => {
|
||||
const voucher = vouchers.get(userVoucher.voucherId);
|
||||
return (
|
||||
<Card key={userVoucher.id} className="max-w-[500px]">
|
||||
<CardBody className="flex flex-row items-center gap-5">
|
||||
<Image
|
||||
alt={voucher.brand}
|
||||
height={100}
|
||||
width={100}
|
||||
src={brandLogoUrls[voucher.id] || '/default-logo.png'}
|
||||
style={{ objectFit: 'cover' }} // Use style prop for objectFit
|
||||
alt={voucher?.brand || "No image available"}
|
||||
height={90}
|
||||
width={90}
|
||||
src={
|
||||
brandLogoUrls[voucher?.id || ""] || "/default-logo.png"
|
||||
}
|
||||
style={{ objectFit: "cover" }}
|
||||
className="flex-shrink-0 mr-4"
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
alt="No image available"
|
||||
height={100}
|
||||
width={100}
|
||||
src='/default-logo.png'
|
||||
style={{ objectFit: 'cover' }} // Use style prop for objectFit
|
||||
/>
|
||||
)}
|
||||
</CardHeader>
|
||||
<Divider />
|
||||
<CardBody>
|
||||
{voucher ? (
|
||||
<>
|
||||
<p>{voucher.brand}</p>
|
||||
<p>{voucher.description}</p>
|
||||
<p>Code: {voucher.code}</p>
|
||||
<p>Expires on: {new Date(voucher.expirationDate).toLocaleDateString()}</p>
|
||||
</>
|
||||
) : (
|
||||
<p>Voucher details are unavailable.</p>
|
||||
)}
|
||||
</CardBody>
|
||||
<Divider />
|
||||
<CardFooter>
|
||||
{/* Add any additional footer content here */}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
{voucher ? (
|
||||
<>
|
||||
<p className="font-bold">{voucher.brand}</p>
|
||||
<p>{voucher.description}</p>
|
||||
<p>
|
||||
Expires on:{" "}
|
||||
{new Date(
|
||||
voucher.expirationDate
|
||||
).toLocaleDateString()}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p>Voucher details are unavailable.</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
isIconOnly
|
||||
variant="light"
|
||||
onClick={() => voucher && handleVoucherButtonClick(voucher, userVoucher.id)}
|
||||
>
|
||||
<VoucherIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user