dev-jaanvi #1
|
@ -164,8 +164,10 @@ import {
|
||||||
InputLabel,
|
InputLabel,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useForm } from "react-hook-form";
|
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 {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
@ -173,6 +175,8 @@ const AddSlotModal = ({ open, handleClose, handleAddSlot }: any) => {
|
||||||
watch,
|
watch,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm();
|
} = useForm();
|
||||||
|
const dispatch = useDispatch(); // Get dispatch from Redux
|
||||||
|
|
||||||
const [isAvailable, setIsAvailable] = useState<boolean>(true);
|
const [isAvailable, setIsAvailable] = useState<boolean>(true);
|
||||||
const [isDateRange, setIsDateRange] = useState<boolean>(false);
|
const [isDateRange, setIsDateRange] = useState<boolean>(false);
|
||||||
const [durationUnit, setDurationUnit] = useState<string>("minutes");
|
const [durationUnit, setDurationUnit] = useState<string>("minutes");
|
||||||
|
@ -190,60 +194,41 @@ const AddSlotModal = ({ open, handleClose, handleAddSlot }: any) => {
|
||||||
}
|
}
|
||||||
}, [startHour]);
|
}, [startHour]);
|
||||||
|
|
||||||
const onSubmit = (data: any) => {
|
const onSubmit = (data: any) => {
|
||||||
const { date, startDate, endDate, startHour, endHour, duration } = data;
|
const {
|
||||||
const slots: { date: string; startHour: string; endHour: string; isAvailable: boolean; duration: number; }[] = [];
|
date,
|
||||||
|
startingDate,
|
||||||
|
endingDate,
|
||||||
|
startHour,
|
||||||
|
endHour,
|
||||||
|
duration,
|
||||||
|
|
||||||
|
} = data;
|
||||||
|
|
||||||
const generateSlotsForDate = (date: string) => {
|
const payload = isDateRange
|
||||||
const startTime = new Date(`1970-01-01T${startHour}:00`);
|
? {
|
||||||
const endTime = new Date(`1970-01-01T${endHour}:00`);
|
startingDate,
|
||||||
let durationMinutes = parseInt(duration, 10);
|
endingDate,
|
||||||
|
startHour,
|
||||||
|
endHour,
|
||||||
|
duration: parseInt(duration, 10),
|
||||||
|
isAvailable,
|
||||||
|
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
date,
|
||||||
|
startHour,
|
||||||
|
endHour,
|
||||||
|
duration: parseInt(duration, 10),
|
||||||
|
isAvailable,
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
if (durationUnit === "hours") {
|
dispatch(createSlot(payload));
|
||||||
durationMinutes *= 60;
|
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 (
|
return (
|
||||||
<Dialog open={open} onClose={handleClose}>
|
<Dialog open={open} onClose={handleClose}>
|
||||||
|
@ -265,7 +250,7 @@ const AddSlotModal = ({ open, handleClose, handleAddSlot }: any) => {
|
||||||
Start Date
|
Start Date
|
||||||
</Typography>
|
</Typography>
|
||||||
<TextField
|
<TextField
|
||||||
{...register("startDate", {
|
{...register("startingDate", {
|
||||||
required: "Start date is required",
|
required: "Start date is required",
|
||||||
validate: (value) =>
|
validate: (value) =>
|
||||||
value >= today ||
|
value >= today ||
|
||||||
|
@ -274,15 +259,15 @@ const AddSlotModal = ({ open, handleClose, handleAddSlot }: any) => {
|
||||||
type="date"
|
type="date"
|
||||||
fullWidth
|
fullWidth
|
||||||
margin="normal"
|
margin="normal"
|
||||||
error={!!errors.startDate}
|
error={!!errors.startingDate}
|
||||||
helperText={errors.startDate?.message}
|
helperText={errors.startingDate?.message}
|
||||||
inputProps={{ min: today }}
|
inputProps={{ min: today }}
|
||||||
/>
|
/>
|
||||||
<Typography variant="body2" fontWeight={500}>
|
<Typography variant="body2" fontWeight={500}>
|
||||||
End Date
|
End Date
|
||||||
</Typography>
|
</Typography>
|
||||||
<TextField
|
<TextField
|
||||||
{...register("endDate", {
|
{...register("endingDate", {
|
||||||
required: "End date is required",
|
required: "End date is required",
|
||||||
validate: (value) =>
|
validate: (value) =>
|
||||||
value >= today ||
|
value >= today ||
|
||||||
|
@ -291,8 +276,8 @@ const AddSlotModal = ({ open, handleClose, handleAddSlot }: any) => {
|
||||||
type="date"
|
type="date"
|
||||||
fullWidth
|
fullWidth
|
||||||
margin="normal"
|
margin="normal"
|
||||||
error={!!errors.endDate}
|
error={!!errors.endingDate}
|
||||||
helperText={errors.endDate?.message}
|
helperText={errors.endingDate?.message}
|
||||||
inputProps={{ min: today }}
|
inputProps={{ min: today }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
@ -366,13 +351,13 @@ const AddSlotModal = ({ open, handleClose, handleAddSlot }: any) => {
|
||||||
helperText={errors.duration?.message}
|
helperText={errors.duration?.message}
|
||||||
/>
|
/>
|
||||||
<FormControl fullWidth>
|
<FormControl fullWidth>
|
||||||
<InputLabel>Unit</InputLabel>
|
|
||||||
<Select
|
<Select
|
||||||
value={durationUnit}
|
value={durationUnit}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setDurationUnit(e.target.value)
|
setDurationUnit(e.target.value)
|
||||||
}
|
}
|
||||||
label="Unit"
|
|
||||||
>
|
>
|
||||||
<MenuItem value="minutes">Minutes</MenuItem>
|
<MenuItem value="minutes">Minutes</MenuItem>
|
||||||
<MenuItem value="hours">Hours</MenuItem>
|
<MenuItem value="hours">Hours</MenuItem>
|
||||||
|
@ -421,5 +406,3 @@ const AddSlotModal = ({ open, handleClose, handleAddSlot }: any) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AddSlotModal;
|
export default AddSlotModal;
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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 { Box, Button, Typography, Modal } from "@mui/material";
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
import { CustomIconButton, CustomTextField } from "../AddUserModal/styled.css";
|
import { CustomIconButton, CustomTextField } from "../AddUserModal/styled.css";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
import { addVehicle } from "../../redux/slices/VehicleSlice";
|
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 {
|
const {
|
||||||
register,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
reset,
|
reset,
|
||||||
} = useForm();
|
setValue,
|
||||||
const dispatch = useDispatch();
|
} = 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(
|
dispatch(
|
||||||
addVehicle({
|
addVehicle({
|
||||||
name,
|
name: data.name,
|
||||||
company,
|
company: data.company,
|
||||||
modelName,
|
modelName: data.modelName,
|
||||||
chargeType,
|
chargeType: data.chargeType,
|
||||||
imageFile: imageFile[0],
|
imageFile: data.imageFile,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
handleClose();
|
handleClose();
|
||||||
reset();
|
reset();
|
||||||
|
setImagePreview(null); // Clear image preview
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -47,6 +84,8 @@ export default function AddVehicleModal({ open, handleClose }) {
|
||||||
aria-labelledby="add-vehicle-modal"
|
aria-labelledby="add-vehicle-modal"
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
|
component="form"
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
sx={{
|
sx={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: "50%",
|
top: "50%",
|
||||||
|
@ -78,190 +117,245 @@ export default function AddVehicleModal({ open, handleClose }) {
|
||||||
{/* Horizontal Line */}
|
{/* Horizontal Line */}
|
||||||
<Box sx={{ borderBottom: "1px solid #ddd", my: 2 }} />
|
<Box sx={{ borderBottom: "1px solid #ddd", my: 2 }} />
|
||||||
|
|
||||||
{/* Form */}
|
{/* Input Fields */}
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||||
{/* Input Fields */}
|
{/* First Row - Two Inputs */}
|
||||||
<Box
|
<Box sx={{ display: "flex", gap: 2 }}>
|
||||||
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 */}
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height:"100%"
|
...autofillFix,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="body2" fontWeight={500}>
|
<Typography
|
||||||
Upload Image
|
variant="body2"
|
||||||
|
fontWeight={500}
|
||||||
|
mb={0.5}
|
||||||
|
>
|
||||||
|
Vehicle Name
|
||||||
</Typography>
|
</Typography>
|
||||||
<input
|
<Controller
|
||||||
type="file"
|
name="name"
|
||||||
accept="image/*"
|
control={control}
|
||||||
{...register("imageFile")}
|
rules={{
|
||||||
style={{ marginTop: "8px"}}
|
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>
|
||||||
</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
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "flex-end",
|
flexDirection: "column",
|
||||||
mt: 3,
|
width: "100%",
|
||||||
|
...autofillFix,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Typography variant="body2" fontWeight={500} mb={0.5}>
|
||||||
|
Upload Image
|
||||||
|
</Typography>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
component="label"
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: "#52ACDF",
|
backgroundColor: "#52ACDF",
|
||||||
color: "white",
|
color: "white",
|
||||||
borderRadius: "8px",
|
borderRadius: "8px",
|
||||||
width: "117px",
|
width: "100%",
|
||||||
"&:hover": { backgroundColor: "#439BC1" },
|
"&:hover": { backgroundColor: "#52ACDF" },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add Vehicle
|
Choose Image
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
hidden
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleImageChange}
|
||||||
|
/>
|
||||||
</Button>
|
</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>
|
</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>
|
</Box>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
|
@ -41,6 +41,7 @@ import {
|
||||||
fetchAvailableSlots,
|
fetchAvailableSlots,
|
||||||
} from "../../redux/slices/slotSlice.ts";
|
} from "../../redux/slices/slotSlice.ts";
|
||||||
import { bookingList, deleteBooking } from "../../redux/slices/bookSlice.ts";
|
import { bookingList, deleteBooking } from "../../redux/slices/bookSlice.ts";
|
||||||
|
import AddCircleIcon from "@mui/icons-material/AddCircle";
|
||||||
// Styled components for customization
|
// Styled components for customization
|
||||||
const StyledTableCell = styled(TableCell)(({ theme }) => ({
|
const StyledTableCell = styled(TableCell)(({ theme }) => ({
|
||||||
[`&.${tableCellClasses.head}`]: {
|
[`&.${tableCellClasses.head}`]: {
|
||||||
|
@ -49,7 +50,7 @@ const StyledTableCell = styled(TableCell)(({ theme }) => ({
|
||||||
borderBottom: "none", // Remove any border at the bottom of the header
|
borderBottom: "none", // Remove any border at the bottom of the header
|
||||||
},
|
},
|
||||||
[`&.${tableCellClasses.body}`]: {
|
[`&.${tableCellClasses.body}`]: {
|
||||||
fontSize: 14,
|
fontSize: "16px",
|
||||||
borderBottom: "1px solid #454545", // Adding border to body cells
|
borderBottom: "1px solid #454545", // Adding border to body cells
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
@ -72,6 +73,7 @@ export interface Column {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
align?: "left" | "center" | "right";
|
align?: "left" | "center" | "right";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Row {
|
interface Row {
|
||||||
|
@ -246,8 +248,8 @@ const CustomTable: React.FC<CustomTableProps> = ({
|
||||||
<Typography
|
<Typography
|
||||||
sx={{
|
sx={{
|
||||||
color: "#FFFFFF",
|
color: "#FFFFFF",
|
||||||
fontWeight: 500,
|
fontWeight: 600,
|
||||||
fontSize: "18px",
|
fontSize: "30px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Dynamic title based on the page type */}
|
{/* Dynamic title based on the page type */}
|
||||||
|
@ -306,7 +308,7 @@ const CustomTable: React.FC<CustomTableProps> = ({
|
||||||
height: "44px",
|
height: "44px",
|
||||||
borderWidth: "1px",
|
borderWidth: "1px",
|
||||||
padding: "14px 12px 14px 12px",
|
padding: "14px 12px 14px 12px",
|
||||||
gap: "16px",
|
|
||||||
"& fieldset": { borderColor: "#FFFFFF" },
|
"& fieldset": { borderColor: "#FFFFFF" },
|
||||||
"&:hover fieldset": { borderColor: "#FFFFFF" },
|
"&:hover fieldset": { borderColor: "#FFFFFF" },
|
||||||
"&.Mui-focused fieldset": {
|
"&.Mui-focused fieldset": {
|
||||||
|
@ -350,12 +352,15 @@ const CustomTable: React.FC<CustomTableProps> = ({
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: "#52ACDF",
|
backgroundColor: "#52ACDF",
|
||||||
color: "white",
|
color: "white",
|
||||||
borderRadius: "8px",
|
minWidth: "115px", // Start small but allow it to grow
|
||||||
width: "184px",
|
maxWidth: "250px", // Optional: limit it from being *too* wide
|
||||||
marginRight: "16px",
|
marginRight: "16px",
|
||||||
|
paddingX: "16px",
|
||||||
|
whiteSpace: "nowrap", // Prevents text from wrapping
|
||||||
"&:hover": { backgroundColor: "#439BC1" },
|
"&:hover": { backgroundColor: "#439BC1" },
|
||||||
}}
|
}}
|
||||||
onClick={() => handleClickOpen()}
|
onClick={handleClickOpen}
|
||||||
|
//startIcon={<AddCircleIcon />} // <-- this adds the icon!
|
||||||
>
|
>
|
||||||
Add{" "}
|
Add{" "}
|
||||||
{(() => {
|
{(() => {
|
||||||
|
@ -450,8 +455,7 @@ const CustomTable: React.FC<CustomTableProps> = ({
|
||||||
backgroundColor: "#272727",
|
backgroundColor: "#272727",
|
||||||
boxShadow:
|
boxShadow:
|
||||||
"-5px 0 5px -2px rgba(0,0,0,0.15)",
|
"-5px 0 5px -2px rgba(0,0,0,0.15)",
|
||||||
borderBottom:
|
borderBottom: "1px solid #454545",
|
||||||
"1px solid #454545",
|
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -533,6 +537,7 @@ const CustomTable: React.FC<CustomTableProps> = ({
|
||||||
style={{
|
style={{
|
||||||
width: "50px",
|
width: "50px",
|
||||||
height: "50px",
|
height: "50px",
|
||||||
|
borderRadius: "50%",
|
||||||
objectFit: "cover",
|
objectFit: "cover",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -576,30 +581,42 @@ const CustomTable: React.FC<CustomTableProps> = ({
|
||||||
marginTop: "16px",
|
marginTop: "16px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography
|
{filteredRows.length > 0 && (
|
||||||
sx={{ color: "white", fontSize: "16px", fontWeight: 500 }}
|
<>
|
||||||
>
|
<Typography
|
||||||
Page Number :
|
sx={{
|
||||||
</Typography>
|
color: "white",
|
||||||
<Pagination
|
fontSize: "16px",
|
||||||
count={Math.ceil(filteredRows.length / usersPerPage)}
|
fontWeight: 500,
|
||||||
page={currentPage}
|
marginRight: "8px", // optional spacing
|
||||||
onChange={handlePageChange}
|
}}
|
||||||
siblingCount={0}
|
>
|
||||||
boundaryCount={0}
|
Page Number :
|
||||||
sx={{
|
</Typography>
|
||||||
"& .MuiPaginationItem-root": {
|
<Pagination
|
||||||
color: "white",
|
count={Math.ceil(
|
||||||
borderRadius: "0px",
|
filteredRows.length / usersPerPage
|
||||||
},
|
)}
|
||||||
"& .MuiPaginationItem-page.Mui-selected": {
|
page={currentPage}
|
||||||
backgroundColor: "transparent",
|
onChange={handlePageChange}
|
||||||
fontWeight: "bold",
|
siblingCount={0}
|
||||||
color: "#FFFFFF",
|
boundaryCount={0}
|
||||||
},
|
sx={{
|
||||||
}}
|
"& .MuiPaginationItem-root": {
|
||||||
/>
|
color: "white",
|
||||||
|
borderRadius: "0px",
|
||||||
|
},
|
||||||
|
"& .MuiPaginationItem-page.Mui-selected": {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: "#FFFFFF",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Menu Actions */}
|
{/* Menu Actions */}
|
||||||
{open && (
|
{open && (
|
||||||
<Menu
|
<Menu
|
||||||
|
@ -624,8 +641,6 @@ const CustomTable: React.FC<CustomTableProps> = ({
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setViewModal(true);
|
setViewModal(true);
|
||||||
|
|
||||||
|
|
||||||
}}
|
}}
|
||||||
color="primary"
|
color="primary"
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -642,7 +657,6 @@ const CustomTable: React.FC<CustomTableProps> = ({
|
||||||
<ViewModal
|
<ViewModal
|
||||||
handleView={() =>
|
handleView={() =>
|
||||||
handleViewButton(selectedRow?.id)
|
handleViewButton(selectedRow?.id)
|
||||||
|
|
||||||
}
|
}
|
||||||
open={viewModal}
|
open={viewModal}
|
||||||
setViewModal={setViewModal}
|
setViewModal={setViewModal}
|
||||||
|
@ -701,7 +715,7 @@ const CustomTable: React.FC<CustomTableProps> = ({
|
||||||
setModalOpen(true); // Only open if a row is selected
|
setModalOpen(true); // Only open if a row is selected
|
||||||
setRowData(selectedRow);
|
setRowData(selectedRow);
|
||||||
}
|
}
|
||||||
handleClose();
|
handleClose();
|
||||||
}}
|
}}
|
||||||
color="primary"
|
color="primary"
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -759,7 +773,7 @@ const CustomTable: React.FC<CustomTableProps> = ({
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setDeleteModal(true);
|
setDeleteModal(true);
|
||||||
handleClose();
|
handleClose();
|
||||||
}}
|
}}
|
||||||
color="error"
|
color="error"
|
||||||
sx={{
|
sx={{
|
||||||
|
|
|
@ -2,7 +2,6 @@ import React, { useEffect, useState } from "react";
|
||||||
import { Box, Button, Typography, Modal } from "@mui/material";
|
import { Box, Button, Typography, Modal } from "@mui/material";
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
import { useForm, Controller } from "react-hook-form";
|
import { useForm, Controller } from "react-hook-form";
|
||||||
import { updateVehicle } from "../../redux/slices/VehicleSlice";
|
|
||||||
import { CustomIconButton, CustomTextField } from "../AddUserModal/styled.css";
|
import { CustomIconButton, CustomTextField } from "../AddUserModal/styled.css";
|
||||||
|
|
||||||
interface EditVehicleModalProps {
|
interface EditVehicleModalProps {
|
||||||
|
@ -49,53 +48,61 @@ const EditVehicleModal: React.FC<EditVehicleModalProps> = ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set values if editRow is provided
|
|
||||||
const [imagePreview, setImagePreview] = useState<string | null>(null);
|
const [imagePreview, setImagePreview] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Set form values and image preview when editRow changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editRow) {
|
if (editRow) {
|
||||||
// Construct full image URL
|
// Set form fields
|
||||||
const imageUrl = `${process.env.REACT_APP_BACKEND_URL}/image/${editRow.imageUrl}`;
|
setValue("name", editRow.name || "");
|
||||||
setImagePreview(imageUrl); // Set the image URL to display the preview
|
setValue("company", editRow.company || "");
|
||||||
setValue("name", editRow.name);
|
setValue("modelName", editRow.modelName || "");
|
||||||
setValue("company", editRow.company);
|
setValue("chargeType", editRow.chargeType || "");
|
||||||
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 {
|
} else {
|
||||||
|
// Reset form and preview when no editRow
|
||||||
reset();
|
reset();
|
||||||
|
setImagePreview(null);
|
||||||
}
|
}
|
||||||
}, [editRow, setValue, reset]);
|
}, [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>) => {
|
handleClose();
|
||||||
const file = e.target.files?.[0];
|
reset();
|
||||||
if (file) {
|
setImagePreview(null); // Clear preview after submission
|
||||||
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();
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
@ -104,7 +111,7 @@ const onSubmit = (data: FormData) => {
|
||||||
if (reason === "backdropClick") {
|
if (reason === "backdropClick") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
handleClose(); // Close modal when clicking cross or cancel
|
handleClose();
|
||||||
}}
|
}}
|
||||||
aria-labelledby="edit-vehicle-modal"
|
aria-labelledby="edit-vehicle-modal"
|
||||||
>
|
>
|
||||||
|
@ -176,7 +183,7 @@ const onSubmit = (data: FormData) => {
|
||||||
"Maximum 30 characters allowed",
|
"Maximum 30 characters allowed",
|
||||||
},
|
},
|
||||||
pattern: {
|
pattern: {
|
||||||
value: /^[A-Za-z\s]+$/, // Only letters and spaces are allowed
|
value: /^[A-Za-z\s]+$/,
|
||||||
message:
|
message:
|
||||||
"Vehicle Name must only contain letters and spaces",
|
"Vehicle Name must only contain letters and spaces",
|
||||||
},
|
},
|
||||||
|
@ -307,17 +314,17 @@ const onSubmit = (data: FormData) => {
|
||||||
Upload Image
|
Upload Image
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
|
||||||
component="label"
|
component="label"
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: "#52ACDF",
|
backgroundColor: "#52ACDF",
|
||||||
color: "white",
|
color: "white",
|
||||||
borderRadius: "8px",
|
borderRadius: "8px",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
"&:hover": { backgroundColor: "#439BC1" },
|
"&:hover": { backgroundColor: "#52ACDF" },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Upload Image
|
Choose Image
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
hidden
|
hidden
|
||||||
|
@ -328,14 +335,17 @@ const onSubmit = (data: FormData) => {
|
||||||
{imagePreview && (
|
{imagePreview && (
|
||||||
<Box sx={{ marginTop: 2 }}>
|
<Box sx={{ marginTop: 2 }}>
|
||||||
<Typography variant="body2" mb={1}>
|
<Typography variant="body2" mb={1}>
|
||||||
Preview:
|
Preview (
|
||||||
|
{imagePreview.startsWith("blob")
|
||||||
|
? "New"
|
||||||
|
: "Existing"}
|
||||||
|
):
|
||||||
</Typography>
|
</Typography>
|
||||||
<img
|
<img
|
||||||
src={imagePreview}
|
src={imagePreview}
|
||||||
alt="image preview"
|
alt="Vehicle"
|
||||||
style={{
|
style={{
|
||||||
maxWidth: "100%",
|
maxWidth: "100%",
|
||||||
height: "auto",
|
|
||||||
borderRadius: "8px",
|
borderRadius: "8px",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -52,7 +52,7 @@ export default function MainGrid() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Dashboard Header */}
|
{/* Dashboard Header */}
|
||||||
<Typography component="h2" variant="h6" sx={{ mb: 2 }}>
|
<Typography component="h2" variant="h6" sx={{ mb: 2 ,fontSize:"30px", fontWeight:"600"}}>
|
||||||
Dashboard
|
Dashboard
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
|
|
|
@ -105,7 +105,7 @@ export default function MenuContent({ hidden }: PropType) {
|
||||||
<Stack
|
<Stack
|
||||||
sx={{
|
sx={{
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
p: 1,
|
// p: 1,
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
backgroundColor: "#202020",
|
backgroundColor: "#202020",
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -113,13 +113,36 @@ export default function VehicleViewModal({ open, setViewModal, id }: Props) {
|
||||||
</Typography>
|
</Typography>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={6}>
|
<Grid item xs={12}>
|
||||||
<Typography variant="body1">
|
<Typography variant="body1" gutterBottom>
|
||||||
<strong>Image URL:</strong>
|
<strong>Image:</strong>
|
||||||
<Typography variant="body2">
|
|
||||||
{selectedVehicle.imageUrl ?? "N/A"}
|
|
||||||
</Typography>
|
|
||||||
</Typography>
|
</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>
|
||||||
</Grid>
|
</Grid>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -13,13 +13,15 @@ import { AppDispatch, RootState } from "../../redux/store/store";
|
||||||
import { Button } from "@mui/material";
|
import { Button } from "@mui/material";
|
||||||
import { fetchAdminProfile } from "../../redux/slices/profileSlice";
|
import { fetchAdminProfile } from "../../redux/slices/profileSlice";
|
||||||
|
|
||||||
|
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
|
||||||
|
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
||||||
const drawerWidth = 240;
|
const drawerWidth = 240;
|
||||||
|
|
||||||
const Drawer = styled(MuiDrawer)({
|
const Drawer = styled(MuiDrawer)({
|
||||||
width: drawerWidth,
|
width: drawerWidth,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
boxSizing: "border-box",
|
boxSizing: "border-box",
|
||||||
mt: 10,
|
|
||||||
[`& .${drawerClasses.paper}`]: {
|
[`& .${drawerClasses.paper}`]: {
|
||||||
width: drawerWidth,
|
width: drawerWidth,
|
||||||
boxSizing: "border-box",
|
boxSizing: "border-box",
|
||||||
|
@ -37,45 +39,45 @@ export default function SideMenu() {
|
||||||
// }, [dispatch]);
|
// }, [dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Box sx={{ position: "relative" }}>
|
||||||
open={open}
|
<Drawer
|
||||||
variant="permanent"
|
open={open}
|
||||||
anchor="left"
|
variant="permanent"
|
||||||
sx={{
|
anchor="left"
|
||||||
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
|
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: {
|
||||||
flexDirection: "row",
|
xs: "none",
|
||||||
justifyContent:"center",
|
md: "block",
|
||||||
alignItems: "center",
|
width: open ? 250 : 80,
|
||||||
pt: 2,
|
transition: "all 0.5s ease",
|
||||||
|
},
|
||||||
|
[`& .${drawerClasses.paper}`]: {
|
||||||
|
backgroundColor: "background.paper",
|
||||||
|
width: open ? 250 : 80,
|
||||||
|
transition: "all 0.5s ease",
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<Box
|
||||||
src="/evLogo.png"
|
sx={{
|
||||||
alt="Logo"
|
display: "flex",
|
||||||
style={{
|
flexDirection: "row",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
width: open ? "120px" : "60px", // Adjust width depending on open state
|
alignItems: "center",
|
||||||
height: "auto",
|
pt: 2,
|
||||||
transition: "width 0.5s ease", // Smooth transition for width change
|
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
{/* <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"
|
alt="Logo"
|
||||||
src="/evLogo.png"
|
src="/evLogo.png"
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -83,7 +85,7 @@ export default function SideMenu() {
|
||||||
height: "100%",
|
height: "100%",
|
||||||
}}
|
}}
|
||||||
/> */}
|
/> */}
|
||||||
{/* <Box
|
{/* <Box
|
||||||
sx={{
|
sx={{
|
||||||
display: open ? "flex" : "none",
|
display: open ? "flex" : "none",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
@ -107,22 +109,45 @@ export default function SideMenu() {
|
||||||
{user?.userType || "N/A"}
|
{user?.userType || "N/A"}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box> */}
|
</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={{
|
sx={{
|
||||||
display: "flex",
|
position: "absolute",
|
||||||
justifyContent: open ? "flex-end" : "center",
|
top: 20,
|
||||||
alignItems: "center",
|
left: open ? 250 : 80,
|
||||||
pt: 1.5,
|
minWidth: 0,
|
||||||
textAlign: "center",
|
width: 32,
|
||||||
|
height: 35,
|
||||||
|
borderRadius: "50%",
|
||||||
|
|
||||||
|
zIndex: 1301,
|
||||||
|
// boxShadow: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="text" onClick={() => setOpen(!open)}>
|
{open ? (
|
||||||
{open ? <ArrowLeftIcon /> : <ArrowRightIcon />}
|
<ChevronLeftIcon fontSize="small" />
|
||||||
</Button>
|
) : (
|
||||||
</Box>
|
<ChevronRightIcon fontSize="small" />
|
||||||
<MenuContent hidden={open} />
|
)}
|
||||||
</Drawer>
|
</Button>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,10 @@ import {
|
||||||
updateSlot,
|
updateSlot,
|
||||||
} from "../../redux/slices/slotSlice";
|
} from "../../redux/slices/slotSlice";
|
||||||
import AddSlotModal from "../../components/AddSlotModal/addSlotModal";
|
import AddSlotModal from "../../components/AddSlotModal/addSlotModal";
|
||||||
import dayjs from "dayjs";
|
|
||||||
import EditSlotModal from "../../components/EditSlotModal/editSlotModal";
|
import EditSlotModal from "../../components/EditSlotModal/editSlotModal";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import isBetween from "dayjs/plugin/isBetween";
|
||||||
|
|
||||||
export default function EVSlotList() {
|
export default function EVSlotList() {
|
||||||
const [addModalOpen, setAddModalOpen] = useState<boolean>(false);
|
const [addModalOpen, setAddModalOpen] = useState<boolean>(false);
|
||||||
const [editModalOpen, setEditModalOpen] = useState<boolean>(false);
|
const [editModalOpen, setEditModalOpen] = useState<boolean>(false);
|
||||||
|
@ -21,8 +23,9 @@ export default function EVSlotList() {
|
||||||
const dispatch = useDispatch<AppDispatch>();
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
const availableSlots = useSelector(
|
const availableSlots = useSelector(
|
||||||
(state: RootState) => state?.slotReducer.availableSlots
|
(state: RootState) => state?.slotReducer.availableSlots
|
||||||
);
|
);
|
||||||
const { user } = useSelector((state: RootState) => state?.profileReducer);
|
const { user } = useSelector((state: RootState) => state?.profileReducer);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchManagersSlots());
|
dispatch(fetchManagersSlots());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
@ -31,6 +34,7 @@ export default function EVSlotList() {
|
||||||
setRowData(null);
|
setRowData(null);
|
||||||
setAddModalOpen(true);
|
setAddModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseModal = () => {
|
const handleCloseModal = () => {
|
||||||
setAddModalOpen(false);
|
setAddModalOpen(false);
|
||||||
setEditModalOpen(false);
|
setEditModalOpen(false);
|
||||||
|
@ -38,6 +42,8 @@ export default function EVSlotList() {
|
||||||
reset();
|
reset();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
dayjs.extend(isBetween);
|
||||||
|
// In handleAddSlot method
|
||||||
const handleAddSlot = async (data: {
|
const handleAddSlot = async (data: {
|
||||||
date: string;
|
date: string;
|
||||||
startTime: string;
|
startTime: string;
|
||||||
|
@ -45,14 +51,69 @@ export default function EVSlotList() {
|
||||||
isAvailable: boolean;
|
isAvailable: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
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());
|
await dispatch(fetchManagersSlots());
|
||||||
|
|
||||||
|
// Close the modal after successful slot creation
|
||||||
handleCloseModal();
|
handleCloseModal();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error adding slot", error);
|
console.error("Error adding slot", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// In handleUpdate method
|
||||||
const handleUpdate = async (
|
const handleUpdate = async (
|
||||||
id: string,
|
id: string,
|
||||||
startTime: string,
|
startTime: string,
|
||||||
|
@ -60,11 +121,19 @@ export default function EVSlotList() {
|
||||||
isAvailable: boolean
|
isAvailable: boolean
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const formattedStartTime = dayjs(startTime, "HH:mm").format(
|
// Convert times using dayjs
|
||||||
"HH:mm"
|
const formattedStartTime = dayjs(
|
||||||
);
|
startTime,
|
||||||
const formattedEndTime = dayjs(endTime, "HH:mm").format("HH:mm");
|
"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(
|
await dispatch(
|
||||||
updateSlot({
|
updateSlot({
|
||||||
id,
|
id,
|
||||||
|
@ -74,17 +143,19 @@ export default function EVSlotList() {
|
||||||
})
|
})
|
||||||
).unwrap();
|
).unwrap();
|
||||||
|
|
||||||
|
// Fetch updated slot data
|
||||||
await dispatch(fetchManagersSlots());
|
await dispatch(fetchManagersSlots());
|
||||||
|
|
||||||
|
// Close modal after successful update
|
||||||
handleCloseModal();
|
handleCloseModal();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Update failed", error);
|
console.error("Update failed", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const slotColumns: Column[] = [
|
const slotColumns: Column[] = [
|
||||||
{ id: "srno", label: "Sr No" },
|
{ id: "srno", label: "Sr No" },
|
||||||
{ id: "stationName", label: "Station Name" },
|
{ id: "stationName", label: "Station Name" },
|
||||||
{ id: "stationId", label: "Station Id" },
|
|
||||||
{ id: "date", label: "Date" },
|
{ id: "date", label: "Date" },
|
||||||
{ id: "startTime", label: "Start Time" },
|
{ id: "startTime", label: "Start Time" },
|
||||||
{ id: "endTime", label: "End Time" },
|
{ id: "endTime", label: "End Time" },
|
||||||
|
@ -94,24 +165,32 @@ export default function EVSlotList() {
|
||||||
: []),
|
: []),
|
||||||
];
|
];
|
||||||
|
|
||||||
const slotRows = availableSlots?.length
|
const slotRows = availableSlots?.length
|
||||||
? availableSlots.map((slot, index) => {
|
? availableSlots.map((slot, index) => {
|
||||||
const formattedDate = dayjs(slot?.date).format("YYYY-MM-DD");
|
const formattedDate = dayjs(slot?.date).format("YYYY-MM-DD");
|
||||||
const startTime = dayjs(slot?.startTime).format("HH:mm");
|
const startTime = dayjs(
|
||||||
const endTime = dayjs(slot?.endTime).format("HH:mm");
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -15,8 +15,8 @@ import EditVehicleModal from "../../components/EditVehicleModal/editVehicleModal
|
||||||
export default function VehicleList() {
|
export default function VehicleList() {
|
||||||
const [addModalOpen, setAddModalOpen] = useState<boolean>(false);
|
const [addModalOpen, setAddModalOpen] = useState<boolean>(false);
|
||||||
const [editModalOpen, setEditModalOpen] = useState<boolean>(false);
|
const [editModalOpen, setEditModalOpen] = useState<boolean>(false);
|
||||||
|
const [isAdding, setIsAdding] = useState<boolean>(false);
|
||||||
const { reset } = useForm();
|
const { reset } = useForm();
|
||||||
|
|
||||||
const [deleteModal, setDeleteModal] = useState<boolean>(false);
|
const [deleteModal, setDeleteModal] = useState<boolean>(false);
|
||||||
const [viewModal, setViewModal] = useState<boolean>(false);
|
const [viewModal, setViewModal] = useState<boolean>(false);
|
||||||
const [rowData, setRowData] = useState<any | null>(null);
|
const [rowData, setRowData] = useState<any | null>(null);
|
||||||
|
@ -29,6 +29,8 @@ export default function VehicleList() {
|
||||||
dispatch(vehicleList());
|
dispatch(vehicleList());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
|
console.log("Backend URL:", process.env.REACT_APP_BACKEND_URL);
|
||||||
|
|
||||||
const handleClickOpen = () => {
|
const handleClickOpen = () => {
|
||||||
setRowData(null);
|
setRowData(null);
|
||||||
setAddModalOpen(true);
|
setAddModalOpen(true);
|
||||||
|
@ -49,53 +51,50 @@ export default function VehicleList() {
|
||||||
imageFile: File;
|
imageFile: File;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
|
setIsAdding(true);
|
||||||
const response = await dispatch(addVehicle(data));
|
const response = await dispatch(addVehicle(data));
|
||||||
console.log("Added vehicle response: ", response); // Check if the image URL is included in the response
|
console.log("Added vehicle response: ", response);
|
||||||
await dispatch(vehicleList());
|
|
||||||
handleCloseModal();
|
handleCloseModal();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error adding vehicle", error);
|
console.error("Error adding vehicle", error);
|
||||||
|
} finally {
|
||||||
|
setIsAdding(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdate = async (
|
|
||||||
id: number,
|
|
||||||
name: string,
|
const handleUpdate = (
|
||||||
company: string,
|
id: string,
|
||||||
modelName: string,
|
name: string,
|
||||||
chargeType: string,
|
company: string,
|
||||||
imageUrl: string
|
modelName: string,
|
||||||
) => {
|
chargeType: string,
|
||||||
try {
|
imageUrl: File | null
|
||||||
await dispatch(
|
) => {
|
||||||
updateVehicle({
|
dispatch(
|
||||||
id,
|
updateVehicle({
|
||||||
name,
|
id,
|
||||||
company,
|
name,
|
||||||
modelName,
|
company,
|
||||||
chargeType,
|
modelName,
|
||||||
imageUrl,
|
chargeType,
|
||||||
})
|
imageUrl, // File or null
|
||||||
);
|
})
|
||||||
await dispatch(vehicleList());
|
);
|
||||||
handleCloseModal();
|
};
|
||||||
} catch (error) {
|
|
||||||
console.error("Update failed", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const categoryColumns: Column[] = [
|
const categoryColumns: Column[] = [
|
||||||
{ id: "srno", label: "Sr No" },
|
{ id: "srno", label: "Sr No" },
|
||||||
|
{ id: "imageUrl", label: "Image" },
|
||||||
{ id: "name", label: "Vehicle Name" },
|
{ id: "name", label: "Vehicle Name" },
|
||||||
{ id: "company", label: "Company" },
|
{ id: "company", label: "Company" },
|
||||||
{ id: "modelName", label: "Model Name" },
|
{ id: "modelName", label: "Model Name" },
|
||||||
{ id: "chargeType", label: "Charge Type" },
|
{ id: "chargeType", label: "Charge Type" },
|
||||||
{ id: "imageUrl", label: "Image" },
|
|
||||||
{ id: "action", label: "Action", align: "center" },
|
{ id: "action", label: "Action", align: "center" },
|
||||||
];
|
];
|
||||||
console.log(
|
|
||||||
`${process.env.REACT_APP_BACKEND_URL}/image/${vehicles[0]?.imageUrl}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const categoryRows = vehicles?.length
|
const categoryRows = vehicles?.length
|
||||||
? vehicles?.map(
|
? vehicles?.map(
|
||||||
|
@ -109,19 +108,32 @@ export default function VehicleList() {
|
||||||
imageUrl: string;
|
imageUrl: string;
|
||||||
},
|
},
|
||||||
index: number
|
index: number
|
||||||
) => ({
|
) => {
|
||||||
id: vehicle?.id,
|
const imageUrl = vehicle?.imageUrl
|
||||||
srno: index + 1,
|
? `${process.env.REACT_APP_BACKEND_URL}/image/${vehicle?.imageUrl}`
|
||||||
name: vehicle?.name,
|
: "/images/fallback.jpg";
|
||||||
company: vehicle?.company,
|
console.log(
|
||||||
modelName: vehicle?.modelName,
|
"Vehicle:",
|
||||||
chargeType: vehicle?.chargeType,
|
vehicle.name,
|
||||||
imageUrl: `${process.env.REACT_APP_BACKEND_URL}/image/${vehicle?.imageUrl}`,
|
"Image URL:",
|
||||||
})
|
imageUrl
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
id: vehicle?.id,
|
||||||
|
srno: index + 1,
|
||||||
|
name: vehicle?.name,
|
||||||
|
company: vehicle?.company,
|
||||||
|
modelName: vehicle?.modelName,
|
||||||
|
chargeType: vehicle?.chargeType,
|
||||||
|
imageUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{isAdding ? <p>Adding vehicle...</p> : null}
|
||||||
<CustomTable
|
<CustomTable
|
||||||
columns={categoryColumns}
|
columns={categoryColumns}
|
||||||
rows={categoryRows}
|
rows={categoryRows}
|
||||||
|
|
|
@ -96,13 +96,14 @@ export const addVehicle = createAsyncThunk<
|
||||||
formData.append("modelName", modelName);
|
formData.append("modelName", modelName);
|
||||||
formData.append("chargeType", chargeType);
|
formData.append("chargeType", chargeType);
|
||||||
formData.append("image", imageFile);
|
formData.append("image", imageFile);
|
||||||
|
|
||||||
const response = await http.post("create-vehicle", formData, {
|
const response = await http.post("create-vehicle", formData, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data",
|
"Content-Type": "multipart/form-data",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return response.data;
|
console.log("first", imageFile);
|
||||||
|
return response.data.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return rejectWithValue(
|
return rejectWithValue(
|
||||||
error.response?.data?.message || "An error occurred"
|
error.response?.data?.message || "An error occurred"
|
||||||
|
@ -111,25 +112,56 @@ export const addVehicle = createAsyncThunk<
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update Vehicle details
|
// Update Vehicle details
|
||||||
export const updateVehicle = createAsyncThunk(
|
export const updateVehicle = createAsyncThunk<
|
||||||
"updateVehicle",
|
Vehicle,
|
||||||
async ({ id, ...vehicleData }: Vehicle, { rejectWithValue }) => {
|
{
|
||||||
try {
|
id: string | number;
|
||||||
const response = await http.patch(
|
name: string;
|
||||||
`/update-vehicle/${id}`,
|
company: string;
|
||||||
vehicleData
|
modelName: string;
|
||||||
);
|
chargeType: string;
|
||||||
toast.success("Vehicle Details updated successfully");
|
imageUrl: File | string | null;
|
||||||
return response?.data;
|
},
|
||||||
} catch (error: any) {
|
{ rejectValue: string }
|
||||||
toast.error("Error updating the user: " + error);
|
>(
|
||||||
return rejectWithValue(
|
"updateVehicle",
|
||||||
error.response?.data?.message || "An error occurred"
|
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<
|
export const deleteVehicle = createAsyncThunk<
|
||||||
string,
|
string,
|
||||||
string,
|
string,
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
|
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
|
||||||
import http from "../../lib/https"; // Assuming you have a custom HTTP library for requests
|
import http from "../../lib/https"; // Assuming you have a custom HTTP library for requests
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
// Define TypeScript types
|
// Define TypeScript types
|
||||||
interface Slot {
|
interface Slot {
|
||||||
id: string;
|
id: string;
|
||||||
stationId: string;
|
stationId: string;
|
||||||
date: string;
|
date?: string;
|
||||||
startTime: string;
|
startTime: string;
|
||||||
|
startingDate?: string;
|
||||||
|
endingDate?: string;
|
||||||
endTime: string;
|
endTime: string;
|
||||||
|
duration: number;
|
||||||
isAvailable: boolean;
|
isAvailable: boolean;
|
||||||
stationName:string;
|
stationName: string;
|
||||||
ChargingStation: { name: string };
|
ChargingStation: { name: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,33 +76,31 @@ export const fetchManagersSlots = createAsyncThunk<
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
export const createSlot = createAsyncThunk<
|
export const createSlot = createAsyncThunk<
|
||||||
Slot,
|
Slot[], // <-- updated from Slot to Slot[]
|
||||||
{
|
{
|
||||||
date: string;
|
date?: string;
|
||||||
startTime: string;
|
startingDate?: string;
|
||||||
endTime: string;
|
endingDate?: string;
|
||||||
|
startHour: string;
|
||||||
|
endHour: string;
|
||||||
isAvailable: boolean;
|
isAvailable: boolean;
|
||||||
|
duration: number;
|
||||||
|
stationId:number;
|
||||||
|
|
||||||
},
|
},
|
||||||
{ rejectValue: string }
|
{ rejectValue: string }
|
||||||
>("slots/createSlot", async (payload, { rejectWithValue }) => {
|
>("slots/createSlot", async (payload, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
// const payload = {
|
|
||||||
// date,
|
|
||||||
// startHour,
|
|
||||||
// endHour,
|
|
||||||
// isAvailable,
|
|
||||||
// };
|
|
||||||
|
|
||||||
const token = localStorage?.getItem("authToken");
|
const token = localStorage?.getItem("authToken");
|
||||||
if (!token) throw new Error("No token found");
|
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(
|
return rejectWithValue(
|
||||||
error.response?.data?.message || "An error occurred"
|
error.response?.data?.message || "An error occurred"
|
||||||
);
|
);
|
||||||
|
@ -191,13 +193,39 @@ const slotSlice = createSlice({
|
||||||
.addCase(createSlot.pending, (state) => {
|
.addCase(createSlot.pending, (state) => {
|
||||||
state.loading = true;
|
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(
|
.addCase(
|
||||||
createSlot.fulfilled,
|
createSlot.fulfilled,
|
||||||
(state, action: PayloadAction<Slot>) => {
|
(state, action: PayloadAction<Slot[]>) => {
|
||||||
state.loading = false;
|
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) => {
|
.addCase(createSlot.rejected, (state, action) => {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
state.error = action.payload || "Failed to create slot";
|
state.error = action.payload || "Failed to create slot";
|
||||||
|
|
Loading…
Reference in a new issue