image slotbooking integration and ui changes
This commit is contained in:
parent
36f6df429a
commit
62777a67de
|
@ -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;
|
||||
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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={{
|
||||
|
|
|
@ -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",
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -105,7 +105,7 @@ export default function MenuContent({ hidden }: PropType) {
|
|||
<Stack
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
p: 1,
|
||||
// p: 1,
|
||||
justifyContent: "space-between",
|
||||
backgroundColor: "#202020",
|
||||
}}
|
||||
|
|
|
@ -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>
|
||||
) : (
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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";
|
||||
|
|
Loading…
Reference in a new issue