Implement the Slot Management UI

This commit is contained in:
jaanvi 2025-03-18 18:18:00 +05:30
parent ef2e6a3b51
commit 25f4563484
6 changed files with 587 additions and 4 deletions

View file

@ -60,7 +60,11 @@ export default function MenuContent({ hidden }: PropType) {
icon: <ManageAccountsOutlinedIcon />,
url: "/panel/vehicle-list", // Placeholder for now
},
userRole === "manager" && {
text: "Add Slots",
icon: <ManageAccountsOutlinedIcon />,
url: "/panel/EVslots", // Placeholder for now
},
];
const filteredMenuItems = baseMenuItems.filter(Boolean);

View file

@ -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<any>({});
const [breakTime, setBreakTime] = useState<any>([]);
const [error, setError] = useState<string>("");
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const [selectedDate, setSelectedDate] = useState<dayjs.Dayjs | null>(
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 (
<Box sx={{ maxWidth: 600, mx: "auto", p: 2 }}>
<Typography variant="h4" gutterBottom align="center">
EV Station Slot Management
</Typography>
{/* Date Picker */}
<Box
sx={{ display: "flex", justifyContent: "space-between", mt: 3 }}
>
<Typography variant="h4" gutterBottom align="center">
Select Date
</Typography>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
value={selectedDate}
onChange={(newDate) => setSelectedDate(newDate)}
renderInput={(props) => (
<CustomTextField
{...props}
fullWidth
label="Select Date"
InputProps={{
startAdornment: (
<CalendarTodayRoundedIcon fontSize="small" />
),
}}
/>
)}
/>
</LocalizationProvider>
</Box>
<Tabs
value={selectedDay}
onChange={(event, newValue) => setSelectedDay(newValue)}
variant="scrollable"
scrollButtons="auto"
sx={{ mt: 3 }}
>
{days.map((day) => (
<Tab
key={day}
label={day}
value={day}
sx={{
fontWeight: "bold",
fontSize: "16px",
"&.Mui-selected": {
backgroundColor: "#52ACDF",
color: "white",
borderRadius: "8px",
},
}}
/>
))}
</Tabs>
<Card sx={{ mt: 2, p: 2 }}>
<Typography variant="h6" gutterBottom>
Set Operating Hours for {selectedDay}
</Typography>
<Box sx={{ display: "flex", gap: 2, mt: 1 }}>
<CustomTextField
type="time"
label="Opening Time"
value={openingTime}
onChange={(e) => setOpeningTime(e.target.value)}
fullWidth
/>
<CustomTextField
type="time"
label="Closing Time"
value={closingTime}
onChange={(e) => setClosingTime(e.target.value)}
fullWidth
/>
</Box>
</Card>
<Card sx={{ mt: 2, p: 2 }}>
<Typography variant="h6">Add Slots</Typography>
<Box sx={{ display: "flex", gap: 2, mt: 1 }}>
<CustomTextField
type="time"
label="Start Time"
id="slotStart"
fullWidth
/>
<CustomTextField
type="time"
label="End Time"
id="slotEnd"
fullWidth
/>
<Button
sx={{
backgroundColor: "#52ACDF",
color: "white",
borderRadius: "8px",
width: "250px",
"&:hover": { backgroundColor: "#439BC1" },
}}
onClick={() =>
addSlot(
document.getElementById("slotStart").value,
document.getElementById("slotEnd").value
)
}
>
<AddCircleIcon sx={{ mr: 1 }} /> Add
</Button>
</Box>
{error && (
<Typography color="error" mt={1}>
{error}
</Typography>
)}
</Card>
<Card sx={{ mt: 2, p: 2 }}>
<Typography variant="h6">Break Time</Typography>
<Box sx={{ display: "flex", gap: 2, mt: 1 }}>
<CustomTextField
type="time"
label="Break Start"
id="breakStart"
fullWidth
/>
<CustomTextField
type="time"
label="Break End"
id="breakEnd"
fullWidth
/>
<Button
sx={{
backgroundColor: "#52ACDF",
color: "white",
borderRadius: "8px",
width: "250px",
"&:hover": { backgroundColor: "#439BC1" },
}}
onClick={() =>
addBreak(
document.getElementById("breakStart").value,
document.getElementById("breakEnd").value
)
}
>
<AddCircleIcon sx={{ mr: 1 }} /> Add
</Button>
</Box>
</Card>
<Card sx={{ mt: 2, p: 2 }}>
<Typography variant="h6" gutterBottom>
Slots for {selectedDay}
</Typography>
{slots[selectedDay]?.length ? (
slots[selectedDay].map((slot: any, index: number) => (
<Box
key={index}
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
mb: 1,
}}
>
<Typography variant="body1">
{slot.start} - {slot.end}
</Typography>
<IconButton
onClick={() => deleteSlot(slot.start, slot.end)}
>
<DeleteIcon color="error" />
</IconButton>
</Box>
))
) : (
<Typography>No slots added</Typography>
)}
</Card>
<Card sx={{ mt: 2, p: 2 }}>
<Typography variant="h6" gutterBottom>
Break Times for {selectedDay}
</Typography>
{breakTime.length ? (
breakTime.map((breakItem: any, index: number) => (
<Box
key={index}
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
mb: 1,
}}
>
<Typography variant="body1">
{breakItem.start} - {breakItem.end}
</Typography>
<IconButton
onClick={() =>
deleteBreak(breakItem.start, breakItem.end)
}
>
<DeleteIcon color="error" />
</IconButton>
</Box>
))
) : (
<Typography>No break times added</Typography>
)}
</Card>
<Box
sx={{ display: "flex", justifyContent: "space-between", mt: 3 }}
>
<Button
sx={{
backgroundColor: "#52ACDF",
color: "white",
borderRadius: "8px",
"&:hover": { backgroundColor: "#439BC1" },
width: "117px",
}}
onClick={handleBack}
>
Back
</Button>
{/* Save Button */}
<Button
type="submit"
sx={{
backgroundColor: "#52ACDF",
color: "white",
borderRadius: "8px",
width: "117px",
"&:hover": { backgroundColor: "#439BC1" },
}}
onClick={saveData}
>
Save
</Button>
</Box>
{/* Success Snackbar */}
<Snackbar
open={!!successMessage}
autoHideDuration={6000}
onClose={handleCloseSnackbar}
message={successMessage}
/>
</Box>
);
}

View file

@ -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...
});

View file

@ -25,6 +25,7 @@ interface Admin {
}
interface AuthState {
managerId: any;
user: User | null;
admins: Admin[];
isAuthenticated: boolean;

View file

@ -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<Slot[]>) => {
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<Slot>) => {
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<Slot>) => {
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<number>) => {
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;

View file

@ -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={<ProtectedRoute component={<ProfilePage />} />}
/>
<Route
path="EVslots"
element={
<ProtectedRoute component={<EVSlotManagement />} />
}
/>
</Route>
{/* Catch-all Route */}