Create Charging Station Ui and Updation in Managers

This commit is contained in:
jaanvi 2025-03-13 16:25:46 +05:30
parent 1e922f1ac5
commit 690af22199
16 changed files with 1211 additions and 150 deletions

View file

@ -31,7 +31,6 @@ export default function AddManagerModal({
reset,
} = useForm();
const [showPassword, setShowPassword] = useState(false);
// Handle form submission
const onSubmit = async (data: any) => {
@ -41,6 +40,7 @@ export default function AddManagerModal({
phone: data.phone,
registeredAddress: data.registeredAddress,
password: data.password,
stationId: data.stationId, // Send stationId here
roleName: "Manager", // You can replace this with dynamic role if needed
};
@ -55,11 +55,10 @@ export default function AddManagerModal({
}
};
const togglePasswordVisibility = (e: React.MouseEvent) => {
const togglePasswordVisibility = (e: React.MouseEvent) => {
e.preventDefault(); // Prevent focus loss
setShowPassword((prev) => !prev);
};
};
return (
<Modal
@ -107,37 +106,59 @@ export default function AddManagerModal({
{/* Form */}
<form onSubmit={handleSubmit(onSubmit)}>
{/* Manager Name */}
<Box
sx={{ display: "flex", flexDirection: "column", mb: 2 }}
>
<Typography variant="body2" fontWeight={500}>
Manager Name
</Typography>
<CustomTextField
fullWidth
placeholder="Enter Manager Name"
size="small"
error={!!errors.name}
helperText={errors.name ? errors.name.message : ""}
{...register("name", {
required: "Manager Name is required",
minLength: {
value: 3,
message: "Minimum 3 characters required",
},
maxLength: {
value: 30,
message: "Maximum 30 characters allowed",
},
pattern: {
value: /^[A-Za-z\s]+$/, // Only letters and spaces are allowed
message:
"Manager Name must only contain letters and spaces",
},
})}
/>
<Box sx={{ display: "flex", gap: 2, mb: 2 }}>
<Box sx={{ flex: 1 }}>
<Typography variant="body2" fontWeight={500}>
Manager Name
</Typography>
<CustomTextField
fullWidth
placeholder="Enter Manager Name"
size="small"
error={!!errors.name}
helperText={
errors.name ? errors.name.message : ""
}
{...register("name", {
required: "Manager Name is required",
minLength: {
value: 3,
message:
"Minimum 3 characters required",
},
maxLength: {
value: 30,
message:
"Maximum 30 characters allowed",
},
pattern: {
value: /^[A-Za-z\s]+$/, // Only letters and spaces are allowed
message:
"Manager Name must only contain letters and spaces",
},
})}
/>
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="body2" fontWeight={500}>
Station Id
</Typography>
<CustomTextField
fullWidth
placeholder="Enter Station Id"
size="small"
error={!!errors.stationId}
helperText={
errors.stationId
? errors.stationId.message
: ""
}
{...register("stationId", {
required: "Station Id is required",
})}
/>
</Box>
</Box>
{/* Email and Password */}
<Box sx={{ display: "flex", gap: 2, mb: 2 }}>
{/* Email */}
@ -256,7 +277,7 @@ export default function AddManagerModal({
{/* Registered Address */}
<Box sx={{ flex: 1 }}>
<Typography variant="body2" fontWeight={500}>
Registered Address
Station Location
</Typography>
<CustomTextField
fullWidth

View file

@ -0,0 +1,205 @@
import { useForm } from "react-hook-form";
import { Box, Button, Typography, Modal } from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
import {
CustomIconButton,
CustomTextField,
} from "../AddUserModel/styled.css.tsx"; // Assuming custom styled components
export default function AddStationModal({
open,
handleClose,
handleAddStation,
}) {
const {
register,
handleSubmit,
formState: { errors },
reset,
} = useForm();
const onSubmit = (data: any) => {
handleAddStation(data); // Add station to the list
handleClose(); // Close modal after adding
reset();
};
return (
<Modal
open={open}
onClose={(e, reason) => {
if (reason === "backdropClick") {
return;
}
handleClose(); // Close modal when clicking cross or cancel
}}
aria-labelledby="add-station-modal"
>
<Box
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}>
Add Charging Station
</Typography>
<CustomIconButton onClick={handleClose}>
<CloseIcon />
</CustomIconButton>
</Box>
{/* Horizontal Line */}
<Box sx={{ borderBottom: "1px solid #ddd", my: 2 }} />
{/* Form */}
<form onSubmit={handleSubmit(onSubmit)}>
{/* Input Fields */}
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: 2,
}}
>
{/* First Row - Two Inputs */}
<Box sx={{ display: "flex", gap: 2 }}>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
}}
>
<Typography variant="body2" fontWeight={500}>
Station Name
</Typography>
<CustomTextField
fullWidth
placeholder="Enter Station Name"
size="small"
error={!!errors.name}
helperText={
errors.name ? errors.name.message : ""
}
{...register("name", {
required: "Station Name is required",
minLength: {
value: 3,
message:
"Minimum 3 characters required",
},
maxLength: {
value: 30,
message:
"Maximum 30 characters allowed",
},
})}
/>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
}}
>
<Typography variant="body2" fontWeight={500}>
Station Location
</Typography>
<CustomTextField
fullWidth
placeholder="Enter Registered Address"
size="small"
error={!!errors.registeredAddress}
helperText={
errors.registeredAddress
? errors.registeredAddress.message
: ""
}
{...register("registeredAddress", {
required:
"Registered Address is required",
})}
/>
</Box>
</Box>
{/* Second Row - Total Slots */}
<Box sx={{ display: "flex", gap: 2 }}>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
}}
>
<Typography variant="body2" fontWeight={500}>
Total Slots
</Typography>
<CustomTextField
fullWidth
placeholder="Enter Total Slots"
size="small"
type="number"
error={!!errors.totalSlots}
helperText={
errors.totalSlots
? errors.totalSlots.message
: ""
}
{...register("totalSlots", {
required: "Total Slots are required",
min: {
value: 1,
message:
"At least 1 slot is required",
},
})}
/>
</Box>
</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" },
}}
>
Add Station
</Button>
</Box>
</form>
</Box>
</Modal>
);
}

