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:
commit
a0db1c0dd8
BIN
public/DigiEVLogo.png
Normal file
BIN
public/DigiEVLogo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2 KiB |
BIN
public/Digilogo.png
Normal file
BIN
public/Digilogo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
BIN
public/digimantra_labs_logo.jpeg
Normal file
BIN
public/digimantra_labs_logo.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
358
src/components/AddBookingModal/index.tsx
Normal file
358
src/components/AddBookingModal/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -378,7 +378,5 @@ const AddUserModal: React.FC<AddUserModalProps> = ({
|
|||
|
||||
export default AddUserModal;
|
||||
|
||||
function setValue(arg0: string, name: any) {
|
||||
throw new Error("Function not implemented.");
|
||||
}
|
||||
|
||||
|
||||
|
|
130
src/components/AddSlotModal/index.tsx
Normal file
130
src/components/AddSlotModal/index.tsx
Normal 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;
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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={{
|
||||
|
|
135
src/pages/BookingList/index.tsx
Normal file
135
src/pages/BookingList/index.tsx
Normal 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
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
221
src/pages/EVSlotManagement/index.tsx
Normal file
221
src/pages/EVSlotManagement/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
110
src/pages/EvSlotList/index.tsx
Normal file
110
src/pages/EvSlotList/index.tsx
Normal 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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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...
|
||||
});
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ interface Admin {
|
|||
}
|
||||
|
||||
interface AuthState {
|
||||
managerId: any;
|
||||
user: User | null;
|
||||
admins: Admin[];
|
||||
isAuthenticated: boolean;
|
||||
|
|
174
src/redux/slices/bookSlice.ts
Normal file
174
src/redux/slices/bookSlice.ts
Normal 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;
|
205
src/redux/slices/slotSlice.ts
Normal file
205
src/redux/slices/slotSlice.ts
Normal 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;
|
|
@ -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 */}
|
||||
|
|
Loading…
Reference in a new issue