feat(auth): Enhance login flow and user profile management

Refactor user information handling and improve the post-login experience.

*   **Login Flow & User Profile:**
    *   Introduced `UserInfoService` to centralize user data storage and retrieval from local storage, replacing direct `localStorage` access for user `person` data.
    *   Modified the `LoginPage` to use `userInfoService.updateUserInfo()` after successful OTP verification.
    *   Implemented immediate fetching of the full user profile (`fetchUserProfile`) after login to ensure up-to-date user details.
    *   Adjusted post-login navigation logic: users with a complete profile (indicated by `userInfo?.username`) are now directed to the campaigns page, while those with incomplete profiles are guided to the profile page for completion.

*   **UI/UX Improvements:**
    *   The `CustomRadio` component now displays its `value` as a fallback if no `label` is explicitly provided, improving usability for radio buttons.
    *   Adjusted styling for form field labels in `DynamicForm` to include a bottom margin, enhancing visual separation and readability.
This commit is contained in:
MehrdadAdabi 2025-11-27 16:25:27 +03:30
parent cacec43cbc
commit 518650ccd5
10 changed files with 206 additions and 54 deletions

View File

@ -77,7 +77,7 @@ const CustomRadio = React.forwardRef<HTMLInputElement, CustomRadioProps>(
: "text-foreground hover:text-foreground/80"
)}
>
{label}
{label ?? value}
</label>
</div>
{error && (

View File

@ -22,6 +22,8 @@ class UserInfoService {
const tokenObj = JSON.parse(tokenStr);
return tokenObj || null;
}
}
export const userInfoService = new UserInfoService();

View File

@ -17,11 +17,13 @@ import { CustomInput } from "@/core/components/base/input";
import { OTPDialog } from "@modules/auth/components/otp/opt-dialog";
import { toast } from "react-toastify";
import { userInfoService } from "@/core/service/user-info.service";
import { DASHBOARD_ROUTE } from "@/modules/dashboard/routes/route.constant";
import { fetchUserProfile } from "@/modules/dashboard/service/user.service";
import { sendOtpService, verifyOtpService } from "@modules/auth/service/auth.service";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { sendOtpService, verifyOtpService } from "../../service/auth.service";
export function LoginPage() {
const navigate = useNavigate();
@ -71,7 +73,7 @@ export function LoginPage() {
const verifyOtpMutation = useMutation({
mutationFn: verifyOtpService,
onSuccess: (data) => {
onSuccess: async(data) => {
setSubmitLoading(false);
if (data.resultType !== 0) {
toast.error(data.message);
@ -81,14 +83,16 @@ export function LoginPage() {
const person = JSON.parse(data.data).Person;
const token = JSON.parse(data.data).Token;
localStorage.setItem("token", JSON.stringify(token));
localStorage.setItem("person", JSON.stringify(person));
userInfoService.updateUserInfo(person)
await fetchUserProfile()
const userInfo = userInfoService.getUserInfo()
setOtpDialog(false);
if (person.NationalCode === "") {
navigate(`${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.profile}`, {
if (userInfo?.username) {
navigate(`${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.campaigns}`, {
replace: true,
});
} else {
navigate(`${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.campaigns}`, {
navigate(`${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.profile}`, {
replace: true,
});
}

View File

@ -16,3 +16,5 @@ export const verifyOtpService = async ({
const res = await api.post(API_ADDRESS.auth.verifyOtp, { mobile, code });
return res.data;
};

View File

@ -242,8 +242,8 @@ const DynamicForm: FC<DynamicFormProps> = ({ fields, processId, stepId }) => {
key={field.ID}
className={`col-span-1 md:col-span-1 ${colSpanClass}`}
>
<label className="text-right text-sm font-medium">
{field.Name}{" "}
<label className="text-right text-sm font-medium mb-1.5 block">
{field.Name}
{!field.AllowNull && <span className="text-red-500">*</span>}
</label>
<FormField

View File

@ -1,4 +1,4 @@
import { useEffect, useState, type FC } from "react";
import { useEffect, useMemo, useState, type FC } from "react";
import type { WorkflowResponse } from "@/core/types/global.type";
import type { FieldDefinition as LocalFieldDefinition } from "@core/utils/dynamic-field.utils";
@ -9,6 +9,8 @@ import {
} from "@modules/dashboard/service/dynamic-form.service";
import { useQuery } from "@tanstack/react-query";
import { useSearchParams } from "react-router-dom";
import { getCampaignStepsService } from "../../service/campaigns.service";
import type { CampaignProcess, StepItems } from "../steps/step.type";
import DynamicForm from "./dynamic-form";
const StepFormPage: FC = () => {
@ -19,11 +21,18 @@ const StepFormPage: FC = () => {
const { data, isLoading, error } = useQuery<WorkflowResponse>({
queryKey: ["dynamic-field", stageID ?? processID],
queryFn: () => {
if (stageID) return fetchFielSecondeIndex(stageID);
if (stageID) return fetchFielSecondeIndex(stageID) ;
return fetchFieldIndex(processID!);
},
});
const { data:steps } = useQuery({
queryKey: ["dynamic-step"],
queryFn: () => getCampaignStepsService(),
});
useEffect(() => {
if (data && data.Fields) {
const processedFields: LocalFieldDefinition[] = data.Fields.map((el) => {
@ -126,6 +135,87 @@ const StepFormPage: FC = () => {
}
}, [data]);
const currentStep = useMemo(() => {
if (!Array.isArray(steps) || steps.length === 0) return [];
const row = steps[0];
if (!row || Object.keys(row).length === 0) return [];
const processes = Object.keys(row)
.filter(
(key) =>
key.startsWith("process") &&
!key.endsWith("_id") &&
key.includes("_")
)
.map((key) => {
const index = key.match(/process(\d+)_/)?.[1];
if (!index) return null;
return {
processId: row[`process${index}`],
category: row[`process${index}_category`],
score: row[`process${index}_score`],
stageId: row[`process${index}_stage_id`],
status: row[`process${index}_status`],
};
})
.filter(Boolean);
// Group + Dedupe
const grouped = Object.values(
processes.reduce(
(
acc: Record<
string,
{
category: string;
processes: CampaignProcess[];
stageID: number;
processId: number;
}
>,
item
) => {
if (!item || !item.category) return acc;
if (!acc[item.category]) {
acc[item.category] = {
category: item.category,
processes: [],
stageID: Number(item.stageId),
processId: Number(item.processId),
};
}
// حذف تکراری‌ها بر اساس processId
const exists = acc[item.category].processes.some(
(p) => p.processId === item.processId
);
if (!exists) {
acc[item.category].processes.push({
processId: String(item.processId),
category: String(item.category),
score: String(item.score),
stageId: String(item.stageId),
status: String(item.status),
});
}
return acc;
},
{}
)
);
let datas:unknown
if(stageID) datas = grouped.find(el=> el.stageID === Number(stageID))
else datas = grouped.find(el=> el.processId === Number(processID))
return datas?.processes as Array<StepItems>;
}, [steps, stageID, processID]);
if (isLoading) {
return (
<div className="container mx-auto p-4 text-center font-vazir">
@ -147,6 +237,17 @@ const StepFormPage: FC = () => {
<h1 className="text-2xl font-bold mb-6 text-right font-vazir">
فرم پویا
</h1>
<div className="flex flex-row justify-center gap-10">
{currentStep.map((el, idx) => {
return (
<div key={idx} className="flex flex-col justify-center mb-4 text-right">
<div className="w-7 h-7 bg-red-950 rounded-full block m-auto"></div>
<div className="font-semibold text-sm mb-2">{el.category}</div>
</div>
);
})}
</div>
<DynamicForm fields={fields} processId={1} stepId={1} />
</div>
);

View File

@ -2,43 +2,66 @@ import { DASHBOARD_ROUTE } from "@modules/dashboard/routes/route.constant";
import { getCampaignStepsService } from "@modules/dashboard/service/campaigns.service";
import { useQuery } from "@tanstack/react-query";
import { MoveLeft } from "lucide-react";
import { useEffect, useState, type FC } from "react";
import { useMemo, type FC } from "react";
import { useNavigate, useParams } from "react-router-dom";
import type { CampaignProcess, GroupedCampaign } from "./step.type";
import type {
CampaignProcess,
GroupedCampaign,
StepItems,
} from "./step.type";
const StepsPage: FC = () => {
const { campaignId } = useParams<{ campaignId: string }>();
const navigate = useNavigate();
const [steps, setSteps] = useState<GroupedCampaign[]>([]);
const { data, isLoading, error } = useQuery<Record<string, any>>({
const { data, isLoading, error } = useQuery({
queryKey: ["dynamic-step"],
queryFn: () => getCampaignStepsService(),
});
useEffect(() => {
if (data && Object.keys(data).length > 0) {
const processes: CampaignProcess[] = Object.keys(data[0])
const steps = useMemo(() => {
if (!Array.isArray(data) || data.length === 0) return [];
const row = data[0];
if (!row || Object.keys(row).length === 0) return [];
const processes = Object.keys(row)
.filter(
(key) =>
key.startsWith("process") &&
key.endsWith("_id") === false &&
!key.endsWith("_id") &&
key.includes("_")
)
.map((key) => {
const index = key.match(/process(\d+)_/)?.[1];
if (!index) return null;
return {
processId: data[0][`process${index}`],
category: data[0][`process${index}_category`],
score: data[0][`process${index}_score`],
stageId: data[0][`process${index}_stage_id`],
status: data[0][`process${index}_status`],
processId: row[`process${index}`],
category: row[`process${index}_category`],
score: row[`process${index}_score`],
stageId: row[`process${index}_stage_id`],
status: row[`process${index}_status`],
};
})
.filter(Boolean) as CampaignProcess[];
.filter(Boolean);
// Group + Dedupe
const grouped = Object.values(
processes.reduce(
(
acc: Record<
string,
{
category: string;
processes: CampaignProcess[];
stageID: number;
processId: number;
}
>,
item
) => {
if (!item || !item.category) return acc;
const grouped: GroupedCampaign[] = Object.values(
processes.reduce((acc, item) => {
if (!acc[item.category]) {
acc[item.category] = {
category: item.category,
@ -47,12 +70,28 @@ const StepsPage: FC = () => {
processId: Number(item.processId),
};
}
acc[item.category].processes.push(item);
return acc;
}, {} as Record<string, GroupedCampaign>)
// حذف تکراری‌ها بر اساس processId
const exists = acc[item.category].processes.some(
(p) => p.processId === item.processId
);
setSteps(grouped);
if (!exists) {
acc[item.category].processes.push({
processId: String(item.processId),
category: String(item.category),
score: String(item.score),
stageId: String(item.stageId),
status: String(item.status),
});
}
return acc;
},
{}
)
);
return grouped as Array<StepItems>;
}, [data]);
const handleBack = () => {
@ -61,12 +100,9 @@ const StepsPage: FC = () => {
);
};
// navigate(`${DASHBOARD_ROUTE.dynamicForm}?stageID=${stageID}`);
// navigate(`${DASHBOARD_ROUTE.dynamicForm}?processID=${processID}`);
const handleStepClick = (step: GroupedCampaign) => {
// setSelectedStepId(step.processId);
if (
step.stageID !== null &&
step.stageID !== 0 &&

View File

@ -12,3 +12,12 @@ export interface GroupedCampaign {
category: string;
processes: CampaignProcess[];
}
export interface StepItems {
category:"بخش اول"
processId:1232
processes:Array<CampaignProcess>
stageID: 18267
}

View File

@ -131,7 +131,7 @@ export const getCommentsService = async (
};
export const getSelectedCampaignsService = async (
campaignId: Number
campaignId: number
): Promise<Campaign> => {
const query = {
ProcessName: "campaign",
@ -246,7 +246,7 @@ export const addCommentService = async (
};
export const removeCommentService = async (
commentId: Number
commentId: number
): Promise<void> => {
const body = {
WorkflowID: commentId,
@ -295,7 +295,7 @@ export const searchUsersService = async (nickname: string): Promise<User[]> => {
export const createCircleService = async (
circleName: string,
memberIds: string[]
): Promise<any> => {
)=> {
const user = userInfoService.getUserInfo();
const body = {
@ -324,9 +324,7 @@ export const createCircleService = async (
return res.data;
};
export const getCampaignStepsService = async (): Promise<
Record<string, any>
> => {
export const getCampaignStepsService = async () => {
const query = {
ProcessName: "run_process",
OutputFields: ["*"],

View File

@ -62,7 +62,7 @@ export const updateUserProfile = async (data: RegistrationFormData) => {
name: "profile_picture",
});
}
let payload = {
const payload = {
...(nationalCode && { WorkflowID: person.WorkflowID }),
user: {
username: person.ID ? String(person.ID) : person.username,