View file

@ -34,6 +34,8 @@ import { CustomIconButton } from "../AddUserModel/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 StationViewModal from "../Modals/StationViewModal/index.tsx";
// Styled components for customization
const StyledTableCell = styled(TableCell)(({ theme }) => ({
[`&.${tableCellClasses.head}`]: {
@ -144,6 +146,9 @@ const CustomTable: React.FC<CustomTableProps> = ({
case "user":
dispatch(deleteUser(id || ""));
break;
case "station":
dispatch(deleteStation(id || ""));
break;
default:
console.error("Unknown table type:", tableType);
return;
@ -231,6 +236,8 @@ const CustomTable: React.FC<CustomTableProps> = ({
? "Managers"
: tableType === "vehicle"
? "Vehicles"
: tableType === "station"
? "Charging Station"
: "List"}
</Typography>
@ -302,6 +309,8 @@ const CustomTable: React.FC<CustomTableProps> = ({
? "Manager"
: tableType === "vehicle"
? "Vehicle"
: tableType === "station"
? "Charging Station"
: "Item"}
</Button>
</Box>
@ -519,6 +528,16 @@ const CustomTable: React.FC<CustomTableProps> = ({
id={selectedRow?.id}
/>
)}
{viewModal && tableType === "station" && (
<StationViewModal
handleView={() =>
handleViewButton(selectedRow?.id)
}
open={viewModal}
setViewModal={setViewModal}
id={selectedRow?.id}
/>
)}
<Button
variant="text"
@ -551,6 +570,25 @@ const CustomTable: React.FC<CustomTableProps> = ({
: "Activate"}
</Button>
)}
{tableType === "station" && (
<Button
variant="text"
onClick={(e) => {
e.stopPropagation();
handleToggleStatus();
}}
color="secondary"
sx={{
justifyContent: "flex-start",
py: 0,
fontWeight: "bold",
}}
>
{selectedRow?.statusValue === 1
? "Not Available"
: "Available"}
</Button>
)}
<Button
variant="text"

View file

@ -24,7 +24,6 @@ interface EditManagerModalProps {
interface FormData {
name: string;
email: string;
registeredAddress: string;
phone: string;
}
@ -44,7 +43,6 @@ const EditManagerModal: React.FC<EditManagerModalProps> = ({
defaultValues: {
name: "",
email: "",
registeredAddress: "",
phone: "",
},
});
@ -56,7 +54,6 @@ const EditManagerModal: React.FC<EditManagerModalProps> = ({
if (editRow) {
setValue("name", editRow.name);
setValue("email", editRow.email);
setValue("registeredAddress", editRow.registeredAddress);
setValue("phone", editRow.phone);
} else {
reset();
@ -72,12 +69,11 @@ const EditManagerModal: React.FC<EditManagerModalProps> = ({
managerData: {
name: data.name,
email: data.email,
registeredAddress: data.registeredAddress,
phone: data.phone,
},
})
).unwrap(); // Ensure that it throws an error if the update fails
dispatch(managerList());
dispatch(managerList());
handleClose(); // Close modal on success
reset(); // Reset form fields after submit
} catch (error) {
@ -134,9 +130,9 @@ const EditManagerModal: React.FC<EditManagerModalProps> = ({
{/* Horizontal Line */}
<Box sx={{ borderBottom: "1px solid #ddd", my: 2 }} />
{/* Input Fields (2 inputs per row) */}
{/* Input Fields (Name, Email, and Phone) */}
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 2 }}>
{/* Manager Name and Email in one row */}
{/* Manager Name */}
<Box sx={{ flex: "1 1 48%" }}>
<Typography variant="body2" fontWeight={500} mb={0.5}>
Manager Name
@ -145,24 +141,22 @@ const EditManagerModal: React.FC<EditManagerModalProps> = ({
name="name"
control={control}
rules={{
required: "Manager Name is required",
minLength: {
value: 3,
message:
"Minimum 3 characters required",
},
maxLength: {
value: 30,
message:
"Maximum 30 characters allowed",
},
pattern: {
value: /^[A-Za-z\s]+$/, // Only letters and spaces are allowed
message:
"Manager Name must only contain letters and spaces",
},
}}
render={({ field }) => (
required: "Manager Name is required",
minLength: {
value: 3,
message: "Minimum 3 characters required",
},
maxLength: {
value: 30,
message: "Maximum 30 characters allowed",
},
pattern: {
value: /^[A-Za-z\s]+$/, // Only letters and spaces are allowed
message:
"Manager Name must only contain letters and spaces",
},
}}
render={({ field }) => (
<CustomTextField
{...field}
fullWidth
@ -170,12 +164,12 @@ const EditManagerModal: React.FC<EditManagerModalProps> = ({
size="small"
error={!!errors.name}
helperText={errors.name?.message}
/>
)}
/>
</Box>
{/* Email */}
<Box sx={{ flex: "1 1 48%" }}>
<Typography variant="body2" fontWeight={500} mb={0.5}>
Email
@ -197,32 +191,7 @@ const EditManagerModal: React.FC<EditManagerModalProps> = ({
/>
</Box>
{/* Registered Address and Phone Number in one row */}
<Box sx={{ flex: "1 1 48%" }}>
<Typography variant="body2" fontWeight={500} mb={0.5}>
Registered Address
</Typography>
<Controller
name="registeredAddress"
control={control}
rules={{
required: "Registered Address is required",
}}
render={({ field }) => (
<CustomTextField
{...field}
fullWidth
placeholder="Enter Registered Address"
size="small"
error={!!errors.registeredAddress}
helperText={
errors.registeredAddress?.message
}
/>
)}
/>
</Box>
{/* Phone Number */}
<Box sx={{ flex: "1 1 48%" }}>
<Typography variant="body2" fontWeight={500} mb={0.5}>
Phone Number

View file

@ -0,0 +1,270 @@
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 "../AddUserModel/styled.css.tsx";
interface EditStationModalProps {
open: boolean;
handleClose: () => void;
handleUpdate: (
id: string,
name: string,
registeredAddress: string,
totalSlots: number,
imageUrl: string
) => void;
editRow: any;
}
interface FormData {
name: string;
registeredAddress: string;
totalSlots: number;
imageUrl: string;
}
const EditStationModal: React.FC<EditStationModalProps> = ({
open,
handleClose,
handleUpdate,
editRow,
}) => {
const {
control,
handleSubmit,
formState: { errors },
setValue,
reset,
} = useForm<FormData>({
defaultValues: {
name: "",
registeredAddress: "",
totalSlots: 0,
imageUrl: "",
},
});
// Set values if editRow is provided
useEffect(() => {
if (editRow) {
setValue("name", editRow.name);
setValue("registeredAddress", editRow.registeredAddress);
setValue("totalSlots", editRow.totalSlots);
setValue("imageUrl", editRow.imageUrl);
} else {
reset();
}
}, [editRow, setValue, reset]);
const onSubmit = (data: FormData) => {
if (editRow) {
handleUpdate(
editRow.id,
data.name,
data.registeredAddress,
data.totalSlots,
data.imageUrl
);
}
handleClose(); // Close the modal
reset(); // Reset the form fields
};
return (
<Modal
open={open}
onClose={(e, reason) => {
if (reason === "backdropClick") {
return;
}
handleClose(); // Close modal when clicking cross or cancel
}}
aria-labelledby="edit-station-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 Charging Station
</Typography>
<CustomIconButton onClick={handleClose}>
<CloseIcon />
</CustomIconButton>
</Box>
{/* Horizontal Line */}
<Box sx={{ borderBottom: "1px solid #ddd", my: 2 }} />
{/* Input Fields */}
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
{/* First Row - Two Inputs */}
<Box sx={{ display: "flex", gap: 2 }}>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
}}
>
<Typography
variant="body2"
fontWeight={500}
mb={0.5}
>
Station Name
</Typography>
<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}
fullWidth
placeholder="Enter Station Name"
size="small"
error={!!errors.name}
helperText={errors.name?.message}
/>
)}
/>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
}}
>
<Typography
variant="body2"
fontWeight={500}
mb={0.5}
>
Station Location
</Typography>
<Controller
name="registeredAddress"
control={control}
rules={{
required: "Registered Address is required",
}}
render={({ field }) => (
<CustomTextField
{...field}
fullWidth
placeholder="Enter Registered Address"
size="small"
error={!!errors.registeredAddress}
helperText={
errors.registeredAddress?.message
}
/>
)}
/>
</Box>
</Box>
{/* Second Row - Total Slots */}
<Box sx={{ display: "flex", gap: 2 }}>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
}}
>
<Typography
variant="body2"
fontWeight={500}
mb={0.5}
>
Total Slots
</Typography>
<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}
fullWidth
placeholder="Enter Total Slots"
size="small"
type="number"
error={!!errors.totalSlots}
helperText={errors.totalSlots?.message}
/>
)}
/>
</Box>
</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" },
}}
>
Update Charging Station
</Button>
</Box>
</Box>
</Modal>
);
};
export default EditStationModal;

View file

@ -43,17 +43,24 @@ export default function MenuContent({ hidden }: PropType) {
text: "Users",
icon: <AnalyticsRoundedIcon />,
url: "/panel/user-list",
},
userRole === "admin" && {
text: "Charging Stations",
icon: <ManageAccountsOutlinedIcon />,
url: "/panel/station-list", // Placeholder for now
},
userRole === "admin" && {
text: "Managers",
icon: <ManageAccountsOutlinedIcon />,
url: "/panel/manager-list", // Placeholder for now
},
userRole === "admin" && {
text: "Vehicles",
icon: <ManageAccountsOutlinedIcon />,
url: "/panel/vehicle-list", // Placeholder for now
},
];
const filteredMenuItems = baseMenuItems.filter(Boolean);
@ -99,4 +106,4 @@ export default function MenuContent({ hidden }: PropType) {
</List>
</Stack>
);
}
}

