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 = *; 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 ( <>
{/* Name */}
handleChange("name", e.target.value)} error={Boolean(errors.name)} helperText={errors.name} placeholder="Enter Name" variant="outlined" />
{/* Gender */}
Gender {errors.gender && (

{errors.gender}

)}
{/* Mobile Number and Send OTP Button */}
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 ? ( ) : null, }} variant="outlined" sx={{ "& .MuiInputBase-input.Mui-disabled": { cursor: "not-allowed", }, }} /> {!showOtp && !mobileOtpVerified && ( )}
{/* OTP Inputs */}
{showOtp && !mobileOtpVerified && ( <> {otp.map((digit, index) => ( handleOtpChange(index, e.target.value)} onKeyDown={(e) => handleOtpKeyDown(index, e)} error={Boolean(otpError)} autoFocus={index === 0} variant="outlined" /> ))} {otpTimer > 0 ? ( ` ${formatTimer(otpTimer) } Seconds` ) : ( 0} > Resend OTP )} {otpError && ( {otpError} )} )}
{/* Other fields like DOB, height, marital status, etc. */} {/* Your other inputs here */} {/* DOB */} {/*
handleChange("dob", e.target.value)} error={Boolean(errors.dob)} helperText={errors.dob} InputLabelProps={{ shrink: true }} variant="outlined" />
*/} {/* DOB with MUI DatePicker */}
{ 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, }, }} />
{/* Height */}
handleChange("height", e.target.value)} error={Boolean(errors.height)} helperText={errors.height} inputProps={{ min: 0, max: 10, step: "0.1" }} variant="outlined" />
{/* Weight */}
handleChange("weight", e.target.value)} error={Boolean(errors.weight)} helperText={errors.weight} inputProps={{ min: 0, max: 300, step: "0.1" }} variant="outlined" />
{/* Marital Status */}
Select Marital Status {errors.maritalStatus && ( {errors.maritalStatus} )}
{/* Religion */}
Select Religion {errors.religion && ( {errors.religion} )}
{/* Profile Created For */}
Select Profile Created For {errors.profileFor && ( {errors.profileFor} )}
{/* Caste / Community */}
Select Caste / Community {errors.caste && ( {errors.caste} )}
{/* Sub-Caste (optional) */}
Select Sub-Caste (optional)
{/* Gothram (optional) */}
Select Gothram (optional)
{/* Raasi */}
Select Raasi {errors.raasi && ( {errors.raasi} )}
{/* Star */}
Select Star {errors.star && ( {errors.star} )}
{/* Email Id */}
handleChange("email", e.target.value)} error={Boolean(errors.email)} helperText={errors.email} variant="outlined" />
{/* Password */} {!isStep1Update && (
handleChange("password", e.target.value)} error={Boolean(errors.password)} helperText={errors.password} InputProps={{ endAdornment: ( setShowPassword((prev) => !prev)} edge="end" aria-label="toggle password visibility" > {showPassword ? : } ), }} variant="outlined" /> {passwordStrength && (
Password strength {passwordStrength.label}
Recommended: 12+ characters with a mix of upper/lowercase, numbers, and symbols.
{passwordStrength.rules.map((rule) => (
{rule.ok ? ( ) : ( )} {rule.label}
))}
)}
)} {/* Confirm Password */} {!isStep1Update && (
handleChange("confirmPassword", e.target.value)} error={Boolean(errors.confirmPassword)} helperText={errors.confirmPassword} InputProps={{ endAdornment: ( setShowConfirmPassword((prev) => !prev)} edge="end" aria-label="toggle confirm password visibility" > {showConfirmPassword ? : } ), }} variant="outlined" />
)} {/* State */}
Select State {errors.state && ( {errors.state} )}
{/* City */}
Select City {errors.city && ( {errors.city} )}
{/* Pin code */}
handleChange("pincode", e.target.value)} error={Boolean(errors.pincode)} helperText={errors.pincode} inputProps={{ maxLength: 6 }} variant="outlined" />
{/* Upload Profile (UI only) */} {/*

Upload PDF, IMG, JPG

*/}
{ // if you want to keep in Redux as plain metadata dispatch(updatePersonalDetails({ profiles: files })); }} />
); }; export default PersonalDetailsForm;