Merge pull request 'Implement the Slot Management and User Booking API' (#23) from frontend/apiIntegration into develop

Reviewed-on: DigiMantra/digiev_frontend#23
This commit is contained in:
Mohit kalshan 2025-03-21 13:08:27 +00:00
commit a0db1c0dd8
18 changed files with 1518 additions and 62 deletions

BIN
public/DigiEVLogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

BIN
public/Digilogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -0,0 +1,358 @@
import React, { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import {
Box,
Button,
Typography,
Modal,
IconButton,
MenuItem,
Select,
InputLabel,
FormControl,
TextField,
} from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
import { useDispatch } from "react-redux";
import {
addBooking,
bookingList,
getCarNames,
getCarPorts,
} from "../../redux/slices/bookSlice";
import { AppDispatch } from "../../redux/store/store";
import { toast } from "sonner";
import {
CustomIconButton,
CustomTextField,
} from "../AddEditUserModel/styled.css.tsx";
export default function AddBookingModal({
open,
handleClose,
}: {
open: boolean;
handleClose: () => void;
}) {
const dispatch = useDispatch<AppDispatch>();
const {
control,
register,
handleSubmit,
formState: { errors },
reset,
} = useForm();
const [carNames, setCarNames] = useState<any[]>([]); // To hold the car names
const [carPorts, setCarPorts] = useState<any[]>([]); // To hold the car ports
useEffect(() => {
// Fetch car names and car ports
dispatch(getCarNames()).then((response: any) => {
const fetchedCarNames = response.payload || [];
setCarNames(fetchedCarNames);
});
dispatch(getCarPorts()).then((response: any) => {
const fetchedCarPorts = response.payload || [];
setCarPorts(fetchedCarPorts);
});
// Fetch the bookings after this
dispatch(bookingList());
}, [dispatch]);
// Get today's date in yyyy-mm-dd format
const today = new Date().toISOString().split("T")[0];
const onSubmit = async (data: any) => {
const bookingData = {
stationId: data.stationId,
date: data.date,
startTime: data.startTime,
endTime: data.endTime,
carName: data.carName,
carNumber: data.carNumber,
carPort: data.carPort,
};
try {
await dispatch(addBooking(bookingData));
dispatch(bookingList());
handleClose(); // Close modal after successful addition
reset(); // Reset form fields
} catch (error) {
console.error("Error adding booking:", error);
toast.error("Failed to add booking.");
}
};
return (
<Modal
open={open}
onClose={(e, reason) => {
if (reason === "backdropClick") return;
handleClose();
}}
aria-labelledby="add-booking-modal"
>
<Box
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 400,
bgcolor: "background.paper",
boxShadow: 24,
p: 3,
borderRadius: 2,
}}
>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography variant="h6" fontWeight={600}>
Add Booking
</Typography>
<CustomIconButton onClick={handleClose}>
<CloseIcon />
</CustomIconButton>
</Box>
<Box sx={{ borderBottom: "1px solid #ddd", my: 2 }} />
<form onSubmit={handleSubmit(onSubmit)}>
{/* Station ID and Car Name */}
<Box sx={{ display: "flex", gap: 2, mb: 2 }}>
{/* Station ID */}
<Box sx={{ flex: 1 }}>
<Typography variant="body2" fontWeight={500}>
Station ID
</Typography>
<CustomTextField
fullWidth
placeholder="Enter Station Id"
size="small"
error={!!errors.stationId}
helperText={
errors.stationId
? errors.stationId.message
: ""
}
{...register("stationId", {
required: "Station ID is required",
})}
/>
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="body2" fontWeight={500}>
Date
</Typography>
<CustomTextField
fullWidth
type="date"
size="small"
error={!!errors.date}
helperText={
errors.date ? errors.date.message : ""
}
{...register("date", {
required: "Date is required",
validate: (value) =>
value >= today ||
"Date cannot be in the past",
})}
InputLabelProps={{
shrink: true,
}}
// Setting minimum date to today
inputProps={{ min: today }}
/>
</Box>
</Box>
{/* Car Port and Date */}
<Box sx={{ display: "flex", gap: 2, mb: 2 }}>
{/* Car Name */}
<Box sx={{ flex: 1 }}>
<Typography variant="body2" fontWeight={500}>
Car Name
</Typography>
<Controller
control={control}
name="carName"
render={({ field }) => (
<FormControl
fullWidth
size="small"
error={!!errors.carName}
>
<InputLabel>Car Name</InputLabel>
<Select
{...field}
label="Car Name"
defaultValue=""
>
{carNames.map((car, index) => (
<MenuItem
key={car.id}
value={car.id}
>
{car.name}
</MenuItem>
))}
</Select>
{errors.carName && (
<Typography
color="error"
variant="body2"
sx={{ mt: 1 }}
>
{errors.carName.message}
</Typography>
)}
</FormControl>
)}
rules={{ required: "Car Name is required" }}
/>
</Box>
{/* Car Port */}
<Box sx={{ flex: 1 }}>
<Typography variant="body2" fontWeight={500}>
Car Port
</Typography>
<Controller
control={control}
name="carPort"
render={({ field }) => (
<FormControl
fullWidth
size="small"
error={!!errors.carPort}
>
<InputLabel>Car Port</InputLabel>
<Select
{...field}
label="Car Port"
defaultValue=""
>
{carPorts.map((port, index) => (
<MenuItem
key={index}
value={port.chargeType}
>
{port.chargeType}
</MenuItem>
))}
</Select>
{errors.carPort && (
<Typography
color="error"
variant="body2"
sx={{ mt: 1 }}
>
{errors.carPort.message}
</Typography>
)}
</FormControl>
)}
rules={{ required: "Car Port is required" }}
/>
</Box>
</Box>
{/* Start Time and End Time */}
<Box sx={{ display: "flex", gap: 2, mb: 2 }}>
{/* Start Time */}
<Box sx={{ flex: 1 }}>
<Typography variant="body2" fontWeight={500}>
Start Time
</Typography>
<CustomTextField
fullWidth
type="time"
size="small"
error={!!errors.startTime}
helperText={
errors.startTime
? errors.startTime.message
: ""
}
{...register("startTime", {
required: "Start Time is required",
})}
/>
</Box>
{/* End Time */}
<Box sx={{ flex: 1 }}>
<Typography variant="body2" fontWeight={500}>
End Time
</Typography>
<CustomTextField
fullWidth
type="time"
size="small"
error={!!errors.endTime}
helperText={
errors.endTime ? errors.endTime.message : ""
}
{...register("endTime", {
required: "End Time is required",
})}
/>
</Box>
</Box>
{/* Car Number */}
<Box sx={{ display: "flex", gap: 2, mb: 2 }}>
<Box sx={{ flex: 1 }}>
<Typography variant="body2" fontWeight={500}>
Car Number
</Typography>
<CustomTextField
fullWidth
placeholder="Enter Car Number"
size="small"
error={!!errors.carNumber}
helperText={
errors.carNumber
? errors.carNumber.message
: ""
}
{...register("carNumber", {
required: "Car Number is required",
})}
/>
</Box>
</Box>
{/* Submit Button */}
<Box
sx={{
display: "flex",
justifyContent: "flex-end",
mt: 2,
}}
>
<Button
type="submit"
sx={{
backgroundColor: "#52ACDF",
color: "white",
borderRadius: "8px",
width: "117px",
"&:hover": { backgroundColor: "#439BC1" },
}}
>
Add Booking
</Button>
</Box>
</form>
</Box>
</Modal>
);
}

