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 { 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 → 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 → 2 vouchers</p>
|
1st Place → 3 vouchers
|
||||||
<p className="text-gray-700 dark:text-gray-300 font-bold">3rd Place → 1 voucher</p>
|
</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>
|
||||||
<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">
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user