feat: параметр source во внешней ссылке ТМЦ сохраняется в заявке

Внешняя система (напр. КПС) передаёт source — пишем его в новую колонку
SourceSystem рядом с SourcePositionId, чтобы по заявке было видно, откуда
она пришла и куда отвечать. Запоминание источника вынесено в отдельный
эффект: positionId/source/склад сохраняются и по ссылке без nomenclatureId.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Тимур Городецкий
2026-10-05 12:39:48 +05:00
parent 7f1e36874a
commit 9400f8546e
9 changed files with 59 additions and 13 deletions
@@ -480,7 +480,8 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
sourceId: route.sourceId,
positionId: route.positionId,
nomenclatureId: route.nomenclatureId,
storeName: route.storeName
storeName: route.storeName,
sourceSystem: route.sourceSystem
}
: undefined
}
@@ -20,7 +20,14 @@ export interface ISubmitFeatureProps {
/** Открыть сразу форму, минуя плитки — с предзаполнением (производная заявка,
* sourceId задан) или без него (пустая новая форма конкретного типа).
* positionId/nomenclatureId — параметры внешней ссылки (только форма ТМЦ). */
initialForm?: { formKey: string; sourceId?: number; positionId?: string; nomenclatureId?: string; storeName?: string };
initialForm?: {
formKey: string;
sourceId?: number;
positionId?: string;
nomenclatureId?: string;
storeName?: string;
sourceSystem?: string;
};
/** Открыть сразу форму на РЕДАКТИРОВАНИЕ существующей заявки (черновик/возвращена). */
editForm?: { formKey: string; itemId: number };
/** Пользователь выбрал плитку — сообщаем наверх, чтобы адрес страницы стал
@@ -145,6 +152,7 @@ const SubmitFeature: React.FC<ISubmitFeatureProps> = ({
positionId={external?.positionId}
nomenclatureId={external?.nomenclatureId}
storeName={external?.storeName}
sourceSystem={external?.sourceSystem}
/>
</React.Suspense>
</>
@@ -91,7 +91,8 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
onCodeLoaded,
positionId,
nomenclatureId,
storeName
storeName,
sourceSystem
}) => {
const [data, setData] = useState<IFormData>(EMPTY_DATA);
const [files, setFiles] = useState<File[]>([]);
@@ -144,18 +145,30 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
};
}, [editId, context]);
// Открытие по внешней ссылке (positionId + nomenclatureId в URL): запоминаем
// строку-источник и предзаполняем таблицу остатками номенклатуры по складам.
// Только для новой заявки — при редактировании позиции уже сохранены.
// Открытие по внешней ссылке: запоминаем, откуда пришла заявка — система
// (source), строка-источник (positionId) и склад. Строка нужна при отправке для
// обратной записи ссылки в Dataverse, система — чтобы знать, откуда заявка,
// склад — справочно (см. model.ts). Не зависит от nomenclatureId: ссылка может
// прийти и без товара. Только для новой заявки — у сохранённой всё уже в SP.
useEffect(() => {
if (editId || (!positionId && !storeName && !sourceSystem)) {
return;
}
setData((prev) => ({
...prev,
SourcePositionId: positionId || '',
SourceStoreName: storeName || '',
SourceSystem: sourceSystem || ''
}));
}, [editId, positionId, storeName, sourceSystem]);
// Предзаполнение таблицы остатками номенклатуры по складам (nomenclatureId из
// внешней ссылки). nomenclatureId отдельно в шапке не храним — он едет в каждой
// позиции ItemsJson. Только для новой заявки — при редактировании позиции уже сохранены.
useEffect(() => {
if (editId || !nomenclatureId) {
return;
}
// Строку-источник и склад фиксируем сразу, независимо от ответа справочника:
// строка нужна при отправке для обратной записи ссылки в Dataverse, склад —
// только справочно для пользователя (см. SourceStoreName в model.ts).
// nomenclatureId отдельно в шапке не храним — он едет в каждой позиции ItemsJson.
setData((prev) => ({ ...prev, SourcePositionId: positionId || '', SourceStoreName: storeName || '' }));
let alive = true;
getNomenclature(context, nomenclatureId, storeName)
.then((rows) => {
@@ -175,7 +188,7 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
return () => {
alive = false;
};
}, [editId, nomenclatureId, positionId, storeName, context, setSubmitError]);
}, [editId, nomenclatureId, storeName, context, setSubmitError]);
// Проект-получатель («Куда») по той же внешней ссылке: строка-источник в
// Dataverse хранит ID позиции ЛЗК, у которой есть родительская запись LZK
@@ -53,6 +53,7 @@ export async function loadTmcTransferRequest(itemId: number, listId?: string): P
'ContactPhone',
'SourcePositionId',
'SourceStoreName',
'SourceSystem',
'ItemsJson',
'RequestStatus',
'RequestUniqueCode'
@@ -80,6 +81,7 @@ export async function loadTmcTransferRequest(itemId: number, listId?: string): P
ContactPhone: (item.ContactPhone as string) || '',
SourcePositionId: (item.SourcePositionId as string) || '',
SourceStoreName: (item.SourceStoreName as string) || '',
SourceSystem: (item.SourceSystem as string) || '',
Items: parseItems(item.ItemsJson)
};
@@ -68,6 +68,12 @@ export interface IFormData {
* именно списываются ТМЦ. '' — ссылка без фильтра по складу или заявка заведена вручную.
*/
SourceStoreName: string;
/**
* Система, из которой открыли форму по внешней ссылке — параметр `source` в URL
* (напр. 'kps'). Вместе с SourcePositionId однозначно указывает, откуда пришла
* заявка и куда отвечать. '' — заявка заведена вручную или ссылка без source.
*/
SourceSystem: string;
// Табличная часть — позиции ТМЦ (сериализуется в ItemsJson). У строк из
// справочника есть nomenclatureId (см. ILineItem), у дописанных вручную — нет.
@@ -115,5 +121,6 @@ export const EMPTY_DATA: IFormData = {
ContactPhone: '',
SourcePositionId: '',
SourceStoreName: '',
SourceSystem: '',
Items: []
};
@@ -25,6 +25,7 @@
{ "internalName": "SourcePositionId", "title": "Id строки-источника (Dataverse)", "type": "Text" },
{ "internalName": "SourceStoreName", "title": "Склад-источник (внешняя ссылка)", "type": "Text" },
{ "internalName": "SourceSystem", "title": "Система-источник (внешняя ссылка)", "type": "Text" },
{ "internalName": "ItemsJson", "title": "Позиции ТМЦ (JSON)", "type": "Note" }
]
@@ -49,6 +49,8 @@ export interface IRequestFormProps {
nomenclatureId?: string;
/** Склад-фильтр для справочника номенклатуры (см. positionId). */
storeName?: string;
/** Система-источник внешней ссылки (параметр `source`, напр. 'kps'; см. positionId). */
sourceSystem?: string;
/**
* Id СВОЕЙ заявки, открытой на редактирование (черновик / возвращённая /
* отклонённая). В отличие от sourceId это не предзаполнение новой заявки, а
@@ -129,6 +129,10 @@ describe('parseRoute', () => {
});
});
it('внешняя ссылка ТМЦ: source прокидывается в create как sourceSystem', () => {
expect(parseRoute('?rptype=wh-tmc-transfer&positionId=abc&source=kps').sourceSystem).toBe('kps');
});
it('positionId/nomenclatureId без rptype игнорируются — обычная подача', () => {
// Внешние параметры имеют смысл только при открытии формы (rptype); сами по
// себе не должны влиять на маршрут.
+9 -1
View File
@@ -86,6 +86,13 @@ export interface IRoute {
* этому складу. Приходит по той же внешней ссылке; пусто — все склады.
*/
storeName?: string;
/**
* Система, из которой открыли форму по внешней ссылке (параметр `source`,
* напр. 'kps'). Сохраняется в заявке, чтобы знать, откуда она пришла и куда
* отвечать. Не путать с sourceId (rpsource) — тот про предзаполнение из
* другой заявки портала.
*/
sourceSystem?: string;
/** Подвкладка «Моих заявок»; задана только при nav='own'. */
ownTab?: OwnTab;
/** Фильтр по статусу заявки; задан только при nav='own' и ownTab='created'. */
@@ -378,6 +385,7 @@ export function parseRoute(search: string = window.location.search): IRoute {
const positionId = params.get('positionId') ?? undefined;
const nomenclatureId = params.get('nomenclatureId') ?? undefined;
const storeName = params.get('storeName') ?? undefined;
const sourceSystem = params.get('source') ?? undefined;
// Подвкладка и фильтр — состояние раздела «Мои заявки», поэтому вне его не
// читаются вовсе (иначе чужой rptab из ссылки «протёк» бы в другой раздел).
@@ -432,7 +440,7 @@ export function parseRoute(search: string = window.location.search): IRoute {
// rpmode=approve без валидного rpid) должна откатываться на обычную подачу,
// а не тихо превращаться в открытие пустой формы другого режима.
if (formKey && mode !== 'approve' && mode !== 'edit') {
return { mode: 'create', nav: 'new', formKey, positionId, nomenclatureId, storeName };
return { mode: 'create', nav: 'new', formKey, positionId, nomenclatureId, storeName, sourceSystem };
}
return { mode: 'submit', nav, ownTab, ownStatus, allTypeId, page, query };
}