Fixed Function of Admin KG Schedule
This commit is contained in:
@@ -10,8 +10,10 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@internationalized/date": "^3.5.5",
|
||||
"@nextui-org/react": "^2.4.2",
|
||||
"axios": "^1.7.2",
|
||||
"dayjs": "^1.11.12",
|
||||
"formik": "^2.4.6",
|
||||
"framer-motion": "^11.2.10",
|
||||
"openai": "^4.53.2",
|
||||
|
||||
6292
client/pnpm-lock.yaml
generated
6292
client/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -26,6 +26,8 @@ import ForgotPasswordPage from "./pages/ForgotPasswordPage";
|
||||
import RestrictedLayout from "./layouts/restricted";
|
||||
import Ranking from "./pages/Ranking";
|
||||
import ManageSchedulePage from "./pages/ManageSchedulePage";
|
||||
import EditSchedulePage from "./pages/EditSchedulePage";
|
||||
import CreateSchedulePage from "./pages/CreateSchedulePage";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
@@ -98,6 +100,8 @@ function App() {
|
||||
|
||||
<Route path="schedules">
|
||||
<Route index element={<ManageSchedulePage />} />
|
||||
<Route path="create-schedule" element={<CreateSchedulePage />} />
|
||||
<Route path="edit-schedule/:id" element={<EditSchedulePage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
42
client/src/components/NextUIFormikDatePicker.tsx
Normal file
42
client/src/components/NextUIFormikDatePicker.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { DatePicker } from "@nextui-org/react";
|
||||
import { useField } from "formik";
|
||||
import { CalendarDate, getLocalTimeZone, today } from "@internationalized/date";
|
||||
|
||||
interface NextUIFormikDatePickerProps {
|
||||
label: string;
|
||||
name: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const dateToCalendarDate = (date: Date): CalendarDate => {
|
||||
return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate());
|
||||
};
|
||||
|
||||
// NextUIFormikDatePicker component
|
||||
export const NextUIFormikDatePicker = ({
|
||||
label,
|
||||
...props
|
||||
}: NextUIFormikDatePickerProps) => {
|
||||
const [field, meta, helpers] = useField(props.name);
|
||||
|
||||
const handleChange = (date: CalendarDate | null) => {
|
||||
if (date) {
|
||||
helpers.setValue(date.toDate('UTC')); // Ensure date is converted to a proper Date object with a timezone
|
||||
} else {
|
||||
helpers.setValue(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DatePicker
|
||||
{...props}
|
||||
label={label}
|
||||
value={field.value ? dateToCalendarDate(new Date(field.value)) : null} // Convert field value to CalendarDate
|
||||
onChange={handleChange}
|
||||
isInvalid={meta.touched && !!meta.error}
|
||||
errorMessage={meta.touched && meta.error ? meta.error : ""}
|
||||
minValue={today(getLocalTimeZone())}
|
||||
defaultValue={today(getLocalTimeZone()).subtract({ days: 1 })}
|
||||
/>
|
||||
);
|
||||
};
|
||||
34
client/src/components/NextUIFormikRadioButton.tsx
Normal file
34
client/src/components/NextUIFormikRadioButton.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { RadioGroup } from "@nextui-org/react";
|
||||
import { useField } from "formik";
|
||||
|
||||
interface NextUIFormikRadioGroupProps {
|
||||
label: string;
|
||||
name: string;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
// NextUIFormikRadioGroup component
|
||||
export const NextUIFormikRadioGroup = ({
|
||||
label,
|
||||
...props
|
||||
}: NextUIFormikRadioGroupProps) => {
|
||||
const [field, meta, helpers] = useField(props.name);
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
helpers.setValue(event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<RadioGroup
|
||||
{...props}
|
||||
label={label}
|
||||
value={field.value}
|
||||
onChange={handleChange}
|
||||
isInvalid={meta.touched && !!meta.error}
|
||||
errorMessage={meta.touched && meta.error ? meta.error : ""}
|
||||
>
|
||||
{props.children}
|
||||
</RadioGroup>
|
||||
);
|
||||
};
|
||||
140
client/src/pages/CreateSchedulePage.tsx
Normal file
140
client/src/pages/CreateSchedulePage.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { Formik, Form } from "formik";
|
||||
import * as yup from "yup";
|
||||
import { Button, Radio } from "@nextui-org/react";
|
||||
import { NextUIFormikRadioGroup } from "../components/NextUIFormikRadioButton";
|
||||
import { NextUIFormikDatePicker } from "../components/NextUIFormikDatePicker";
|
||||
import NextUIFormikInput from "../components/NextUIFormikInput";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import instance from "../security/http";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
// Validation schema
|
||||
const validationSchema = yup.object().shape({
|
||||
date: yup.date().required(),
|
||||
time: yup.string().trim().required(),
|
||||
location: yup.string().trim().min(15).max(50).required(),
|
||||
postalCode: yup.string().matches(/^\d{6}$/, "Postal code must be exactly 6 digits").required(),
|
||||
status: yup.string().trim().required()
|
||||
});
|
||||
|
||||
const initialValues: any = {
|
||||
date: "",
|
||||
time: "",
|
||||
location: "",
|
||||
postalCode: "",
|
||||
status: ""
|
||||
};
|
||||
|
||||
export default function CreateSchedulePage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = (values: any) => {
|
||||
let data = { ...values };
|
||||
|
||||
// Ensure the date is in the correct format
|
||||
const formattedDate = dayjs(data.date).format('YYYY-MM-DD');
|
||||
console.log("Formatted Date:", formattedDate);
|
||||
|
||||
// Ensure the time is in the correct format
|
||||
const formattedTime = data.time.length === 5 ? `${data.time}:00` : data.time;
|
||||
console.log("Formatted Time:", formattedTime);
|
||||
|
||||
// Combine the formatted date and time
|
||||
const dateTimeString = `${formattedDate} ${formattedTime}`;
|
||||
console.log("Combined dateTime string:", dateTimeString);
|
||||
|
||||
// Parse the combined string with dayjs
|
||||
const dateTime = dayjs(dateTimeString, 'YYYY-MM-DD HH:mm:ss');
|
||||
console.log("Parsed dateTime:", dateTime);
|
||||
|
||||
// Check if the parsed dateTime is valid
|
||||
if (!dateTime.isValid()) {
|
||||
console.error("Invalid dateTime format");
|
||||
return;
|
||||
}
|
||||
|
||||
data.dateTime = dateTime.toISOString();
|
||||
|
||||
data.location = data.location.trim();
|
||||
|
||||
if (typeof data.postalCode === 'string') {
|
||||
data.postalCode = data.postalCode.trim();
|
||||
}
|
||||
|
||||
data.status = data.status.trim();
|
||||
|
||||
console.log("Data to be sent:", data);
|
||||
|
||||
instance.post("/schedule", data)
|
||||
.then((res) => {
|
||||
console.log(res.data);
|
||||
navigate(-1);
|
||||
})
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="flex flex-col items-center justify-center gap-20 py-8 md:py-10">
|
||||
<div className="w-full flex items-start">
|
||||
<Button
|
||||
variant="light"
|
||||
onPress={() => navigate(-1)}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<div className="flex-grow text-center">
|
||||
<p className="text-3xl font-bold">Add New Schedule</p>
|
||||
</div>
|
||||
</div>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{({ isValid, dirty }) => (
|
||||
<Form className="flex flex-col gap-4">
|
||||
<div className="flex gap-8">
|
||||
<NextUIFormikDatePicker
|
||||
label="Date"
|
||||
name="date"
|
||||
className="max-w-[280px]"
|
||||
/>
|
||||
<NextUIFormikInput
|
||||
type='time'
|
||||
label="Time"
|
||||
name="time"
|
||||
placeholder=""
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-8">
|
||||
<NextUIFormikInput
|
||||
type="text"
|
||||
label="Location"
|
||||
name="location"
|
||||
placeholder="Enter the location"
|
||||
/>
|
||||
<NextUIFormikInput
|
||||
type="text"
|
||||
label="Postal Code"
|
||||
name="postalCode"
|
||||
placeholder="123456"
|
||||
/>
|
||||
</div>
|
||||
<NextUIFormikRadioGroup
|
||||
label="Status"
|
||||
name="status"
|
||||
className="flex gap-2"
|
||||
>
|
||||
<Radio value="Up coming">Up coming</Radio>
|
||||
<Radio value="On going">On going</Radio>
|
||||
<Radio value="Ended">Ended</Radio>
|
||||
</NextUIFormikRadioGroup>
|
||||
<Button type="submit" color="primary" className="w-[100px]">Create</Button>
|
||||
{/* Example of using isValid and dirty */}
|
||||
<p>Form is {isValid ? 'valid' : 'invalid'}</p>
|
||||
<p>Form has been {dirty ? 'touched' : 'not touched'}</p>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</section >
|
||||
)
|
||||
}
|
||||
164
client/src/pages/EditSchedulePage.tsx
Normal file
164
client/src/pages/EditSchedulePage.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Formik, Form } from "formik";
|
||||
import * as yup from "yup";
|
||||
import { Button, Radio } from "@nextui-org/react";
|
||||
import { NextUIFormikRadioGroup } from "../components/NextUIFormikRadioButton";
|
||||
import { NextUIFormikDatePicker } from "../components/NextUIFormikDatePicker";
|
||||
import NextUIFormikInput from "../components/NextUIFormikInput";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import instance from "../security/http";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
|
||||
// Validation schema
|
||||
const validationSchema = yup.object().shape({
|
||||
date: yup.date().required(),
|
||||
time: yup.string().trim().required(),
|
||||
location: yup.string().trim().min(15).max(50).required(),
|
||||
postalCode: yup.string().matches(/^\d{6}$/, "Postal code must be exactly 6 digits").required(),
|
||||
status: yup.string().trim().required()
|
||||
});
|
||||
|
||||
export default function EditSchedulePage() {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
|
||||
const [schedule, setSchedule] = useState<any>({
|
||||
date: "",
|
||||
time: "",
|
||||
location: "",
|
||||
postalCode: "",
|
||||
status: ""
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
instance.get(`/schedule/${id}`).then((res) => {
|
||||
const scheduleData = res.data;
|
||||
|
||||
scheduleData.date = dayjs(scheduleData.dateTime).format("YYYY-MM-DD");
|
||||
scheduleData.time = dayjs(scheduleData.dateTime).format("HH:mm");
|
||||
|
||||
console.log("Fetched schedule data:", scheduleData);
|
||||
setSchedule(scheduleData);
|
||||
}).catch((err) => {
|
||||
console.error("Error fetching schedule:", err);
|
||||
});
|
||||
}, [id]);
|
||||
|
||||
const handleSubmit = (values: any) => {
|
||||
let data = { ...values };
|
||||
|
||||
// Ensure the date is in the correct format
|
||||
const formattedDate = dayjs(data.date).format("YYYY-MM-DD");
|
||||
console.log("Formatted Date:", formattedDate);
|
||||
|
||||
// Ensure the time is in the correct format
|
||||
const formattedTime = data.time.length === 5 ? `${data.time}:00` : data.time;
|
||||
console.log("Formatted Time:", formattedTime);
|
||||
|
||||
// Combine the formatted date and time
|
||||
const dateTimeString = `${formattedDate} ${formattedTime}`;
|
||||
console.log("Combined dateTime string:", dateTimeString);
|
||||
|
||||
// Parse the combined string with dayjs
|
||||
const dateTime = dayjs(dateTimeString, "YYYY-MM-DD HH:mm:ss");
|
||||
console.log("Parsed dateTime:", dateTime);
|
||||
|
||||
// Check if the parsed dateTime is valid
|
||||
if (!dateTime.isValid()) {
|
||||
console.error("Invalid dateTime format");
|
||||
return;
|
||||
}
|
||||
|
||||
data.dateTime = dateTime.toISOString();
|
||||
data.location = data.location.trim();
|
||||
|
||||
if (typeof data.postalCode === "string") {
|
||||
data.postalCode = data.postalCode.trim();
|
||||
}
|
||||
|
||||
data.status = data.status.trim();
|
||||
|
||||
console.log("Data to be sent:", data);
|
||||
|
||||
instance.put(`/schedule/${id}`, data)
|
||||
.then((res) => {
|
||||
console.log(res.data);
|
||||
navigate(-1);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error updating schedule:", error);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="flex flex-col items-center justify-center gap-20 py-8 md:py-10">
|
||||
<div className="w-full flex items-start">
|
||||
<Button
|
||||
variant="light"
|
||||
onPress={() => navigate(-1)}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<div className="flex-grow text-center">
|
||||
<p className="text-3xl font-bold">Add New Schedule</p>
|
||||
</div>
|
||||
</div>
|
||||
<Formik
|
||||
enableReinitialize
|
||||
initialValues={schedule}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{({ isValid, dirty }) => (
|
||||
<Form className="flex flex-col gap-4">
|
||||
<div className="flex gap-8">
|
||||
<NextUIFormikDatePicker
|
||||
label="Date"
|
||||
name="date"
|
||||
className="max-w-[284px]"
|
||||
/>
|
||||
<NextUIFormikInput
|
||||
type='time'
|
||||
label="Time"
|
||||
name="time"
|
||||
placeholder=""
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-8">
|
||||
<NextUIFormikInput
|
||||
type="text"
|
||||
label="Location"
|
||||
name="location"
|
||||
placeholder="Enter the location"
|
||||
/>
|
||||
<NextUIFormikInput
|
||||
type="text"
|
||||
label="Postal Code"
|
||||
name="postalCode"
|
||||
placeholder="123456"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<NextUIFormikRadioGroup
|
||||
label="Status"
|
||||
name="status"
|
||||
className="flex gap-2"
|
||||
>
|
||||
<Radio value="Up coming">Up coming</Radio>
|
||||
<Radio value="On going">On going</Radio>
|
||||
<Radio value="Ended">Ended</Radio>
|
||||
</NextUIFormikRadioGroup>
|
||||
</div>
|
||||
<div className="flex gap-5">
|
||||
<Button type="submit" color="secondary" className="max-w-[100px]">Update</Button>
|
||||
</div>
|
||||
{/* Example of using isValid and dirty */}
|
||||
<p>Form is {isValid ? 'valid' : 'invalid'}</p>
|
||||
<p>Form has been {dirty ? 'touched' : 'not touched'}</p>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useEffect, useState } from 'react';
|
||||
import instance from "../security/http";
|
||||
import { PencilSquareIcon, PlusIcon, TrashDeleteIcon } from "../icons";
|
||||
@@ -13,7 +13,7 @@ interface Schedule {
|
||||
}
|
||||
|
||||
export default function ManageSchedulePage() {
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [scheduleList, setScheduleList] = useState<Schedule[]>([]);
|
||||
const [scheduleIdToDelete, setScheduleIdToDelete] = useState<number | null>(null);
|
||||
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
||||
@@ -36,6 +36,11 @@ export default function ManageSchedulePage() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleEdit = (id: number) => {
|
||||
navigate(`edit-schedule/${id}`);
|
||||
};
|
||||
|
||||
|
||||
const deleteSchedule = () => {
|
||||
if (scheduleIdToDelete !== null) {
|
||||
instance.delete(`/schedule/${scheduleIdToDelete}`)
|
||||
@@ -96,7 +101,11 @@ export default function ManageSchedulePage() {
|
||||
<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">
|
||||
<Button
|
||||
isIconOnly
|
||||
color="primary"
|
||||
onPress={() => navigate("create-schedule")}
|
||||
>
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
</Link>
|
||||
@@ -122,8 +131,21 @@ export default function ManageSchedulePage() {
|
||||
<TableCell>{schedule.postalCode}</TableCell>
|
||||
<TableCell>{schedule.status}</TableCell>
|
||||
<TableCell>
|
||||
<Link to={`/Editschedules/${schedule.id}`}><Button isIconOnly color="success" variant="light"><PencilSquareIcon /></Button></Link>
|
||||
<Button isIconOnly color="danger" variant="light" onPress={() => { setScheduleIdToDelete(schedule.id); onOpen(); }}><TrashDeleteIcon /></Button>
|
||||
<Button
|
||||
isIconOnly
|
||||
variant="light"
|
||||
color="success"
|
||||
onPress={() => handleEdit(schedule.id)}
|
||||
>
|
||||
<PencilSquareIcon />
|
||||
</Button>
|
||||
<Button
|
||||
isIconOnly
|
||||
color="danger"
|
||||
variant="light"
|
||||
onPress={() => { setScheduleIdToDelete(schedule.id); onOpen(); }}>
|
||||
<TrashDeleteIcon />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@@ -98,26 +98,29 @@ export default function SchedulePage() {
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10">
|
||||
<h1>Karang Guni Schedule</h1>
|
||||
<div className="flex gap-4">
|
||||
{/* Search Input */}
|
||||
<Input
|
||||
value={search}
|
||||
className="w-[400px]"
|
||||
placeholder="Search Address/Postal"
|
||||
onChange={onSearchChange}
|
||||
onKeyDown={onSearchKeyDown}
|
||||
endContent={
|
||||
<div className="flex flex-row -mr-3">
|
||||
<Button isIconOnly variant="light" onPress={onClickSearch}>
|
||||
<MagnifyingGlassIcon />
|
||||
</Button>
|
||||
<Button isIconOnly variant="light" onPress={onClickClear}>
|
||||
<XMarkIcon />
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div className="flex flex-row gap-10">
|
||||
<p className="text-2xl font-bold">Karang Guni Schedule</p>
|
||||
|
||||
<div className="flex gap-4">
|
||||
{/* Search Input */}
|
||||
<Input
|
||||
value={search}
|
||||
className="w-[400px]"
|
||||
placeholder="Search"
|
||||
onChange={onSearchChange}
|
||||
onKeyDown={onSearchKeyDown}
|
||||
endContent={
|
||||
<div className="flex flex-row -mr-3">
|
||||
<Button isIconOnly variant="light" onPress={onClickSearch}>
|
||||
<MagnifyingGlassIcon />
|
||||
</Button>
|
||||
<Button isIconOnly variant="light" onPress={onClickClear}>
|
||||
<XMarkIcon />
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-8">
|
||||
<Table aria-label="Schedule Table">
|
||||
@@ -145,7 +148,7 @@ export default function SchedulePage() {
|
||||
<TableCell>{schedule.location}</TableCell>
|
||||
<TableCell>{schedule.postalCode}</TableCell>
|
||||
<TableCell>
|
||||
<Chip color={getStatusColor(schedule.status)}>
|
||||
<Chip variant="flat" color={getStatusColor(schedule.status)}>
|
||||
{schedule.status}
|
||||
</Chip>
|
||||
</TableCell>
|
||||
|
||||
Reference in New Issue
Block a user