feat: подразделения в подборе и приёме фильтруются по организации
Tests / frontend-test (push) Successful in 1m14s
Tests / frontend-test (push) Successful in 1m14s
This commit is contained in:
@@ -1,9 +1,16 @@
|
||||
import * as React from "react";
|
||||
import { CollapseSection, FormField, TextInput } from "../../../shared/ui";
|
||||
import { PositionPicker } from "../../../shared/dictionary";
|
||||
import { ISectionProps } from "../model";
|
||||
import { DepartmentPicker, PositionPicker } from "../../../shared/dictionary";
|
||||
import { IFormData, ISectionProps } from "../model";
|
||||
import { HireDateField } from "./HireDateField";
|
||||
|
||||
// Отдел ищем в «Основном месте работы», если оно выбрано, иначе — во всех
|
||||
// организациях заявки.
|
||||
function departmentCompanyIds(data: IFormData): string[] {
|
||||
if (data.MainCompanyId) return [data.MainCompanyId];
|
||||
return data.Companies.map((c) => c.Id);
|
||||
}
|
||||
|
||||
interface ICandidateSectionProps extends ISectionProps {
|
||||
/** Начальник расчётной группы правит отдел/участок даже в режиме просмотра (см. approverRights.ts). */
|
||||
payrollEditable?: boolean;
|
||||
@@ -42,11 +49,15 @@ export const CandidateSection: React.FC<ICandidateSectionProps> = ({
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="2.4 Отдел/Участок" htmlFor="Department">
|
||||
<TextInput
|
||||
{/* Выбор из справочника или свой текст — отдела может не быть в справочнике. */}
|
||||
<DepartmentPicker
|
||||
context={context}
|
||||
id="Department"
|
||||
value={data.Department}
|
||||
onChange={(v) => set("Department", v)}
|
||||
onChange={(name) => set("Department", name)}
|
||||
companyIds={departmentCompanyIds(data)}
|
||||
readOnly={payrollEditable ? false : undefined}
|
||||
placeholder="Выберите из списка или введите вручную"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="2.5 Местоположение" htmlFor="SiteLocation">
|
||||
|
||||
@@ -15,6 +15,9 @@ export const DepartmentSection: React.FC<ISectionProps> = ({ data, set, context
|
||||
id="Department"
|
||||
context={context}
|
||||
value={data.Department}
|
||||
valueId={data.DepartmentId}
|
||||
// Подразделения всех организаций из «1.5 Организация».
|
||||
companyIds={data.Companies.map((c) => c.Id)}
|
||||
onChange={(name, id) => {
|
||||
set('Department', name);
|
||||
set('DepartmentId', id);
|
||||
|
||||
@@ -4,32 +4,45 @@ import styles from './DepartmentPicker.module.scss';
|
||||
import { useFormReadOnly } from '@tnsu/ui-kit';
|
||||
import { getDepartments } from '../departments.service';
|
||||
import { IDepartment } from '../models/IDepartment';
|
||||
import { filterDepartmentsByCompanies, isDepartmentOutsideCompanies } from './departmentCompanyFilter';
|
||||
|
||||
export interface IDepartmentPickerProps {
|
||||
context: WebPartContext;
|
||||
id?: string;
|
||||
/** Текущее название отдела (то, что видно в поле). */
|
||||
value: string;
|
||||
/** Id выбранного отдела, если форма его хранит, — точнее, чем сверка по названию. */
|
||||
valueId?: string;
|
||||
/** name — введённый/выбранный текст; id — Id из справочника ('' если текст введён вручную и не выбран из списка). */
|
||||
onChange: (name: string, id: string) => void;
|
||||
placeholder?: string;
|
||||
/** Только чтение (режим согласующего). */
|
||||
readOnly?: boolean;
|
||||
/**
|
||||
* Показывать подразделения только этих компаний (Id из Dictionary/Companies).
|
||||
* undefined — все компании; [] — организация не выбрана, поле заблокировано.
|
||||
* При смене набора выбранное подразделение чужой компании очищается.
|
||||
*/
|
||||
companyIds?: string[];
|
||||
}
|
||||
|
||||
const MAX_SUGGESTIONS = 30;
|
||||
const NO_COMPANY_PLACEHOLDER = 'Сначала выберите организацию';
|
||||
|
||||
/** Поле «Отдел / участок» со справочником (Dictionary/Departments): ввод текста фильтрует список, выбор проставляет Id. */
|
||||
export const DepartmentPicker: React.FC<IDepartmentPickerProps> = ({
|
||||
context,
|
||||
id,
|
||||
value,
|
||||
valueId,
|
||||
onChange,
|
||||
placeholder,
|
||||
readOnly
|
||||
readOnly,
|
||||
companyIds
|
||||
}) => {
|
||||
const contextReadOnly = useFormReadOnly();
|
||||
const effectiveReadOnly = readOnly ?? contextReadOnly;
|
||||
const isCompanyMissing = !!companyIds && companyIds.length === 0;
|
||||
const effectiveReadOnly = (readOnly ?? contextReadOnly) || isCompanyMissing;
|
||||
|
||||
const [departments, setDepartments] = React.useState<IDepartment[]>([]);
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
@@ -46,11 +59,33 @@ export const DepartmentPicker: React.FC<IDepartmentPickerProps> = ({
|
||||
};
|
||||
}, [context]);
|
||||
|
||||
// Сверку делаем только при смене компаний (или приходе справочника), а не на
|
||||
// каждое нажатие клавиши — иначе набираемое название, совпавшее с отделом
|
||||
// чужой компании, стиралось бы прямо во время ввода. Поэтому текущее значение
|
||||
// читаем из ref, а не держим в зависимостях.
|
||||
const latest = React.useRef({ value, valueId, onChange });
|
||||
latest.current = { value, valueId, onChange };
|
||||
const companyKey = companyIds ? companyIds.join('|') : undefined;
|
||||
|
||||
React.useEffect(() => {
|
||||
if ((readOnly ?? contextReadOnly) || departments.length === 0) return;
|
||||
const current = latest.current;
|
||||
if (isDepartmentOutsideCompanies(departments, current.value, current.valueId, companyIds)) {
|
||||
current.onChange('', '');
|
||||
}
|
||||
// companyIds сравниваем по companyKey: массив пересоздаётся на каждом рендере.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [departments, companyKey, readOnly, contextReadOnly]);
|
||||
|
||||
const suggestions = React.useMemo(() => {
|
||||
const available = filterDepartmentsByCompanies(departments, companyIds);
|
||||
const query = value.trim().toLowerCase();
|
||||
if (!query) return [];
|
||||
return departments.filter((d) => d.Name.toLowerCase().indexOf(query) !== -1).slice(0, MAX_SUGGESTIONS);
|
||||
}, [departments, value]);
|
||||
// Без фильтра по компании справочник — сотни записей, показываем их только
|
||||
// по вводу. С фильтром список короткий — открываем его сразу, по фокусу.
|
||||
if (!query) return companyIds ? available.slice(0, MAX_SUGGESTIONS) : [];
|
||||
return available.filter((d) => d.Name.toLowerCase().indexOf(query) !== -1).slice(0, MAX_SUGGESTIONS);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [departments, value, companyKey]);
|
||||
|
||||
const handlePick = (dept: IDepartment): void => {
|
||||
onChange(dept.Name, dept.Id);
|
||||
@@ -63,7 +98,7 @@ export const DepartmentPicker: React.FC<IDepartmentPickerProps> = ({
|
||||
id={id}
|
||||
className={styles.input}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
placeholder={isCompanyMissing ? NO_COMPANY_PLACEHOLDER : placeholder}
|
||||
readOnly={effectiveReadOnly}
|
||||
// setOpen(true) и при вводе, а не только по фокусу: после выбора список
|
||||
// закрывается (handlePick), но фокус остаётся на поле — без этого
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { IDepartment } from '../models/IDepartment';
|
||||
|
||||
/**
|
||||
* Подразделения только указанных компаний. companyIds === undefined — без
|
||||
* ограничения (формы, где поле не зависит от организации).
|
||||
*/
|
||||
export function filterDepartmentsByCompanies(departments: IDepartment[], companyIds?: string[]): IDepartment[] {
|
||||
if (!companyIds) return departments;
|
||||
return departments.filter((d) => companyIds.indexOf(d.CompanyId) !== -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Выбранное подразделение относится к ДРУГОЙ компании, чем разрешённые, — его
|
||||
* надо очистить.
|
||||
*
|
||||
* Сначала по Id (точно: названия вроде «Бухгалтерия» повторяются в разных
|
||||
* компаниях), без Id — по названию. Текст, которого в справочнике нет вовсе
|
||||
* (введён вручную), не трогаем: компанию у него определить нельзя.
|
||||
*/
|
||||
export function isDepartmentOutsideCompanies(
|
||||
departments: IDepartment[],
|
||||
name: string,
|
||||
id: string | undefined,
|
||||
companyIds?: string[]
|
||||
): boolean {
|
||||
if (!companyIds || !name.trim()) return false;
|
||||
|
||||
const byId = id ? departments.filter((d) => d.Id === id)[0] : undefined;
|
||||
if (byId) return companyIds.indexOf(byId.CompanyId) === -1;
|
||||
|
||||
const normalizedName = name.trim().toLowerCase();
|
||||
const byName = departments.filter((d) => d.Name.trim().toLowerCase() === normalizedName);
|
||||
if (byName.length === 0) return false;
|
||||
return !byName.some((d) => companyIds.indexOf(d.CompanyId) !== -1);
|
||||
}
|
||||
Reference in New Issue
Block a user