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:
parent
cacec43cbc
commit
518650ccd5
|
|
@ -77,7 +77,7 @@ const CustomRadio = React.forwardRef<HTMLInputElement, CustomRadioProps>(
|
|||
: "text-foreground hover:text-foreground/80"
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
{label ?? value}
|
||||
</label>
|
||||
</div>
|
||||
{error && (
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ class UserInfoService {
|
|||
const tokenObj = JSON.parse(tokenStr);
|
||||
return tokenObj || null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
export const userInfoService = new UserInfoService();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,3 +16,5 @@ export const verifyOtpService = async ({
|
|||
const res = await api.post(API_ADDRESS.auth.verifyOtp, { mobile, code });
|
||||
return res.data;
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
.filter(
|
||||
(key) =>
|
||||
key.startsWith("process") &&
|
||||
key.endsWith("_id") === false &&
|
||||
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`],
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as CampaignProcess[];
|
||||
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") &&
|
||||
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;
|
||||
|
||||
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);
|
||||
|
||||
// حذف تکراریها بر اساس 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;
|
||||
}, {} as Record<string, GroupedCampaign>)
|
||||
);
|
||||
setSteps(grouped);
|
||||
}
|
||||
},
|
||||
{}
|
||||
)
|
||||
);
|
||||
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 &&
|
||||
|
|
|
|||
|
|
@ -12,3 +12,12 @@ export interface GroupedCampaign {
|
|||
category: string;
|
||||
processes: CampaignProcess[];
|
||||
}
|
||||
|
||||
export interface StepItems {
|
||||
category:"بخش اول"
|
||||
processId:1232
|
||||
processes:Array<CampaignProcess>
|
||||
stageID: 18267
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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: ["*"],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user