Merge pull request 'frontend/apiIntegration' (#16) from frontend/apiIntegration into develop

Reviewed-on: DigiMantra/digiev_frontend#16
This commit is contained in:
Mohit kalshan 2025-03-05 12:21:46 +00:00
commit 5eafb62f95
13 changed files with 531 additions and 170 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View file

@ -8,15 +8,28 @@ import SearchIcon from "@mui/icons-material/Search";
import Divider from "@mui/material/Divider";
import MenuButton from "../MenuButton";
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
import ColorModeIconDropdown from "../../shared-theme/ColorModeIconDropdown";
import NotificationsRoundedIcon from "@mui/icons-material/NotificationsRounded";
import SideMenu from "../SideMenu";
import { useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "../../redux/store/store";
import { fetchAdminProfile } from "../../redux/slices/profileSlice";
import OptionsMenu from "../OptionsMenu";
export default function Header() {
const [showNotifications, setShowNotifications] = React.useState(false);
const toggleNotifications = () => {
setShowNotifications((prev) => !prev);
};
const [open, setOpen] = React.useState(true);
const dispatch = useDispatch<AppDispatch>();
const { user } = useSelector(
(state: RootState) => state?.profileReducer
);
React.useEffect(() => {
dispatch(fetchAdminProfile());
}, [dispatch]);
return (
<Box
sx={{
@ -92,11 +105,11 @@ export default function Header() {
sx={{ width: 36, height: 36 }}
/>
<Typography variant="body1" sx={{ color: "#202020" }}>
Momah
{user?.name || "No Admin"}
</Typography>
{/* Dropdown Icon */}
<ArrowDropDownIcon
sx={{ color: "#202020", width: 16, height: 16 }}
<OptionsMenu
/>
</Stack>
{/* <ColorModeIconDropdown /> */}

View file

@ -47,6 +47,11 @@ export default function MenuContent({ hidden }: PropType) {
icon: <AnalyticsRoundedIcon />,
url: "/panel/role-list",
},
userRole === "admin" && {
text: "Vehicles",
icon: <AnalyticsRoundedIcon />,
url: "/panel/vehicle-list",
},
];
const filteredMenuItems = baseMenuItems.filter(Boolean);

View file

@ -13,6 +13,7 @@ import MenuButton from "../MenuButton";
import { Avatar } from "@mui/material";
import { useNavigate } from "react-router-dom";
import Logout from "../LogOutFunction/LogOutFunction";
import { ArrowDropDownIcon } from "@mui/x-date-pickers";
const MenuItem = styled(MuiMenuItem)({
margin: "2px 0",
@ -46,7 +47,7 @@ export default function OptionsMenu({ avatar }: { avatar?: boolean }) {
onClick={handleClick}
sx={{ borderColor: "transparent" }}
>
{avatar ? (
{/* {avatar ? (
<MoreVertRoundedIcon />
) : (
<Avatar
@ -55,7 +56,8 @@ export default function OptionsMenu({ avatar }: { avatar?: boolean }) {
src="/static/images/avatar/7.jpg"
sx={{ width: 36, height: 36 }}
/>
)}
)} */}
<ArrowDropDownIcon />
</MenuButton>
<Menu
anchorEl={anchorEl}
@ -78,11 +80,11 @@ export default function OptionsMenu({ avatar }: { avatar?: boolean }) {
}}
>
<MenuItem onClick={handleProfile}>Profile</MenuItem>
<MenuItem onClick={handleClose}>My account</MenuItem>
{/* <MenuItem onClick={handleClose}>My account</MenuItem>
<Divider />
<MenuItem onClick={handleClose}>Add another account</MenuItem>
<MenuItem onClick={handleClose}>Settings</MenuItem>
<Divider />
<MenuItem onClick={handleClose}>Settings</MenuItem> */}
{/* <Divider /> */}
<MenuItem
onClick={handleClose}
sx={{
@ -92,15 +94,12 @@ export default function OptionsMenu({ avatar }: { avatar?: boolean }) {
},
}}
>
{/* //Eknoor singh and jaanvi
//date:- 13-Feb-2025
//Implemented logout functionality which was static previously */}
<ListItemText
onClick={(e) => {
e.stopPropagation();
setLogoutModal(true);
}}
sx={{color:"red"}}
>
Logout
</ListItemText>
@ -109,9 +108,9 @@ export default function OptionsMenu({ avatar }: { avatar?: boolean }) {
logoutModal={logoutModal}
/>
<ListItemIcon>
{/* <ListItemIcon>
<LogoutRoundedIcon fontSize="small" />
</ListItemIcon>
</ListItemIcon> */}
</MenuItem>
</Menu>
</React.Fragment>

View file

@ -29,10 +29,6 @@ const Drawer = styled(MuiDrawer)({
export default function SideMenu() {
const [open, setOpen] = React.useState(true);
//Eknoor singh
//date:- 12-Feb-2025
//Dispatch is called with user from Authstate Interface
const dispatch = useDispatch<AppDispatch>();
const { user } = useSelector((state: RootState) => state?.profileReducer);
@ -74,7 +70,7 @@ export default function SideMenu() {
</Box>
<MenuContent hidden={open} />
{/* <CardAlert /> */}
<Stack
{/* <Stack
direction="row"
sx={{
p: 2,
@ -104,8 +100,8 @@ export default function SideMenu() {
{user?.email || "No Email"}
</Typography>
</Box>
<OptionsMenu avatar={open} />
</Stack>
<OptionsMenu avatar={open} />
</Stack> */}
</Drawer>
);
}

View file

@ -33,8 +33,8 @@ export default function Login(props: { disableCustomTheme?: boolean }) {
const {
control,
handleSubmit,
formState: { errors },
} = useForm<ILoginForm>();
formState: { errors, isValid },
} = useForm<ILoginForm>({ mode: "onChange" });
const dispatch = useDispatch();
const router = useNavigate();
@ -228,6 +228,11 @@ export default function Login(props: { disableCustomTheme?: boolean }) {
message:
"Password must be at least 6 characters long.",
},
pattern: {
value: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{6,}$/,
message:
"Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character.",
},
}}
render={({ field }) => (
<Box sx={{ position: "relative" }}>
@ -255,17 +260,22 @@ export default function Login(props: { disableCustomTheme?: boolean }) {
? "error"
: "primary"
}
sx={{
paddingRight: "40px",
height: "40px",
marginBottom: "8px",
}}
/>
<IconButton
sx={{
position: "absolute",
top: "50%",
right: "10px",
transform:
"translateY(-50%)",
background: "none",
borderColor:
"transparent",
transform:
"translateY(-50%)",
"&:hover": {
backgroundColor:
"transparent",
@ -295,6 +305,8 @@ export default function Login(props: { disableCustomTheme?: boolean }) {
display: "flex",
justifyContent: "space-between",
color: "white",
alignItems: "center",
flexWrap: "wrap",
}}
>
<FormControlLabel
@ -326,6 +338,7 @@ export default function Login(props: { disableCustomTheme?: boolean }) {
<Button
type="submit"
fullWidth
disabled={!isValid}
sx={{
color: "white",
backgroundColor: "#52ACDF",

View file

@ -1,7 +1,3 @@
//Eknoor singh
//date:- 12-Feb-2025
//Made a special page for showing the profile details
import { useEffect } from "react";
import {
Container,
@ -12,16 +8,15 @@ import {
Grid,
Avatar,
Box,
Stack,
Divider,
Link,
} from "@mui/material";
import { useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "../../redux/store/store";
import { fetchAdminProfile } from "../../redux/slices/profileSlice";
const ProfilePage = () => {
//Eknoor singh
//date:- 12-Feb-2025
//Dispatch is called and user, isLoading, and error from Authstate Interface
const dispatch = useDispatch<AppDispatch>();
const { user, isLoading } = useSelector(
(state: RootState) => state?.profileReducer
@ -49,34 +44,110 @@ const ProfilePage = () => {
return (
<Container sx={{ py: 4 }}>
<Typography variant="h4" gutterBottom>
Profile
Account Info
</Typography>
<Card sx={{ maxWidth: 600, margin: "0 auto" }}>
<Card sx={{ maxWidth: "100%", margin: "0 auto" }}>
<CardContent>
<Grid container spacing={2} alignItems="center">
<Grid item>
<Stack direction="column" spacing={2}>
<Stack
direction="row"
spacing={1.5}
alignItems="center"
>
<Avatar
alt={user?.name || "User Avatar"}
src={"/static/images/avatar/7.jpg"}
sx={{ width: 80, height: 80 }}
alt="User Avatar"
src="/avatar.png"
sx={{ width: 36, height: 36 }}
/>
<Box>
<Typography
variant="body1"
sx={{ color: "#202020" }}
>
{user?.name || "No Admin"}
</Typography>
<Typography
variant="body2"
color="text.secondary"
>
{user?.userType || "N/A"}
</Typography>
</Box>
</Stack>
<Divider
flexItem
sx={{ backgroundColor: "rgba(32, 32, 32, 0.5)" }}
/>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Typography
variant="body1"
sx={{ color: "#202020" }}
>
Personal Information
</Typography>
<Link href="/edit-profile" underline="hover">
Edit
</Link>
</Stack>
<Grid container >
<Grid item xs={12} sm={4}>
<Typography
variant="body1"
sx={{ color: "#202020" }}
>
Name:
</Typography>
<Typography
variant="body2"
color="text.secondary"
>
{user?.name || "N/A"}
</Typography>
</Grid>
<Grid item xs={12} sm={4}>
<Typography
variant="body1"
sx={{ color: "#202020" }}
>
Phone:
</Typography>
<Typography
variant="body2"
color="text.secondary"
>
{user?.phone || "N/A"}
</Typography>
</Grid>
<Grid item xs={12} sm={4}>
<Typography
variant="body1"
sx={{ color: "#202020" }}
>
Email:
</Typography>
<Typography
variant="body2"
color="text.secondary"
>
{user?.email || "N/A"}
</Typography>
</Grid>
</Grid>
<Grid item xs>
<Typography variant="h6">
{user?.name || "N/A"}
</Typography>
<Typography variant="body2" color="text.secondary">
Email: {user?.email || "N/A"}
</Typography>
<Typography variant="body2" color="text.secondary">
Phone: {user?.phone || "N/A"}
</Typography>
<Typography variant="body2" color="text.secondary">
Role: <b>{user?.userType || "N/A"}</b>
</Typography>
</Grid>
</Grid>
<Typography variant="body1" sx={{ color: "#202020" }}>
Bio:
</Typography>
<Typography variant="body2" color="text.secondary">
{user?.bio || "No bio available."}
</Typography>
</Stack>
</CardContent>
</Card>
</Container>

View file

@ -0,0 +1,227 @@
import React, { useEffect, useState } from "react";
import { Box, Button, TextField, Typography } from "@mui/material";
import AddEditCategoryModal from "../../components/AddEditCategoryModal";
import { useForm } from "react-hook-form";
import CustomTable, { Column } from "../../components/CustomTable";
import DeleteModal from "../../components/Modals/DeleteModal";
import { RootState } from "../../redux/reducers";
import { useDispatch, useSelector } from "react-redux";
import { AppDispatch } from "../../redux/store/store";
import {
addVehicle,
updateVehicle,
vehicleList,
} from "../../redux/slices/VehicleSlice";
import SearchIcon from "@mui/icons-material/Search";
const categoryRows = [
{
srno: 1,
id: 1, // Using a number for 'id'
name: "Tesla Model S",
brand: "Tesla",
imageUrl:
"https://example.com/https://imgd-ct.aeplcdn.com/1056x660/n/cw/ec/93821/model-s-exterior-front-view.jpeg?q=80.jpg",
},
{
srno: 2,
id: 2,
name: "BMW X5",
brand: "BMW",
imageUrl: "https://example.com/bmw_x5.jpg",
},
{
srno: 3,
id: 3,
name: "Audi A6",
brand: "Audi",
imageUrl: "https://example.com/audi_a6.jpg",
},
{
srno: 4,
id: 4,
name: "Mercedes-Benz S-Class",
brand: "Mercedes-Benz",
imageUrl: "https://example.com/mercedes_s_class.jpg",
},
{
srno: 5,
id: 5,
name: "Ford Mustang",
brand: "Ford",
imageUrl: "https://example.com/ford_mustang.jpg",
},
];
export default function VehicleList() {
const [modalOpen, setModalOpen] = useState<boolean>(false);
const [editRow, setEditRow] = useState<any>(null);
const { reset } = useForm();
const [deleteModal, setDeleteModal] = React.useState<boolean>(false);
const [viewModal, setViewModal] = React.useState<boolean>(false);
const [rowData, setRowData] = React.useState<any | null>(null);
const [searchTerm, setSearchTerm] = useState("");
const dispatch = useDispatch<AppDispatch>();
const vehicles = useSelector(
(state: RootState) => state.vehicleReducer.vehicles
);
useEffect(() => {
dispatch(vehicleList());
}, [dispatch]);
const handleClickOpen = () => {
setRowData(null); // Reset row data when opening for new admin
setModalOpen(true);
};
const handleCloseModal = () => {
setModalOpen(false);
setRowData(null);
reset();
};
const handleCreate = async (data: {
name: string;
brand: string;
imageUrl: string;
}) => {
try {
await dispatch(addVehicle(data));
await dispatch(vehicleList());
handleCloseModal();
} catch (error) {
console.error("Creation failed", error);
}
};
const handleUpdate = async (
id: number,
name: string,
brand: string,
imageUrl: string
) => {
try {
await dispatch(
updateVehicle({
id,
name,
brand,
imageUrl,
})
);
await dispatch(vehicleList());
} catch (error) {
console.error("Update failed", error);
}
};
const categoryColumns: Column[] = [
{ id: "srno", label: "Sr No" },
{ id: "name", label: "Vehicle Name" },
{ id: "brand", label: "Brand" },
{ id: "imageUrl", label: "Image" },
{ id: "action", label: "Action", align: "center" },
];
const filteredVehicles = vehicles?.filter(
(vehicle) =>
vehicle.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
vehicle.brand.toLowerCase().includes(searchTerm.toLowerCase())
);
// const categoryRows = filteredVehicles?.length
// ? filteredVehicles?.map(
// (
// vehicle: {
// id: number;
// name: string;
// brand: string;
// imageUrl: string;
// },
// index: number
// ) => ({
// id: vehicle?.id,
// srno: index + 1,
// name: vehicle?.name,
// brand: vehicle?.brand,
// imageUrl: vehicle?.imageUrl,
// })
// )
// : [];
return (
<>
{/* Title and Add Category button */}
<Typography
component="h2"
variant="h6"
sx={{ mt: 2, fontWeight: 600 }}
>
Vehicles
</Typography>
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: { xs: "column", sm: "row" },
justifyContent: "space-between",
alignItems: "center",
mb: 2, // Add margin bottom for spacing
}}
>
<TextField
variant="outlined"
size="small"
placeholder="Search..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
sx={{
width: { xs: "100%", sm: "30%" },
marginBottom: { xs: 2, sm: 0 },
}}
InputProps={{
startAdornment: (
<SearchIcon
sx={{ color: "#202020", marginRight: 1 }}
/>
),
}}
/>
<Button
variant="contained"
size="medium"
sx={{
textAlign: "center",
width: { xs: "100%", sm: "auto" },
}}
onClick={handleClickOpen}
>
Add Vehicle
</Button>
</Box>
<CustomTable
columns={categoryColumns}
rows={categoryRows}
setDeleteModal={setDeleteModal}
deleteModal={deleteModal}
setViewModal={setViewModal}
viewModal={viewModal}
setRowData={setRowData}
setModalOpen={setModalOpen}
/>
{/* <AddEditCategoryModal
open={modalOpen}
handleClose={handleCloseModal}
editRow={rowData}
/>
<DeleteModal
open={deleteModal}
setDeleteModal={setDeleteModal}
handleDelete={handleDelete}
/> */}
</>
);
}

View file

@ -1,102 +0,0 @@
import React, { useState } from 'react';
import { Box, Button, Typography } from '@mui/material';
import AddEditCategoryModal from '../../components/AddEditCategoryModal';
import { useForm } from 'react-hook-form';
import CustomTable from '../../components/CustomTable';
import DeleteModal from '../../components/Modals/DeleteModal';
// Sample data for categories
// const categoryRows = [
// { srno: 1, name: 'Strength', date: '01/03/2025' },
// {
// srno: 2,
// name: 'HIIT (High-Intensity Interval Training)',
// date: '01/03/2025',
// },
// { srno: 3, name: 'Cardio', date: '01/03/2025' },
// { srno: 4, name: 'Combat', date: '01/03/2025' },
// { srno: 5, name: 'Yoga', date: '01/03/2025' },
// ];
export default function Vehicles() {
const [modalOpen, setModalOpen] = useState<boolean>(false);
const [editRow, setEditRow] = useState<any>(null);
const { reset } = useForm();
const [deleteModal, setDeleteModal] = React.useState<boolean>(false);
const [rowData, setRowData] = React.useState<any | null>(null);
const handleClickOpen = () => {
setModalOpen(true);
setEditRow(null);
};
const handleCloseModal = () => {
setModalOpen(false);
reset();
};
// const handleEdit = () => {
// setEditRow(rowData);
// };
const handleDelete = () => {
console.log('Deleted row:', rowData);
setDeleteModal(false);
};
const categoryColumns = [
{ id: 'srno', label: 'Sr No' },
{ id: 'name', label: 'Category Name' },
{ id: 'date', label: 'Date' },
{ id: 'action', label: 'Action', align: 'center' },
];
return (
<>
<Box
sx={{
width: '100%',
maxWidth: {
sm: '100%',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
},
}}
>
{/* Title and Add Category button */}
{/* <Typography component="h2" variant="h6" sx={{ mt: 2, fontWeight: 600 }}>
Vehicles
</Typography> */}
<Button
variant="contained"
size="medium"
sx={{ textAlign: 'right' }}
onClick={handleClickOpen}
>
Add Categorywewfw
</Button>
</Box>
<CustomTable
columns={categoryColumns}
rows={categoryRows}
editRow={editRow}
setDeleteModal={setDeleteModal}
setRowData={setRowData}
setModalOpen={setModalOpen}
/>
<AddEditCategoryModal
open={modalOpen}
handleClose={handleCloseModal}
editRow={rowData}
/>
<DeleteModal
open={deleteModal}
setDeleteModal={setDeleteModal}
handleDelete={handleDelete}
/>
</>
);
}

View file

@ -5,6 +5,8 @@ import adminReducer from "./slices/adminSlice";
import profileReducer from "./slices/profileSlice";
import userReducer from "./slices/userSlice.ts";
import roleReducer from "./slices/roleSlice.ts";
import vehicleReducer from "./slices/VehicleSlice.ts";
const rootReducer = combineReducers({
authReducer,
@ -12,6 +14,7 @@ const rootReducer = combineReducers({
profileReducer,
userReducer,
roleReducer,
vehicleReducer
});
export type RootState = ReturnType<typeof rootReducer>;

View file

@ -0,0 +1,135 @@
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
import http from "../../lib/https";
import { toast } from "sonner";
interface Vehicle {
id:number;
name:string;
brand:string;
imageUrl:string;
}
interface VehicleState {
vehicles:Vehicle[];
loading: boolean;
error: string | null;
}
const initialState: VehicleState = {
vehicles: [],
loading: false,
error: null,
};
export const vehicleList = createAsyncThunk<Vehicle, void, { rejectValue: string }>(
"fetchVehicles",
async (_, { rejectWithValue }) => {
try {
const token = localStorage?.getItem("authToken");
if (!token) throw new Error("No token found");
const response = await http.get("/");
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"
);
}
}
);
//Add Vehicle
export const addVehicle = createAsyncThunk<
Vehicle,
{
name: string;
brand: string;
imageUrl: string;
},
{ rejectValue: string }
>("/AddVehicle", async (data, { rejectWithValue }) => {
try {
const response = await http.post("/", data);
return response.data;
} catch (error: any) {
return rejectWithValue(
error.response?.data?.message || "An error occurred"
);
}
});
// Update Vehicle details
export const updateVehicle = createAsyncThunk(
"updateVehicle",
async ({ id, ...vehicleData }: Vehicle, { rejectWithValue }) => {
try {
const response = await http.put(`/${id}`, vehicleData);
toast.success("Vehicle Deatils updated successfully");
return response?.data;
} catch (error: any) {
toast.error("Error updating the user: " + error);
return rejectWithValue(
error.response?.data?.message || "An error occurred"
);
}
}
);
const vehicleSlice = createSlice({
name: "vehicle",
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(vehicleList.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(
vehicleList.fulfilled,
(state, action: PayloadAction<Vehicle[]>) => {
state.loading = false;
state.vehicles = action.payload;
}
)
.addCase(vehicleList.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message || "Failed to fetch users";
})
.addCase(addVehicle.pending, (state) => {
state.loading = true;
// state.error = null;
})
.addCase(
addVehicle.fulfilled,
(state, action: PayloadAction<Vehicle>) => {
state.loading = false;
state.vehicles.push(action.payload);
}
)
.addCase(
addVehicle.rejected,
(state, action: PayloadAction<string | undefined>) => {
state.loading = false;
}
)
.addCase(updateVehicle.pending, (state) => {
state.loading = true;
})
.addCase(updateVehicle.fulfilled, (state, action) => {
const updateVehicle = action.payload;
state.vehicles = state?.vehicles?.map((vehicle) =>
vehicle?.id === updateVehicle?.id ? updateVehicle : vehicle
);
state.loading = false;
})
.addCase(updateVehicle.rejected, (state) => {
state.loading = false;
});
},
});
export default vehicleSlice.reducer;

View file

@ -28,15 +28,6 @@ const initialState: UserState = {
error: null,
};
// Async thunk to fetch user list
// export const userList = createAsyncThunk<User[]>("users/fetchUsers", async () => {
// try {
// const response = await axios.get<User[]>("/api/users"); // Adjust the API endpoint as needed
// return response.data;
// } catch (error: any) {
// throw new Error(error.response?.data?.message || "Failed to fetch users");
// }
// });
export const userList = createAsyncThunk<User, void, { rejectValue: string }>(
"fetchUsers",
async (_, { rejectWithValue }) => {

View file

@ -4,12 +4,13 @@ import LoadingComponent from "./components/Loading";
import DashboardLayout from "./layouts/DashboardLayout";
import RoleList from "./pages/RoleList";
import AddEditRolePage from "./pages/AddEditRolePage";
import VehicleList from "./pages/VehicleList";
// Page imports
const Login = lazy(() => import("./pages/Auth/Login"));
const SignUp = lazy(() => import("./pages/Auth/SignUp"));
const Dashboard = lazy(() => import("./pages/Dashboard"));
const Vehicles = lazy(() => import("./pages/Vehicles"));
const Vehicles = lazy(() => import("./pages/VehicleList"));
const AdminList = lazy(() => import("./pages/AdminList"));
const ProfilePage = lazy(() => import("./pages/ProfilePage"));
const NotFoundPage = lazy(() => import("./pages/NotFound"));
@ -95,6 +96,15 @@ export default function AppRouter() {
/>
}
/>
<Route
path="vehicle-list"
element={
<ProtectedRoute
caps={[]}
component={<VehicleList />}
/>
}
/>
<Route
path="permissions"
element={