Merge pull request 'frontend/apiIntegration' (#29) from frontend/apiIntegration into develop
Reviewed-on: DigiMantra/digiev_frontend#29
BIN
public/apple_store.png
Normal file
After Width: | Height: | Size: 5 KiB |
BIN
public/arrow-top-right.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
28
public/bg.svg
Normal file
|
@ -0,0 +1,28 @@
|
|||
<svg width="1920" height="751" viewBox="0 0 1920 751" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.1" filter="url(#filter0_f_1407_4696)">
|
||||
<path d="M357.765 483.542C426.213 434.499 1144.46 549.989 1258.37 579.451C1372.28 608.913 1757.57 743.868 1689.13 792.911C1620.68 841.954 527.424 923.713 413.507 894.245C299.59 864.776 289.318 532.588 357.765 483.542Z" fill="#0DB3D5"/>
|
||||
</g>
|
||||
<g opacity="0.2" filter="url(#filter1_f_1407_4696)">
|
||||
<path d="M605.311 371.098C596.35 427.875 656.872 561.013 526.007 556.64C395.141 552.267 122.44 412.046 131.401 355.264C140.363 298.481 198.295 172.294 329.163 176.666C460.031 181.038 614.27 314.317 605.311 371.098Z" fill="#EAF1B8"/>
|
||||
</g>
|
||||
<g opacity="0.2" filter="url(#filter2_f_1407_4696)">
|
||||
<path d="M1222 456.032C1222 409.041 1493.74 156 1551.77 156C1609.81 156 1882 384.378 1882 431.369C1882 478.359 1515.47 735 1457.43 735C1399.39 735 1222 503.022 1222 456.032Z" fill="#B59EDB"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_f_1407_4696" x="6.99048" y="163.563" width="1998.22" height="1044.92" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="154" result="effect1_foregroundBlur_1407_4696"/>
|
||||
</filter>
|
||||
<filter id="filter1_f_1407_4696" x="-176.814" y="-131.445" width="1096.48" height="996.19" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="154" result="effect1_foregroundBlur_1407_4696"/>
|
||||
</filter>
|
||||
<filter id="filter2_f_1407_4696" x="914" y="-152" width="1276" height="1195" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="154" result="effect1_foregroundBlur_1407_4696"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
BIN
public/bgBottom.png
Normal file
After Width: | Height: | Size: 316 KiB |
BIN
public/bgEV.png
Normal file
After Width: | Height: | Size: 316 KiB |
34
public/bgev.svg
Normal file
|
@ -0,0 +1,34 @@
|
|||
<svg width="1920" height="1425" viewBox="0 0 1920 1425" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1407_4268)">
|
||||
<rect width="1920" height="1425" fill="#111111"/>
|
||||
<g opacity="0.1" filter="url(#filter0_f_1407_4268)">
|
||||
<path d="M357.765 483.542C426.213 434.499 1144.46 549.989 1258.37 579.451C1372.28 608.913 1757.57 743.868 1689.13 792.911C1620.68 841.955 527.425 923.713 413.508 894.245C299.591 864.776 289.318 532.588 357.765 483.542Z" fill="#0DB3D5"/>
|
||||
</g>
|
||||
<g opacity="0.2" filter="url(#filter1_f_1407_4268)">
|
||||
<path d="M605.311 371.098C596.349 427.875 656.872 561.013 526.007 556.64C395.141 552.267 122.439 412.046 131.401 355.264C140.363 298.481 198.295 172.294 329.163 176.666C460.031 181.038 614.27 314.317 605.311 371.098Z" fill="#EAF1B8"/>
|
||||
</g>
|
||||
<g opacity="0.2" filter="url(#filter2_f_1407_4268)">
|
||||
<path d="M1222 456.032C1222 409.042 1493.74 156 1551.77 156C1609.81 156 1882 384.379 1882 431.369C1882 478.359 1515.47 735 1457.43 735C1399.39 735 1222 503.022 1222 456.032Z" fill="#B59EDB"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_f_1407_4268" x="6.99072" y="163.564" width="1998.22" height="1044.92" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="154" result="effect1_foregroundBlur_1407_4268"/>
|
||||
</filter>
|
||||
<filter id="filter1_f_1407_4268" x="-176.814" y="-131.445" width="1096.48" height="996.19" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="154" result="effect1_foregroundBlur_1407_4268"/>
|
||||
</filter>
|
||||
<filter id="filter2_f_1407_4268" x="914" y="-152" width="1276" height="1195" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="154" result="effect1_foregroundBlur_1407_4268"/>
|
||||
</filter>
|
||||
<clipPath id="clip0_1407_4268">
|
||||
<rect width="1920" height="1425" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
BIN
public/dev-bg.png
Normal file
After Width: | Height: | Size: 9.9 KiB |
BIN
public/developer.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
public/google_play.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
public/hero.png
Normal file
After Width: | Height: | Size: 154 KiB |
BIN
public/iMockup - iPhone.png
Normal file
After Width: | Height: | Size: 221 KiB |
BIN
public/left-bottom-arrow.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
public/tablet-img.png
Normal file
After Width: | Height: | Size: 148 KiB |
BIN
public/vector-arrows.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
|
@ -219,7 +219,7 @@ export default function AddBookingModal({
|
|||
{carNames.map((car, index) => (
|
||||
<MenuItem
|
||||
key={car.id}
|
||||
value={car.id}
|
||||
value={car.name}
|
||||
>
|
||||
{car.name}
|
||||
</MenuItem>
|
||||
|
|
|
@ -300,10 +300,17 @@ export default function AddManagerModal({
|
|||
}
|
||||
{...register("phone", {
|
||||
required: "Phone Number is required",
|
||||
pattern: {
|
||||
value: /^[0-9]{10}$/,
|
||||
message:
|
||||
"Phone number must be 10 digits",
|
||||
validate: (value) => {
|
||||
if (!/^[0-9]*$/.test(value)) {
|
||||
return "Only numbers are allowed";
|
||||
}
|
||||
if (value.length < 6) {
|
||||
return "Phone number must be at least 6 digits";
|
||||
}
|
||||
if (value.length > 14) {
|
||||
return "Phone number must be at most 14 digits";
|
||||
}
|
||||
return true; // No errors
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
|
|
@ -264,19 +264,17 @@ const AddUserModal: React.FC<AddUserModalProps> = ({
|
|||
control={control}
|
||||
rules={{
|
||||
required: "Phone number is required",
|
||||
pattern: {
|
||||
value: /^[0-9]*$/,
|
||||
message: "Only numbers are allowed",
|
||||
},
|
||||
minLength: {
|
||||
value: 6,
|
||||
message:
|
||||
"Phone number must be at least 6 digits",
|
||||
},
|
||||
maxLength: {
|
||||
value: 14,
|
||||
message:
|
||||
"Phone number must be at most 14 digits",
|
||||
validate: (value) => {
|
||||
if (!/^[0-9]*$/.test(value)) {
|
||||
return "Only numbers are allowed";
|
||||
}
|
||||
if (value.length < 6) {
|
||||
return "Phone number must be at least 6 digits";
|
||||
}
|
||||
if (value.length > 14) {
|
||||
return "Phone number must be at most 14 digits";
|
||||
}
|
||||
return true; // No errors
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
|
|
|
@ -203,17 +203,15 @@ const CustomTable: React.FC<CustomTableProps> = ({
|
|||
handleClose();
|
||||
};
|
||||
|
||||
const filteredRows = rows.filter((row) => {
|
||||
if (!searchQuery.trim()) return true; // Return all rows if searchQuery is empty or whitespace
|
||||
const lowerCaseQuery = searchQuery.toLowerCase().trim();
|
||||
return (
|
||||
(row.name && row.name.toLowerCase().includes(lowerCaseQuery)) ||
|
||||
(row.registeredAddress &&
|
||||
row.registeredAddress.toLowerCase().includes(lowerCaseQuery))
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
const filteredRows = rows.filter((row) => {
|
||||
if (!searchQuery.trim()) return true; // Return all rows if searchQuery is empty or whitespace
|
||||
const lowerCaseQuery = searchQuery.toLowerCase().trim();
|
||||
return (
|
||||
(row.name && row.name.toLowerCase().includes(lowerCaseQuery)) ||
|
||||
(row.registeredAddress &&
|
||||
row.registeredAddress.toLowerCase().includes(lowerCaseQuery))
|
||||
);
|
||||
});
|
||||
|
||||
const indexOfLastRow = currentPage * usersPerPage;
|
||||
const indexOfFirstRow = indexOfLastRow - usersPerPage;
|
||||
|
@ -247,13 +245,15 @@ const filteredRows = rows.filter((row) => {
|
|||
{(() => {
|
||||
switch (tableType) {
|
||||
case "admin":
|
||||
return "Admin";
|
||||
return "Admins";
|
||||
case "role":
|
||||
return "Roles";
|
||||
case "user":
|
||||
return "Users";
|
||||
case "manager":
|
||||
return "Managers";
|
||||
case "all-managers":
|
||||
return "Managers";
|
||||
case "vehicle":
|
||||
return "Vehicles";
|
||||
case "station":
|
||||
|
@ -263,7 +263,9 @@ const filteredRows = rows.filter((row) => {
|
|||
case "booking":
|
||||
return "Booking";
|
||||
case "slots":
|
||||
return "Slot";
|
||||
return "Slots";
|
||||
case "all-available-slots":
|
||||
return "Available Slots";
|
||||
default:
|
||||
return "List";
|
||||
}
|
||||
|
@ -325,8 +327,13 @@ const filteredRows = rows.filter((row) => {
|
|||
}}
|
||||
>
|
||||
{!(
|
||||
user?.userType === "user" &&
|
||||
(tableType === "slots" )) && (
|
||||
(user?.userType === "user" &&
|
||||
tableType === "all-available-slots") ||
|
||||
(user?.userType === "superadmin" &&
|
||||
tableType === "all-managers") ||
|
||||
(user?.userType === "superadmin" &&
|
||||
tableType === "user")
|
||||
) && (
|
||||
<Button
|
||||
sx={{
|
||||
backgroundColor: "#52ACDF",
|
||||
|
|
|
@ -208,7 +208,21 @@ const EditManagerModal: React.FC<EditManagerModalProps> = ({
|
|||
<Controller
|
||||
name="phone"
|
||||
control={control}
|
||||
rules={{ required: "Phone number is required" }}
|
||||
rules={{
|
||||
required: "Phone number is required",
|
||||
validate: (value) => {
|
||||
if (!/^[0-9]*$/.test(value)) {
|
||||
return "Only numbers are allowed";
|
||||
}
|
||||
if (value.length < 6) {
|
||||
return "Phone number must be at least 6 digits";
|
||||
}
|
||||
if (value.length > 14) {
|
||||
return "Phone number must be at most 14 digits";
|
||||
}
|
||||
return true; // No errors
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<CustomTextField
|
||||
{...field}
|
||||
|
@ -236,7 +250,7 @@ const EditManagerModal: React.FC<EditManagerModalProps> = ({
|
|||
width: "117px",
|
||||
"&:hover": { backgroundColor: "#439BC1" },
|
||||
}}
|
||||
disabled={loading}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<CircularProgress size={24} color="inherit" />
|
||||
|
|
|
@ -12,6 +12,7 @@ import { useDispatch } from "react-redux";
|
|||
import {
|
||||
updateSlot,
|
||||
fetchAvailableSlots,
|
||||
fetchManagersSlots,
|
||||
} from "../../redux/slices/slotSlice.ts"; // Update with correct action
|
||||
import { CustomIconButton, CustomTextField } from "../AddUserModal/styled.css"; // Custom styled components
|
||||
import { AppDispatch } from "../../redux/store/store.ts";
|
||||
|
@ -86,7 +87,7 @@ const EditSlotModal: React.FC<EditSlotModalProps> = ({
|
|||
isAvailable: availabilityStatus,
|
||||
})
|
||||
).unwrap();
|
||||
dispatch(fetchAvailableSlots());
|
||||
dispatch(fetchManagersSlots());
|
||||
handleClose(); // Close modal on success
|
||||
reset(); // Reset form fields after submit
|
||||
} catch (error) {
|
||||
|
@ -145,7 +146,6 @@ const EditSlotModal: React.FC<EditSlotModalProps> = ({
|
|||
|
||||
{/* Input Fields */}
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 2 }}>
|
||||
|
||||
{/* Start Time */}
|
||||
<Box sx={{ flex: "1 1 48%" }}>
|
||||
<Typography variant="body2" fontWeight={500} mb={0.5}>
|
||||
|
|
|
@ -2,10 +2,7 @@ import React, { useEffect } from "react";
|
|||
import { Box, Button, Typography, Modal } from "@mui/material";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import {
|
||||
CustomIconButton,
|
||||
CustomTextField,
|
||||
} from "../AddUserModal/styled.css";
|
||||
import { CustomIconButton, CustomTextField } from "../AddUserModal/styled.css";
|
||||
|
||||
interface EditUserModalProps {
|
||||
open: boolean;
|
||||
|
@ -14,7 +11,7 @@ interface EditUserModalProps {
|
|||
id: number,
|
||||
name: string,
|
||||
email: string,
|
||||
phone: string,
|
||||
phone: string
|
||||
) => void;
|
||||
editRow: any;
|
||||
}
|
||||
|
@ -23,7 +20,6 @@ interface FormData {
|
|||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
|
||||
}
|
||||
|
||||
const EditUserModal: React.FC<EditUserModalProps> = ({
|
||||
|
@ -59,12 +55,7 @@ const EditUserModal: React.FC<EditUserModalProps> = ({
|
|||
|
||||
const onSubmit = (data: FormData) => {
|
||||
if (editRow) {
|
||||
handleUpdate(
|
||||
editRow.id,
|
||||
data.name,
|
||||
data.email,
|
||||
data.phone,
|
||||
);
|
||||
handleUpdate(editRow.id, data.name, data.email, data.phone);
|
||||
}
|
||||
handleClose(); // Close the modal
|
||||
reset(); // Reset the form fields
|
||||
|
@ -125,7 +116,17 @@ const EditUserModal: React.FC<EditUserModalProps> = ({
|
|||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
rules={{ required: "Name is required" }}
|
||||
rules={{
|
||||
required: "User Name is required",
|
||||
minLength: {
|
||||
value: 3,
|
||||
message: "Minimum 3 characters required",
|
||||
},
|
||||
maxLength: {
|
||||
value: 30,
|
||||
message: "Maximum 30 characters allowed",
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<CustomTextField
|
||||
{...field}
|
||||
|
@ -147,7 +148,14 @@ const EditUserModal: React.FC<EditUserModalProps> = ({
|
|||
<Controller
|
||||
name="email"
|
||||
control={control}
|
||||
rules={{ required: "Email is required" }}
|
||||
rules={{
|
||||
required: "Email is required",
|
||||
pattern: {
|
||||
value: /\S+@\S+\.\S+/,
|
||||
message:
|
||||
"Please enter a valid email address.",
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<CustomTextField
|
||||
{...field}
|
||||
|
@ -170,7 +178,21 @@ const EditUserModal: React.FC<EditUserModalProps> = ({
|
|||
<Controller
|
||||
name="phone"
|
||||
control={control}
|
||||
rules={{ required: "Phone number is required" }}
|
||||
rules={{
|
||||
required: "Phone number is required",
|
||||
validate: (value) => {
|
||||
if (!/^[0-9]*$/.test(value)) {
|
||||
return "Only numbers are allowed";
|
||||
}
|
||||
if (value.length < 6) {
|
||||
return "Phone number must be at least 6 digits";
|
||||
}
|
||||
if (value.length > 14) {
|
||||
return "Phone number must be at most 14 digits";
|
||||
}
|
||||
return true; // No errors
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<CustomTextField
|
||||
{...field}
|
||||
|
@ -183,7 +205,6 @@ const EditUserModal: React.FC<EditUserModalProps> = ({
|
|||
)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
</Box>
|
||||
|
||||
{/* Submit Button */}
|
||||
|
|
|
@ -4,188 +4,194 @@ import Card from "@mui/material/Card";
|
|||
import CardContent from "@mui/material/CardContent";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { LineChart } from "@mui/x-charts/LineChart";
|
||||
import { FormControl, MenuItem, Select } from "@mui/material";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Button from "@mui/material/Button";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { AppDispatch, RootState } from "../../redux/store/store";
|
||||
import { fetchDashboardData } from "../../redux/slices/dashboardSlice";
|
||||
|
||||
function AreaGradient({ color, id }: { color: string; id: string }) {
|
||||
return (
|
||||
<defs>
|
||||
<linearGradient id={id} x1="50%" y1="0%" x2="50%" y2="100%">
|
||||
<stop offset="0%" stopColor={color} stopOpacity={0.5} />
|
||||
<stop offset="100%" stopColor={color} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
);
|
||||
}
|
||||
|
||||
function getDaysInMonth(month: number, year: number) {
|
||||
const date = new Date(year, month, 0);
|
||||
const monthName = date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
});
|
||||
const daysInMonth = date.getDate();
|
||||
const days = [];
|
||||
let i = 1;
|
||||
while (days.length < daysInMonth) {
|
||||
days.push(`${monthName} ${i}`);
|
||||
i += 1;
|
||||
}
|
||||
return days;
|
||||
return (
|
||||
<defs>
|
||||
<linearGradient id={id} x1="50%" y1="0%" x2="50%" y2="100%">
|
||||
<stop offset="0%" stopColor={color} stopOpacity={0.5} />
|
||||
<stop offset="100%" stopColor={color} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
);
|
||||
}
|
||||
|
||||
export default function LineChartCard() {
|
||||
const theme = useTheme();
|
||||
const isXsScreen = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
const isSmScreen = useMediaQuery(theme.breakpoints.between("sm", "md"));
|
||||
|
||||
const data = getDaysInMonth(4, 2024);
|
||||
const colorPalette = [theme.palette.primary.main];
|
||||
const [selectedOption, setSelectedOption] = React.useState("Weekly");
|
||||
const theme = useTheme();
|
||||
const isXsScreen = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
const isSmScreen = useMediaQuery(theme.breakpoints.between("sm", "md"));
|
||||
|
||||
const handleChange = (event: { target: { value: React.SetStateAction<string> } }) => {
|
||||
setSelectedOption(event.target.value);
|
||||
};
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const { totalBookings, loading } = useSelector(
|
||||
(state: RootState) => state.dashboardReducer
|
||||
);
|
||||
|
||||
// Calculate responsive dimensions
|
||||
const getChartHeight = () => {
|
||||
if (isXsScreen) return 200;
|
||||
if (isSmScreen) return 220;
|
||||
return 250;
|
||||
};
|
||||
// States for date range inputs
|
||||
const [startDateBookings, setStartDateBookings] = React.useState("");
|
||||
const [endDateBookings, setEndDateBookings] = React.useState("");
|
||||
|
||||
const getChartMargin = () => {
|
||||
if (isXsScreen) return { left: 35, right: 10, top: 15, bottom: 15 };
|
||||
if (isSmScreen) return { left: 40, right: 15, top: 18, bottom: 18 };
|
||||
return { left: 50, right: 20, top: 20, bottom: 20 };
|
||||
};
|
||||
// Function to fetch data with date filters
|
||||
const handleFetchData = () => {
|
||||
dispatch(fetchDashboardData({ startDateBookings, endDateBookings }));
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
variant="outlined"
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "auto",
|
||||
minHeight: { xs: "360px", sm: "400px", md: "444px" },
|
||||
borderRadius: "16px",
|
||||
border: "none",
|
||||
"*:where([data-mui-color-scheme='dark']) &": {
|
||||
backgroundColor: "#202020",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CardContent sx={{ padding: { xs: 2, md: 3 }, "&:last-child": { paddingBottom: { xs: 2, md: 3 } } }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexDirection: isXsScreen ? "column" : "row",
|
||||
color: "#F2F2F2",
|
||||
marginBottom: isXsScreen ? 16 : 0,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
align={isXsScreen ? "center" : "left"}
|
||||
color="#F2F2F2"
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
fontSize: { xs: "16px", sm: "17px", md: "18px" },
|
||||
lineHeight: "24px",
|
||||
marginBottom: isXsScreen ? 2 : 0,
|
||||
width: isXsScreen ? "100%" : "auto",
|
||||
}}
|
||||
>
|
||||
Sales Stats
|
||||
</Typography>
|
||||
<FormControl
|
||||
sx={{
|
||||
mt: isXsScreen ? 0 : 2,
|
||||
mb: isXsScreen ? 0 : 2,
|
||||
backgroundColor: "#202020",
|
||||
marginLeft: isXsScreen ? 0 : "auto",
|
||||
marginRight: isXsScreen ? 0 : "16px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
color: "#F2F2F2",
|
||||
width: { xs: "100%", sm: "140px", md: "149px" },
|
||||
height: { xs: "40px", md: "44px" },
|
||||
padding: { xs: "8px 12px", md: "12px 16px" },
|
||||
gap: "8px",
|
||||
}}
|
||||
>
|
||||
<Select
|
||||
value={selectedOption}
|
||||
onChange={handleChange}
|
||||
sx={{
|
||||
fontSize: { xs: "14px", md: "16px" },
|
||||
color: "#D9D8D8",
|
||||
".MuiSelect-icon": {
|
||||
color: "#F2F2F2",
|
||||
},
|
||||
}}
|
||||
IconComponent={ExpandMoreIcon}
|
||||
>
|
||||
<MenuItem value="Monthly">Monthly</MenuItem>
|
||||
<MenuItem value="Weekly">Weekly</MenuItem>
|
||||
<MenuItem value="Daily">Daily</MenuItem>
|
||||
<MenuItem value="Yearly">Yearly</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
// Generate chart data points (simulated here)
|
||||
const totalPoints = 10; // Divide total bookings into 10 intervals
|
||||
const chartData = Array.from({ length: totalPoints }, (_, i) => ({
|
||||
label: `Day ${i + 1}`,
|
||||
value: Math.round((totalBookings / totalPoints) * (i + 1)),
|
||||
}));
|
||||
|
||||
<LineChart
|
||||
colors={colorPalette}
|
||||
xAxis={[
|
||||
{
|
||||
scaleType: "point",
|
||||
data,
|
||||
tickInterval: (index, i) => isXsScreen
|
||||
? (i + 1) % 10 === 0
|
||||
: isSmScreen
|
||||
? (i + 1) % 7 === 0
|
||||
: (i + 1) % 5 === 0,
|
||||
},
|
||||
]}
|
||||
series={[
|
||||
{
|
||||
id: "direct",
|
||||
label: "Direct",
|
||||
showMark: false,
|
||||
curve: "linear",
|
||||
stack: "total",
|
||||
area: true,
|
||||
stackOrder: "ascending",
|
||||
data: [
|
||||
300, 900, 500, 1200, 1500, 1800, 2400, 2100,
|
||||
2700, 3000, 1800, 3300, 3600, 3900, 4200, 4500,
|
||||
3900, 4800, 5100, 5400, 4500, 5700, 6000, 6300,
|
||||
6600, 6900, 7200, 7500, 7800, 8100,
|
||||
],
|
||||
color: "#28ACFF",
|
||||
},
|
||||
]}
|
||||
height={getChartHeight()}
|
||||
margin={getChartMargin()}
|
||||
grid={{ horizontal: true }}
|
||||
sx={{
|
||||
"& .MuiAreaElement-series-direct": {
|
||||
fill: "url('#direct')",
|
||||
},
|
||||
}}
|
||||
slotProps={{
|
||||
legend: {
|
||||
hidden: true,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AreaGradient
|
||||
color={theme.palette.primary.main}
|
||||
id="direct"
|
||||
/>
|
||||
</LineChart>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
// Calculate responsive chart dimensions
|
||||
const getChartHeight = () => (isXsScreen ? 200 : isSmScreen ? 220 : 250);
|
||||
const getChartMargin = () =>
|
||||
isXsScreen
|
||||
? { left: 35, right: 10, top: 15, bottom: 15 }
|
||||
: isSmScreen
|
||||
? { left: 40, right: 15, top: 18, bottom: 18 }
|
||||
: { left: 50, right: 20, top: 20 };
|
||||
|
||||
return (
|
||||
<Card
|
||||
variant="outlined"
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "auto",
|
||||
minHeight: { xs: "360px", sm: "400px", md: "444px" },
|
||||
borderRadius: "16px",
|
||||
border: "none",
|
||||
"*:where([data-mui-color-scheme='dark']) &": {
|
||||
backgroundColor: "#202020",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CardContent
|
||||
sx={{
|
||||
padding: { xs: 2, md: 3 },
|
||||
"&:last-child": { paddingBottom: { xs: 2, md: 3 } },
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexDirection: isXsScreen ? "column" : "row",
|
||||
color: "#F2F2F2",
|
||||
marginBottom: isXsScreen ? 16 : 0,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
align={isXsScreen ? "center" : "left"}
|
||||
color="#F2F2F2"
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
fontSize: { xs: "16px", sm: "17px", md: "18px" },
|
||||
lineHeight: "24px",
|
||||
marginBottom: isXsScreen ? 2 : 0,
|
||||
width: isXsScreen ? "100%" : "auto",
|
||||
}}
|
||||
>
|
||||
Total Bookings Stats
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
{/* Input fields for start and end date */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: isXsScreen ? "column" : "row",
|
||||
gap: "16px",
|
||||
marginTop: "16px",
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
type="date"
|
||||
label="Start Date"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
InputLabelProps={{ shrink: true }}
|
||||
value={startDateBookings}
|
||||
onChange={(e) => setStartDateBookings(e.target.value)}
|
||||
sx={{
|
||||
backgroundColor: "#202020",
|
||||
borderRadius: "8px",
|
||||
"& .MuiInputBase-input": {
|
||||
color: "#F2F2F2",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
type="date"
|
||||
label="End Date"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
InputLabelProps={{ shrink: true }}
|
||||
value={endDateBookings}
|
||||
onChange={(e) => setEndDateBookings(e.target.value)}
|
||||
sx={{
|
||||
backgroundColor: "#202020",
|
||||
borderRadius: "8px",
|
||||
"& .MuiInputBase-input": {
|
||||
color: "#F2F2F2",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleFetchData}
|
||||
sx={{
|
||||
backgroundColor: "#52ACDF",
|
||||
color: "white",
|
||||
borderRadius: "8px",
|
||||
padding: "10px 20px",
|
||||
|
||||
"&:hover": { backgroundColor: "#3A94C0" },
|
||||
}}
|
||||
>
|
||||
Fetch
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Line Chart */}
|
||||
<LineChart
|
||||
colors={[theme.palette.primary.main]}
|
||||
xAxis={[
|
||||
{
|
||||
scaleType: "point",
|
||||
data: chartData.map((data) => data.label), // Use intervals as x-axis labels
|
||||
},
|
||||
]}
|
||||
series={[
|
||||
{
|
||||
id: "totalBookings",
|
||||
showMark: false,
|
||||
curve: "linear",
|
||||
area: true,
|
||||
data: chartData.map((data) => data.value), // Use interval data for y-axis
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
]}
|
||||
height={getChartHeight()}
|
||||
margin={getChartMargin()}
|
||||
grid={{ horizontal: true }}
|
||||
sx={{
|
||||
"& .MuiAreaElement-series-totalBookings": {
|
||||
fill: "url('#totalBookings')",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AreaGradient
|
||||
color={theme.palette.primary.main}
|
||||
id="totalBookings"
|
||||
/>
|
||||
</LineChart>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,45 +6,85 @@ import StatCard, { StatCardProps } from "../StatCard/statCard";
|
|||
import ResourcesPieChart from "../ResourcePieChart/resourcePieChart";
|
||||
import RoundedBarChart from "../barChartCard/barChartCard";
|
||||
import LineChartCard from "../LineChartCard/lineChartCard";
|
||||
|
||||
const data: StatCardProps[] = [
|
||||
{ title: "Total Charge Stations", value: "86" },
|
||||
{ title: "Charging Completed", value: "12" },
|
||||
{ title: "Active Users", value: "24" },
|
||||
{ title: "Total Energy Consumed", value: "08" },
|
||||
];
|
||||
import { AppDispatch, RootState } from "../../redux/store/store";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { fetchDashboardData } from "../../redux/slices/dashboardSlice";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function MainGrid() {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const {
|
||||
totalAdmins,
|
||||
totalManagers,
|
||||
totalUsers,
|
||||
totalStations,
|
||||
loading,
|
||||
error,
|
||||
} = useSelector((state: RootState) => state.dashboardReducer);
|
||||
|
||||
|
||||
|
||||
const staticData = {
|
||||
totalAdmins: 86,
|
||||
totalManagers: 12,
|
||||
totalUsers: 24,
|
||||
totalStations: 8,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
dispatch(fetchDashboardData());
|
||||
|
||||
}, [dispatch]);
|
||||
|
||||
const data =
|
||||
{ totalAdmins, totalManagers, totalUsers, totalStations }
|
||||
|
||||
|
||||
const statCards = [
|
||||
{ title: "Total Admins", value: data.totalAdmins },
|
||||
{ title: "Total Managers", value: data.totalManagers },
|
||||
{ title: "Total Users", value: data.totalUsers },
|
||||
{ title: "Total Stations", value: data.totalStations },
|
||||
];
|
||||
|
||||
return (
|
||||
<Box sx={{ width: "100%", maxWidth: "1600px", mx: "auto", px: { xs: 2, sm: 1, md: 0 } }}>
|
||||
{/* Dashboard Header */}
|
||||
<Typography component="h2" variant="h6" sx={{ mb: 2 }}>
|
||||
Dashboard
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
maxWidth: "1600px",
|
||||
mx: "auto",
|
||||
px: { xs: 2, sm: 1, md: 0 },
|
||||
}}
|
||||
>
|
||||
{/* Dashboard Header */}
|
||||
<Typography component="h2" variant="h6" sx={{ mb: 2 }}>
|
||||
Dashboard
|
||||
</Typography>
|
||||
|
||||
{/* Grid Layout */}
|
||||
<Grid container spacing={3} columns={12}>
|
||||
{/* Statistic Cards */}
|
||||
{data.map((card, index) => (
|
||||
<Grid key={index} item xs={12} sm={6} md={3} lg={3}>
|
||||
<StatCard {...card} />
|
||||
</Grid>
|
||||
))}
|
||||
{/* Grid Layout */}
|
||||
<Grid container spacing={3} columns={12}>
|
||||
{/* Statistic Cards */}
|
||||
{statCards.map((card, index) => (
|
||||
<Grid key={index} item xs={12} sm={6} md={3} lg={3}>
|
||||
<StatCard {...card} />
|
||||
</Grid>
|
||||
))}
|
||||
|
||||
{/* Charts */}
|
||||
<Grid item xs={12} sm={12} md={6} lg={6}>
|
||||
<SessionsChart />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={6}>
|
||||
<ResourcesPieChart />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={6}>
|
||||
<RoundedBarChart />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={6}>
|
||||
<LineChartCard />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
{/* Charts */}
|
||||
<Grid item xs={12} sm={12} md={6} lg={6}>
|
||||
<SessionsChart />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={6}>
|
||||
<ResourcesPieChart />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={6}>
|
||||
<RoundedBarChart />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6} lg={6}>
|
||||
<LineChartCard />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -36,9 +36,9 @@ export default function MenuContent({ hidden }: PropType) {
|
|||
url: "/panel/admin-list",
|
||||
},
|
||||
userRole === "superadmin" && {
|
||||
text: "Managers",
|
||||
text: "Manager",
|
||||
icon: <PeopleOutlinedIcon />,
|
||||
url: "/panel/manager-list",
|
||||
url: "/panel/all-managers-list",
|
||||
},
|
||||
userRole === "superadmin" && {
|
||||
text: "User",
|
||||
|
@ -89,7 +89,7 @@ export default function MenuContent({ hidden }: PropType) {
|
|||
userRole === "user" && {
|
||||
text: "Available Slots",
|
||||
icon: <ChecklistSharpIcon />,
|
||||
url: "/panel/slot-list", // Placeholder for now
|
||||
url: "/panel/all-available-slots", // Placeholder for now
|
||||
},
|
||||
userRole === "user" && {
|
||||
text: "Near By Stations",
|
||||
|
|
|
@ -6,6 +6,10 @@ import Box from "@mui/material/Box";
|
|||
import Stack from "@mui/material/Stack";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { AppDispatch, RootState } from "../../redux/store/store";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useEffect } from "react";
|
||||
import { fetchDashboardData } from "../../redux/slices/dashboardSlice";
|
||||
|
||||
const colorPalette = [
|
||||
"hsla(202, 69%, 60%, 1)",
|
||||
|
@ -14,19 +18,48 @@ const colorPalette = [
|
|||
"hsl(222, 6.80%, 50.80%)",
|
||||
];
|
||||
|
||||
const data = [
|
||||
{ title: "Total Resources", value: 50, color: colorPalette[0] },
|
||||
{ title: "Total Stations", value: 20, color: colorPalette[1] },
|
||||
{ title: "Station Manager", value: 15, color: colorPalette[2] },
|
||||
{ title: "Total Booth", value: 15, color: colorPalette[3] },
|
||||
];
|
||||
// const data = [
|
||||
// { title: "Total Resources", value: 50, color: colorPalette[0] },
|
||||
// { title: "Total Stations", value: 20, color: colorPalette[1] },
|
||||
// { title: "Station Manager", value: 15, color: colorPalette[2] },
|
||||
// { title: "Total Booth", value: 15, color: colorPalette[3] },
|
||||
// ];
|
||||
|
||||
export default function ResourcePieChart() {
|
||||
const theme = useTheme();
|
||||
const isXsScreen = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
const isSmScreen = useMediaQuery(theme.breakpoints.between("sm", "md"));
|
||||
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
|
||||
// // Fetch role and carPortCounts from Redux state
|
||||
// const {user} = useSelector((state: RootState) => state.profileReducer); // Assuming user role is stored in Redux
|
||||
const { carPortCounts } = useSelector(
|
||||
(state: RootState) => state.dashboardReducer
|
||||
);
|
||||
console.log("first",carPortCounts)
|
||||
// Static data for non-superadmin roles
|
||||
// const staticCarPorts = [
|
||||
// { carPort: "240V", count: 5 },
|
||||
// { carPort: "120V", count: 3 },
|
||||
// { carPort: "DCFC", count: 2 },
|
||||
// { carPort: "Other", count: 7 },
|
||||
// ];
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
dispatch(fetchDashboardData());
|
||||
|
||||
}, [dispatch]);
|
||||
|
||||
// console.log("Raw CarPortCounts from API:", carPortCounts);
|
||||
|
||||
const dataToDisplay =carPortCounts
|
||||
|
||||
// const dataToDisplay =
|
||||
// user?.userType === "superadmin"
|
||||
// ? carPortCounts.filter((entry) => entry.count > 0) // Exclude zero counts
|
||||
// : staticCarPorts.filter((entry) => entry.count > 0);
|
||||
// console.log("Filtered Data to Display:", dataToDisplay);
|
||||
const getChartDimensions = () => {
|
||||
if (isXsScreen) {
|
||||
return {
|
||||
|
@ -58,106 +91,121 @@ export default function ResourcePieChart() {
|
|||
const dimensions = getChartDimensions();
|
||||
|
||||
return (
|
||||
<Card
|
||||
variant="outlined"
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: { xs: "8px", sm: "10px", md: "12px" },
|
||||
flexGrow: 1,
|
||||
width: "100%",
|
||||
height: "auto",
|
||||
minHeight: { xs: "320px", sm: "340px", md: "360px" },
|
||||
padding: { xs: "12px", sm: "14px", md: "16px" },
|
||||
borderRadius: "16px",
|
||||
border: "none",
|
||||
"*:where([data-mui-color-scheme='dark']) &": {
|
||||
backgroundColor: "#202020",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CardContent sx={{ padding: 0, "&:last-child": { paddingBottom: 0 } }}>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="subtitle2"
|
||||
color="#F2F2F2"
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
fontSize: { xs: "16px", sm: "17px", md: "18px" },
|
||||
lineHeight: "24px",
|
||||
color: "#FFFFFF",
|
||||
marginBottom: { xs: 1, sm: 1.5, md: 2 },
|
||||
}}
|
||||
>
|
||||
Resources
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: { xs: "column", sm: "row" },
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
<PieChart
|
||||
colors={colorPalette}
|
||||
margin={dimensions.margin}
|
||||
series={[
|
||||
{
|
||||
data,
|
||||
innerRadius: dimensions.innerRadius,
|
||||
outerRadius: dimensions.outerRadius,
|
||||
paddingAngle: 2,
|
||||
cornerRadius: 8,
|
||||
highlightScope: {
|
||||
faded: "global",
|
||||
highlighted: "item",
|
||||
},
|
||||
},
|
||||
]}
|
||||
height={dimensions.height}
|
||||
width={dimensions.width}
|
||||
slotProps={{
|
||||
legend: { hidden: true },
|
||||
}}
|
||||
/>
|
||||
<Card
|
||||
variant="outlined"
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: { xs: "8px", sm: "10px", md: "12px" },
|
||||
flexGrow: 1,
|
||||
width: "100%",
|
||||
height: "auto",
|
||||
minHeight: { xs: "320px", sm: "340px", md: "360px" },
|
||||
padding: { xs: "12px", sm: "14px", md: "16px" },
|
||||
borderRadius: "16px",
|
||||
border: "none",
|
||||
"*:where([data-mui-color-scheme='dark']) &": {
|
||||
backgroundColor: "#202020",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CardContent
|
||||
sx={{ padding: 0, "&:last-child": { paddingBottom: 0 } }}
|
||||
>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="subtitle2"
|
||||
color="#F2F2F2"
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
fontSize: { xs: "16px", sm: "17px", md: "18px" },
|
||||
lineHeight: "24px",
|
||||
color: "#FFFFFF",
|
||||
marginBottom: { xs: 1, sm: 1.5, md: 2 },
|
||||
}}
|
||||
>
|
||||
Car Port Usage
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: { xs: "column", sm: "row" },
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<PieChart
|
||||
colors={colorPalette}
|
||||
margin={dimensions.margin}
|
||||
series={[
|
||||
{
|
||||
data: dataToDisplay.map((entry, index) => ({
|
||||
title: entry.carPort,
|
||||
value: entry.count,
|
||||
color: colorPalette[
|
||||
index % colorPalette.length
|
||||
],
|
||||
})),
|
||||
innerRadius: dimensions.innerRadius,
|
||||
outerRadius: dimensions.outerRadius,
|
||||
paddingAngle: 2,
|
||||
cornerRadius: 8,
|
||||
highlightScope: {
|
||||
faded: "global",
|
||||
highlighted: "item",
|
||||
},
|
||||
},
|
||||
]}
|
||||
height={dimensions.height}
|
||||
width={dimensions.width}
|
||||
slotProps={{
|
||||
legend: { hidden: true },
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack
|
||||
spacing={1}
|
||||
sx={{
|
||||
mt: { xs: 2, sm: 0 },
|
||||
ml: { xs: 0, sm: 2 }
|
||||
}}
|
||||
>
|
||||
{data.map((entry, index) => (
|
||||
<Stack
|
||||
key={index}
|
||||
direction="row"
|
||||
spacing={1}
|
||||
alignItems="center"
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: { xs: 12, sm: 14, md: 16 },
|
||||
height: { xs: 12, sm: 14, md: 16 },
|
||||
backgroundColor: entry.color,
|
||||
borderRadius: "50%",
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontSize: { xs: "12px", sm: "13px", md: "14px" },
|
||||
color: "#FFFFFF",
|
||||
}}
|
||||
>
|
||||
{entry.title}
|
||||
</Typography>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Stack
|
||||
spacing={1}
|
||||
sx={{
|
||||
mt: { xs: 2, sm: 0 },
|
||||
ml: { xs: 0, sm: 2 },
|
||||
}}
|
||||
>
|
||||
{dataToDisplay.map((entry, index) => (
|
||||
<Stack
|
||||
key={index}
|
||||
direction="row"
|
||||
spacing={1}
|
||||
alignItems="center"
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: { xs: 12, sm: 14, md: 16 },
|
||||
height: { xs: 12, sm: 14, md: 16 },
|
||||
backgroundColor:
|
||||
colorPalette[
|
||||
index % colorPalette.length
|
||||
],
|
||||
borderRadius: "50%",
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontSize: {
|
||||
xs: "12px",
|
||||
sm: "13px",
|
||||
md: "14px",
|
||||
},
|
||||
color: "#FFFFFF",
|
||||
}}
|
||||
>
|
||||
{entry.carPort}
|
||||
</Typography>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
|
@ -6,7 +6,7 @@ import useMediaQuery from "@mui/material/useMediaQuery";
|
|||
|
||||
export type StatCardProps = {
|
||||
title: string;
|
||||
value: string;
|
||||
value: number;
|
||||
};
|
||||
|
||||
export default function StatCard({ title, value }: StatCardProps) {
|
||||
|
|
|
@ -5,43 +5,47 @@ import {
|
|||
CardContent,
|
||||
Typography,
|
||||
Box,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
SelectChangeEvent,
|
||||
TextField,
|
||||
Button,
|
||||
} from "@mui/material";
|
||||
import { BarChart } from "@mui/x-charts/BarChart";
|
||||
import { axisClasses } from "@mui/x-charts/ChartsAxis";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
|
||||
// Sample Data
|
||||
const data = [
|
||||
{ name: "Jan", v1: 40 },
|
||||
{ name: "Feb", v1: 50 },
|
||||
{ name: "Mar", v1: 80 },
|
||||
{ name: "Apr", v1: 20 },
|
||||
{ name: "May", v1: 60 },
|
||||
{ name: "Jun", v1: 30 },
|
||||
];
|
||||
import { AppDispatch, RootState } from "../../redux/store/store";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { fetchDashboardData } from "../../redux/slices/dashboardSlice";
|
||||
|
||||
export default function RoundedBarChart() {
|
||||
const theme = useTheme();
|
||||
const isXsScreen = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
const isSmScreen = useMediaQuery(theme.breakpoints.between("sm", "md"));
|
||||
const [selectedOption, setSelectedOption] = React.useState("Monthly");
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
|
||||
const handleChange = (event: SelectChangeEvent<string>) => {
|
||||
setSelectedOption(event.target.value);
|
||||
// Fetch role and topStations from Redux state
|
||||
const { user } = useSelector((state: RootState) => state.profileReducer); // Assuming user role is stored in Redux
|
||||
const { topStations } = useSelector(
|
||||
(state: RootState) => state.dashboardReducer
|
||||
);
|
||||
|
||||
// State for filtering
|
||||
const [startDateStations, setStartDateStations] = React.useState("");
|
||||
const [endDateStations, setEndDateStations] = React.useState("");
|
||||
|
||||
// Handle fetching data with date filters
|
||||
const handleFetchData = () => {
|
||||
|
||||
dispatch(
|
||||
fetchDashboardData({ startDateStations, endDateStations })
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
// Responsive chart settings
|
||||
const getChartSettings = () => {
|
||||
const baseSettings = {
|
||||
|
||||
yAxis: [
|
||||
{
|
||||
label: isXsScreen ? "" : "Value",
|
||||
label: isXsScreen ? "" : "Count",
|
||||
tickFormatter: (value: number) => `${value}`,
|
||||
},
|
||||
],
|
||||
|
@ -49,6 +53,7 @@ export default function RoundedBarChart() {
|
|||
{
|
||||
dataKey: "name",
|
||||
scaleType: "band" as const,
|
||||
|
||||
},
|
||||
],
|
||||
sx: {
|
||||
|
@ -82,6 +87,12 @@ export default function RoundedBarChart() {
|
|||
|
||||
const chartSetting = getChartSettings();
|
||||
|
||||
// Data transformation for BarChart
|
||||
const chartData = topStations.map((station) => ({
|
||||
name: station?.ChargingStation?.name,
|
||||
count: parseInt(station.count, 10), // Ensure count is a number
|
||||
}));
|
||||
|
||||
return (
|
||||
<Card
|
||||
variant="outlined"
|
||||
|
@ -102,6 +113,7 @@ export default function RoundedBarChart() {
|
|||
"&:last-child": { paddingBottom: { xs: 2, md: 3 } },
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
|
@ -121,56 +133,80 @@ export default function RoundedBarChart() {
|
|||
width: isXsScreen ? "100%" : "auto",
|
||||
}}
|
||||
>
|
||||
Charge Stats
|
||||
Top Stations Stats
|
||||
</Typography>
|
||||
|
||||
<FormControl
|
||||
sx={{
|
||||
mt: isXsScreen ? 0 : 2,
|
||||
ml: isXsScreen ? 0 : "auto",
|
||||
backgroundColor: "#202020",
|
||||
color: "#F2F2F2",
|
||||
width: { xs: "100%", sm: "140px", md: "149px" },
|
||||
height: { xs: "40px", md: "44px" },
|
||||
padding: { xs: "8px 12px", md: "12px 16px" },
|
||||
gap: "8px",
|
||||
}}
|
||||
>
|
||||
<Select
|
||||
value={selectedOption}
|
||||
onChange={handleChange}
|
||||
sx={{
|
||||
fontSize: { xs: "14px", md: "16px" },
|
||||
color: "#D9D8D8",
|
||||
".MuiSelect-icon": {
|
||||
color: "#F2F2F2",
|
||||
},
|
||||
}}
|
||||
IconComponent={ExpandMoreIcon}
|
||||
>
|
||||
<MenuItem value="Monthly">Monthly</MenuItem>
|
||||
<MenuItem value="Weekly">Weekly</MenuItem>
|
||||
<MenuItem value="Daily">Daily</MenuItem>
|
||||
<MenuItem value="Yearly">Yearly</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
{/* Date Filters */}
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection={isXsScreen ? "column" : "row"}
|
||||
gap={2}
|
||||
mt={2}
|
||||
>
|
||||
<TextField
|
||||
type="date"
|
||||
label="Start Date"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
InputLabelProps={{ shrink: true }}
|
||||
value={startDateStations}
|
||||
onChange={(e) => setStartDateStations(e.target.value)}
|
||||
sx={{
|
||||
backgroundColor: "#202020",
|
||||
borderRadius: "8px",
|
||||
"& .MuiInputBase-input": {
|
||||
color: "#F2F2F2",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
type="date"
|
||||
label="End Date"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
InputLabelProps={{ shrink: true }}
|
||||
value={endDateStations}
|
||||
onChange={(e) => setEndDateStations(e.target.value)}
|
||||
sx={{
|
||||
backgroundColor: "#202020",
|
||||
borderRadius: "8px",
|
||||
"& .MuiInputBase-input": {
|
||||
color: "#F2F2F2",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
|
||||
onClick={handleFetchData}
|
||||
sx={{
|
||||
backgroundColor: "#52ACDF",
|
||||
color: "white",
|
||||
borderRadius: "8px",
|
||||
width: "117px",
|
||||
"&:hover": { backgroundColor: "#439BC1" },
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Chart */}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
mt: 2,
|
||||
mt: 4,
|
||||
}}
|
||||
>
|
||||
<BarChart
|
||||
borderRadius={0}
|
||||
dataset={data}
|
||||
dataset={chartData}
|
||||
series={[
|
||||
{
|
||||
dataKey: "v1",
|
||||
dataKey: "count",
|
||||
color: "#52ACDF",
|
||||
},
|
||||
]}
|
||||
|
|
|
@ -84,12 +84,13 @@ export default function AdminList() {
|
|||
{ id: "email", label: "Email" },
|
||||
{ id: "phone", label: "Phone" },
|
||||
{ id: "registeredAddress", label: "Address" },
|
||||
{ id: "userType", label: "Role" },
|
||||
// { id: "userType", label: "Role" },
|
||||
|
||||
{ id: "action", label: "Action", align: "center" },
|
||||
];
|
||||
|
||||
const categoryRows = admins?.map(
|
||||
const categoryRows = admins
|
||||
?.filter((admin) => admin?.userType === "admin").map(
|
||||
(
|
||||
admin: {
|
||||
id: string;
|
||||
|
|
54
src/pages/AllMangersList/index.tsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { useEffect } from "react";
|
||||
import CustomTable, { Column } from "../../components/CustomTable/customTable";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState, AppDispatch } from "../../redux/store/store";
|
||||
import { AllmanagerList,} from "../../redux/slices/managerSlice";
|
||||
|
||||
export default function ManagerList() {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const managers = useSelector(
|
||||
(state: RootState) => state.managerReducer.managers
|
||||
);
|
||||
|
||||
// Fetch manager list on component mount
|
||||
useEffect(() => {
|
||||
dispatch(AllmanagerList());
|
||||
}, [dispatch]);
|
||||
|
||||
// Columns for the manager table
|
||||
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" },
|
||||
{ id: "registeredAddress", label: "Station Location" },
|
||||
];
|
||||
|
||||
// Prepare rows for the table
|
||||
const categoryRows = managers?.length
|
||||
? managers.map((manager, index) => {
|
||||
const station = manager?.chargingStation;
|
||||
return {
|
||||
id: manager.id,
|
||||
srno: index + 1,
|
||||
name: manager.name,
|
||||
email: manager.email,
|
||||
phone: manager.phone ?? "NA",
|
||||
stationName: station?.name ?? "NA",
|
||||
registeredAddress: station?.registeredAddress ?? "NA",
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Display manager list */}
|
||||
<CustomTable
|
||||
columns={categoryColumns}
|
||||
rows={categoryRows}
|
||||
tableType="all-managers"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
67
src/pages/AvailableSlotsList/index.tsx
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import CustomTable, { Column } from "../../components/CustomTable/customTable";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState, AppDispatch } from "../../redux/store/store";
|
||||
|
||||
import {
|
||||
fetchAvailableSlots,
|
||||
|
||||
} from "../../redux/slices/slotSlice";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export default function EVSlotList() {
|
||||
|
||||
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const availableSlots = useSelector(
|
||||
(state: RootState) => state?.slotReducer.availableSlots
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchAvailableSlots());
|
||||
}, [dispatch]);
|
||||
|
||||
|
||||
|
||||
const slotColumns: Column[] = [
|
||||
{ id: "srno", label: "Sr No" },
|
||||
{ id: "name", label: "Station Name" },
|
||||
{ id: "stationId", label: "Station Id" },
|
||||
{ id: "date", label: "Date" },
|
||||
{ id: "startTime", label: "Start Time" },
|
||||
{ id: "endTime", label: "End Time" },
|
||||
{ id: "isAvailable", label: "Is Available", align: "center" },
|
||||
|
||||
];
|
||||
|
||||
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");
|
||||
|
||||
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 (
|
||||
<>
|
||||
<CustomTable
|
||||
columns={slotColumns}
|
||||
rows={slotRows}
|
||||
tableType="all-available-slots"
|
||||
|
||||
/>
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -5,7 +5,7 @@ import { RootState, AppDispatch } from "../../redux/store/store";
|
|||
import { useForm } from "react-hook-form";
|
||||
import {
|
||||
createSlot,
|
||||
fetchAvailableSlots,
|
||||
fetchManagersSlots,
|
||||
updateSlot,
|
||||
} from "../../redux/slices/slotSlice";
|
||||
import AddSlotModal from "../../components/AddSlotModal/addSlotModal";
|
||||
|
@ -21,10 +21,10 @@ export default function EVSlotList() {
|
|||
const dispatch = useDispatch<AppDispatch>();
|
||||
const availableSlots = useSelector(
|
||||
(state: RootState) => state?.slotReducer.availableSlots
|
||||
);
|
||||
);
|
||||
const { user } = useSelector((state: RootState) => state?.profileReducer);
|
||||
useEffect(() => {
|
||||
dispatch(fetchAvailableSlots());
|
||||
dispatch(fetchManagersSlots());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleClickOpen = () => {
|
||||
|
@ -46,7 +46,7 @@ export default function EVSlotList() {
|
|||
}) => {
|
||||
try {
|
||||
await dispatch(createSlot(data));
|
||||
await dispatch(fetchAvailableSlots());
|
||||
await dispatch(fetchManagersSlots());
|
||||
handleCloseModal();
|
||||
} catch (error) {
|
||||
console.error("Error adding slot", error);
|
||||
|
@ -74,16 +74,16 @@ export default function EVSlotList() {
|
|||
})
|
||||
).unwrap();
|
||||
|
||||
await dispatch(fetchAvailableSlots());
|
||||
await dispatch(fetchManagersSlots());
|
||||
handleCloseModal();
|
||||
} catch (error) {
|
||||
console.error("Update failed", error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const slotColumns: Column[] = [
|
||||
{ id: "srno", label: "Sr No" },
|
||||
{ id: "name", label: "Station Name" },
|
||||
{ id: "stationName", label: "Station Name" },
|
||||
{ id: "stationId", label: "Station Id" },
|
||||
{ id: "date", label: "Date" },
|
||||
{ id: "startTime", label: "Start Time" },
|
||||
|
@ -104,7 +104,7 @@ export default function EVSlotList() {
|
|||
srno: index + 1,
|
||||
id: slot?.id ?? "NA",
|
||||
stationId: slot?.stationId ?? "NA",
|
||||
name: slot?.ChargingStation?.name ?? "NA",
|
||||
stationName: slot?.stationName?? "NA",
|
||||
date: formattedDate ?? "NA",
|
||||
startTime: startTime ?? "NA",
|
||||
endTime: endTime ?? "NA",
|
||||
|
|
|
@ -20,7 +20,7 @@ export default function UserList() {
|
|||
const dispatch = useDispatch<AppDispatch>();
|
||||
|
||||
const users = useSelector((state: RootState) => state.userReducer.users);
|
||||
|
||||
const { user } = useSelector((state: RootState) => state?.profileReducer);
|
||||
useEffect(() => {
|
||||
dispatch(userList());
|
||||
}, [dispatch]);
|
||||
|
@ -78,7 +78,9 @@ export default function UserList() {
|
|||
{ id: "email", label: "Email" },
|
||||
{ id: "phone", label: "Phone" },
|
||||
|
||||
{ id: "action", label: "Action", align: "center" },
|
||||
...(user?.userType === "admin"
|
||||
? [{ id: "action", label: "Action", align: "center" as const }]
|
||||
: []),
|
||||
];
|
||||
const categoryRows = users?.length
|
||||
? users.map((user, index) => ({
|
||||
|
|
|
@ -10,7 +10,7 @@ import managerReducer from "../redux/slices/managerSlice.ts";
|
|||
import stationReducer from "../redux/slices/stationSlice.ts";
|
||||
import slotReducer from "../redux/slices/slotSlice.ts";
|
||||
import bookReducer from "../redux/slices/bookSlice.ts";
|
||||
|
||||
import dashboardReducer from "../redux/slices/dashboardSlice.ts";
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
authReducer,
|
||||
|
@ -23,7 +23,8 @@ const rootReducer = combineReducers({
|
|||
stationReducer,
|
||||
slotReducer,
|
||||
bookReducer,
|
||||
// Add other reducers here...
|
||||
dashboardReducer,
|
||||
|
||||
});
|
||||
|
||||
export type RootState = ReturnType<typeof rootReducer>;
|
||||
|
|
|
@ -13,9 +13,10 @@ interface User {
|
|||
}
|
||||
|
||||
interface Admin {
|
||||
Admins: any;
|
||||
id: string;
|
||||
name: string;
|
||||
role: string;
|
||||
userType: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
registeredAddress: string;
|
||||
|
|
|
@ -19,6 +19,8 @@ interface Booking {
|
|||
startTime: string;
|
||||
endTime: string;
|
||||
carDetails: CarDetails;
|
||||
carNames: string[]; // For car names
|
||||
carPorts: string[]; // For car ports
|
||||
}
|
||||
|
||||
interface BookingState {
|
||||
|
|
94
src/redux/slices/dashboardSlice.ts
Normal file
|
@ -0,0 +1,94 @@
|
|||
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
|
||||
import http from "../../lib/https";
|
||||
import { toast } from "sonner";
|
||||
|
||||
// Define interfaces for the dashboard data structure
|
||||
interface CarPortCount {
|
||||
carPort: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface TopStation {
|
||||
stationId: number;
|
||||
count: string;
|
||||
ChargingStation: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DashboardState {
|
||||
totalAdmins: number;
|
||||
totalManagers: number;
|
||||
totalUsers: number;
|
||||
totalStations: number;
|
||||
carPortCounts: CarPortCount[];
|
||||
topStations: TopStation[];
|
||||
totalBookings: number;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const initialState: DashboardState = {
|
||||
totalAdmins: 0,
|
||||
totalManagers: 0,
|
||||
totalUsers: 0,
|
||||
totalStations: 0,
|
||||
carPortCounts: [],
|
||||
topStations: [],
|
||||
totalBookings: 0,
|
||||
loading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
// Async thunk for fetching dashboard data
|
||||
export const fetchDashboardData = createAsyncThunk<
|
||||
DashboardState,
|
||||
{ startDateBookings?: string; endDateBookings?: string; startDateStations?: string; endDateStations?: string}, // Accept startDate and endDate as optional parameters
|
||||
{ rejectValue: string }
|
||||
>("dashboard/fetchDashboardData", async (params, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await http.get("/dashboard", { params }); // Pass startDate and endDate as query params
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
toast.error("Error Fetching Dashboard Data: " + error.message);
|
||||
return rejectWithValue(
|
||||
error?.response?.data?.message || "An error occurred"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Redux slice for the dashboard
|
||||
const dashboardSlice = createSlice({
|
||||
name: "dashboard",
|
||||
initialState,
|
||||
reducers: {},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(fetchDashboardData.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(
|
||||
fetchDashboardData.fulfilled,
|
||||
(state, action: PayloadAction<DashboardState>) => {
|
||||
state.loading = false;
|
||||
state.totalAdmins = action.payload.totalAdmins;
|
||||
state.totalManagers = action.payload.totalManagers;
|
||||
state.totalUsers = action.payload.totalUsers;
|
||||
state.totalStations = action.payload.totalStations;
|
||||
state.carPortCounts = action.payload.carPortCounts;
|
||||
state.topStations = action.payload.topStations;
|
||||
state.totalBookings = action.payload.totalBookings;
|
||||
}
|
||||
|
||||
)
|
||||
.addCase(fetchDashboardData.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error =
|
||||
action.payload || "Failed to fetch dashboard data";
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default dashboardSlice.reducer;
|
|
@ -46,7 +46,25 @@ export const managerList = createAsyncThunk<
|
|||
);
|
||||
}
|
||||
});
|
||||
export const AllmanagerList = createAsyncThunk<
|
||||
Manager[],
|
||||
void,
|
||||
{ rejectValue: string }
|
||||
>("fetchAllManagers", async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const token = localStorage?.getItem("authToken");
|
||||
if (!token) throw new Error("No token found");
|
||||
|
||||
const response = await http.get("/all-manager-list");
|
||||
if (!response.data?.data) throw new Error("Invalid API response");
|
||||
return response.data.data;
|
||||
} catch (error: any) {
|
||||
toast.error("Error Fetching Managers: " + error.message);
|
||||
return rejectWithValue(
|
||||
error?.response?.data?.message || "An error occurred"
|
||||
);
|
||||
}
|
||||
});
|
||||
// Create Manager (Async Thunk)
|
||||
export const addManager = createAsyncThunk<
|
||||
Manager,
|
||||
|
@ -132,6 +150,21 @@ const managerSlice = createSlice({
|
|||
state.loading = false;
|
||||
state.error = action.payload || "Failed to fetch managers";
|
||||
})
|
||||
.addCase(AllmanagerList.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(
|
||||
AllmanagerList.fulfilled,
|
||||
(state, action: PayloadAction<Manager[]>) => {
|
||||
state.loading = false;
|
||||
state.managers = action.payload;
|
||||
}
|
||||
)
|
||||
.addCase(AllmanagerList.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload || "Failed to fetch managers";
|
||||
})
|
||||
|
||||
// Add Manager
|
||||
.addCase(addManager.pending, (state) => {
|
||||
|
|
|
@ -10,7 +10,7 @@ interface Slot {
|
|||
startTime: string;
|
||||
endTime: string;
|
||||
isAvailable: boolean;
|
||||
|
||||
stationName:string;
|
||||
ChargingStation: { name: string };
|
||||
}
|
||||
|
||||
|
@ -50,7 +50,27 @@ export const fetchAvailableSlots = createAsyncThunk<
|
|||
);
|
||||
}
|
||||
});
|
||||
export const fetchManagersSlots = createAsyncThunk<
|
||||
Slot[],
|
||||
void,
|
||||
{ rejectValue: string }
|
||||
>("fetchManagersSlots", async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const token = localStorage?.getItem("authToken");
|
||||
if (!token) throw new Error("No token found");
|
||||
|
||||
const response = await http.get("/manager-slots");
|
||||
|
||||
if (!response.data?.data) throw new Error("Invalid API response");
|
||||
|
||||
return response.data.data;
|
||||
} catch (error: any) {
|
||||
toast.error("Error Fetching Profile" + error);
|
||||
return rejectWithValue(
|
||||
error?.response?.data?.message || "An error occurred"
|
||||
);
|
||||
}
|
||||
});
|
||||
export const createSlot = createAsyncThunk<
|
||||
Slot,
|
||||
{
|
||||
|
@ -152,6 +172,22 @@ const slotSlice = createSlice({
|
|||
state.error =
|
||||
action.payload || "Failed to fetch available slots";
|
||||
})
|
||||
.addCase(fetchManagersSlots.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(
|
||||
fetchManagersSlots.fulfilled,
|
||||
(state, action: PayloadAction<Slot[]>) => {
|
||||
state.loading = false;
|
||||
state.availableSlots = action.payload;
|
||||
}
|
||||
)
|
||||
.addCase(fetchManagersSlots.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error =
|
||||
action.payload || "Failed to fetch available slots";
|
||||
})
|
||||
.addCase(createSlot.pending, (state) => {
|
||||
state.loading = true;
|
||||
})
|
||||
|
|
|
@ -24,7 +24,8 @@ const EVSlotManagement = lazy(() => import("./pages/EVSlotManagement"));
|
|||
const BookingList = lazy(() => import("./pages/BookingList"));
|
||||
const EvSlotList = lazy(() => import("./pages/EvSlotList"));
|
||||
const ExternalStationList = lazy(() => import("./pages/ExternalStationList/externalStationList.tsx"));
|
||||
|
||||
const AllManagersList = lazy(() => import("./pages/AllMangersList"));
|
||||
const AvailableSlotsList = lazy(() => import("./pages/AvailableSlotsList"));
|
||||
interface ProtectedRouteProps {
|
||||
// caps: string[];
|
||||
component: React.ReactNode;
|
||||
|
@ -44,7 +45,7 @@ export default function AppRouter() {
|
|||
<Suspense fallback={<LoadingComponent />}>
|
||||
<BaseRoutes>
|
||||
{/* Default Route */}
|
||||
<Route path="" element={<LandingPage /> } />
|
||||
<Route path="" element={<LandingPage />} />
|
||||
|
||||
{/* Auth Routes */}
|
||||
<Route path="">
|
||||
|
@ -117,6 +118,18 @@ export default function AppRouter() {
|
|||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="all-managers-list"
|
||||
element={
|
||||
<ProtectedRoute component={<AllManagersList />} />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="all-available-slots"
|
||||
element={
|
||||
<ProtectedRoute component={<AvailableSlotsList />} />
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
{/* Catch-all Route */}
|
||||
|
|