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 */}