1302 lines
42 KiB
JavaScript
1302 lines
42 KiB
JavaScript
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
import { useDispatch, useSelector } from "react-redux";
|
|
import { updatePersonalDetails } from "../redux/registrationFormSlice";
|
|
import {
|
|
Grid,
|
|
TextField,
|
|
FormControl,
|
|
InputLabel,
|
|
Select,
|
|
MenuItem,
|
|
Button,
|
|
Box,
|
|
Typography,
|
|
Link,
|
|
InputAdornment,
|
|
IconButton,
|
|
} from "@mui/material";
|
|
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
|
import Visibility from "@mui/icons-material/Visibility";
|
|
import VisibilityOff from "@mui/icons-material/VisibilityOff";
|
|
import AdvancedDropzone from "./AdvancedDropzone";
|
|
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
|
import toast from "react-hot-toast";
|
|
|
|
import { LocalizationProvider } from "@mui/x-date-pickers";
|
|
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
|
|
import {
|
|
usePersonalDetailsMasters,
|
|
useCasteMasters,
|
|
useSubCasteMasters,
|
|
useCityMasters,
|
|
useStarMasters,
|
|
} from "../hooks/useDependentMasters";
|
|
import { useSendOtp, useVerifyOtp } from "../hooks/useAuth";
|
|
const OTP_LENGTH = 4;
|
|
const OTP_TIMER_SEC = 120; // 2 minutes
|
|
|
|
const PersonalDetailsForm = ({ onSubmitStep, errors, onFieldChange, isStep1Update }) => {
|
|
const dispatch = useDispatch();
|
|
const data = useSelector((state) => state.registerform.personalDetails);
|
|
const nameInputRef = useRef(null);
|
|
const requiredMark = <span style={{ color: "#d32f2f" }}> *</span>;
|
|
|
|
const [showOtp, setShowOtp] = useState(false);
|
|
const [otp, setOtp] = useState(new Array(OTP_LENGTH).fill(""));
|
|
const [otpTimer, setOtpTimer] = useState(0);
|
|
const [otpError, setOtpError] = useState("");
|
|
const [mobileOtpVerified, setMobileOtpVerified] = useState(false);
|
|
const [mobileNumberError, setMobileNumberError] = useState("");
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
const sendOtp = useSendOtp();
|
|
const verifyOtp = useVerifyOtp();
|
|
|
|
const { data: personalMasters, isLoading: isPersonalMastersLoading } =
|
|
usePersonalDetailsMasters();
|
|
|
|
const genderOptions = useMemo(
|
|
() => personalMasters?.gender ?? [],
|
|
[personalMasters]
|
|
);
|
|
const maritalStatusOptions = useMemo(
|
|
() => personalMasters?.marital_status ?? [],
|
|
[personalMasters]
|
|
);
|
|
const religionOptions = useMemo(
|
|
() => personalMasters?.religion ?? [],
|
|
[personalMasters]
|
|
);
|
|
const profileCreatedForOptions = useMemo(
|
|
() => personalMasters?.profileCreatedFor ?? [],
|
|
[personalMasters]
|
|
);
|
|
const gothramOptions = useMemo(
|
|
() => personalMasters?.gothram ?? [],
|
|
[personalMasters]
|
|
);
|
|
const raasiOptions = useMemo(
|
|
() => personalMasters?.raasi ?? [],
|
|
[personalMasters]
|
|
);
|
|
const stateOptions = useMemo(
|
|
() => personalMasters?.state ?? [],
|
|
[personalMasters]
|
|
);
|
|
|
|
const casteQuery = useCasteMasters(data.religion);
|
|
const subCasteQuery = useSubCasteMasters(data.caste);
|
|
const cityQuery = useCityMasters(data.state);
|
|
const starQuery = useStarMasters(data.raasi);
|
|
|
|
const casteOptions = useMemo(() => {
|
|
const raw = casteQuery.data;
|
|
if (!raw) return [];
|
|
if (Array.isArray(raw)) return raw;
|
|
return raw.caste || raw.data || [];
|
|
}, [casteQuery.data]);
|
|
|
|
const subCasteOptions = useMemo(() => {
|
|
const raw = subCasteQuery.data;
|
|
if (!raw) return [];
|
|
if (Array.isArray(raw)) return raw;
|
|
return raw.sub_caste || raw.subCaste || raw.data || [];
|
|
}, [subCasteQuery.data]);
|
|
|
|
const cityOptions = useMemo(() => {
|
|
const raw = cityQuery.data;
|
|
if (!raw) return [];
|
|
if (Array.isArray(raw)) return raw;
|
|
return raw.subCaste || raw.district || raw.data || [];
|
|
}, [cityQuery.data]);
|
|
|
|
const starOptions = useMemo(() => {
|
|
const raw = starQuery.data;
|
|
if (!raw) return [];
|
|
if (Array.isArray(raw)) return raw;
|
|
return raw.star || raw.data || [];
|
|
}, [starQuery.data]);
|
|
|
|
const getOptionLabel = useCallback((item, fallback = "") => {
|
|
if (!item) return fallback;
|
|
if (typeof item === "string") return item;
|
|
return (
|
|
item.name ||
|
|
item.caste_name ||
|
|
item.sub_caste_name ||
|
|
item.district_name ||
|
|
item.city_name ||
|
|
item.state_name ||
|
|
item.religion_name ||
|
|
item.marital_status_name ||
|
|
item.gothram_name ||
|
|
item.raasi_name ||
|
|
item.profile_for_name ||
|
|
item.star_name ||
|
|
fallback
|
|
);
|
|
}, []);
|
|
|
|
const startOtpTimer = useCallback(() => {
|
|
setOtpTimer(OTP_TIMER_SEC);
|
|
}, []);
|
|
|
|
const getApiErrorMessage = useCallback((error, fallback) => {
|
|
const data = error?.response?.data ?? error?.data ?? error;
|
|
if (!data) return fallback;
|
|
if (typeof data === "string") return data;
|
|
|
|
const directMessage =
|
|
data.message || data.error || data.detail || data.msg;
|
|
if (directMessage) return directMessage;
|
|
|
|
if (Array.isArray(data.errors)) {
|
|
const first = data.errors[0];
|
|
if (typeof first === "string") return first;
|
|
if (first && typeof first === "object") {
|
|
return (
|
|
first.message ||
|
|
first.msg ||
|
|
first.error ||
|
|
first.detail ||
|
|
fallback
|
|
);
|
|
}
|
|
}
|
|
|
|
if (data.errors && typeof data.errors === "object") {
|
|
const firstValue = Object.values(data.errors)[0];
|
|
if (Array.isArray(firstValue)) {
|
|
const joined = firstValue.filter(Boolean).join(" ");
|
|
if (joined) return joined;
|
|
}
|
|
if (typeof firstValue === "string") return firstValue;
|
|
if (firstValue && typeof firstValue === "object") {
|
|
return (
|
|
firstValue.message ||
|
|
firstValue.msg ||
|
|
firstValue.error ||
|
|
firstValue.detail ||
|
|
fallback
|
|
);
|
|
}
|
|
}
|
|
|
|
if (data.otp) return String(data.otp);
|
|
if (error?.message) return error.message;
|
|
return fallback;
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (otpTimer <= 0) return;
|
|
const timerId = setInterval(() => {
|
|
setOtpTimer((sec) => sec - 1);
|
|
}, 1000);
|
|
return () => clearInterval(timerId);
|
|
}, [otpTimer]);
|
|
|
|
useEffect(() => {
|
|
nameInputRef.current?.focus();
|
|
}, []);
|
|
|
|
const handleOtpChange = (index, value) => {
|
|
if (!/^\d*$/.test(value)) return;
|
|
const newOtp = [...otp];
|
|
newOtp[index] = value.slice(-1);
|
|
setOtp(newOtp);
|
|
if (value && index < OTP_LENGTH - 1) {
|
|
const next = document.getElementById(`otp-${index + 1}`);
|
|
if (next) next.focus();
|
|
}
|
|
};
|
|
|
|
const handleOtpKeyDown = (index, event) => {
|
|
if (event.key !== "Backspace") return;
|
|
event.preventDefault();
|
|
const newOtp = [...otp];
|
|
if (newOtp[index]) {
|
|
newOtp[index] = "";
|
|
setOtp(newOtp);
|
|
return;
|
|
}
|
|
if (index > 0) {
|
|
newOtp[index - 1] = "";
|
|
setOtp(newOtp);
|
|
const prev = document.getElementById(`otp-${index - 1}`);
|
|
if (prev) prev.focus();
|
|
}
|
|
};
|
|
|
|
const resetOtp = () => {
|
|
setOtp(new Array(OTP_LENGTH).fill(""));
|
|
setOtpError("");
|
|
startOtpTimer();
|
|
};
|
|
|
|
const handleMobileSubmit = async () => {
|
|
if (!data.mobileNumber || data.mobileNumber.length !== 10) {
|
|
setMobileNumberError(
|
|
"Please enter a valid 10-digit mobile number before sending OTP"
|
|
);
|
|
return;
|
|
}
|
|
try {
|
|
setMobileNumberError("");
|
|
const res = await sendOtp.mutateAsync(data.mobileNumber);
|
|
const successMessage =
|
|
res?.message ||
|
|
res?.otp ||
|
|
res?.status ||
|
|
"OTP sent successfully";
|
|
toast.success(successMessage, { position: "top-right" });
|
|
setShowOtp(true);
|
|
resetOtp();
|
|
setMobileOtpVerified(false);
|
|
} catch (error) {
|
|
const message = getApiErrorMessage(
|
|
error,
|
|
"Failed to send OTP. Please try again."
|
|
);
|
|
setMobileNumberError(message);
|
|
toast.error(message, { position: "top-right" });
|
|
}
|
|
};
|
|
|
|
const handleChange = (field, value) => {
|
|
const updates = { [field]: value };
|
|
const fieldsToClear = [field];
|
|
if (field === "mobileNumber") {
|
|
setMobileNumberError("");
|
|
setShowOtp(false);
|
|
setOtp(new Array(OTP_LENGTH).fill(""));
|
|
setOtpError("");
|
|
setOtpTimer(0);
|
|
setMobileOtpVerified(false);
|
|
}
|
|
if (field === "religion") {
|
|
updates.caste = "";
|
|
updates.subCaste = "";
|
|
fieldsToClear.push("caste", "subCaste");
|
|
}
|
|
if (field === "caste") {
|
|
updates.subCaste = "";
|
|
fieldsToClear.push("subCaste");
|
|
}
|
|
if (field === "state") {
|
|
updates.city = "";
|
|
fieldsToClear.push("city");
|
|
}
|
|
if (field === "raasi") {
|
|
updates.star = "";
|
|
fieldsToClear.push("star");
|
|
}
|
|
if (field === "password") {
|
|
fieldsToClear.push("confirmPassword");
|
|
}
|
|
dispatch(updatePersonalDetails(updates));
|
|
if (onFieldChange) onFieldChange(fieldsToClear);
|
|
};
|
|
|
|
const isOtpComplete = otp.every((digit) => digit !== "");
|
|
|
|
const passwordStrength = useMemo(() => {
|
|
const value = data.password || "";
|
|
if (!value) return null;
|
|
const rules = [
|
|
{ key: "length", label: "At least 8 characters", ok: value.length >= 8 },
|
|
{ key: "upper", label: "Uppercase letter", ok: /[A-Z]/.test(value) },
|
|
{ key: "lower", label: "Lowercase letter", ok: /[a-z]/.test(value) },
|
|
{ key: "number", label: "Number", ok: /\d/.test(value) },
|
|
{ key: "symbol", label: "Symbol", ok: /[^A-Za-z0-9]/.test(value) },
|
|
{ key: "length12", label: "12+ characters (recommended)", ok: value.length >= 12 },
|
|
];
|
|
const score = rules.filter((rule) => rule.ok).length;
|
|
const percent = Math.round((score / rules.length) * 100);
|
|
|
|
let label = "Weak";
|
|
let color = "#d32f2f";
|
|
if (score >= 5) {
|
|
label = "Strong";
|
|
color = "#2e7d32";
|
|
} else if (score >= 3) {
|
|
label = "Medium";
|
|
color = "#ed6c02";
|
|
}
|
|
|
|
return {
|
|
label,
|
|
color,
|
|
percent,
|
|
rules,
|
|
};
|
|
}, [data.password]);
|
|
|
|
const handleOtpSubmit = async () => {
|
|
if (!isOtpComplete) {
|
|
setOtpError("Complete OTP is required");
|
|
return;
|
|
}
|
|
try {
|
|
const res = await verifyOtp.mutateAsync({
|
|
mobile: data.mobileNumber,
|
|
otp: otp.join(""),
|
|
});
|
|
const successMessage =
|
|
res?.message ||
|
|
res?.otp ||
|
|
res?.status ||
|
|
"OTP verified successfully";
|
|
toast.success(successMessage, { position: "top-right" });
|
|
setMobileOtpVerified(true);
|
|
setMobileNumberError("");
|
|
setOtpError("");
|
|
} catch (error) {
|
|
const message = getApiErrorMessage(
|
|
error,
|
|
"Invalid or expired OTP"
|
|
);
|
|
setOtpError(message);
|
|
toast.error(message, { position: "top-right" });
|
|
}
|
|
};
|
|
|
|
|
|
// file upload
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// const handleSubmit = async () => {
|
|
// if (showOtp && !isOtpComplete) {
|
|
// setOtpError("OTP is required and must be complete");
|
|
// return;
|
|
// }
|
|
// if (showOtp && !mobileOtpVerified) {
|
|
// try {
|
|
// await verifyOtpApi(data.mobileNumber, otp.join(""));
|
|
// setMobileOtpVerified(true);
|
|
// setOtpError("");
|
|
// onSubmitStep();
|
|
// console.log("OTP verified on submit");
|
|
// } catch (err) {
|
|
// setOtpError(err || "OTP verification failed");
|
|
// }
|
|
// return;
|
|
// }
|
|
// onSubmitStep();
|
|
|
|
// };
|
|
|
|
|
|
const handleSubmit = async () => {
|
|
if (showOtp && !isOtpComplete) {
|
|
setOtpError("OTP is required and must be complete");
|
|
return;
|
|
}
|
|
|
|
if (showOtp && !mobileOtpVerified) {
|
|
try {
|
|
await verifyOtp.mutateAsync({
|
|
mobile: data.mobileNumber,
|
|
otp: otp.join(""),
|
|
});
|
|
setMobileOtpVerified(true);
|
|
setOtpError("");
|
|
console.log("Submitting personal details:", data); // log here
|
|
onSubmitStep();
|
|
console.log("OTP verified on submit");
|
|
} catch (err) {
|
|
setOtpError(err || "OTP verification failed");
|
|
}
|
|
return;
|
|
}
|
|
|
|
// no OTP or already verified
|
|
console.log("Submitting personal details:", data); // log here
|
|
onSubmitStep();
|
|
};
|
|
|
|
|
|
const formatTimer = (sec) => {
|
|
const m = Math.floor(sec / 60)
|
|
.toString()
|
|
.padStart(2, "0");
|
|
const s = (sec % 60).toString().padStart(2, "0");
|
|
return `${m}:${s}`;
|
|
};
|
|
const parseDobValue = data.dob ? new Date(data.dob) : null;
|
|
return (
|
|
<>
|
|
<div className="w-full max-w-[1200px] mx-auto bg-[#fff2f2] 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">
|
|
{/* Name */}
|
|
<div className="flex flex-col gap-4">
|
|
<label className="text-gray-900 text-[17px]">
|
|
Enter the Name{requiredMark}
|
|
</label>
|
|
<TextField
|
|
fullWidth
|
|
inputRef={nameInputRef}
|
|
name="name"
|
|
label="Name"
|
|
value={data.name}
|
|
onChange={(e) => handleChange("name", e.target.value)}
|
|
error={Boolean(errors.name)}
|
|
helperText={errors.name}
|
|
placeholder="Enter Name"
|
|
variant="outlined"
|
|
/>
|
|
</div>
|
|
|
|
{/* Gender */}
|
|
<div className="flex flex-col gap-6">
|
|
<label className="text-gray-900 text-[17px]">
|
|
Enter the Gender{requiredMark}
|
|
</label>
|
|
<FormControl
|
|
fullWidth
|
|
variant="outlined"
|
|
error={Boolean(errors.gender)}
|
|
>
|
|
<InputLabel id="gender-label">Gender</InputLabel>
|
|
<Select
|
|
labelId="gender-label"
|
|
label="Gender"
|
|
name="gender"
|
|
value={data.gender}
|
|
onChange={(e) => handleChange("gender", e.target.value)}
|
|
>
|
|
{genderOptions.map((gender) => (
|
|
<MenuItem key={gender} value={gender}>
|
|
{gender}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
{errors.gender && (
|
|
<p
|
|
style={{
|
|
color: "#d32f2f",
|
|
margin: "3px 14px 0 14px",
|
|
fontSize: "0.75rem",
|
|
}}
|
|
>
|
|
{errors.gender}
|
|
</p>
|
|
)}
|
|
</FormControl>
|
|
</div>
|
|
|
|
{/* Mobile Number and Send OTP Button */}
|
|
<div className="flex flex-col gap-6 relative">
|
|
<label className="text-gray-900 text-[17px]">
|
|
Mobile Number{requiredMark}
|
|
</label>
|
|
<Box sx={{ display: "flex", alignItems: "start", gap: 2 }}>
|
|
<TextField
|
|
fullWidth
|
|
name="mobileNumber"
|
|
label="Mobile Number"
|
|
type="tel"
|
|
value={data.mobileNumber}
|
|
onChange={(e) => handleChange("mobileNumber", e.target.value)}
|
|
error={
|
|
Boolean(errors.mobileNumber) || Boolean(mobileNumberError)
|
|
}
|
|
helperText={mobileNumberError || errors.mobileNumber}
|
|
placeholder="Enter Mobile Number"
|
|
inputProps={{ maxLength: 10 }}
|
|
InputProps={{
|
|
endAdornment: mobileOtpVerified ? (
|
|
<InputAdornment position="end">
|
|
<CheckCircleIcon
|
|
sx={{
|
|
color: "#2e7d32",
|
|
animation: "verifiedPulse 1.2s ease-in-out infinite",
|
|
"@keyframes verifiedPulse": {
|
|
"0%": { transform: "scale(1)", opacity: 0.8 },
|
|
"50%": { transform: "scale(1.08)", opacity: 1 },
|
|
"100%": { transform: "scale(1)", opacity: 0.8 },
|
|
},
|
|
}}
|
|
/>
|
|
</InputAdornment>
|
|
) : null,
|
|
}}
|
|
variant="outlined"
|
|
sx={{
|
|
"& .MuiInputBase-input.Mui-disabled": {
|
|
cursor: "not-allowed",
|
|
},
|
|
}}
|
|
/>
|
|
{!showOtp && !mobileOtpVerified && (
|
|
<Button
|
|
variant="outlined"
|
|
color="primary"
|
|
onClick={handleMobileSubmit}
|
|
sx={{
|
|
whiteSpace: "nowrap",
|
|
height: "56px",
|
|
background: "green",
|
|
color: "#fff",
|
|
}}
|
|
>
|
|
Verify
|
|
</Button>
|
|
)}
|
|
</Box>
|
|
</div>
|
|
|
|
{/* OTP Inputs */}
|
|
<div className="flex flex-col gap-6">
|
|
{showOtp && !mobileOtpVerified && (
|
|
<>
|
|
<Box display="flex" gap={1} alignItems="center">
|
|
{otp.map((digit, index) => (
|
|
<TextField
|
|
key={index}
|
|
id={`otp-${index}`}
|
|
inputProps={{
|
|
maxLength: 1,
|
|
style: {
|
|
textAlign: "center",
|
|
width: 40,
|
|
fontSize: 20,
|
|
borderRadius: 4,
|
|
backgroundColor: digit
|
|
? "#028f02"
|
|
: "transparent",
|
|
color: digit ? "#fff" : "#000",
|
|
},
|
|
}}
|
|
value={digit}
|
|
onChange={(e) => handleOtpChange(index, e.target.value)}
|
|
onKeyDown={(e) => handleOtpKeyDown(index, e)}
|
|
error={Boolean(otpError)}
|
|
autoFocus={index === 0}
|
|
variant="outlined"
|
|
/>
|
|
))}
|
|
|
|
<Button
|
|
variant="contained"
|
|
color="secondary"
|
|
onClick={handleOtpSubmit} // new handler to verify OTP
|
|
sx={{ height: 40, ml: 2 }}
|
|
disabled={!isOtpComplete}
|
|
>
|
|
Submit OTP
|
|
</Button>
|
|
</Box>
|
|
|
|
<Typography sx={{ ml: 2, minWidth: 56 }}>
|
|
{otpTimer > 0 ? (
|
|
` ${formatTimer(otpTimer) } Seconds`
|
|
) : (
|
|
<Link
|
|
component="button"
|
|
variant="body2"
|
|
onClick={resetOtp}
|
|
disabled={otpTimer > 0}
|
|
>
|
|
Resend OTP
|
|
</Link>
|
|
)}
|
|
</Typography>
|
|
|
|
{otpError && (
|
|
<Typography color="error" variant="caption" sx={{ mt: "-10px", fontSize:"12px" }}>
|
|
{otpError}
|
|
</Typography>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
{/* Other fields like DOB, height, marital status, etc. */}
|
|
{/* Your other inputs here */}
|
|
{/* DOB */}
|
|
{/* <div className="flex flex-col gap-2">
|
|
<label className="text-gray-900 text-[15px]">Date of Birth</label>
|
|
<TextField
|
|
fullWidth
|
|
name="dob"
|
|
type="date"
|
|
value={data.dob}
|
|
onChange={(e) => handleChange("dob", e.target.value)}
|
|
error={Boolean(errors.dob)}
|
|
helperText={errors.dob}
|
|
InputLabelProps={{ shrink: true }}
|
|
variant="outlined"
|
|
/>
|
|
</div> */}
|
|
|
|
{/* DOB with MUI DatePicker */}
|
|
<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"
|
|
value={parseDobValue}
|
|
onChange={(value) => {
|
|
let formatted = "";
|
|
if (value instanceof Date && !isNaN(value)) {
|
|
const y = value.getFullYear();
|
|
const m = String(value.getMonth() + 1).padStart(2, "0");
|
|
const d = String(value.getDate()).padStart(2, "0");
|
|
formatted = `${y}-${m}-${d}`;
|
|
}
|
|
handleChange("dob", formatted);
|
|
}}
|
|
slotProps={{
|
|
textField: {
|
|
name: "dob",
|
|
fullWidth: true,
|
|
error: Boolean(errors.dob),
|
|
helperText: errors.dob,
|
|
},
|
|
}}
|
|
/>
|
|
</LocalizationProvider>
|
|
</div>
|
|
|
|
{/* Height */}
|
|
<div className="flex flex-col gap-2">
|
|
<label className="text-gray-900 text-[15px]">Height</label>
|
|
<TextField
|
|
fullWidth
|
|
name="height"
|
|
label="Enter Height"
|
|
type="number"
|
|
value={data.height}
|
|
onChange={(e) => handleChange("height", e.target.value)}
|
|
error={Boolean(errors.height)}
|
|
helperText={errors.height}
|
|
inputProps={{ min: 0, max: 10, step: "0.1" }}
|
|
variant="outlined"
|
|
/>
|
|
</div>
|
|
|
|
{/* Weight */}
|
|
<div className="flex flex-col gap-2">
|
|
<label className="text-gray-900 text-[15px]">Weight</label>
|
|
<TextField
|
|
fullWidth
|
|
name="weight"
|
|
label="Enter Weight"
|
|
type="number"
|
|
value={data.weight}
|
|
onChange={(e) => handleChange("weight", e.target.value)}
|
|
error={Boolean(errors.weight)}
|
|
helperText={errors.weight}
|
|
inputProps={{ min: 0, max: 300, step: "0.1" }}
|
|
variant="outlined"
|
|
/>
|
|
</div>
|
|
|
|
{/* Marital Status */}
|
|
<div className="flex flex-col gap-2">
|
|
<label className="text-gray-900 text-[15px]">
|
|
Marital Status{requiredMark}
|
|
</label>
|
|
<FormControl
|
|
fullWidth
|
|
variant="outlined"
|
|
error={Boolean(errors.maritalStatus)}
|
|
>
|
|
<InputLabel id="maritalStatus-label">
|
|
Select Marital Status
|
|
</InputLabel>
|
|
<Select
|
|
labelId="maritalStatus-label"
|
|
label="Select Marital Status"
|
|
name="maritalStatus"
|
|
value={data.maritalStatus}
|
|
onChange={(e) =>
|
|
handleChange("maritalStatus", e.target.value)
|
|
}
|
|
>
|
|
{maritalStatusOptions.map((status) => (
|
|
<MenuItem key={status.id} value={status.id}>
|
|
{status.marital_status_name}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
{errors.maritalStatus && (
|
|
<Typography
|
|
color="error"
|
|
variant="caption"
|
|
sx={{ mt: 0.5, ml: 1.8 }}
|
|
>
|
|
{errors.maritalStatus}
|
|
</Typography>
|
|
)}
|
|
</FormControl>
|
|
</div>
|
|
|
|
{/* Religion */}
|
|
<div className="flex flex-col gap-2">
|
|
<label className="text-gray-900 text-[15px]">
|
|
Religion
|
|
</label>
|
|
<FormControl
|
|
fullWidth
|
|
variant="outlined"
|
|
error={Boolean(errors.religion)}
|
|
>
|
|
<InputLabel id="religion-label">Select Religion</InputLabel>
|
|
<Select
|
|
labelId="religion-label"
|
|
label="Select Religion"
|
|
name="religion"
|
|
value={data.religion}
|
|
onChange={(e) => handleChange("religion", e.target.value)}
|
|
>
|
|
{religionOptions.map((religion) => (
|
|
<MenuItem key={religion.id} value={religion.id}>
|
|
{religion.religion_name}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
{errors.religion && (
|
|
<Typography
|
|
color="error"
|
|
variant="caption"
|
|
sx={{ mt: 0.5, ml: 1.8 }}
|
|
>
|
|
{errors.religion}
|
|
</Typography>
|
|
)}
|
|
</FormControl>
|
|
</div>
|
|
|
|
{/* Profile Created For */}
|
|
<div className="flex flex-col gap-2">
|
|
<label className="text-gray-900 text-[15px]">
|
|
Profile Created For{requiredMark}
|
|
</label>
|
|
<FormControl
|
|
fullWidth
|
|
variant="outlined"
|
|
error={Boolean(errors.profileFor)}
|
|
>
|
|
<InputLabel id="profileFor-label">
|
|
Select Profile Created For
|
|
</InputLabel>
|
|
<Select
|
|
labelId="profileFor-label"
|
|
label="Select Profile Created For"
|
|
name="profileFor"
|
|
value={data.profileFor}
|
|
onChange={(e) => handleChange("profileFor", e.target.value)}
|
|
>
|
|
{profileCreatedForOptions.map((profileFor) => (
|
|
<MenuItem key={profileFor.id} value={profileFor.id}>
|
|
{profileFor.profile_for_name}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
{errors.profileFor && (
|
|
<Typography
|
|
color="error"
|
|
variant="caption"
|
|
sx={{ mt: 0.5, ml: 1.8 }}
|
|
>
|
|
{errors.profileFor}
|
|
</Typography>
|
|
)}
|
|
</FormControl>
|
|
</div>
|
|
|
|
{/* Caste / Community */}
|
|
<div className="flex flex-col gap-2">
|
|
<label className="text-gray-900 text-[15px]">
|
|
Caste / Community{requiredMark}
|
|
</label>
|
|
<FormControl
|
|
fullWidth
|
|
variant="outlined"
|
|
error={Boolean(errors.caste)}
|
|
>
|
|
<InputLabel id="caste-label">Select Caste / Community</InputLabel>
|
|
<Select
|
|
labelId="caste-label"
|
|
label="Select Caste / Community"
|
|
name="caste"
|
|
value={data.caste}
|
|
onChange={(e) => handleChange("caste", e.target.value)}
|
|
disabled={!data.religion || casteQuery.isLoading}
|
|
sx={{
|
|
"& .MuiSelect-select.Mui-disabled": {
|
|
cursor: "not-allowed",
|
|
},
|
|
}}
|
|
>
|
|
{casteOptions.map((caste) => (
|
|
<MenuItem key={caste.id} value={caste.id}>
|
|
{getOptionLabel(caste, "Caste")}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
{errors.caste && (
|
|
<Typography
|
|
color="error"
|
|
variant="caption"
|
|
sx={{ mt: 0.5, ml: 1.8 }}
|
|
>
|
|
{errors.caste}
|
|
</Typography>
|
|
)}
|
|
</FormControl>
|
|
</div>
|
|
|
|
{/* Sub-Caste (optional) */}
|
|
<div className="flex flex-col gap-2">
|
|
<label className="text-gray-900 text-[15px]">
|
|
Sub-Caste (optional)
|
|
</label>
|
|
<FormControl fullWidth variant="outlined">
|
|
<InputLabel id="subCaste-label">
|
|
Select Sub-Caste (optional)
|
|
</InputLabel>
|
|
<Select
|
|
labelId="subCaste-label"
|
|
label="Select Sub-Caste (optional)"
|
|
name="subCaste"
|
|
value={data.subCaste}
|
|
onChange={(e) => handleChange("subCaste", e.target.value)}
|
|
disabled={!data.caste || subCasteQuery.isLoading}
|
|
sx={{
|
|
"& .MuiSelect-select.Mui-disabled": {
|
|
cursor: "not-allowed",
|
|
},
|
|
}}
|
|
>
|
|
<MenuItem value="">
|
|
<em>None</em>
|
|
</MenuItem>
|
|
{subCasteOptions.map((subCaste) => (
|
|
<MenuItem key={subCaste.id} value={subCaste.id}>
|
|
{getOptionLabel(subCaste, "Sub Caste")}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
</FormControl>
|
|
</div>
|
|
|
|
{/* Gothram (optional) */}
|
|
<div className="flex flex-col gap-2">
|
|
<label className="text-gray-900 text-[15px]">
|
|
Gothram (optional)
|
|
</label>
|
|
<FormControl fullWidth variant="outlined">
|
|
<InputLabel id="gothram-label">
|
|
Select Gothram (optional)
|
|
</InputLabel>
|
|
<Select
|
|
labelId="gothram-label"
|
|
label="Select Gothram (optional)"
|
|
name="gothram"
|
|
value={data.gothram}
|
|
onChange={(e) => handleChange("gothram", e.target.value)}
|
|
>
|
|
<MenuItem value="">
|
|
<em>None</em>
|
|
</MenuItem>
|
|
{gothramOptions.map((gothram) => (
|
|
<MenuItem key={gothram.id} value={gothram.id}>
|
|
{gothram.gothram_name}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
</FormControl>
|
|
</div>
|
|
|
|
{/* Raasi */}
|
|
<div className="flex flex-col gap-2">
|
|
<label className="text-gray-900 text-[15px]">Raasi</label>
|
|
<FormControl
|
|
fullWidth
|
|
variant="outlined"
|
|
error={Boolean(errors.raasi)}
|
|
>
|
|
<InputLabel id="raasi-label">Select Raasi</InputLabel>
|
|
<Select
|
|
labelId="raasi-label"
|
|
label="Select Raasi"
|
|
name="raasi"
|
|
value={data.raasi}
|
|
onChange={(e) => handleChange("raasi", e.target.value)}
|
|
>
|
|
{raasiOptions.map((raasi) => (
|
|
<MenuItem key={raasi.id} value={raasi.id}>
|
|
{raasi.raasi_name}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
{errors.raasi && (
|
|
<Typography
|
|
color="error"
|
|
variant="caption"
|
|
sx={{ mt: 0.5, ml: 1.8 }}
|
|
>
|
|
{errors.raasi}
|
|
</Typography>
|
|
)}
|
|
</FormControl>
|
|
</div>
|
|
|
|
{/* Star */}
|
|
<div className="flex flex-col gap-2">
|
|
<label className="text-gray-900 text-[15px]">Star</label>
|
|
<FormControl
|
|
fullWidth
|
|
variant="outlined"
|
|
error={Boolean(errors.star)}
|
|
>
|
|
<InputLabel id="star-label">Select Star</InputLabel>
|
|
<Select
|
|
labelId="star-label"
|
|
label="Select Star"
|
|
name="star"
|
|
value={data.star}
|
|
onChange={(e) => handleChange("star", e.target.value)}
|
|
disabled={!data.raasi || starQuery.isLoading}
|
|
>
|
|
{starOptions.map((star) => (
|
|
<MenuItem key={star.id} value={star.id}>
|
|
{getOptionLabel(star, "Star")}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
{errors.star && (
|
|
<Typography
|
|
color="error"
|
|
variant="caption"
|
|
sx={{ mt: 0.5, ml: 1.8 }}
|
|
>
|
|
{errors.star}
|
|
</Typography>
|
|
)}
|
|
</FormControl>
|
|
</div>
|
|
|
|
|
|
|
|
{/* Email Id */}
|
|
<div className="flex flex-col gap-2">
|
|
<label className="text-gray-900 text-[15px]">
|
|
Email Id{requiredMark}
|
|
</label>
|
|
<TextField
|
|
fullWidth
|
|
name="email"
|
|
label="Enter Email Id"
|
|
value={data.email}
|
|
onChange={(e) => handleChange("email", e.target.value)}
|
|
error={Boolean(errors.email)}
|
|
helperText={errors.email}
|
|
variant="outlined"
|
|
/>
|
|
</div>
|
|
|
|
{/* Password */}
|
|
{!isStep1Update && (
|
|
<div className="flex flex-col gap-2">
|
|
<label className="text-gray-900 text-[15px]">
|
|
Password{requiredMark}
|
|
</label>
|
|
<TextField
|
|
fullWidth
|
|
type={showPassword ? "text" : "password"}
|
|
name="password"
|
|
label="Enter Password"
|
|
value={data.password}
|
|
onChange={(e) => handleChange("password", e.target.value)}
|
|
error={Boolean(errors.password)}
|
|
helperText={errors.password}
|
|
InputProps={{
|
|
endAdornment: (
|
|
<InputAdornment position="end">
|
|
<IconButton
|
|
onClick={() => setShowPassword((prev) => !prev)}
|
|
edge="end"
|
|
aria-label="toggle password visibility"
|
|
>
|
|
{showPassword ? <VisibilityOff /> : <Visibility />}
|
|
</IconButton>
|
|
</InputAdornment>
|
|
),
|
|
}}
|
|
variant="outlined"
|
|
/>
|
|
{passwordStrength && (
|
|
<div className="rounded-lg border border-gray-200 bg-white/70 p-3">
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span className="text-gray-600">Password strength</span>
|
|
<span
|
|
style={{
|
|
color: passwordStrength.color,
|
|
fontWeight: 600,
|
|
}}
|
|
>
|
|
{passwordStrength.label}
|
|
</span>
|
|
</div>
|
|
<div className="mt-2 h-2 w-full rounded-full bg-gray-200 overflow-hidden">
|
|
<div
|
|
style={{
|
|
width: `${passwordStrength.percent}%`,
|
|
height: "100%",
|
|
background: passwordStrength.color,
|
|
transition: "width 200ms ease",
|
|
}}
|
|
/>
|
|
</div>
|
|
<div className="mt-2 text-xs text-gray-500">
|
|
Recommended: 12+ characters with a mix of upper/lowercase,
|
|
numbers, and symbols.
|
|
</div>
|
|
<div className="mt-2 grid grid-cols-1 sm:grid-cols-2 gap-y-1 gap-x-3 text-xs">
|
|
{passwordStrength.rules.map((rule) => (
|
|
<div
|
|
key={rule.key}
|
|
className="flex items-center gap-2"
|
|
>
|
|
{rule.ok ? (
|
|
<CheckCircleIcon sx={{ fontSize: 14, color: "#2e7d32" }} />
|
|
) : (
|
|
<span
|
|
style={{
|
|
width: 12,
|
|
height: 12,
|
|
borderRadius: "999px",
|
|
border: "1px solid #d1d5db",
|
|
display: "inline-block",
|
|
}}
|
|
/>
|
|
)}
|
|
<span
|
|
style={{
|
|
color: rule.ok ? "#111827" : "#6b7280",
|
|
}}
|
|
>
|
|
{rule.label}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Confirm Password */}
|
|
{!isStep1Update && (
|
|
<div className="flex flex-col gap-2">
|
|
<label className="text-gray-900 text-[15px]">
|
|
Confirm Password{requiredMark}
|
|
</label>
|
|
<TextField
|
|
fullWidth
|
|
type={showConfirmPassword ? "text" : "password"}
|
|
name="confirmPassword"
|
|
label="Confirm Password"
|
|
value={data.confirmPassword}
|
|
onChange={(e) => handleChange("confirmPassword", e.target.value)}
|
|
error={Boolean(errors.confirmPassword)}
|
|
helperText={errors.confirmPassword}
|
|
InputProps={{
|
|
endAdornment: (
|
|
<InputAdornment position="end">
|
|
<IconButton
|
|
onClick={() => setShowConfirmPassword((prev) => !prev)}
|
|
edge="end"
|
|
aria-label="toggle confirm password visibility"
|
|
>
|
|
{showConfirmPassword ? <VisibilityOff /> : <Visibility />}
|
|
</IconButton>
|
|
</InputAdornment>
|
|
),
|
|
}}
|
|
variant="outlined"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* State */}
|
|
<div className="flex flex-col gap-2">
|
|
<label className="text-gray-900 text-[15px]">
|
|
State{requiredMark}
|
|
</label>
|
|
<FormControl
|
|
fullWidth
|
|
variant="outlined"
|
|
error={Boolean(errors.state)}
|
|
>
|
|
<InputLabel id="state-label">Select State</InputLabel>
|
|
<Select
|
|
labelId="state-label"
|
|
label="Select State"
|
|
name="state"
|
|
value={data.state}
|
|
onChange={(e) => handleChange("state", e.target.value)}
|
|
disabled={isPersonalMastersLoading}
|
|
sx={{
|
|
"& .MuiSelect-select.Mui-disabled": {
|
|
cursor: "not-allowed",
|
|
},
|
|
}}
|
|
>
|
|
{stateOptions.map((state) => (
|
|
<MenuItem key={state.id} value={state.id}>
|
|
{state.state_name}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
{errors.state && (
|
|
<Typography
|
|
color="error"
|
|
variant="caption"
|
|
sx={{ mt: 0.5, ml: 1.8 }}
|
|
>
|
|
{errors.state}
|
|
</Typography>
|
|
)}
|
|
</FormControl>
|
|
</div>
|
|
|
|
{/* City */}
|
|
<div className="flex flex-col gap-2">
|
|
<label className="text-gray-900 text-[15px]">
|
|
City{requiredMark}
|
|
</label>
|
|
<FormControl
|
|
fullWidth
|
|
variant="outlined"
|
|
error={Boolean(errors.city)}
|
|
>
|
|
<InputLabel id="city-label">Select City</InputLabel>
|
|
<Select
|
|
labelId="city-label"
|
|
label="Select City"
|
|
name="city"
|
|
value={data.city}
|
|
onChange={(e) => handleChange("city", e.target.value)}
|
|
disabled={!data.state || cityQuery.isLoading}
|
|
sx={{
|
|
"& .MuiSelect-select.Mui-disabled": {
|
|
cursor: "not-allowed",
|
|
},
|
|
}}
|
|
>
|
|
{cityOptions.map((city) => (
|
|
<MenuItem key={city.id} value={city.id}>
|
|
{getOptionLabel(city.district_name, "City")}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
{errors.city && (
|
|
<Typography
|
|
color="error"
|
|
variant="caption"
|
|
sx={{ mt: 0.5, ml: 1.8 }}
|
|
>
|
|
{errors.city}
|
|
</Typography>
|
|
)}
|
|
</FormControl>
|
|
</div>
|
|
|
|
{/* Pin code */}
|
|
<div className="flex flex-col gap-2">
|
|
<label className="text-gray-900 text-[15px]">
|
|
Pin code{requiredMark}
|
|
</label>
|
|
<TextField
|
|
fullWidth
|
|
name="pincode"
|
|
label="Enter Pin code"
|
|
value={data.pincode}
|
|
onChange={(e) => handleChange("pincode", e.target.value)}
|
|
error={Boolean(errors.pincode)}
|
|
helperText={errors.pincode}
|
|
inputProps={{ maxLength: 6 }}
|
|
variant="outlined"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-2">
|
|
{/* Upload Profile (UI only) */}
|
|
{/* <div className="flex flex-col gap-2 md:col-span-2 w-full max-w-[900px] ">
|
|
<label className="text-gray-900 text-[15px]">
|
|
Upload Profile (Multi - Select)
|
|
</label>
|
|
<div className=" border-2 border-dashed border-gray-300 rounded-lg p-6 text-center bg-white">
|
|
<div className="text-3xl mb-2">⬆</div>
|
|
<p className="text-sm text-gray-600">Upload PDF, IMG, JPG</p>
|
|
</div>
|
|
</div> */}
|
|
|
|
|
|
<div className="flex flex-col gap-2 md:col-span-2 w-full">
|
|
<label className="text-gray-900 text-[15px]">
|
|
Upload Profile (Max 3 images, 10 MB each)
|
|
</label>
|
|
<AdvancedDropzone
|
|
value={data.profiles || []}
|
|
onChange={(files) => {
|
|
// if you want to keep in Redux as plain metadata
|
|
dispatch(updatePersonalDetails({ profiles: files }));
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Grid
|
|
item
|
|
xs={12}
|
|
style={{
|
|
marginTop: "40px",
|
|
display: "flex",
|
|
gap: 16,
|
|
justifyContent: "center",
|
|
}}
|
|
>
|
|
|
|
<Button
|
|
variant="contained"
|
|
color="primary"
|
|
onClick={handleSubmit}
|
|
// disabled={!mobileOtpVerified}
|
|
>
|
|
Submit
|
|
</Button>
|
|
</Grid>
|
|
</form>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default PersonalDetailsForm;
|