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"
|
: "text-foreground hover:text-foreground/80"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{label}
|
{label ?? value}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ class UserInfoService {
|
||||||
const tokenObj = JSON.parse(tokenStr);
|
const tokenObj = JSON.parse(tokenStr);
|
||||||
return tokenObj || null;
|
return tokenObj || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const userInfoService = new UserInfoService();
|
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 { OTPDialog } from "@modules/auth/components/otp/opt-dialog";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
|
import { userInfoService } from "@/core/service/user-info.service";
|
||||||
import { DASHBOARD_ROUTE } from "@/modules/dashboard/routes/route.constant";
|
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 { useMutation } from "@tanstack/react-query";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { sendOtpService, verifyOtpService } from "../../service/auth.service";
|
|
||||||
|
|
||||||
export function LoginPage() {
|
export function LoginPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -71,7 +73,7 @@ export function LoginPage() {
|
||||||
const verifyOtpMutation = useMutation({
|
const verifyOtpMutation = useMutation({
|
||||||
mutationFn: verifyOtpService,
|
mutationFn: verifyOtpService,
|
||||||
|
|
||||||
onSuccess: (data) => {
|
onSuccess: async(data) => {
|
||||||
setSubmitLoading(false);
|
setSubmitLoading(false);
|
||||||
if (data.resultType !== 0) {
|
if (data.resultType !== 0) {
|
||||||
toast.error(data.message);
|
toast.error(data.message);
|
||||||
|
|
@ -81,14 +83,16 @@ export function LoginPage() {
|
||||||
const person = JSON.parse(data.data).Person;
|
const person = JSON.parse(data.data).Person;
|
||||||
const token = JSON.parse(data.data).Token;
|
const token = JSON.parse(data.data).Token;
|
||||||
localStorage.setItem("token", JSON.stringify(token));
|
localStorage.setItem("token", JSON.stringify(token));
|
||||||
localStorage.setItem("person", JSON.stringify(person));
|
userInfoService.updateUserInfo(person)
|
||||||
|
await fetchUserProfile()
|
||||||
|
const userInfo = userInfoService.getUserInfo()
|
||||||
setOtpDialog(false);
|
setOtpDialog(false);
|
||||||
if (person.NationalCode === "") {
|
if (userInfo?.username) {
|
||||||
navigate(`${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.profile}`, {
|
navigate(`${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.campaigns}`, {
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
navigate(`${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.campaigns}`, {
|
navigate(`${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.profile}`, {
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,3 +16,5 @@ export const verifyOtpService = async ({
|
||||||
const res = await api.post(API_ADDRESS.auth.verifyOtp, { mobile, code });
|
const res = await api.post(API_ADDRESS.auth.verifyOtp, { mobile, code });
|
||||||
return res.data;
|
return res.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -242,8 +242,8 @@ const DynamicForm: FC<DynamicFormProps> = ({ fields, processId, stepId }) => {
|
||||||
key={field.ID}
|
key={field.ID}
|
||||||
className={`col-span-1 md:col-span-1 ${colSpanClass}`}
|
className={`col-span-1 md:col-span-1 ${colSpanClass}`}
|
||||||
>
|
>
|
||||||
<label className="text-right text-sm font-medium">
|
<label className="text-right text-sm font-medium mb-1.5 block">
|
||||||
{field.Name}{" "}
|
{field.Name}
|
||||||
{!field.AllowNull && <span className="text-red-500">*</span>}
|
{!field.AllowNull && <span className="text-red-500">*</span>}
|
||||||
</label>
|
</label>
|
||||||
<FormField
|
<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 { WorkflowResponse } from "@/core/types/global.type";
|
||||||
import type { FieldDefinition as LocalFieldDefinition } from "@core/utils/dynamic-field.utils";
|
import type { FieldDefinition as LocalFieldDefinition } from "@core/utils/dynamic-field.utils";
|
||||||
|
|
@ -9,6 +9,8 @@ import {
|
||||||
} from "@modules/dashboard/service/dynamic-form.service";
|
} from "@modules/dashboard/service/dynamic-form.service";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useSearchParams } from "react-router-dom";
|
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";
|
import DynamicForm from "./dynamic-form";
|
||||||
|
|
||||||
const StepFormPage: FC = () => {
|
const StepFormPage: FC = () => {
|
||||||
|
|
@ -19,11 +21,18 @@ const StepFormPage: FC = () => {
|
||||||
const { data, isLoading, error } = useQuery<WorkflowResponse>({
|
const { data, isLoading, error } = useQuery<WorkflowResponse>({
|
||||||
queryKey: ["dynamic-field", stageID ?? processID],
|
queryKey: ["dynamic-field", stageID ?? processID],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
if (stageID) return fetchFielSecondeIndex(stageID);
|
if (stageID) return fetchFielSecondeIndex(stageID) ;
|
||||||
return fetchFieldIndex(processID!);
|
return fetchFieldIndex(processID!);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const { data:steps } = useQuery({
|
||||||
|
queryKey: ["dynamic-step"],
|
||||||
|
queryFn: () => getCampaignStepsService(),
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data && data.Fields) {
|
if (data && data.Fields) {
|
||||||
const processedFields: LocalFieldDefinition[] = data.Fields.map((el) => {
|
const processedFields: LocalFieldDefinition[] = data.Fields.map((el) => {
|
||||||
|
|
@ -126,6 +135,87 @@ const StepFormPage: FC = () => {
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-4 text-center font-vazir">
|
<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 className="text-2xl font-bold mb-6 text-right font-vazir">
|
||||||
فرم پویا
|
فرم پویا
|
||||||
</h1>
|
</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} />
|
<DynamicForm fields={fields} processId={1} stepId={1} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,43 +2,66 @@ import { DASHBOARD_ROUTE } from "@modules/dashboard/routes/route.constant";
|
||||||
import { getCampaignStepsService } from "@modules/dashboard/service/campaigns.service";
|
import { getCampaignStepsService } from "@modules/dashboard/service/campaigns.service";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { MoveLeft } from "lucide-react";
|
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 { 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 StepsPage: FC = () => {
|
||||||
const { campaignId } = useParams<{ campaignId: string }>();
|
const { campaignId } = useParams<{ campaignId: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [steps, setSteps] = useState<GroupedCampaign[]>([]);
|
const { data, isLoading, error } = useQuery({
|
||||||
const { data, isLoading, error } = useQuery<Record<string, any>>({
|
|
||||||
queryKey: ["dynamic-step"],
|
queryKey: ["dynamic-step"],
|
||||||
queryFn: () => getCampaignStepsService(),
|
queryFn: () => getCampaignStepsService(),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const steps = useMemo(() => {
|
||||||
if (data && Object.keys(data).length > 0) {
|
if (!Array.isArray(data) || data.length === 0) return [];
|
||||||
const processes: CampaignProcess[] = Object.keys(data[0])
|
|
||||||
.filter(
|
const row = data[0];
|
||||||
(key) =>
|
if (!row || Object.keys(row).length === 0) return [];
|
||||||
key.startsWith("process") &&
|
|
||||||
key.endsWith("_id") === false &&
|
const processes = Object.keys(row)
|
||||||
key.includes("_")
|
.filter(
|
||||||
)
|
(key) =>
|
||||||
.map((key) => {
|
key.startsWith("process") &&
|
||||||
const index = key.match(/process(\d+)_/)?.[1];
|
!key.endsWith("_id") &&
|
||||||
if (!index) return null;
|
key.includes("_")
|
||||||
return {
|
)
|
||||||
processId: data[0][`process${index}`],
|
.map((key) => {
|
||||||
category: data[0][`process${index}_category`],
|
const index = key.match(/process(\d+)_/)?.[1];
|
||||||
score: data[0][`process${index}_score`],
|
if (!index) return null;
|
||||||
stageId: data[0][`process${index}_stage_id`],
|
|
||||||
status: data[0][`process${index}_status`],
|
return {
|
||||||
};
|
processId: row[`process${index}`],
|
||||||
})
|
category: row[`process${index}_category`],
|
||||||
.filter(Boolean) as CampaignProcess[];
|
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]) {
|
if (!acc[item.category]) {
|
||||||
acc[item.category] = {
|
acc[item.category] = {
|
||||||
category: item.category,
|
category: item.category,
|
||||||
|
|
@ -47,12 +70,28 @@ const StepsPage: FC = () => {
|
||||||
processId: Number(item.processId),
|
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;
|
return acc;
|
||||||
}, {} as Record<string, GroupedCampaign>)
|
},
|
||||||
);
|
{}
|
||||||
setSteps(grouped);
|
)
|
||||||
}
|
);
|
||||||
|
return grouped as Array<StepItems>;
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
const handleBack = () => {
|
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) => {
|
const handleStepClick = (step: GroupedCampaign) => {
|
||||||
// setSelectedStepId(step.processId);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
step.stageID !== null &&
|
step.stageID !== null &&
|
||||||
step.stageID !== 0 &&
|
step.stageID !== 0 &&
|
||||||
|
|
|
||||||
|
|
@ -12,3 +12,12 @@ export interface GroupedCampaign {
|
||||||
category: string;
|
category: string;
|
||||||
processes: CampaignProcess[];
|
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 (
|
export const getSelectedCampaignsService = async (
|
||||||
campaignId: Number
|
campaignId: number
|
||||||
): Promise<Campaign> => {
|
): Promise<Campaign> => {
|
||||||
const query = {
|
const query = {
|
||||||
ProcessName: "campaign",
|
ProcessName: "campaign",
|
||||||
|
|
@ -246,7 +246,7 @@ export const addCommentService = async (
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeCommentService = async (
|
export const removeCommentService = async (
|
||||||
commentId: Number
|
commentId: number
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const body = {
|
const body = {
|
||||||
WorkflowID: commentId,
|
WorkflowID: commentId,
|
||||||
|
|
@ -295,7 +295,7 @@ export const searchUsersService = async (nickname: string): Promise<User[]> => {
|
||||||
export const createCircleService = async (
|
export const createCircleService = async (
|
||||||
circleName: string,
|
circleName: string,
|
||||||
memberIds: string[]
|
memberIds: string[]
|
||||||
): Promise<any> => {
|
)=> {
|
||||||
const user = userInfoService.getUserInfo();
|
const user = userInfoService.getUserInfo();
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
|
|
@ -324,9 +324,7 @@ export const createCircleService = async (
|
||||||
return res.data;
|
return res.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCampaignStepsService = async (): Promise<
|
export const getCampaignStepsService = async () => {
|
||||||
Record<string, any>
|
|
||||||
> => {
|
|
||||||
const query = {
|
const query = {
|
||||||
ProcessName: "run_process",
|
ProcessName: "run_process",
|
||||||
OutputFields: ["*"],
|
OutputFields: ["*"],
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ export const updateUserProfile = async (data: RegistrationFormData) => {
|
||||||
name: "profile_picture",
|
name: "profile_picture",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let payload = {
|
const payload = {
|
||||||
...(nationalCode && { WorkflowID: person.WorkflowID }),
|
...(nationalCode && { WorkflowID: person.WorkflowID }),
|
||||||
user: {
|
user: {
|
||||||
username: person.ID ? String(person.ID) : person.username,
|
username: person.ID ? String(person.ID) : person.username,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user