feat: add file uploader component and refactor form field validation
- Add FileUploader component import and integration in form-field - Refactor validateField function to accept typed parameters (string | number | boolean) - Move validateField definition before useEffect for better code organization - Improve code formatting and consistency in select case block - Add file upload type handling in renderInput switch statement - Extend FormFieldType interface with comprehensive field properties - Format template strings for better readability This change enhances the form field component to support file uploads and improves type safety in validation logic. The refactoring also improves code maintainability by reorganizing function definitions and standardizing formatting.
This commit is contained in:
parent
518650ccd5
commit
7c8590c075
|
|
@ -9,6 +9,7 @@ import {
|
||||||
import { useEffect, useState, type ChangeEvent, type FC } from "react";
|
import { useEffect, useState, type ChangeEvent, type FC } from "react";
|
||||||
import { BaseDropdown } from "./base-drop-down";
|
import { BaseDropdown } from "./base-drop-down";
|
||||||
import { CustomCheckbox } from "./checkbox";
|
import { CustomCheckbox } from "./checkbox";
|
||||||
|
import { FileUploader } from "./file-uploader";
|
||||||
import { ImageUploader } from "./image-uploader";
|
import { ImageUploader } from "./image-uploader";
|
||||||
import { CustomInput } from "./input";
|
import { CustomInput } from "./input";
|
||||||
import { PersonSearchInput } from "./person-search-input"; // Import PersonSearchInput
|
import { PersonSearchInput } from "./person-search-input"; // Import PersonSearchInput
|
||||||
|
|
@ -30,11 +31,9 @@ const FormField: FC<FormFieldProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
validateField(value);
|
|
||||||
}, [value]);
|
|
||||||
|
|
||||||
const validateField = (currentValue: any) => {
|
|
||||||
|
const validateField = (currentValue: string | number | boolean) => {
|
||||||
let errorMessage: string | null = null;
|
let errorMessage: string | null = null;
|
||||||
|
|
||||||
// Required validation
|
// Required validation
|
||||||
|
|
@ -127,6 +126,10 @@ const FormField: FC<FormFieldProps> = ({
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
validateField(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
const handleChange = (
|
const handleChange = (
|
||||||
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
|
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
|
||||||
) => {
|
) => {
|
||||||
|
|
@ -152,8 +155,7 @@ const FormField: FC<FormFieldProps> = ({
|
||||||
value: value || "",
|
value: value || "",
|
||||||
onChange: handleChange,
|
onChange: handleChange,
|
||||||
required: field.Required,
|
required: field.Required,
|
||||||
className: `w-full p-2 border rounded-md text-right ${
|
className: `w-full p-2 border rounded-md text-right ${error ? "border-red-500" : "border-gray-300"
|
||||||
error ? "border-red-500" : "border-gray-300"
|
|
||||||
}`,
|
}`,
|
||||||
dir: "rtl",
|
dir: "rtl",
|
||||||
};
|
};
|
||||||
|
|
@ -251,6 +253,7 @@ const FormField: FC<FormFieldProps> = ({
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "select":
|
case "select":
|
||||||
|
{
|
||||||
const isDependent = field.Parent_FieldID !== undefined;
|
const isDependent = field.Parent_FieldID !== undefined;
|
||||||
const parentFieldValue =
|
const parentFieldValue =
|
||||||
isDependent && field.Parent_FieldID !== undefined
|
isDependent && field.Parent_FieldID !== undefined
|
||||||
|
|
@ -281,13 +284,13 @@ const FormField: FC<FormFieldProps> = ({
|
||||||
undefined
|
undefined
|
||||||
}
|
}
|
||||||
onChange={(optionValue: string) => onChange(optionValue)}
|
onChange={(optionValue: string) => onChange(optionValue)}
|
||||||
className={`w-full p-2 border rounded-md text-right ${
|
className={`w-full p-2 border rounded-md text-right ${error ? "border-red-500" : "border-gray-300"
|
||||||
error ? "border-red-500" : "border-gray-300"
|
|
||||||
}`}
|
}`}
|
||||||
fetchOptions={fetchDependentOptions}
|
fetchOptions={fetchDependentOptions}
|
||||||
disabled={isDependent && !parentFieldValue} // Disable if dependent and parent value is not set
|
disabled={isDependent && !parentFieldValue} // Disable if dependent and parent value is not set
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
case "radio":
|
case "radio":
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col space-y-2" dir="rtl">
|
<div className="flex flex-col space-y-2" dir="rtl">
|
||||||
|
|
@ -325,19 +328,17 @@ const FormField: FC<FormFieldProps> = ({
|
||||||
<ImageUploader
|
<ImageUploader
|
||||||
onImageChange={(file: File | null) => onChange(file)}
|
onImageChange={(file: File | null) => onChange(file)}
|
||||||
previewImage={value}
|
previewImage={value}
|
||||||
className={`w-full p-2 border rounded-md text-right ${
|
className={`w-full p-2 border rounded-md text-right ${error ? "border-red-500" : "border-gray-300"
|
||||||
error ? "border-red-500" : "border-gray-300"
|
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "file-upload":
|
case "file-upload":
|
||||||
return (
|
return (
|
||||||
<CustomInput
|
<FileUploader
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
type="file"
|
|
||||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
onFileChange={(file) => onChange(file)}
|
||||||
onChange(e.target.files ? e.target.files[0] : null)
|
|
||||||
}
|
|
||||||
value={""} // File inputs are uncontrolled, value should be empty
|
value={""} // File inputs are uncontrolled, value should be empty
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -356,15 +357,15 @@ const FormField: FC<FormFieldProps> = ({
|
||||||
{field.Description || field.Name}
|
{field.Description || field.Name}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
default:
|
// default:
|
||||||
return (
|
// return (
|
||||||
<CustomInput
|
// <CustomInput
|
||||||
{...commonProps}
|
// {...commonProps}
|
||||||
type="text"
|
// type="text"
|
||||||
maxLength={field.Length!}
|
// maxLength={field.Length!}
|
||||||
placeholder={field.Name}
|
// placeholder={field.Name}
|
||||||
/>
|
// />
|
||||||
);
|
// );
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import to from "await-to-js";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { API_ADDRESS } from "../service/api-address";
|
import { API_ADDRESS } from "../service/api-address";
|
||||||
|
|
||||||
export const getContactImageUrl = (stageID: Number): string => {
|
export const getContactImageUrl = (stageID: number): string => {
|
||||||
const token = userInfoService.getToken();
|
const token = userInfoService.getToken();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new Error("توکن یافت نشد");
|
throw new Error("توکن یافت نشد");
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import React, {
|
||||||
|
|
||||||
import { CustomButton } from "@/core/components/base/button";
|
import { CustomButton } from "@/core/components/base/button";
|
||||||
import FormField from "@/core/components/base/form-field";
|
import FormField from "@/core/components/base/form-field";
|
||||||
|
import { uploadImage } from "@/core/utils";
|
||||||
import {
|
import {
|
||||||
FieldTypes,
|
FieldTypes,
|
||||||
FormService,
|
FormService,
|
||||||
|
|
@ -16,20 +17,37 @@ import {
|
||||||
} from "@/core/utils/dynamic-field.utils";
|
} from "@/core/utils/dynamic-field.utils";
|
||||||
import type { DynamicFormProps } from "./step-form.type";
|
import type { DynamicFormProps } from "./step-form.type";
|
||||||
|
|
||||||
const DynamicForm: FC<DynamicFormProps> = ({ fields, processId, stepId }) => {
|
const DynamicForm: FC<DynamicFormProps> = ({ fields, processId, stepId, workflowId, onChange }) => {
|
||||||
const [formValues, setFormValues] = useState<Record<string, any>>({});
|
const [formValues, setFormValues] = useState({});
|
||||||
const [_, setFormErrors] = useState<Record<string, string | null>>({});
|
const [_, setFormErrors] = useState<Record<string, string | null>>({});
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const evaluateShowCondition = useCallback(
|
||||||
|
(condition: string, currentFormValues: Record<string, any>): boolean => {
|
||||||
|
try {
|
||||||
|
const parts = condition.split("===").map((s) => s.trim());
|
||||||
|
if (parts.length === 2) {
|
||||||
|
const fieldName = parts[0];
|
||||||
|
const expectedValue = parts[1].replace(/^['"]|['"]$/g, ""); // Remove quotes
|
||||||
|
return currentFormValues[fieldName] == expectedValue;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error evaluating ShowCondition:", e);
|
||||||
|
}
|
||||||
|
return true; // Default to visible if condition cannot be parsed
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initialValues: Record<string, any> = {};
|
const initialValues: Record<string, unknown> = {};
|
||||||
fields.forEach((field) => {
|
fields.forEach((field) => {
|
||||||
if (field.DefaultValue !== undefined) {
|
if (field.DefaultValue !== undefined) {
|
||||||
initialValues[field.Name] = field.DefaultValue;
|
initialValues[field.NameOrID] = field.DefaultValue;
|
||||||
} else if (field.Type === FieldTypes.چندانتخابی) {
|
} else if (field.Type === FieldTypes.چندانتخابی) {
|
||||||
initialValues[field.Name] = [];
|
initialValues[field.NameOrID] = [];
|
||||||
} else {
|
} else {
|
||||||
initialValues[field.Name] = "";
|
initialValues[field.NameOrID] = "";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setFormValues(initialValues);
|
setFormValues(initialValues);
|
||||||
|
|
@ -39,7 +57,7 @@ const DynamicForm: FC<DynamicFormProps> = ({ fields, processId, stepId }) => {
|
||||||
(
|
(
|
||||||
field: FieldDefinition,
|
field: FieldDefinition,
|
||||||
value: any,
|
value: any,
|
||||||
allValues: Record<string, any>
|
allValues: Record<string, unknown>
|
||||||
): string | null => {
|
): string | null => {
|
||||||
// Evaluate ShowCondition first. If not visible, no validation needed.
|
// Evaluate ShowCondition first. If not visible, no validation needed.
|
||||||
if (
|
if (
|
||||||
|
|
@ -141,20 +159,20 @@ const DynamicForm: FC<DynamicFormProps> = ({ fields, processId, stepId }) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFieldChange = useCallback(
|
const handleFieldChange = useCallback(
|
||||||
(fieldName: string, newValue: unknown) => {
|
(nameOrID: string, newValue: unknown) => {
|
||||||
setFormValues((prevValues) => {
|
setFormValues((prevValues) => {
|
||||||
const updatedValues = {
|
const updatedValues = {
|
||||||
...prevValues,
|
...prevValues,
|
||||||
[fieldName]: newValue,
|
[nameOrID]: newValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Re-validate the field immediately
|
// Re-validate the field immediately
|
||||||
const fieldDef = fields.find((f) => f.Name === fieldName);
|
const fieldDef = fields.find((f) => f.NameOrID === nameOrID);
|
||||||
if (fieldDef) {
|
if (fieldDef) {
|
||||||
const error = validateField(fieldDef, newValue, updatedValues);
|
const error = validateField(fieldDef, newValue, updatedValues);
|
||||||
setFormErrors((prevErrors) => ({
|
setFormErrors((prevErrors) => ({
|
||||||
...prevErrors,
|
...prevErrors,
|
||||||
[fieldName]: error,
|
[nameOrID]: error,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
return updatedValues;
|
return updatedValues;
|
||||||
|
|
@ -163,25 +181,7 @@ const DynamicForm: FC<DynamicFormProps> = ({ fields, processId, stepId }) => {
|
||||||
[fields, validateField]
|
[fields, validateField]
|
||||||
);
|
);
|
||||||
|
|
||||||
const evaluateShowCondition = useCallback(
|
|
||||||
(condition: string, currentFormValues: Record<string, any>): boolean => {
|
|
||||||
try {
|
|
||||||
// This is a simplified evaluation. In a real app, you'd use a more robust expression parser.
|
|
||||||
// Example: "FieldName === 'someValue'"
|
|
||||||
// For now, we'll just check for simple equality.
|
|
||||||
const parts = condition.split("===").map((s) => s.trim());
|
|
||||||
if (parts.length === 2) {
|
|
||||||
const fieldName = parts[0];
|
|
||||||
const expectedValue = parts[1].replace(/^['"]|['"]$/g, ""); // Remove quotes
|
|
||||||
return currentFormValues[fieldName] == expectedValue;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error evaluating ShowCondition:", e);
|
|
||||||
}
|
|
||||||
return true; // Default to visible if condition cannot be parsed
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const visibleFields = useMemo(() => {
|
const visibleFields = useMemo(() => {
|
||||||
return fields.filter((field) => {
|
return fields.filter((field) => {
|
||||||
|
|
@ -200,9 +200,9 @@ const DynamicForm: FC<DynamicFormProps> = ({ fields, processId, stepId }) => {
|
||||||
let hasErrors = false;
|
let hasErrors = false;
|
||||||
|
|
||||||
for (const field of visibleFields) {
|
for (const field of visibleFields) {
|
||||||
const value = formValues[field.Name];
|
const value = formValues[field.NameOrID];
|
||||||
const error = validateField(field, value, formValues); // Pass formValues for conditional validation
|
const error = validateField(field, value, formValues); // Pass formValues for conditional validation
|
||||||
newErrors[field.Name] = error;
|
newErrors[field.NameOrID] = error;
|
||||||
if (error) {
|
if (error) {
|
||||||
hasErrors = true;
|
hasErrors = true;
|
||||||
}
|
}
|
||||||
|
|
@ -219,7 +219,29 @@ const DynamicForm: FC<DynamicFormProps> = ({ fields, processId, stepId }) => {
|
||||||
console.error("Form submission failed:", err);
|
console.error("Form submission failed:", err);
|
||||||
alert("Form submission failed.");
|
alert("Form submission failed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updatedValues: any = { ...formValues };
|
||||||
|
for (const [key, value] of Object.entries(formValues)) {
|
||||||
|
if (value instanceof File && value !== null) {
|
||||||
|
const fileID = await uploadImage({
|
||||||
|
file: value,
|
||||||
|
name: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
updatedValues[key] = fileID.data.data;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterData = {
|
||||||
|
...updatedValues,
|
||||||
|
run_process: workflowId,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if (workflowId)
|
||||||
|
onChange(filterData)
|
||||||
|
}
|
||||||
|
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -248,9 +270,11 @@ const DynamicForm: FC<DynamicFormProps> = ({ fields, processId, stepId }) => {
|
||||||
</label>
|
</label>
|
||||||
<FormField
|
<FormField
|
||||||
field={field}
|
field={field}
|
||||||
value={formValues[field.Name]}
|
value={formValues[field.NameOrID]}
|
||||||
onChange={(newValue: any) =>
|
onChange={(newValue: any) => {
|
||||||
handleFieldChange(field.Name, newValue)
|
handleFieldChange(field.NameOrID, newValue)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
allFormValues={formValues} // Pass all form values for dependent fields
|
allFormValues={formValues} // Pass all form values for dependent fields
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -6,20 +6,23 @@ import { FieldTypes } from "@core/utils/dynamic-field.utils";
|
||||||
import {
|
import {
|
||||||
fetchFieldIndex,
|
fetchFieldIndex,
|
||||||
fetchFielSecondeIndex,
|
fetchFielSecondeIndex,
|
||||||
|
saveFormService,
|
||||||
} from "@modules/dashboard/service/dynamic-form.service";
|
} from "@modules/dashboard/service/dynamic-form.service";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
import { getCampaignStepsService } from "../../service/campaigns.service";
|
import { getCampaignStepsService } from "../../service/campaigns.service";
|
||||||
import type { CampaignProcess, StepItems } from "../steps/step.type";
|
|
||||||
import DynamicForm from "./dynamic-form";
|
import DynamicForm from "./dynamic-form";
|
||||||
|
import type { CampaignProcess, FilterData, GroupedCampaign } from "./step-form.type";
|
||||||
|
|
||||||
const StepFormPage: FC = () => {
|
const StepFormPage: FC = () => {
|
||||||
const [fields, setFields] = useState<LocalFieldDefinition[]>([]);
|
const [fields, setFields] = useState<LocalFieldDefinition[]>([]);
|
||||||
const [params] = useSearchParams();
|
const [params] = useSearchParams();
|
||||||
const stageID = params.get("stageID");
|
const stageID = params.get("stageID");
|
||||||
const processID = params.get("processID");
|
const processID = params.get("processID");
|
||||||
|
|
||||||
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!);
|
||||||
|
|
@ -33,9 +36,12 @@ const StepFormPage: FC = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
const processedFields = useMemo(() => {
|
||||||
if (data && data.Fields) {
|
if (!data || !data.Fields) {
|
||||||
const processedFields: LocalFieldDefinition[] = data.Fields.map((el) => {
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.Fields.map((el) => {
|
||||||
const field: any = {
|
const field: any = {
|
||||||
ID: el.ID,
|
ID: el.ID,
|
||||||
Name: el.Name,
|
Name: el.Name,
|
||||||
|
|
@ -131,91 +137,131 @@ const StepFormPage: FC = () => {
|
||||||
}
|
}
|
||||||
return field;
|
return field;
|
||||||
});
|
});
|
||||||
setFields(processedFields);
|
|
||||||
}
|
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFields(processedFields);
|
||||||
|
}, [processedFields]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const currentStep = useMemo(() => {
|
const currentStep = useMemo(() => {
|
||||||
if (!Array.isArray(steps) || steps.length === 0) return [];
|
if (!Array.isArray(steps) || steps.length === 0) return [];
|
||||||
|
|
||||||
const row = steps[0];
|
const row = steps[0];
|
||||||
if (!row || Object.keys(row).length === 0) return [];
|
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 {
|
// 1) جمعآوری فرآیندها
|
||||||
processId: row[`process${index}`],
|
const temp: CampaignProcess[] = [];
|
||||||
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
|
Object.keys(row).forEach(key => {
|
||||||
const grouped = Object.values(
|
const match = key.match(/^process(\d+)_category$/);
|
||||||
processes.reduce(
|
if (match) {
|
||||||
(
|
const i = match[1];
|
||||||
acc: Record<
|
temp.push({
|
||||||
string,
|
processId: String(row[`process${i}`]),
|
||||||
{
|
category: String(row[`process${i}_category`]),
|
||||||
category: string;
|
score: String(row[`process${i}_score`]),
|
||||||
processes: CampaignProcess[];
|
stageId: String(row[`process${i}_stage_id`]),
|
||||||
stageID: number;
|
status: String(row[`process${i}_status`]),
|
||||||
processId: number;
|
title: String(row[`process${i}_title`]),
|
||||||
|
selected: false,
|
||||||
|
groupIndex: 0, // placeholder
|
||||||
|
});
|
||||||
}
|
}
|
||||||
>,
|
});
|
||||||
item
|
|
||||||
) => {
|
// 2) گروهبندی + حذف تکراری
|
||||||
if (!item || !item.category) return acc;
|
const grouped: GroupedCampaign[] = Object.values(
|
||||||
|
temp.reduce((acc: Record<string, GroupedCampaign>, item) => {
|
||||||
|
if (!item?.category) return acc;
|
||||||
|
|
||||||
if (!acc[item.category]) {
|
if (!acc[item.category]) {
|
||||||
acc[item.category] = {
|
acc[item.category] = {
|
||||||
category: item.category,
|
category: item.category,
|
||||||
processes: [],
|
processes: [],
|
||||||
stageID: Number(item.stageId),
|
groupIndex: 0,
|
||||||
processId: Number(item.processId),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// حذف تکراریها بر اساس processId
|
|
||||||
const exists = acc[item.category].processes.some(
|
const exists = acc[item.category].processes.some(
|
||||||
(p) => p.processId === item.processId
|
p => p.processId === item.processId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
acc[item.category].processes.push({
|
acc[item.category].processes.push(item);
|
||||||
processId: String(item.processId),
|
|
||||||
category: String(item.category),
|
|
||||||
score: String(item.score),
|
|
||||||
stageId: String(item.stageId),
|
|
||||||
status: String(item.status),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
},
|
}, {})
|
||||||
{}
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let datas:unknown
|
// 3) ست کردن groupIndex روی گروهها و process های داخلش
|
||||||
if(stageID) datas = grouped.find(el=> el.stageID === Number(stageID))
|
grouped.forEach((group, index) => {
|
||||||
else datas = grouped.find(el=> el.processId === Number(processID))
|
group.groupIndex = index;
|
||||||
return datas?.processes as Array<StepItems>;
|
group.processes.forEach(proc => {
|
||||||
|
proc.groupIndex = index;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4) فیلتر مناسب بر اساس Stage یا Process
|
||||||
|
let filtered = grouped;
|
||||||
|
|
||||||
|
if (stageID) {
|
||||||
|
filtered = grouped.filter(g => g.processes.some(p => p.stageId === String(stageID)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processID) {
|
||||||
|
filtered = grouped.filter(g => g.processes.some(p => p.processId === String(processID)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) حالا سلکت داینامیک فقط روی گروههای فیلترشده اجرا میشه
|
||||||
|
filtered.forEach(group => {
|
||||||
|
const arr = group.processes;
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
if (arr[i].status === "انجام نشده") {
|
||||||
|
arr[i].selected = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return filtered.flatMap(g => g.processes);
|
||||||
}, [steps, stageID, processID]);
|
}, [steps, stageID, processID]);
|
||||||
|
|
||||||
|
|
||||||
|
const saveForm = useMutation({
|
||||||
|
mutationFn: saveFormService,
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (data.resultType !== 0) {
|
||||||
|
toast.error(data.message || "خطایی رخ داد");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success("ثبت نام با موفقیت انجام شد");
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
console.error("Registration error:", error);
|
||||||
|
toast.error(
|
||||||
|
"خطا در ثبت نام: " + (error?.message || "لطفاً دوباره تلاش کنید")
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const saveFormHandler = (data: FilterData) => {
|
||||||
|
if (currentStep) {
|
||||||
|
const changeData = {
|
||||||
|
[`process${currentStep[0]?.groupIndex + 1}`]: data
|
||||||
|
}
|
||||||
|
saveForm.mutate(changeData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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">
|
||||||
|
|
@ -238,17 +284,18 @@ const StepFormPage: FC = () => {
|
||||||
فرم پویا
|
فرم پویا
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex flex-row justify-center gap-10">
|
<div className="flex flex-row justify-center gap-10">
|
||||||
{currentStep.map((el, idx) => {
|
{currentStep?.map((el, idx) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={idx} className="flex flex-col justify-center mb-4 text-right">
|
<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={`w-7 h-7 bg-red-950 rounded-full block m-auto ${el.selected ? 'bg-green-700!' : ''}`}></div>
|
||||||
<div className="font-semibold text-sm mb-2">{el.category}</div>
|
<div className="font-semibold text-sm mb-2">{el.title}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DynamicForm fields={fields} processId={1} stepId={1} />
|
<DynamicForm fields={fields} processId={1} stepId={1} workflowId={steps[0]?.WorkflowID} onChange={saveFormHandler} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,29 @@ export interface DynamicFormProps {
|
||||||
fields: FieldDefinition[];
|
fields: FieldDefinition[];
|
||||||
processId: number;
|
processId: number;
|
||||||
stepId: number;
|
stepId: number;
|
||||||
|
workflowId: number
|
||||||
|
onChange: (items: any) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface FilterData {
|
||||||
|
[key: string]: any;
|
||||||
|
run_process: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CampaignProcess = {
|
||||||
|
processId: string;
|
||||||
|
category: string;
|
||||||
|
score: string;
|
||||||
|
stageId: string;
|
||||||
|
status: string;
|
||||||
|
title: string;
|
||||||
|
selected: boolean;
|
||||||
|
groupIndex: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GroupedCampaign = {
|
||||||
|
category: string;
|
||||||
|
processes: CampaignProcess[];
|
||||||
|
groupIndex: number;
|
||||||
|
};
|
||||||
|
|
@ -3,6 +3,7 @@ import api from "@/core/service/axios";
|
||||||
import type { WorkflowResponse } from "@/core/types/global.type";
|
import type { WorkflowResponse } from "@/core/types/global.type";
|
||||||
import to from "await-to-js";
|
import to from "await-to-js";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
import type { FilterData } from "../pages/step-form/step-form.type";
|
||||||
|
|
||||||
export const fetchFieldIndex = async (
|
export const fetchFieldIndex = async (
|
||||||
id: string
|
id: string
|
||||||
|
|
@ -23,7 +24,7 @@ export const fetchFieldIndex = async (
|
||||||
export const fetchFielSecondeIndex = async (
|
export const fetchFielSecondeIndex = async (
|
||||||
id: string
|
id: string
|
||||||
): Promise<WorkflowResponse> => {
|
): Promise<WorkflowResponse> => {
|
||||||
const [err, res] = await to(api.post(API_ADDRESS.index, id));
|
const [err, res] = await to(api.post(API_ADDRESS.index2, id));
|
||||||
if (err) {
|
if (err) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
@ -35,3 +36,18 @@ export const fetchFielSecondeIndex = async (
|
||||||
const data = JSON.parse(res.data.data);
|
const data = JSON.parse(res.data.data);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const saveFormService = async (items: FilterData) => {
|
||||||
|
const [err, res] = await to(api.post(API_ADDRESS.save, items));
|
||||||
|
if (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.data.resultType !== 0) {
|
||||||
|
toast.error(res.data.message || "خطا در دریافت فیلدها");
|
||||||
|
throw new Error("خطا در دریافت فیلدها");
|
||||||
|
}
|
||||||
|
const data = JSON.parse(res.data.data);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user