Set up vouchers base
This commit is contained in:
@@ -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 />} />
|
||||
|
||||
@@ -168,7 +168,7 @@ export default function AdministratorNavigationPanel() {
|
||||
<AdministratorNavigationPanelNavigationButton
|
||||
text="Vouchers"
|
||||
icon={<GiftTopIcon />}
|
||||
onClickRef="#"
|
||||
onClickRef="voucher"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
144
client/src/pages/CreateVoucherPage.tsx
Normal file
144
client/src/pages/CreateVoucherPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
139
client/src/pages/EditVoucherPage.tsx
Normal file
139
client/src/pages/EditVoucherPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>↑</span>;
|
||||
} else {
|
||||
return <span>↓</span>;
|
||||
@@ -100,7 +100,6 @@ 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"
|
||||
@@ -108,7 +107,6 @@ export default function ManageSchedulePage() {
|
||||
>
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
</Link>
|
||||
</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(); }}>
|
||||
|
||||
193
client/src/pages/ManageVoucherPage.tsx
Normal file
193
client/src/pages/ManageVoucherPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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(() => {
|
||||
|
||||
44
server/models/Voucher.js
Normal file
44
server/models/Voucher.js
Normal file
@@ -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;
|
||||
}
|
||||
154
server/routes/vouchers.js
Normal file
154
server/routes/vouchers.js
Normal file
@@ -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;
|
||||
Reference in New Issue
Block a user