feat: доработал заявку перемещение тмц
Tests / frontend-test (push) Successful in 4m45s

This commit is contained in:
Тимур Городецкий
2026-09-10 11:19:16 +05:00
parent c5958c3348
commit 11eb127764
17 changed files with 429 additions and 15 deletions
@@ -36,7 +36,15 @@ import {
IAttachmentInfo
} from '../../shared/attachments/attachments.service';
import { effectiveUserEmail } from '../../shared/impersonation';
import { getEmployeeOrganizations, formatEmployeeOrganizations } from '../../shared/dictionary';
import {
getEmployeeOrganizations,
formatEmployeeOrganizations,
getNomenclature,
INomenclatureRow
} from '../../shared/dictionary';
import { ILineItem } from '../../shared/ui';
import { requestLinkUrl } from '../../shared/routing';
import { writeDemandLinePortalLink } from '../../shared/dataverse/demandLine.service';
import { friendlyErrorMessage } from '../../shared/diagnostics';
import { IRequestFormProps } from '../../models/types';
import {
@@ -52,6 +60,23 @@ import { HeaderSection } from './sections/HeaderSection';
import { ItemsSection } from './sections/ItemsSection';
import { useStableCallback } from '../../shared/useStableCallback';
/**
* Строка ответа номенклатуры → строка табличной части (name/код/ед.изм/кол-во).
* nomenclatureId (row.id) сохраняем в позиции: по нему потом отличают строки из
* справочника от дописанных пользователем вручную.
* NB: «Инв. номер» в ответе номенклатуры отсутствует — оставляем пустым.
*/
function nomenclatureRowToLineItem(row: INomenclatureRow): ILineItem {
return {
name: row.name || row.fullName || '',
invNumber: '',
code: row.code || '',
unit: row.unit || '',
qty: row.quantity !== undefined && row.quantity !== null ? String(row.quantity) : '',
nomenclatureId: row.id
};
}
const TmcTransferForm: React.FC<IRequestFormProps> = ({
requestType,
context,
@@ -59,7 +84,10 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
onSaved,
editId,
editListId,
onCodeLoaded
onCodeLoaded,
positionId,
nomenclatureId,
storeName
}) => {
const [data, setData] = useState<IFormData>(EMPTY_DATA);
const [files, setFiles] = useState<File[]>([]);
@@ -112,6 +140,38 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
};
}, [editId, context]);
// Открытие по внешней ссылке (positionId + nomenclatureId в URL): запоминаем
// строку-источник и предзаполняем таблицу остатками номенклатуры по складам.
// Только для новой заявки — при редактировании позиции уже сохранены.
useEffect(() => {
if (editId || !nomenclatureId) {
return;
}
// Строку-источник фиксируем сразу, независимо от ответа справочника: она
// нужна при отправке для обратной записи ссылки в Dataverse. nomenclatureId
// отдельно в шапке не храним — он едет в каждой позиции ItemsJson.
setData((prev) => ({ ...prev, SourcePositionId: positionId || '' }));
let alive = true;
getNomenclature(context, nomenclatureId, storeName)
.then((rows) => {
if (!alive) {
return;
}
const items = rows.map(nomenclatureRowToLineItem);
if (items.length > 0) {
setData((prev) => ({ ...prev, Items: items }));
}
})
.catch((e) => {
if (alive) {
setSubmitError(`Не удалось загрузить номенклатуру: ${friendlyErrorMessage(e)}`);
}
});
return () => {
alive = false;
};
}, [editId, nomenclatureId, positionId, storeName, context, setSubmitError]);
// Выбор другого заявителя подтягивает его ФИО (для печати) и организации (как
// в Vacation.handleEmployeeChange, но источник тот же Employee/ByEmail — здесь
// дополнительно берём ФИО, т.к. TMC хранит его отдельным полем AuthorName).
@@ -290,6 +350,19 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID),
approvalListName: writeListId ? approvalListTitleForListId(FORM_ID, writeListId) : approvalListTitle(FORM_ID)
});
// Обратная связь во внешнюю систему: пишем ссылку на эту заявку в
// строку-источник Dataverse (только если заявка открыта по внешней ссылке).
// Не критично для самой заявки — она уже сохранена и отправлена, поэтому
// сбой записи не роняем, а только логируем (см. флаг ниже для UX-решения).
if (data.SourcePositionId) {
const link = requestLinkUrl(FORM_ID, id, writeListId);
try {
await writeDemandLinePortalLink(context, data.SourcePositionId, link);
} catch (e) {
// eslint-disable-next-line no-console
console.warn('Не удалось записать ссылку в строку-источник (Dataverse):', e);
}
}
}
};
@@ -51,6 +51,7 @@ export async function loadTmcTransferRequest(itemId: number, listId?: string): P
'Reason',
'ContactPerson',
'ContactPhone',
'SourcePositionId',
'ItemsJson',
'RequestStatus',
'RequestUniqueCode'
@@ -76,6 +77,7 @@ export async function loadTmcTransferRequest(itemId: number, listId?: string): P
Reason: (item.Reason as string) || '',
ContactPerson: (item.ContactPerson as string) || '',
ContactPhone: (item.ContactPhone as string) || '',
SourcePositionId: (item.SourcePositionId as string) || '',
Items: parseItems(item.ItemsJson)
};
@@ -54,7 +54,16 @@ export interface IFormData {
ContactPerson: string; // Исполнитель («Исп. …»), Ф.И.О.
ContactPhone: string; // Телефон исполнителя
// Табличная часть — позиции ТМЦ (сериализуется в ItemsJson)
/**
* Строка-источник во внешней системе (Dataverse «Строка заявки», GUID). Одна
* на всю заявку (не на позицию) — приходит в URL как positionId. После отправки
* на согласование по нему пишем обратно ссылку на заявку. '' — заявка заведена
* вручную в портале, без внешней ссылки.
*/
SourcePositionId: string;
// Табличная часть — позиции ТМЦ (сериализуется в ItemsJson). У строк из
// справочника есть nomenclatureId (см. ILineItem), у дописанных вручную — нет.
Items: ILineItem[];
}
@@ -97,5 +106,6 @@ export const EMPTY_DATA: IFormData = {
Reason: '',
ContactPerson: '',
ContactPhone: '',
SourcePositionId: '',
Items: []
};
@@ -23,6 +23,8 @@
{ "internalName": "ContactPerson", "title": "Исполнитель (Ф.И.О.)", "type": "Text" },
{ "internalName": "ContactPhone", "title": "Телефон исполнителя", "type": "Text" },
{ "internalName": "SourcePositionId", "title": "Id строки-источника (Dataverse)", "type": "Text" },
{ "internalName": "ItemsJson", "title": "Позиции ТМЦ (JSON)", "type": "Note" }
]
}