View file

@ -378,7 +378,5 @@ const AddUserModal: React.FC<AddUserModalProps> = ({
export default AddUserModal;
function setValue(arg0: string, name: any) {
throw new Error("Function not implemented.");
}

View file

@ -0,0 +1,130 @@
import React, { useState } from "react";
import {
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Button,
TextField,
Typography,
Box,
} from "@mui/material";
import { useForm } from "react-hook-form";
const AddSlotModal = ({ open, handleClose, handleAddSlot }: any) => {
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm();
const [isAvailable, setIsAvailable] = useState<boolean>(true); // Default is available
// Get today's date in the format yyyy-mm-dd
const today = new Date().toISOString().split("T")[0];
const onSubmit = (data: {
date: string;
startHour: string;
endHour: string;
}) => {
handleAddSlot({ ...data, isAvailable });
reset();
handleClose();
};
return (
<Dialog open={open} onClose={handleClose}>
<DialogTitle>Add EV Slot</DialogTitle>
<DialogContent>
<form onSubmit={handleSubmit(onSubmit)}>
<TextField
{...register("date", {
required: "Date is required",
validate: (value) =>
value >= today || "Date cannot be in the past",
})}
label="Date"
type="date"
fullWidth
margin="normal"
InputLabelProps={{
shrink: true,
}}
error={!!errors.date}
helperText={errors.date?.message}
// Set the min value to today's date
inputProps={{ min: today }}
/>
<TextField
{...register("startHour", {
required: "Start hour is required",
})}
label="Start Hour"
type="time"
fullWidth
margin="normal"
InputLabelProps={{
shrink: true,
}}
error={!!errors.startHour}
helperText={errors.startHour?.message}
/>
<TextField
{...register("endHour", {
required: "End hour is required",
})}
label="End Hour"
type="time"
fullWidth
margin="normal"
InputLabelProps={{
shrink: true,
}}
error={!!errors.endHour}
helperText={errors.endHour?.message}
/>
{/* Availability Toggle */}
<Box
display="flex"
alignItems="center"
justifyContent="space-between"
gap={2}
>
<Button
onClick={() => setIsAvailable((prev) => !prev)}
variant={isAvailable ? "contained" : "outlined"}
color="primary"
>
Check Availability
</Button>
<Typography>
{isAvailable ? "Available" : "Not Available"}
</Typography>
</Box>
<DialogActions>
<Button onClick={handleClose} color="secondary">
Cancel
</Button>
<Button
type="submit"
sx={{
backgroundColor: "#52ACDF",
color: "white",
borderRadius: "8px",
width: "100px",
"&:hover": { backgroundColor: "#439BC1" },
}}
>
Add Booking
</Button>
</DialogActions>
</form>
</DialogContent>
</Dialog>
);
};
export default AddSlotModal;

View file

@ -11,7 +11,7 @@ import { adminList, deleteAdmin } from "../../redux/slices/adminSlice";
import { vehicleList, deleteVehicle } from "../../redux/slices/VehicleSlice";
import { deleteManager, managerList } from "../../redux/slices/managerSlice";
import { useDispatch } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import {
Box,
Button,
@ -24,7 +24,7 @@ import {
} from "@mui/material";
import MoreHorizRoundedIcon from "@mui/icons-material/MoreHorizRounded";
import DeleteModal from "../Modals/DeleteModal";
import { AppDispatch } from "../../redux/store/store";
import { AppDispatch, RootState } from "../../redux/store/store";
import ViewModal from "../Modals/ViewModal";
import VehicleViewModal from "../Modals/VehicleViewModal";
@ -36,6 +36,11 @@ import UserViewModal from "../Modals/UserViewModal/index.tsx";
import { deleteUser, userList } from "../../redux/slices/userSlice.ts";
import { deleteStation } from "../../redux/slices/stationSlice.ts";
import StationViewModal from "../Modals/StationViewModal/index.tsx";
import {
deleteSlot,
fetchAvailableSlots,
} from "../../redux/slices/slotSlice.ts";
import { bookingList } from "../../redux/slices/bookSlice.ts";
// Styled components for customization
const StyledTableCell = styled(TableCell)(({ theme }) => ({
[`&.${tableCellClasses.head}`]: {
@ -103,21 +108,16 @@ const CustomTable: React.FC<CustomTableProps> = ({
const [searchQuery, setSearchQuery] = React.useState("");
const [currentPage, setCurrentPage] = React.useState(1);
const usersPerPage = 10;
const { user, isLoading } = useSelector(
(state: RootState) => state?.profileReducer
);
const open = Boolean(anchorEl);
console.log("Rows", rows);
const handleClick = (event: React.MouseEvent<HTMLElement>, row: Row) => {
setAnchorEl(event.currentTarget);
setSelectedRow(row);
setRowData(row);
};
// const handleViewButton = (id: string | undefined) => {
// if (!id) {
// console.error("ID not found for viewing.");
// return;
// }
// setViewModal(true);
// };
const handleClose = () => {
setAnchorEl(null);
@ -131,9 +131,13 @@ const CustomTable: React.FC<CustomTableProps> = ({
};
const handleDeleteButton = (id: string | undefined) => {
if (!id) console.error("ID not found", id);
if (!id) {
console.error("ID not found", id);
return;
}
switch (tableType) {
case "admin":
dispatch(deleteAdmin(id || ""));
break;
@ -149,6 +153,9 @@ const CustomTable: React.FC<CustomTableProps> = ({
case "station":
dispatch(deleteStation(id || ""));
break;
case "slots":
dispatch(deleteSlot(id || ""));
break;
default:
console.error("Unknown table type:", tableType);
return;
@ -159,23 +166,29 @@ const CustomTable: React.FC<CustomTableProps> = ({
const handleViewButton = (id: string | undefined) => {
if (!id) console.error("ID not found", id);
// switch (tableType) {
// case "admin":
// dispatch(adminList());
// break;
// case "vehicle":
// dispatch(vehicleList());
// break;
// case "manager":
// dispatch(managerList());
// break;
// case "user":
// dispatch(userList());
// break;
// default:
// console.error("Unknown table type:", tableType);
// return;
// }
switch (tableType) {
case "admin":
dispatch(adminList());
break;
case "vehicle":
dispatch(vehicleList());
break;
case "manager":
dispatch(managerList());
break;
case "user":
dispatch(userList());
break;
case "booking":
dispatch(bookingList());
break;
case "slots":
dispatch(fetchAvailableSlots());
break;
default:
console.error("Unknown table type:", tableType);
return;
}
setViewModal(false);
};
@ -238,6 +251,10 @@ const CustomTable: React.FC<CustomTableProps> = ({
? "Vehicles"
: tableType === "station"
? "Charging Station"
: tableType === "booking"
? "Booking"
: tableType === "slots"
? "Slot"
: "List"}
</Typography>
@ -288,31 +305,37 @@ const CustomTable: React.FC<CustomTableProps> = ({
width: "100%",
}}
>
<Button
sx={{
backgroundColor: "#52ACDF",
color: "white",
borderRadius: "8px",
width: "184px",
"&:hover": { backgroundColor: "#439BC1" },
}}
onClick={() => handleClickOpen()}
>
Add{" "}
{tableType === "admin"
? "Admin"
: tableType === "role"
? "Role"
: tableType === "user"
? "User"
: tableType === "manager"
? "Manager"
: tableType === "vehicle"
? "Vehicle"
: tableType === "station"
? "Charging Station"
: "Item"}
</Button>
{!(user?.userType === "user" && tableType === "slots") && (
<Button
sx={{
backgroundColor: "#52ACDF",
color: "white",
borderRadius: "8px",
width: "184px",
"&:hover": { backgroundColor: "#439BC1" },
}}
onClick={() => handleClickOpen()}
>
Add{" "}
{tableType === "admin"
? "Admin"
: tableType === "role"
? "Role"
: tableType === "user"
? "User"
: tableType === "manager"
? "Manager"
: tableType === "vehicle"
? "Vehicle"
: tableType === "station"
? "Charging Station"
: tableType === "booking"
? "Booking"
: tableType === "slots"
? "Slot"
: "Item"}
</Button>
)}
</Box>
<IconButton

View file

@ -43,7 +43,7 @@ export default function MenuContent({ hidden }: PropType) {
text: "Users",
icon: <AnalyticsRoundedIcon />,
url: "/panel/user-list",
},
},
userRole === "admin" && {
text: "Charging Stations",
icon: <ManageAccountsOutlinedIcon />,
@ -54,13 +54,32 @@ export default function MenuContent({ hidden }: PropType) {
icon: <ManageAccountsOutlinedIcon />,
url: "/panel/manager-list", // Placeholder for now
},
userRole === "admin" && {
text: "Vehicles",
icon: <ManageAccountsOutlinedIcon />,
url: "/panel/vehicle-list", // Placeholder for now
},
// userRole === "manager" && {
// text: "Add Slots",
// icon: <ManageAccountsOutlinedIcon />,
// url: "/panel/EVslots", // Placeholder for now
// },
userRole === "user" && {
text: "Bookings",
icon: <ManageAccountsOutlinedIcon />,
url: "/panel/booking-list", // Placeholder for now
},
userRole === "manager" && {
text: "Available Slots",
icon: <ManageAccountsOutlinedIcon />,
url: "/panel/slot-list", // Placeholder for now
},
userRole === "user" && {
text: "Available Slots",
icon: <ManageAccountsOutlinedIcon />,
url: "/panel/slot-list", // Placeholder for now
},
];
const filteredMenuItems = baseMenuItems.filter(Boolean);

View file

@ -55,6 +55,52 @@ export default function SideMenu() {
},
}}
>
<Box
sx={{
display: "flex",
flexDirection: "row",
alignItems: "center",
pt: 2,
pl: 2,
}}
>
<Avatar
alt="Logo"
src="/Digilogo.png"
sx={{
width: "50px",
height: "50px",
}}
/>
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
pt: 2,
textAlign: "center",
}}
>
{/* Digi EV Text Section */}
<Typography
variant="body2"
color="#D9D8D8"
sx={{
fontWeight: "bold",
fontSize: "16px",
}}
>
Digi EV
</Typography>
<Typography variant="subtitle2" color="#D9D8D8" >
{user?.userType || "N/A"}
</Typography>
</Box>
</Box>
<Box
sx={{
display: "flex",
@ -69,7 +115,6 @@ export default function SideMenu() {
</Button>
</Box>
<MenuContent hidden={open} />
</Drawer>
);
}

View file

@ -97,6 +97,22 @@ export default function Login(props: { disableCustomTheme?: boolean }) {
height: "auto",
}}
>
<Box
sx={{
textAlign: "center",
marginBottom: "1rem",
}}
>
<img
src="/DigiEVLogo.png"
alt="Logo"
style={{
justifyContent: "center",
width: "250px",
height: "auto",
}}
/>
</Box>
<Typography
variant="h3"
sx={{

View file

@ -0,0 +1,135 @@
import React, { useEffect, useState } from "react";
import { Box, Button, TextField, Typography } from "@mui/material";
import CustomTable, { Column } from "../../components/CustomTable";
import { useDispatch, useSelector } from "react-redux";
import { RootState, AppDispatch } from "../../redux/store/store";
import { useForm } from "react-hook-form";
import { addBooking, bookingList } from "../../redux/slices/bookSlice";
import AddBookingModal from "../../components/AddBookingModal";
export default function BookingList() {
const [addModalOpen, setAddModalOpen] = useState<boolean>(false);
const [editModalOpen, setEditModalOpen] = useState<boolean>(false);
const [editRow, setEditRow] = useState<any>(null);
const { reset } = useForm();
const [deleteModal, setDeleteModal] = useState<boolean>(false);
const [viewModal, setViewModal] = useState<boolean>(false);
const [rowData, setRowData] = useState<any | null>(null);
const [searchTerm, setSearchTerm] = useState("");
const dispatch = useDispatch<AppDispatch>();
const bookings = useSelector(
(state: RootState) => state?.bookReducer.bookings
);
const users = useSelector((state: RootState) => state?.profileReducer.user);
useEffect(() => {
dispatch(bookingList());
}, [dispatch]);
const handleClickOpen = () => {
setRowData(null);
setAddModalOpen(true);
};
const handleCloseModal = () => {
setAddModalOpen(false);
setEditModalOpen(false);
setRowData(null);
reset();
};
// Updated handleAddBooking function to handle the added fields
const handleAddBooking = async (data: {
stationId: string;
date: string;
startTime: string;
endTime: string;
carName: string;
carNumber: string;
carPort: string;
}) => {
try {
// Dispatch addBooking with new data
await dispatch(addBooking(data));
await dispatch(bookingList());
handleCloseModal(); // Close the modal after adding the booking
} catch (error) {
console.error("Error adding booking", error);
}
};
// Table Columns for bookings
const categoryColumns: Column[] = [
{ id: "srno", label: "Sr No" },
{ id: "name", label: "User Name" },
{ id: "stationId", label: "Station Id" },
{ id: "stationName", label: "Station Name" },
{ id: "stationLocation", label: "Station Location" },
{ id: "date", label: "Date" },
{ id: "startTime", label: "Start Time" },
{ id: "endTime", label: "End Time" },
{ id: "carName", label: "Vehicle Name" },
{ id: "carNumber", label: "Vehicle Number" },
{ id: "carPort", label: "Charging Port" },
{ id: "action", label: "Action", align: "center" },
];
// Table Rows for bookings
// Table Rows for bookings
const categoryRows = bookings?.length
? bookings?.map(function (
booking: {
id: number;
stationId: string;
stationName: string;
stationLocation: string;
date: string;
startTime: string;
endTime: string;
carName: string;
carNumber: string;
carPort: string;
},
index: number
) {
return {
id: booking?.id ?? "NA",
srno: index + 1,
name: users?.name ?? "NA",
stationId: booking?.stationId ?? "NA",
stationName: booking?.stationName ?? "NA",
stationLocation: booking?.stationLocation ?? "NA",
date: booking?.date ?? "NA",
startTime: booking?.startTime ?? "NA",
endTime: booking?.endTime ?? "NA",
carName: booking?.carName ?? "NA", // Directly access carName
carNumber: booking?.carNumber ?? "NA", // Directly access carNumber
carPort: booking?.carPort ?? "NA",
};
})
: [];
return (
<>
<CustomTable
columns={categoryColumns}
rows={categoryRows}
setDeleteModal={setDeleteModal}
deleteModal={deleteModal}
setViewModal={setViewModal}
viewModal={viewModal}
setRowData={setRowData}
setModalOpen={() => setEditModalOpen(true)}
tableType="booking"
handleClickOpen={handleClickOpen}
/>
{/* Add Booking Modal */}
<AddBookingModal
open={addModalOpen}
handleClose={handleCloseModal}
handleAddBooking={handleAddBooking} // Passing the handleAddBooking function
/>
</>
);
}

View file

@ -0,0 +1,221 @@
import React, { useEffect, useState } from "react";
import {
Tabs,
Tab,
Box,
Card,
Typography,
IconButton,
Button,
} 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";
import { useDispatch, useSelector } from "react-redux";
import {
fetchAvailableSlots,
createSlot,
deleteSlot,
} from "../../redux/slices/slotSlice.ts";
import { RootState } from "../../redux/reducers.ts";
const days = [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
];
export default function EVSlotManagement() {
const [selectedDay, setSelectedDay] = useState("Monday");
const [breakTime, setBreakTime] = useState<any>([]);
const [error, setError] = useState<string>("");
const [selectedDate, setSelectedDate] = useState<dayjs.Dayjs | null>(
dayjs()
);
const navigate = useNavigate();
const dispatch = useDispatch();
const { availableSlots } = useSelector(
(state: RootState) => state?.slotReducer
);
useEffect(() => {
dispatch(fetchAvailableSlots());
}, [dispatch]);
const handleBack = () => {
navigate("/panel/dashboard");
};
// Add Slot
const addSlot = (start: string, end: string) => {
const selectedDateFormatted = selectedDate?.format("YYYY-MM-DD");
dispatch(
createSlot({
date: selectedDateFormatted,
startHour: start,
endHour: end,
isAvailable: true,
})
).then(() => {
dispatch(fetchAvailableSlots());
});
};
// Delete slot
const handleDeleteSlot = (slotId: string) => {
dispatch(deleteSlot(slotId));
dispatch(fetchAvailableSlots());
};
// Filter the available slots for the selected date's day
const filteredSlots = availableSlots.filter((slot: any) => {
// Get the day of the week for each slot
const slotDate = dayjs(slot.date);
const slotDay = slotDate.format("dddd"); // Get the day of the week (e.g., "Monday")
// Compare the selected day with the slot's day
return slotDay === selectedDay; // Only show slots that match the selected day
});
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">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" gutterBottom>
Slots for {selectedDay}
</Typography>
{filteredSlots.length ? (
filteredSlots.map((slot: any, index: number) => (
<Box
key={index}
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
mb: 1,
}}
>
<Typography variant="body1">
{dayjs(slot.date).format("YYYY-MM-DD")} -{" "}
{dayjs(slot.startTime).format("HH:mm")} -{" "}
{dayjs(slot.endTime).format("HH:mm")}
</Typography>
<IconButton
onClick={() => handleDeleteSlot(slot.id)}
>
<DeleteIcon color="error" />
</IconButton>
</Box>
))
) : (
<Typography>No slots added for {selectedDay}</Typography>
)}
</Card>
</Box>
);
}

View file

@ -0,0 +1,110 @@
import React, { useEffect, useState } from "react";
import { Box, Button, TextField, Typography } from "@mui/material";
import CustomTable, { Column } from "../../components/CustomTable";
import { useDispatch, useSelector } from "react-redux";
import { RootState, AppDispatch } from "../../redux/store/store";
import { useForm } from "react-hook-form";
import { createSlot, fetchAvailableSlots } from "../../redux/slices/slotSlice";
import AddSlotModal from "../../components/AddSlotModal";
import dayjs from "dayjs";
export default function EVSlotList() {
const [addModalOpen, setAddModalOpen] = useState<boolean>(false);
const [editModalOpen, setEditModalOpen] = useState<boolean>(false);
const [editRow, setEditRow] = useState<any>(null);
const { reset } = useForm();
const [deleteModal, setDeleteModal] = useState<boolean>(false);
const [viewModal, setViewModal] = useState<boolean>(false);
const [rowData, setRowData] = useState<any | null>(null);
const [searchTerm, setSearchTerm] = useState("");
const dispatch = useDispatch<AppDispatch>();
const availableSlots = useSelector(
(state: RootState) => state?.slotReducer.availableSlots
);
useEffect(() => {
dispatch(fetchAvailableSlots());
}, [dispatch]);
const handleClickOpen = () => {
setRowData(null);
setAddModalOpen(true);
};
const handleCloseModal = () => {
setAddModalOpen(false);
setEditModalOpen(false);
setRowData(null);
reset();
};
const handleAddSlot = async (data: {
date: string;
startTime: string;
endTime: string;
isAvailable: boolean;
}) => {
try {
await dispatch(createSlot(data));
await dispatch(fetchAvailableSlots());
handleCloseModal();
} catch (error) {
console.error("Error adding slot", error);
}
};
const slotColumns: Column[] = [
{ id: "srno", label: "Sr No" },
{ id: "name", label: "Station Name" },
{ id: "stationId", label: "Station Id" },
{ id: "date", label: "Date" },
{ id: "startTime", label: "Start Time" },
{ id: "endTime", label: "End Time" },
{ id: "isAvailable", label: "Is Available", align: "center" },
{ id: "action", label: "Action", align: "center" },
];
// Make sure dayjs is imported
const slotRows = availableSlots?.length
? availableSlots.map((slot, index) => {
const formattedDate = dayjs(slot?.date).format("YYYY-MM-DD");
const startTime = dayjs(slot?.startTime).format("HH:mm");
const endTime = dayjs(slot?.endTime).format("HH:mm");
console.log("first", startTime);
return {
srno: index + 1,
id: slot?.id ?? "NA",
stationId: slot?.stationId ?? "NA",
name: slot?.ChargingStation?.name ?? "NA",
date: formattedDate ?? "NA",
startTime: startTime ?? "NA",
endTime: endTime ?? "NA",
isAvailable: slot?.isAvailable ? "Yes" : "No",
};
})
: [];
console.log("Rows",slotRows)
return (
<>
<CustomTable
columns={slotColumns}
rows={slotRows}
setDeleteModal={setDeleteModal}
deleteModal={deleteModal}
setViewModal={setViewModal}
viewModal={viewModal}
setRowData={setRowData}
setModalOpen={() => setEditModalOpen(true)}
tableType="slots"
handleClickOpen={handleClickOpen}
/>
<AddSlotModal
open={addModalOpen}
handleClose={handleCloseModal}
handleAddSlot={handleAddSlot}
/>
</>
);
}

View file

@ -8,6 +8,8 @@ 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";
import bookReducer from "../redux/slices/bookSlice.ts";
const rootReducer = combineReducers({
@ -18,7 +20,9 @@ const rootReducer = combineReducers({
roleReducer,
vehicleReducer,
managerReducer,
stationReducer
stationReducer,
slotReducer,
bookReducer,
// 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,174 @@
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
import http from "../../lib/https";
import { toast } from "sonner";
// Define the structure for carDetails
interface CarDetails {
name: string;
number: string;
model: string;
}
interface Booking {
id: number;
slotId: number;
stationId: string;
stationName: string;
stationLocation: string;
date: string;
startTime: string;
endTime: string;
carDetails: CarDetails;
}
interface BookingState {
bookings: Booking[];
loading: boolean;
error: string | null;
carNames: string[]; // For car names
carPorts: string[]; // For car ports
}
const initialState: BookingState = {
bookings: [],
loading: false,
error: null,
carNames: [], // Initialize carNames
carPorts: [], // Initialize carPorts
};
// Redux slice for fetching car names and ports
export const getCarNames = createAsyncThunk<
string[],
void,
{ rejectValue: string }
>("fetchCarNames", async (_, { rejectWithValue }) => {
try {
const response = await http.get("/get-vehicle-dropdown");
return response.data.data; // Adjust based on actual API response
} catch (error: any) {
return rejectWithValue(
error?.response?.data?.message || "An error occurred"
);
}
});
export const getCarPorts = createAsyncThunk<
string[],
void,
{ rejectValue: string }
>("fetchCarPorts", async (_, { rejectWithValue }) => {
try {
const response = await http.get("/get-vehicle-port-dropdown");
return response.data.data; // Adjust based on actual API response
} catch (error: any) {
return rejectWithValue(
error?.response?.data?.message || "An error occurred"
);
}
});
// Fetch booking list
export const bookingList = createAsyncThunk<
Booking[],
void,
{ rejectValue: string }
>("fetchBooking", async (_, { rejectWithValue }) => {
try {
const token = localStorage?.getItem("authToken");
if (!token) throw new Error("No token found");
const response = await http.get("/user-bookings");
console.log("API Response:", response);
if (!response.data?.data) throw new Error("Invalid API response");
return response.data.data; // Updated to access data
} catch (error: any) {
toast.error("Error Fetching User Booking List: " + error.message);
return rejectWithValue(
error?.response?.data?.message || "An error occurred"
);
}
});
// Add a new booking
export const addBooking = createAsyncThunk<
Booking,
{
stationId: string;
date: string;
startTime: string;
endTime: string;
carName: string;
carNumber: string;
carPort: string;
},
{ rejectValue: string }
>("/AddBooking", async (data, { rejectWithValue }) => {
try {
const response = await http.post("/book-slot", data);
toast.success("EV Booked Successfully!");
return response.data.data; // Adjust to match the new API response structure
} catch (error: any) {
toast.error(
"The requested time slot doesn't fall within any available slot "
);
return rejectWithValue(
error.response?.data?.message || "An error occurred"
);
}
});
const bookSlice = createSlice({
name: "booking",
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(bookingList.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(
bookingList.fulfilled,
(state, action: PayloadAction<Booking[]>) => {
state.loading = false;
state.bookings = action.payload;
}
)
.addCase(bookingList.rejected, (state, action) => {
state.loading = false;
state.error =
action.error.message || "Failed to fetch bookings";
})
.addCase(addBooking.pending, (state) => {
state.loading = true;
})
.addCase(
addBooking.fulfilled,
(state, action: PayloadAction<Booking>) => {
state.loading = false;
state.bookings.push(action.payload); // Add new booking to state
}
)
.addCase(addBooking.rejected, (state) => {
state.loading = false;
})
// Add case reducers for fetching car names and ports
.addCase(
getCarNames.fulfilled,
(state, action: PayloadAction<string[]>) => {
state.carNames = action.payload;
}
)
.addCase(
getCarPorts.fulfilled,
(state, action: PayloadAction<string[]>) => {
state.carPorts = action.payload;
}
);
},
});
export default bookSlice.reducer;

View file

@ -0,0 +1,205 @@
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: string;
stationId:string;
date: string;
startTime: string;
endTime: string;
isAvailable: boolean;
ChargingStation: { name: string };
}
interface SlotState {
slots: Slot[];
availableSlots: Slot[];
loading: boolean;
error: string | null;
}
// Initial state
const initialState: SlotState = {
slots: [],
availableSlots: [], // <-- Initialize this as an empty array
loading: false,
error: null,
};
export const fetchAvailableSlots = createAsyncThunk<
Slot[],
void,
{ rejectValue: string }
>("fetchVehicles", async (_, { rejectWithValue }) => {
try {
const token = localStorage?.getItem("authToken");
if (!token) throw new Error("No token found");
const response = await http.get("/available");
if (!response.data?.data) throw new Error("Invalid API response");
return response.data.data;
} catch (error: any) {
toast.error("Error Fetching Profile" + error);
return rejectWithValue(
error?.response?.data?.message || "An error occurred"
);
}
});
export const createSlot = createAsyncThunk<
Slot,
{
date: string;
startTime: string;
endTime: string;
isAvailable: boolean;
},
{ rejectValue: string }
>("slots/createSlot", async (payload, { rejectWithValue }) => {
try {
// const payload = {
// date,
// startHour,
// endHour,
// isAvailable,
// };
const token = localStorage?.getItem("authToken");
if (!token) throw new Error("No token found");
const response = await http.post("/create-slot", payload);
toast.success("Slot created successfully");
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,
{ 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"
);
}
});
export const deleteSlot = createAsyncThunk<
string, // Return type (id of deleted slot)
string,
{ rejectValue: string }
>("slots/deleteSlot", async (id, { rejectWithValue }) => {
try {
const response = await http.delete(`/delete-slot/${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<string>) => {
state.loading = false;
// Ensure we're filtering the correct array (availableSlots)
state.availableSlots = state.availableSlots.filter(
(slot) => String(slot.id) !== String(action.payload)
);
// Also update slots array if it exists
if (state.slots) {
state.slots = state.slots.filter(
(slot) => String(slot.id) !== String(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,9 @@ 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"));
const BookingList = lazy(() => import("./pages/BookingList"));
const EvSlotList = lazy(() => import("./pages/EvSlotList"));
interface ProtectedRouteProps {
@ -91,6 +94,20 @@ export default function AppRouter() {
path="profile"
element={<ProtectedRoute component={<ProfilePage />} />}
/>
<Route
path="EVslots"
element={
<ProtectedRoute component={<EVSlotManagement />} />
}
/>
<Route
path="booking-list"
element={<ProtectedRoute component={<BookingList />} />}
/>
<Route
path="slot-list"
element={<ProtectedRoute component={<EvSlotList />} />}
/>
</Route>
{/* Catch-all Route */}