View file

@ -0,0 +1,127 @@
import React, { useEffect, useState } from "react";
import { Box, Modal, Typography, Divider, Grid } from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
import { useSelector } from "react-redux";
import { RootState } from "../../../redux/reducers";
type Props = {
open: boolean;
setViewModal: Function;
handleView: (id: string | undefined) => void;
id?: number | undefined;
};
const style = {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 400,
bgcolor: "background.paper",
borderRadius: 2,
boxShadow: "0px 4px 20px rgba(0, 0, 0, 0.15)",
p: 4,
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 2,
};
export default function StationViewModal({ open, setViewModal, id }: Props) {
const { stations } = useSelector(
(state: RootState) => state.stationReducer
);
const [selectedStation, setSelectedStation] = useState<any>(null);
useEffect(() => {
if (id) {
const station = stations.find((station) => station.id === id);
setSelectedStation(station || null);
}
}, [id, stations]);
return (
<Modal
open={open}
aria-labelledby="modal-title"
aria-describedby="modal-description"
>
<Box sx={style}>
<Typography
id="modal-title"
variant="h5"
fontWeight="bold"
sx={{ width: "100%" }}
>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
}}
>
<Box sx={{ flex: 1, textAlign: "center" }}>
{selectedStation?.name || "Station"}'s Details
</Box>
<Box
onClick={() => setViewModal(false)}
sx={{
cursor: "pointer",
display: "flex",
alignItems: "center",
}}
>
<CloseIcon />
</Box>
</Box>
</Typography>
<Divider sx={{ width: "100%" }} />
{selectedStation ? (
<Grid container spacing={2} sx={{ width: "80%" }}>
<Grid item xs={6}>
<Typography variant="body1">
<strong>Station Name:</strong>{" "}
<Typography variant="body2">
{selectedStation.name}
</Typography>
</Typography>
</Grid>
<Grid item xs={6}>
<Typography variant="body1">
<strong>Station Location:</strong>{" "}
<Typography variant="body2">
{selectedStation.registeredAddress}
</Typography>
</Typography>
</Grid>
<Grid item xs={6}>
<Typography variant="body1">
<strong>Total Slots:</strong>
<Typography variant="body2">
{selectedStation.totalSlots}
</Typography>
</Typography>
</Grid>
<Grid item xs={6}>
<Typography variant="body1">
<strong>Status:</strong>
<Typography variant="body2">
{selectedStation.status === "available"
? "Available"
: "Not Available"}
</Typography>
</Typography>
</Grid>
</Grid>
) : (
<Typography align="center">
No station found with this ID
</Typography>
)}
</Box>
</Modal>
);
}

