Voucher given to user
I am going to fix all design now
This commit is contained in:
@@ -34,6 +34,7 @@ import ManageVoucherPage from "./pages/ManageVoucherPage";
|
|||||||
import CreateVoucherPage from "./pages/CreateVoucherPage";
|
import CreateVoucherPage from "./pages/CreateVoucherPage";
|
||||||
import EditVoucherPage from "./pages/EditVoucherPage";
|
import EditVoucherPage from "./pages/EditVoucherPage";
|
||||||
import FeedbackPage from "./pages/FeedbackPage";
|
import FeedbackPage from "./pages/FeedbackPage";
|
||||||
|
import UserVouchersPage from "./pages/UserVouchersPage";
|
||||||
import ManageFeedbacksPage from "./pages/ManageFeedbacksPage";
|
import ManageFeedbacksPage from "./pages/ManageFeedbacksPage";
|
||||||
import TagManagement from "./pages/TagManagement";
|
import TagManagement from "./pages/TagManagement";
|
||||||
|
|
||||||
@@ -79,6 +80,8 @@ function App() {
|
|||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
<Route element={<UserVouchersPage />} path="user-voucher" />
|
||||||
|
|
||||||
{/* Special (Restricted) Routes */}
|
{/* Special (Restricted) Routes */}
|
||||||
<Route element={<RestrictedLayout />}>
|
<Route element={<RestrictedLayout />}>
|
||||||
<Route element={<ForgotPasswordPage />} path="forgot-password" />
|
<Route element={<ForgotPasswordPage />} path="forgot-password" />
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
ArrowRightStartOnRectangleIcon,
|
ArrowRightStartOnRectangleIcon,
|
||||||
PencilSquareIcon,
|
PencilSquareIcon,
|
||||||
RocketLaunchIcon,
|
RocketLaunchIcon,
|
||||||
|
VoucherIcon,
|
||||||
} from "../icons";
|
} from "../icons";
|
||||||
|
|
||||||
import config from "../config";
|
import config from "../config";
|
||||||
@@ -155,6 +156,14 @@ export default function NavigationBar() {
|
|||||||
navigate("/manage-account");
|
navigate("/manage-account");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<DropdownItem
|
||||||
|
key="user-voucher"
|
||||||
|
title="Voucher"
|
||||||
|
startContent={<VoucherIcon />}
|
||||||
|
onPress={() => {
|
||||||
|
navigate("/user-voucher");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</DropdownSection>
|
</DropdownSection>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key="signout"
|
key="signout"
|
||||||
|
|||||||
@@ -654,6 +654,25 @@ export const ChevronDoubleDownIcon = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const VoucherIcon = () => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
className="size-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M16.5 6v.75m0 3v.75m0 3v.75m0 3V18m-9-5.25h5.25M7.5 15h3M3.375 5.25c-.621 0-1.125.504-1.125 1.125v3.026a2.999 2.999 0 0 1 0 5.198v3.026c0 .621.504 1.125 1.125 1.125h17.25c.621 0 1.125-.504 1.125-1.125v-3.026a2.999 2.999 0 0 1 0-5.198V6.375c0-.621-.504-1.125-1.125-1.125H3.375Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const CheckmarkIcon = () => {
|
export const CheckmarkIcon = () => {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export default function HBContestPage() {
|
|||||||
}, [selectedTownCouncil, combinedData]);
|
}, [selectedTownCouncil, combinedData]);
|
||||||
|
|
||||||
const topUser = filteredData.length > 0 ? filteredData[0] : null;
|
const topUser = filteredData.length > 0 ? filteredData[0] : null;
|
||||||
const top5Users = filteredData.slice(1, 10);
|
const top10Users = filteredData.slice(1, 10);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center">
|
||||||
@@ -180,7 +180,7 @@ export default function HBContestPage() {
|
|||||||
)}
|
)}
|
||||||
</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">
|
||||||
{top5Users.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>
|
||||||
|
|||||||
@@ -104,17 +104,26 @@ export default function ManageVoucherPage() {
|
|||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
<Table aria-label="Voucher Table">
|
<Table aria-label="Voucher Table">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
|
<TableColumn>Brand Logo</TableColumn>
|
||||||
<TableColumn>Brand</TableColumn>
|
<TableColumn>Brand</TableColumn>
|
||||||
<TableColumn>Description</TableColumn>
|
<TableColumn>Description</TableColumn>
|
||||||
<TableColumn>Expiration Date</TableColumn>
|
<TableColumn>Expiration Date</TableColumn>
|
||||||
<TableColumn>Quantity Available</TableColumn>
|
<TableColumn>Quantity Available</TableColumn>
|
||||||
<TableColumn>Code</TableColumn>
|
<TableColumn>Code</TableColumn>
|
||||||
<TableColumn>Brand Logo</TableColumn>
|
|
||||||
<TableColumn>Actions</TableColumn>
|
<TableColumn>Actions</TableColumn>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{voucherList.map((voucher) => (
|
{voucherList.map((voucher) => (
|
||||||
<TableRow key={voucher.id}>
|
<TableRow key={voucher.id}>
|
||||||
|
<TableCell>
|
||||||
|
{brandLogoUrls[voucher.id] && (
|
||||||
|
<img
|
||||||
|
src={brandLogoUrls[voucher.id]}
|
||||||
|
alt={voucher.brand}
|
||||||
|
style={{ width: "50px", height: "50px" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
<TableCell>{voucher.brand}</TableCell>
|
<TableCell>{voucher.brand}</TableCell>
|
||||||
<TableCell>{voucher.description}</TableCell>
|
<TableCell>{voucher.description}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@@ -122,15 +131,6 @@ export default function ManageVoucherPage() {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{voucher.quantityAvailable}</TableCell>
|
<TableCell>{voucher.quantityAvailable}</TableCell>
|
||||||
<TableCell>{voucher.code}</TableCell>
|
<TableCell>{voucher.code}</TableCell>
|
||||||
<TableCell>
|
|
||||||
{brandLogoUrls[voucher.id] && (
|
|
||||||
<img
|
|
||||||
src={brandLogoUrls[voucher.id]}
|
|
||||||
alt={voucher.brand}
|
|
||||||
style={{ width: "75px", height: "60px" }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ interface User {
|
|||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
townCouncil: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FormData {
|
interface FormData {
|
||||||
id: number;
|
id: string;
|
||||||
electricalBill: number;
|
electricalBill: number;
|
||||||
waterBill: number;
|
waterBill: number;
|
||||||
totalBill: number;
|
totalBill: number;
|
||||||
@@ -22,8 +23,31 @@ interface FormData {
|
|||||||
userId: string;
|
userId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FormDataWithUser extends FormData {
|
||||||
|
userName: string;
|
||||||
|
userEmail: string;
|
||||||
|
userTownCouncil: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Voucher {
|
||||||
|
id: string;
|
||||||
|
brandLogo: string | null;
|
||||||
|
brand: string;
|
||||||
|
description: string;
|
||||||
|
expirationDate: Date;
|
||||||
|
quantityAvailable: number;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserVoucher {
|
||||||
|
id: string; // UUID type
|
||||||
|
userId: string;
|
||||||
|
voucherId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Ranking() {
|
export default function Ranking() {
|
||||||
const [formData, setFormData] = useState<FormData[]>([]);
|
const [originalFormData, setOriginalFormData] = useState<FormData[]>([]);
|
||||||
|
const [filteredFormData, setFilteredFormData] = useState<FormData[]>([]);
|
||||||
const [userData, setUserData] = useState<User[]>([]);
|
const [userData, setUserData] = useState<User[]>([]);
|
||||||
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
|
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
|
||||||
column: "avgBill",
|
column: "avgBill",
|
||||||
@@ -32,12 +56,15 @@ export default function Ranking() {
|
|||||||
const [isEmailModalOpen, setIsEmailModalOpen] = useState(false);
|
const [isEmailModalOpen, setIsEmailModalOpen] = useState(false);
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [selectedUser, setSelectedUser] = useState<{ email: string, name: string }>({ email: "", name: "" });
|
const [selectedUser, setSelectedUser] = useState<{ email: string, name: string }>({ email: "", name: "" });
|
||||||
const [selectedFormId, setSelectedFormId] = useState<number | null>(null);
|
const [selectedFormId, setSelectedFormId] = useState<string | null>(null); // Changed to string to match UUID type
|
||||||
|
const [townCouncils, setTownCouncils] = useState<string[]>([]);
|
||||||
|
const [selectedTownCouncil, setSelectedTownCouncil] = useState<string>("");
|
||||||
|
const [top3Users, setTop3Users] = useState<FormDataWithUser[]>([]);
|
||||||
|
|
||||||
|
// Fetch data on component mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
// Fetch form data
|
|
||||||
const formResponse = await instance.get<FormData[]>(`${config.serverAddress}/hbcform`);
|
const formResponse = await instance.get<FormData[]>(`${config.serverAddress}/hbcform`);
|
||||||
const processedFormData = formResponse.data.map((data) => ({
|
const processedFormData = formResponse.data.map((data) => ({
|
||||||
...data,
|
...data,
|
||||||
@@ -46,19 +73,57 @@ export default function Ranking() {
|
|||||||
totalBill: Number(data.totalBill),
|
totalBill: Number(data.totalBill),
|
||||||
avgBill: Number(data.avgBill),
|
avgBill: Number(data.avgBill),
|
||||||
}));
|
}));
|
||||||
setFormData(processedFormData);
|
setOriginalFormData(processedFormData);
|
||||||
|
setFilteredFormData(processedFormData);
|
||||||
|
|
||||||
// Fetch user data
|
|
||||||
const userResponse = await instance.get<User[]>(`${config.serverAddress}/users/all`);
|
const userResponse = await instance.get<User[]>(`${config.serverAddress}/users/all`);
|
||||||
setUserData(userResponse.data);
|
setUserData(userResponse.data);
|
||||||
|
|
||||||
|
const townCouncilsResponse = await instance.get(`${config.serverAddress}/users/town-councils-metadata`);
|
||||||
|
setTownCouncils(JSON.parse(townCouncilsResponse.data).townCouncils);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Failed to fetch data");
|
console.log("Failed to fetch data:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchData();
|
fetchData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Filter form data based on selected town council
|
||||||
|
useEffect(() => {
|
||||||
|
const filtered = originalFormData.filter((data) => {
|
||||||
|
const user = userData.find((user) => user.id === data.userId);
|
||||||
|
return selectedTownCouncil ? user?.townCouncil === selectedTownCouncil : true;
|
||||||
|
});
|
||||||
|
setFilteredFormData(filtered);
|
||||||
|
}, [selectedTownCouncil, originalFormData, userData]);
|
||||||
|
|
||||||
|
// Compute top 3 users based on average bill
|
||||||
|
useEffect(() => {
|
||||||
|
const combinedData: FormDataWithUser[] = filteredFormData.map((data) => {
|
||||||
|
const user = userData.find((user) => user.id === data.userId);
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
userName: user ? `${user.firstName} ${user.lastName}` : "Unknown User",
|
||||||
|
userEmail: user ? user.email : "Unknown Email",
|
||||||
|
userTownCouncil: user ? user.townCouncil : "Unknown Town Council",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const townCouncilTopUsers: Record<string, FormDataWithUser> = {};
|
||||||
|
combinedData.forEach((data) => {
|
||||||
|
if (!townCouncilTopUsers[data.userTownCouncil] || data.avgBill > townCouncilTopUsers[data.userTownCouncil].avgBill) {
|
||||||
|
townCouncilTopUsers[data.userTownCouncil] = data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const topUsers = Object.values(townCouncilTopUsers);
|
||||||
|
const sortedTopUsers = topUsers.sort((a, b) => b.avgBill - a.avgBill).slice(0, 3);
|
||||||
|
|
||||||
|
setTop3Users(sortedTopUsers);
|
||||||
|
}, [filteredFormData, userData]);
|
||||||
|
|
||||||
|
// Sort form data based on descriptor
|
||||||
const sortFormData = (list: FormData[], descriptor: SortDescriptor) => {
|
const sortFormData = (list: FormData[], descriptor: SortDescriptor) => {
|
||||||
const { column, direction } = descriptor;
|
const { column, direction } = descriptor;
|
||||||
|
|
||||||
@@ -68,7 +133,7 @@ export default function Ranking() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return list; // No sorting if the column is not 'avgBill'
|
return list;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSort = () => {
|
const handleSort = () => {
|
||||||
@@ -85,15 +150,15 @@ export default function Ranking() {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const sortedFormData = sortFormData(formData, sortDescriptor);
|
const sortedFormData = sortFormData(filteredFormData, sortDescriptor);
|
||||||
|
|
||||||
// Combine form data with user information
|
const combinedData: FormDataWithUser[] = sortedFormData.map((data) => {
|
||||||
const combinedData = sortedFormData.map((data) => {
|
|
||||||
const user = userData.find((user) => user.id === data.userId);
|
const user = userData.find((user) => user.id === data.userId);
|
||||||
return {
|
return {
|
||||||
...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",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -115,8 +180,7 @@ export default function Ranking() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDeleteClick = (id: string) => {
|
||||||
const handleDeleteClick = (id: number) => {
|
|
||||||
setSelectedFormId(id);
|
setSelectedFormId(id);
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
};
|
};
|
||||||
@@ -125,7 +189,8 @@ export default function Ranking() {
|
|||||||
if (selectedFormId === null) return;
|
if (selectedFormId === null) return;
|
||||||
try {
|
try {
|
||||||
await instance.delete(`${config.serverAddress}/hbcform/${selectedFormId}`);
|
await instance.delete(`${config.serverAddress}/hbcform/${selectedFormId}`);
|
||||||
setFormData(formData.filter((data) => data.id !== selectedFormId));
|
setOriginalFormData(originalFormData.filter((data) => data.id !== selectedFormId));
|
||||||
|
setFilteredFormData(filteredFormData.filter((data) => data.id !== selectedFormId));
|
||||||
setSelectedFormId(null);
|
setSelectedFormId(null);
|
||||||
setIsDeleteModalOpen(false);
|
setIsDeleteModalOpen(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -133,13 +198,76 @@ export default function Ranking() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchVouchers = async (): Promise<Voucher[]> => {
|
||||||
|
try {
|
||||||
|
const response = await instance.get(`${config.serverAddress}/vouchers`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch vouchers:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const assignVouchersToUsers = async (topUsers: FormDataWithUser[]) => {
|
||||||
|
try {
|
||||||
|
const vouchers = await fetchVouchers();
|
||||||
|
if (vouchers.length === 0) {
|
||||||
|
console.warn("No vouchers available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const randomVoucher = (vouchers: Voucher[]) => vouchers[Math.floor(Math.random() * vouchers.length)];
|
||||||
|
|
||||||
|
for (const user of topUsers) {
|
||||||
|
const voucher = randomVoucher(vouchers);
|
||||||
|
if (voucher) {
|
||||||
|
await instance.post(`${config.serverAddress}/user-vouchers`, {
|
||||||
|
userId: user.userId,
|
||||||
|
voucherId: voucher.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to assign vouchers:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGiveVouchers = async () => {
|
||||||
|
try {
|
||||||
|
await assignVouchersToUsers(top3Users);
|
||||||
|
console.log("Vouchers assigned successfully");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to give vouchers:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="flex flex-col items-center justify-center py-5">
|
<section className="flex flex-col items-center justify-center py-5">
|
||||||
|
<div className="flex justify-between w-full">
|
||||||
<p className="text-xl font-bold">Form Data</p>
|
<p className="text-xl font-bold">Form Data</p>
|
||||||
|
{top3Users.length > 0 && (
|
||||||
|
<Button color="primary" onPress={handleGiveVouchers}>
|
||||||
|
Give Vouchers
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="gap-8 p-8">
|
<div className="gap-8 p-8">
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
<Table aria-label="Form Data Table">
|
<Table aria-label="Form Data Table">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableColumn>User ID</TableColumn>
|
|
||||||
<TableColumn>User Name</TableColumn>
|
<TableColumn>User Name</TableColumn>
|
||||||
<TableColumn>User Email</TableColumn>
|
<TableColumn>User Email</TableColumn>
|
||||||
<TableColumn>Electrical Bill</TableColumn>
|
<TableColumn>Electrical Bill</TableColumn>
|
||||||
@@ -155,7 +283,6 @@ export default function Ranking() {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{combinedData.map((data) => (
|
{combinedData.map((data) => (
|
||||||
<TableRow key={data.id}>
|
<TableRow key={data.id}>
|
||||||
<TableCell>{data.userId}</TableCell>
|
|
||||||
<TableCell>{data.userName}</TableCell>
|
<TableCell>{data.userName}</TableCell>
|
||||||
<TableCell>{data.userEmail}</TableCell>
|
<TableCell>{data.userEmail}</TableCell>
|
||||||
<TableCell>${data.electricalBill.toFixed(2)}</TableCell>
|
<TableCell>${data.electricalBill.toFixed(2)}</TableCell>
|
||||||
@@ -182,13 +309,13 @@ export default function Ranking() {
|
|||||||
<>
|
<>
|
||||||
<ModalHeader className="flex flex-col gap-1">Send Email</ModalHeader>
|
<ModalHeader className="flex flex-col gap-1">Send Email</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<p>Are you sure you want to send this email to {selectedUser.email}?</p>
|
<p>Are you sure you want to send an email to {selectedUser.name} ({selectedUser.email})?</p>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="danger" variant="light" onPress={onClose}>
|
<Button color="danger" variant="light" onPress={onClose}>
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="danger" onPress={() => { sendEmail(); onClose(); }}>
|
<Button color="primary" onPress={() => { sendEmail(); onClose(); }}>
|
||||||
Send
|
Send
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
@@ -203,11 +330,11 @@ export default function Ranking() {
|
|||||||
<>
|
<>
|
||||||
<ModalHeader className="flex flex-col gap-1">Delete Entry</ModalHeader>
|
<ModalHeader className="flex flex-col gap-1">Delete Entry</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<p>Are you sure you want to delete this entry?</p>
|
<p>Are you sure you want to delete this form entry?</p>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="danger" variant="light" onPress={onClose}>
|
<Button color="danger" variant="light" onPress={onClose}>
|
||||||
Cancel
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="danger" onPress={() => { deleteForm(); onClose(); }}>
|
<Button color="danger" onPress={() => { deleteForm(); onClose(); }}>
|
||||||
Delete
|
Delete
|
||||||
|
|||||||
153
client/src/pages/UserVouchersPage.tsx
Normal file
153
client/src/pages/UserVouchersPage.tsx
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
interface Voucher {
|
||||||
|
id: string;
|
||||||
|
brandLogo: string | null;
|
||||||
|
brand: string;
|
||||||
|
description: string;
|
||||||
|
expirationDate: Date;
|
||||||
|
quantityAvailable: number;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserVoucher {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
voucherId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserVoucherPage() {
|
||||||
|
const [userVouchers, setUserVouchers] = useState<UserVoucher[]>([]);
|
||||||
|
const [vouchers, setVouchers] = useState<Map<string, Voucher>>(new Map());
|
||||||
|
const [brandLogoUrls, setBrandLogoUrls] = useState<{ [key: string]: string }>({});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [userInformation, setUserInformation] = useState<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchUserInformation = async () => {
|
||||||
|
try {
|
||||||
|
const response = await retrieveUserInformation();
|
||||||
|
setUserInformation(response);
|
||||||
|
} catch (error) {
|
||||||
|
navigate("/signin");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchUserInformation();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchUserVouchers = async () => {
|
||||||
|
try {
|
||||||
|
if (userInformation && userInformation.id) {
|
||||||
|
// Fetch user vouchers
|
||||||
|
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}`)
|
||||||
|
);
|
||||||
|
|
||||||
|
const voucherResponses = await Promise.all(voucherDetailsPromises);
|
||||||
|
const voucherMap = new Map<string, Voucher>();
|
||||||
|
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' })
|
||||||
|
.then((res) => {
|
||||||
|
const url = URL.createObjectURL(res.data);
|
||||||
|
setBrandLogoUrls(prev => ({ ...prev, [voucher.id]: url }));
|
||||||
|
})
|
||||||
|
.catch(err => console.error(`Error fetching brand logo for voucher ${voucher.id}:`, err));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setVouchers(voucherMap);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setError('Failed to fetch vouchers');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (userInformation) {
|
||||||
|
fetchUserVouchers();
|
||||||
|
}
|
||||||
|
}, [userInformation]);
|
||||||
|
|
||||||
|
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 ? (
|
||||||
|
<Image
|
||||||
|
alt={voucher.brand}
|
||||||
|
height={100}
|
||||||
|
width={100}
|
||||||
|
src={brandLogoUrls[voucher.id] || '/default-logo.png'}
|
||||||
|
style={{ objectFit: 'cover' }} // Use style prop for objectFit
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -46,6 +46,9 @@ app.use("/vouchers", vouchers);
|
|||||||
const feedback = require("./routes/feedback.js");
|
const feedback = require("./routes/feedback.js");
|
||||||
app.use("/feedback", feedback)
|
app.use("/feedback", feedback)
|
||||||
|
|
||||||
|
const uservoucher = require("./routes/uservoucher.js");
|
||||||
|
app.use("/user-vouchers", uservoucher);
|
||||||
|
|
||||||
db.sequelize
|
db.sequelize
|
||||||
.sync({ alter: true })
|
.sync({ alter: true })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|||||||
@@ -45,4 +45,4 @@ module.exports = (sequelize, DataTypes) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return HBCform;
|
return HBCform;
|
||||||
}
|
};
|
||||||
@@ -18,7 +18,7 @@ module.exports = (sequelize, DataTypes) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tableName: 'schedule'
|
tableName: "schedule"
|
||||||
});
|
});
|
||||||
return Schedule;
|
return Schedule;
|
||||||
}
|
};
|
||||||
28
server/models/UserVoucher.js
Normal file
28
server/models/UserVoucher.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
const { DataTypes } = require("sequelize");
|
||||||
|
|
||||||
|
module.exports = (sequelize, DataTypes) => {
|
||||||
|
const UserVoucher = sequelize.define(
|
||||||
|
"UserVoucher",
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
allowNull: false,
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
voucherId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tableName: "user_vouchers",
|
||||||
|
timestamps: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return UserVoucher;
|
||||||
|
};
|
||||||
@@ -41,4 +41,4 @@ module.exports = (sequelize, DataTypes) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return Voucher;
|
return Voucher;
|
||||||
}
|
};
|
||||||
54
server/routes/uservoucher.js
Normal file
54
server/routes/uservoucher.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { UserVoucher } = require("../models");
|
||||||
|
const { Op } = require('sequelize');
|
||||||
|
const yup = require('yup');
|
||||||
|
|
||||||
|
router.get("/:id", async (req, res) => {
|
||||||
|
let id = req.params.id;
|
||||||
|
let vouchers = await UserVoucher.findByPk(id);
|
||||||
|
if (!vouchers) {
|
||||||
|
res.sendStatus(404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json(vouchers);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/', async (req, res) => {
|
||||||
|
const { userId, voucherId } = req.body;
|
||||||
|
|
||||||
|
if (!userId || !voucherId) {
|
||||||
|
return res.status(400).json({ error: 'userId and voucherId are required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userVoucher = await UserVoucher.create({ userId, voucherId });
|
||||||
|
res.status(201).json(userVoucher);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: "Failed to create UserVoucher entry" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/user/:userId', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { userId } = req.params;
|
||||||
|
|
||||||
|
// Fetch user vouchers by userId
|
||||||
|
const userVouchers = await UserVoucher.findAll({ where: { userId } });
|
||||||
|
|
||||||
|
if (!userVouchers) {
|
||||||
|
return res.status(404).json({ error: 'No vouchers found for this user' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract voucherIds from the userVouchers
|
||||||
|
const voucherIds = userVouchers.map(userVoucher => userVoucher.voucherId);
|
||||||
|
|
||||||
|
// Send the voucherIds along with userVoucher information
|
||||||
|
res.json({ userVouchers, voucherIds });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Failed to fetch user vouchers' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
Reference in New Issue
Block a user