From e5b4af63fd220750c9fba41e5f6fc8fdaa1ba1bc Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Fri, 31 Jul 2026 15:34:45 +0500 Subject: [PATCH] =?UTF-8?q?=D0=B2=20=D1=80=D0=B5=D0=B7=D0=BE=D0=BB=D0=B2?= =?UTF-8?q?=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=B0=D1=8F=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../approvalAdmin/components/app/AdminApp.tsx | 2 +- .../screens/ResolveScreen.module.scss | 34 +++++++ .../components/screens/ResolveScreen.tsx | 90 +++++++++++++++++-- .../hooks/useLocalStorageState.ts | 14 ++- .../webparts/approvalAdmin/models/resolve.ts | 7 ++ .../webparts/approvalAdmin/services/api.ts | 4 +- frontend/src/webparts/approvalAdmin/texts.ts | 7 ++ 7 files changed, 148 insertions(+), 10 deletions(-) diff --git a/frontend/src/webparts/approvalAdmin/components/app/AdminApp.tsx b/frontend/src/webparts/approvalAdmin/components/app/AdminApp.tsx index a2b645f..474e25b 100644 --- a/frontend/src/webparts/approvalAdmin/components/app/AdminApp.tsx +++ b/frontend/src/webparts/approvalAdmin/components/app/AdminApp.tsx @@ -56,7 +56,7 @@ export const AdminApp: React.FC = ({ api, dictionary, sharePoint /> )} {screen === 'resolve' && ( - + )} diff --git a/frontend/src/webparts/approvalAdmin/components/screens/ResolveScreen.module.scss b/frontend/src/webparts/approvalAdmin/components/screens/ResolveScreen.module.scss index add0064..bc814a0 100644 --- a/frontend/src/webparts/approvalAdmin/components/screens/ResolveScreen.module.scss +++ b/frontend/src/webparts/approvalAdmin/components/screens/ResolveScreen.module.scss @@ -1,3 +1,37 @@ +@import '../../../../styles/design-tokens'; + +.conditions { + margin-top: 20px; + padding-top: 18px; + border-top: 1px solid $gray-200; +} + +.conditionsHeader { + display: flex; + align-items: baseline; + gap: 10px; + margin-bottom: 12px; +} + +.conditionsTitle { + font-size: 13px; + font-weight: 600; + color: $gray-700; +} + +.conditionsHint { + font-size: 11.5px; + color: $gray-400; +} + +.conditionRow { + display: grid; + grid-template-columns: 1fr 1fr auto; + gap: 10px; + align-items: center; + margin-bottom: 10px; +} + .runButtonRow { margin-top: 18px; } diff --git a/frontend/src/webparts/approvalAdmin/components/screens/ResolveScreen.tsx b/frontend/src/webparts/approvalAdmin/components/screens/ResolveScreen.tsx index 7973d78..37c8e45 100644 --- a/frontend/src/webparts/approvalAdmin/components/screens/ResolveScreen.tsx +++ b/frontend/src/webparts/approvalAdmin/components/screens/ResolveScreen.tsx @@ -2,11 +2,13 @@ import * as React from 'react'; import styles from './ResolveScreen.module.scss'; import { ApprovalRouteApi } from '../../services/api'; import { PortalDictionaryClient } from '../../services/portalDictionary'; -import { Project, RouteStepStatus } from '../../models'; +import { isNumericFieldType, SharePointListsClient } from '../../services/sharePointLists'; +import { Project, ResolveFieldValue, RouteStepStatus } from '../../models'; import { routeStepStatusLabels, texts } from '../../texts'; import { useRequestTypes } from '../../hooks/useRequestTypes'; import { useResolveRoute } from '../../hooks/useResolveRoute'; import { usePortalEmployees } from '../../hooks/usePortalEmployees'; +import { useSharePointFields } from '../../hooks/useSharePointFields'; import { useLocalStorageState } from '../../hooks/useLocalStorageState'; import { findEmployeeByEmail, toEmployeeOption } from '../../utils/employee'; import { @@ -18,17 +20,20 @@ import { DataTable, Field, FormGrid, + IconButton, PageHeader, Panel, PrimaryCell, SearchableSelect, - Select + Select, + TextInput } from '../ui'; export interface IResolveScreenProps { api: ApprovalRouteApi; /** Undefined, пока SPFx выдаёт AAD-клиент для api.tnsu.kz. */ dictionary?: PortalDictionaryClient; + sharePoint: SharePointListsClient; currentUserEmail?: string; } @@ -46,6 +51,7 @@ interface IResolveFormState { projectId: string; projectLabel: string; status: RouteStepStatus; + fieldValues: ResolveFieldValue[]; } const EMPTY_RESOLVE_FORM: IResolveFormState = { @@ -53,13 +59,19 @@ const EMPTY_RESOLVE_FORM: IResolveFormState = { userEmail: '', projectId: '', projectLabel: '', - status: RouteStepStatus.Approval + status: RouteStepStatus.Approval, + fieldValues: [] }; -export const ResolveScreen: React.FC = ({ api, dictionary, currentUserEmail }) => { +export const ResolveScreen: React.FC = ({ api, dictionary, sharePoint, currentUserEmail }) => { const requestTypes = useRequestTypes(api); const [form, setForm] = useLocalStorageState(RESOLVE_FORM_STORAGE_KEY, EMPTY_RESOLVE_FORM); - const { requestTypeId, userEmail, projectId, projectLabel, status } = form; + const { requestTypeId, userEmail, projectId, projectLabel, status, fieldValues = [] } = form; + + const requestType = requestTypes.items.find(rt => rt.id === Number(requestTypeId)); + const hasListName = Boolean(requestType?.listName); + const sharePointFields = useSharePointFields(sharePoint, requestType?.siteUrl, requestType?.listName); + const fieldNameOptions = sharePointFields.items.map(f => ({ value: f.internalName, label: f.title })); const resolved = useResolveRoute(api); const employees = usePortalEmployees(dictionary); @@ -74,16 +86,35 @@ export const ResolveScreen: React.FC = ({ api, dictionary, [dictionary] ); + const updateFieldValue = (index: number, patch: Partial): void => { + setForm({ ...form, fieldValues: fieldValues.map((fv, i) => (i === index ? { ...fv, ...patch } : fv)) }); + }; + + const addFieldValue = (): void => { + setForm({ ...form, fieldValues: [...fieldValues, { fieldName: '', value: '' }] }); + }; + + const removeFieldValue = (index: number): void => { + setForm({ ...form, fieldValues: fieldValues.filter((_, i) => i !== index) }); + }; + const run = (): void => { if (!requestTypeId) { resolved.fail(texts.resolve.validationRequestType); return; } + const conditions = fieldValues.filter(fv => fv.fieldName.trim() || fv.value.trim()); + if (conditions.some(fv => !fv.fieldName.trim() || !fv.value.trim())) { + resolved.fail(texts.resolve.validationConditionField); + return; + } + resolved.resolve({ requestTypeId: Number(requestTypeId), userEmail: userEmail.trim() || undefined, projectId: projectId.trim() || undefined, - status + status, + fieldValues: conditions.length > 0 ? conditions : undefined }); }; @@ -102,7 +133,7 @@ export const ResolveScreen: React.FC = ({ api, dictionary, options={requestTypes.items.map(rt => ({ value: String(rt.id), label: rt.name }))} placeholder={texts.resolve.requestTypePlaceholder} value={requestTypeId} - onChange={e => setForm({ ...form, requestTypeId: e.target.value })} + onChange={e => setForm({ ...form, requestTypeId: e.target.value, fieldValues: [] })} /> = ({ api, dictionary, /> + +
+
+ {texts.resolve.conditionsLabel} + {requestTypeId && !hasListName && ( + {texts.resolve.conditionsDisabledHint} + )} +
+ + {hasListName && ( + <> + {fieldValues.map((fv, index) => { + const field = sharePointFields.items.find(f => f.internalName === fv.fieldName); + const isNumeric = field ? isNumericFieldType(field.typeAsString) : false; + return ( + // eslint-disable-next-line react/no-array-index-key -- строки не переупорядочиваются, только добавляются/удаляются с конца или явным индексом +
+