api registration done

This commit is contained in:
meena committed 2026-03-01 19:47:06 +05:30
1 parent fd0a327b13
commit 653055debc
38 files changed
+3411 -1385

No files matched your search

+375 -62
View File
@@ -1,19 +1,102 @@
import React, { useEffect, useRef } from "react";
import React, { useEffect, useMemo, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { updateEducationalDetails } from "../redux/registrationFormSlice";
import { TextField, Button, Grid } from "@mui/material";
import {
TextField,
Button,
Grid,
FormControl,
InputLabel,
Select,
MenuItem,
} from "@mui/material";
import { useEducationMasters, useEducationList } from "../hooks/useMasters";
const EducationalDetailsForm = ({ onSubmitStep, onSkipStep, errors }) => {
const EducationalDetailsForm = ({
onSubmitStep,
onSkipStep,
errors,
onFieldChange,
}) => {
const dispatch = useDispatch();
const data = useSelector((state) => state.registerform.educationalDetails);
const inputRef = useRef(null);
const requiredMark = <span style={{ color: "#d32f2f" }}> *</span>;
const { data: educationMasters, isLoading: isEducationMastersLoading } =
useEducationMasters();
const educationListQuery = useEducationList(data.fieldOfStudy);
const studyFieldOptions = useMemo(() => {
const raw = educationMasters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.studyFields || raw.study_fields || raw.fieldOfStudy || [];
}, [educationMasters]);
const qualificationOptions = useMemo(() => {
const raw = educationListQuery.data;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.education || raw.data || [];
}, [educationListQuery.data]);
const occupationOptions = useMemo(() => {
const raw = educationMasters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.occupation || raw.occupations || [];
}, [educationMasters]);
const employeeTypeOptions = useMemo(() => {
const raw = educationMasters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.employeeType || raw.employee_type || [];
}, [educationMasters]);
const annualIncomeOptions = useMemo(() => {
const raw = educationMasters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.annualIncome || raw.annual_income || [];
}, [educationMasters]);
const workLocationOptions = useMemo(() => {
const raw = educationMasters;
if (!raw) return [];
if (Array.isArray(raw)) return [];
return raw.workLocation || raw.work_location || raw.workLocations || [];
}, [educationMasters]);
const getOptionLabel = (item, fallback = "") => {
if (!item) return fallback;
if (typeof item === "string") return item;
return (
item.study_field_name ||
item.education_name ||
item.occupation_name ||
item.employee_type_name ||
item.annual_income_name ||
item.work_location_name ||
item.name ||
fallback
);
};
useEffect(() => {
inputRef.current?.focus();
}, []);
const handleChange = (field, value) => {
dispatch(updateEducationalDetails({ [field]: value }));
const updates = { [field]: value };
const fieldsToClear = [field];
if (field === "fieldOfStudy") {
updates.qualification = "";
fieldsToClear.push("qualification");
}
dispatch(updateEducationalDetails(updates));
if (onFieldChange) onFieldChange(fieldsToClear);
};
const handleSubmit = () => {
@@ -26,58 +109,168 @@ const EducationalDetailsForm = ({ onSubmitStep, onSkipStep, errors }) => {
<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">
{/* Highest Qualification */}
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">Enter Highest Qualification</label>
<TextField
fullWidth
inputRef={inputRef}
name="qualification"
label="Highest Qualification"
value={data.qualification}
onChange={(e) => handleChange("qualification", e.target.value)}
error={Boolean(errors.qualification)}
helperText={errors.qualification}
placeholder="Enter Highest Qualification"
variant="outlined"
/>
</div>
{/* Field of Study */}
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">Enter Field of Study</label>
<label className="text-gray-900 text-[15px]">
Field of Study{requiredMark}
</label>
<FormControl
fullWidth
variant="outlined"
error={Boolean(errors.fieldOfStudy)}
>
<InputLabel id="fieldOfStudy-label">
Select Field of Study
</InputLabel>
<Select
labelId="fieldOfStudy-label"
label="Select Field of Study"
name="fieldOfStudy"
value={data.fieldOfStudy}
onChange={(e) => handleChange("fieldOfStudy", e.target.value)}
inputRef={inputRef}
disabled={isEducationMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
{studyFieldOptions.map((field) => (
<MenuItem key={field.id ?? field} value={field.id ?? field}>
{getOptionLabel(field, "Field of Study")}
</MenuItem>
))}
</Select>
{errors.fieldOfStudy && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.fieldOfStudy}
</p>
)}
</FormControl>
</div>
{/* Highest Qualification */}
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Highest Educational Qualification{requiredMark}
</label>
<FormControl
fullWidth
variant="outlined"
error={Boolean(errors.qualification)}
>
<InputLabel id="qualification-label">
Select Highest Qualification
</InputLabel>
<Select
labelId="qualification-label"
label="Select Highest Qualification"
name="qualification"
value={data.qualification}
onChange={(e) => handleChange("qualification", e.target.value)}
disabled={
!data.fieldOfStudy || educationListQuery.isLoading
}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
{qualificationOptions.map((item) => (
<MenuItem key={item.id ?? item} value={item.id ?? item}>
{getOptionLabel(item, "Qualification")}
</MenuItem>
))}
</Select>
{errors.qualification && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.qualification}
</p>
)}
</FormControl>
</div>
{/* College Name */}
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
College Name
</label>
<TextField
fullWidth
name="fieldOfStudy"
label="Field of Study"
value={data.fieldOfStudy}
onChange={(e) => handleChange("fieldOfStudy", e.target.value)}
error={Boolean(errors.fieldOfStudy)}
helperText={errors.fieldOfStudy}
placeholder="Enter Field of Study"
name="collegeName"
label="College Name"
value={data.collegeName}
onChange={(e) => handleChange("collegeName", e.target.value)}
error={Boolean(errors.collegeName)}
helperText={errors.collegeName}
placeholder="Enter College Name"
variant="outlined"
/>
</div>
{/* Occupation */}
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">Enter Occupation</label>
<TextField
<label className="text-gray-900 text-[15px]">
Occupation{requiredMark}
</label>
<FormControl
fullWidth
name="occupation"
label="Occupation"
value={data.occupation}
onChange={(e) => handleChange("occupation", e.target.value)}
error={Boolean(errors.occupation)}
helperText={errors.occupation}
placeholder="Enter Occupation"
variant="outlined"
/>
error={Boolean(errors.occupation)}
>
<InputLabel id="occupation-label">Select Occupation</InputLabel>
<Select
labelId="occupation-label"
label="Select Occupation"
name="occupation"
value={data.occupation}
onChange={(e) => handleChange("occupation", e.target.value)}
disabled={isEducationMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
{occupationOptions.map((item) => (
<MenuItem key={item.id ?? item} value={item.id ?? item}>
{getOptionLabel(item, "Occupation")}
</MenuItem>
))}
</Select>
{errors.occupation && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.occupation}
</p>
)}
</FormControl>
</div>
{/* Company / Organization Name */}
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">Enter Company / Organization Name</label>
<label className="text-gray-900 text-[15px]">
Company / Organization Name{requiredMark}
</label>
<TextField
fullWidth
name="organization"
@@ -91,36 +284,156 @@ const EducationalDetailsForm = ({ onSubmitStep, onSkipStep, errors }) => {
/>
</div>
{/* Employee Type */}
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Employee Type{requiredMark}
</label>
<FormControl
fullWidth
variant="outlined"
error={Boolean(errors.employeeType)}
>
<InputLabel id="employeeType-label">
Select Employee Type
</InputLabel>
<Select
labelId="employeeType-label"
label="Select Employee Type"
name="employeeType"
value={data.employeeType}
onChange={(e) => handleChange("employeeType", e.target.value)}
disabled={isEducationMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
{employeeTypeOptions.map((item) => (
<MenuItem key={item.id ?? item} value={item.id ?? item}>
{getOptionLabel(item, "Employee Type")}
</MenuItem>
))}
</Select>
{errors.employeeType && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.employeeType}
</p>
)}
</FormControl>
</div>
{/* Annual Income */}
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">Enter Annual Income</label>
<TextField
<label className="text-gray-900 text-[15px]">
Annual Income{requiredMark}
</label>
<FormControl
fullWidth
name="income"
label="Annual Income"
value={data.income}
onChange={(e) => handleChange("income", e.target.value)}
error={Boolean(errors.income)}
helperText={errors.income}
placeholder="Enter Annual Income"
variant="outlined"
/>
error={Boolean(errors.income)}
>
<InputLabel id="income-label">Select Annual Income</InputLabel>
<Select
labelId="income-label"
label="Select Annual Income"
name="income"
value={data.income}
onChange={(e) => handleChange("income", e.target.value)}
disabled={isEducationMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
{annualIncomeOptions.map((item) => (
<MenuItem key={item.id ?? item} value={item.id ?? item}>
{getOptionLabel(item, "Annual Income")}
</MenuItem>
))}
</Select>
{errors.income && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.income}
</p>
)}
</FormControl>
</div>
{/* Work Location */}
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">Enter Work Location</label>
<TextField
fullWidth
name="workLocation"
label="Work Location"
value={data.workLocation}
onChange={(e) => handleChange("workLocation", e.target.value)}
error={Boolean(errors.workLocation)}
helperText={errors.workLocation}
placeholder="Enter Work Location"
variant="outlined"
/>
<label className="text-gray-900 text-[15px]">
Work Location
</label>
{workLocationOptions.length > 0 ? (
<FormControl
fullWidth
variant="outlined"
error={Boolean(errors.workLocation)}
>
<InputLabel id="workLocation-label">
Select Work Location
</InputLabel>
<Select
labelId="workLocation-label"
label="Select Work Location"
name="workLocation"
value={data.workLocation}
onChange={(e) =>
handleChange("workLocation", e.target.value)
}
disabled={isEducationMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
{workLocationOptions.map((item) => (
<MenuItem key={item.id ?? item} value={item.id ?? item}>
{getOptionLabel(item, "Work Location")}
</MenuItem>
))}
</Select>
{errors.workLocation && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.workLocation}
</p>
)}
</FormControl>
) : (
<TextField
fullWidth
name="workLocation"
label="Work Location"
value={data.workLocation}
onChange={(e) => handleChange("workLocation", e.target.value)}
error={Boolean(errors.workLocation)}
helperText={errors.workLocation}
placeholder="Enter Work Location"
variant="outlined"
/>
)}
</div>
</div>
+378 -132
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useRef } from "react";
import React, { useEffect, useMemo, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { updateFamilyDetails } from "../redux/registrationFormSlice";
import {
@@ -9,19 +9,91 @@ import {
Select,
MenuItem,
Button,
Box,
} from "@mui/material";
import { useFamilyMasters } from "../hooks/useMasters";
const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors }) => {
const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange }) => {
const dispatch = useDispatch();
const data = useSelector((state) => state.registerform.familyDetails);
const inputRef = useRef(null);
const requiredMark = <span style={{ color: "#d32f2f" }}> *</span>;
const { data: familyMasters, isLoading: isFamilyMastersLoading } =
useFamilyMasters();
const occupationOptions = useMemo(() => {
const raw = familyMasters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.occupation || raw.occupations || [];
}, [familyMasters]);
const maritalStatusOptions = useMemo(() => {
const raw = familyMasters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.maritalStatus || raw.marital_status || [];
}, [familyMasters]);
const familyStatusOptions = useMemo(() => {
const raw = familyMasters;
if (!raw) return [];
if (Array.isArray(raw)) return [];
return raw.familyStatus || raw.family_status || [];
}, [familyMasters]);
useEffect(() => {
inputRef.current?.focus();
}, []);
const createSibling = () => ({
name: "",
occupation: "",
maritalStatus: "",
haveChildrens: "",
});
const syncSiblingArray = (arr, count) => {
const target = Math.max(0, Number(count) || 0);
const next = [...(arr || [])];
while (next.length < target) {
next.push(createSibling());
}
if (next.length > target) {
next.length = target;
}
return next;
};
const handleChange = (field, value) => {
dispatch(updateFamilyDetails({ [field]: value }));
const updates = { [field]: value };
const fieldsToClear = [field];
if (field === "brotherCount") {
const count = Number(value) || 0;
updates.brotherCount = count;
updates.brothers = syncSiblingArray(data.brothers, count);
fieldsToClear.push("brothers");
}
if (field === "sisterCount") {
const count = Number(value) || 0;
updates.sisterCount = count;
updates.sisters = syncSiblingArray(data.sisters, count);
fieldsToClear.push("sisters");
}
dispatch(updateFamilyDetails(updates));
if (onFieldChange) onFieldChange(fieldsToClear);
};
const handleSiblingChange = (type, index, field, value) => {
const list = [...(data[type] || [])];
if (!list[index]) list[index] = createSibling();
list[index] = { ...list[index], [field]: value };
dispatch(updateFamilyDetails({ [type]: list }));
if (onFieldChange) onFieldChange(type);
};
const handleSubmit = () => {
@@ -29,155 +101,329 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors }) => {
onSubmitStep();
};
return (
const countOptions = Array.from({ length: 11 }, (_, i) => i);
<>
<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">
{/* Highest Qualification */}
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">Enter Father Name</label>
<TextField
const renderSiblingCard = (type, index) => {
const sibling = (data[type] || [])[index] || createSibling();
const labelPrefix = type === "brothers" ? "Brother" : "Sister";
return (
<Box
key={`${type}-${index}`}
sx={{
border: "1px solid #e5e7eb",
borderRadius: 2,
padding: 2,
backgroundColor: "#fff",
}}
>
<div className="text-gray-900 text-[14px] font-semibold mb-3">
{labelPrefix} {index + 1}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
<TextField
fullWidth
inputRef={inputRef}
name="fatherName"
label="Father Name"
value={data.fatherName}
onChange={(e) => handleChange("fatherName", e.target.value)}
error={Boolean(errors.fatherName)}
helperText={errors.fatherName}
placeholder="Enter Father Name"
variant="outlined"
/>
</div>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">Enter Father Occupation</label>
<TextField
fullWidth
name="fatherOccupation"
label="Father Occupation"
value={data.fatherOccupation}
onChange={(e) => handleChange("fatherOccupation", e.target.value)}
error={Boolean(errors.fatherOccupation)}
helperText={errors.fatherOccupation}
placeholder="Enter Father Occupation"
variant="outlined"
/>
</div>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">Enter Mother Name</label>
<TextField
fullWidth
name="motherName"
label="Mother Name"
value={data.motherName}
onChange={(e) => handleChange("motherName", e.target.value)}
error={Boolean(errors.motherName)}
helperText={errors.motherName}
placeholder="Enter Mother Name"
label="Name"
value={sibling.name}
onChange={(e) =>
handleSiblingChange(type, index, "name", e.target.value)
}
variant="outlined"
/>
</div>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">Enter Mother Occupation</label>
<TextField
fullWidth
name="motherOccupation"
label="Mother Occupation"
value={data.motherOccupation}
onChange={(e) => handleChange("motherOccupation", e.target.value)}
error={Boolean(errors.motherOccupation)}
helperText={errors.motherOccupation}
placeholder="Enter Mother Occupation"
variant="outlined"
/>
</div>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">Enter Number of Brothers / Sisters</label>
<TextField
fullWidth
name="siblings"
label="Number of Brothers / Sisters"
value={data.siblings}
onChange={(e) => handleChange("siblings", e.target.value)}
error={Boolean(errors.siblings)}
helperText={errors.siblings}
placeholder="Enter Number of Brothers / Sisters"
variant="outlined"
/>
</div>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]"> Brothers / Sisters (Married / Unmarried)</label>
<FormControl
fullWidth
variant="outlined"
error={Boolean(errors.siblingsStatus)}
>
<InputLabel id="siblingsStatus-label">
Brothers / Sisters (Married / Unmarried)
<FormControl fullWidth variant="outlined">
<InputLabel id={`${type}-${index}-occupation-label`}>
Occupation
</InputLabel>
<Select
labelId="siblingsStatus-label"
label="Brothers / Sisters (Married / Unmarried)"
name="siblingsStatus"
value={data.siblingsStatus}
onChange={(e) => handleChange("siblingsStatus", e.target.value)}
labelId={`${type}-${index}-occupation-label`}
label="Occupation"
value={sibling.occupation}
onChange={(e) =>
handleSiblingChange(type, index, "occupation", e.target.value)
}
disabled={isFamilyMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
<MenuItem value="All Married">All Married</MenuItem>
<MenuItem value="All Unmarried">All Unmarried</MenuItem>
<MenuItem value="Mixed">Mixed</MenuItem>
{occupationOptions.map((opt) => (
<MenuItem key={opt} value={opt}>
{opt}
</MenuItem>
))}
</Select>
{errors.siblingsStatus && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.siblingsStatus}
</p>
)}
</FormControl>
</div>
<FormControl fullWidth variant="outlined">
<InputLabel id={`${type}-${index}-marital-label`}>
Marital Status
</InputLabel>
<Select
labelId={`${type}-${index}-marital-label`}
label="Marital Status"
value={sibling.maritalStatus}
onChange={(e) =>
handleSiblingChange(type, index, "maritalStatus", e.target.value)
}
disabled={isFamilyMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
{maritalStatusOptions.map((opt) => (
<MenuItem key={opt} value={opt}>
{opt}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl fullWidth variant="outlined">
<InputLabel id={`${type}-${index}-children-label`}>
Have Children
</InputLabel>
<Select
labelId={`${type}-${index}-children-label`}
label="Have Children"
value={sibling.haveChildrens}
onChange={(e) =>
handleSiblingChange(
type,
index,
"haveChildrens",
e.target.value
)
}
>
<MenuItem value={1}>Yes</MenuItem>
<MenuItem value={0}>No</MenuItem>
</Select>
</FormControl>
</div>
</Box>
);
};
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">
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Father Name{requiredMark}
</label>
<TextField
fullWidth
inputRef={inputRef}
name="fatherName"
label="Father Name"
value={data.fatherName}
onChange={(e) => handleChange("fatherName", e.target.value)}
error={Boolean(errors.fatherName)}
helperText={errors.fatherName}
placeholder="Enter Father Name"
variant="outlined"
/>
</div>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Father Occupation
</label>
<TextField
fullWidth
name="fatherOccupation"
label="Father Occupation"
value={data.fatherOccupation}
onChange={(e) => handleChange("fatherOccupation", e.target.value)}
error={Boolean(errors.fatherOccupation)}
helperText={errors.fatherOccupation}
placeholder="Enter Father Occupation"
variant="outlined"
/>
</div>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Mother Name{requiredMark}
</label>
<TextField
fullWidth
name="motherName"
label="Mother Name"
value={data.motherName}
onChange={(e) => handleChange("motherName", e.target.value)}
error={Boolean(errors.motherName)}
helperText={errors.motherName}
placeholder="Enter Mother Name"
variant="outlined"
/>
</div>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Mother Occupation
</label>
<TextField
fullWidth
name="motherOccupation"
label="Mother Occupation"
value={data.motherOccupation}
onChange={(e) => handleChange("motherOccupation", e.target.value)}
error={Boolean(errors.motherOccupation)}
helperText={errors.motherOccupation}
placeholder="Enter Mother Occupation"
variant="outlined"
/>
</div>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Brother Count
</label>
<FormControl fullWidth variant="outlined">
<InputLabel id="brotherCount-label">Select Brother Count</InputLabel>
<Select
labelId="brotherCount-label"
label="Select Brother Count"
name="brotherCount"
value={data.brotherCount}
onChange={(e) => handleChange("brotherCount", e.target.value)}
>
{countOptions.map((count) => (
<MenuItem key={count} value={count}>
{count}
</MenuItem>
))}
</Select>
</FormControl>
</div>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Sister Count
</label>
<FormControl fullWidth variant="outlined">
<InputLabel id="sisterCount-label">Select Sister Count</InputLabel>
<Select
labelId="sisterCount-label"
label="Select Sister Count"
name="sisterCount"
value={data.sisterCount}
onChange={(e) => handleChange("sisterCount", e.target.value)}
>
{countOptions.map((count) => (
<MenuItem key={count} value={count}>
{count}
</MenuItem>
))}
</Select>
</FormControl>
</div>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Family Status{requiredMark}
</label>
<FormControl
fullWidth
variant="outlined"
error={Boolean(errors.familyStatus)}
>
<InputLabel id="familyStatus-label">Select Family Status</InputLabel>
<Select
labelId="familyStatus-label"
label="Select Family Status"
name="familyStatus"
value={data.familyStatus}
onChange={(e) => handleChange("familyStatus", e.target.value)}
disabled={isFamilyMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
{familyStatusOptions.map((item) => (
<MenuItem key={item.id ?? item} value={item.id ?? item}>
{item.family_type_name || item.name || item}
</MenuItem>
))}
</Select>
{errors.familyStatus && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.familyStatus}
</p>
)}
</FormControl>
</div>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">Native Place</label>
<TextField
fullWidth
name="nativePlace"
label="Native Place"
value={data.nativePlace}
onChange={(e) => handleChange("nativePlace", e.target.value)}
error={Boolean(errors.nativePlace)}
helperText={errors.nativePlace}
placeholder="Enter Native Place"
variant="outlined"
/>
</div>
</div>
{Number(data.brotherCount) > 0 && (
<div className="mt-6">
<div className="text-gray-900 text-[16px] font-semibold mb-3">
Brother Details
</div>
<div className="grid grid-cols-1 gap-4">
{Array.from({ length: Number(data.brotherCount) }).map(
(_, index) => renderSiblingCard("brothers", index)
)}
</div>
</div>
)}
<Grid item
xs={12} sx={{ marginTop: 10, display: "flex", gap: 4 , justifyContent: "center"}}>
<Button variant="outlined" onClick={onSkipStep}>
{Number(data.sisterCount) > 0 && (
<div className="mt-6">
<div className="text-gray-900 text-[16px] font-semibold mb-3">
Sister Details
</div>
<div className="grid grid-cols-1 gap-4">
{Array.from({ length: Number(data.sisterCount) }).map(
(_, index) => renderSiblingCard("sisters", index)
)}
</div>
</div>
)}
<Grid
item
xs={12}
sx={{ marginTop: 10, display: "flex", gap: 4, justifyContent: "center" }}
>
<Button variant="outlined" onClick={onSkipStep}>
Skip
</Button>
<Button variant="contained" color="primary" onClick={handleSubmit}>
Submit
</Button>
</Grid>
</form>
</div>
</>
</form>
</div>
);
};
+349 -55
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useRef } from "react";
import React, { useEffect, useMemo, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { updateLifestyleDetails } from "../redux/registrationFormSlice";
import {
@@ -8,12 +8,50 @@ import {
Select,
MenuItem,
Button,
TextField,
Checkbox,
ListItemText,
} 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";
const LifestyleDetailsForm = ({ onSubmitStep, onSkipStep, errors }) => {
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]);
useEffect(() => {
inputRef.current?.focus();
@@ -21,6 +59,27 @@ const LifestyleDetailsForm = ({ onSubmitStep, onSkipStep, errors }) => {
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 handleGrahaChange = (house, value) => {
const next = { ...(data.graha || {}) };
next[house] = value;
dispatch(updateLifestyleDetails({ graha: next }));
if (onFieldChange) onFieldChange("graha");
};
const handleAmsamChange = (house, value) => {
const next = { ...(data.amsam || {}) };
next[house] = value;
dispatch(updateLifestyleDetails({ amsam: next }));
if (onFieldChange) onFieldChange("amsam");
};
const handleSubmit = () => {
@@ -28,67 +87,302 @@ const LifestyleDetailsForm = ({ onSubmitStep, onSkipStep, errors }) => {
onSubmitStep();
};
const fields = [
{ name: "diet", label: "Diet", options: ["Veg", "Non-Veg", "Eggetarian"] },
// {
// name: "drinking",
// label: "Drinking Habits",
// options: ["No", "Occasionally", "Regularly"],
// },
// {
// name: "smoking",
// label: "Smoking Habits",
// options: ["No", "Occasionally", "Regularly"],
// },
{
name: "hobbies",
label: "Hobbies & Interests",
options: ["Song", "Reading", "Sports", "Travel"],
},
];
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) => (
<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 selected.join(", ");
}}
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>
);
const renderChartGrid = (type) => {
const getValue = (house) =>
(type === "graha" ? data.graha : data.amsam)?.[house] || [];
const onChange = (house, value) =>
type === "graha"
? handleGrahaChange(house, value)
: handleAmsamChange(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 border-4 border-gray-200 shadow-lg bg-white" />
</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 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">
{fields.map(({ name, label, options }) => (
<div key={name} className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">{label}</label>
<FormControl
fullWidth
variant="outlined"
error={Boolean(errors[name])}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Diet (Multi-select){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"
multiple
value={data.diets}
onChange={(e) => handleMultiChange("diets", e.target.value)}
inputRef={inputRef}
disabled={isLifestyleMastersLoading}
renderValue={(selected) =>
selected
.map((id) => {
const item = dietOptions.find(
(opt) => (opt.id ?? opt) === id
);
return item?.diet_name || item?.name || id;
})
.join(", ")
}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
<InputLabel id={`${name}-label`}>Select {label}</InputLabel>
<Select
labelId={`${name}-label`}
label={`Select ${label}`}
name={name}
value={data[name]}
onChange={(e) => handleChange(name, e.target.value)}
inputRef={name === "diet" ? inputRef : null}
>
{options.map((opt) => (
<MenuItem key={opt} value={opt}>
{opt}
{dietOptions.map((opt) => {
const value = opt.id ?? opt;
const label = opt.diet_name || opt.name || String(opt);
return (
<MenuItem key={value} value={value}>
<Checkbox checked={data.diets.indexOf(value) > -1} />
<ListItemText primary={label} />
</MenuItem>
))}
</Select>
{errors[name] && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors[name]}
</p>
)}
</FormControl>
);
})}
</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"
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")}
</div>
))}
</div>
<div>
<div className="py-4">
<h3 className="text-base font-semibold text-gray-800 mb-3">
Add Navamsam
</h3>
{renderChartGrid("amsam")}
</div>
</div>
</div>
<Grid
+314 -182
View File
@@ -1,27 +1,137 @@
import React, { useEffect, useRef } from "react";
import React, { useEffect, useMemo, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { updatePartnerPreferences } from "../redux/registrationFormSlice";
import {
Grid,
TextField,
FormControl,
InputLabel,
Select,
MenuItem,
Button,
Checkbox,
ListItemText,
} from "@mui/material";
import { usePartnerPreferenceMasters } from "../hooks/useMasters";
import { useCityMasters, useSubCasteMasters } from "../hooks/useDependentMasters";
const PartnerPreferencesForm = ({ onSubmitStep, onSkipStep, errors }) => {
const PartnerPreferencesForm = ({
onSubmitStep,
onSkipStep,
errors,
onFieldChange,
}) => {
const dispatch = useDispatch();
const data = useSelector((state) => state.registerform.partnerPreferences);
const inputRef = useRef(null);
const requiredMark = <span style={{ color: "#d32f2f" }}> *</span>;
const { data: masters, isLoading: isPartnerMastersLoading } =
usePartnerPreferenceMasters();
const subCasteQuery = useSubCasteMasters(data.castes);
const cityQuery = useCityMasters(data.states);
const ageRangeOptions = useMemo(() => {
const raw = masters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.ageRange || raw.age_range || [];
}, [masters]);
const casteOptions = useMemo(() => {
const raw = masters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.caste || raw.castes || [];
}, [masters]);
const occupationOptions = useMemo(() => {
const raw = masters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.occupation || raw.occupations || [];
}, [masters]);
const educationOptions = useMemo(() => {
const raw = masters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.education || raw.educations || [];
}, [masters]);
const hobbyOptions = useMemo(() => {
const raw = masters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.hobbies || raw.hobby || [];
}, [masters]);
const annualIncomeOptions = useMemo(() => {
const raw = masters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.annual_income || raw.annualIncome || [];
}, [masters]);
const stateOptions = useMemo(() => {
const raw = masters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.state || raw.states || [];
}, [masters]);
const subCasteOptions = useMemo(() => {
const raw = subCasteQuery.data;
if (!raw) return [];
if (Array.isArray(raw)) {
const merged = raw.flatMap((entry) => {
if (!entry) return [];
if (Array.isArray(entry)) return entry;
return entry.sub_caste || entry.subCaste || entry.data || [];
});
return merged;
}
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]);
useEffect(() => {
inputRef.current?.focus();
}, []);
const handleChange = (field, value) => {
dispatch(updatePartnerPreferences({ [field]: value }));
const arrayFields = new Set([
"castes",
"subCastes",
"occupations",
"educations",
"hobbies",
"states",
"districts",
]);
const nextValue =
arrayFields.has(field) && typeof value === "string"
? value.split(",").filter(Boolean)
: value;
const updates = { [field]: nextValue };
const fieldsToClear = [field];
if (field === "castes") {
updates.subCastes = [];
fieldsToClear.push("subCastes");
}
if (field === "states") {
updates.districts = [];
fieldsToClear.push("districts");
}
dispatch(updatePartnerPreferences(updates));
if (onFieldChange) onFieldChange(fieldsToClear);
};
const handleSubmit = () => {
@@ -29,36 +139,98 @@ const PartnerPreferencesForm = ({ onSubmitStep, onSkipStep, errors }) => {
onSubmitStep();
};
const renderMultiSelect = ({
name,
label,
options,
value,
getLabel,
getValue,
disabled,
}) => (
<FormControl fullWidth variant="outlined" error={Boolean(errors[name])}>
<InputLabel id={`${name}-label`}>Select {label}</InputLabel>
<Select
labelId={`${name}-label`}
label={`Select ${label}`}
name={name}
multiple
value={value}
onChange={(e) => handleChange(name, e.target.value)}
disabled={disabled}
renderValue={(selected) =>
selected
.map((id) => {
const item = options.find((opt) => getValue(opt) === id);
return item ? getLabel(item) : id;
})
.join(", ")
}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
{options.map((opt) => {
const optValue = getValue(opt);
const optLabel = getLabel(opt);
return (
<MenuItem key={optValue} value={optValue}>
<Checkbox checked={value.indexOf(optValue) > -1} />
<ListItemText primary={optLabel} />
</MenuItem>
);
})}
</Select>
{errors[name] && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors[name]}
</p>
)}
</FormControl>
);
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">
{/* Preferred Age Range */}
{/* Age Range */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Preferred Age Range
Age Range{requiredMark}
</label>
<FormControl
fullWidth
variant="outlined"
error={Boolean(errors.ageRange)}
>
<InputLabel id="ageRange-label">
Select Preferred Age Range
</InputLabel>
<InputLabel id="ageRange-label">Select Age Range</InputLabel>
<Select
labelId="ageRange-label"
label="Select Preferred Age Range"
label="Select Age Range"
name="ageRange"
value={data.ageRange}
onChange={(e) => handleChange("ageRange", e.target.value)}
inputRef={inputRef}
disabled={isPartnerMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
<MenuItem value="25 - 28">25 - 28</MenuItem>
<MenuItem value="29 - 32">29 - 32</MenuItem>
<MenuItem value="33 - 36">33 - 36</MenuItem>
{ageRangeOptions.map((opt) => (
<MenuItem key={opt.id ?? opt} value={opt.id ?? opt}>
{opt.name || opt}
</MenuItem>
))}
</Select>
{errors.ageRange && (
<p
@@ -74,36 +246,120 @@ const PartnerPreferencesForm = ({ onSubmitStep, onSkipStep, errors }) => {
</FormControl>
</div>
{/* Religion / Caste Preference */}
{/* Caste */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Religion / Caste Preference
Caste{requiredMark}
</label>
{renderMultiSelect({
name: "castes",
label: "Caste",
options: casteOptions,
value: data.castes,
getLabel: (opt) => opt.caste_name || opt.name || String(opt),
getValue: (opt) => opt.id ?? opt,
disabled: isPartnerMastersLoading,
})}
</div>
{/* Sub Caste */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Sub Caste{requiredMark}
</label>
{renderMultiSelect({
name: "subCastes",
label: "Sub Caste",
options: subCasteOptions,
value: data.subCastes,
getLabel: (opt) =>
opt.sub_caste_name || opt.subCaste_name || opt.name || String(opt),
getValue: (opt) => opt.id ?? opt,
disabled: data.castes.length === 0 || subCasteQuery.isLoading,
})}
</div>
{/* Occupation */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Occupation{requiredMark}
</label>
{renderMultiSelect({
name: "occupations",
label: "Occupation",
options: occupationOptions,
value: data.occupations,
getLabel: (opt) => opt.occupation_name || opt.name || String(opt),
getValue: (opt) => opt.id ?? opt,
disabled: isPartnerMastersLoading,
})}
</div>
{/* Qualification */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Qualification{requiredMark}
</label>
{renderMultiSelect({
name: "educations",
label: "Qualification",
options: educationOptions,
value: data.educations,
getLabel: (opt) => opt.education_name || opt.name || String(opt),
getValue: (opt) => opt.id ?? opt,
disabled: isPartnerMastersLoading,
})}
</div>
{/* Lifestyle and Hobbies */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Lifestyle & Hobbies{requiredMark}
</label>
{renderMultiSelect({
name: "hobbies",
label: "Lifestyle & Hobbies",
options: hobbyOptions,
value: data.hobbies,
getLabel: (opt) => opt.hobby_name || opt.name || String(opt),
getValue: (opt) => opt.id ?? opt,
disabled: isPartnerMastersLoading,
})}
</div>
{/* Annual Income */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Annual Income{requiredMark}
</label>
<FormControl
fullWidth
variant="outlined"
error={Boolean(errors.religionCaste)}
error={Boolean(errors.annualIncome)}
>
<InputLabel id="religionCaste-label">
Select Religion / Caste Preference
<InputLabel id="annualIncome-label">
Select Annual Income
</InputLabel>
<Select
labelId="religionCaste-label"
label="Select Religion / Caste Preference"
name="religionCaste"
value={data.religionCaste}
onChange={(e) =>
handleChange("religionCaste", e.target.value)
}
labelId="annualIncome-label"
label="Select Annual Income"
name="annualIncome"
value={data.annualIncome}
onChange={(e) => handleChange("annualIncome", e.target.value)}
disabled={isPartnerMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
<MenuItem value="Hindu">Hindu</MenuItem>
<MenuItem value="Muslim">Muslim</MenuItem>
<MenuItem value="Christian">Christian</MenuItem>
<MenuItem value="Any">Any</MenuItem>
{annualIncomeOptions.map((opt) => (
<MenuItem key={opt.id ?? opt} value={opt.id ?? opt}>
{opt.annual_income_name || opt.name || opt}
</MenuItem>
))}
</Select>
{errors.religionCaste && (
{errors.annualIncome && (
<p
style={{
color: "#d32f2f",
@@ -111,167 +367,43 @@ const PartnerPreferencesForm = ({ onSubmitStep, onSkipStep, errors }) => {
fontSize: "0.75rem",
}}
>
{errors.religionCaste}
{errors.annualIncome}
</p>
)}
</FormControl>
</div>
{/* Occupation Preference */}
{/* State */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Occupation Preference
State{requiredMark}
</label>
<FormControl
fullWidth
variant="outlined"
error={Boolean(errors.occupationPref)}
>
<InputLabel id="occupationPref-label">
Select Occupation Preference
</InputLabel>
<Select
labelId="occupationPref-label"
label="Select Occupation Preference"
name="occupationPref"
value={data.occupationPref}
onChange={(e) =>
handleChange("occupationPref", e.target.value)
}
>
<MenuItem value="IT, Business">IT, Business</MenuItem>
<MenuItem value="Doctor">Doctor</MenuItem>
<MenuItem value="Engineer">Engineer</MenuItem>
<MenuItem value="Any">Any</MenuItem>
</Select>
{errors.occupationPref && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.occupationPref}
</p>
)}
</FormControl>
{renderMultiSelect({
name: "states",
label: "State",
options: stateOptions,
value: data.states,
getLabel: (opt) => opt.state_name || opt.name || String(opt),
getValue: (opt) => opt.id ?? opt,
disabled: isPartnerMastersLoading,
})}
</div>
{/* Lifestyle & Habits Preference */}
{/* City */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Lifestyle & Habits Preference
City{requiredMark}
</label>
<FormControl
fullWidth
variant="outlined"
error={Boolean(errors.lifestylePref)}
>
<InputLabel id="lifestylePref-label">
Select Lifestyle & Habits Preference
</InputLabel>
<Select
labelId="lifestylePref-label"
label="Select Lifestyle & Habits Preference"
name="lifestylePref"
value={data.lifestylePref}
onChange={(e) =>
handleChange("lifestylePref", e.target.value)
}
>
<MenuItem value="Non-Smoking, Non-Drinking">
Non-Smoking, Non-Drinking
</MenuItem>
<MenuItem value="Social Drinker">Social Drinker</MenuItem>
<MenuItem value="Any">Any</MenuItem>
</Select>
{errors.lifestylePref && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.lifestylePref}
</p>
)}
</FormControl>
</div>
{/* Prefer Qualification */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Prefer Qualification
</label>
<TextField
fullWidth
name="qualificationPref"
label="Enter prefer Qualification"
value={data.qualificationPref}
onChange={(e) =>
handleChange("qualificationPref", e.target.value)
}
error={Boolean(errors.qualificationPref)}
helperText={errors.qualificationPref}
variant="outlined"
/>
</div>
{/* Occupation (text) */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Occupation</label>
<TextField
fullWidth
name="occupationText"
label="Enter Occupation"
value={data.occupationText}
onChange={(e) =>
handleChange("occupationText", e.target.value)
}
error={Boolean(errors.occupationText)}
helperText={errors.occupationText}
variant="outlined"
/>
</div>
{/* Prefer Annual Income */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Prefer Annual Income
</label>
<TextField
fullWidth
name="incomePref"
label="Enter Annual Income"
value={data.incomePref}
onChange={(e) => handleChange("incomePref", e.target.value)}
error={Boolean(errors.incomePref)}
helperText={errors.incomePref}
variant="outlined"
/>
</div>
{/* Prefer Location */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Prefer Location
</label>
<TextField
fullWidth
name="locationPref"
label="Enter Prefer Location"
value={data.locationPref}
onChange={(e) => handleChange("locationPref", e.target.value)}
error={Boolean(errors.locationPref)}
helperText={errors.locationPref}
variant="outlined"
/>
{renderMultiSelect({
name: "districts",
label: "City",
options: cityOptions,
value: data.districts,
getLabel: (opt) =>
opt.district_name || opt.city_name || opt.name || String(opt),
getValue: (opt) => opt.id ?? opt,
disabled: data.states.length === 0 || cityQuery.isLoading,
})}
</div>
</div>
+156 -12
View File
@@ -35,7 +35,7 @@ import { useSendOtp, useVerifyOtp } from "../hooks/useAuth";
const OTP_LENGTH = 4;
const OTP_TIMER_SEC = 120; // 2 minutes
const PersonalDetailsForm = ({ onSubmitStep, errors }) => {
const PersonalDetailsForm = ({ onSubmitStep, errors, onFieldChange }) => {
const dispatch = useDispatch();
const data = useSelector((state) => state.registerform.personalDetails);
const nameInputRef = useRef(null);
@@ -141,6 +141,52 @@ const PersonalDetailsForm = ({ onSubmitStep, errors }) => {
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(() => {
@@ -207,18 +253,18 @@ const PersonalDetailsForm = ({ onSubmitStep, errors }) => {
resetOtp();
setMobileOtpVerified(false);
} catch (error) {
setMobileNumberError("Failed to send OTP. Please try again.");
const message =
error?.response?.data?.message ||
error?.response?.data?.otp ||
error?.message ||
"Failed to send OTP";
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);
@@ -230,21 +276,61 @@ const PersonalDetailsForm = ({ onSubmitStep, errors }) => {
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");
@@ -265,11 +351,10 @@ const PersonalDetailsForm = ({ onSubmitStep, errors }) => {
setMobileNumberError("");
setOtpError("");
} catch (error) {
const message =
error?.response?.data?.message ||
error?.response?.data?.otp ||
error?.message ||
"Invalid or expired OTP";
const message = getApiErrorMessage(
error,
"Invalid or expired OTP"
);
setOtpError(message);
toast.error(message, { position: "top-right" });
}
@@ -576,6 +661,7 @@ const PersonalDetailsForm = ({ onSubmitStep, errors }) => {
}}
slotProps={{
textField: {
name: "dob",
fullWidth: true,
error: Boolean(errors.dob),
helperText: errors.dob,
@@ -954,6 +1040,64 @@ const PersonalDetailsForm = ({ onSubmitStep, errors }) => {
}}
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 */}
+162 -82
View File
@@ -1,4 +1,4 @@
import React from 'react';
import React from "react";
import { useSelector } from 'react-redux';
import { Edit2,Info } from 'lucide-react';
import {
@@ -12,44 +12,154 @@ import {
Button,
Grid,
} from '@mui/material';
import { usePreviewDetails } from "../hooks/usePreview";
const PreviewScreen = ({ onEdit, onSubmit }) => {
const formData = useSelector((state) => state.registerform);
const { data: previewData, isLoading, isError } = usePreviewDetails();
const sections = [
{
title: 'Personal Details',
step: 1,
data: formData.personalDetails,
},
{
title: 'Educational & Professional Details',
step: 2,
data: formData.educationalDetails,
},
{
title: 'Family Details',
step: 3,
data: formData.familyDetails,
},
{
title: 'Lifestyle & Habits',
step: 4,
data: formData.lifestyleDetails,
},
{
title: 'Partner Preferences',
step: 5,
data: formData.partnerPreferences,
},
];
const sections = previewData?.personal_details
? [
{
title: "Personal Details",
step: 1,
data: previewData.personal_details,
},
{
title: "Educational & Professional Details",
step: 2,
data: previewData.educational_details,
},
{
title: "Family Details",
step: 3,
data: previewData.family_details,
},
{
title: "Lifestyle & Habits",
step: 4,
data: previewData.lifestyle_details,
},
{
title: "Partner Preferences",
step: 5,
data: previewData.partner_preferences,
},
]
: [
{
title: 'Personal Details',
step: 1,
data: formData.personalDetails,
},
{
title: 'Educational & Professional Details',
step: 2,
data: formData.educationalDetails,
},
{
title: 'Family Details',
step: 3,
data: formData.familyDetails,
},
{
title: 'Lifestyle & Habits',
step: 4,
data: formData.lifestyleDetails,
},
{
title: 'Partner Preferences',
step: 5,
data: formData.partnerPreferences,
},
];
const renderValue = (key, value) => {
if (key === "profiles" || key === "profile") {
const list = Array.isArray(value) ? value : [];
return (
<Box display="flex" gap={0} flexWrap="wrap">
{list.length > 0 ? (
list.map((imgObj, index) => {
const src =
typeof imgObj === "string"
? imgObj
: imgObj?.preview ||
imgObj?.url ||
(imgObj?.file ? URL.createObjectURL(imgObj.file) : null);
if (!src) return null;
return (
<img
key={index}
src={src}
alt="Profile"
style={{
width: "90px",
height: "90px",
objectFit: "cover",
borderRadius: "50%",
border: "1px solid #ccc",
marginLeft: "-20px",
}}
/>
);
})
) : (
<Box display="flex" alignItems="center" gap={1} sx={{ color: "#888" }}>
<Info size={16} />
<Typography>No photos uploaded</Typography>
</Box>
)}
</Box>
);
}
if (Array.isArray(value)) {
if (value.length === 0) return "-";
if (typeof value[0] === "object") {
return value.map((item, idx) => (
<div key={idx}>{JSON.stringify(item)}</div>
));
}
return value.join(", ");
}
if (value && typeof value === "object") {
return "-";
}
return value || "-";
};
return (
<Box p={3} sx={{maxWidth:"1400px", width:"100%"}} mx="auto" >
<div className='grid grid-cols-1 md:grid-cols-2 gap-4 my-10'>
{sections.map((section) => (
{isLoading && (
<Box
py={4}
display="flex"
justifyContent="center"
sx={{ color: "#666" }}
>
Loading preview...
</Box>
)}
{isError && (
<Box
py={4}
display="flex"
justifyContent="center"
sx={{ color: "#d32f2f" }}
>
Failed to load preview.
</Box>
)}
{!isLoading &&
sections.map((section) => (
<Card key={section.title} variant="outlined" sx={{ borderRadius: 2, background:"#fff5ed" }}>
<CardHeader
@@ -72,7 +182,7 @@ const PreviewScreen = ({ onEdit, onSubmit }) => {
/>
<Divider />
<CardContent sx={{ pt: 1, }}>
{Object.entries(section.data).map(([key, value]) => {
{Object.entries(section.data || {}).map(([key, value]) => {
// if (value && key !== 'profiles') {
@@ -82,9 +192,7 @@ const PreviewScreen = ({ onEdit, onSubmit }) => {
.replace(/_/g, ' ')
.replace(/\b\w/g, (l) => l.toUpperCase())
.trim();
const isImage =
typeof value === "string" &&
(value.startsWith("http") || value.startsWith("data:image"));
const content = renderValue(key, value);
return (
<Box
@@ -106,56 +214,28 @@ const PreviewScreen = ({ onEdit, onSubmit }) => {
</Typography>
{/* Special Case: Profiles Image Preview */}
{key === "profiles" ? (
<Box display="flex" gap={0} flexWrap="wrap">
{value && value.length > 0 ? (
value.map((imgObj, index) => (
<img
key={index}
src={URL.createObjectURL(imgObj.file)}
alt="Profile"
style={{
width: "90px",
height: "90px",
objectFit: "cover",
borderRadius: "50%",
border: "1px solid #ccc",
marginLeft:"-20px"
}}
/>
))
) : (
<Box display="flex" alignItems="center" gap={1} sx={{ color: "#888" }}>
<Info size={16} />
<Typography>No photos uploaded</Typography>
</Box>
)}
</Box>
{key === "profiles" || key === "profile" ? (
content
) : value ? (
<Typography
sx={{
fontWeight: 600,
wordBreak: "break-word",
textAlign: "left",
}}
>
{content}
</Typography>
) : (
// Text OR No Data
<>
{value ? (
<Typography
sx={{
fontWeight: 600,
wordBreak: "break-word",
textAlign: "left",
}}
>
{value}
</Typography>
) : (
<Box
display="flex"
alignItems="center"
gap={1}
sx={{ color: "#888", fontStyle: "italic" }}
>
<Info size={16} />
<Typography>No data available</Typography>
</Box>
)}
</>
<Box
display="flex"
alignItems="center"
gap={1}
sx={{ color: "#888", fontStyle: "italic" }}
>
<Info size={16} />
<Typography>No data available</Typography>
</Box>
)}
</Box>
+65 -15
View File
@@ -234,8 +234,10 @@ const ProfilePreviewPage = () => {
<CardContent sx={{ pt: 1 }}>
{renderField("Qualification", educationalDetails.qualification, 2)}
{renderField("Field of Study", educationalDetails.fieldOfStudy, 2)}
{renderField("College Name", educationalDetails.collegeName, 2)}
{renderField("Occupation", educationalDetails.occupation, 2)}
{renderField("Organization", educationalDetails.organization, 2)}
{renderField("Employee Type", educationalDetails.employeeType, 2)}
{renderField("Income", educationalDetails.income, 2)}
{renderField("Work Location", educationalDetails.workLocation, 2)}
</CardContent>
@@ -269,8 +271,10 @@ const ProfilePreviewPage = () => {
{renderField("Father Occupation", familyDetails.fatherOccupation)}
{renderField("Mother Name", familyDetails.motherName)}
{renderField("Mother Occupation", familyDetails.motherOccupation)}
{renderField("Number of Siblings", familyDetails.siblings)}
{renderField("Siblings Marital status", familyDetails.siblingsStatus)}
{renderField("Brother Count", familyDetails.brotherCount)}
{renderField("Sister Count", familyDetails.sisterCount)}
{renderField("Family Status", familyDetails.familyStatus)}
{renderField("Native Place", familyDetails.nativePlace)}
</CardContent>
</Card>
);
@@ -298,11 +302,21 @@ const ProfilePreviewPage = () => {
<Divider />
<CardContent sx={{ pt: 1 }}>
{renderField("Diet", lifestyleDetails.diet)}
{/* {renderField("Drinking", lifestyleDetails.drinking)}
{renderField("Smoking", lifestyleDetails.smoking)} */}
{renderField("Hobbies", lifestyleDetails.hobbies)}
{renderField(
"Diet",
Array.isArray(lifestyleDetails.diets)
? lifestyleDetails.diets.join(", ")
: lifestyleDetails.diets
)}
{renderField(
"Hobbies",
Array.isArray(lifestyleDetails.hobbies)
? lifestyleDetails.hobbies.join(", ")
: lifestyleDetails.hobbies
)}
{renderField("Date of Birth", lifestyleDetails.dob)}
{renderField("Time of Birth", lifestyleDetails.tob)}
{renderField("Place of Birth", lifestyleDetails.placeOfBirth)}
</CardContent>
</Card>
);
@@ -331,14 +345,50 @@ const ProfilePreviewPage = () => {
<Divider />
<CardContent sx={{ pt: 1 }}>
{renderField("Age", partnerPreferences.ageRange)}
{renderField("Religion", partnerPreferences.religionCaste)}
{renderField("Occupation", partnerPreferences.occupationPref)}
{renderField("Life style", partnerPreferences.lifestylePref)}
{renderField("Qualification", partnerPreferences.qualificationPref)}
{renderField("Occupation", partnerPreferences.occupationText)}
{renderField("Income", partnerPreferences.incomePref)}
{renderField("Location", partnerPreferences.locationPref)}
{renderField("Age Range", partnerPreferences.ageRange)}
{renderField(
"Caste",
Array.isArray(partnerPreferences.castes)
? partnerPreferences.castes.join(", ")
: partnerPreferences.castes
)}
{renderField(
"Sub Caste",
Array.isArray(partnerPreferences.subCastes)
? partnerPreferences.subCastes.join(", ")
: partnerPreferences.subCastes
)}
{renderField(
"Occupation",
Array.isArray(partnerPreferences.occupations)
? partnerPreferences.occupations.join(", ")
: partnerPreferences.occupations
)}
{renderField(
"Qualification",
Array.isArray(partnerPreferences.educations)
? partnerPreferences.educations.join(", ")
: partnerPreferences.educations
)}
{renderField(
"Lifestyle & Hobbies",
Array.isArray(partnerPreferences.hobbies)
? partnerPreferences.hobbies.join(", ")
: partnerPreferences.hobbies
)}
{renderField("Annual Income", partnerPreferences.annualIncome)}
{renderField(
"State",
Array.isArray(partnerPreferences.states)
? partnerPreferences.states.join(", ")
: partnerPreferences.states
)}
{renderField(
"City",
Array.isArray(partnerPreferences.districts)
? partnerPreferences.districts.join(", ")
: partnerPreferences.districts
)}
</CardContent>
</Card>
+420 -23
View File
@@ -1,4 +1,4 @@
import React, { useState,useEffect } from "react";
import React, { useState, useEffect } from "react";
import { ChevronLeft } from "lucide-react";
import { useDispatch, useSelector } from "react-redux";
import {
@@ -16,7 +16,105 @@ import LifestyleDetailsForm from "./LifestyleDetailsForm";
import PartnerPreferencesForm from "./PartnerPreferencesForm";
import PreviewScreen from "./PreviewScreen";
import { useLocation } from "react-router-dom";
import { useRegisterStep1 } from "../hooks/useRegister";
import {
useRegisterStep1,
useRegisterStep2,
useRegisterStep3,
useRegisterStep4,
useRegisterStep5,
} from "../hooks/useRegister";
import { setAccessToken } from "../api/axiosInstance";
import toast from "react-hot-toast";
const STEP_FIELD_ORDER = {
1: [
"name",
"gender",
"mobileNumber",
"dob",
"height",
"weight",
"maritalStatus",
"religion",
"profileFor",
"caste",
"subCaste",
"gothram",
"raasi",
"star",
"email",
"password",
"confirmPassword",
"state",
"city",
"pincode",
"profiles",
],
2: [
"fieldOfStudy",
"qualification",
"collegeName",
"occupation",
"organization",
"employeeType",
"income",
"workLocation",
],
3: [
"fatherName",
"fatherOccupation",
"motherName",
"motherOccupation",
"brotherCount",
"sisterCount",
"familyStatus",
"nativePlace",
],
4: ["diets", "hobbies", "dob", "tob", "placeOfBirth"],
5: [
"ageRange",
"castes",
"subCastes",
"occupations",
"educations",
"hobbies",
"annualIncome",
"states",
"districts",
],
};
const STEP1_SERVER_FIELD_MAP = {
name: "name",
mobile: "mobileNumber",
mobile_number: "mobileNumber",
mobileNumber: "mobileNumber",
phone: "mobileNumber",
email: "email",
gender: "gender",
dob: "dob",
height: "height",
weight: "weight",
marital_status: "maritalStatus",
maritalStatus: "maritalStatus",
religion: "religion",
profile_for: "profileFor",
profileFor: "profileFor",
caste: "caste",
sub_caste: "subCaste",
subCaste: "subCaste",
gothram: "gothram",
raasi: "raasi",
star: "star",
state: "state",
district: "city",
city: "city",
pincode: "pincode",
password: "password",
confirm_password: "confirmPassword",
confirmPassword: "confirmPassword",
profiles: "profiles",
};
const Stepper = ({ currentStep, onStepClick }) => {
@@ -84,6 +182,136 @@ const StepperForm = () => {
const [errors, setErrors] = useState({});
const registerStep1 = useRegisterStep1();
const registerStep2 = useRegisterStep2();
const registerStep3 = useRegisterStep3();
const registerStep4 = useRegisterStep4();
const registerStep5 = useRegisterStep5();
const normalizeStep1Field = (key) => {
if (!key) return key;
const trimmed = String(key).trim();
if (!trimmed) return trimmed;
return (
STEP1_SERVER_FIELD_MAP[trimmed] ||
STEP1_SERVER_FIELD_MAP[trimmed.toLowerCase()] ||
trimmed
);
};
const coerceErrorMessage = (value) => {
if (Array.isArray(value)) {
return value.filter(Boolean).join(" ");
}
if (typeof value === "string") return value;
if (value && typeof value === "object") {
return (
value.message ||
value.msg ||
value.error ||
value.detail ||
""
);
}
if (value === null || value === undefined) return "";
return String(value);
};
const mapServerErrors = (error) => {
const data = error?.response?.data ?? error?.data ?? error;
if (!data) return {};
const payload = data.errors ?? data.error ?? data.data ?? data;
if (typeof payload === "string") {
return { _form: payload };
}
if (Array.isArray(payload)) {
const out = {};
payload.forEach((item) => {
if (!item) return;
if (typeof item === "string") {
out._form = out._form ? `${out._form} ${item}` : item;
return;
}
const key = item.field || item.name || item.param || item.key;
const message =
item.message || item.msg || item.error || item.detail || "";
if (key) {
out[normalizeStep1Field(key)] =
String(message || "Invalid value");
}
});
return out;
}
if (typeof payload === "object") {
const out = {};
Object.entries(payload).forEach(([key, value]) => {
if (
(key === "message" || key === "error" || key === "detail") &&
typeof value === "string"
) {
out._form = out._form ? `${out._form} ${value}` : value;
return;
}
const normalizedKey = normalizeStep1Field(key);
const message = coerceErrorMessage(value);
if (!normalizedKey) return;
out[normalizedKey] = message || "Invalid value";
});
return out;
}
return {};
};
const focusFirstError = (errorMap, fieldOrder = []) => {
if (!errorMap || Object.keys(errorMap).length === 0) return;
const order = fieldOrder.filter((key) => errorMap[key]);
const firstKey =
order[0] ||
Object.keys(errorMap).find((key) => key !== "_form");
if (!firstKey) return;
setTimeout(() => {
const byAria = document.querySelector(
`[aria-labelledby~="${firstKey}-label"]`
);
if (byAria && typeof byAria.focus === "function") {
byAria.focus();
return;
}
const byName = document.querySelector(`[name="${firstKey}"]`);
if (byName && typeof byName.focus === "function") {
byName.focus();
return;
}
const byId = document.getElementById(firstKey);
if (byId && typeof byId.focus === "function") {
byId.focus();
}
}, 0);
};
const clearFieldErrors = (fields) => {
if (!fields) return;
const list = Array.isArray(fields) ? fields : [fields];
setErrors((prev) => {
if (!prev || Object.keys(prev).length === 0) return prev;
let changed = false;
const next = { ...prev };
list.forEach((field) => {
if (field in next) {
delete next[field];
changed = true;
}
});
if (next._form) {
delete next._form;
changed = true;
}
return changed ? next : prev;
});
};
useEffect(() => {
@@ -154,8 +382,8 @@ const StepperForm = () => {
"fieldOfStudy",
"occupation",
"organization",
"employeeType",
"income",
"workLocation",
];
required.forEach((field) => {
if (!educationalDetails[field]) {
@@ -165,12 +393,8 @@ const StepperForm = () => {
} else if (step === 3) {
const required = [
"fatherName",
"fatherOccupation",
"motherName",
"motherOccupation",
"siblings",
"siblingsStatus",
"familyStatus",
];
required.forEach((field) => {
if (!familyDetails[field]) {
@@ -178,25 +402,36 @@ const StepperForm = () => {
}
});
} else if (step === 4) {
const required = ["diet", "drinking", "smoking", "hobbies"];
const required = ["diets", "hobbies", "dob", "tob"];
required.forEach((field) => {
if (!lifestyleDetails[field]) {
const value = lifestyleDetails[field];
if (Array.isArray(value)) {
if (value.length === 0) newErrors[field] = "This field is required";
} else if (!value) {
newErrors[field] = "This field is required";
}
});
} else if (step === 5) {
const required = [
"ageRange",
"religionCaste",
"occupationPref",
"lifestylePref",
"qualificationPref",
"occupationText",
"incomePref",
"locationPref",
"castes",
"subCastes",
"occupations",
"educations",
"hobbies",
"annualIncome",
"states",
"districts",
];
required.forEach((field) => {
if (!partnerPreferences[field]) {
const value = partnerPreferences[field];
if (Array.isArray(value)) {
if (value.length === 0) {
newErrors[field] = "This field is required";
}
return;
}
if (!value) {
newErrors[field] = "This field is required";
}
});
@@ -204,11 +439,8 @@ const StepperForm = () => {
setErrors(newErrors);
// Autofocus first invalid field
if (Object.keys(newErrors).length > 0) {
const firstKey = Object.keys(newErrors)[0];
const el = document.querySelector(`[name="${firstKey}"]`);
if (el) el.focus();
focusFirstError(newErrors, STEP_FIELD_ORDER[step] || []);
}
return Object.keys(newErrors).length === 0;
@@ -237,6 +469,132 @@ const StepperForm = () => {
fcm_token: localStorage.getItem("fcm_token") || "",
});
const buildRegisterStep2Payload = () => ({
college_name: educationalDetails.collegeName || "",
study_field: educationalDetails.fieldOfStudy,
education: educationalDetails.qualification,
occupation: educationalDetails.occupation,
company_name: educationalDetails.organization || "",
employee_type: educationalDetails.employeeType,
annual_income: educationalDetails.income,
work_location: educationalDetails.workLocation || "",
});
const buildRegisterStep3Payload = () => {
const payload = {
father_name: familyDetails.fatherName,
father_occupation: familyDetails.fatherOccupation || "",
mother_name: familyDetails.motherName,
mother_occupation: familyDetails.motherOccupation || "",
family_status: familyDetails.familyStatus,
native_place: familyDetails.nativePlace || "",
brother_count: familyDetails.brotherCount || 0,
sister_count: familyDetails.sisterCount || 0,
};
(familyDetails.brothers || []).forEach((brother, index) => {
payload[`brothers[${index}][name]`] = brother?.name || "";
payload[`brothers[${index}][occupation]`] = brother?.occupation || "";
payload[`brothers[${index}][marital_status]`] =
brother?.maritalStatus || "";
payload[`brothers[${index}][have_childrens]`] =
brother?.haveChildrens ?? "";
});
(familyDetails.sisters || []).forEach((sister, index) => {
payload[`sisters[${index}][name]`] = sister?.name || "";
payload[`sisters[${index}][occupation]`] = sister?.occupation || "";
payload[`sisters[${index}][marital_status]`] =
sister?.maritalStatus || "";
payload[`sisters[${index}][have_childrens]`] =
sister?.haveChildrens ?? "";
});
return payload;
};
const buildRegisterStep4Payload = () => {
const payload = {
dob: lifestyleDetails.dob,
tob: lifestyleDetails.tob,
};
(lifestyleDetails.diets || []).forEach((id, index) => {
payload[`diets[${index}]`] = id;
});
(lifestyleDetails.hobbies || []).forEach((id, index) => {
payload[`hobbies[${index}]`] = id;
});
const graha = lifestyleDetails.graha || {};
Object.keys(graha).forEach((house) => {
const values = graha[house] || [];
values.forEach((value, index) => {
payload[`graha_${house}[${index}]`] = value;
});
});
const amsam = lifestyleDetails.amsam || {};
Object.keys(amsam).forEach((house) => {
const values = amsam[house] || [];
values.forEach((value, index) => {
payload[`amsam_${house}[${index}]`] = value;
});
});
return payload;
};
const buildRegisterStep5Payload = () => {
const payload = {
age_range: partnerPreferences.ageRange,
annual_income: partnerPreferences.annualIncome,
};
(partnerPreferences.castes || []).forEach((id, index) => {
payload[`castes[${index}]`] = id;
});
(partnerPreferences.subCastes || []).forEach((id, index) => {
payload[`sub_castes[${index}]`] = id;
});
(partnerPreferences.occupations || []).forEach((id, index) => {
payload[`occupations[${index}]`] = id;
});
(partnerPreferences.educations || []).forEach((id, index) => {
payload[`educations[${index}]`] = id;
});
(partnerPreferences.hobbies || []).forEach((id, index) => {
payload[`hobbies[${index}]`] = id;
});
(partnerPreferences.states || []).forEach((id, index) => {
payload[`states[${index}]`] = id;
});
(partnerPreferences.districts || []).forEach((id, index) => {
payload[`districts[${index}]`] = id;
});
return payload;
};
const extractAccessToken = (res) =>
res?.access_token ||
res?.accessToken ||
res?.token ||
res?.data?.access_token ||
res?.data?.accessToken ||
res?.data?.token ||
res?.result?.access_token ||
res?.result?.accessToken ||
res?.result?.token ||
null;
const handleStepSubmit = async () => {
const isValid = validateStep(currentStep);
if (!isValid) return;
@@ -244,11 +602,45 @@ const StepperForm = () => {
try {
if (currentStep === 1) {
const payload = buildRegisterStep1Payload();
await registerStep1.mutateAsync(payload);
const res = await registerStep1.mutateAsync(payload);
const token = extractAccessToken(res);
if (token) {
setAccessToken(token);
}
} else if (currentStep === 2) {
const payload = buildRegisterStep2Payload();
await registerStep2.mutateAsync(payload);
} else if (currentStep === 3) {
const payload = buildRegisterStep3Payload();
await registerStep3.mutateAsync(payload);
} else if (currentStep === 4) {
const payload = buildRegisterStep4Payload();
await registerStep4.mutateAsync(payload);
} else if (currentStep === 5) {
const payload = buildRegisterStep5Payload();
await registerStep5.mutateAsync(payload);
}
setErrors({});
setCurrentStep((prev) => Math.min(prev + 1, 6));
window.scrollTo(0, 0);
} catch (e) {
if (currentStep === 1) {
const serverErrors = mapServerErrors(e);
if (Object.keys(serverErrors).length > 0) {
const hasFieldErrors = Object.keys(serverErrors).some(
(key) => key !== "_form"
);
if (hasFieldErrors) {
setErrors(serverErrors);
focusFirstError(serverErrors, STEP_FIELD_ORDER[1]);
return;
}
if (serverErrors._form) {
toast.error(serverErrors._form, { position: "top-right" });
return;
}
}
}
alert("Failed to submit step. Please try again.");
}
};
@@ -296,6 +688,7 @@ const StepperForm = () => {
<PersonalDetailsForm
onSubmitStep={handleStepSubmit}
errors={errors}
onFieldChange={clearFieldErrors}
/>
);
case 2:
@@ -304,6 +697,7 @@ const StepperForm = () => {
onSubmitStep={handleStepSubmit}
onSkipStep={handleSkip}
errors={errors}
onFieldChange={clearFieldErrors}
/>
);
case 3:
@@ -312,6 +706,7 @@ const StepperForm = () => {
onSubmitStep={handleStepSubmit}
onSkipStep={handleSkip}
errors={errors}
onFieldChange={clearFieldErrors}
/>
);
case 4:
@@ -320,6 +715,7 @@ const StepperForm = () => {
onSubmitStep={handleStepSubmit}
onSkipStep={handleSkip}
errors={errors}
onFieldChange={clearFieldErrors}
/>
);
case 5:
@@ -328,6 +724,7 @@ const StepperForm = () => {
onSubmitStep={handleStepSubmit}
onSkipStep={handleSkip}
errors={errors}
onFieldChange={clearFieldErrors}
/>
);
case 6: