diff --git a/public/Group 14.svg b/public/Group 14.svg new file mode 100644 index 0000000..503ae50 --- /dev/null +++ b/public/Group 14.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/evLogo.png b/public/evLogo.png new file mode 100644 index 0000000..0c96ea1 Binary files /dev/null and b/public/evLogo.png differ diff --git a/src/components/AddBookingModal/index.tsx b/src/components/AddBookingModal/index.tsx index c28f93a..06bfb82 100644 --- a/src/components/AddBookingModal/index.tsx +++ b/src/components/AddBookingModal/index.tsx @@ -13,19 +13,22 @@ import { TextField, } from "@mui/material"; import CloseIcon from "@mui/icons-material/Close"; -import { useDispatch } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { addBooking, bookingList, getCarNames, getCarPorts, } from "../../redux/slices/bookSlice"; -import { AppDispatch } from "../../redux/store/store"; +import { AppDispatch, RootState } from "../../redux/store/store"; import { toast } from "sonner"; import { CustomIconButton, CustomTextField, } from "../AddEditUserModel/styled.css.tsx"; +import { getAllStations, stationList } from "../../redux/slices/stationSlice.ts"; +import { fetchAvailableSlots } from "../../redux/slices/slotSlice.ts"; +import dayjs from "dayjs"; export default function AddBookingModal({ open, @@ -44,6 +47,17 @@ export default function AddBookingModal({ } = useForm(); const [carNames, setCarNames] = useState([]); // To hold the car names const [carPorts, setCarPorts] = useState([]); // To hold the car ports + const stations = useSelector( + (state: RootState) => state?.stationReducer.stations + ); + const availableSlots = useSelector( + (state: RootState) => state.slotReducer.availableSlots + ); + console.log("first", availableSlots); + useEffect(() => { + dispatch(fetchAvailableSlots()); + dispatch(getAllStations()); + }, [dispatch]); useEffect(() => { // Fetch car names and car ports @@ -60,13 +74,13 @@ export default function AddBookingModal({ // Fetch the bookings after this dispatch(bookingList()); }, [dispatch]); - +console.log("Car Ports: ", carPorts); // 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, + stationId: data.stationId, // Using stationId here for the backend date: data.date, startTime: data.startTime, endTime: data.endTime, @@ -130,23 +144,34 @@ export default function AddBookingModal({ {/* Station ID */} - Station ID + Select Station - + + Select Station + + {errors.stationName && ( + + {errors.stationName.message} + + )} + + Date @@ -190,7 +215,6 @@ export default function AddBookingModal({ size="small" error={!!errors.carName} > - Car Name @@ -266,7 +290,6 @@ export default function AddBookingModal({ {/* Start Time and End Time */} - {/* Start Time */} Start Time @@ -287,7 +310,6 @@ export default function AddBookingModal({ /> - {/* End Time */} End Time @@ -328,6 +350,114 @@ export default function AddBookingModal({ })} /> + + {/* + + {" Time Slot "} + + ( + + + Select Time Slot + + + {errors.timeSlot && ( + + {errors.timeSlot.message} + + )} + + )} + rules={{ required: "Time Slot is required" }} + /> + */} {/* Submit Button */} diff --git a/src/components/AddEditUserModel/styled.css.tsx b/src/components/AddEditUserModel/styled.css.tsx index 7bd3874..ebed712 100644 --- a/src/components/AddEditUserModel/styled.css.tsx +++ b/src/components/AddEditUserModel/styled.css.tsx @@ -3,7 +3,7 @@ import { IconButton, TextField } from "@mui/material"; export const CustomIconButton = styled(IconButton)({ backgroundColor: "transparent", "&:hover": { - backgroundColor: "#272727", + backgroundColor: "transparent", }, "*:where([data-mui-color-scheme='dark']) &": { backgroundColor: "transparent", @@ -20,7 +20,4 @@ export const CustomTextField = styled(TextField)({ "& .MuiInputBase-root.Mui-focused .MuiInputBase-input::placeholder": { color: "darkgray", }, - }); - - diff --git a/src/components/AddManagerModal/index.tsx b/src/components/AddManagerModal/index.tsx index 5309be9..41f745c 100644 --- a/src/components/AddManagerModal/index.tsx +++ b/src/components/AddManagerModal/index.tsx @@ -6,21 +6,28 @@ import { Modal, IconButton, InputAdornment, + Select, + MenuItem, + InputLabel, + FormControl, } from "@mui/material"; import CloseIcon from "@mui/icons-material/Close"; import { Visibility, VisibilityOff } from "@mui/icons-material"; -import { useDispatch } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { addManager, managerList } from "../../redux/slices/managerSlice"; import { CustomIconButton, CustomTextField, } from "../AddEditUserModel/styled.css.tsx"; -import React, { useState, useRef } from "react"; +import React, { useEffect, useState } from "react"; +import { RootState } from "../../redux/reducers.ts"; +import { stationList } from "../../redux/slices/stationSlice.ts"; export default function AddManagerModal({ open, handleClose, handleAddManager, + // Assume the stations prop contains a list of station objects with 'id' and 'name' }) { const dispatch = useDispatch(); const { @@ -31,16 +38,26 @@ export default function AddManagerModal({ reset, } = useForm(); const [showPassword, setShowPassword] = useState(false); + const stations = useSelector( + (state: RootState) => state?.stationReducer.stations + ); + +useEffect(() => { + dispatch(stationList()); +}, [dispatch]); // Handle form submission const onSubmit = async (data: any) => { + // Retrieve the stationId based on the stationName selected + const selectedStation = stations.find( + (station) => station.name === data.stationName + ); const managerData = { name: data.name, email: data.email, phone: data.phone, password: data.password, - stationId: data.stationId, // Send stationId here - + stationId: selectedStation?.id, // Send stationId here }; try { @@ -131,33 +148,55 @@ export default function AddManagerModal({ "Maximum 30 characters allowed", }, pattern: { - value: /^[A-Za-z\s]+$/, // Only letters and spaces are allowed + value: /^[A-Za-z\s]+$/, message: "Manager Name must only contain letters and spaces", }, })} /> + + + {/* Station Dropdown */} + - Station Id + Select Station - + + Select Station + + {errors.stationName && ( + + {errors.stationName.message} + + )} + + {/* Email and Password */} {/* Email */} @@ -219,7 +258,6 @@ export default function AddManagerModal({ : "primary" } size="small" - // onMouseDown={togglePasswordVisibility} InputProps={{ endAdornment: ( @@ -247,9 +285,8 @@ export default function AddManagerModal({ - {/* Phone and Registered Address */} + {/* Phone Number */} - {/* Phone */} Phone Number @@ -272,7 +309,6 @@ export default function AddManagerModal({ })} /> - {/* Submit Button */} diff --git a/src/components/AddStationModal/index.tsx b/src/components/AddStationModal/index.tsx index 16cb174..2919838 100644 --- a/src/components/AddStationModal/index.tsx +++ b/src/components/AddStationModal/index.tsx @@ -1,6 +1,25 @@ import { useForm } from "react-hook-form"; -import { Box, Button, Typography, Modal } from "@mui/material"; +import { + Box, + Button, + Typography, + Modal, + FormControl, + FormHelperText, + Select, + MenuItem, + Checkbox, + ListItemText, + InputLabel, +} from "@mui/material"; import CloseIcon from "@mui/icons-material/Close"; +import { useSelector, useDispatch } from "react-redux"; +import { useEffect, useState } from "react"; +import { RootState } from "../../redux/reducers.ts"; +import { + fetchVehicleBrands, + vehicleList, +} from "../../redux/slices/VehicleSlice"; // Adjust this import path accordingly import { CustomIconButton, CustomTextField, @@ -18,10 +37,68 @@ export default function AddStationModal({ reset, } = useForm(); + const dispatch = useDispatch(); + + // Get vehicle names from the Redux store + const vehicles = useSelector( + (state: RootState) => state.vehicleReducer.vehicles + ); // Adjust according to your actual state structure + const vehicleBrands = useSelector( + (state: RootState) => state.vehicleReducer.vehicleBrands + ); + + // State for selected vehicle brand and vehicles + const [selectedBrands, setSelectedBrands] = useState([]); + const [selectedVehicles, setSelectedVehicles] = useState([]); + + // Fetch vehicle brands and vehicle list when the component mounts + useEffect(() => { + dispatch(fetchVehicleBrands()); + dispatch(vehicleList()); + }, [dispatch]); + + // Filter vehicles based on selected vehicle brands + const filteredVehicles = vehicles.filter((vehicle) => + selectedBrands.includes(vehicle.company) + ); + + // Handle changes in selected vehicle brands (checkboxes) + const handleBrandChange = ( + event: React.ChangeEvent<{ value: unknown }> + ) => { + const value = event.target.value as string[]; + setSelectedBrands(value); // Update the selected vehicle brands + }; + + // Handle changes in selected vehicles (checkboxes) + const handleVehicleChange = ( + event: React.ChangeEvent<{ value: unknown }> + ) => { + setSelectedVehicles(event.target.value as string[]); + }; + + // Function to map selected vehicle names to corresponding vehicle ids + const getVehicleIds = () => { + return vehicles + .filter((vehicle) => selectedVehicles.includes(vehicle.name)) + .map((vehicle) => vehicle.id); + }; + const onSubmit = (data: any) => { - handleAddStation(data); // Add station to the list + const vehicleIds = getVehicleIds(); // Get the ids of the selected vehicles + + const payload = { + ...data, + status: 1, // Default status + allowedCarIds: vehicleIds, // Pass the vehicle ids to the backend + totalSlots: Number(data.totalSlots), // Ensure this is a number + }; + + handleAddStation(payload); handleClose(); // Close modal after adding reset(); + setSelectedVehicles([]); + setSelectedBrands([]); // Reset selected brands after submission }; return ( @@ -77,7 +154,6 @@ export default function AddStationModal({ gap: 2, }} > - {/* First Row - Two Inputs */} + + {/* Vehicle Brand Dropdown with Checkboxes */} + + + + Select Vehicle Brands + + + Choose Brands + + + {errors.vehicleBrand + ? errors.vehicleBrand.message + : ""} + + + + + + + + + Vehicle Name + + + + Choose Vehicles + + + {errors.vehicleName + ? errors.vehicleName.message + : ""} + + + + {/* Submit Button */} diff --git a/src/components/AddVehicleModal/index.tsx b/src/components/AddVehicleModal/index.tsx index f7747f8..3334d40 100644 --- a/src/components/AddVehicleModal/index.tsx +++ b/src/components/AddVehicleModal/index.tsx @@ -83,6 +83,36 @@ export default function AddVehicleModal({ > {/* First Row - Two Inputs */} + + + Company Name + + + - - - - Company - - - {/* Second Row - Two Inputs */} diff --git a/src/components/AvailableSlotModal/index.tsx b/src/components/AvailableSlotModal/index.tsx new file mode 100644 index 0000000..fd27d0c --- /dev/null +++ b/src/components/AvailableSlotModal/index.tsx @@ -0,0 +1,108 @@ +import React, { useEffect, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { + Button, + Dialog, + DialogTitle, + Box, + CircularProgress, +} from "@mui/material"; +import { styled } from "@mui/system"; +import { + fetchAvailableSlots, + availableSlots, +} from "../../redux/slices/slotSlice"; // Update with the correct import path + +const SlotButton = styled(Button)<{ selected: boolean }>(({ selected }) => ({ + backgroundColor: selected ? "#1976d2" : "#333", + color: "#fff", + border: "1px solid #555", + width: "120px", + height: "40px", + margin: "5px", + "&:hover": { + backgroundColor: selected ? "#1565c0" : "#444", + }, +})); + +const AvailableSlotsModal = ({ + open, + onClose, +}: { + open: boolean; + onClose: () => void; +}) => { + const dispatch = useDispatch(); + + // Fetch slots from the Redux store + const { availableSlots, loading, error } = useSelector( + (state: any) => state.slotReducer.availableSlots + ); // Adjust selector path + const [selectedSlot, setSelectedSlot] = useState( + null + ); + + useEffect(() => { + if (open) { + // Fetch available slots when the modal opens + dispatch(fetchAvailableSlots()); + } + }, [open, dispatch]); + + const handleSlotSelect = (slot: availableSlots) => { + setSelectedSlot(slot); // Update the selected slot + }; + + const handleSave = () => { + // Save the selected slot (you can dispatch an action here if needed) + console.log("Selected Slot: ", selectedSlot); + onClose(); // Close the modal after saving + }; + + return ( + + + Available Slots + + + {loading ? ( + + ) : error ? ( +
Error: {error}
+ ) : Array.isArray(availableSlots) && + availableSlots.length > 0 ? ( + availableSlots.map((slot: availableSlots) => ( + handleSlotSelect(slot)} + selected={selectedSlot?.id === slot?.id} + > + {`${slot?.startTime} - ${slot?.endTime}`} + + )) + ) : ( +
No available slots
+ )} + +
+
+ ); +}; + +export default AvailableSlotsModal; diff --git a/src/components/CustomTable/index.tsx b/src/components/CustomTable/index.tsx index 2090518..9d9703d 100644 --- a/src/components/CustomTable/index.tsx +++ b/src/components/CustomTable/index.tsx @@ -34,13 +34,13 @@ import { CustomIconButton } from "../AddEditUserModel/styled.css.tsx"; import ManagerViewModal from "../Modals/ViewManagerModal"; import UserViewModal from "../Modals/UserViewModal/index.tsx"; import { deleteUser, userList } from "../../redux/slices/userSlice.ts"; -import { deleteStation } from "../../redux/slices/stationSlice.ts"; +import { deleteStation, stationList } 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"; +import { bookingList, deleteBooking } from "../../redux/slices/bookSlice.ts"; // Styled components for customization const StyledTableCell = styled(TableCell)(({ theme }) => ({ [`&.${tableCellClasses.head}`]: { @@ -83,10 +83,9 @@ interface CustomTableProps { viewModal: boolean; setViewModal: Function; deleteModal: boolean; - handleStatusToggle: (id: string, currentStatus: number) => void; + handleStatusToggle?: (id: string, currentStatus: number) => void; tableType: string; // Adding tableType prop to change header text dynamically handleClickOpen: () => void; - //handleDeleteButton: (id: string | number | undefined) => void; } const CustomTable: React.FC = ({ @@ -108,11 +107,8 @@ const CustomTable: React.FC = ({ const [searchQuery, setSearchQuery] = React.useState(""); const [currentPage, setCurrentPage] = React.useState(1); const usersPerPage = 10; - const { user, isLoading } = useSelector( - (state: RootState) => state?.profileReducer - ); + const { user } = useSelector((state: RootState) => state?.profileReducer); const open = Boolean(anchorEl); - console.log("Rows", rows); const handleClick = (event: React.MouseEvent, row: Row) => { setAnchorEl(event.currentTarget); setSelectedRow(row); @@ -137,7 +133,6 @@ const CustomTable: React.FC = ({ } switch (tableType) { - case "admin": dispatch(deleteAdmin(id || "")); break; @@ -156,6 +151,9 @@ const CustomTable: React.FC = ({ case "slots": dispatch(deleteSlot(id || "")); break; + case "booking": + dispatch(deleteBooking(id || "")); + break; default: console.error("Unknown table type:", tableType); return; @@ -185,6 +183,9 @@ const CustomTable: React.FC = ({ case "slots": dispatch(fetchAvailableSlots()); break; + case "station": + dispatch(stationList()); + break; default: console.error("Unknown table type:", tableType); return; @@ -197,7 +198,7 @@ const CustomTable: React.FC = ({ if (selectedRow) { // Toggle the opposite of current status const newStatus = selectedRow.statusValue === 1 ? 0 : 1; - handleStatusToggle(selectedRow.id, newStatus); + handleStatusToggle?.(selectedRow.id, newStatus); } handleClose(); }; @@ -214,7 +215,7 @@ const CustomTable: React.FC = ({ const currentRows = filteredRows.slice(indexOfFirstRow, indexOfLastRow); const handlePageChange = ( - event: React.ChangeEvent, + _event: React.ChangeEvent, value: number ) => { setCurrentPage(value); @@ -239,30 +240,35 @@ const CustomTable: React.FC = ({ }} > {/* Dynamic title based on the page type */} - {tableType === "admin" - ? "Admin" - : tableType === "role" - ? "Roles" - : tableType === "user" - ? "Users" - : tableType === "manager" - ? "Managers" - : tableType === "vehicle" - ? "Vehicles" - : tableType === "station" - ? "Charging Station" - : tableType === "booking" - ? "Booking" - : tableType === "slots" - ? "Slot" - : "List"} + {(() => { + switch (tableType) { + case "admin": + return "Admin"; + case "role": + return "Roles"; + case "user": + return "Users"; + case "manager": + return "Managers"; + case "vehicle": + return "Vehicles"; + case "station": + return "Charging Station"; + case "booking": + return "Booking"; + case "slots": + return "Slot"; + default: + return "List"; + } + })()} {/* Search & Buttons Section */} = ({ backgroundColor: "#272727", "& .MuiOutlinedInput-root": { borderRadius: "12px", + width: "422px", + height: "44px", + borderWidth: "1px", + padding: "14px 12px 14px 12px", + gap: "16px", "& fieldset": { borderColor: "#FFFFFF" }, "&:hover fieldset": { borderColor: "#FFFFFF" }, "&.Mui-focused fieldset": { @@ -312,28 +323,34 @@ const CustomTable: React.FC = ({ color: "white", borderRadius: "8px", width: "184px", + marginRight: "16px", "&: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"} + {(() => { + switch (tableType) { + case "admin": + return "Admin"; + case "role": + return "Role"; + case "user": + return "User"; + case "manager": + return "Manager"; + case "vehicle": + return "Vehicle"; + case "station": + return "Charging Station"; + case "booking": + return "Booking"; + case "slots": + return "Slot"; + default: + return "Item"; + } + })()} )} @@ -343,15 +360,17 @@ const CustomTable: React.FC = ({ width: "44px", height: "44px", borderRadius: "8px", - backgroundColor: "#272727", + backgroundColor: "#1C1C1C", color: "#52ACDF", "&:hover": { backgroundColor: "#333333" }, + "*:where([data-mui-color-scheme='dark']) &": { + backgroundColor: "#1C1C1C", + }, }} > - {/* Table Section */} = ({ sx={{ backgroundColor: "#272727", borderBottom: "none", + ".css-1ex4ubw-MuiTableCell-root.MuiTableCell-head ": + { + backgroundColor: "#272727", + }, }} > {" "} @@ -384,7 +407,13 @@ const CustomTable: React.FC = ({ ))} - + {currentRows.map((row, rowIndex) => ( {columns.map((column) => ( @@ -433,7 +462,6 @@ const CustomTable: React.FC = ({ - {/* Pagination */} = ({ }} /> - {/* Menu Actions */} {open && ( = ({ open={open} onClose={handleClose} onClick={handleClose} - transformOrigin={{ horizontal: "right", vertical: "top" }} - anchorOrigin={{ horizontal: "right", vertical: "bottom" }} + transformOrigin={{ + horizontal: "right", + vertical: "top", + }} + anchorOrigin={{ + horizontal: "right", + vertical: "bottom", + }} sx={{ [`& .${paperClasses.root}`]: { padding: 0, @@ -631,7 +664,6 @@ const CustomTable: React.FC = ({ )} - {/* Modals */} {deleteModal && ( void; - editRow: any; // Manager data including id + handleUpdate: ( + id: string, + name: string, + email: string, + phone: string, + stationId: string + ) => Promise; + editRow: any; } interface FormData { @@ -33,7 +41,7 @@ const EditManagerModal: React.FC = ({ handleClose, editRow, }) => { - const dispatch = useDispatch(); // Use dispatch to send Redux actions + const dispatch = useDispatch(); const { control, handleSubmit, @@ -74,7 +82,7 @@ const EditManagerModal: React.FC = ({ email: data.email, phone: data.phone, stationId: data.stationId, - + }, }) ).unwrap(); // Ensure that it throws an error if the update fails diff --git a/src/components/EditSlotModal/index.tsx b/src/components/EditSlotModal/index.tsx new file mode 100644 index 0000000..f227886 --- /dev/null +++ b/src/components/EditSlotModal/index.tsx @@ -0,0 +1,267 @@ +import React, { useEffect, useState } from "react"; +import { + Box, + Button, + Typography, + Modal, + CircularProgress, +} from "@mui/material"; +import CloseIcon from "@mui/icons-material/Close"; +import { useForm, Controller } from "react-hook-form"; +import { useDispatch } from "react-redux"; +import { updateSlot, fetchAvailableSlots } from "../../redux/slices/slotSlice"; // Update with correct action +import { + CustomIconButton, + CustomTextField, +} from "../AddEditUserModel/styled.css.tsx"; // Custom styled components +import { AppDispatch } from "../../redux/store/store.ts"; + +interface EditSlotModalProps { + open: boolean; + handleClose: () => void; + handleUpdate: ( + id: string, + startTime: string, + endTime: string, + isAvailable: boolean + ) => Promise; + editRow: any; // Slot data including id +} + +interface FormData { + date: string; + startTime: string; + endTime: string; + isAvailable: boolean; +} + +const EditSlotModal: React.FC = ({ + open, + handleClose, + editRow, +}) => { + const dispatch = useDispatch(); + const { + control, + handleSubmit, + formState: { errors }, + setValue, + reset, + } = useForm({ + defaultValues: { + date: "", + startTime: "", + endTime: "", + isAvailable: false, + }, + }); + + const [loading, setLoading] = useState(false); + const [isAvailable, setIsAvailable] = useState( + editRow?.isAvailable || false + ); + + useEffect(() => { + if (editRow) { + setValue("startTime", editRow.startTime); + setValue("endTime", editRow.endTime); + setIsAvailable(editRow.isAvailable); + } else { + reset(); + } + }, [editRow, setValue, reset]); + + const onSubmit = async (data: FormData) => { + if (editRow) { + setLoading(true); + + try { + + const availabilityStatus = isAvailable ? true : false; + + await dispatch( + updateSlot({ + id: editRow.id, // Slot ID// date: data.date, + startTime: data.startTime, + endTime: data.endTime, + isAvailable: availabilityStatus, + }) + ).unwrap(); + dispatch(fetchAvailableSlots()); + handleClose(); // Close modal on success + reset(); // Reset form fields after submit + } catch (error) { + console.error("Error updating slot:", error); + // Handle the error or show a toast message + } finally { + setLoading(false); // Stop loading state + } + } + }; + + return ( + { + if (reason === "backdropClick") { + return; + } + handleClose(); // Close modal when clicking cross or cancel + }} + aria-labelledby="edit-slot-modal" + > + + {/* Header */} + + + Edit Slot + + + + + + + {/* Horizontal Line */} + + + {/* Input Fields */} + + {/* Date */} + {/* + + Date + + ( + + )} + /> + */} + + {/* Start Time */} + + + Start Time + + ( + + )} + /> + + + {/* End Time */} + + + End Time + + ( + + )} + /> + + + {/* Availability Toggle */} + + + + {isAvailable ? "Available" : "Not Available"} + + + + + {/* Submit Button */} + + + + + + ); +}; + +export default EditSlotModal; diff --git a/src/components/EditStationModal/index.tsx b/src/components/EditStationModal/index.tsx index ada74a7..11fdc3b 100644 --- a/src/components/EditStationModal/index.tsx +++ b/src/components/EditStationModal/index.tsx @@ -1,11 +1,37 @@ -import React, { useEffect } from "react"; -import { Box, Button, Typography, Modal } from "@mui/material"; +import React, { useEffect, useState } from "react"; +import { + Box, + Button, + Typography, + Modal, + FormControl, + InputLabel, + Select, + MenuItem, + Checkbox, + ListItemText, + FormHelperText, +} from "@mui/material"; import CloseIcon from "@mui/icons-material/Close"; import { useForm, Controller } from "react-hook-form"; +import { useSelector, useDispatch } from "react-redux"; +import { RootState } from "../../redux/reducers.ts"; +import { + fetchVehicleBrands, + vehicleList, +} from "../../redux/slices/VehicleSlice"; import { CustomIconButton, CustomTextField, -} from "../AddEditUserModel/styled.css.tsx"; +} from "../AddEditUserModel/styled.css.tsx"; // Assuming custom styled components + +interface FormData { + name: string; + registeredAddress: string; + totalSlots: number; + allowedCarIds: string[]; + status: number; +} interface EditStationModalProps { open: boolean; @@ -15,16 +41,9 @@ interface EditStationModalProps { name: string, registeredAddress: string, totalSlots: number, - status: number + allowedCarIds: number[] ) => void; - editRow: any; -} - -interface FormData { - name: string; - registeredAddress: string; - totalSlots: number; - status: number; + editRow?: any; } const EditStationModal: React.FC = ({ @@ -36,42 +55,85 @@ const EditStationModal: React.FC = ({ const { control, handleSubmit, - formState: { errors }, setValue, reset, + formState: { errors }, } = useForm({ defaultValues: { name: "", registeredAddress: "", totalSlots: 0, status: 1, + allowedCarIds: [], }, }); - // Set values if editRow is provided + const dispatch = useDispatch(); + const vehicles = useSelector( + (state: RootState) => state.vehicleReducer.vehicles + ); + const vehicleBrands = useSelector( + (state: RootState) => state.vehicleReducer.vehicleBrands + ); + + const [selectedBrands, setSelectedBrands] = useState([]); + const [selectedVehicles, setSelectedVehicles] = useState([]); + + useEffect(() => { + dispatch(fetchVehicleBrands()); + dispatch(vehicleList()); // Fetch vehicles when the component mounts + }, [dispatch]); + useEffect(() => { if (editRow) { setValue("name", editRow.name); setValue("registeredAddress", editRow.registeredAddress); setValue("totalSlots", editRow.totalSlots); - setValue("status", editRow.number); + setValue("status", editRow.status); + setValue("allowedCarIds", editRow.allowedCarIds || []); + setSelectedVehicles(editRow.allowedCarIds || []); } else { reset(); } }, [editRow, setValue, reset]); + // Filter vehicles based on selected brands + const filteredVehicles = vehicles.filter((vehicle) => + selectedBrands.includes(vehicle.company) + ); + + // Handle changes in vehicle brand selection + const handleBrandChange = ( + event: React.ChangeEvent<{ value: unknown }> + ) => { + setSelectedBrands(event.target.value as string[]); + }; + + // Handle changes in vehicle selection + const handleCheckboxChange = ( + event: React.ChangeEvent<{ value: unknown }> + ) => { + const value = event.target.value as string[]; + setSelectedVehicles(value); + setValue("allowedCarIds", value); // Update allowedCarIds in form state + }; + const onSubmit = (data: FormData) => { - if (editRow) { - handleUpdate( - editRow.id, - data.name, - data.registeredAddress, - data.totalSlots, - data.status - ); - } - handleClose(); // Close the modal - reset(); // Reset the form fields + const vehicleIds = vehicles + .filter((vehicle) => selectedVehicles.includes(vehicle.name)) + .map((vehicle) => vehicle.id); + + handleUpdate( + editRow.id, + data.name, + data.registeredAddress, + data.totalSlots, + vehicleIds + ); + handleClose(); + reset(); + //setSelectedBrands([]); // Reset brands after submit + setSelectedVehicles([]); // Reset selected vehicles }; return ( @@ -81,7 +143,7 @@ const EditStationModal: React.FC = ({ if (reason === "backdropClick") { return; } - handleClose(); // Close modal when clicking cross or cancel + handleClose(); }} aria-labelledby="edit-station-modal" > @@ -119,9 +181,9 @@ const EditStationModal: React.FC = ({ {/* Horizontal Line */} - {/* Input Fields */} + {/* Form */} - {/* First Row - Two Inputs */} + {/* Station Name and Address */} = ({ ( = ({ placeholder="Enter Station Name" size="small" error={!!errors.name} - helperText={errors.name?.message} + helperText={ + errors.name + ? errors.name.message + : "" + } /> )} /> - = ({ ( = ({ size="small" error={!!errors.registeredAddress} helperText={ - errors.registeredAddress?.message + errors.registeredAddress + ? errors.registeredAddress + .message + : "" } /> )} @@ -202,7 +254,7 @@ const EditStationModal: React.FC = ({ - {/* Second Row - Total Slots */} + {/* Total Slots */} = ({ ( = ({ size="small" type="number" error={!!errors.totalSlots} - helperText={errors.totalSlots?.message} + helperText={ + errors.totalSlots + ? errors.totalSlots.message + : "" + } /> )} /> + {/* Vehicle Brand Selection with Checkboxes */} + + + + Select Vehicle Brands + + + Choose Brand + + + + + + Vehicle Name + + + Choose Vehicles + + + {errors.allowedCarIds + ? errors.allowedCarIds.message + : ""} + + + + + + {/* Submit Button */} = ({ "&:hover": { backgroundColor: "#439BC1" }, }} > - Update Charging Station + Update Station diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx index 83dca3c..80247c2 100644 --- a/src/components/Header/index.tsx +++ b/src/components/Header/index.tsx @@ -15,22 +15,21 @@ import { AppDispatch, RootState } from "../../redux/store/store"; import { fetchAdminProfile } from "../../redux/slices/profileSlice"; import OptionsMenu from "../OptionsMenu"; import NotificationsNoneIcon from "@mui/icons-material/NotificationsNone"; +import ColorModeIconDropdown from "../../shared-theme/ColorModeIconDropdown"; export default function Header() { const [showNotifications, setShowNotifications] = React.useState(false); const toggleNotifications = () => { setShowNotifications((prev) => !prev); }; - const [open, setOpen] = React.useState(true); + const [open, setOpen] = React.useState(true); - const dispatch = useDispatch(); - const { user } = useSelector( - (state: RootState) => state?.profileReducer - ); + const dispatch = useDispatch(); + const { user } = useSelector((state: RootState) => state?.profileReducer); - React.useEffect(() => { - dispatch(fetchAdminProfile()); - }, [dispatch]); + React.useEffect(() => { + dispatch(fetchAdminProfile()); + }, [dispatch]); return ( - @@ -43,7 +43,14 @@ export default function LineChartCard() { return (
Weekly - +
diff --git a/src/components/MainGrid/index.tsx b/src/components/MainGrid/index.tsx index 491dc4e..f3bf217 100644 --- a/src/components/MainGrid/index.tsx +++ b/src/components/MainGrid/index.tsx @@ -45,7 +45,7 @@ export default function MainGrid() { theme.spacing(2) }} diff --git a/src/components/MenuContent/index.tsx b/src/components/MenuContent/index.tsx index 9285c40..8d5df61 100644 --- a/src/components/MenuContent/index.tsx +++ b/src/components/MenuContent/index.tsx @@ -12,7 +12,12 @@ import { useSelector } from "react-redux"; import { RootState } from "../../redux/store/store"; import DashboardOutlinedIcon from "@mui/icons-material/DashboardOutlined"; import ManageAccountsOutlinedIcon from "@mui/icons-material/ManageAccountsOutlined"; - +import EvStationOutlinedIcon from "@mui/icons-material/EvStationOutlined"; +import EvStationIcon from "@mui/icons-material/EvStation"; +import BookOnlineOutlinedIcon from "@mui/icons-material/BookOnlineOutlined"; +import ChecklistSharpIcon from "@mui/icons-material/ChecklistSharp"; +import AnalyticsOutlinedIcon from "@mui/icons-material/AnalyticsOutlined"; +import PeopleOutlinedIcon from "@mui/icons-material/PeopleOutlined"; type PropType = { hidden: boolean; }; @@ -31,53 +36,53 @@ export default function MenuContent({ hidden }: PropType) { }, userRole === "superadmin" && { text: "Admins", - icon: , + icon: , url: "/panel/admin-list", }, userRole === "superadmin" && { text: "Roles", - icon: , + icon: , url: "/panel/role-list", }, userRole === "admin" && { text: "Users", - icon: , + icon: , url: "/panel/user-list", }, userRole === "admin" && { text: "Charging Stations", - icon: , + icon: , url: "/panel/station-list", // Placeholder for now }, userRole === "admin" && { text: "Managers", - icon: , + icon: , url: "/panel/manager-list", // Placeholder for now }, userRole === "admin" && { text: "Vehicles", - icon: , + icon: , url: "/panel/vehicle-list", // Placeholder for now }, // userRole === "manager" && { - // text: "Add Slots", - // icon: , - // url: "/panel/EVslots", // Placeholder for now + // text: "Add Slots", + // icon: , + // url: "/panel/EVslots", // Placeholder for now // }, userRole === "user" && { text: "Bookings", - icon: , + icon: , url: "/panel/booking-list", // Placeholder for now }, userRole === "manager" && { text: "Available Slots", - icon: , + icon: , url: "/panel/slot-list", // Placeholder for now }, userRole === "user" && { text: "Available Slots", - icon: , + icon: , url: "/panel/slot-list", // Placeholder for now }, ]; @@ -85,25 +90,33 @@ export default function MenuContent({ hidden }: PropType) { const filteredMenuItems = baseMenuItems.filter(Boolean); return ( - + {filteredMenuItems.map((item, index) => ( @@ -113,8 +126,14 @@ export default function MenuContent({ hidden }: PropType) { sx={{ display: !hidden ? "none" : "", transition: "all 0.5s ease", + ".MuiListItemText-primary": { + width: "118px", + height: "19px", fontSize: "16px", + letterSpacing: "0%", + lineHeight: "100%", + color: "#D9D8D8", }, }} primary={item.text} diff --git a/src/components/Modals/StationViewModal/index.tsx b/src/components/Modals/StationViewModal/index.tsx index b47ac08..9efc2a0 100644 --- a/src/components/Modals/StationViewModal/index.tsx +++ b/src/components/Modals/StationViewModal/index.tsx @@ -8,7 +8,7 @@ type Props = { open: boolean; setViewModal: Function; handleView: (id: string | undefined) => void; - id?: number | undefined; + id?: string | undefined; }; const style = { diff --git a/src/components/Modals/ViewManagerModal/index.tsx b/src/components/Modals/ViewManagerModal/index.tsx index 5ee6b2f..37d40aa 100644 --- a/src/components/Modals/ViewManagerModal/index.tsx +++ b/src/components/Modals/ViewManagerModal/index.tsx @@ -32,7 +32,7 @@ export default function ManagerViewModal({ open, setViewModal, id }: Props) { (state: RootState) => state.managerReducer ); const [selectedManager, setSelectedManager] = useState(null); - + useEffect(() => { if (id) { @@ -110,7 +110,8 @@ export default function ManagerViewModal({ open, setViewModal, id }: Props) { Station Location: - {selectedManager.registeredAddress} + {selectedManager.chargingStation + ?.registeredAddress || "Not Available"} @@ -118,7 +119,8 @@ export default function ManagerViewModal({ open, setViewModal, id }: Props) { Station Name: - {selectedManager.stationName} + {selectedManager.chargingStation?.name || + "Not Available"} diff --git a/src/components/Modals/ViewModal/index.tsx b/src/components/Modals/ViewModal/index.tsx index 68d24db..138d34d 100644 --- a/src/components/Modals/ViewModal/index.tsx +++ b/src/components/Modals/ViewModal/index.tsx @@ -7,6 +7,7 @@ import CloseIcon from "@mui/icons-material/Close"; type Props = { open: boolean; setViewModal: Function; + handleView: (id: string | undefined) => void; id?: string; }; diff --git a/src/components/ResourcePieChart/index.tsx b/src/components/ResourcePieChart/index.tsx index f257558..2c0f779 100644 --- a/src/components/ResourcePieChart/index.tsx +++ b/src/components/ResourcePieChart/index.tsx @@ -28,11 +28,15 @@ export default function ResourcePieChart() { sx={{ display: "flex", flexDirection: "column", - gap: "8px", + gap: "12px", flexGrow: 1, - width: "100%", - height: "100%", - backgroundColor: "#202020", + width: "553px", + height: "324px", + padding: "16px", + borderRadius: "16px", + "*:where([data-mui-color-scheme='dark']) &": { + backgroundColor: "#202020", + }, }} > @@ -40,11 +44,13 @@ export default function ResourcePieChart() { component="h2" variant="subtitle2" color="#F2F2F2" + width="84px" + height="24px" sx={{ - fontFamily: "Gilroy", fontWeight: 500, fontSize: "18px", lineHeight: "24px", + color: "#FFFFFF", }} > Resources @@ -93,7 +99,12 @@ export default function ResourcePieChart() { borderRadius: "50%", }} /> - + {entry.title} diff --git a/src/components/SessionsChart/index.tsx b/src/components/SessionsChart/index.tsx index 381df18..2ff16df 100644 --- a/src/components/SessionsChart/index.tsx +++ b/src/components/SessionsChart/index.tsx @@ -5,15 +5,21 @@ import Typography from "@mui/material/Typography"; import Box from "@mui/material/Box"; import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; + export default function SessionsChart() { return ( @@ -21,11 +27,16 @@ export default function SessionsChart() { variant="h6" align="left" color="#F2F2F2" + width="132px" + height="24px" + gap={"12px"} sx={{ fontFamily: "Gilroy", fontWeight: 500, fontSize: "18px", lineHeight: "24px", + letterSpacing: "0%", + color: "#FAFAFA", }} > Charging prices @@ -50,13 +61,15 @@ export default function SessionsChart() { }} > Delhi NCR EV Station @@ -78,7 +91,6 @@ export default function SessionsChart() { mx: "auto", }} > - {/* You can map over your data here; for simplicity, we’re using static boxes */} {[1, 2, 3, 4].map((item) => ( Basic Charging - - 16.83 cents/kWh - + + + 16.83 + + + + cents/kWh + + ))}
diff --git a/src/components/SideMenu/index.tsx b/src/components/SideMenu/index.tsx index f5b2f2b..4efdf7c 100644 --- a/src/components/SideMenu/index.tsx +++ b/src/components/SideMenu/index.tsx @@ -61,44 +61,51 @@ export default function SideMenu() { flexDirection: "row", alignItems: "center", pt: 2, - pl: 2, + pl: 2, }} > - - - */} + {/* - {/* Digi EV Text Section */} + Digi EV - + {user?.userType || "N/A"} - - + */} {title} @@ -33,8 +48,12 @@ export default function StatCard({ title, value }: StatCardProps) { component="h1" variant="body1" color="#F2F2F2" - fontSize={30} - fontWeight={700} + width={"36px"} + height={"36px"} + fontSize={"32px"} + fontWeight={600} + lineHeight={"36px"} + letterSpacing={"0%"} gutterBottom > {value} diff --git a/src/components/barChartCard/index.tsx b/src/components/barChartCard/index.tsx index 156f343..5bf70ec 100644 --- a/src/components/barChartCard/index.tsx +++ b/src/components/barChartCard/index.tsx @@ -1,20 +1,8 @@ import * as React from "react"; import { useTheme } from "@mui/material/styles"; -import { - BarChart, - Bar, - XAxis, - YAxis, - CartesianGrid, -} from "recharts"; -import { - Card, - CardContent, - Typography, - Box, - -} from "@mui/material"; -import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; +import { BarChart, Bar, XAxis, YAxis, CartesianGrid } from "recharts"; +import { Card, CardContent, Typography, Box } from "@mui/material"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; const data = [ { name: "Jan", v1: 40 }, { name: "Feb", v1: 50 }, @@ -30,7 +18,14 @@ export default function RoundedBarChart() { return (
Monthly - +
= ({ customStyles }) => { ...customStyles, mt: { xs: 8, md: 0 }, padding: 0, - })} > - - - { const authToken = localStorage.getItem("authToken"); if (authToken) { config.headers.Authorization = `Bearer ${authToken}`; } - return config; }); + http.interceptors.response.use( - (response) => response, + (response) => response, (error) => { - if (error.response && error.response.status === 401) { - - window.location.href = "/login"; - - // const history = useHistory(); - // history.push("/login"); + if (error.response) { + const status = error.response.status; + const requestUrl = error.config.url; // Get the API route + + // Handle token expiration (401) but NOT for login failures + if (status === 401 && !requestUrl.includes("/login")) { + localStorage.removeItem("authToken"); + window.location.href = "/login"; + } + + // Handle forbidden access + if (status === 403) { + localStorage.removeItem("authToken"); + window.location.href = "/login"; + } } - return Promise.reject(error); + return Promise.reject(error); } ); diff --git a/src/pages/AdminList/index.tsx b/src/pages/AdminList/index.tsx index 7af0318..b064f9c 100644 --- a/src/pages/AdminList/index.tsx +++ b/src/pages/AdminList/index.tsx @@ -83,6 +83,8 @@ export default function AdminList() { { id: "email", label: "Email" }, { id: "phone", label: "Phone" }, { id: "registeredAddress", label: "Address" }, + { id: "userType", label: "Role" }, + { id: "action", label: "Action", align: "center" }, ]; @@ -93,7 +95,8 @@ export default function AdminList() { name: string; email: string; phone: string; - Admins: { registeredAddress: string }[]; // Adjusted to support array of Admins + Admins: { registeredAddress: string }[]; + userType:string; }, index: number ) => ({ @@ -103,6 +106,7 @@ export default function AdminList() { email: admin?.email, phone: admin?.phone, registeredAddress: admin?.Admins?.[0]?.registeredAddress || "NA", + userType:admin?.userType||"NA", }) ); diff --git a/src/pages/Auth/Login/index.tsx b/src/pages/Auth/Login/index.tsx index ec96af4..a9f82ff 100644 --- a/src/pages/Auth/Login/index.tsx +++ b/src/pages/Auth/Login/index.tsx @@ -23,6 +23,7 @@ import { useNavigate } from "react-router-dom"; import { Visibility, VisibilityOff } from "@mui/icons-material"; import { Card, SignInContainer } from "./styled.css.tsx"; import { CustomIconButton } from "../../../components/AddEditUserModel/styled.css.tsx"; +import { AppDispatch } from "../../../redux/store/store.ts"; interface ILoginForm { email: string; password: string; @@ -37,7 +38,7 @@ export default function Login(props: { disableCustomTheme?: boolean }) { handleSubmit, formState: { errors, isValid }, } = useForm({ mode: "onChange" }); - const dispatch = useDispatch(); + const dispatch = useDispatch(); const router = useNavigate(); const handleClickOpen = () => { @@ -104,11 +105,11 @@ export default function Login(props: { disableCustomTheme?: boolean }) { }} > Logo @@ -329,9 +330,7 @@ export default function Login(props: { disableCustomTheme?: boolean }) { ? "error" : "primary" } - onMouseDown={ - togglePasswordVisibility - } + InputProps={{ endAdornment: ( diff --git a/src/pages/EvSlotList/index.tsx b/src/pages/EvSlotList/index.tsx index 207f26e..ef4f174 100644 --- a/src/pages/EvSlotList/index.tsx +++ b/src/pages/EvSlotList/index.tsx @@ -1,21 +1,23 @@ -import React, { useEffect, useState } from "react"; -import { Box, Button, TextField, Typography } from "@mui/material"; +import { useEffect, useState } from "react"; 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 { + createSlot, + fetchAvailableSlots, + updateSlot, +} from "../../redux/slices/slotSlice"; import AddSlotModal from "../../components/AddSlotModal"; import dayjs from "dayjs"; +import EditSlotModal from "../../components/EditSlotModal"; export default function EVSlotList() { const [addModalOpen, setAddModalOpen] = useState(false); const [editModalOpen, setEditModalOpen] = useState(false); - const [editRow, setEditRow] = useState(null); const { reset } = useForm(); const [deleteModal, setDeleteModal] = useState(false); const [viewModal, setViewModal] = useState(false); const [rowData, setRowData] = useState(null); - const [searchTerm, setSearchTerm] = useState(""); const dispatch = useDispatch(); const availableSlots = useSelector( (state: RootState) => state?.slotReducer.availableSlots @@ -51,6 +53,34 @@ export default function EVSlotList() { } }; + const handleUpdate = async ( + id: string, + startTime: string, + endTime: string, + isAvailable: boolean + ) => { + try { + const formattedStartTime = dayjs(startTime, "HH:mm").format( + "HH:mm" + ); + const formattedEndTime = dayjs(endTime, "HH:mm").format("HH:mm"); + + await dispatch( + updateSlot({ + id, + startTime: formattedStartTime, + endTime: formattedEndTime, + isAvailable, + }) + ).unwrap(); + + await dispatch(fetchAvailableSlots()); + handleCloseModal(); + } catch (error) { + console.error("Update failed", error); + } + }; + const slotColumns: Column[] = [ { id: "srno", label: "Sr No" }, { id: "name", label: "Station Name" }, @@ -62,29 +92,24 @@ export default function EVSlotList() { { 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", - }; - }) + 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"); + + 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 ( <> @@ -105,6 +130,12 @@ console.log("Rows",slotRows) handleClose={handleCloseModal} handleAddSlot={handleAddSlot} /> + ); } diff --git a/src/pages/ManagerList/index.tsx b/src/pages/ManagerList/index.tsx index 650e756..a2db5ca 100644 --- a/src/pages/ManagerList/index.tsx +++ b/src/pages/ManagerList/index.tsx @@ -9,11 +9,9 @@ import { managerList, addManager, updateManager, - deleteManager, } from "../../redux/slices/managerSlice"; import { useForm } from "react-hook-form"; - export default function ManagerList() { const [addModalOpen, setAddModalOpen] = useState(false); const [editModalOpen, setEditModalOpen] = useState(false); @@ -67,7 +65,7 @@ export default function ManagerList() { // Handle updating an existing manager const handleUpdate = async ( - id: number, + id: string, name: string, email: string, phone: string, @@ -101,65 +99,29 @@ export default function ManagerList() { { id: "registeredAddress", label: "Station Location" }, { id: "action", label: "Action", align: "center" }, ]; - // const categoryColumns: Column[] = [ - // { id: "srno", label: "Sr No" }, - // { id: "name", label: "Name" }, - // { id: "email", label: "Email" }, - // { id: "phone", label: "Phone" }, - // { id: "stationName", label: "Station Name" }, // Added station name column - // { id: "stationAddress", label: "Station Location" }, // Added station address column - // { id: "action", label: "Action", align: "center" }, - // ]; - // Filter managers based on search term - const filteredManagers = managers?.filter( - (manager) => - manager.name?.toLowerCase().includes(searchTerm.toLowerCase()) || - manager.email?.toLowerCase().includes(searchTerm.toLowerCase()) || - manager.phone?.toLowerCase().includes(searchTerm.toLowerCase()) - - ); - - // Format rows to display manager details - // const categoryRows = filteredManagers?.length - // ? filteredManagers?.map( - // ( - // manager: { - // id: number; - // name: string; - // email: string; - // phone: string; - - - // }, - // index: number - // ) => ({ - // id: manager?.id, - // srno: index + 1, - // name: manager?.name, - // email: manager?.email, - // phone: manager.phone ?? "NA", - // }) - // ) - // : []; - -const categoryRows = filteredManagers?.length - ? filteredManagers.map((manager, index) => { - const station = manager?.ChargingStation; // Correct access to the ChargingStation data - return { - id: manager.id, - srno: index + 1, - name: manager.name, - email: manager.email, - phone: manager.phone ?? "NA", - stationName: station?.name ?? "NA", // Corrected station name - registeredAddress: station?.registeredAddress ?? "NA", // Corrected station address - }; - }) - : []; - + // const filteredManagers = managers?.filter( + // (manager) => + // manager.name?.toLowerCase().includes(searchTerm.toLowerCase()) || + // manager.email?.toLowerCase().includes(searchTerm.toLowerCase()) || + // manager.phone?.toLowerCase().includes(searchTerm.toLowerCase()) + // ); + const categoryRows = managers?.length + ? managers.map((manager, index) => { + const station = manager?.chargingStation; // Correct access to the ChargingStation data + return { + id: manager.id, + srno: index + 1, + name: manager.name, + email: manager.email, + phone: manager.phone ?? "NA", + stationName: station?.name ?? "NA", // Corrected station name + registeredAddress: station?.registeredAddress ?? "NA", // Corrected station address + }; + }) + : []; return ( <> diff --git a/src/pages/ProfilePage/index.tsx b/src/pages/ProfilePage/index.tsx index f6567a2..0de4db3 100644 --- a/src/pages/ProfilePage/index.tsx +++ b/src/pages/ProfilePage/index.tsx @@ -42,14 +42,24 @@ const ProfilePage = () => { } return ( - + Account Info @@ -62,16 +72,29 @@ const ProfilePage = () => { {user?.name || "No Admin"} - + {user?.userType || "N/A"} @@ -88,6 +111,11 @@ const ProfilePage = () => { > Personal Information @@ -95,23 +123,39 @@ const ProfilePage = () => { Edit - - + + Full Name: - + {user?.name || "N/A"} @@ -139,10 +183,20 @@ const ProfilePage = () => { - + Bio: - + {user?.bio || "No bio available."} diff --git a/src/pages/StationList/index.tsx b/src/pages/StationList/index.tsx index 82395ed..67dae63 100644 --- a/src/pages/StationList/index.tsx +++ b/src/pages/StationList/index.tsx @@ -1,10 +1,6 @@ -import React, { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import CustomTable, { Column } from "../../components/CustomTable"; -import { RootState } from "../../redux/reducers"; -import { useDispatch, useSelector } from "react-redux"; -import { AppDispatch } from "../../redux/store/store"; +import { Chip } from "@mui/material"; import AddStationModal from "../../components/AddStationModal"; +import CustomTable, { Column } from "../../components/CustomTable"; import EditStationModal from "../../components/EditStationModal"; import { createStation, @@ -12,7 +8,11 @@ import { toggleStatus, updateStation, } from "../../redux/slices/stationSlice"; -import { Chip, Switch } from "@mui/material"; +import { useEffect, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { RootState } from "../../redux/reducers"; +import { AppDispatch } from "../../redux/store/store"; +import { useForm } from "react-hook-form"; export default function StationList() { const [addModalOpen, setAddModalOpen] = useState(false); @@ -25,10 +25,13 @@ export default function StationList() { const [rowData, setRowData] = useState(null); const [searchTerm, setSearchTerm] = useState(""); const dispatch = useDispatch(); + + const vehicles = useSelector( + (state: RootState) => state.vehicleReducer.vehicles + ); const stations = useSelector( (state: RootState) => state.stationReducer.stations ); - useEffect(() => { dispatch(stationList()); }, [dispatch]); @@ -48,7 +51,8 @@ export default function StationList() { const handleAddStation = async (data: { name: string; registeredAddress: string; - totalSlots: string; + totalSlots: number; + allowedCarIds: number[]; }) => { try { await dispatch(createStation(data)); // Dispatch action to add Station @@ -63,7 +67,8 @@ export default function StationList() { id: string, name: string, registeredAddress: string, - totalSlots: string + totalSlots: number, + allowedCarIds: number[] ) => { try { await dispatch( @@ -72,7 +77,8 @@ export default function StationList() { name, registeredAddress, totalSlots, - + status: 0, + allowedCarIds, // Pass the updated allowedCarIds }) ); await dispatch(stationList()); @@ -82,57 +88,49 @@ export default function StationList() { } }; - const handleStatusToggle = async(id: string, newStatus: number) => { - await dispatch(toggleStatus({ id, status: newStatus })); - + const handleStatusToggle = async (id: string, newStatus: number) => { + await dispatch(toggleStatus({ id, status: newStatus })); }; - // Toggle station status - // const handleStatusToggle = async (id: string, currentStatus: number) => { - // try { - // const newStatus = currentStatus === 1 ? 0 : 1; // Toggle between Active(1) and Inactive(0) - // await dispatch( - // updateStation({ - // id, - // status: newStatus, - // name: stations.name, - // registeredAddress: stations.registeredAddress, - // totalSlots: stations.totalSlots, - // }) - // ); - // await dispatch(stationList()); - // } catch (error) { - // console.error("Error toggling status", error); - // } - // }; - const filterStations = stations?.filter((station) => - station.name.toLocaleLowerCase().includes(searchTerm.toLowerCase()) - ); - const categoryRows = filterStations?.length - ? filterStations?.map((station: Station, index: number) => ({ - id: station.id, - srno: index + 1, - name: station.name, - registeredAddress:station.registeredAddress, - totalSlots:station.totalSlots, - status: ( - - ), - statusValue: station.status, - })) + // const filterStations = Array.isArray(stations) + // ? stations.filter((station) => + // station.name + // .toLocaleLowerCase() + // .includes(searchTerm.toLowerCase()) + // ) + // : []; + + // Mapping and formatting vehicles + const categoryRows = stations?.length + ? stations?.map((station: any, index: number) => { + // Format the selected vehicles from the allowedCars array + const formattedVehicles = station.allowedCars?.map( + (car: any) => car.name + ); + + // Format the vehicle list like "Tata Punch Electric, Royal" or similar + const vehicleDisplay = formattedVehicles + ? formattedVehicles.length > 1 + ? `${formattedVehicles.slice(0, 2).join(", ")} + ${ + formattedVehicles.length - 2 + }` + : formattedVehicles.join(", ") + : "No vehicles"; // In case there are no vehicles selected + + return { + id: station.id, + srno: index + 1, + name: station.name, + registeredAddress: station.registeredAddress, + totalSlots: station.totalSlots, + vehicles: vehicleDisplay, // Add the formatted vehicle display here + status: + station.status === 1 ? "Available" : "Not Available", // Format status text + statusValue: station.status, // Status value for toggling + }; + }) : []; + console.log("Rowssss", categoryRows); const categoryColumns: Column[] = [ { id: "srno", label: "Sr No" }, @@ -140,35 +138,11 @@ export default function StationList() { { id: "name", label: "Station Name" }, { id: "registeredAddress", label: "Station Location" }, { id: "totalSlots", label: "Total Slots" }, - //{ id: "status", label: "Status" }, + { id: "vehicles", label: "Vehicles" }, // Add Vehicles column here + { id: "status", label: "Status" }, // Add Status column here { id: "action", label: "Action", align: "center" }, ]; - // Filter stations based on search term - // const filterStations = stations?.filter((station) => - // station.name.toLocaleLowerCase().includes(searchTerm.toLowerCase()) - // ); - - // // Prepare categoryRows with toggle switch for status - // const categoryRows = filterStations?.length - // ? filterStations?.map((station: any, index: number) => ({ - // id: station.id, - // srno: index + 1, - // name: station.name, - // status: ( - // - // handleStatusToggle(station.id, station.status) - // } - // color="primary" - // inputProps={{ "aria-label": "station-status-toggle" }} - // /> - // ), - // statusValue: station.status, - // })) - // : []; - return ( <> ); diff --git a/src/pages/VehicleList/index.tsx b/src/pages/VehicleList/index.tsx index 317e1d4..5edaa6d 100644 --- a/src/pages/VehicleList/index.tsx +++ b/src/pages/VehicleList/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import CustomTable, { Column } from "../../components/CustomTable"; import { RootState } from "../../redux/reducers"; @@ -15,13 +15,11 @@ import EditVehicleModal from "../../components/EditVehicleModal"; export default function VehicleList() { const [addModalOpen, setAddModalOpen] = useState(false); const [editModalOpen, setEditModalOpen] = useState(false); - const [editRow, setEditRow] = useState(null); const { reset } = useForm(); const [deleteModal, setDeleteModal] = useState(false); const [viewModal, setViewModal] = useState(false); const [rowData, setRowData] = useState(null); - const [searchTerm, setSearchTerm] = useState(""); const dispatch = useDispatch(); const vehicles = useSelector( (state: RootState) => state.vehicleReducer.vehicles @@ -32,7 +30,7 @@ export default function VehicleList() { }, [dispatch]); const handleClickOpen = () => { - setRowData(null); // Reset row data when opening for new admin + setRowData(null); setAddModalOpen(true); }; @@ -44,7 +42,7 @@ export default function VehicleList() { }; const handleAddVehicle = async (data: { - vehicleName: string; + name: string; company: string; modelName: string; chargeType: string; @@ -60,7 +58,7 @@ export default function VehicleList() { }; const handleUpdate = async ( - id: number, + id: string, name: string, company: string, modelName: string, @@ -95,24 +93,24 @@ export default function VehicleList() { { id: "action", label: "Action", align: "center" }, ]; - const filteredVehicles = vehicles?.filter( - (vehicle) => - vehicle.name?.toLowerCase().includes(searchTerm.toLowerCase()) || - vehicle.company?.toLowerCase().includes(searchTerm.toLowerCase()) || - vehicle.modelName - ?.toLowerCase() - .includes(searchTerm.toLowerCase()) || - vehicle.chargeType - ?.toLowerCase() - .includes(searchTerm.toLowerCase()) || - vehicle.imageUrl?.toLowerCase().includes(searchTerm.toLowerCase()) - ); + // const filteredVehicles = vehicles?.filter( + // (vehicle) => + // vehicle.name?.toLowerCase().includes(searchTerm.toLowerCase()) || + // vehicle.company?.toLowerCase().includes(searchTerm.toLowerCase()) || + // vehicle.modelName + // ?.toLowerCase() + // .includes(searchTerm.toLowerCase()) || + // vehicle.chargeType + // ?.toLowerCase() + // .includes(searchTerm.toLowerCase()) || + // vehicle.imageUrl?.toLowerCase().includes(searchTerm.toLowerCase()) + // ); - const categoryRows = filteredVehicles?.length - ? filteredVehicles?.map( + const categoryRows = vehicles?.length + ? vehicles?.map( ( vehicle: { - id: number; + id: string; name: string; company: string; modelName: string; @@ -132,8 +130,6 @@ export default function VehicleList() { ) : []; - - return ( <> - ); } diff --git a/src/redux/slices/VehicleSlice.ts b/src/redux/slices/VehicleSlice.ts index c4de897..77c3310 100644 --- a/src/redux/slices/VehicleSlice.ts +++ b/src/redux/slices/VehicleSlice.ts @@ -1,9 +1,15 @@ import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit"; import http from "../../lib/https"; import { toast } from "sonner"; +interface VehicleBrand { + name: string; + id: string; + company: string; +} interface Vehicle { - id: number; + brandId?: string; + id: string; name: string; company: string; modelName: string; @@ -11,18 +17,41 @@ interface Vehicle { imageUrl: string; } interface VehicleState { + vehicleBrands: VehicleBrand[]; vehicles: Vehicle[]; loading: boolean; error: string | null; } const initialState: VehicleState = { + vehicleBrands: [], vehicles: [], loading: false, error: null, }; +export const fetchVehicleBrands = createAsyncThunk< + VehicleBrand[], + void, + { rejectValue: string } +>("vehicle/fetchVehicleBrands", async (_, { rejectWithValue }) => { + try { + const response = await http.get("/get-vehicle-brand"); + + if (!response.data || !Array.isArray(response.data.data)) { + throw new Error("Expected array of vehicle brands"); + } + + // Map the brand names (strings) to objects with 'id' and 'name' properties + return response.data.data.map((brand: string) => ({ + id: brand, // Use brand name as the ID + name: brand, // Use brand name as the label + })); + } catch (error: any) { + return rejectWithValue("Failed to fetch vehicle brands"); + } +}); export const vehicleList = createAsyncThunk< - Vehicle, + Vehicle[], void, { rejectValue: string } >("fetchVehicles", async (_, { rejectWithValue }) => { @@ -122,6 +151,22 @@ const vehicleSlice = createSlice({ state.loading = false; state.error = action.error.message || "Failed to fetch users"; }) + .addCase(fetchVehicleBrands.pending, (state) => { + state.loading = true; + }) + .addCase( + fetchVehicleBrands.fulfilled, + (state, action: PayloadAction) => { + state.loading = false; + state.vehicleBrands = action.payload; + } + ) + + .addCase(fetchVehicleBrands.rejected, (state, action) => { + state.loading = false; + state.error = + action.payload || "Failed to fetch vehicle brands"; + }) .addCase(addVehicle.pending, (state) => { state.loading = true; // state.error = null; @@ -133,12 +178,9 @@ const vehicleSlice = createSlice({ state.vehicles.push(action.payload); } ) - .addCase( - addVehicle.rejected, - (state, action: PayloadAction) => { - state.loading = false; - } - ) + .addCase(addVehicle.rejected, (state) => { + state.loading = false; + }) .addCase(updateVehicle.pending, (state) => { state.loading = true; }) diff --git a/src/redux/slices/bookSlice.ts b/src/redux/slices/bookSlice.ts index b96e81c..33335aa 100644 --- a/src/redux/slices/bookSlice.ts +++ b/src/redux/slices/bookSlice.ts @@ -60,6 +60,7 @@ export const getCarPorts = createAsyncThunk< >("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( @@ -119,7 +120,22 @@ export const addBooking = createAsyncThunk< ); } }); - +export const deleteBooking = createAsyncThunk< + string, // Return type (id of deleted slot) + string, + { rejectValue: string } +>("booking/deleteBooking", async (id, { rejectWithValue }) => { + try { + const response = await http.delete(`/delete-booking/${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 bookSlice = createSlice({ name: "booking", initialState, @@ -167,7 +183,32 @@ const bookSlice = createSlice({ (state, action: PayloadAction) => { state.carPorts = action.payload; } - ); + ) + .addCase(deleteBooking.pending, (state) => { + state.loading = true; + }) + .addCase( + deleteBooking.fulfilled, + (state, action: PayloadAction) => { + state.loading = false; + // Ensure we're filtering the correct array (bookings) + state.bookings = state.bookings.filter( + (booking) => + String(booking.id) !== String(action.payload) + ); + // Also update slots array if it exists + if (state.bookings) { + state.bookings = state.bookings.filter( + (booking) => + String(booking.id) !== String(action.payload) + ); + } + } + ) + .addCase(deleteBooking.rejected, (state, action) => { + state.loading = false; + state.error = action.payload || "Failed to delete slot"; + }); }, }); diff --git a/src/redux/slices/managerSlice.ts b/src/redux/slices/managerSlice.ts index 783668d..a32cf0f 100644 --- a/src/redux/slices/managerSlice.ts +++ b/src/redux/slices/managerSlice.ts @@ -6,11 +6,12 @@ import { toast } from "sonner"; interface Manager { Manager: any; - id: number; + id: string; name: string; email: string; phone: string; stationId: string; + chargingStation:{name:string, registeredAddress:string}; } interface ManagerState { @@ -73,7 +74,7 @@ export const addManager = createAsyncThunk< // Update Manager (Async Thunk) export const updateManager = createAsyncThunk< Manager, - { id: number; managerData: Manager }, + { id: string; managerData: Manager }, { rejectValue: string } >("updateManager", async ({ id, managerData }, { rejectWithValue }) => { if (!id) { @@ -174,6 +175,9 @@ const managerSlice = createSlice({ }) .addCase(deleteManager.fulfilled, (state, action) => { state.loading = false; + state.managers = state.managers.filter( + (manager) => String(manager.id) !== String(action.payload) + ); }) .addCase(deleteManager.rejected, (state, action) => { state.loading = false; diff --git a/src/redux/slices/slotSlice.ts b/src/redux/slices/slotSlice.ts index 2541727..04a9b58 100644 --- a/src/redux/slices/slotSlice.ts +++ b/src/redux/slices/slotSlice.ts @@ -5,7 +5,7 @@ import { toast } from "sonner"; // Define TypeScript types interface Slot { id: string; - stationId:string; + stationId: string; date: string; startTime: string; endTime: string; @@ -88,13 +88,23 @@ export const createSlot = createAsyncThunk< // Update Slot details export const updateSlot = createAsyncThunk< Slot, - { id: number; startTime: string; endTime: string }, // Argument type (slot update data) + { + id: string; + startTime: string; + endTime: string; + isAvailable: boolean; + }, { rejectValue: string } >("slots/updateSlot", async ({ id, ...slotData }, { rejectWithValue }) => { try { - const response = await http.patch(`/slots/${id}`, slotData); + const response = await http.patch(`/update-availability/${id}`, { + ...slotData, + // Ensure data matches exactly what backend expects + startHour: slotData.startTime, + endHour: slotData.endTime, + }); toast.success("Slot updated successfully"); - return response.data.data; // Return updated slot data + return response.data.data; } catch (error: any) { toast.error("Error updating the slot: " + error?.message); return rejectWithValue( @@ -164,14 +174,15 @@ const slotSlice = createSlice({ (state, action: PayloadAction) => { state.loading = false; // Update the slot in the state with the updated data - const index = state.slots.findIndex( + const index = state.availableSlots.findIndex( (slot) => slot.id === action.payload.id ); if (index !== -1) { - state.slots[index] = action.payload; + state.availableSlots[index] = action.payload; } } ) + .addCase(updateSlot.rejected, (state, action) => { state.loading = false; state.error = action.payload || "Failed to update slot"; diff --git a/src/redux/slices/stationSlice.ts b/src/redux/slices/stationSlice.ts index f5e4203..a9ead8a 100644 --- a/src/redux/slices/stationSlice.ts +++ b/src/redux/slices/stationSlice.ts @@ -8,8 +8,9 @@ interface Station { id: string; name: string; registeredAddress: string; - totalSlots: string; + totalSlots: number; status: number; + allowedCarIds: number[]; } interface StationState { @@ -46,21 +47,43 @@ export const stationList = createAsyncThunk( } } ); +export const getAllStations = createAsyncThunk< + any, + void, + { rejectValue: string } +>("getAllStations", async (_, { rejectWithValue }) => { + try { + const token = localStorage?.getItem("authToken"); + if (!token) throw new Error("No token found"); + const response = await http.get("/get-all-stations"); + + if (!response.data) throw new Error("Invalid API response"); + + // Return the full response to handle in the reducer + return response.data; + } catch (error: any) { + toast.error("Error Fetching Stations: " + error.message); + return rejectWithValue( + error?.response?.data?.message || "An error occurred" + ); + } +}); // Create Station export const createStation = createAsyncThunk< any, { name: string; registeredAddress: string; - totalSlots: string; + totalSlots: number; + allowedCarIds: number[]; }, { rejectValue: string } >("Station/createStation", async (data, { rejectWithValue }) => { try { const response = await http.post("/create-station", data); toast.success("Station created successfully"); - return response.data; + return response.data; // Assuming the response contains the created station data } catch (error: any) { toast.error( "Failed to create Station: " + @@ -72,25 +95,31 @@ export const createStation = createAsyncThunk< } }); +// Update Station details // Update Station details export const updateStation = createAsyncThunk( "updateStation", async ({ id, ...stationData }: Station, { rejectWithValue }) => { try { + // Exclude the `status` from the update payload + const { status, ...updateData } = stationData; + + // Send the update request without the `status` const response = await http.patch( `/update-station/${id}`, - stationData + updateData ); - toast.success("Station Deatils updated successfully"); + toast.success("Station Details updated successfully"); return response?.data; } catch (error: any) { - toast.error("Error updating the user: " + error); + toast.error("Error updating the station: " + error); return rejectWithValue( error.response?.data?.message || "An error occurred" ); } } ); + export const deleteStation = createAsyncThunk< string, string, @@ -156,17 +185,30 @@ const stationSlice = createSlice({ stationList.fulfilled, (state, action: PayloadAction) => { state.loading = false; - // Properly extract stations from the response data structure - state.stations = - action.payload.data?.results || - action.payload.data || - []; + // Correct data extraction + state.stations = action.payload.data?.stations || []; } ) .addCase(stationList.rejected, (state, action) => { state.loading = false; state.error = action.payload || "Failed to fetch stations"; }) + .addCase(getAllStations.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase( + getAllStations.fulfilled, + (state, action: PayloadAction) => { + state.loading = false; + state.stations = action.payload.data || []; + } + ) + + .addCase(getAllStations.rejected, (state, action) => { + state.loading = false; + state.error = action.payload || "Failed to fetch stations"; + }) .addCase(createStation.pending, (state) => { state.loading = true; }) @@ -174,12 +216,12 @@ const stationSlice = createSlice({ createStation.fulfilled, (state, action: PayloadAction) => { state.loading = false; - // Add the newly created station to the state if it exists in the response if (action.payload.data) { state.stations.push(action.payload.data); } } ) + .addCase( createStation.rejected, (state, action: PayloadAction) => { @@ -223,8 +265,21 @@ const stationSlice = createSlice({ }) .addCase(updateStation.fulfilled, (state, action) => { state.loading = false; - state.error = action.payload; + // If the station was updated, find and update it in the state + const updatedStation = action.payload; + const stationIndex = state.stations.findIndex( + (station) => station.id === updatedStation.id + ); + if (stationIndex !== -1) { + // Here, merge the updated station with the existing one + // Ensure `status` is not overwritten if not explicitly updated + state.stations[stationIndex] = { + ...state.stations[stationIndex], + ...updatedStation, + }; + } }) + .addCase(updateStation.rejected, (state) => { state.loading = false; })