View file

@ -29,15 +29,18 @@ export default function ManagerList() {
(state: RootState) => state.managerReducer.managers
);
// Fetch manager list on component mount
useEffect(() => {
dispatch(managerList());
}, [dispatch]);
// Open Add Manager Modal
const handleClickOpen = () => {
setRowData(null); // Reset row data when opening for new manager
setAddModalOpen(true);
};
// Close all modals
const handleCloseModal = () => {
setAddModalOpen(false);
setEditModalOpen(false);
@ -45,13 +48,16 @@ export default function ManagerList() {
reset();
};
// Handle adding a new manager
const handleAddManager = async (data: {
name: string;
email: string;
phone: string;
registeredAddress: string;
stationId: string;
}) => {
try {
// Add manager with stationId
await dispatch(addManager(data)); // Dispatch action to add manager
await dispatch(managerList()); // Fetch the updated list
handleCloseModal(); // Close the modal
@ -60,14 +66,17 @@ export default function ManagerList() {
}
};
// Handle updating an existing manager
const handleUpdate = async (
id: number,
name: string,
email: string,
phone: string,
registeredAddress: string
registeredAddress: string,
stationId: string
) => {
try {
// Update manager with stationId
await dispatch(
updateManager({
id,
@ -75,6 +84,7 @@ export default function ManagerList() {
email,
phone,
registeredAddress,
stationId,
})
);
await dispatch(managerList()); // Refresh the list after update
@ -84,16 +94,18 @@ export default function ManagerList() {
}
};
// 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: "registeredAddress", label: "Station Location" },
{ id: "stationName", label: "Station Name" },
{ id: "action", label: "Action", align: "center" },
];
// Filter managers based on search term
const filteredManagers = managers?.filter(
(manager) =>
manager.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
@ -104,6 +116,7 @@ export default function ManagerList() {
.includes(searchTerm.toLowerCase())
);
// Format rows to display manager details
const categoryRows = filteredManagers?.length
? filteredManagers?.map(
(
@ -113,6 +126,7 @@ export default function ManagerList() {
email: string;
phone: string;
registeredAddress: string;
stationName: string;
},
index: number
) => ({
@ -122,12 +136,14 @@ export default function ManagerList() {
email: manager?.email,
phone: manager.phone ?? "NA",
registeredAddress: manager?.registeredAddress ?? "NA",
stationName: manager?.stationName ?? "NA",
})
)
: [];
return (
<>
{/* Custom Table to show manager list */}
<CustomTable
columns={categoryColumns}
rows={categoryRows}
@ -140,11 +156,13 @@ export default function ManagerList() {
tableType="manager"
handleClickOpen={handleClickOpen}
/>
{/* Add Manager Modal */}
<AddManagerModal
open={addModalOpen}
handleClose={handleCloseModal}
handleAddManager={handleAddManager}
/>
{/* Edit Manager Modal */}
<EditManagerModal
open={editModalOpen}
handleClose={handleCloseModal}

View file

@ -0,0 +1,192 @@
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 {
addVehicle,
updateVehicle,
vehicleList,
} from "../../redux/slices/VehicleSlice";
import AddStationModal from "../../components/AddStationModal";
import EditStationModal from "../../components/EditStationModal";
import {
createStation,
stationList,
updateStation,
} from "../../redux/slices/stationSlice";
import { Chip, Switch } from "@mui/material";
export default function StationList() {
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 stations = useSelector(
(state: RootState) => state.stationReducer.stations
);
useEffect(() => {
dispatch(stationList());
}, [dispatch]);
const handleClickOpen = () => {
setRowData(null); // Reset row data when opening for new admin
setAddModalOpen(true);
};
const handleCloseModal = () => {
setAddModalOpen(false);
setEditModalOpen(false);
setRowData(null);
reset();
};
const handleAddStation = async (data: {
name: string;
registeredAddress: string;
totalSlots: string;
}) => {
try {
await dispatch(createStation(data)); // Dispatch action to add Station
await dispatch(stationList()); // Fetch the updated list
handleCloseModal(); // Close the modal
} catch (error) {
console.error("Error adding Station", error);
}
};
const handleUpdate = async (
id: string,
name: string,
registeredAddress: string,
totalSlots: string
) => {
try {
await dispatch(
updateStation({
id,
name,
registeredAddress,
totalSlots,
})
);
await dispatch(stationList());
handleCloseModal();
} catch (error) {
console.error("Update failed", error);
}
};
// 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: "",
// registeredAddress: "",
// 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,
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 categoryColumns: Column[] = [
{ id: "srno", label: "Sr No" },
{ id: "name", label: "Station Name" },
{ id: "registeredAddress", label: "Station Location" },
{ id: "totalSlots", label: "Total Slots" },
{ id: "status", label: "Status" },
{ 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
columns={categoryColumns}
rows={categoryRows}
setDeleteModal={setDeleteModal}
deleteModal={deleteModal}
setViewModal={setViewModal}
viewModal={viewModal}
setRowData={setRowData}
setModalOpen={() => setEditModalOpen(true)}
tableType="station"
handleClickOpen={handleClickOpen}
/>
<AddStationModal
open={addModalOpen}
handleClose={handleCloseModal}
handleAddStation={handleAddStation}
/>
<EditStationModal
open={editModalOpen}
handleClose={handleCloseModal}
handleUpdate={handleUpdate}
editRow={rowData}
/>
</>
);
}

View file

@ -7,6 +7,7 @@ import userReducer from "./slices/userSlice.ts";
import roleReducer from "./slices/roleSlice.ts";
import vehicleReducer from "./slices/VehicleSlice.ts";
import managerReducer from "../redux/slices/managerSlice.ts";
import stationReducer from "../redux/slices/stationSlice.ts";
const rootReducer = combineReducers({
@ -17,6 +18,7 @@ const rootReducer = combineReducers({
roleReducer,
vehicleReducer,
managerReducer,
stationReducer
// Add other reducers here...
});

View file

@ -71,7 +71,7 @@ export const updateVehicle = createAsyncThunk(
async ({ id, ...vehicleData }: Vehicle, { rejectWithValue }) => {
try {
const response = await http.patch(
`${id}/update-vehicle`,
`/update-vehicle/${id}`,
vehicleData
);
toast.success("Vehicle Deatils updated successfully");
@ -90,7 +90,7 @@ export const deleteVehicle = createAsyncThunk<
{ rejectValue: string }
>("deleteVehicle", async (id, { rejectWithValue }) => {
try {
const response = await http.delete(`/${id}/delete-vehicle`);
const response = await http.delete(`/delete-vehicle/${id}`);
toast.success(response.data?.message);
return id;
} catch (error: any) {

View file

@ -55,7 +55,7 @@ export const deleteAdmin = createAsyncThunk<
{ rejectValue: string }
>("deleteAdmin", async (id, { rejectWithValue }) => {
try {
const response = await http.delete(`/${id}/delete-admin`);
const response = await http.delete(`/delete-admin/${id}`);
toast.success(response.data?.message);
return id;
} catch (error: any) {
@ -92,7 +92,7 @@ export const updateAdmin = createAsyncThunk(
"updateAdmin",
async ({ id, ...userData }: User, { rejectWithValue }) => {
try {
const response = await http.put(`/${id}/update-admin`, userData);
const response = await http.put(`/update-admin/${id}`, userData);
toast.success("Admin updated successfully");
return response?.data;
} catch (error: any) {

View file

@ -10,7 +10,7 @@ interface Manager {
email: string;
phone: string;
registeredAddress: string;
roleId: number;
stationId: string;
}
interface ManagerState {
@ -74,7 +74,7 @@ export const updateManager = createAsyncThunk<
return rejectWithValue("Manager ID is required.");
}
try {
const response = await http.put(`/${id}/update-manager`, managerData);
const response = await http.put(`/update-manager/${id}`, managerData);
toast.success("Manager updated successfully");
return response?.data;
} catch (error: any) {
@ -92,7 +92,7 @@ export const deleteManager = createAsyncThunk<
{ rejectValue: string }
>("deleteManager", async (id, { rejectWithValue }) => {
try {
await http.delete(`/${id}/delete-manager`);
await http.delete(`/delete-manager/${id}`);
toast.success("Manager deleted successfully!");
return id;
} catch (error: any) {

View file

@ -0,0 +1,246 @@
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
import axios from "axios";
import http from "../../lib/https";
import { toast } from "sonner";
// Define TypeScript types
interface Station {
id: string;
name: string;
registeredAddress: string;
totalSlots: string;
status: number;
}
interface StationState {
stations: Station[];
loading: boolean;
error: string | null;
}
// Initial state
const initialState: StationState = {
stations: [],
loading: false,
error: null,
};
export const stationList = createAsyncThunk<any, void, { rejectValue: string }>(
"fetchStations",
async (_, { rejectWithValue }) => {
try {
const token = localStorage?.getItem("authToken");
if (!token) throw new Error("No token found");
const response = await http.get("/get-station");
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;
},
{ rejectValue: string }
>("Station/createStation", async (data, { rejectWithValue }) => {
try {
const response = await http.post("/create-station", data);
toast.success("Station created successfully");
return response.data;
} catch (error: any) {
toast.error(
"Failed to create Station: " +
(error.response?.data?.message || "Unknown error")
);
return rejectWithValue(
error.response?.data?.message || "An error occurred"
);
}
});
// Update Station details
export const updateStation = createAsyncThunk(
"updateStation",
async ({ id, ...stationData }: Station, { rejectWithValue }) => {
try {
const response = await http.patch(
`/update-station/${id}`,
stationData
);
toast.success("Station 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"
);
}
}
);
export const deleteStation = createAsyncThunk<
string,
string,
{ rejectValue: string }
>("deleteStation", async (id, { rejectWithValue }) => {
try {
const response = await http.delete(`/delete-station/${id}`);
toast.success(response.data?.message);
return id;
} catch (error: any) {
toast.error("Error deleting the Station" + error);
return rejectWithValue(
error.response?.data?.message || "An error occurred"
);
}
});
export const toggleStatus = createAsyncThunk<
any,
{ id: string; status: number },
{ rejectValue: string }
>("Station/toggleStatus", async ({ id, status }, { rejectWithValue }) => {
try {
const response = await http.patch(`${id}`, { status });
if (response.data.statusCode === 200) {
toast.success(
response.data.message || "Status updated successfully"
);
// Return both the response data and the requested status for reliable state updates
return {
responseData: response.data,
id,
status,
};
} else {
throw new Error(response.data.message || "Failed to update status");
}
} catch (error: any) {
toast.error(
"Error updating status: " + (error.message || "Unknown error")
);
return rejectWithValue(
error.response?.data?.message ||
error.message ||
"An error occurred"
);
}
});
const stationSlice = createSlice({
name: "stations",
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(stationList.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(
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 ||
[];
}
)
.addCase(stationList.rejected, (state, action) => {
state.loading = false;
state.error = action.payload || "Failed to fetch stations";
})
.addCase(createStation.pending, (state) => {
state.loading = true;
})
.addCase(
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>) => {
state.loading = false;
state.error = action.payload || "Failed to create station";
}
)
.addCase(toggleStatus.pending, (state) => {
state.loading = true;
})
.addCase(
toggleStatus.fulfilled,
(state, action: PayloadAction<any>) => {
state.loading = false;
// Get the id and updated status from the action payload
const { id, status } = action.payload;
// Find and update the station with the new status
const stationIndex = state.stations.findIndex(
(station) => station.id === id
);
if (stationIndex !== -1) {
state.stations[stationIndex] = {
...state.stations[stationIndex],
status: status,
};
}
}
)
.addCase(
toggleStatus.rejected,
(state, action: PayloadAction<string | undefined>) => {
state.loading = false;
state.error =
action.payload || "Failed to toggle station status";
}
)
.addCase(updateStation.pending, (state) => {
state.loading = true;
})
.addCase(updateStation.fulfilled, (state, action) => {
state.loading = false;
state.error = action.payload;
})
.addCase(updateStation.rejected, (state) => {
state.loading = false;
})
.addCase(deleteStation.pending, (state) => {
state.loading = true;
})
.addCase(deleteStation.fulfilled, (state, action) => {
state.loading = false;
state.stations = state.stations.filter(
(station) => String(station.id) !== String(action.payload)
);
})
.addCase(deleteStation.rejected, (state) => {
state.loading = false;
});
},
});
export default stationSlice.reducer;

View file

@ -60,7 +60,7 @@ export const createUser = createAsyncThunk<
{ rejectValue: string }
>("/CreateUser", async (data, { rejectWithValue }) => {
try {
const response = await http.post("create-user", data);
const response = await http.post("/create-user", data);
return response.data;
} catch (error: any) {
return rejectWithValue(
@ -73,7 +73,7 @@ export const updateUser = createAsyncThunk(
"updateUser",
async ({ id, ...userData }: User, { rejectWithValue }) => {
try {
const response = await http.put(`/${id}/update-user`, userData);
const response = await http.put(`/update-user/${id}`, userData);
toast.success("User updated successfully");
return response?.data;
} catch (error: any) {
@ -91,7 +91,7 @@ export const deleteUser = createAsyncThunk<
{ rejectValue: string }
>("deleteUser", async (id, { rejectWithValue }) => {
try {
const response = await http.delete(`/${id}/delete-user`);
const response = await http.delete(`/delete-user/${id}`);
toast.success(response.data?.message);
return id;
} catch (error: any) {

View file

@ -18,6 +18,7 @@ const UserList = lazy(() => import("./pages/UserList"));
const AddEditRolePage = lazy(() => import("./pages/AddEditRolePage"));
const RoleList = lazy(() => import("./pages/RoleList"));
const ManagerList = lazy(() => import("./pages/ManagerList"));
const StationList = lazy(() => import("./pages/StationList"));
interface ProtectedRouteProps {
@ -51,79 +52,44 @@ export default function AppRouter() {
<Route path="/panel" element={<DashboardLayout />}>
<Route
path="dashboard"
element={
<ProtectedRoute
component={<Dashboard />}
/>
}
element={<ProtectedRoute component={<Dashboard />} />}
/>
<Route
path="admin-list"
element={
<ProtectedRoute
component={<AdminList />}
/>
}
element={<ProtectedRoute component={<AdminList />} />}
/>
<Route
path="user-list"
element={
<ProtectedRoute
component={<UserList />}
/>
}
/>
<Route
path="manager-list"
element={
<ProtectedRoute
component={<ManagerList />}
/>
}
element={<ProtectedRoute component={<UserList />} />}
/>
<Route
path="manager-list"
element={<ProtectedRoute component={<ManagerList />} />}
/>
<Route
path="role-list"
element={
<ProtectedRoute
component={<RoleList />}
/>
}
element={<ProtectedRoute component={<RoleList />} />}
/>
<Route
path="vehicle-list"
element={
<ProtectedRoute
component={<VehicleList />}
/>
}
element={<ProtectedRoute component={<VehicleList />} />}
/>
<Route
path="permissions"
element={
<ProtectedRoute
component={<AddEditRolePage />}
/>
<ProtectedRoute component={<AddEditRolePage />} />
}
/>
<Route
path="station-list"
element={<ProtectedRoute component={<StationList />} />}
/>
<Route
path="profile"
element={
<ProtectedRoute
component={<ProfilePage />}
/>
}
element={<ProtectedRoute component={<ProfilePage />} />}
/>
</Route>