fix: update dropdown search to use name property and improve UI

- Fix dropdown filter and display to use option.name instead of option.value
- Change disabled input opacity from 50 to 100 for better visibility
- Remove default fallback in mobile navbar active item detection
- Update campaign join route to include school_code parameter
- Refactor join-to-campaign page to use classmate nickname API
- Add school_code to campaign service and route parameters
- Fix various formatting and whitespace issues
This commit is contained in:
MehrdadAdabi 2025-11-28 12:57:36 +03:30
parent d97c1d0fb7
commit 213e865aab
13 changed files with 200 additions and 144 deletions

View File

@ -125,7 +125,7 @@ export const BaseDropdown = forwardRef<HTMLDivElement, BaseDropdownProps>(
};
const filteredOptions = internalOptions.filter((option) =>
option.value.toLowerCase().includes(searchQuery.toLowerCase())
option.name.toLowerCase().includes(searchQuery.toLowerCase())
);
const hasError = !!error;
@ -193,7 +193,7 @@ export const BaseDropdown = forwardRef<HTMLDivElement, BaseDropdownProps>(
onClick={() => handleSelect(option)}
className="cursor-pointer px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
>
{option.value}
{option.name}
</li>
))
) : (

View File

@ -33,7 +33,7 @@ const CustomInput = React.forwardRef<HTMLInputElement, CustomInputProps>(
)}
<input
className={cn(
"flex h-12 w-full rounded-lg border-2 bg-background px-4 py-2 text-sm transition-all duration-200 placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50",
"flex h-12 w-full rounded-lg border-2 bg-background px-4 py-2 text-sm transition-all duration-200 placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-100",
{
// Primary variant
"border-gray-300 focus-visible:border-blue-600 focus-visible:ring-blue-600":

View File

@ -53,7 +53,7 @@ export function MobileNavbar({
const matchedItem = items.find((item) =>
location.pathname.startsWith(item.path)
);
return matchedItem?.id || items[0]?.id;
return matchedItem?.id;
}, [location.pathname, items]);
const handleNavClick = (path: string) => {

View File

@ -110,7 +110,7 @@ export function CampaignsPage() {
const handleJoin = (campaign: Campaign) => {
navigate(
`${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.joinToCampaing}?id=${campaign.WorkflowID}`,
`${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.joinToCampaing}/${campaign.school_code}/${campaign.WorkflowID}`,
{
replace: true,
}
@ -185,8 +185,7 @@ export function CampaignsPage() {
<button
key={tab.value}
onClick={() => handleTabChange(tab.value)}
className={`px-4 py-2 text-sm font-medium whitespace-nowrap rounded-t-lg transition-colors ${
activeTab === tab.value
className={`px-4 py-2 text-sm font-medium whitespace-nowrap rounded-t-lg transition-colors ${activeTab === tab.value
? "bg-white border-gray-200 border-t border-x text-blue-600"
: "text-slate-600 hover:text-slate-800 hover:bg-gray-100"
}`}
@ -252,7 +251,7 @@ export function CampaignsPage() {
variant="info"
className="shrink-0"
>
انتخاب{" "}
انتخاب
</CustomButton>
)}

View File

@ -4,16 +4,20 @@ import { CustomButton } from "@/core/components/base/button";
import { CustomInput } from "@/core/components/base/input";
import {
createCircleService,
searchUsersService,
getclassmateNickName,
} from "@modules/dashboard/service/campaigns.service";
import { useMutation, useQuery } from "@tanstack/react-query";
import { X } from "lucide-react";
import { useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { DASHBOARD_ROUTE } from "../../routes/route.constant";
import type { User } from "./join-to-campaing";
export const JoinToCampaing = () => {
const [circleName, setCircleName] = useState("");
const [searchTerm, setSearchTerm] = useState("");
const { scoolId, campId } = useParams();
const navigate = useNavigate()
const [selectedMembers, setSelectedMembers] = useState<User[]>([]);
const [errors, setErrors] = useState<{
circleName?: string;
@ -21,32 +25,41 @@ export const JoinToCampaing = () => {
}>({});
const { data: users } = useQuery({
queryKey: ["users", searchTerm],
queryFn: () => searchUsersService(searchTerm),
enabled: !!searchTerm,
queryKey: ["users", scoolId],
queryFn: () => getclassmateNickName(Number(scoolId)),
enabled: !!scoolId,
});
const { mutate: createCircle, isPending: isCreating } = useMutation({
const { mutate: createCircle, isPending: isCreating } = useMutation<{
group_id: number
}>({
mutationFn: () =>
createCircleService(
circleName,
selectedMembers.map((m) => m.user_id)
String(campId),
selectedMembers.map((m) => m.WorkflowID)
),
onSuccess: () => {
onSuccess: ({ group_id }) => {
setCircleName("");
setSelectedMembers([]);
navigate(
`${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.steps}/${campId}/${group_id}`
);
},
});
const handleAddMember = (userId: string) => {
const user = users?.find((u) => u.user_id === userId);
if (user && !selectedMembers.some((m) => m.user_id === userId)) {
const handleAddMember = (userId: number) => {
const user = users?.find((u) => u.WorkflowID === userId);
if (user && !selectedMembers.some((m) => m.WorkflowID === userId)) {
setSelectedMembers([...selectedMembers, user]);
}
};
const handleRemoveMember = (userId: string) => {
setSelectedMembers(selectedMembers.filter((m) => m.user_id !== userId));
const handleRemoveMember = (userId: number) => {
setSelectedMembers(selectedMembers.filter((m) => m.WorkflowID !== userId));
};
const checkRequired = () => {
@ -91,13 +104,12 @@ export const JoinToCampaing = () => {
placeholder="جستجوی لقب عضو..."
options={
users?.map((user) => ({
value: user.user_id,
label: user.nickname,
})) || []
value: String(user.WorkflowID),
name: user.nickname,
})) ?? []
}
onInputChange={(inputValue) => setSearchTerm(inputValue)}
onChange={(value) => {
handleAddMember(value);
handleAddMember(Number(value));
if (errors.members) {
setErrors((prev) => ({ ...prev, members: undefined }));
}
@ -117,12 +129,12 @@ export const JoinToCampaing = () => {
<ul className="space-y-2">
{selectedMembers.map((member) => (
<li
key={member.user_id}
key={member.WorkflowID}
className="flex items-center justify-between bg-gray-100 p-2 rounded-md"
>
<span>{member.nickname}</span>
<button
onClick={() => handleRemoveMember(member.user_id)}
onClick={() => handleRemoveMember(member.WorkflowID)}
className="text-red-500 hover:text-red-700"
>
<X size={18} />
@ -138,7 +150,7 @@ export const JoinToCampaing = () => {
<CustomButton
onClick={handleSubmit}
className="w-full"
disabled={isCreating}
disabled={isCreating || selectedMembers.length < 2 || circleName.length === 0}
>
تایید و ایجاد محفل
</CustomButton>

View File

@ -1,4 +1,7 @@
export type User = {
user_id: string;
export interface User {
ValueP1224S1943StageID: number;
ValueP1224S1943ValueID: number;
WorkflowID: number;
nickname: string;
};
}

View File

@ -39,6 +39,7 @@ export function RegisterPage() {
image: undefined,
nationalcode: data?.nationalcode || "",
base: data?.base || "",
group: ""
});
useEffect(() => {
@ -53,6 +54,7 @@ export function RegisterPage() {
invitor: data.invitor || "",
nationalcode: data.nationalcode || "",
base: data.base || "",
group: data.group
}));
if (data.name)
setPreviewImage(getContactImageUrl((data as any).stageID) ?? "");
@ -150,7 +152,7 @@ export function RegisterPage() {
queryKey: ["userProfile"],
});
},
onError: (error: any) => {
onError: (error: Error) => {
console.error("Registration error:", error);
toast.error(
"خطا در ثبت نام: " + (error?.message || "لطفاً دوباره تلاش کنید")
@ -289,7 +291,6 @@ export function RegisterPage() {
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* مقطع تحصیلی */}
<BaseDropdown
label="مقطع تحصیلی"
name="education_level"
@ -362,6 +363,15 @@ export function RegisterPage() {
onChange={handleInputChange}
/>
<CustomInput
label="گروه"
name="group"
type="text"
disabled={true}
value={formData.group || ""}
/>
<ImageUploader
label="عکس پروفایل "
previewImage={previewImage}

View File

@ -1,5 +1,5 @@
export interface RegistrationFormData {
ValueP1224S1943StageID?: Number;
ValueP1224S1943StageID?: number;
username?: string;
WorkflowID?: string;
name: string;
@ -11,6 +11,7 @@ export interface RegistrationFormData {
image?: File | null;
nationalcode: string;
base: string;
group: string
}
export interface RegistrationResponse {

View File

@ -9,8 +9,10 @@ import {
saveFormService,
} from "@modules/dashboard/service/dynamic-form.service";
import { useMutation, useQuery } from "@tanstack/react-query";
import { useSearchParams } from "react-router-dom";
import { MoveLeft } from "lucide-react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { toast } from "react-toastify";
import { DASHBOARD_ROUTE } from "../../routes/route.constant";
import { getCampaignStepsService } from "../../service/campaigns.service";
import DynamicForm from "./dynamic-form";
import type { CampaignProcess, FilterData, GroupedCampaign } from "./step-form.type";
@ -18,9 +20,13 @@ import type { CampaignProcess, FilterData, GroupedCampaign } from "./step-form.t
const StepFormPage: FC = () => {
const [fields, setFields] = useState<LocalFieldDefinition[]>([]);
const [params] = useSearchParams();
// const { campaignId, groupId } = useParams();
const navigate = useNavigate()
const stageID = params.get("stageID");
const processID = params.get("processID");
const campaignId = params.get("campaignId")
const groupId = params.get("groupId")
const { data, isLoading, error } = useQuery<WorkflowResponse>({
@ -34,7 +40,7 @@ const StepFormPage: FC = () => {
const { data: steps } = useQuery({
queryKey: ["dynamic-step"],
queryFn: () => getCampaignStepsService(),
queryFn: () => getCampaignStepsService(Number(campaignId), Number(groupId)),
});
@ -243,6 +249,9 @@ const StepFormPage: FC = () => {
}
toast.success("ثبت نام با موفقیت انجام شد");
navigate(
`${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.steps}/${campaignId}/${groupId}`
);
},
onError: (error: any) => {
console.error("Registration error:", error);
@ -262,6 +271,11 @@ const StepFormPage: FC = () => {
}
}
const handleBack = () => {
navigate(
`${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.steps}/${campaignId}/${groupId}`
);
}
if (isLoading) {
@ -282,9 +296,16 @@ const StepFormPage: FC = () => {
return (
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-6 text-right font-vazir">
فرم پویا
</h1>
<div className="mb-6">
<button
onClick={handleBack}
className="flex items-center gap-2 text-blue-600 hover:text-blue-800 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 rounded-md p-2"
>
<MoveLeft className="rotate-180" size={20} />
بازگشت به صفحه قبل
</button>
</div>
<div className="flex flex-row justify-center gap-10">
{currentStep?.map((el, idx) => {

View File

@ -10,11 +10,11 @@ import type {
} from "./step.type";
const StepsPage: FC = () => {
const { campaignId } = useParams<{ campaignId: string }>();
const { campaignId, groupId } = useParams();
const navigate = useNavigate();
const { data, isLoading, error } = useQuery({
queryKey: ["dynamic-step"],
queryFn: () => getCampaignStepsService(),
queryFn: () => getCampaignStepsService(Number(campaignId), Number(groupId)),
});
@ -82,7 +82,7 @@ const StepsPage: FC = () => {
const handleBack = () => {
navigate(
`${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.campaigns}/${campaignId}`
`${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.campaigns}`
);
};
@ -90,19 +90,20 @@ const StepsPage: FC = () => {
const handleStepClick = (step: GroupedCampaign) => {
const filteIncompleteProcess = step.processes.filter(el => el.status === 'انجام نشده')
// `${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.steps}/${campId}/${group_id}`
if (filteIncompleteProcess.length > 0) {
const firstItem = filteIncompleteProcess[0]
if (firstItem.stageId) {
navigate(
`${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.dynamicForm}?stageID=${firstItem.stageId}`,
`${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.dynamicForm}?stageID=${firstItem.stageId}&campaignId=${campaignId}&groupId=${groupId}`,
{
replace: true,
})
}
if (firstItem.processId) {
navigate(
`${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.dynamicForm}?processID=${firstItem.processId}`,
`${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.dynamicForm}?processID=${firstItem.processId}&campaignId=${campaignId}&groupId=${groupId}`,
{
replace: true,
}

View File

@ -31,7 +31,7 @@ export const dashboardRoutes: AppRoute[] = [
element: <CampaignDetailPage />,
},
{
path: `${DASHBOARD_ROUTE.joinToCampaing}/:id`,
path: `${DASHBOARD_ROUTE.joinToCampaing}/:scoolId/:campId`,
element: <JoinToCampaing />,
},
{
@ -39,7 +39,7 @@ export const dashboardRoutes: AppRoute[] = [
element: <StepFormPage />,
},
{
path: `${DASHBOARD_ROUTE.steps}`,
path: `${DASHBOARD_ROUTE.steps}/:campaignId/:groupId`,
element: <StepsPage />,
},
],

View File

@ -8,9 +8,9 @@ import type {
CreateCampaignData,
SignatureItem,
} from "@modules/dashboard/pages/campaigns/campaigns.type";
import type { User } from "@modules/dashboard/pages/join-to-campaing/join-to-campaing";
import to from "await-to-js";
import { toast } from "react-toastify";
import type { User } from "../pages/join-to-campaing/join-to-campaing";
export const getCampaignsService = async (): Promise<Campaign[]> => {
const userStr = userInfoService.getUserInfo();
@ -165,6 +165,63 @@ export const getSelectedCampaignsService = async (
return data[0];
};
export const getCampaignStepsService = async (campId: number, groupId: number) => {
const query = {
ProcessName: "run_process",
OutputFields: ["*"],
conditions: [
["campaign_id", "=", campId, "and"],
["group_id", "=", groupId],
],
};
const [err, res] = await to(api.post(API_ADDRESS.select, query));
if (err) {
throw err;
}
if (res.data.resultType !== 0) {
toast.error("خطا در دریافت مراحل کارزار");
throw new Error("خطا در دریافت مراحل کارزار");
}
const data = JSON.parse(res.data.data);
if (!data || Object.keys(data).length === 0) {
return {};
}
return data;
};
export const getclassmateNickName = async (schoolId: number): Promise<Array<User>> => {
const query = {
ProcessName: "user",
OutputFields: ["nickname"],
"conditions": [
[
"school_code",
"=",
schoolId
]
]
};
const [err, res] = await to(api.post(API_ADDRESS.select, query));
if (err) {
throw err;
}
if (res.data.resultType !== 0) {
toast.error("خطا در دریافت مراحل کارزار");
throw new Error("خطا در دریافت مراحل کارزار");
}
const data = JSON.parse(res.data.data);
if (!data || Object.keys(data).length === 0) {
return [];
}
return data;
}
export const createCampaignService = async (
data: CreateCampaignData
): Promise<Campaign> => {
@ -245,6 +302,41 @@ export const addCommentService = async (
}
};
export const createCircleService = async (
circleName: string,
campId: string,
memberIds: number[]
): Promise<{ group_id: number }> => {
const members = Object.fromEntries(
Array.from({ length: memberIds.length }, (_, i) => [`member_id${i + 1}`, memberIds[i]])
);
const body = {
"group_save_function": {
...members,
"title": circleName,
"campaign_id": campId
}
}
const [err, res] = await to(api.post(API_ADDRESS.save, body));
if (err) {
toast.error("خطا در ایجاد محفل");
throw err;
}
if (res.data.resultType !== 0) {
toast.error(res.data.message || "خطا در ایجاد محفل");
throw new Error("خطا در ایجاد محفل");
}
toast.success("محفل با موفقیت ایجاد شد");
const parsed = JSON.parse(res.data.data);
return { group_id: Number(parsed.group_id) };
};
export const removeCommentService = async (
commentId: number
): Promise<void> => {
@ -264,89 +356,3 @@ export const removeCommentService = async (
throw new Error("خطا در حذف نظر");
}
};
export const searchUsersService = async (nickname: string): Promise<User[]> => {
if (!nickname) {
return [];
}
const query = {
ProcessName: "users",
OutputFields: ["nickname", "user_id"],
conditions: [["nickname", "like", `%${nickname}%`]],
};
const [err, res] = await to(api.post(API_ADDRESS.select, query));
if (err) {
toast.error("خطا در جستجوی کاربر");
throw err;
}
if (res.data.resultType !== 0) {
toast.error("خطا در جستجوی کاربر");
throw new Error("خطا در جستجوی کاربر");
}
const data = JSON.parse(res.data.data);
return data || [];
};
export const createCircleService = async (
circleName: string,
memberIds: string[]
)=> {
const user = userInfoService.getUserInfo();
const body = {
ProcessName: "circle",
circle: {
circle_name: circleName,
creator_id: user.username,
status: "فعال",
},
members: memberIds.map((id) => ({ user_id: id })),
};
const [err, res] = await to(api.post(API_ADDRESS.save, body));
if (err) {
toast.error("خطا در ایجاد محفل");
throw err;
}
if (res.data.resultType !== 0) {
toast.error(res.data.message || "خطا در ایجاد محفل");
throw new Error("خطا در ایجاد محفل");
}
toast.success("محفل با موفقیت ایجاد شد");
return res.data;
};
export const getCampaignStepsService = async () => {
const query = {
ProcessName: "run_process",
OutputFields: ["*"],
conditions: [
["campaign_id", "=", "18909", "and"],
["group_id", "=", "18950"],
],
};
const [err, res] = await to(api.post(API_ADDRESS.select, query));
if (err) {
throw err;
}
if (res.data.resultType !== 0) {
toast.error("خطا در دریافت مراحل کارزار");
throw new Error("خطا در دریافت مراحل کارزار");
}
const data = JSON.parse(res.data.data);
if (!data || Object.keys(data).length === 0) {
return {};
}
return data;
};

View File

@ -20,6 +20,8 @@ export const fetchUserProfile = async () => {
"school_code.title",
"invitor",
"nationalcode",
"user_group.title"
,
],
conditions: [["username", "=", person.ID ? person.ID : person.username]],
};
@ -44,6 +46,7 @@ export const fetchUserProfile = async () => {
schoolCode: user?.school_code,
invitor: user?.invitor,
nationalcode: user?.nationalcode,
group: user.user_group_title
};
};