Merge pull request 'New Updates' (#24) from frontend/apiIntegration into develop

Reviewed-on: DigiMantra/digiev_frontend#24
This commit is contained in:
Mohit kalshan 2025-03-31 04:46:34 +00:00
commit 6d2708e0f4
38 changed files with 1763 additions and 557 deletions

4
public/Group 14.svg Normal file
View file

@ -0,0 +1,4 @@
<svg width="535" height="226" viewBox="0 0 535 226" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M43.561 68.1C55.753 68.1 65.913 72.3757 74.041 80.927C82.2537 89.4783 86.36 100.019 86.36 112.55C86.36 125.081 82.2537 135.622 74.041 144.173C65.913 152.724 55.753 157 43.561 157H8.255V68.1H43.561ZM43.561 140.236C51.2657 140.236 57.531 137.696 62.357 132.616C67.183 127.451 69.596 120.763 69.596 112.55C69.596 104.337 67.183 97.691 62.357 92.611C57.531 87.4463 51.2657 84.864 43.561 84.864H25.781V140.236H43.561ZM98.7921 68.1H116.318V157H98.7921V68.1ZM220.024 109.248V116.106C220.024 128.806 215.96 139.093 207.832 146.967C199.704 154.841 189.205 158.778 176.336 158.778C162.62 158.778 151.232 154.333 142.173 145.443C133.198 136.468 128.711 125.546 128.711 112.677C128.711 99.723 133.156 88.7587 142.046 79.784C151.02 70.8093 162.112 66.322 175.32 66.322C183.617 66.322 191.195 68.227 198.053 72.037C204.911 75.7623 210.245 80.7577 214.055 87.023L199.069 95.659C196.952 92.0183 193.735 89.055 189.417 86.769C185.183 84.483 180.442 83.34 175.193 83.34C166.726 83.34 159.741 86.134 154.238 91.722C148.819 97.2253 146.11 104.21 146.11 112.677C146.11 121.059 148.861 128.002 154.365 133.505C159.953 138.924 167.319 141.633 176.463 141.633C183.236 141.633 188.824 140.151 193.227 137.188C197.714 134.14 200.762 129.991 202.371 124.742H175.447V109.248H220.024ZM232.489 68.1H250.015V157H232.489V68.1ZM410.998 140.236H448.463V157H393.472V68.1H447.828V84.864H410.998V103.787H444.653V120.297H410.998V140.236ZM482.788 157L452.943 68.1H471.993L493.71 136.426L515.3 68.1H534.477L504.505 157H482.788Z" fill="white"/>
<path d="M374.268 108.459L324.874 94.3623L347.782 14.0969L264.907 117.311L314.302 131.408L291.394 211.673L374.268 108.459Z" fill="#52ACDF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
public/evLogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View file

@ -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<any[]>([]); // To hold the car names
const [carPorts, setCarPorts] = useState<any[]>([]); // 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 */}
<Box sx={{ flex: 1 }}>
<Typography variant="body2" fontWeight={500}>
Station ID
Select Station
</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",
})}
/>
<FormControl fullWidth error={!!errors.stationName}>
<InputLabel>Select Station</InputLabel>
<Select
{...register("stationId", {
required: "Station Name is required", // Change to stationId
})}
defaultValue=""
size="small"
>
{stations.map((station) => (
<MenuItem
key={station.id}
value={station.id}
>
{station.name}
</MenuItem>
))}
</Select>
{errors.stationName && (
<Typography color="error" variant="body2">
{errors.stationName.message}
</Typography>
)}
</FormControl>
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="body2" fontWeight={500}>
Date
@ -190,7 +215,6 @@ export default function AddBookingModal({
size="small"
error={!!errors.carName}
>
<InputLabel>Car Name</InputLabel>
<Select
{...field}
label="Car Name"
@ -242,9 +266,9 @@ export default function AddBookingModal({
{carPorts.map((port, index) => (
<MenuItem
key={index}
value={port.chargeType}
value={port}
>
{port.chargeType}
{port}
</MenuItem>
))}
</Select>
@ -266,7 +290,6 @@ export default function AddBookingModal({
{/* 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
@ -287,7 +310,6 @@ export default function AddBookingModal({
/>
</Box>
{/* End Time */}
<Box sx={{ flex: 1 }}>
<Typography variant="body2" fontWeight={500}>
End Time
@ -328,6 +350,114 @@ export default function AddBookingModal({
})}
/>
</Box>
{/* <Box sx={{ flex: 1 }}>
<Typography variant="body2" fontWeight={500}>
{" Time Slot "}
</Typography>
<Controller
control={control}
name="timeSlot"
render={({ field }) => (
<FormControl
fullWidth
size="small"
error={!!errors.timeSlot}
>
<InputLabel>
Select Time Slot
</InputLabel>
<Select
{...field}
label="Time Slot"
multiple // Allow multiple selections
value={field.value || []} // Ensure the value is an array, even if it's empty
onChange={(e) =>
field.onChange(e.target.value)
} // Ensure the selected value is updated properly
>
{availableSlots.map(
(slot, index) => {
const start = dayjs(
slot.startTime
);
const end = dayjs(
slot.endTime
);
// Function to generate half-hour time slots between start and end time
const generateHalfHourSlots =
(
startTime: dayjs.Dayjs,
endTime: dayjs.Dayjs
) => {
const slots = [];
let currentTime =
startTime;
while (
currentTime.isBefore(
endTime
)
) {
const nextTime =
currentTime.add(
30,
"minute"
);
slots.push({
id: `${currentTime.format(
"HH:mm"
)}-${nextTime.format(
"HH:mm"
)}`,
label: `${currentTime.format(
"hh:mm A"
)} - ${nextTime.format(
"hh:mm A"
)}`,
});
currentTime =
nextTime;
}
return slots;
};
// Generate half-hour slots for the current available slot
const halfHourSlots =
generateHalfHourSlots(
start,
end
);
return halfHourSlots.map(
(slot, slotIndex) => (
<MenuItem
key={`${index}-${slotIndex}`}
value={slot.id}
>
{slot.label}
</MenuItem>
)
);
}
)}
</Select>
{errors.timeSlot && (
<Typography
color="error"
variant="body2"
sx={{ mt: 1 }}
>
{errors.timeSlot.message}
</Typography>
)}
</FormControl>
)}
rules={{ required: "Time Slot is required" }}
/>
</Box> */}
</Box>
{/* Submit Button */}

View file

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

View file

@ -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",
},
})}
/>
</Box>
</Box>
{/* Station Dropdown */}
<Box sx={{ display: "flex", gap: 2, mb: 2 }}>
<Box sx={{ flex: 1 }}>
<Typography variant="body2" fontWeight={500}>
Station Id
Select Station
</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",
})}
/>
<FormControl fullWidth error={!!errors.stationName}>
<InputLabel>Select Station</InputLabel>
<Select
{...register("stationName", {
required: "Station Name is required",
})}
defaultValue=""
size="small"
>
{Array.isArray(stations) &&
stations.length > 0 ? (
stations.map((station) => (
<MenuItem
key={station.id}
value={station.name}
>
{station.name}
</MenuItem>
))
) : (
<MenuItem disabled>
No stations available
</MenuItem>
)}
</Select>
{errors.stationName && (
<Typography color="error" variant="body2">
{errors.stationName.message}
</Typography>
)}
</FormControl>
</Box>
</Box>
{/* Email and Password */}
<Box sx={{ display: "flex", gap: 2, mb: 2 }}>
{/* Email */}
@ -219,7 +258,6 @@ export default function AddManagerModal({
: "primary"
}
size="small"
// onMouseDown={togglePasswordVisibility}
InputProps={{
endAdornment: (
<InputAdornment position="end">
@ -247,9 +285,8 @@ export default function AddManagerModal({
</Box>
</Box>
{/* Phone and Registered Address */}
{/* Phone Number */}
<Box sx={{ display: "flex", gap: 2, mb: 2 }}>
{/* Phone */}
<Box sx={{ flex: 1 }}>
<Typography variant="body2" fontWeight={500}>
Phone Number
@ -272,7 +309,6 @@ export default function AddManagerModal({
})}
/>
</Box>
</Box>
{/* Submit Button */}

View file

@ -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<string[]>([]);
const [selectedVehicles, setSelectedVehicles] = useState<string[]>([]);
// 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 */}
<Box sx={{ display: "flex", gap: 2 }}>
<Box
sx={{
@ -175,6 +251,114 @@ export default function AddStationModal({
/>
</Box>
</Box>
{/* Vehicle Brand Dropdown with Checkboxes */}
<Box sx={{ display: "flex", gap: 2 }}>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
}}
>
<Typography variant="body2" fontWeight={500}>
Select Vehicle Brands
</Typography>
<FormControl fullWidth>
<InputLabel>Choose Brands</InputLabel>
<Select
multiple
value={selectedBrands}
onChange={handleBrandChange}
renderValue={(selected) =>
(selected as string[]).join(", ")
}
label="Choose Brands"
>
{vehicleBrands.length > 0 ? (
vehicleBrands.map((brand) => (
<MenuItem
key={brand.id}
value={brand.id}
>
<Checkbox
checked={selectedBrands.includes(
brand.id
)}
/>
<ListItemText
primary={brand.name}
/>
</MenuItem>
))
) : (
<MenuItem disabled>
No vehicle brands available
</MenuItem>
)}
</Select>
<FormHelperText>
{errors.vehicleBrand
? errors.vehicleBrand.message
: ""}
</FormHelperText>
</FormControl>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
}}
>
<Typography variant="body2" fontWeight={500}>
Vehicle Name
</Typography>
<FormControl fullWidth>
<InputLabel>Choose Vehicles</InputLabel>
<Select
multiple
value={selectedVehicles}
onChange={handleVehicleChange}
renderValue={(selected) =>
(selected as string[]).join(", ")
}
>
{filteredVehicles.length > 0 ? (
filteredVehicles.map((vehicle) => (
<MenuItem
key={vehicle.id}
value={vehicle.name}
>
<Checkbox
checked={selectedVehicles.includes(
vehicle.name
)}
/>
<ListItemText
primary={vehicle.name}
/>
</MenuItem>
))
) : (
<MenuItem disabled>
No vehicles available for the
selected brands
</MenuItem>
)}
</Select>
<FormHelperText>
{errors.vehicleName
? errors.vehicleName.message
: ""}
</FormHelperText>
</FormControl>
</Box>
</Box>
</Box>
{/* Submit Button */}

View file

@ -83,6 +83,36 @@ export default function AddVehicleModal({
>
{/* First Row - Two Inputs */}
<Box sx={{ display: "flex", gap: 2 }}>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
}}
>
<Typography variant="body2" fontWeight={500}>
Company Name
</Typography>
<CustomTextField
fullWidth
placeholder="Enter Company Name"
size="small"
error={!!errors.company}
helperText={
errors.company
? errors.company.message
: ""
}
{...register("company", {
required: "Company is required",
minLength: {
value: 3,
message:
"Company must be at least 5 characters long",
},
})}
/>
</Box>
<Box
sx={{
display: "flex",
@ -121,37 +151,6 @@ export default function AddVehicleModal({
})}
/>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
}}
>
<Typography variant="body2" fontWeight={500}>
Company
</Typography>
<CustomTextField
fullWidth
placeholder="Enter Company Name"
size="small"
error={!!errors.company}
helperText={
errors.company
? errors.company.message
: ""
}
{...register("company", {
required: "Company is required",
minLength: {
value: 5,
message:
"Company must be at least 5 characters long",
},
})}
/>
</Box>
</Box>
{/* Second Row - Two Inputs */}

View file

@ -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<availableSlots | null>(
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 (
<Dialog open={open} onClose={onClose}>
<DialogTitle sx={{ color: "#fff", background: "#222" }}>
Available Slots
</DialogTitle>
<Box
sx={{
background: "#111",
padding: "20px",
display: "flex",
flexWrap: "wrap",
justifyContent: "center",
}}
>
{loading ? (
<CircularProgress color="primary" />
) : error ? (
<div style={{ color: "red" }}>Error: {error}</div>
) : Array.isArray(availableSlots) &&
availableSlots.length > 0 ? (
availableSlots.map((slot: availableSlots) => (
<SlotButton
key={slot?.id}
onClick={() => handleSlotSelect(slot)}
selected={selectedSlot?.id === slot?.id}
>
{`${slot?.startTime} - ${slot?.endTime}`}
</SlotButton>
))
) : (
<div>No available slots</div>
)}
<Button
variant="contained"
color="primary"
sx={{ marginTop: "10px" }}
onClick={handleSave}
disabled={!selectedSlot}
>
Save
</Button>
</Box>
</Dialog>
);
};
export default AvailableSlotsModal;

View file

@ -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<CustomTableProps> = ({
@ -108,11 +107,8 @@ 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 { user } = 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);
@ -137,7 +133,6 @@ const CustomTable: React.FC<CustomTableProps> = ({
}
switch (tableType) {
case "admin":
dispatch(deleteAdmin(id || ""));
break;
@ -156,6 +151,9 @@ const CustomTable: React.FC<CustomTableProps> = ({
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<CustomTableProps> = ({
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<CustomTableProps> = ({
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<CustomTableProps> = ({
const currentRows = filteredRows.slice(indexOfFirstRow, indexOfLastRow);
const handlePageChange = (
event: React.ChangeEvent<unknown>,
_event: React.ChangeEvent<unknown>,
value: number
) => {
setCurrentPage(value);
@ -239,30 +240,35 @@ const CustomTable: React.FC<CustomTableProps> = ({
}}
>
{/* 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";
}
})()}
</Typography>
{/* Search & Buttons Section */}
<Box
sx={{
display: "flex",
gap: "16px",
marginTop: "16px",
alignItems: "center",
}}
@ -277,6 +283,11 @@ const CustomTable: React.FC<CustomTableProps> = ({
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<CustomTableProps> = ({
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";
}
})()}
</Button>
)}
</Box>
@ -343,15 +360,17 @@ const CustomTable: React.FC<CustomTableProps> = ({
width: "44px",
height: "44px",
borderRadius: "8px",
backgroundColor: "#272727",
backgroundColor: "#1C1C1C",
color: "#52ACDF",
"&:hover": { backgroundColor: "#333333" },
"*:where([data-mui-color-scheme='dark']) &": {
backgroundColor: "#1C1C1C",
},
}}
>
<TuneIcon />
</IconButton>
</Box>
{/* Table Section */}
<TableContainer
component={Paper}
@ -367,6 +386,10 @@ const CustomTable: React.FC<CustomTableProps> = ({
sx={{
backgroundColor: "#272727",
borderBottom: "none",
".css-1ex4ubw-MuiTableCell-root.MuiTableCell-head ":
{
backgroundColor: "#272727",
},
}}
>
{" "}
@ -384,7 +407,13 @@ const CustomTable: React.FC<CustomTableProps> = ({
))}
</TableRow>
</TableHead>
<TableBody>
<TableBody
sx={{
".MuiTableCell-root": {
backgroundColor: "#1C1C1C",
},
}}
>
{currentRows.map((row, rowIndex) => (
<StyledTableRow key={rowIndex}>
{columns.map((column) => (
@ -433,7 +462,6 @@ const CustomTable: React.FC<CustomTableProps> = ({
</TableBody>
</Table>
</TableContainer>
{/* Pagination */}
<Box
sx={{
@ -467,7 +495,6 @@ const CustomTable: React.FC<CustomTableProps> = ({
}}
/>
</Box>
{/* Menu Actions */}
{open && (
<Menu
@ -476,8 +503,14 @@ const CustomTable: React.FC<CustomTableProps> = ({
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<CustomTableProps> = ({
</Box>
</Menu>
)}
{/* Modals */}
{deleteModal && (
<DeleteModal

View file

@ -14,11 +14,19 @@ import {
CustomIconButton,
CustomTextField,
} from "../AddEditUserModel/styled.css.tsx"; // Custom styled components
import { AppDispatch } from "../../redux/store/store.ts";
interface EditManagerModalProps {
open: boolean;
handleClose: () => void;
editRow: any; // Manager data including id
handleUpdate: (
id: string,
name: string,
email: string,
phone: string,
stationId: string
) => Promise<void>;
editRow: any;
}
interface FormData {
@ -33,7 +41,7 @@ const EditManagerModal: React.FC<EditManagerModalProps> = ({
handleClose,
editRow,
}) => {
const dispatch = useDispatch(); // Use dispatch to send Redux actions
const dispatch = useDispatch<AppDispatch>();
const {
control,
handleSubmit,

View file

@ -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<void>;
editRow: any; // Slot data including id
}
interface FormData {
date: string;
startTime: string;
endTime: string;
isAvailable: boolean;
}
const EditSlotModal: React.FC<EditSlotModalProps> = ({
open,
handleClose,
editRow,
}) => {
const dispatch = useDispatch<AppDispatch>();
const {
control,
handleSubmit,
formState: { errors },
setValue,
reset,
} = useForm<FormData>({
defaultValues: {
date: "",
startTime: "",
endTime: "",
isAvailable: false,
},
});
const [loading, setLoading] = useState(false);
const [isAvailable, setIsAvailable] = useState<boolean>(
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 (
<Modal
open={open}
onClose={(e, reason) => {
if (reason === "backdropClick") {
return;
}
handleClose(); // Close modal when clicking cross or cancel
}}
aria-labelledby="edit-slot-modal"
>
<Box
component="form"
onSubmit={handleSubmit(onSubmit)}
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 400,
bgcolor: "background.paper",
boxShadow: 24,
p: 3,
borderRadius: 2,
}}
>
{/* Header */}
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography variant="h6" fontWeight={600}>
Edit Slot
</Typography>
<CustomIconButton onClick={handleClose}>
<CloseIcon />
</CustomIconButton>
</Box>
{/* Horizontal Line */}
<Box sx={{ borderBottom: "1px solid #ddd", my: 2 }} />
{/* Input Fields */}
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 2 }}>
{/* Date */}
{/* <Box sx={{ flex: "1 1 100%" }}>
<Typography variant="body2" fontWeight={500} mb={0.5}>
Date
</Typography>
<Controller
name="date"
control={control}
rules={{ required: "Date is required" }}
render={({ field }) => (
<CustomTextField
{...field}
type="date"
fullWidth
size="small"
error={!!errors.date}
helperText={errors.date?.message}
/>
)}
/>
</Box> */}
{/* Start Time */}
<Box sx={{ flex: "1 1 48%" }}>
<Typography variant="body2" fontWeight={500} mb={0.5}>
Start Time
</Typography>
<Controller
name="startTime"
control={control}
rules={{ required: "Start Time is required" }}
render={({ field }) => (
<CustomTextField
{...field}
type="time"
fullWidth
size="small"
error={!!errors.startTime}
helperText={errors.startTime?.message}
/>
)}
/>
</Box>
{/* End Time */}
<Box sx={{ flex: "1 1 48%" }}>
<Typography variant="body2" fontWeight={500} mb={0.5}>
End Time
</Typography>
<Controller
name="endTime"
control={control}
rules={{ required: "End Time is required" }}
render={({ field }) => (
<CustomTextField
{...field}
type="time"
fullWidth
size="small"
error={!!errors.endTime}
helperText={errors.endTime?.message}
/>
)}
/>
</Box>
{/* Availability Toggle */}
<Box
display="flex"
alignItems="center"
justifyContent="space-between"
gap={2}
sx={{ flex: "1 1 100%" }}
>
<Button
onClick={() => {
const newAvailability = !isAvailable;
setIsAvailable(newAvailability); // Update local state
setValue("isAvailable", newAvailability); // Update form value for isAvailable
}}
variant={isAvailable ? "contained" : "outlined"}
color="primary"
>
{isAvailable ? "Available" : "Not Available"}
</Button>
<Typography>
{isAvailable ? "Available" : "Not Available"}
</Typography>
</Box>
</Box>
{/* Submit Button */}
<Box
sx={{ display: "flex", justifyContent: "flex-end", mt: 3 }}
>
<Button
type="submit"
sx={{
backgroundColor: "#52ACDF",
color: "white",
borderRadius: "8px",
width: "117px",
"&:hover": { backgroundColor: "#439BC1" },
}}
disabled={loading} // Disable the button during loading state
>
{loading ? (
<CircularProgress size={24} color="inherit" />
) : (
"Update Slot"
)}
</Button>
</Box>
</Box>
</Modal>
);
};
export default EditSlotModal;

View file

@ -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<EditStationModalProps> = ({
@ -36,42 +55,85 @@ const EditStationModal: React.FC<EditStationModalProps> = ({
const {
control,
handleSubmit,
formState: { errors },
setValue,
reset,
formState: { errors },
} = useForm<FormData>({
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<string[]>([]);
const [selectedVehicles, setSelectedVehicles] = useState<string[]>([]);
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<EditStationModalProps> = ({
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<EditStationModalProps> = ({
{/* Horizontal Line */}
<Box sx={{ borderBottom: "1px solid #ddd", my: 2 }} />
{/* Input Fields */}
{/* Form */}
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
{/* First Row - Two Inputs */}
{/* Station Name and Address */}
<Box sx={{ display: "flex", gap: 2 }}>
<Box
sx={{
@ -140,19 +202,6 @@ const EditStationModal: React.FC<EditStationModalProps> = ({
<Controller
name="name"
control={control}
rules={{
required: "Station Name is required",
minLength: {
value: 3,
message:
"Minimum 3 characters required",
},
maxLength: {
value: 30,
message:
"Maximum 30 characters allowed",
},
}}
render={({ field }) => (
<CustomTextField
{...field}
@ -160,12 +209,15 @@ const EditStationModal: React.FC<EditStationModalProps> = ({
placeholder="Enter Station Name"
size="small"
error={!!errors.name}
helperText={errors.name?.message}
helperText={
errors.name
? errors.name.message
: ""
}
/>
)}
/>
</Box>
<Box
sx={{
display: "flex",
@ -183,9 +235,6 @@ const EditStationModal: React.FC<EditStationModalProps> = ({
<Controller
name="registeredAddress"
control={control}
rules={{
required: "Registered Address is required",
}}
render={({ field }) => (
<CustomTextField
{...field}
@ -194,7 +243,10 @@ const EditStationModal: React.FC<EditStationModalProps> = ({
size="small"
error={!!errors.registeredAddress}
helperText={
errors.registeredAddress?.message
errors.registeredAddress
? errors.registeredAddress
.message
: ""
}
/>
)}
@ -202,7 +254,7 @@ const EditStationModal: React.FC<EditStationModalProps> = ({
</Box>
</Box>
{/* Second Row - Total Slots */}
{/* Total Slots */}
<Box sx={{ display: "flex", gap: 2 }}>
<Box
sx={{
@ -221,13 +273,6 @@ const EditStationModal: React.FC<EditStationModalProps> = ({
<Controller
name="totalSlots"
control={control}
rules={{
required: "Total Slots are required",
min: {
value: 1,
message: "At least 1 slot is required",
},
}}
render={({ field }) => (
<CustomTextField
{...field}
@ -236,15 +281,115 @@ const EditStationModal: React.FC<EditStationModalProps> = ({
size="small"
type="number"
error={!!errors.totalSlots}
helperText={errors.totalSlots?.message}
helperText={
errors.totalSlots
? errors.totalSlots.message
: ""
}
/>
)}
/>
</Box>
</Box>
{/* Vehicle Brand Selection with Checkboxes */}
<Box sx={{ display: "flex", gap: 2 }}>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
}}
>
<Typography variant="body2" fontWeight={500}>
Select Vehicle Brands
</Typography>
<FormControl fullWidth>
<InputLabel>Choose Brand</InputLabel>
<Select
multiple
value={selectedBrands}
onChange={handleBrandChange}
label="Choose Brands"
>
{vehicleBrands.length > 0 ? (
vehicleBrands.map((brand) => (
<MenuItem
key={brand.id}
value={brand.id}
>
<Checkbox
checked={selectedBrands.includes(
brand.id
)}
/>
<ListItemText
primary={brand.name}
/>
</MenuItem>
))
) : (
<MenuItem disabled>
No vehicle brands available
</MenuItem>
)}
</Select>
</FormControl>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
}}
>
<Typography variant="body2" fontWeight={500}>
Vehicle Name
</Typography>
<FormControl fullWidth>
<InputLabel>Choose Vehicles</InputLabel>
<Select
multiple
value={selectedVehicles}
onChange={handleCheckboxChange}
renderValue={(selected) =>
(selected as string[]).join(", ")
}
>
{filteredVehicles.length > 0 ? (
filteredVehicles.map((vehicle) => (
<MenuItem
key={vehicle.id}
value={vehicle.name}
>
<Checkbox
checked={selectedVehicles.includes(
vehicle.name
)}
/>
<ListItemText
primary={vehicle.name}
/>
</MenuItem>
))
) : (
<MenuItem disabled>
No vehicles available for this brand
</MenuItem>
)}
</Select>
<FormHelperText>
{errors.allowedCarIds
? errors.allowedCarIds.message
: ""}
</FormHelperText>
</FormControl>
</Box>
</Box>
</Box>
{/* Submit Button */}
<Box
sx={{ display: "flex", justifyContent: "flex-end", mt: 3 }}
@ -259,7 +404,7 @@ const EditStationModal: React.FC<EditStationModalProps> = ({
"&:hover": { backgroundColor: "#439BC1" },
}}
>
Update Charging Station
Update Station
</Button>
</Box>
</Box>

View file

@ -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<AppDispatch>();
const { user } = useSelector(
(state: RootState) => state?.profileReducer
);
const dispatch = useDispatch<AppDispatch>();
const { user } = useSelector((state: RootState) => state?.profileReducer);
React.useEffect(() => {
dispatch(fetchAdminProfile());
}, [dispatch]);
React.useEffect(() => {
dispatch(fetchAdminProfile());
}, [dispatch]);
return (
<Box
sx={{
@ -89,7 +88,6 @@ export default function Header() {
display: { xs: "none", sm: "flex" }, // Hide on mobile, show on larger screens
}}
>
<NotificationsNoneIcon onClick={toggleNotifications} />
<Avatar
alt="User Avatar"

View file

@ -7,7 +7,7 @@ import Typography from "@mui/material/Typography";
import Stack from "@mui/material/Stack";
import { LineChart } from "@mui/x-charts/LineChart";
import { Box } from "@mui/material";
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
function AreaGradient({ color, id }: { color: string; id: string }) {
return (
<defs>
@ -43,7 +43,14 @@ export default function LineChartCard() {
return (
<Card
variant="outlined"
sx={{ width: "100%", height: "100%", backgroundColor: "#202020" }}
sx={{
width: "553px",
height: "444px",
borderRadius: "16px",
"*:where([data-mui-color-scheme='dark']) &": {
backgroundColor: "#202020",
},
}}
>
<CardContent>
<div
@ -74,29 +81,39 @@ export default function LineChartCard() {
marginLeft: "auto",
marginRight: "16px",
display: "flex",
justifyContent: "space-between",
justifyContent: "center",
alignItems: "center",
p: 1.5,
borderRadius: "8px",
border: "1px solid #454545",
padding: "4px 8px",
color: "#F2F2F2",
width: "129px",
height: "44px",
padding: "12px 16px",
gap: "8px",
}}
>
<Typography
width={"69px"}
height={"16px"}
sx={{
fontFamily: "Gilroy",
fontWeight: 500,
fontSize: "14px",
lineHeight: "24px",
color: "#F2F2F2",
p: "4px",
color: "#D9D8D8",
textAlign: "center",
}}
>
Weekly
</Typography>
<ArrowDropDownIcon sx={{ color: "#F2F2F2" }} />
<ExpandMoreIcon
width="20px"
height="20px"
sx={{ color: "#F2F2F2" }}
/>
</Box>
</div>

View file

@ -45,7 +45,7 @@ export default function MainGrid() {
</Typography>
<Grid
container
spacing={2}
spacing={3}
columns={12}
// sx={{ mb: (theme) => theme.spacing(2) }}

View file

@ -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: <AnalyticsRoundedIcon />,
icon: <ManageAccountsOutlinedIcon />,
url: "/panel/admin-list",
},
userRole === "superadmin" && {
text: "Roles",
icon: <AnalyticsRoundedIcon />,
icon: <AnalyticsOutlinedIcon />,
url: "/panel/role-list",
},
userRole === "admin" && {
text: "Users",
icon: <AnalyticsRoundedIcon />,
icon: <PeopleOutlinedIcon />,
url: "/panel/user-list",
},
userRole === "admin" && {
text: "Charging Stations",
icon: <ManageAccountsOutlinedIcon />,
icon: <EvStationIcon />,
url: "/panel/station-list", // Placeholder for now
},
userRole === "admin" && {
text: "Managers",
icon: <ManageAccountsOutlinedIcon />,
icon: <PeopleOutlinedIcon />,
url: "/panel/manager-list", // Placeholder for now
},
userRole === "admin" && {
text: "Vehicles",
icon: <ManageAccountsOutlinedIcon />,
icon: <AnalyticsOutlinedIcon />,
url: "/panel/vehicle-list", // Placeholder for now
},
// userRole === "manager" && {
// text: "Add Slots",
// icon: <ManageAccountsOutlinedIcon />,
// url: "/panel/EVslots", // Placeholder for now
// text: "Add Slots",
// icon: <ManageAccountsOutlinedIcon />,
// url: "/panel/EVslots", // Placeholder for now
// },
userRole === "user" && {
text: "Bookings",
icon: <ManageAccountsOutlinedIcon />,
icon: <BookOnlineOutlinedIcon />,
url: "/panel/booking-list", // Placeholder for now
},
userRole === "manager" && {
text: "Available Slots",
icon: <ManageAccountsOutlinedIcon />,
icon: <ChecklistSharpIcon />,
url: "/panel/slot-list", // Placeholder for now
},
userRole === "user" && {
text: "Available Slots",
icon: <ManageAccountsOutlinedIcon />,
icon: <ChecklistSharpIcon />,
url: "/panel/slot-list", // Placeholder for now
},
];
@ -85,25 +90,33 @@ export default function MenuContent({ hidden }: PropType) {
const filteredMenuItems = baseMenuItems.filter(Boolean);
return (
<Stack sx={{ flexGrow: 1, p: 1, justifyContent: "space-between" }}>
<Stack
sx={{
flexGrow: 1,
p: 1,
justifyContent: "space-between",
backgroundColor: "#202020",
}}
>
<List dense>
{filteredMenuItems.map((item, index) => (
<ListItem
key={index}
disablePadding
sx={{ display: "block", py: 1 }}
sx={{ display: "block", py: 1, px: 0.9 }}
>
<ListItemButton
component={Link}
to={item.url}
selected={item.url === location.pathname}
sx={{ alignItems: "center", columnGap: 1 }}
sx={{ alignItems: "center", columnGap: 0.5 }}
>
<ListItemIcon
sx={{
minWidth: "fit-content",
".MuiSvgIcon-root": {
fontSize: 24,
color: "#FFFFFF",
},
}}
>
@ -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}

View file

@ -8,7 +8,7 @@ type Props = {
open: boolean;
setViewModal: Function;
handleView: (id: string | undefined) => void;
id?: number | undefined;
id?: string | undefined;
};
const style = {

View file

@ -110,7 +110,8 @@ export default function ManagerViewModal({ open, setViewModal, id }: Props) {
<Typography variant="body1">
<strong>Station Location:</strong>
<Typography variant="body2">
{selectedManager.registeredAddress}
{selectedManager.chargingStation
?.registeredAddress || "Not Available"}
</Typography>
</Typography>
</Grid>
@ -118,7 +119,8 @@ export default function ManagerViewModal({ open, setViewModal, id }: Props) {
<Typography variant="body1">
<strong>Station Name:</strong>
<Typography variant="body2">
{selectedManager.stationName}
{selectedManager.chargingStation?.name ||
"Not Available"}
</Typography>
</Typography>
</Grid>

View file

@ -7,6 +7,7 @@ import CloseIcon from "@mui/icons-material/Close";
type Props = {
open: boolean;
setViewModal: Function;
handleView: (id: string | undefined) => void;
id?: string;
};

View file

@ -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",
},
}}
>
<CardContent>
@ -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%",
}}
/>
<Typography variant="body2" color="#F2F2F2">
<Typography
variant="body2"
width="100px"
height="16px"
color="#FFFFFF"
>
{entry.title}
</Typography>
</Stack>

View file

@ -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 (
<Card
variant="outlined"
sx={{
width: "100%",
height: "100%",
backgroundColor: "#202020",
p: 2,
width: "553px",
height: "324px",
gap: "16px",
borderRadius: "16px",
padding: "20px",
"*:where([data-mui-color-scheme='dark']) &": {
backgroundColor: "#202020",
},
}}
>
<CardContent>
@ -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() {
}}
>
<Typography
width="134px"
height="24px"
sx={{
fontFamily: "Gilroy",
fontWeight: 500,
fontSize: "14px",
lineHeight: "24px",
color: "#F2F2F2",
p: "4px",
color: "#D9D8D8",
}}
>
Delhi NCR EV Station
@ -78,7 +91,6 @@ export default function SessionsChart() {
mx: "auto",
}}
>
{/* You can map over your data here; for simplicity, were using static boxes */}
{[1, 2, 3, 4].map((item) => (
<Box
key={item}
@ -87,23 +99,47 @@ export default function SessionsChart() {
borderRadius: "8px",
p: "12px 16px",
backgroundColor: "#272727",
color: "#F2F2F2",
color: "#D9D8D8",
}}
>
<Typography
component="h1"
variant="body2"
width="98px"
height="24px"
fontWeight={400}
fontSize={"14px"}
lineHeight={"24px"}
gutterBottom
>
Basic Charging
</Typography>
<Typography
component="h1"
variant="subtitle2"
gutterBottom
>
16.83 cents/kWh
</Typography>
<Box display={"flex"} gap={1}>
<Typography
component="h1"
variant="subtitle2"
color="#FFFFFF"
width="40px"
height={"24px"}
fontWeight={500}
fontSize="18px"
lineHeight="24px"
gutterBottom
>
16.83
</Typography>
<Typography
width="71px"
height="24px"
gap="2px"
fontWeight={400}
fontSize={"14px"}
lineHeight={"24px"}
>
cents/kWh
</Typography>
</Box>
</Box>
))}
</Box>

View file

@ -64,26 +64,34 @@ export default function SideMenu() {
pl: 2,
}}
>
<Avatar
<img
src="/evLogo.png"
alt="Logo"
src="/Digilogo.png"
sx={{
width: "50px",
height: "50px",
style={{
justifyContent: "center",
width: open ? "200px" : "60px", // Adjust width depending on open state
height: "auto",
transition: "width 0.5s ease", // Smooth transition for width change
}}
/>
<Box
{/* <Avatar
alt="Logo"
src="/evLogo.png"
sx={{
display: "flex",
width: "100%",
height: "100%",
}}
/> */}
{/* <Box
sx={{
display: open ? "flex" : "none",
flexDirection: "column",
alignItems: "center",
pt: 2,
textAlign: "center",
}}
>
{/* Digi EV Text Section */}
<Typography
variant="body2"
color="#D9D8D8"
@ -94,11 +102,10 @@ export default function SideMenu() {
>
Digi EV
</Typography>
<Typography variant="subtitle2" color="#D9D8D8" >
<Typography variant="subtitle2" color="#D9D8D8">
{user?.userType || "N/A"}
</Typography>
</Box>
</Box> */}
</Box>
<Box

View file

@ -18,13 +18,28 @@ export default function StatCard({ title, value }: StatCardProps) {
return (
<Card
variant="outlined"
sx={{ height: "100%", backgroundColor: "#202020" }}
sx={{
width: "264.5px",
height: "90px",
padding: "16px",
borderRadius: "12px",
gap: "24px",
"*:where([data-mui-color-scheme='dark']) &": {
backgroundColor: "#202020",
},
}}
>
<CardContent>
<Typography
component="h2"
variant="subtitle2"
color="#F2F2F2"
width={"118px"}
height={"14px"}
fontWeight={400}
fontSize={"12px"}
lineHeight={"14px"}
letterSpacing={"0%"}
gutterBottom
>
{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}

View file

@ -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 (
<Card
variant="outlined"
sx={{ width: "100%", height: "100%", backgroundColor: "#202020" }}
sx={{
width: "553px",
height: "444px",
borderRadius: "16px",
"*:where([data-mui-color-scheme='dark']) &": {
backgroundColor: "#202020",
},
}}
>
<CardContent>
<div
@ -61,29 +56,41 @@ export default function RoundedBarChart() {
marginLeft: "auto",
marginRight: "16px",
display: "flex",
justifyContent: "space-between",
justifyContent: "center",
alignItems: "center",
p: 1.5,
borderRadius: "8px",
border: "1px solid #454545",
padding: "4px 8px",
// padding: "4px 8px",
color: "#F2F2F2",
width: "129px",
height: "44px",
padding: "12px 16px",
gap: "8px",
}}
>
<Typography
width={"69px"}
height={"16px"}
sx={{
fontFamily: "Gilroy",
fontWeight: 500,
fontSize: "14px",
lineHeight: "24px",
color: "#F2F2F2",
p: "4px",
color: "#D9D8D8",
textAlign: "center",
}}
>
Monthly
</Typography>
<ArrowDropDownIcon sx={{ color: "#F2F2F2" }} />
<ExpandMoreIcon
width="20px"
height="20px"
sx={{ color: "#F2F2F2" ,textAlign: "center", }}
/>
</Box>
</div>
<BarChart

View file

@ -50,12 +50,8 @@ const DashboardLayout: React.FC<LayoutProps> = ({ customStyles }) => {
...customStyles,
mt: { xs: 8, md: 0 },
padding: 0,
})}
>
<Stack
spacing={2}
sx={{

View file

@ -1,25 +1,35 @@
import axios from "axios";
// import { useHistory } from "react-router-dom";
const http = axios.create({
baseURL: process.env.REACT_APP_BACKEND_URL,
});
http.interceptors.request.use((config) => {
const authToken = localStorage.getItem("authToken");
if (authToken) {
config.headers.Authorization = `Bearer ${authToken}`;
}
return config;
});
http.interceptors.response.use(
(response) => response,
(error) => {
if (error.response && error.response.status === 401) {
if (error.response) {
const status = error.response.status;
const requestUrl = error.config.url; // Get the API route
window.location.href = "/login";
// Handle token expiration (401) but NOT for login failures
if (status === 401 && !requestUrl.includes("/login")) {
localStorage.removeItem("authToken");
window.location.href = "/login";
}
// const history = useHistory();
// history.push("/login");
// Handle forbidden access
if (status === 403) {
localStorage.removeItem("authToken");
window.location.href = "/login";
}
}
return Promise.reject(error);
}

View file

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

View file

@ -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<ILoginForm>({ mode: "onChange" });
const dispatch = useDispatch();
const dispatch = useDispatch<AppDispatch>();
const router = useNavigate();
const handleClickOpen = () => {
@ -104,11 +105,11 @@ export default function Login(props: { disableCustomTheme?: boolean }) {
}}
>
<img
src="/DigiEVLogo.png"
src="/evLogo.png"
alt="Logo"
style={{
justifyContent: "center",
width: "250px",
width: "180px",
height: "auto",
}}
/>
@ -329,9 +330,7 @@ export default function Login(props: { disableCustomTheme?: boolean }) {
? "error"
: "primary"
}
onMouseDown={
togglePasswordVisibility
}
InputProps={{
endAdornment: (
<InputAdornment position="end">

View file

@ -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<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
@ -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");
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",
};
})
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}
/>
<EditSlotModal
open={editModalOpen}
handleClose={handleCloseModal}
handleUpdate={handleUpdate}
editRow={rowData}
/>
</>
);
}

View file

@ -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<boolean>(false);
const [editModalOpen, setEditModalOpen] = useState<boolean>(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 (
<>

View file

@ -42,14 +42,24 @@ const ProfilePage = () => {
}
return (
<Container sx={{ py: 4 }}>
<Container
sx={{
py: 4,
}}
>
<Typography variant="h4" gutterBottom>
Account Info
</Typography>
<Card
sx={{
width: "1132px",
height: "331px",
gap: "24px",
borderRadius: "12px",
padding: "16px",
maxWidth: "100%",
margin: "0 auto",
backgroundColor: "#1C1C1C",
}}
>
<CardContent>
@ -62,16 +72,29 @@ const ProfilePage = () => {
<Avatar
alt="User Avatar"
src="/avatar.png"
sx={{ width: 36, height: 36 }}
sx={{ width: "60px", height: "60px" }}
/>
<Box>
<Typography
variant="body1"
width="1028px"
height="24px"
fontWeight={500}
fontSize={"20px"}
lineHeight={"100%"}
sx={{ color: "#FFFFFF" }}
>
{user?.name || "No Admin"}
</Typography>
<Typography variant="body2" color="#D9D8D8">
<Typography
variant="body2"
color="#D9D8D8"
fontWeight={400}
fontSize={"16px"}
lineHeight={"100%"}
width="46px"
height="19px"
>
{user?.userType || "N/A"}
</Typography>
</Box>
@ -88,6 +111,11 @@ const ProfilePage = () => {
>
<Typography
variant="body1"
width="1048px"
height="19px"
fontWeight={500}
fontSize={"16px"}
lineHeight={"100%"}
sx={{ color: "#FFFFFF" }}
>
Personal Information
@ -95,23 +123,39 @@ const ProfilePage = () => {
<Link
variant="body1"
href="/edit-profile"
color="#52ACDF"
>
Edit
</Link>
</Stack>
<Grid container>
<Grid item xs={12} sm={4}>
<Grid
container
width="1100px"
height="38px"
// gap={"100px"}
>
<Grid
item
xs={12}
sm={4}
width="64px"
height="16px"
fontSize={"14px"}
lineHeight={"100%"}
>
<Typography
variant="body1"
fontWeight={500}
sx={{ color: "#FFFFFF" }}
>
Full Name:
</Typography>
<Typography variant="body2" color="#D9D8D8">
<Typography
variant="body2"
fontWeight={400}
color="#D9D8D8"
>
{user?.name || "N/A"}
</Typography>
</Grid>
@ -139,10 +183,20 @@ const ProfilePage = () => {
</Grid>
</Grid>
<Typography variant="body1" sx={{ color: "#FFFFFF" }}>
<Typography
variant="body1"
width="21px"
height="16px"
sx={{ color: "#FFFFFF" }}
>
Bio:
</Typography>
<Typography variant="body2" color="#D9D8D8">
<Typography
variant="body2"
width="1100px"
height="32px"
color="#D9D8D8"
>
{user?.bio || "No bio available."}
</Typography>
</Stack>

View file

@ -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<boolean>(false);
@ -25,10 +25,13 @@ export default function StationList() {
const [rowData, setRowData] = useState<any | null>(null);
const [searchTerm, setSearchTerm] = useState("");
const dispatch = useDispatch<AppDispatch>();
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: (
<Chip
label={
station.status === 1 ? "Available" : "Not Available"
}
color={station.status === 1 ? "primary" : "default"}
variant="outlined"
sx={{
fontWeight: 600,
width: "80px",
textAlign: "center",
borderRadius: "4px",
}}
/>
),
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: (
// <Switch
// checked={station.status === 1}
// onChange={() =>
// handleStatusToggle(station.id, station.status)
// }
// color="primary"
// inputProps={{ "aria-label": "station-status-toggle" }}
// />
// ),
// statusValue: station.status,
// }))
// : [];
return (
<>
<CustomTable
@ -194,6 +168,7 @@ export default function StationList() {
handleClose={handleCloseModal}
handleUpdate={handleUpdate}
editRow={rowData}
vehicles={vehicles}
/>
</>
);

View file

@ -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<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 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 (
<>
<CustomTable
@ -159,7 +155,6 @@ export default function VehicleList() {
handleUpdate={handleUpdate}
editRow={rowData}
/>
</>
);
}

View file

@ -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<VehicleBrand[]>) => {
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<string | undefined>) => {
state.loading = false;
}
)
.addCase(addVehicle.rejected, (state) => {
state.loading = false;
})
.addCase(updateVehicle.pending, (state) => {
state.loading = true;
})

View file

@ -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<string[]>) => {
state.carPorts = action.payload;
}
);
)
.addCase(deleteBooking.pending, (state) => {
state.loading = true;
})
.addCase(
deleteBooking.fulfilled,
(state, action: PayloadAction<string>) => {
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";
});
},
});

View file

@ -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;

View file

@ -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<Slot>) => {
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";

View file

@ -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<any, void, { rejectValue: string }>(
}
}
);
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<any>) => {
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<any>) => {
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<any>) => {
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<string | undefined>) => {
@ -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;
})