509 lines
17 KiB
JavaScript
509 lines
17 KiB
JavaScript
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
import { useDispatch, useSelector } from "react-redux";
|
|
import { updateLifestyleDetails } from "../redux/registrationFormSlice";
|
|
import {
|
|
Grid,
|
|
FormControl,
|
|
InputLabel,
|
|
Select,
|
|
MenuItem,
|
|
Button,
|
|
TextField,
|
|
Checkbox,
|
|
ListItemText,
|
|
Dialog,
|
|
DialogTitle,
|
|
DialogContent,
|
|
DialogContentText,
|
|
DialogActions,
|
|
Tooltip,
|
|
} from "@mui/material";
|
|
import { LocalizationProvider } from "@mui/x-date-pickers";
|
|
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
|
import { TimePicker } from "@mui/x-date-pickers/TimePicker";
|
|
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
|
|
import { useLifestyleMasters } from "../hooks/useMasters";
|
|
import horoscopeImg from "../assets/images/horoscopeimg.png";
|
|
|
|
const LifestyleDetailsForm = ({
|
|
onSubmitStep,
|
|
onSkipStep,
|
|
errors,
|
|
onFieldChange,
|
|
}) => {
|
|
const dispatch = useDispatch();
|
|
const data = useSelector((state) => state.registerform.lifestyleDetails);
|
|
const inputRef = useRef(null);
|
|
const requiredMark = <span style={{ color: "#d32f2f" }}> *</span>;
|
|
|
|
const { data: lifestyleMasters, isLoading: isLifestyleMastersLoading } =
|
|
useLifestyleMasters();
|
|
|
|
const dietOptions = useMemo(() => {
|
|
const raw = lifestyleMasters;
|
|
if (!raw) return [];
|
|
if (Array.isArray(raw)) return raw;
|
|
return raw.diet || raw.diets || [];
|
|
}, [lifestyleMasters]);
|
|
|
|
const hobbyOptions = useMemo(() => {
|
|
const raw = lifestyleMasters;
|
|
if (!raw) return [];
|
|
if (Array.isArray(raw)) return raw;
|
|
return raw.hobbies || raw.hobby || [];
|
|
}, [lifestyleMasters]);
|
|
|
|
const grahaOptions = useMemo(() => {
|
|
const raw = lifestyleMasters;
|
|
if (!raw) return [];
|
|
if (Array.isArray(raw)) return raw;
|
|
return raw.grahas || raw.graha || [];
|
|
}, [lifestyleMasters]);
|
|
|
|
const [confirmDialog, setConfirmDialog] = useState({
|
|
open: false,
|
|
type: "",
|
|
house: null,
|
|
item: "",
|
|
sourceHouse: null,
|
|
pendingValue: [],
|
|
});
|
|
|
|
useEffect(() => {
|
|
inputRef.current?.focus();
|
|
}, []);
|
|
|
|
const handleChange = (field, value) => {
|
|
dispatch(updateLifestyleDetails({ [field]: value }));
|
|
if (onFieldChange) onFieldChange(field);
|
|
};
|
|
|
|
const handleMultiChange = (field, value) => {
|
|
const nextValue = Array.isArray(value) ? value : String(value).split(",");
|
|
dispatch(updateLifestyleDetails({ [field]: nextValue }));
|
|
if (onFieldChange) onFieldChange(field);
|
|
};
|
|
|
|
const handleChartChange = (type, house, newValue) => {
|
|
const currentChart = type === "graha" ? data.graha : data.amsam;
|
|
const currentHouseValue = currentChart?.[house] || [];
|
|
|
|
// Find added items
|
|
const addedItems = newValue.filter((item) => !currentHouseValue.includes(item));
|
|
|
|
if (addedItems.length > 0) {
|
|
for (const addedItem of addedItems) {
|
|
for (const [otherHouse, items] of Object.entries(currentChart || {})) {
|
|
if (String(otherHouse) !== String(house) && items && items.includes(addedItem)) {
|
|
setConfirmDialog({
|
|
open: true,
|
|
type,
|
|
house,
|
|
item: addedItem,
|
|
sourceHouse: otherHouse,
|
|
pendingValue: newValue,
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// No duplicates, proceed with update
|
|
const updates = {};
|
|
const nextChart = { ...(currentChart || {}) };
|
|
nextChart[house] = newValue;
|
|
|
|
if (type === "graha") {
|
|
updates.graha = nextChart;
|
|
} else {
|
|
updates.amsam = nextChart;
|
|
}
|
|
|
|
dispatch(updateLifestyleDetails(updates));
|
|
if (onFieldChange) onFieldChange(type);
|
|
};
|
|
|
|
const handleConfirmMove = () => {
|
|
const { type, house, item, sourceHouse, pendingValue } = confirmDialog;
|
|
const currentChart = type === "graha" ? data.graha : data.amsam;
|
|
const nextChart = { ...(currentChart || {}) };
|
|
|
|
// Remove from source house
|
|
if (nextChart[sourceHouse]) {
|
|
nextChart[sourceHouse] = nextChart[sourceHouse].filter((i) => i !== item);
|
|
}
|
|
|
|
// Add to target house
|
|
nextChart[house] = pendingValue;
|
|
|
|
const updates = type === "graha" ? { graha: nextChart } : { amsam: nextChart };
|
|
dispatch(updateLifestyleDetails(updates));
|
|
if (onFieldChange) onFieldChange(type);
|
|
|
|
setConfirmDialog({ ...confirmDialog, open: false });
|
|
};
|
|
|
|
const handleCancelMove = () => {
|
|
setConfirmDialog({ ...confirmDialog, open: false });
|
|
};
|
|
|
|
const handleSubmit = () => {
|
|
console.log("Submitting lifestyle details:", data);
|
|
onSubmitStep();
|
|
|
|
};
|
|
|
|
const parseDateValue = (value) => {
|
|
if (!value) return null;
|
|
const date = new Date(value);
|
|
return Number.isNaN(date.getTime()) ? null : date;
|
|
};
|
|
|
|
const parseTimeValue = (value) => {
|
|
if (!value || typeof value !== "string") return null;
|
|
const [hours, minutes] = value.split(":").map(Number);
|
|
if (Number.isNaN(hours) || Number.isNaN(minutes)) return null;
|
|
const date = new Date();
|
|
date.setHours(hours, minutes, 0, 0);
|
|
return date;
|
|
};
|
|
|
|
const formatDate = (value) => {
|
|
if (!(value instanceof Date) || Number.isNaN(value)) return "";
|
|
const y = value.getFullYear();
|
|
const m = String(value.getMonth() + 1).padStart(2, "0");
|
|
const d = String(value.getDate()).padStart(2, "0");
|
|
return `${y}-${m}-${d}`;
|
|
};
|
|
|
|
const formatTime = (value) => {
|
|
if (!(value instanceof Date) || Number.isNaN(value)) return "";
|
|
const h = String(value.getHours()).padStart(2, "0");
|
|
const m = String(value.getMinutes()).padStart(2, "0");
|
|
return `${h}:${m}`;
|
|
};
|
|
|
|
const renderChartCell = (label, value, onChange) => (
|
|
<Tooltip title={value && value.length > 0 ? value.join(", ") : ""} arrow placement="top">
|
|
<div className="bg-white border border-gray-300 rounded-lg p-2 flex flex-col items-center justify-center min-h-[70px]">
|
|
<span className="text-[10px] font-medium text-gray-700 text-center">
|
|
{label}
|
|
</span>
|
|
<FormControl fullWidth variant="standard" sx={{ mt: 0.5 }}>
|
|
<Select
|
|
multiple
|
|
displayEmpty
|
|
value={value}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
renderValue={(selected) => {
|
|
if (!selected || selected.length === 0) return `+ Add ${label}`;
|
|
return (
|
|
<div style={{
|
|
whiteSpace: "nowrap",
|
|
overflow: "hidden",
|
|
textOverflow: "ellipsis",
|
|
maxWidth: "60px",
|
|
fontSize: "11px"
|
|
}}>
|
|
{selected.join(", ")}
|
|
</div>
|
|
);
|
|
}}
|
|
MenuProps={{
|
|
PaperProps: { style: { maxHeight: 280 } },
|
|
}}
|
|
>
|
|
{grahaOptions.map((opt) => (
|
|
<MenuItem key={opt} value={opt}>
|
|
<Checkbox checked={value.indexOf(opt) > -1} />
|
|
<ListItemText primary={opt} />
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
</FormControl>
|
|
</div>
|
|
</Tooltip>
|
|
);
|
|
|
|
const renderChartGrid = (type) => {
|
|
const getValue = (house) =>
|
|
(type === "graha" ? data.graha : data.amsam)?.[house] || [];
|
|
const onChange = (house, value) =>
|
|
handleChartChange(type, house, value);
|
|
const label = type === "graha" ? "Rasi" : "Navamsam";
|
|
|
|
return (
|
|
<div className="grid grid-cols-4 gap-2">
|
|
{renderChartCell(label, getValue(1), (value) => onChange(1, value))}
|
|
{renderChartCell(label, getValue(2), (value) => onChange(2, value))}
|
|
{renderChartCell(label, getValue(3), (value) => onChange(3, value))}
|
|
{renderChartCell(label, getValue(4), (value) => onChange(4, value))}
|
|
|
|
{renderChartCell(label, getValue(5), (value) => onChange(5, value))}
|
|
<div className="col-span-2 row-span-2 flex items-center justify-center">
|
|
<div className="w-full aspect-square rounded-full overflow-hidden bg-white">
|
|
<img src={horoscopeImg} alt="Horoscope" className="w-full h-full object-contain" />
|
|
</div>
|
|
</div>
|
|
{renderChartCell(label, getValue(6), (value) => onChange(6, value))}
|
|
|
|
{renderChartCell(label, getValue(7), (value) => onChange(7, value))}
|
|
{renderChartCell(label, getValue(8), (value) => onChange(8, value))}
|
|
|
|
{renderChartCell(label, getValue(9), (value) => onChange(9, value))}
|
|
{renderChartCell(label, getValue(10), (value) => onChange(10, value))}
|
|
{renderChartCell(label, getValue(11), (value) => onChange(11, value))}
|
|
{renderChartCell(label, getValue(12), (value) => onChange(12, value))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="w-full max-w-[1200px] mx-auto py-6 md:px-2 rounded-8">
|
|
<form noValidate autoComplete="off" style={{ padding: 16 }}>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-20 gap-y-10 mb-6">
|
|
<div className="flex flex-col gap-2">
|
|
<label className="text-gray-900 text-[15px]">
|
|
Diet{requiredMark}
|
|
</label>
|
|
<FormControl fullWidth variant="outlined" error={Boolean(errors.diets)}>
|
|
<InputLabel id="diets-label">Select Diet</InputLabel>
|
|
<Select
|
|
labelId="diets-label"
|
|
label="Select Diet"
|
|
name="diets"
|
|
value={data.diets}
|
|
onChange={(e) => handleChange("diets", e.target.value)}
|
|
inputRef={inputRef}
|
|
disabled={isLifestyleMastersLoading}
|
|
sx={{
|
|
"& .MuiSelect-select.Mui-disabled": {
|
|
cursor: "not-allowed",
|
|
},
|
|
}}
|
|
>
|
|
{dietOptions.map((opt) => {
|
|
const value = opt.id ?? opt;
|
|
const label = opt.diet_name || opt.name || String(opt);
|
|
return (
|
|
<MenuItem key={value} value={value}>
|
|
{label}
|
|
</MenuItem>
|
|
);
|
|
})}
|
|
</Select>
|
|
{errors.diets && (
|
|
<p
|
|
style={{
|
|
color: "#d32f2f",
|
|
margin: "3px 14px 0 14px",
|
|
fontSize: "0.75rem",
|
|
}}
|
|
>
|
|
{errors.diets}
|
|
</p>
|
|
)}
|
|
</FormControl>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-2">
|
|
<label className="text-gray-900 text-[15px]">
|
|
Hobbies & Interests (Multi-select){requiredMark}
|
|
</label>
|
|
<FormControl
|
|
fullWidth
|
|
variant="outlined"
|
|
error={Boolean(errors.hobbies)}
|
|
>
|
|
<InputLabel id="hobbies-label">
|
|
Select Hobbies & Interests
|
|
</InputLabel>
|
|
<Select
|
|
labelId="hobbies-label"
|
|
label="Select Hobbies & Interests"
|
|
name="hobbies"
|
|
multiple
|
|
value={data.hobbies}
|
|
onChange={(e) => handleMultiChange("hobbies", e.target.value)}
|
|
disabled={isLifestyleMastersLoading}
|
|
renderValue={(selected) =>
|
|
selected
|
|
.map((id) => {
|
|
const item = hobbyOptions.find(
|
|
(opt) => (opt.id ?? opt) === id
|
|
);
|
|
return item?.hobby_name || item?.name || id;
|
|
})
|
|
.join(", ")
|
|
}
|
|
sx={{
|
|
"& .MuiSelect-select.Mui-disabled": {
|
|
cursor: "not-allowed",
|
|
},
|
|
}}
|
|
>
|
|
{hobbyOptions.map((opt) => {
|
|
const value = opt.id ?? opt;
|
|
const label = opt.hobby_name || opt.name || String(opt);
|
|
return (
|
|
<MenuItem key={value} value={value}>
|
|
<Checkbox checked={data.hobbies.indexOf(value) > -1} />
|
|
<ListItemText primary={label} />
|
|
</MenuItem>
|
|
);
|
|
})}
|
|
</Select>
|
|
{errors.hobbies && (
|
|
<p
|
|
style={{
|
|
color: "#d32f2f",
|
|
margin: "3px 14px 0 14px",
|
|
fontSize: "0.75rem",
|
|
}}
|
|
>
|
|
{errors.hobbies}
|
|
</p>
|
|
)}
|
|
</FormControl>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="text-center py-4">
|
|
<h2 className="text-[18px] font-semibold text-gray-800">
|
|
Astrology / Horoscope
|
|
</h2>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 w-full max-w-[900px] mx-auto">
|
|
<div className="flex flex-col gap-2">
|
|
<label className="text-gray-900 text-[15px]">
|
|
Date of Birth{requiredMark}
|
|
</label>
|
|
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
|
<DatePicker
|
|
format="dd/MM/yyyy"
|
|
disableFuture
|
|
value={parseDateValue(data.dob)}
|
|
onChange={(value) => handleChange("dob", formatDate(value))}
|
|
slotProps={{
|
|
textField: {
|
|
name: "dob",
|
|
fullWidth: true,
|
|
error: Boolean(errors.dob),
|
|
helperText: errors.dob,
|
|
},
|
|
}}
|
|
/>
|
|
</LocalizationProvider>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-2">
|
|
<label className="text-gray-900 text-[15px]">
|
|
Time of Birth{requiredMark}
|
|
</label>
|
|
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
|
<TimePicker
|
|
value={parseTimeValue(data.tob)}
|
|
onChange={(value) => handleChange("tob", formatTime(value))}
|
|
slotProps={{
|
|
textField: {
|
|
name: "tob",
|
|
fullWidth: true,
|
|
error: Boolean(errors.tob),
|
|
helperText: errors.tob,
|
|
},
|
|
}}
|
|
/>
|
|
</LocalizationProvider>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-2">
|
|
<label className="text-gray-900 text-[15px]">Place of Birth</label>
|
|
<TextField
|
|
fullWidth
|
|
name="placeOfBirth"
|
|
value={data.placeOfBirth}
|
|
onChange={(e) => handleChange("placeOfBirth", e.target.value)}
|
|
error={Boolean(errors.placeOfBirth)}
|
|
helperText={errors.placeOfBirth}
|
|
placeholder="Enter Place of Birth"
|
|
variant="outlined"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 w-full max-w-[950px] mx-auto mt-6">
|
|
<div>
|
|
<div className="py-4">
|
|
<h3 className="text-base font-semibold text-gray-800 mb-3">
|
|
Add Rasi
|
|
</h3>
|
|
{renderChartGrid("graha")}
|
|
{errors.graha_duplicate && (
|
|
<p style={{ color: "#d32f2f", fontSize: "0.75rem", marginTop: "4px" }}>
|
|
{errors.graha_duplicate}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="py-4">
|
|
<h3 className="text-base font-semibold text-gray-800 mb-3">
|
|
Add Navamsam
|
|
</h3>
|
|
{renderChartGrid("amsam")}
|
|
{errors.amsam_duplicate && (
|
|
<p style={{ color: "#d32f2f", fontSize: "0.75rem", marginTop: "4px" }}>
|
|
{errors.amsam_duplicate}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Grid
|
|
item
|
|
xs={12}
|
|
style={{
|
|
marginTop: 50,
|
|
display: "flex",
|
|
gap: 16,
|
|
justifyContent: "center",
|
|
}}
|
|
>
|
|
{onSkipStep && (
|
|
<Button variant="outlined" onClick={onSkipStep}>
|
|
Skip
|
|
</Button>
|
|
)}
|
|
<Button variant="contained" color="primary" onClick={handleSubmit}>
|
|
{onSkipStep ? "Next" : "Update"}
|
|
</Button>
|
|
</Grid>
|
|
|
|
<Dialog
|
|
open={confirmDialog.open}
|
|
onClose={handleCancelMove}
|
|
aria-labelledby="alert-dialog-title"
|
|
aria-describedby="alert-dialog-description"
|
|
>
|
|
<DialogTitle id="alert-dialog-title">{"Duplicate Selection"}</DialogTitle>
|
|
<DialogContent>
|
|
<DialogContentText id="alert-dialog-description">
|
|
{confirmDialog.item} is already selected in House {confirmDialog.sourceHouse}. Do you want to move it to House {confirmDialog.house}?
|
|
</DialogContentText>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={handleCancelMove}>Cancel</Button>
|
|
<Button onClick={handleConfirmMove} autoFocus>Move</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
</form>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default LifestyleDetailsForm;
|