Set up vouchers base

This commit is contained in:
ZacTohZY
2024-08-11 02:49:14 +08:00
parent 307be23e56
commit b35c74ac68
9 changed files with 712 additions and 28 deletions

View File

@@ -30,6 +30,9 @@ import ManageSchedulePage from "./pages/ManageSchedulePage";
import EditSchedulePage from "./pages/EditSchedulePage";
import CreateSchedulePage from "./pages/CreateSchedulePage";
import CommunityPostManagement from "./pages/CommunityPostManagement";
import ManageVoucherPage from "./pages/ManageVoucherPage";
import CreateVoucherPage from "./pages/CreateVoucherPage";
import EditVoucherPage from "./pages/EditVoucherPage";
function App() {
@@ -102,6 +105,12 @@ function App() {
<Route index element={<Ranking />} />
</Route>
<Route path="voucher">
<Route index element={<ManageVoucherPage />} />
<Route path="create-voucher" element={<CreateVoucherPage />} />
<Route path="edit-voucher/:id" element={<EditVoucherPage />} />
</Route>
<Route path="schedules">
<Route index element={<ManageSchedulePage />} />
<Route path="create-schedule" element={<CreateSchedulePage />} />

View File

@@ -168,7 +168,7 @@ export default function AdministratorNavigationPanel() {
<AdministratorNavigationPanelNavigationButton
text="Vouchers"
icon={<GiftTopIcon />}
onClickRef="#"
onClickRef="voucher"
/>
</div>
<div>

View File

@@ -0,0 +1,144 @@
import { Formik, Form } from "formik";
import * as Yup from "yup";
import { Button } from "@nextui-org/react";
import { useNavigate } from "react-router-dom";
import InsertPostImage from "../components/InsertPostImage";
import NextUIFormikInput from "../components/NextUIFormikInput";
import { NextUIFormikDatePicker } from "../components/NextUIFormikDatePicker";
import instance from "../security/http";
// Validation schema
const validationSchema = Yup.object({
brandLogo: Yup.mixed().required("Brand logo is required"),
brand: Yup.string().trim().required("Brand name is required"),
description: Yup.string().trim().required("Brand name is required"),
expirationDate: Yup.date().required("Expiry date is required"),
quantityAvailable: Yup.number().typeError("Must be a number").positive("Must be a positive value").required("Quantity is required"),
code: Yup.string().trim().required("Code is required"),
});
// Initial form values
const initialValues = {
brandLogo: null,
brand: "",
description: "",
expirationDate: "",
quantityAvailable: "",
code: "",
};
export default function CreateVoucherPage() {
const navigate = useNavigate();
const handleSubmit = async (
values: any,
{ setSubmitting, resetForm, setFieldError, setFieldValue }: any
) => {
const formData = new FormData();
if (values.brandLogo) {
formData.append("brandLogo", values.brandLogo);
}
formData.append("brand", values.brand);
formData.append("description", values.description);
formData.append("expirationDate", values.expirationDate);
formData.append("quantityAvailable", values.quantityAvailable);
formData.append("code", values.code);
try {
const response = await instance.post("/vouchers", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});
if (response.status === 200) {
console.log("Voucher created successfully:", response.data);
resetForm(); // Clear form after successful submit
setFieldValue("brandLogo", null);
navigate(-1);
} else {
console.error("Error creating voucher:", response.statusText);
}
} catch (error: any) {
if (error.response && error.response.data && error.response.data.errors) {
const errors = error.response.data.errors;
Object.keys(errors).forEach((key) => {
setFieldError(key, errors[key]);
});
} else {
console.error("Unexpected error:", error);
}
} finally {
setSubmitting(false);
}
};
return (
<div className="w-full h-full pb-12">
<div className="w-[680px] mx-auto p-6 bg-gray-100 dark:bg-gray-950 border border-primary-100 rounded-2xl">
<div className="py-2">
<Button variant="light" onPress={() => navigate(-1)}>
Back
</Button>
</div>
<div className="flex-grow overflow-y-auto">
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
{({ isValid, dirty, setFieldValue }) => (
<Form className="flex flex-col gap-4">
<div className="flex flex-col gap-4">
<InsertPostImage
onImageSelected={(file) => {
setFieldValue("brandLogo", file);
}}
/>
<NextUIFormikInput
label="Brand name"
name="brand"
type="text"
placeholder="Jollibean, KFC.."
labelPlacement="inside"
/>
<NextUIFormikInput
label="Description"
name="description"
type="text"
placeholder="10% off"
labelPlacement="inside"
/>
<NextUIFormikDatePicker
label="Expiry date"
name="expirationDate"
className="max-w-[280px]"
/>
<NextUIFormikInput
label="Quantity"
name="quantityAvailable"
type="number"
placeholder="000"
labelPlacement="inside"
/>
<NextUIFormikInput
label="Code"
name="code"
type="text"
placeholder="SD4FRC"
labelPlacement="inside"
/>
</div>
<Button type="submit" color="primary" className="w-[100px]"
isDisabled={!isValid || !dirty}>
Create
</Button>
</Form>
)}
</Formik>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,139 @@
import { Formik, Form } from "formik";
import * as Yup from "yup";
import { Button } from "@nextui-org/react";
import { useNavigate, useParams } from "react-router-dom";
import NextUIFormikInput from "../components/NextUIFormikInput";
import { NextUIFormikDatePicker } from "../components/NextUIFormikDatePicker";
import instance from "../security/http";
import { useEffect, useState } from "react";
// Validation schema
const validationSchema = Yup.object({
brand: Yup.string().trim().required("Brand name is required"),
description: Yup.string().trim().required("Description is required"),
expirationDate: Yup.date().required("Expiry date is required"),
quantityAvailable: Yup.number().typeError("Must be a number").positive("Must be a positive value").required("Quantity is required"),
code: Yup.string().trim().required("Code is required"),
});
export default function EditVoucherPage() {
const { id } = useParams(); // Get voucher ID from URL
const navigate = useNavigate();
const [initialValues, setInitialValues] = useState({
brand: "",
description: "",
expirationDate: "",
quantityAvailable: "",
code: "",
});
useEffect(() => {
// Fetch voucher details by ID
instance.get(`/vouchers/${id}`)
.then((res) => {
const { brand, description, expirationDate, quantityAvailable, code } = res.data;
setInitialValues({
brand,
description,
expirationDate: new Date(expirationDate).toISOString().slice(0, 10), // Format for date input
quantityAvailable,
code,
});
})
.catch((err) => {
console.error("Error fetching voucher details:", err);
});
}, [id]);
const handleSubmit = async (
values: any,
{ setSubmitting, resetForm, setFieldError }: any
) => {
try {
const response = await instance.put(`/vouchers/${id}`, values);
if (response.status === 200) {
console.log("Voucher updated successfully:", response.data);
resetForm(); // Clear form after successful update
navigate(-1); // Navigate back after updating
} else {
console.error("Error updating voucher:", response.statusText);
}
} catch (error: any) {
if (error.response && error.response.data && error.response.data.errors) {
const errors = error.response.data.errors;
Object.keys(errors).forEach((key) => {
setFieldError(key, errors[key]);
});
} else {
console.error("Unexpected error:", error);
}
} finally {
setSubmitting(false);
}
};
return (
<div className="w-full h-full pb-12">
<div className="w-[680px] mx-auto p-6 bg-gray-100 dark:bg-gray-950 border border-primary-100 rounded-2xl">
<div className="py-2">
<Button variant="light" onPress={() => navigate(-1)}>
Back
</Button>
</div>
<div className="flex-grow overflow-y-auto">
<Formik
initialValues={initialValues}
enableReinitialize
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
{({ isValid }) => (
<Form className="flex flex-col gap-4">
<div className="flex flex-col gap-4">
<NextUIFormikInput
label="Brand name"
name="brand"
type="text"
placeholder="Jollibean, KFC.."
labelPlacement="inside"
/>
<NextUIFormikInput
label="Description"
name="description"
type="text"
placeholder="10% off"
labelPlacement="inside"
/>
<NextUIFormikDatePicker
label="Expiry date"
name="expirationDate"
className="max-w-[280px]"
/>
<NextUIFormikInput
label="Quantity"
name="quantityAvailable"
type="number"
placeholder="000"
labelPlacement="inside"
/>
<NextUIFormikInput
label="Code"
name="code"
type="text"
placeholder="SD4FRC"
labelPlacement="inside"
/>
</div>
<Button type="submit" color="primary" className="w-[100px]"
isDisabled={!isValid}>
Update
</Button>
</Form>
)}
</Formik>
</div>
</div>
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { Link, useNavigate } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import { useEffect, useState } from 'react';
import instance from "../security/http";
import { PencilSquareIcon, PlusIcon, TrashDeleteIcon } from "../icons";
@@ -18,12 +18,12 @@ export default function ManageSchedulePage() {
const [scheduleIdToDelete, setScheduleIdToDelete] = useState<number | null>(null);
const { isOpen, onOpen, onOpenChange } = useDisclosure();
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
column: 'dateTime',
direction: 'ascending',
column: "dateTime",
direction: "ascending",
});
useEffect(() => {
instance.get('/schedule')
instance.get("/schedule")
.then((res) => {
const schedules = res.data.map((schedule: Schedule) => ({
...schedule,
@@ -32,7 +32,7 @@ export default function ManageSchedulePage() {
setScheduleList(schedules);
})
.catch((err) => {
console.error('Error fetching schedules:', err);
console.error("Error fetching schedules:", err);
});
}, []);
@@ -51,7 +51,7 @@ export default function ManageSchedulePage() {
setScheduleIdToDelete(null);
})
.catch((err) => {
console.error('Error deleting schedule:', err);
console.error("Error deleting schedule:", err);
});
}
};
@@ -61,16 +61,16 @@ export default function ManageSchedulePage() {
const sortedList = [...list].sort((a, b) => {
switch (column) {
case 'dateTime':
case "dateTime":
const dateA = new Date(a.dateTime);
const dateB = new Date(b.dateTime);
return direction === 'ascending' ? dateA.getTime() - dateB.getTime() : dateB.getTime() - dateA.getTime();
case 'location':
return direction === 'ascending' ? a.location.localeCompare(b.location) : b.location.localeCompare(a.location);
case 'postalCode':
return direction === 'ascending' ? a.postalCode.localeCompare(b.postalCode) : b.postalCode.localeCompare(a.postalCode);
case 'status':
return direction === 'ascending' ? a.status.localeCompare(b.status) : b.status.localeCompare(a.status);
return direction === "ascending" ? dateA.getTime() - dateB.getTime() : dateB.getTime() - dateA.getTime();
case "location":
return direction === "ascending" ? a.location.localeCompare(b.location) : b.location.localeCompare(a.location);
case "postalCode":
return direction === "ascending" ? a.postalCode.localeCompare(b.postalCode) : b.postalCode.localeCompare(a.postalCode);
case "status":
return direction === "ascending" ? a.status.localeCompare(b.status) : b.status.localeCompare(a.status);
default:
throw new Error(`Unsupported column: ${column}`);
}
@@ -81,13 +81,13 @@ export default function ManageSchedulePage() {
const handleSort = () => {
const { column, direction } = sortDescriptor;
const newDirection = direction === 'ascending' ? 'descending' : 'ascending';
const newDirection = direction === "ascending" ? "descending" : "ascending";
setSortDescriptor({ column, direction: newDirection });
};
const renderSortIndicator = () => {
if (sortDescriptor.direction === 'ascending') {
if (sortDescriptor.direction === "ascending") {
return <span>&uarr;</span>;
} else {
return <span>&darr;</span>;
@@ -100,15 +100,13 @@ export default function ManageSchedulePage() {
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10">
<div className="inline-block text-center justify-center flex flex-row gap-10">
<p className="text-3xl font-bold">Admin Karang Guni Schedule</p>
<Link to="/Addschedules">
<Button
isIconOnly
color="primary"
onPress={() => navigate("create-schedule")}
>
<PlusIcon />
</Button>
</Link>
<Button
isIconOnly
color="primary"
onPress={() => navigate("create-schedule")}
>
<PlusIcon />
</Button>
</div>
<div className="w-full overflow-auto max-w-screen-lg">
<Table aria-label="Schedule Table">
@@ -126,7 +124,7 @@ export default function ManageSchedulePage() {
{sortedScheduleList.map((schedule) => (
<TableRow key={schedule.id}>
<TableCell>{((schedule.dateTime as unknown) as Date).toLocaleDateString()}</TableCell>
<TableCell>{((schedule.dateTime as unknown) as Date).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</TableCell>
<TableCell>{((schedule.dateTime as unknown) as Date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}</TableCell>
<TableCell>{schedule.location}</TableCell>
<TableCell>{schedule.postalCode}</TableCell>
<TableCell>{schedule.status}</TableCell>
@@ -161,7 +159,7 @@ export default function ManageSchedulePage() {
<p>Are you sure you want to delete this schedule?</p>
</ModalBody>
<ModalFooter>
<Button color="danger" variant="light" onPress={onClose}>
<Button color="default" variant="light" onPress={onClose}>
Close
</Button>
<Button color="danger" onPress={() => { deleteSchedule(); onClose(); }}>

View File

@@ -0,0 +1,193 @@
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, Table, TableBody, TableCell, TableColumn, TableHeader, TableRow } from "@nextui-org/react";
import config from "../config";
import instance from "../security/http";
import { PencilSquareIcon, PlusIcon, TrashDeleteIcon } from "../icons";
interface Voucher {
id: number;
brand: string;
description: string;
expirationDate: Date;
quantityAvailable: number;
code: string;
}
export default function ManageVoucherPage() {
const navigate = useNavigate();
const [voucherList, setVoucherList] = useState<Voucher[]>([]);
const [brandLogoUrls, setBrandLogoUrls] = useState<{ [key: number]: string }>({});
const [voucherIdToDelete, setVoucherIdToDelete] = useState<number | null>(null);
const [showConfirmDelete, setShowConfirmDelete] = useState(false);
useEffect(() => {
getVouchers();
}, []);
const getVouchers = () => {
instance
.get(config.serverAddress + "/vouchers")
.then((res) => {
const vouchers = res.data.map((voucher: Voucher) => ({
...voucher,
expirationDate: new Date(voucher.expirationDate),
}));
vouchers.sort(
(a: Voucher, b: Voucher) =>
b.expirationDate.getTime() - a.expirationDate.getTime()
);
setVoucherList(vouchers);
// Fetch brand logos
fetchBrandLogos(vouchers);
})
.catch((err) => {
console.error("Error fetching vouchers:", err);
});
};
const fetchBrandLogos = (vouchers: Voucher[]) => {
const urls: { [key: number]: string } = {};
vouchers.forEach((voucher) => {
instance
.get(`${config.serverAddress}/vouchers/brandLogo/${voucher.id}`, { responseType: "blob" })
.then((res) => {
const url = URL.createObjectURL(res.data);
urls[voucher.id] = url;
setBrandLogoUrls((prev) => ({ ...prev, ...urls }));
})
.catch((err) => {
console.error(`Error fetching brand logo for voucher ${voucher.id}:`, err);
});
});
};
const handleEdit = (id: number) => {
navigate(`edit-voucher/${id}`);
};
const handleDelete = (id: number) => {
setVoucherIdToDelete(id);
setShowConfirmDelete(true);
};
const deleteVoucher = () => {
if (voucherIdToDelete !== null) {
instance.delete(`/vouchers/${voucherIdToDelete}`)
.then((res) => {
console.log(res.data);
setVoucherList((prev) => prev.filter(voucher => voucher.id !== voucherIdToDelete));
setShowConfirmDelete(false);
setVoucherIdToDelete(null);
})
.catch((err) => {
console.error("Error deleting voucher:", err);
});
}
};
return (
<div className="w-full h-full">
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10">
<div className="flex flex-row gap-10">
<p className="text-2xl font-bold">Manage Vouchers</p>
<div>
<Button
isIconOnly
onPress={() => navigate("create-voucher")}
>
<PlusIcon />
</Button>
</div>
</div>
<div className="flex flex-col gap-8">
<Table aria-label="Voucher Table">
<TableHeader>
<TableColumn>Brand</TableColumn>
<TableColumn>Description</TableColumn>
<TableColumn>Expiration Date</TableColumn>
<TableColumn>Quantity Available</TableColumn>
<TableColumn>Code</TableColumn>
<TableColumn>Brand Logo</TableColumn>
<TableColumn>Actions</TableColumn>
</TableHeader>
<TableBody>
{voucherList.map((voucher) => (
<TableRow key={voucher.id}>
<TableCell>{voucher.brand}</TableCell>
<TableCell>{voucher.description}</TableCell>
<TableCell>
{voucher.expirationDate.toLocaleDateString()}
</TableCell>
<TableCell>{voucher.quantityAvailable}</TableCell>
<TableCell>{voucher.code}</TableCell>
<TableCell>
{brandLogoUrls[voucher.id] && (
<img
src={brandLogoUrls[voucher.id]}
alt={voucher.brand}
style={{ width: "75px", height: "60px" }}
/>
)}
</TableCell>
<TableCell>
<Button
isIconOnly
variant="light"
color="success"
onPress={() => handleEdit(voucher.id)}>
<PencilSquareIcon />
</Button>
<Button
isIconOnly
variant="light"
color="danger"
onPress={() => handleDelete(voucher.id)}>
<TrashDeleteIcon />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{/* Confirmation Modal for Deleting */}
<Modal
isOpen={showConfirmDelete}
onOpenChange={setShowConfirmDelete}
isDismissable={false}
isKeyboardDismissDisabled={true}
>
<ModalContent>
{(onClose) => (
<>
<ModalHeader className="flex flex-col gap-1">
Delete Voucher
</ModalHeader>
<ModalBody>
<p>Are you sure you want to delete this voucher?</p>
</ModalBody>
<ModalFooter>
<Button color="danger" variant="light" onPress={onClose}>
No
</Button>
<Button
color="danger"
onPress={() => {
deleteVoucher();
onClose();
}}
>
Yes
</Button>
</ModalFooter>
</>
)}
</ModalContent>
</Modal>
</section>
</div>
);
}