From b35c74ac687cdbf5c508b0549f9ed31bb9b459fb Mon Sep 17 00:00:00 2001 From: ZacTohZY Date: Sun, 11 Aug 2024 02:49:14 +0800 Subject: [PATCH] Set up vouchers base --- client/src/App.tsx | 9 + .../AdministratorNavigationPanel.tsx | 2 +- client/src/pages/CreateVoucherPage.tsx | 144 +++++++++++++ client/src/pages/EditVoucherPage.tsx | 139 +++++++++++++ client/src/pages/ManageSchedulePage.tsx | 52 +++-- client/src/pages/ManageVoucherPage.tsx | 193 ++++++++++++++++++ server/index.js | 3 + server/models/Voucher.js | 44 ++++ server/routes/vouchers.js | 154 ++++++++++++++ 9 files changed, 712 insertions(+), 28 deletions(-) create mode 100644 client/src/pages/CreateVoucherPage.tsx create mode 100644 client/src/pages/EditVoucherPage.tsx create mode 100644 client/src/pages/ManageVoucherPage.tsx create mode 100644 server/models/Voucher.js create mode 100644 server/routes/vouchers.js diff --git a/client/src/App.tsx b/client/src/App.tsx index be01cb8..cebb6ed 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -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() { } /> + + } /> + } /> + } /> + + } /> } /> diff --git a/client/src/components/AdministratorNavigationPanel.tsx b/client/src/components/AdministratorNavigationPanel.tsx index 7bfbf2d..3ed5a48 100644 --- a/client/src/components/AdministratorNavigationPanel.tsx +++ b/client/src/components/AdministratorNavigationPanel.tsx @@ -168,7 +168,7 @@ export default function AdministratorNavigationPanel() { } - onClickRef="#" + onClickRef="voucher" />
diff --git a/client/src/pages/CreateVoucherPage.tsx b/client/src/pages/CreateVoucherPage.tsx new file mode 100644 index 0000000..5e55104 --- /dev/null +++ b/client/src/pages/CreateVoucherPage.tsx @@ -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 ( +
+
+
+ +
+
+ + {({ isValid, dirty, setFieldValue }) => ( +
+
+ { + setFieldValue("brandLogo", file); + }} + /> + + + + + +
+ +
+ )} +
+
+
+
+ ); +} diff --git a/client/src/pages/EditVoucherPage.tsx b/client/src/pages/EditVoucherPage.tsx new file mode 100644 index 0000000..2edd8a6 --- /dev/null +++ b/client/src/pages/EditVoucherPage.tsx @@ -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 ( +
+
+
+ +
+
+ + {({ isValid }) => ( +
+
+ + + + + +
+ +
+ )} +
+
+
+
+ ); +} diff --git a/client/src/pages/ManageSchedulePage.tsx b/client/src/pages/ManageSchedulePage.tsx index c88eab6..ff60826 100644 --- a/client/src/pages/ManageSchedulePage.tsx +++ b/client/src/pages/ManageSchedulePage.tsx @@ -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(null); const { isOpen, onOpen, onOpenChange } = useDisclosure(); const [sortDescriptor, setSortDescriptor] = useState({ - 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 ; } else { return ; @@ -100,15 +100,13 @@ export default function ManageSchedulePage() {

Admin Karang Guni Schedule

- - - +
@@ -126,7 +124,7 @@ export default function ManageSchedulePage() { {sortedScheduleList.map((schedule) => ( {((schedule.dateTime as unknown) as Date).toLocaleDateString()} - {((schedule.dateTime as unknown) as Date).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + {((schedule.dateTime as unknown) as Date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })} {schedule.location} {schedule.postalCode} {schedule.status} @@ -161,7 +159,7 @@ export default function ManageSchedulePage() {

Are you sure you want to delete this schedule?

- + + +
+
+ + Brand + Description + Expiration Date + Quantity Available + Code + Brand Logo + Actions + + + {voucherList.map((voucher) => ( + + {voucher.brand} + {voucher.description} + + {voucher.expirationDate.toLocaleDateString()} + + {voucher.quantityAvailable} + {voucher.code} + + {brandLogoUrls[voucher.id] && ( + {voucher.brand} + )} + + + + + + + ))} + +
+
+ + {/* Confirmation Modal for Deleting */} + + + {(onClose) => ( + <> + + Delete Voucher + + +

Are you sure you want to delete this voucher?

+
+ + + + + + )} +
+
+
+
+ ); +} diff --git a/server/index.js b/server/index.js index 1b6738f..de49c3f 100644 --- a/server/index.js +++ b/server/index.js @@ -40,6 +40,9 @@ app.use("/hbcform", HBCformRoute); const connections = require("./routes/connections"); app.use("/connections", connections); +const vouchers = require("./routes/vouchers.js"); +app.use("/vouchers", vouchers); + db.sequelize .sync({ alter: true }) .then(() => { diff --git a/server/models/Voucher.js b/server/models/Voucher.js new file mode 100644 index 0000000..0fdf898 --- /dev/null +++ b/server/models/Voucher.js @@ -0,0 +1,44 @@ +const { DataTypes } = require("sequelize"); + +module.exports = (sequelize, DataTypes) => { + const Voucher = sequelize.define( + "Voucher", + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + allowNull: false, + primaryKey: true, + }, + brandLogo: { + type: DataTypes.BLOB("long"), + allowNull: true, + }, + brand: { + type: DataTypes.STRING, + allowNull: false, + }, + description: { + type: DataTypes.STRING, + allowNull: false, + }, + expirationDate: { + type: DataTypes.DATE, + allowNull: false, + }, + quantityAvailable: { + type: DataTypes.INTEGER, + allowNull: false + }, + code: { + type: DataTypes.STRING, + allowNull: false, + }, + }, + { + tableName: "vouchers", + timestamps: true, + }); + + return Voucher; +} \ No newline at end of file diff --git a/server/routes/vouchers.js b/server/routes/vouchers.js new file mode 100644 index 0000000..d74cf1d --- /dev/null +++ b/server/routes/vouchers.js @@ -0,0 +1,154 @@ +const express = require('express'); +const router = express.Router(); +const { Voucher } = require('../models'); +const { Op } = require("sequelize"); +const yup = require("yup"); +const multer = require("multer"); +const sharp = require("sharp"); + +const upload = multer({ storage: multer.memoryStorage() }); + +router.post( + "/", + upload.fields([{ name: "brandLogo", maxCount: 1 }]), + async (req, res) => { + let data = req.body; + let files = req.files; + + // Validate request body + let validationSchema = yup.object().shape({ + brand: yup.string().trim().max(100).required(), + description: yup.string().trim().required(), + expirationDate: yup.date().required(), + quantityAvailable: yup.number().min(0).positive().required(), + code: yup.string().trim().required(), + brandLogo: yup.string().trim().max(255), // Optional field + }); + + try { + data = await validationSchema.validate(data, { abortEarly: false }); + + // Process brandLogo if it exists + let brandLogo = files.brandLogo ? files.brandLogo[0].buffer : null; + + if (brandLogo) { + // Resize and compress image + brandLogo = await sharp(brandLogo) + .resize(512, 512, { + fit: sharp.fit.inside, + withoutEnlargement: true, + }) + .jpeg({ quality: 80 }) + .toBuffer(); + } + + // Include brandLogo in data if processed + let result = await Voucher.create({ ...data, brandLogo }); + res.json(result); + } catch (err) { + res.status(400).json({ errors: err.errors }); + } + } +); + + +router.get("/", async (req, res) => { + let condition = {}; + let search = req.query.search; + if (search) { + condition[Op.or] = [ + { brand: { [Op.like]: `%${search}%` } }, + ]; + } + let list = await Voucher.findAll({ + where: condition, + order: [["createdAt", "ASC"]] + }); + res.json(list); +}); + +router.get("/:id", async (req, res) => { + let id = req.params.id; + let vouchers = await Voucher.findByPk(id); + if (!vouchers) { + res.sendStatus(404); + return; + } + res.json(vouchers); +}); + +router.get("/brandLogo/:id", async (req, res) => { + let id = req.params.id; + let vouchers = await Voucher.findByPk(id); + + if (!vouchers || !vouchers.brandLogo) { + res.sendStatus(404); + return; + } + + try { + res.set("Content-Type", "image/jpeg"); // Adjust the content type as necessary + res.send(vouchers.brandLogo); + } catch (err) { + res + .status(500) + .json({ message: "Error retrieving brand", error: err }); + } +}); + +router.put("/:id", async (req, res) => { //update + let id = req.params.id; + let vouchers = await Voucher.findByPk(id); + if (!vouchers) { + res.sendStatus(404); + return; + } + let data = req.body; + let validationSchema = yup.object().shape({ + brand: yup.string().trim().max(100).required(), + description: yup.string().trim().required(), + expirationDate: yup.date().required(), + quantityAvailable: yup.number().min(0).positive().required(), + code: yup.string().trim().required(), + }); + try { + data = await validationSchema.validate(data, + { abortEarly: false }); + let num = await Voucher.update(data, { + where: { id: id } + }); + if (num == 1) { + res.json({ + message: "Voucher was updated successfully." + }); + } + else { + res.status(400).json({ + message: `Cannot update voucher with id ${id}.` + }); + } + } + catch (err) { + res.status(400).json({ errors: err.errors }); + } +}); + +router.delete("/:id", async (req, res) => { + let id = req.params.id; + let num = await Voucher.destroy({ + where: { id: id } + }) + if (num == 1) { + res.json({ + message: "Voucher was deleted successfully." + }); + } + else { + res.status(400).json({ + message: `Cannot delete voucher with id ${id}.` + }); + } +}); + + +module.exports = router; \ No newline at end of file