diff --git a/src/webparts/requestPortal/assets/logos/astel-k.png b/src/webparts/requestPortal/assets/logos/astel-k.png new file mode 100644 index 0000000..f9ca649 Binary files /dev/null and b/src/webparts/requestPortal/assets/logos/astel-k.png differ diff --git a/src/webparts/requestPortal/assets/logos/ctglobal.png b/src/webparts/requestPortal/assets/logos/ctglobal.png new file mode 100644 index 0000000..a5128e4 Binary files /dev/null and b/src/webparts/requestPortal/assets/logos/ctglobal.png differ diff --git a/src/webparts/requestPortal/assets/logos/kazprom-service.png b/src/webparts/requestPortal/assets/logos/kazprom-service.png new file mode 100644 index 0000000..c2e0afb Binary files /dev/null and b/src/webparts/requestPortal/assets/logos/kazprom-service.png differ diff --git a/src/webparts/requestPortal/assets/logos/qazgeomax.png b/src/webparts/requestPortal/assets/logos/qazgeomax.png new file mode 100644 index 0000000..822475b Binary files /dev/null and b/src/webparts/requestPortal/assets/logos/qazgeomax.png differ diff --git a/src/webparts/requestPortal/assets/logos/tansu-construction.png b/src/webparts/requestPortal/assets/logos/tansu-construction.png new file mode 100644 index 0000000..a5873aa Binary files /dev/null and b/src/webparts/requestPortal/assets/logos/tansu-construction.png differ diff --git a/src/webparts/requestPortal/assets/welcome-dark.png b/src/webparts/requestPortal/assets/welcome-dark.png deleted file mode 100644 index 42f0b8d..0000000 Binary files a/src/webparts/requestPortal/assets/welcome-dark.png and /dev/null differ diff --git a/src/webparts/requestPortal/assets/welcome-light.png b/src/webparts/requestPortal/assets/welcome-light.png deleted file mode 100644 index 69eb3b4..0000000 Binary files a/src/webparts/requestPortal/assets/welcome-light.png and /dev/null differ diff --git a/src/webparts/requestPortal/forms/Clearance/ClearanceView.module.scss b/src/webparts/requestPortal/forms/Clearance/ClearanceView.module.scss index 78c0f57..3e29cc7 100644 --- a/src/webparts/requestPortal/forms/Clearance/ClearanceView.module.scss +++ b/src/webparts/requestPortal/forms/Clearance/ClearanceView.module.scss @@ -9,7 +9,10 @@ .toolbar { display: flex; + flex-wrap: wrap; + align-items: center; justify-content: flex-end; + gap: $sp-3; margin-bottom: $sp-4; } diff --git a/src/webparts/requestPortal/forms/Clearance/ClearanceView.tsx b/src/webparts/requestPortal/forms/Clearance/ClearanceView.tsx index 7511dd9..63ace74 100644 --- a/src/webparts/requestPortal/forms/Clearance/ClearanceView.tsx +++ b/src/webparts/requestPortal/forms/Clearance/ClearanceView.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { useState, useEffect, useMemo, useCallback } from 'react'; import type { WebPartContext } from '@microsoft/sp-webpart-base'; import styles from './ClearanceView.module.scss'; -import { Alert, Button, FormCard, InitiatorLine, FormModeProvider, PrintButton } from '../../shared/ui'; +import { Alert, Button, FormCard, InitiatorLine, FormModeProvider, PrintButton, PrintOrgSelect, OrgLetterhead } from '../../shared/ui'; import { ApproverService, DecisionPanel, @@ -62,6 +62,9 @@ const ClearanceView: React.FC = ({ context, itemId, formKey // ввода (набрал сумму → data стал непустым → поле сразу стало бы readOnly). const [lockedWriteOffs, setLockedWriteOffs] = useState>(new Set()); const [saveState, setSaveState] = useState('idle'); + // Какая из организаций сотрудника (см. OrganizationField) идёт в печать — + // выбор в тулбаре через PrintOrgSelect, по умолчанию первая. + const [printOrgIndex, setPrintOrgIndex] = useState(0); const [saveErrorMessage, setSaveErrorMessage] = useState(''); const set: SetField = (key, value) => setData((prev) => ({ ...prev, [key]: value })); @@ -238,7 +241,8 @@ const ClearanceView: React.FC = ({ context, itemId, formKey context, hrRecordsEditable, parkingEditable, - writeOffEditableKeys: editableWriteOffKeys + writeOffEditableKeys: editableWriteOffKeys, + printOrgIndex }; const notify = (): void => { @@ -256,6 +260,7 @@ const ClearanceView: React.FC = ({ context, itemId, formKey return (
+
@@ -275,6 +280,7 @@ const ClearanceView: React.FC = ({ context, itemId, formKey )}
+ void; + /** Индекс организации для печати (см. shared/ui/PrintOrgSelect и OrganizationField.printIndex). */ + printOrgIndex?: number; } export const EMPTY_DATA: IFormData = { @@ -131,6 +137,8 @@ export const EMPTY_DATA: IFormData = { AccessCard: false, TrainingAmount: '', Organization: '', + Project: '', + ProjectId: '', DismissalUrl: '', WriteOffAssetsAmount: '', WriteOffPpeAmount: '', diff --git a/src/webparts/requestPortal/forms/Clearance/provisioning.json b/src/webparts/requestPortal/forms/Clearance/provisioning.json index d9b321c..1d5c1b2 100644 --- a/src/webparts/requestPortal/forms/Clearance/provisioning.json +++ b/src/webparts/requestPortal/forms/Clearance/provisioning.json @@ -13,6 +13,8 @@ { "internalName": "AccessCard", "title": "Карта СКУД", "type": "Boolean" }, { "internalName": "TrainingAmount", "title": "Обучение: сумма", "type": "Number", "decimals": 2 }, { "internalName": "Organization", "title": "Организация", "type": "Note" }, + { "internalName": "Project", "title": "Проект", "type": "Text" }, + { "internalName": "ProjectId", "title": "Id проекта (справочник)", "type": "Text" }, { "internalName": "DismissalUrl", "title": "Ссылка на заявление на увольнение", "type": "Url" }, diff --git a/src/webparts/requestPortal/forms/Clearance/sections/RequestSection.tsx b/src/webparts/requestPortal/forms/Clearance/sections/RequestSection.tsx index 9d1850f..e0401d0 100644 --- a/src/webparts/requestPortal/forms/Clearance/sections/RequestSection.tsx +++ b/src/webparts/requestPortal/forms/Clearance/sections/RequestSection.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { FormGrid, CollapseSection, FormField, TextInput, MoneyInput, Toggle, PeopleField, OrganizationField, LinkField } from '../../../shared/ui'; +import { ProjectPicker } from '../../../shared/dictionary'; import { ISectionProps, EMPLOYEE_PLACEHOLDER, WRITE_OFF_CATEGORIES } from '../model'; // Подразделение подтягивается автоматически при выборе сотрудника (см. @@ -30,6 +31,7 @@ export const RequestSection: React.FC = ({ onEmployeeChange, hrRecordsEditable, parkingEditable, + printOrgIndex, writeOffEditableKeys }) => { return ( @@ -65,7 +67,19 @@ export const RequestSection: React.FC = ({ placeholder="Подставится автоматически по выбранному сотруднику" /> - + + + { + set('Project', name); + set('ProjectId', id); + }} + placeholder="Начните вводить название или код проекта…" + /> + = ({ context, itemI Организация рабочего места на основе этой - + {isApprovedStatus(status) && }
diff --git a/src/webparts/requestPortal/forms/RecruitingHire/load.ts b/src/webparts/requestPortal/forms/RecruitingHire/load.ts index 90d5a95..505634b 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/load.ts +++ b/src/webparts/requestPortal/forms/RecruitingHire/load.ts @@ -18,7 +18,7 @@ function parseJsonArray(raw: unknown): T[] { const SCALAR_FIELDS: (keyof IFormData)[] = [ 'Phone', 'HireType', 'ReplacedEmployee', 'Project', 'ProjectId', 'ProjectCode', 'CandidateName', 'HireDate', 'Position', 'WorkKind', - 'SiteName', 'SiteLocation', 'PpeHeight', 'PpeClothingSize', 'PpeShoeSize', 'ProbationPeriod', + 'Department', 'DepartmentId', 'SiteLocation', 'PpeHeight', 'PpeClothingSize', 'PpeShoeSize', 'ProbationPeriod', 'SalaryProbation', 'SalaryAfterProbation', 'WorkSchedule', 'WorkScheduleOther', 'AgencyName', 'AgencyTerms', 'SelectionRequestId', 'SelectionRequestListId', 'SelectionRequestCode' diff --git a/src/webparts/requestPortal/forms/RecruitingHire/model.ts b/src/webparts/requestPortal/forms/RecruitingHire/model.ts index e62ae93..37b1a37 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/model.ts +++ b/src/webparts/requestPortal/forms/RecruitingHire/model.ts @@ -34,7 +34,16 @@ export interface IFormData { HireDate: string; Position: string; WorkKind: string; - SiteName: string; + /** + * Подразделение по справочнику (Dictionary/Departments, см. + * shared/dictionary/DepartmentPicker) — как в WorkplaceSetup/RecruitingSelection: + * ручной выбор, чисто информационное, на маршрут согласования НЕ влияет. + * Заменило прежнее свободнотекстовое поле «Отдел / участок» (колонка SiteName + * в SP осталась в схеме нетронутой — просто больше не используется формой). + */ + Department: string; + /** Id отдела из справочника; '' — если введён вручную без выбора из списка. */ + DepartmentId: string; SiteLocation: string; // СИЗ PpeHeight: string; @@ -77,7 +86,8 @@ export const EMPTY_DATA: IFormData = { HireDate: '', Position: '', WorkKind: '', - SiteName: '', + Department: '', + DepartmentId: '', SiteLocation: '', PpeHeight: '', PpeClothingSize: '', diff --git a/src/webparts/requestPortal/forms/RecruitingHire/provisioning.json b/src/webparts/requestPortal/forms/RecruitingHire/provisioning.json index 3f6407e..4935d2f 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/provisioning.json +++ b/src/webparts/requestPortal/forms/RecruitingHire/provisioning.json @@ -22,7 +22,9 @@ { "internalName": "HireDate", "title": "Дата приёма", "type": "DateTime", "dateOnly": true }, { "internalName": "Position", "title": "Должность", "type": "Text", "required": true }, { "internalName": "WorkKind", "title": "Вид работ", "type": "Text" }, - { "internalName": "SiteName", "title": "Отдел / участок", "type": "Text" }, + { "internalName": "SiteName", "title": "Отдел / участок (устарело)", "type": "Text" }, + { "internalName": "Department", "title": "Подразделение (справочник)", "type": "Text" }, + { "internalName": "DepartmentId", "title": "Id подразделения (справочник)", "type": "Text" }, { "internalName": "SiteLocation", "title": "Местоположение", "type": "Text" }, { "internalName": "PpeHeight", "title": "СИЗ: рост", "type": "Number" }, diff --git a/src/webparts/requestPortal/forms/RecruitingHire/sections/CandidateSection.tsx b/src/webparts/requestPortal/forms/RecruitingHire/sections/CandidateSection.tsx index 50b1ce8..437847b 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/sections/CandidateSection.tsx +++ b/src/webparts/requestPortal/forms/RecruitingHire/sections/CandidateSection.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; import { CollapseSection, FormField, TextInput } from '../../../shared/ui'; +import { DepartmentPicker } from '../../../shared/dictionary'; import { ISectionProps } from '../model'; -export const CandidateSection: React.FC = ({ data, set }) => ( +export const CandidateSection: React.FC = ({ data, set, context }) => ( set('CandidateName', v)} /> @@ -16,8 +17,16 @@ export const CandidateSection: React.FC = ({ data, set }) => ( set('WorkKind', v)} /> - - set('SiteName', v)} /> + + { + set('Department', name); + set('DepartmentId', id); + }} + /> set('SiteLocation', v)} /> diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx index f4b2485..2b36172 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx +++ b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx @@ -35,6 +35,7 @@ import { IAttachmentInfo } from '../../shared/attachments/attachments.service'; import { effectiveUserEmail } from '../../shared/impersonation'; +import { getEmployeeOrganizations, ICompany, IEmployeeOrganization } from '../../shared/dictionary'; import { friendlyErrorMessage } from '../../shared/diagnostics'; import { IRequestFormProps } from '../../models/types'; import { @@ -98,6 +99,57 @@ const RecruitingSelectionForm: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [editId, editListId]); + // Компании ответственного (Employee/ByEmail.company/companyId) → предзаполнение + // «1.6 Организация» (Companies/CompanyPickerMulti). companyId из Employee/ByEmail + // совпадает с Id из Dictionary/Companies (тот же источник) — доп. запрос не нужен. + // Поле остаётся редактируемым: значения только ПРЕДЛАГАЮТСЯ, как и DepartmentName. + const toCompanies = (records: IEmployeeOrganization[]): ICompany[] => + records.filter((r) => r.companyId && r.company).map((r) => ({ Id: r.companyId, Name: r.company })); + + // Новая заявка — по умолчанию ответственный: сам подающий, подразделение и + // организация — из Employee/ByEmail (Section/company), как в остальных формах + // с авто-«Организацией» (см. shared/dictionary/employeeOrganizations.service.ts). + useEffect(() => { + if (editId) { + return; + } + let alive = true; + const user = context.pageContext.user; + setData((prev) => ({ ...prev, Responsible: prev.Responsible || user.email })); + getEmployeeOrganizations(context, user.email) + .then((records) => { + if (alive) { + setData((prev) => ({ + ...prev, + DepartmentName: prev.DepartmentName || records[0]?.section || prev.DepartmentName, + Companies: prev.Companies.length > 0 ? prev.Companies : toCompanies(records) + })); + } + }) + .catch(() => undefined); + return () => { + alive = false; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [editId]); + + // Выбор другого ответственного подтягивает его подразделение и организацию. + const handleResponsibleChange = (login: string): void => { + setData((prev) => ({ ...prev, Responsible: login })); + if (!login) { + return; + } + getEmployeeOrganizations(context, login) + .then((records) => + setData((prev) => ({ + ...prev, + DepartmentName: records[0]?.section || prev.DepartmentName, + Companies: toCompanies(records) + })) + ) + .catch(() => undefined); + }; + // Маршрут согласования резолвится по email инициатора (текущего пользователя). // У заявки на редактировании маршрут читается из уже записанных строк, чтобы не // затереть правки инициатора при первом сохранении. @@ -242,7 +294,7 @@ const RecruitingSelectionForm: React.FC = ({ const handleSaveDraft = (): Promise => submit.run('draft', save(REQUEST_STATUS.draft), validate); const handleSubmit = (): Promise => submit.run('submit', save(REQUEST_STATUS.onApproval), validateSubmit); - const sectionProps = { data, set, context }; + const sectionProps = { data, set, context, onResponsibleChange: handleResponsibleChange }; return (
diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionView.tsx b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionView.tsx index 8eba89a..face6b1 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionView.tsx +++ b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionView.tsx @@ -10,7 +10,8 @@ import { ApprovalMatrix, useMatrixEditor, notifyActiveStep, - useRequestTypeId + useRequestTypeId, + isApprovedStatus } from '../../shared/approval'; import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service'; import { friendlyErrorMessage } from '../../shared/diagnostics'; @@ -118,7 +119,7 @@ const RecruitingSelectionView: React.FC = ({ cont return (
- + {isApprovedStatus(status) && }
diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/model.ts b/src/webparts/requestPortal/forms/RecruitingSelection/model.ts index 74d43ee..36c1d24 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/model.ts +++ b/src/webparts/requestPortal/forms/RecruitingSelection/model.ts @@ -25,7 +25,16 @@ export interface IFormData { ObjectProjectId: string; ObjectProjectCode: string; // Отдел + /** Подразделение ОТВЕТСТВЕННОГО за подачу (Employee/ByEmail.Section при выборе Responsible). */ DepartmentName: string; + /** + * Подразделение по справочнику (Dictionary/Departments, см. + * shared/dictionary/DepartmentPicker) — как в WorkplaceSetup: ручной выбор, + * чисто информационное, на маршрут согласования НЕ влияет. + */ + Department: string; + /** Id отдела из справочника; '' — если введён вручную без выбора из списка. */ + DepartmentId: string; OpenVacanciesCount: string; // Вакансия Position: string; @@ -71,6 +80,12 @@ export interface ISectionProps { data: IFormData; set: SetField; context: WebPartContext; + /** + * Выбор ответственного подтягивает его подразделение (Employee/ByEmail → + * Section) — поэтому у поля отдельный обработчик. Если не передан (режим + * чтения) — правка через set. + */ + onResponsibleChange?: (login: string) => void; } export const EMPTY_DATA: IFormData = { @@ -84,6 +99,8 @@ export const EMPTY_DATA: IFormData = { ObjectProjectId: '', ObjectProjectCode: '', DepartmentName: '', + Department: '', + DepartmentId: '', OpenVacanciesCount: '', Position: '', EmploymentType: '', diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/provisioning.json b/src/webparts/requestPortal/forms/RecruitingSelection/provisioning.json index d8dd602..78cf996 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/provisioning.json +++ b/src/webparts/requestPortal/forms/RecruitingSelection/provisioning.json @@ -13,7 +13,9 @@ { "internalName": "ObjectProjectCode", "title": "Объект/Проект — код", "type": "Text" }, { "internalName": "ClosingDeadline", "title": "Крайний срок закрытия", "type": "DateTime", "dateOnly": true }, - { "internalName": "DepartmentName", "title": "Название отдела/департамента", "type": "Text", "required": true }, + { "internalName": "DepartmentName", "title": "Подразделение ответственного за подачу", "type": "Text", "required": true }, + { "internalName": "Department", "title": "Подразделение (справочник)", "type": "Text" }, + { "internalName": "DepartmentId", "title": "Id подразделения (справочник)", "type": "Text" }, { "internalName": "OpenVacanciesCount", "title": "Количество открытых вакансий", "type": "Number" }, { "internalName": "Position", "title": "Должность", "type": "Text", "required": true }, diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/sections/ApplicationSection.tsx b/src/webparts/requestPortal/forms/RecruitingSelection/sections/ApplicationSection.tsx index 3853ea7..d540745 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/sections/ApplicationSection.tsx +++ b/src/webparts/requestPortal/forms/RecruitingSelection/sections/ApplicationSection.tsx @@ -10,7 +10,7 @@ import { OPEN_REASON_NEW } from '../model'; -export const ApplicationSection: React.FC = ({ data, set, context }) => { +export const ApplicationSection: React.FC = ({ data, set, context, onResponsibleChange }) => { const reason = data.OpenReason; const showReasonDetails = reason !== '' && reason !== OPEN_REASON_NEW; const reasonDetailsLabel = @@ -22,14 +22,22 @@ export const ApplicationSection: React.FC = ({ data, set, context set('Responsible', v)} + onChange={onResponsibleChange ?? ((v) => set('Responsible', v))} placeholder={PERSON_PLACEHOLDER} /> - + + set('DepartmentName', v)} + placeholder="Подставится автоматически по выбранному ответственному" + /> + + set('SubmitDate', v)} /> - + = ({ data, set, context onChange={(v) => set('ClosingDeadline', v)} /> - + onChange(Number(v))} options={options} /> +
+ ); +}; diff --git a/src/webparts/requestPortal/shared/ui/index.ts b/src/webparts/requestPortal/shared/ui/index.ts index 22c9bf5..da1478b 100644 --- a/src/webparts/requestPortal/shared/ui/index.ts +++ b/src/webparts/requestPortal/shared/ui/index.ts @@ -39,6 +39,8 @@ export { parseExcelRows } from './ItemsTable/parseExcelRows'; export { PeopleField } from './PeopleField/PeopleField'; export { PeopleFieldMulti } from './PeopleField/PeopleFieldMulti'; export { OrganizationField } from './OrganizationField/OrganizationField'; +export { PrintOrgSelect } from './PrintOrgSelect/PrintOrgSelect'; +export { OrgLetterhead } from './OrgLetterhead/OrgLetterhead'; export { LinkField } from './LinkField/LinkField'; export { ErrorOverlay } from './ErrorOverlay/ErrorOverlay'; export { SubmitActions } from './SubmitActions/SubmitActions';