image slotbooking integration and ui changes

This commit is contained in:
jaanvi 2025-04-11 18:30:41 +05:30
parent 36f6df429a
commit 62777a67de
12 changed files with 786 additions and 486 deletions

View file

@ -164,8 +164,10 @@ import {
InputLabel,
} from "@mui/material";
import { useForm } from "react-hook-form";
import { useDispatch } from "react-redux"; // Import the Redux dispatch
import { createSlot } from "../../redux/slices/slotSlice.ts"; // Assuming this is your slice
const AddSlotModal = ({ open, handleClose, handleAddSlot }: any) => {
const AddSlotModal = ({ open, handleClose }: any) => {
const {
register,
handleSubmit,
@ -173,6 +175,8 @@ const AddSlotModal = ({ open, handleClose, handleAddSlot }: any) => {
watch,
formState: { errors },
} = useForm();
const dispatch = useDispatch(); // Get dispatch from Redux
const [isAvailable, setIsAvailable] = useState<boolean>(true);
const [isDateRange, setIsDateRange] = useState<boolean>(false);
const [durationUnit, setDurationUnit] = useState<string>("minutes");
@ -190,60 +194,41 @@ const AddSlotModal = ({ open, handleClose, handleAddSlot }: any) => {
}
}, [startHour]);
const onSubmit = (data: any) => {
const { date, startDate, endDate, startHour, endHour, duration } = data;
const slots: { date: string; startHour: string; endHour: string; isAvailable: boolean; duration: number; }[] = [];
const onSubmit = (data: any) => {
const {
date,
startingDate,
endingDate,
startHour,
endHour,
duration,
} = data;
const generateSlotsForDate = (date: string) => {
const startTime = new Date(`1970-01-01T${startHour}:00`);
const endTime = new Date(`1970-01-01T${endHour}:00`);
let durationMinutes = parseInt(duration, 10);
const payload = isDateRange
? {
startingDate,
endingDate,
startHour,
endHour,
duration: parseInt(duration, 10),
isAvailable,
}
: {
date,
startHour,
endHour,
duration: parseInt(duration, 10),
isAvailable,
};
if (durationUnit === "hours") {
durationMinutes *= 60;
}
dispatch(createSlot(payload));
reset();
handleClose();
};
for (
let time = startTime;
time < endTime;
time.setMinutes(time.getMinutes() + durationMinutes)
) {
const slotEndTime = new Date(time);
slotEndTime.setMinutes(
slotEndTime.getMinutes() + durationMinutes
);
if (slotEndTime <= endTime) {
slots.push({
date,
startHour: time.toTimeString().slice(0, 5),
endHour: slotEndTime.toTimeString().slice(0, 5),
isAvailable,
duration: durationMinutes,
});
}
}
};
if (isDateRange) {
const start = new Date(startDate);
const end = new Date(endDate);
for (
let d = new Date(start);
d <= end;
d.setDate(d.getDate() + 1)
) {
const dateString = d.toISOString().split("T")[0];
generateSlotsForDate(dateString);
}
} else {
generateSlotsForDate(date);
}
handleAddSlot(slots);
reset();
handleClose();
};
return (
<Dialog open={open} onClose={handleClose}>
@ -265,7 +250,7 @@ const AddSlotModal = ({ open, handleClose, handleAddSlot }: any) => {
Start Date
</Typography>
<TextField
{...register("startDate", {
{...register("startingDate", {
required: "Start date is required",
validate: (value) =>
value >= today ||
@ -274,15 +259,15 @@ const AddSlotModal = ({ open, handleClose, handleAddSlot }: any) => {
type="date"
fullWidth
margin="normal"
error={!!errors.startDate}
helperText={errors.startDate?.message}
error={!!errors.startingDate}
helperText={errors.startingDate?.message}
inputProps={{ min: today }}
/>
<Typography variant="body2" fontWeight={500}>
End Date
</Typography>
<TextField
{...register("endDate", {
{...register("endingDate", {
required: "End date is required",
validate: (value) =>
value >= today ||
@ -291,8 +276,8 @@ const AddSlotModal = ({ open, handleClose, handleAddSlot }: any) => {
type="date"
fullWidth
margin="normal"
error={!!errors.endDate}
helperText={errors.endDate?.message}
error={!!errors.endingDate}
helperText={errors.endingDate?.message}
inputProps={{ min: today }}
/>
</>
@ -366,13 +351,13 @@ const AddSlotModal = ({ open, handleClose, handleAddSlot }: any) => {
helperText={errors.duration?.message}
/>
<FormControl fullWidth>
<InputLabel>Unit</InputLabel>
<Select
value={durationUnit}
onChange={(e) =>
setDurationUnit(e.target.value)
}
label="Unit"
>
<MenuItem value="minutes">Minutes</MenuItem>
<MenuItem value="hours">Hours</MenuItem>
@ -421,5 +406,3 @@ const AddSlotModal = ({ open, handleClose, handleAddSlot }: any) => {
};
export default AddSlotModal;

View file

@ -1,38 +1,75 @@
import { useForm } from "react-hook-form";
import { useForm, Controller } from "react-hook-form";
import { Box, Button, Typography, Modal } from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
import { CustomIconButton, CustomTextField } from "../AddUserModal/styled.css";
import { useDispatch } from "react-redux";
import { addVehicle } from "../../redux/slices/VehicleSlice";
import { autofillFix } from "../../shared-theme/customizations/autoFill";
import { useState } from "react";
export default function AddVehicleModal({ open, handleClose }) {
interface FormData {
name: string;
company: string;
modelName: string;
chargeType: string;
imageFile: File | null;
}
interface AddVehicleModalProps {
open: boolean;
handleClose: () => void;
}
export default function AddVehicleModal({
open,
handleClose,
}: AddVehicleModalProps) {
const {
register,
control,
handleSubmit,
formState: { errors },
reset,
} = useForm();
const dispatch = useDispatch();
setValue,
} = useForm<FormData>({
defaultValues: {
name: "",
company: "",
modelName: "",
chargeType: "",
imageFile: null,
},
});
const dispatch = useDispatch();
const [imagePreview, setImagePreview] = useState<string | null>(null);
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setImagePreview(URL.createObjectURL(file)); // Preview the image
setValue("imageFile", file); // Set the file in the form
}
};
const onSubmit = (data: FormData) => {
if (!data.imageFile) {
console.error("No file selected");
return;
}
const onSubmit = (data: {
name: string;
company: string;
modelName: string;
chargeType: string;
imageFile: File;
}) => {
const { name, company, modelName, chargeType, imageFile } = data;
dispatch(
addVehicle({
name,
company,
modelName,
chargeType,
imageFile: imageFile[0],
name: data.name,
company: data.company,
modelName: data.modelName,
chargeType: data.chargeType,
imageFile: data.imageFile,
})
);
handleClose();
reset();
setImagePreview(null); // Clear image preview
};
return (
@ -47,6 +84,8 @@ export default function AddVehicleModal({ open, handleClose }) {
aria-labelledby="add-vehicle-modal"
>
<Box
component="form"
onSubmit={handleSubmit(onSubmit)}
sx={{
position: "absolute",
top: "50%",
@ -78,190 +117,245 @@ export default function AddVehicleModal({ open, handleClose }) {
{/* 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}>
Company Name
</Typography>
<CustomTextField
fullWidth
placeholder="Enter Company Name"
size="small"
sx={{ marginTop: 1 }}
error={!!errors.company}
helperText={
errors.company
? errors.company.message
: ""
}
{...register("company", {
required: "Company is required",
minLength: {
value: 3,
message:
"Company must be at least 5 characters long",
},
})}
/>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
}}
>
<Typography variant="body2" fontWeight={500}>
Vehicle Name
</Typography>
<CustomTextField
fullWidth
placeholder="Enter Vehicle Name"
size="small"
sx={{ marginTop: 1 }}
error={!!errors.name}
helperText={
errors.name ? errors.name.message : ""
}
{...register("name", {
required: "Vehicle 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:
"Vehicle Name must only contain letters and spaces",
},
})}
/>
</Box>
</Box>
{/* Second Row - Two Inputs */}
<Box sx={{ display: "flex", gap: 2 }}>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
}}
>
<Typography variant="body2" fontWeight={500}>
Model Name
</Typography>
<CustomTextField
fullWidth
placeholder="Enter Model Name"
size="small"
sx={{ marginTop: 1 }}
error={!!errors.modelName}
helperText={
errors.modelName
? errors.modelName.message
: ""
}
{...register("modelName", {
required: "Model Name is required",
})}
/>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
}}
>
<Typography variant="body2" fontWeight={500}>
Charge Type
</Typography>
<CustomTextField
fullWidth
placeholder="Enter Charge Type"
size="small"
sx={{ marginTop: 1 }}
error={!!errors.chargeType}
helperText={
errors.chargeType
? errors.chargeType.message
: ""
}
{...register("chargeType", {
required: "Charge Type is required",
})}
/>
</Box>
</Box>
{/* Image Upload */}
{/* 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%",
height:"100%"
...autofillFix,
}}
>
<Typography variant="body2" fontWeight={500}>
Upload Image
<Typography
variant="body2"
fontWeight={500}
mb={0.5}
>
Vehicle Name
</Typography>
<input
type="file"
accept="image/*"
{...register("imageFile")}
style={{ marginTop: "8px"}}
<Controller
name="name"
control={control}
rules={{
required: "Vehicle 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]+$/,
message:
"Vehicle Name must only contain letters and spaces",
},
}}
render={({ field }) => (
<CustomTextField
{...field}
fullWidth
placeholder="Enter Vehicle Name"
size="small"
sx={{ marginTop: 1 }}
error={!!errors.name}
helperText={errors.name?.message}
/>
)}
/>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
...autofillFix,
}}
>
<Typography
variant="body2"
fontWeight={500}
mb={0.5}
>
Company
</Typography>
<Controller
name="company"
control={control}
rules={{
required: "Company is required",
minLength: {
value: 3,
message:
"Minimum 3 characters required",
},
}}
render={({ field }) => (
<CustomTextField
{...field}
fullWidth
placeholder="Enter Company Name"
size="small"
sx={{ marginTop: 1 }}
error={!!errors.company}
helperText={errors.company?.message}
/>
)}
/>
</Box>
</Box>
{/* Submit Button */}
{/* Second Row - Two Inputs */}
<Box sx={{ display: "flex", gap: 2 }}>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
...autofillFix,
}}
>
<Typography
variant="body2"
fontWeight={500}
mb={0.5}
>
Model Name
</Typography>
<Controller
name="modelName"
control={control}
rules={{ required: "Model Name is required" }}
render={({ field }) => (
<CustomTextField
{...field}
fullWidth
placeholder="Enter Model Name"
size="small"
sx={{ marginTop: 1 }}
error={!!errors.modelName}
helperText={errors.modelName?.message}
/>
)}
/>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
...autofillFix,
}}
>
<Typography
variant="body2"
fontWeight={500}
mb={0.5}
>
Charge Type
</Typography>
<Controller
name="chargeType"
control={control}
rules={{ required: "Charge Type is required" }}
render={({ field }) => (
<CustomTextField
{...field}
fullWidth
placeholder="Enter Charge Type"
size="small"
sx={{ marginTop: 1 }}
error={!!errors.chargeType}
helperText={errors.chargeType?.message}
/>
)}
/>
</Box>
</Box>
{/* Image Upload */}
<Box
sx={{
display: "flex",
justifyContent: "flex-end",
mt: 3,
flexDirection: "column",
width: "100%",
...autofillFix,
}}
>
<Typography variant="body2" fontWeight={500} mb={0.5}>
Upload Image
</Typography>
<Button
type="submit"
component="label"
sx={{
backgroundColor: "#52ACDF",
color: "white",
borderRadius: "8px",
width: "117px",
"&:hover": { backgroundColor: "#439BC1" },
width: "100%",
"&:hover": { backgroundColor: "#52ACDF" },
}}
>
Add Vehicle
Choose Image
<input
type="file"
hidden
accept="image/*"
onChange={handleImageChange}
/>
</Button>
{errors.imageFile && (
<Typography
variant="caption"
color="error"
sx={{ mt: 1 }}
>
{errors.imageFile.message}
</Typography>
)}
{imagePreview && (
<Box sx={{ marginTop: 2 }}>
<Typography variant="body2" mb={1}>
Preview:
</Typography>
<img
src={imagePreview}
alt="image preview"
style={{
maxWidth: "100%",
height: "auto",
borderRadius: "8px",
}}
/>
</Box>
)}
</Box>
</form>
</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 Vehicle
</Button>
</Box>
</Box>
</Modal>
);

View file

@ -41,6 +41,7 @@ import {
fetchAvailableSlots,
} from "../../redux/slices/slotSlice.ts";
import { bookingList, deleteBooking } from "../../redux/slices/bookSlice.ts";
import AddCircleIcon from "@mui/icons-material/AddCircle";
// Styled components for customization
const StyledTableCell = styled(TableCell)(({ theme }) => ({
[`&.${tableCellClasses.head}`]: {
@ -49,7 +50,7 @@ const StyledTableCell = styled(TableCell)(({ theme }) => ({
borderBottom: "none", // Remove any border at the bottom of the header
},
[`&.${tableCellClasses.body}`]: {
fontSize: 14,
fontSize: "16px",
borderBottom: "1px solid #454545", // Adding border to body cells
},
}));
@ -72,6 +73,7 @@ export interface Column {
id: string;
label: string;
align?: "left" | "center" | "right";
}
interface Row {
@ -246,8 +248,8 @@ const CustomTable: React.FC<CustomTableProps> = ({
<Typography
sx={{
color: "#FFFFFF",
fontWeight: 500,
fontSize: "18px",
fontWeight: 600,
fontSize: "30px",
}}
>
{/* Dynamic title based on the page type */}
@ -306,7 +308,7 @@ const CustomTable: React.FC<CustomTableProps> = ({
height: "44px",
borderWidth: "1px",
padding: "14px 12px 14px 12px",
gap: "16px",
"& fieldset": { borderColor: "#FFFFFF" },
"&:hover fieldset": { borderColor: "#FFFFFF" },
"&.Mui-focused fieldset": {
@ -350,12 +352,15 @@ const CustomTable: React.FC<CustomTableProps> = ({
sx={{
backgroundColor: "#52ACDF",
color: "white",
borderRadius: "8px",
width: "184px",
minWidth: "115px", // Start small but allow it to grow
maxWidth: "250px", // Optional: limit it from being *too* wide
marginRight: "16px",
paddingX: "16px",
whiteSpace: "nowrap", // Prevents text from wrapping
"&:hover": { backgroundColor: "#439BC1" },
}}
onClick={() => handleClickOpen()}
onClick={handleClickOpen}
//startIcon={<AddCircleIcon />} // <-- this adds the icon!
>
Add{" "}
{(() => {
@ -450,8 +455,7 @@ const CustomTable: React.FC<CustomTableProps> = ({
backgroundColor: "#272727",
boxShadow:
"-5px 0 5px -2px rgba(0,0,0,0.15)",
borderBottom:
"1px solid #454545",
borderBottom: "1px solid #454545",
}),
}}
>
@ -533,6 +537,7 @@ const CustomTable: React.FC<CustomTableProps> = ({
style={{
width: "50px",
height: "50px",
borderRadius: "50%",
objectFit: "cover",
}}
/>
@ -576,30 +581,42 @@ const CustomTable: React.FC<CustomTableProps> = ({
marginTop: "16px",
}}
>
<Typography
sx={{ color: "white", fontSize: "16px", fontWeight: 500 }}
>
Page Number :
</Typography>
<Pagination
count={Math.ceil(filteredRows.length / usersPerPage)}
page={currentPage}
onChange={handlePageChange}
siblingCount={0}
boundaryCount={0}
sx={{
"& .MuiPaginationItem-root": {
color: "white",
borderRadius: "0px",
},
"& .MuiPaginationItem-page.Mui-selected": {
backgroundColor: "transparent",
fontWeight: "bold",
color: "#FFFFFF",
},
}}
/>
{filteredRows.length > 0 && (
<>
<Typography
sx={{
color: "white",
fontSize: "16px",
fontWeight: 500,
marginRight: "8px", // optional spacing
}}
>
Page Number :
</Typography>
<Pagination
count={Math.ceil(
filteredRows.length / usersPerPage
)}
page={currentPage}
onChange={handlePageChange}
siblingCount={0}
boundaryCount={0}
sx={{
"& .MuiPaginationItem-root": {
color: "white",
borderRadius: "0px",
},
"& .MuiPaginationItem-page.Mui-selected": {
backgroundColor: "transparent",
fontWeight: "bold",
color: "#FFFFFF",
},
}}
/>
</>
)}
</Box>
{/* Menu Actions */}
{open && (
<Menu
@ -624,8 +641,6 @@ const CustomTable: React.FC<CustomTableProps> = ({
onClick={(e) => {
e.stopPropagation();
setViewModal(true);
}}
color="primary"
sx={{
@ -642,7 +657,6 @@ const CustomTable: React.FC<CustomTableProps> = ({
<ViewModal
handleView={() =>
handleViewButton(selectedRow?.id)
}
open={viewModal}
setViewModal={setViewModal}
@ -701,7 +715,7 @@ const CustomTable: React.FC<CustomTableProps> = ({
setModalOpen(true); // Only open if a row is selected
setRowData(selectedRow);
}
handleClose();
handleClose();
}}
color="primary"
sx={{
@ -759,7 +773,7 @@ const CustomTable: React.FC<CustomTableProps> = ({
onClick={(e) => {
e.stopPropagation();
setDeleteModal(true);
handleClose();
handleClose();
}}
color="error"
sx={{

View file

@ -2,7 +2,6 @@ import React, { useEffect, useState } 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 { updateVehicle } from "../../redux/slices/VehicleSlice";
import { CustomIconButton, CustomTextField } from "../AddUserModal/styled.css";
interface EditVehicleModalProps {
@ -49,53 +48,61 @@ const EditVehicleModal: React.FC<EditVehicleModalProps> = ({
},
});
// Set values if editRow is provided
const [imagePreview, setImagePreview] = useState<string | null>(null);
// Set form values and image preview when editRow changes
useEffect(() => {
if (editRow) {
// Construct full image URL
const imageUrl = `${process.env.REACT_APP_BACKEND_URL}/image/${editRow.imageUrl}`;
setImagePreview(imageUrl); // Set the image URL to display the preview
setValue("name", editRow.name);
setValue("company", editRow.company);
setValue("modelName", editRow.modelName);
setValue("chargeType", editRow.chargeType);
// Set form fields
setValue("name", editRow.name || "");
setValue("company", editRow.company || "");
setValue("modelName", editRow.modelName || "");
setValue("chargeType", editRow.chargeType || "");
// Set image preview for existing image
if (editRow?.imageUrl) {
const imageUrl =
editRow.imageUrl.startsWith("http") ||
editRow.imageUrl.startsWith("blob")
? editRow.imageUrl
: `${process.env.REACT_APP_BACKEND_URL}/image/${editRow.imageUrl}`;
setImagePreview(imageUrl);
} else {
setImagePreview(null);
}
} else {
// Reset form and preview when no editRow
reset();
setImagePreview(null);
}
}, [editRow, setValue, reset]);
// Handle image upload
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setImagePreview(URL.createObjectURL(file)); // Show preview of new image
setValue("imageUrl", file); // Update form with new file
}
};
// Handle form submission
const onSubmit = (data: FormData) => {
handleUpdate(
editRow.id,
data.name,
data.company,
data.modelName,
data.chargeType,
data.imageUrl // Pass File | null to handleUpdate
);
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setImagePreview(URL.createObjectURL(file)); // Preview the image
setValue("imageUrl", file); // Set the file in the form
}
};
const onSubmit = (data: FormData) => {
// Check if a new image was selected and set its URL or filename
const imageUrl = data.imageUrl
? `${process.env.REACT_APP_BACKEND_URL}/image/${data.imageUrl.name}` // Assuming image is a file object
: editRow?.imageUrl; // Keep existing image if not changed
handleUpdate(
editRow.id,
data.name,
data.company,
data.modelName,
data.chargeType,
imageUrl // Send the updated image URL or filename to backend
);
handleClose();
reset();
};
handleClose();
reset();
setImagePreview(null); // Clear preview after submission
};
return (
<Modal
@ -104,7 +111,7 @@ const onSubmit = (data: FormData) => {
if (reason === "backdropClick") {
return;
}
handleClose(); // Close modal when clicking cross or cancel
handleClose();
}}
aria-labelledby="edit-vehicle-modal"
>
@ -176,7 +183,7 @@ const onSubmit = (data: FormData) => {
"Maximum 30 characters allowed",
},
pattern: {
value: /^[A-Za-z\s]+$/, // Only letters and spaces are allowed
value: /^[A-Za-z\s]+$/,
message:
"Vehicle Name must only contain letters and spaces",
},
@ -307,17 +314,17 @@ const onSubmit = (data: FormData) => {
Upload Image
</Typography>
<Button
variant="contained"
component="label"
sx={{
backgroundColor: "#52ACDF",
color: "white",
borderRadius: "8px",
width: "100%",
"&:hover": { backgroundColor: "#439BC1" },
"&:hover": { backgroundColor: "#52ACDF" },
}}
>
Upload Image
Choose Image
<input
type="file"
hidden
@ -328,14 +335,17 @@ const onSubmit = (data: FormData) => {
{imagePreview && (
<Box sx={{ marginTop: 2 }}>
<Typography variant="body2" mb={1}>
Preview:
Preview (
{imagePreview.startsWith("blob")
? "New"
: "Existing"}
):
</Typography>
<img
src={imagePreview}
alt="image preview"
alt="Vehicle"
style={{
maxWidth: "100%",
height: "auto",
borderRadius: "8px",
}}
/>

View file

@ -52,7 +52,7 @@ export default function MainGrid() {
}}
>
{/* Dashboard Header */}
<Typography component="h2" variant="h6" sx={{ mb: 2 }}>
<Typography component="h2" variant="h6" sx={{ mb: 2 ,fontSize:"30px", fontWeight:"600"}}>
Dashboard
</Typography>

View file

@ -105,7 +105,7 @@ export default function MenuContent({ hidden }: PropType) {
<Stack
sx={{
flexGrow: 1,
p: 1,
// p: 1,
justifyContent: "space-between",
backgroundColor: "#202020",
}}

View file

@ -113,13 +113,36 @@ export default function VehicleViewModal({ open, setViewModal, id }: Props) {
</Typography>
</Typography>
</Grid>
<Grid item xs={6}>
<Typography variant="body1">
<strong>Image URL:</strong>
<Typography variant="body2">
{selectedVehicle.imageUrl ?? "N/A"}
</Typography>
<Grid item xs={12}>
<Typography variant="body1" gutterBottom>
<strong>Image:</strong>
</Typography>
{selectedVehicle.imageUrl ? (
<img
src={
selectedVehicle.imageUrl.startsWith(
"http"
)
? selectedVehicle.imageUrl
: `${process.env.REACT_APP_BACKEND_URL}/image/${selectedVehicle.imageUrl}`
}
alt="Vehicle"
style={{
width: "100%",
maxHeight: "auto",
objectFit: "cover",
borderRadius: "8px",
}}
onError={(e) => {
e.currentTarget.src =
"/placeholder-image.png"; // fallback image
}}
/>
) : (
<Typography variant="body2">
No image available
</Typography>
)}
</Grid>
</Grid>
) : (

View file

@ -13,13 +13,15 @@ import { AppDispatch, RootState } from "../../redux/store/store";
import { Button } from "@mui/material";
import { fetchAdminProfile } from "../../redux/slices/profileSlice";
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
const drawerWidth = 240;
const Drawer = styled(MuiDrawer)({
width: drawerWidth,
flexShrink: 0,
boxSizing: "border-box",
mt: 10,
[`& .${drawerClasses.paper}`]: {
width: drawerWidth,
boxSizing: "border-box",
@ -37,45 +39,45 @@ export default function SideMenu() {
// }, [dispatch]);
return (
<Drawer
open={open}
variant="permanent"
anchor="left"
sx={{
display: {
xs: "none",
md: "block",
width: open ? 250 : 80,
transition: "all 0.5s ease",
},
[`& .${drawerClasses.paper}`]: {
backgroundColor: "background.paper",
width: open ? 250 : 80,
transition: "all 0.5s ease",
},
}}
>
<Box
<Box sx={{ position: "relative" }}>
<Drawer
open={open}
variant="permanent"
anchor="left"
sx={{
display: "flex",
flexDirection: "row",
justifyContent:"center",
alignItems: "center",
pt: 2,
display: {
xs: "none",
md: "block",
width: open ? 250 : 80,
transition: "all 0.5s ease",
},
[`& .${drawerClasses.paper}`]: {
backgroundColor: "background.paper",
width: open ? 250 : 80,
transition: "all 0.5s ease",
},
}}
>
<img
src="/evLogo.png"
alt="Logo"
style={{
<Box
sx={{
display: "flex",
flexDirection: "row",
justifyContent: "center",
width: open ? "120px" : "60px", // Adjust width depending on open state
height: "auto",
transition: "width 0.5s ease", // Smooth transition for width change
alignItems: "center",
pt: 2,
}}
/>
{/* <Avatar
>
<img
src="/evLogo.png"
alt="Logo"
style={{
justifyContent: "center",
width: open ? "120px" : "60px", // Adjust width depending on open state
height: "auto",
transition: "width 0.5s ease", // Smooth transition for width change
}}
/>
{/* <Avatar
alt="Logo"
src="/evLogo.png"
sx={{
@ -83,7 +85,7 @@ export default function SideMenu() {
height: "100%",
}}
/> */}
{/* <Box
{/* <Box
sx={{
display: open ? "flex" : "none",
flexDirection: "column",
@ -107,22 +109,45 @@ export default function SideMenu() {
{user?.userType || "N/A"}
</Typography>
</Box> */}
</Box>
</Box>
<Box
<Box
sx={{
display: "flex",
justifyContent: open ? "flex-end" : "center",
alignItems: "center",
pt: 1.5,
textAlign: "center",
}}
>
{/* <Button variant="text" onClick={() => setOpen(!open)}>
{open ? <ArrowLeftIcon /> : <ArrowRightIcon />}
</Button> */}
</Box>
<MenuContent hidden={open} />
</Drawer>
<Button
onClick={() => setOpen(!open)}
sx={{
display: "flex",
justifyContent: open ? "flex-end" : "center",
alignItems: "center",
pt: 1.5,
textAlign: "center",
position: "absolute",
top: 20,
left: open ? 250 : 80,
minWidth: 0,
width: 32,
height: 35,
borderRadius: "50%",
zIndex: 1301,
// boxShadow: 2,
}}
>
<Button variant="text" onClick={() => setOpen(!open)}>
{open ? <ArrowLeftIcon /> : <ArrowRightIcon />}
</Button>
</Box>
<MenuContent hidden={open} />
</Drawer>
{open ? (
<ChevronLeftIcon fontSize="small" />
) : (
<ChevronRightIcon fontSize="small" />
)}
</Button>
</Box>
);
}

View file

@ -9,8 +9,10 @@ import {
updateSlot,
} from "../../redux/slices/slotSlice";
import AddSlotModal from "../../components/AddSlotModal/addSlotModal";
import dayjs from "dayjs";
import EditSlotModal from "../../components/EditSlotModal/editSlotModal";
import dayjs from "dayjs";
import isBetween from "dayjs/plugin/isBetween";
export default function EVSlotList() {
const [addModalOpen, setAddModalOpen] = useState<boolean>(false);
const [editModalOpen, setEditModalOpen] = useState<boolean>(false);
@ -21,8 +23,9 @@ export default function EVSlotList() {
const dispatch = useDispatch<AppDispatch>();
const availableSlots = useSelector(
(state: RootState) => state?.slotReducer.availableSlots
);
const { user } = useSelector((state: RootState) => state?.profileReducer);
);
const { user } = useSelector((state: RootState) => state?.profileReducer);
useEffect(() => {
dispatch(fetchManagersSlots());
}, [dispatch]);
@ -31,6 +34,7 @@ export default function EVSlotList() {
setRowData(null);
setAddModalOpen(true);
};
const handleCloseModal = () => {
setAddModalOpen(false);
setEditModalOpen(false);
@ -38,6 +42,8 @@ export default function EVSlotList() {
reset();
};
dayjs.extend(isBetween);
// In handleAddSlot method
const handleAddSlot = async (data: {
date: string;
startTime: string;
@ -45,14 +51,69 @@ export default function EVSlotList() {
isAvailable: boolean;
}) => {
try {
await dispatch(createSlot(data));
// Parse start and end time from the backend format using dayjs
const startTime = dayjs(data.startTime, "MM/DD/YYYY, h:mm:ss A");
const endTime = dayjs(data.endTime, "MM/DD/YYYY, h:mm:ss A");
// Check for overlap with existing slots
const conflict = availableSlots.find((slot) => {
const slotStartTime = dayjs(
slot.startTime,
"MM/DD/YYYY, h:mm:ss A"
);
const slotEndTime = dayjs(
slot.endTime,
"MM/DD/YYYY, h:mm:ss A"
);
return (
slot.date === data.date &&
(startTime.isBetween(
slotStartTime,
slotEndTime,
null,
"[)"
) ||
endTime.isBetween(
slotStartTime,
slotEndTime,
null,
"(]"
))
);
});
if (conflict) {
alert(
"There is an overlapping slot. Please choose another time."
);
return;
}
// Create the slot with duration
const duration = endTime.diff(startTime, "minute");
const payload = {
date: data.date,
startHour: startTime.format("hh:mm A"), // Ensure formatting is consistent
endHour: endTime.format("hh:mm A"), // Ensure formatting is consistent
isAvailable: data.isAvailable,
duration: duration,
};
// Dispatch action to create slot and refresh available slots
await dispatch(createSlot(payload));
await dispatch(fetchManagersSlots());
// Close the modal after successful slot creation
handleCloseModal();
} catch (error) {
console.error("Error adding slot", error);
}
};
// In handleUpdate method
const handleUpdate = async (
id: string,
startTime: string,
@ -60,11 +121,19 @@ export default function EVSlotList() {
isAvailable: boolean
) => {
try {
const formattedStartTime = dayjs(startTime, "HH:mm").format(
"HH:mm"
);
const formattedEndTime = dayjs(endTime, "HH:mm").format("HH:mm");
// Convert times using dayjs
const formattedStartTime = dayjs(
startTime,
"MM/DD/YYYY, h:mm:ss A"
).format("hh:mm A");
const formattedEndTime = dayjs(
endTime,
"MM/DD/YYYY, h:mm:ss A"
).format("hh:mm A");
// Dispatch the update action
await dispatch(
updateSlot({
id,
@ -74,17 +143,19 @@ export default function EVSlotList() {
})
).unwrap();
// Fetch updated slot data
await dispatch(fetchManagersSlots());
// Close modal after successful update
handleCloseModal();
} catch (error) {
console.error("Update failed", error);
}
};
const slotColumns: Column[] = [
{ id: "srno", label: "Sr No" },
{ id: "stationName", label: "Station Name" },
{ id: "stationId", label: "Station Id" },
{ id: "date", label: "Date" },
{ id: "startTime", label: "Start Time" },
{ id: "endTime", label: "End Time" },
@ -94,24 +165,32 @@ export default function EVSlotList() {
: []),
];
const slotRows = availableSlots?.length
? availableSlots.map((slot, index) => {
const formattedDate = dayjs(slot?.date).format("YYYY-MM-DD");
const startTime = dayjs(slot?.startTime).format("HH:mm");
const endTime = dayjs(slot?.endTime).format("HH:mm");
const slotRows = availableSlots?.length
? availableSlots.map((slot, index) => {
const formattedDate = dayjs(slot?.date).format("YYYY-MM-DD");
const startTime = dayjs(
slot?.startTime,
"YYYY-MM-DD hh:mm A"
).isValid()
? dayjs(slot?.startTime, "YYYY-MM-DD hh:mm A").format("hh:mm A")
: "Invalid";
const endTime = dayjs(slot?.endTime, "YYYY-MM-DD hh:mm A").isValid()
? dayjs(slot?.endTime, "YYYY-MM-DD hh:mm A").format("hh:mm A")
: "Invalid";
return {
srno: index + 1,
id: slot?.id ?? "NA",
stationName: slot?.stationName ?? "NA",
date: formattedDate ?? "NA",
startTime: startTime ?? "NA",
endTime: endTime ?? "NA",
isAvailable: slot?.isAvailable ? "Yes" : "No",
};
})
: [];
return {
srno: index + 1,
id: slot?.id ?? "NA",
stationId: slot?.stationId ?? "NA",
stationName: slot?.stationName?? "NA",
date: formattedDate ?? "NA",
startTime: startTime ?? "NA",
endTime: endTime ?? "NA",
isAvailable: slot?.isAvailable ? "Yes" : "No",
};
})
: [];
return (
<>

View file

@ -15,8 +15,8 @@ import EditVehicleModal from "../../components/EditVehicleModal/editVehicleModal
export default function VehicleList() {
const [addModalOpen, setAddModalOpen] = useState<boolean>(false);
const [editModalOpen, setEditModalOpen] = useState<boolean>(false);
const [isAdding, setIsAdding] = useState<boolean>(false);
const { reset } = useForm();
const [deleteModal, setDeleteModal] = useState<boolean>(false);
const [viewModal, setViewModal] = useState<boolean>(false);
const [rowData, setRowData] = useState<any | null>(null);
@ -29,6 +29,8 @@ export default function VehicleList() {
dispatch(vehicleList());
}, [dispatch]);
console.log("Backend URL:", process.env.REACT_APP_BACKEND_URL);
const handleClickOpen = () => {
setRowData(null);
setAddModalOpen(true);
@ -49,53 +51,50 @@ export default function VehicleList() {
imageFile: File;
}) => {
try {
setIsAdding(true);
const response = await dispatch(addVehicle(data));
console.log("Added vehicle response: ", response); // Check if the image URL is included in the response
await dispatch(vehicleList());
console.log("Added vehicle response: ", response);
handleCloseModal();
} catch (error) {
console.error("Error adding vehicle", error);
} finally {
setIsAdding(false);
}
};
const handleUpdate = async (
id: number,
name: string,
company: string,
modelName: string,
chargeType: string,
imageUrl: string
) => {
try {
await dispatch(
updateVehicle({
id,
name,
company,
modelName,
chargeType,
imageUrl,
})
);
await dispatch(vehicleList());
handleCloseModal();
} catch (error) {
console.error("Update failed", error);
}
};
const handleUpdate = (
id: string,
name: string,
company: string,
modelName: string,
chargeType: string,
imageUrl: File | null
) => {
dispatch(
updateVehicle({
id,
name,
company,
modelName,
chargeType,
imageUrl, // File or null
})
);
};
const categoryColumns: Column[] = [
{ id: "srno", label: "Sr No" },
{ id: "imageUrl", label: "Image" },
{ id: "name", label: "Vehicle Name" },
{ id: "company", label: "Company" },
{ id: "modelName", label: "Model Name" },
{ id: "chargeType", label: "Charge Type" },
{ id: "imageUrl", label: "Image" },
{ id: "action", label: "Action", align: "center" },
];
console.log(
`${process.env.REACT_APP_BACKEND_URL}/image/${vehicles[0]?.imageUrl}`
);
const categoryRows = vehicles?.length
? vehicles?.map(
@ -109,19 +108,32 @@ export default function VehicleList() {
imageUrl: string;
},
index: number
) => ({
id: vehicle?.id,
srno: index + 1,
name: vehicle?.name,
company: vehicle?.company,
modelName: vehicle?.modelName,
chargeType: vehicle?.chargeType,
imageUrl: `${process.env.REACT_APP_BACKEND_URL}/image/${vehicle?.imageUrl}`,
})
) => {
const imageUrl = vehicle?.imageUrl
? `${process.env.REACT_APP_BACKEND_URL}/image/${vehicle?.imageUrl}`
: "/images/fallback.jpg";
console.log(
"Vehicle:",
vehicle.name,
"Image URL:",
imageUrl
);
return {
id: vehicle?.id,
srno: index + 1,
name: vehicle?.name,
company: vehicle?.company,
modelName: vehicle?.modelName,
chargeType: vehicle?.chargeType,
imageUrl,
};
}
)
: [];
return (
<>
{isAdding ? <p>Adding vehicle...</p> : null}
<CustomTable
columns={categoryColumns}
rows={categoryRows}

View file

@ -96,13 +96,14 @@ export const addVehicle = createAsyncThunk<
formData.append("modelName", modelName);
formData.append("chargeType", chargeType);
formData.append("image", imageFile);
const response = await http.post("create-vehicle", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});
return response.data;
console.log("first", imageFile);
return response.data.data;
} catch (error: any) {
return rejectWithValue(
error.response?.data?.message || "An error occurred"
@ -111,25 +112,56 @@ export const addVehicle = createAsyncThunk<
});
// Update Vehicle details
export const updateVehicle = createAsyncThunk(
"updateVehicle",
async ({ id, ...vehicleData }: Vehicle, { rejectWithValue }) => {
try {
const response = await http.patch(
`/update-vehicle/${id}`,
vehicleData
);
toast.success("Vehicle Details 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 updateVehicle = createAsyncThunk<
Vehicle,
{
id: string | number;
name: string;
company: string;
modelName: string;
chargeType: string;
imageUrl: File | string | null;
},
{ rejectValue: string }
>(
"updateVehicle",
async (
{ id, name, company, modelName, chargeType, imageUrl },
{ rejectWithValue }
) => {
try {
const formData = new FormData();
formData.append("name", name);
formData.append("company", company);
formData.append("modelName", modelName);
formData.append("chargeType", chargeType);
if (imageUrl instanceof File) {
formData.append("image", imageUrl); // Append new file
}
const response = await http.patch(
`/update-vehicle/${id}`,
formData,
{
headers: {
"Content-Type": "multipart/form-data",
},
}
);
toast.success("Vehicle Details updated successfully");
return response.data.data;
} catch (error: any) {
toast.error("Error updating the vehicle: " + error);
return rejectWithValue(
error.response?.data?.message || "An error occurred"
);
}
}
);
export const deleteVehicle = createAsyncThunk<
string,
string,

View file

@ -1,16 +1,20 @@
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
import http from "../../lib/https"; // Assuming you have a custom HTTP library for requests
import { toast } from "sonner";
import dayjs from "dayjs";
// Define TypeScript types
interface Slot {
id: string;
stationId: string;
date: string;
date?: string;
startTime: string;
startingDate?: string;
endingDate?: string;
endTime: string;
duration: number;
isAvailable: boolean;
stationName:string;
stationName: string;
ChargingStation: { name: string };
}
@ -72,33 +76,31 @@ export const fetchManagersSlots = createAsyncThunk<
}
});
export const createSlot = createAsyncThunk<
Slot,
Slot[], // <-- updated from Slot to Slot[]
{
date: string;
startTime: string;
endTime: string;
date?: string;
startingDate?: string;
endingDate?: string;
startHour: string;
endHour: string;
isAvailable: boolean;
duration: number;
stationId:number;
},
{ rejectValue: string }
>("slots/createSlot", async (payload, { rejectWithValue }) => {
try {
// const payload = {
// date,
// startHour,
// endHour,
// isAvailable,
// };
const token = localStorage?.getItem("authToken");
if (!token) throw new Error("No token found");
const response = await http.post("/create-slot", payload);
toast.success("Slot created successfully");
return response.data.data;
} catch (error: any) {
// Show error message
toast.error("Error creating slot: " + error?.message);
// Return a detailed error message if possible
// Make the API call to create the slots
const response = await http.post("create-slot", payload);
toast.success("Slot(s) created successfully");
return response.data.data; // Return the array of created slots
} catch (error: any) {
toast.error("Error creating slot: " + error?.message);
return rejectWithValue(
error.response?.data?.message || "An error occurred"
);
@ -191,13 +193,39 @@ const slotSlice = createSlice({
.addCase(createSlot.pending, (state) => {
state.loading = true;
})
// .addCase(
// createSlot.fulfilled,
// (state, action: PayloadAction<Slot[]>) => {
// state.loading = false;
// // Add the new slots to both arrays
// state.slots.push(...action.payload);
// state.availableSlots.push(...action.payload);
// }
// )
.addCase(
createSlot.fulfilled,
(state, action: PayloadAction<Slot>) => {
(state, action: PayloadAction<Slot[]>) => {
state.loading = false;
state.slots.push(action.payload);
const normalizedSlots = action.payload.map((slot) => {
const combinedStart = `${slot.date} ${slot.startTime}`; // Keep raw for now
const combinedEnd = `${slot.date} ${slot.endTime}`;
return {
...slot,
startTime: combinedStart,
endTime: combinedEnd,
};
});
console.log("Normalized Slots →", normalizedSlots); // Check this in console
state.slots.push(...normalizedSlots);
state.availableSlots.push(...normalizedSlots);
}
)
.addCase(createSlot.rejected, (state, action) => {
state.loading = false;
state.error = action.payload || "Failed to create slot";