image slotbooking integration and ui changes

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

View file

@ -164,8 +164,10 @@ import {
InputLabel, 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;

View file

@ -1,38 +1,75 @@
import { useForm } from "react-hook-form"; import { useForm, Controller } from "react-hook-form";
import { Box, Button, Typography, Modal } from "@mui/material"; import { 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>
); );

View file

@ -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={{

View file

@ -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",
}} }}
/> />

View file

@ -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>

View file

@ -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",
}} }}

View file

@ -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>
) : ( ) : (

View file

@ -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>
); );
} }

View file

@ -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 (
<> <>

View file

@ -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}

View file

@ -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,

View file

@ -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";