diff --git a/src/components/MenuContent/index.tsx b/src/components/MenuContent/index.tsx index 8884e48..afb0d33 100644 --- a/src/components/MenuContent/index.tsx +++ b/src/components/MenuContent/index.tsx @@ -43,7 +43,7 @@ export default function MenuContent({ hidden }: PropType) { text: "Users", icon: , url: "/panel/user-list", - }, + }, userRole === "admin" && { text: "Charging Stations", icon: , @@ -54,13 +54,17 @@ export default function MenuContent({ hidden }: PropType) { icon: , url: "/panel/manager-list", // Placeholder for now }, - + userRole === "admin" && { text: "Vehicles", icon: , url: "/panel/vehicle-list", // Placeholder for now }, - + userRole === "manager" && { + text: "Add Slots", + icon: , + url: "/panel/EVslots", // Placeholder for now + }, ]; const filteredMenuItems = baseMenuItems.filter(Boolean); diff --git a/src/pages/EVSlotManagement/index.tsx b/src/pages/EVSlotManagement/index.tsx new file mode 100644 index 0000000..4d07a05 --- /dev/null +++ b/src/pages/EVSlotManagement/index.tsx @@ -0,0 +1,379 @@ +import React, { useState } from "react"; +import { + Tabs, + Tab, + Box, + Card, + CardContent, + Button, + Typography, + IconButton, + Stack, + Snackbar, +} from "@mui/material"; +import { CustomTextField } from "../../components/AddEditUserModel/styled.css.tsx"; +import AddCircleIcon from "@mui/icons-material/AddCircle"; +import DeleteIcon from "@mui/icons-material/Delete"; +import CalendarTodayRoundedIcon from "@mui/icons-material/CalendarTodayRounded"; +import { LocalizationProvider, DatePicker } from "@mui/x-date-pickers"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import dayjs from "dayjs"; +import { useNavigate } from "react-router-dom"; +const days = [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", +]; + +export default function EVSlotManagement() { + const [selectedDay, setSelectedDay] = useState("Monday"); + const [openingTime, setOpeningTime] = useState(""); + const [closingTime, setClosingTime] = useState(""); + const [slots, setSlots] = useState({}); + const [breakTime, setBreakTime] = useState([]); + const [error, setError] = useState(""); + const [successMessage, setSuccessMessage] = useState(null); + const [selectedDate, setSelectedDate] = useState( + dayjs() + ); + const navigate = useNavigate(); + const handleBack = () => { + navigate("/panel/dashboard"); // Navigate back to Role List + }; + // Add slot function with basic validation + const addSlot = (start: string, end: string) => { + if (!start || !end) { + setError("Start and End times are required."); + return; + } + + if (slots[selectedDay]) { + // Check if start time is before end time + const newSlot = { start, end }; + const overlap = slots[selectedDay].some( + (slot: any) => + (slot.start <= newSlot.start && slot.end > newSlot.start) || + (slot.start < newSlot.end && slot.end >= newSlot.end) + ); + + if (overlap) { + setError("The selected time overlaps with an existing slot."); + return; + } + } else { + slots[selectedDay] = []; + } + + slots[selectedDay].push({ start, end }); + setSlots({ ...slots }); + setError(""); + }; + + // Add break time function + const addBreak = (start: string, end: string) => { + if (!start || !end) { + setError("Break Start and End times are required."); + return; + } + + setBreakTime([...breakTime, { start, end }]); + setError(""); + }; + + // Delete slot function + const deleteSlot = (start: string, end: string) => { + const updatedSlots = slots[selectedDay].filter( + (slot: any) => !(slot.start === start && slot.end === end) + ); + setSlots({ + ...slots, + [selectedDay]: updatedSlots, + }); + }; + + // Delete break function + const deleteBreak = (start: string, end: string) => { + const updatedBreaks = breakTime.filter( + (breakItem: any) => + !(breakItem.start === start && breakItem.end === end) + ); + setBreakTime(updatedBreaks); + }; + + // Save function + const saveData = () => { + if (!openingTime || !closingTime) { + setError("Operating hours are required."); + return; + } + + // Simulate saving data (e.g., to a database or API) + setSuccessMessage( + `Data for ${selectedDay} has been saved successfully!` + ); + setError(""); // Clear any previous errors + }; + + // Close the success message + const handleCloseSnackbar = () => { + setSuccessMessage(null); + }; + + return ( + + + EV Station Slot Management + + {/* Date Picker */} + + + Select Date + + + setSelectedDate(newDate)} + renderInput={(props) => ( + + ), + }} + /> + )} + /> + + + setSelectedDay(newValue)} + variant="scrollable" + scrollButtons="auto" + sx={{ mt: 3 }} + > + {days.map((day) => ( + + ))} + + + + + Set Operating Hours for {selectedDay} + + + setOpeningTime(e.target.value)} + fullWidth + /> + setClosingTime(e.target.value)} + fullWidth + /> + + + + + Add Slots + + + + + + {error && ( + + {error} + + )} + + + + Break Time + + + + + + + + + + Slots for {selectedDay} + + {slots[selectedDay]?.length ? ( + slots[selectedDay].map((slot: any, index: number) => ( + + + {slot.start} - {slot.end} + + deleteSlot(slot.start, slot.end)} + > + + + + )) + ) : ( + No slots added + )} + + + + + Break Times for {selectedDay} + + {breakTime.length ? ( + breakTime.map((breakItem: any, index: number) => ( + + + {breakItem.start} - {breakItem.end} + + + deleteBreak(breakItem.start, breakItem.end) + } + > + + + + )) + ) : ( + No break times added + )} + + + + {/* Save Button */} + + + + {/* Success Snackbar */} + + + ); +} diff --git a/src/redux/reducers.ts b/src/redux/reducers.ts index b732c03..9faa3f6 100644 --- a/src/redux/reducers.ts +++ b/src/redux/reducers.ts @@ -8,6 +8,7 @@ import roleReducer from "./slices/roleSlice.ts"; import vehicleReducer from "./slices/VehicleSlice.ts"; import managerReducer from "../redux/slices/managerSlice.ts"; import stationReducer from "../redux/slices/stationSlice.ts"; +import slotReducer from "../redux/slices/slotSlice.ts"; const rootReducer = combineReducers({ @@ -18,7 +19,8 @@ const rootReducer = combineReducers({ roleReducer, vehicleReducer, managerReducer, - stationReducer + stationReducer, + slotReducer, // Add other reducers here... }); diff --git a/src/redux/slices/authSlice.ts b/src/redux/slices/authSlice.ts index 2f0f3f2..5b1b2e4 100644 --- a/src/redux/slices/authSlice.ts +++ b/src/redux/slices/authSlice.ts @@ -25,6 +25,7 @@ interface Admin { } interface AuthState { + managerId: any; user: User | null; admins: Admin[]; isAuthenticated: boolean; diff --git a/src/redux/slices/slotSlice.ts b/src/redux/slices/slotSlice.ts new file mode 100644 index 0000000..ee1e0e1 --- /dev/null +++ b/src/redux/slices/slotSlice.ts @@ -0,0 +1,190 @@ +import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit"; +import http from "../../lib/https"; // Assuming you have a custom HTTP library for requests +import { toast } from "sonner"; + +// Define TypeScript types +interface Slot { + id: number; + stationId: number; + startTime: Date; + endTime: Date; + status: number; +} + + +interface SlotState { + slots: Slot[]; + availableSlots: Slot[]; // Ensure it's initialized as an empty array + loading: boolean; + error: string | null; +} + +// Initial state +const initialState: SlotState = { + slots: [], + availableSlots: [], // <-- Initialize this as an empty array + loading: false, + error: null, +}; + +// Fetch Available Slots +export const fetchAvailableSlots = createAsyncThunk< + Slot[], // Return type + number, // Argument type (stationId) + { rejectValue: string } +>("slots/fetchAvailableSlots", async (stationId, { rejectWithValue }) => { + try { + const response = await http.get("/available", { stationId }); + return response.data.data; // Return the available slot data + } catch (error: any) { + toast.error("Error fetching available slots: " + error?.message); + return rejectWithValue(error.response?.data?.message || "An error occurred"); + } +}); + + + +export const createSlot = createAsyncThunk< + Slot, // Return type (Slot) + { + stationId: number; + startTime: Date; + endTime: Date; + isAvailable: boolean; + }, // Argument type (slot data) + { rejectValue: string } // Error type +>("slots/createSlot", async (slotData, { rejectWithValue }) => { + try { + const { stationId, startTime, endTime, isAvailable } = slotData; + + // Ensure that the start and end time are correctly formatted in ISO 8601 + const startDateTime = new Date(startTime).toISOString(); // convert to ISO string + const endDateTime = new Date(endTime).toISOString(); // convert to ISO string + + // Make sure the formatted times are valid + if ( + isNaN(new Date(startDateTime).getTime()) || + isNaN(new Date(endDateTime).getTime()) + ) { + throw new Error("Invalid date format"); + } + + const payload = { + stationId, + startTime: startDateTime, + endTime: endDateTime, + isAvailable, + }; + + // Send the request to create the slot + const response = await http.post("/create-slot", payload); + + // Show success message + toast.success("Slot created successfully"); + + // Return the response data (created slot) + return response.data.data; + } catch (error: any) { + // Show error message + toast.error("Error creating slot: " + error?.message); + + // Return a detailed error message if possible + return rejectWithValue( + error.response?.data?.message || "An error occurred" + ); + } +}); + + +// Update Slot details +export const updateSlot = createAsyncThunk< + Slot, // Return type + { id: number; startTime: string; endTime: string }, // Argument type (slot update data) + { rejectValue: string } +>("slots/updateSlot", async ({ id, ...slotData }, { rejectWithValue }) => { + try { + const response = await http.patch(`/slots/${id}`, slotData); + toast.success("Slot updated successfully"); + return response.data.data; // Return updated slot data + } catch (error: any) { + toast.error("Error updating the slot: " + error?.message); + return rejectWithValue(error.response?.data?.message || "An error occurred"); + } +}); + +// Delete Slot +export const deleteSlot = createAsyncThunk< + number, // Return type (id of deleted slot) + number, // Argument type (id of the slot) + { rejectValue: string } +>("slots/deleteSlot", async (id, { rejectWithValue }) => { + try { + const response = await http.delete(`/slots/${id}`); + toast.success("Slot deleted successfully"); + return id; // Return the id of the deleted slot + } catch (error: any) { + toast.error("Error deleting the slot: " + error?.message); + return rejectWithValue(error.response?.data?.message || "An error occurred"); + } +}); + +const slotSlice = createSlice({ + name: "slots", + initialState, + reducers: {}, + extraReducers: (builder) => { + builder + .addCase(fetchAvailableSlots.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase(fetchAvailableSlots.fulfilled, (state, action: PayloadAction) => { + state.loading = false; + state.availableSlots = action.payload; + }) + .addCase(fetchAvailableSlots.rejected, (state, action) => { + state.loading = false; + state.error = action.payload || "Failed to fetch available slots"; + }) + .addCase(createSlot.pending, (state) => { + state.loading = true; + }) + .addCase(createSlot.fulfilled, (state, action: PayloadAction) => { + state.loading = false; + state.slots.push(action.payload); + }) + .addCase(createSlot.rejected, (state, action) => { + state.loading = false; + state.error = action.payload || "Failed to create slot"; + }) + .addCase(updateSlot.pending, (state) => { + state.loading = true; + }) + .addCase(updateSlot.fulfilled, (state, action: PayloadAction) => { + state.loading = false; + // Update the slot in the state with the updated data + const index = state.slots.findIndex((slot) => slot.id === action.payload.id); + if (index !== -1) { + state.slots[index] = action.payload; + } + }) + .addCase(updateSlot.rejected, (state, action) => { + state.loading = false; + state.error = action.payload || "Failed to update slot"; + }) + .addCase(deleteSlot.pending, (state) => { + state.loading = true; + }) + .addCase(deleteSlot.fulfilled, (state, action: PayloadAction) => { + state.loading = false; + // Remove the deleted slot from the state + state.slots = state.slots.filter((slot) => slot.id !== action.payload); + }) + .addCase(deleteSlot.rejected, (state, action) => { + state.loading = false; + state.error = action.payload || "Failed to delete slot"; + }); + }, +}); + +export default slotSlice.reducer; diff --git a/src/router.tsx b/src/router.tsx index 5b2cbdb..3a7ca83 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -19,6 +19,7 @@ const AddEditRolePage = lazy(() => import("./pages/AddEditRolePage")); const RoleList = lazy(() => import("./pages/RoleList")); const ManagerList = lazy(() => import("./pages/ManagerList")); const StationList = lazy(() => import("./pages/StationList")); +const EVSlotManagement = lazy(() => import("./pages/EVSlotManagement")); interface ProtectedRouteProps { @@ -91,6 +92,12 @@ export default function AppRouter() { path="profile" element={} />} /> + } /> + } + /> {/* Catch-all Route */}