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:
@@ -480,7 +480,8 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
|
|||||||
sourceId: route.sourceId,
|
sourceId: route.sourceId,
|
||||||
positionId: route.positionId,
|
positionId: route.positionId,
|
||||||
nomenclatureId: route.nomenclatureId,
|
nomenclatureId: route.nomenclatureId,
|
||||||
storeName: route.storeName
|
storeName: route.storeName,
|
||||||
|
sourceSystem: route.sourceSystem
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,14 @@ export interface ISubmitFeatureProps {
|
|||||||
/** Открыть сразу форму, минуя плитки — с предзаполнением (производная заявка,
|
/** Открыть сразу форму, минуя плитки — с предзаполнением (производная заявка,
|
||||||
* sourceId задан) или без него (пустая новая форма конкретного типа).
|
* sourceId задан) или без него (пустая новая форма конкретного типа).
|
||||||
* positionId/nomenclatureId — параметры внешней ссылки (только форма ТМЦ). */
|
* 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 };
|
editForm?: { formKey: string; itemId: number };
|
||||||
/** Пользователь выбрал плитку — сообщаем наверх, чтобы адрес страницы стал
|
/** Пользователь выбрал плитку — сообщаем наверх, чтобы адрес страницы стал
|
||||||
@@ -145,6 +152,7 @@ const SubmitFeature: React.FC<ISubmitFeatureProps> = ({
|
|||||||
positionId={external?.positionId}
|
positionId={external?.positionId}
|
||||||
nomenclatureId={external?.nomenclatureId}
|
nomenclatureId={external?.nomenclatureId}
|
||||||
storeName={external?.storeName}
|
storeName={external?.storeName}
|
||||||
|
sourceSystem={external?.sourceSystem}
|
||||||
/>
|
/>
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -91,7 +91,8 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
|||||||
onCodeLoaded,
|
onCodeLoaded,
|
||||||
positionId,
|
positionId,
|
||||||
nomenclatureId,
|
nomenclatureId,
|
||||||
storeName
|
storeName,
|
||||||
|
sourceSystem
|
||||||
}) => {
|
}) => {
|
||||||
const [data, setData] = useState<IFormData>(EMPTY_DATA);
|
const [data, setData] = useState<IFormData>(EMPTY_DATA);
|
||||||
const [files, setFiles] = useState<File[]>([]);
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
@@ -144,18 +145,30 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
|||||||
};
|
};
|
||||||
}, [editId, context]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (editId || !nomenclatureId) {
|
if (editId || !nomenclatureId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Строку-источник и склад фиксируем сразу, независимо от ответа справочника:
|
|
||||||
// строка нужна при отправке для обратной записи ссылки в Dataverse, склад —
|
|
||||||
// только справочно для пользователя (см. SourceStoreName в model.ts).
|
|
||||||
// nomenclatureId отдельно в шапке не храним — он едет в каждой позиции ItemsJson.
|
|
||||||
setData((prev) => ({ ...prev, SourcePositionId: positionId || '', SourceStoreName: storeName || '' }));
|
|
||||||
let alive = true;
|
let alive = true;
|
||||||
getNomenclature(context, nomenclatureId, storeName)
|
getNomenclature(context, nomenclatureId, storeName)
|
||||||
.then((rows) => {
|
.then((rows) => {
|
||||||
@@ -175,7 +188,7 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
|||||||
return () => {
|
return () => {
|
||||||
alive = false;
|
alive = false;
|
||||||
};
|
};
|
||||||
}, [editId, nomenclatureId, positionId, storeName, context, setSubmitError]);
|
}, [editId, nomenclatureId, storeName, context, setSubmitError]);
|
||||||
|
|
||||||
// Проект-получатель («Куда») по той же внешней ссылке: строка-источник в
|
// Проект-получатель («Куда») по той же внешней ссылке: строка-источник в
|
||||||
// Dataverse хранит ID позиции ЛЗК, у которой есть родительская запись LZK
|
// Dataverse хранит ID позиции ЛЗК, у которой есть родительская запись LZK
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export async function loadTmcTransferRequest(itemId: number, listId?: string): P
|
|||||||
'ContactPhone',
|
'ContactPhone',
|
||||||
'SourcePositionId',
|
'SourcePositionId',
|
||||||
'SourceStoreName',
|
'SourceStoreName',
|
||||||
|
'SourceSystem',
|
||||||
'ItemsJson',
|
'ItemsJson',
|
||||||
'RequestStatus',
|
'RequestStatus',
|
||||||
'RequestUniqueCode'
|
'RequestUniqueCode'
|
||||||
@@ -80,6 +81,7 @@ export async function loadTmcTransferRequest(itemId: number, listId?: string): P
|
|||||||
ContactPhone: (item.ContactPhone as string) || '',
|
ContactPhone: (item.ContactPhone as string) || '',
|
||||||
SourcePositionId: (item.SourcePositionId as string) || '',
|
SourcePositionId: (item.SourcePositionId as string) || '',
|
||||||
SourceStoreName: (item.SourceStoreName as string) || '',
|
SourceStoreName: (item.SourceStoreName as string) || '',
|
||||||
|
SourceSystem: (item.SourceSystem as string) || '',
|
||||||
Items: parseItems(item.ItemsJson)
|
Items: parseItems(item.ItemsJson)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,12 @@ export interface IFormData {
|
|||||||
* именно списываются ТМЦ. '' — ссылка без фильтра по складу или заявка заведена вручную.
|
* именно списываются ТМЦ. '' — ссылка без фильтра по складу или заявка заведена вручную.
|
||||||
*/
|
*/
|
||||||
SourceStoreName: string;
|
SourceStoreName: string;
|
||||||
|
/**
|
||||||
|
* Система, из которой открыли форму по внешней ссылке — параметр `source` в URL
|
||||||
|
* (напр. 'kps'). Вместе с SourcePositionId однозначно указывает, откуда пришла
|
||||||
|
* заявка и куда отвечать. '' — заявка заведена вручную или ссылка без source.
|
||||||
|
*/
|
||||||
|
SourceSystem: string;
|
||||||
|
|
||||||
// Табличная часть — позиции ТМЦ (сериализуется в ItemsJson). У строк из
|
// Табличная часть — позиции ТМЦ (сериализуется в ItemsJson). У строк из
|
||||||
// справочника есть nomenclatureId (см. ILineItem), у дописанных вручную — нет.
|
// справочника есть nomenclatureId (см. ILineItem), у дописанных вручную — нет.
|
||||||
@@ -115,5 +121,6 @@ export const EMPTY_DATA: IFormData = {
|
|||||||
ContactPhone: '',
|
ContactPhone: '',
|
||||||
SourcePositionId: '',
|
SourcePositionId: '',
|
||||||
SourceStoreName: '',
|
SourceStoreName: '',
|
||||||
|
SourceSystem: '',
|
||||||
Items: []
|
Items: []
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
|
|
||||||
{ "internalName": "SourcePositionId", "title": "Id строки-источника (Dataverse)", "type": "Text" },
|
{ "internalName": "SourcePositionId", "title": "Id строки-источника (Dataverse)", "type": "Text" },
|
||||||
{ "internalName": "SourceStoreName", "title": "Склад-источник (внешняя ссылка)", "type": "Text" },
|
{ "internalName": "SourceStoreName", "title": "Склад-источник (внешняя ссылка)", "type": "Text" },
|
||||||
|
{ "internalName": "SourceSystem", "title": "Система-источник (внешняя ссылка)", "type": "Text" },
|
||||||
|
|
||||||
{ "internalName": "ItemsJson", "title": "Позиции ТМЦ (JSON)", "type": "Note" }
|
{ "internalName": "ItemsJson", "title": "Позиции ТМЦ (JSON)", "type": "Note" }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ export interface IRequestFormProps {
|
|||||||
nomenclatureId?: string;
|
nomenclatureId?: string;
|
||||||
/** Склад-фильтр для справочника номенклатуры (см. positionId). */
|
/** Склад-фильтр для справочника номенклатуры (см. positionId). */
|
||||||
storeName?: string;
|
storeName?: string;
|
||||||
|
/** Система-источник внешней ссылки (параметр `source`, напр. 'kps'; см. positionId). */
|
||||||
|
sourceSystem?: string;
|
||||||
/**
|
/**
|
||||||
* Id СВОЕЙ заявки, открытой на редактирование (черновик / возвращённая /
|
* Id СВОЕЙ заявки, открытой на редактирование (черновик / возвращённая /
|
||||||
* отклонённая). В отличие от sourceId это не предзаполнение новой заявки, а
|
* отклонённая). В отличие от 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 игнорируются — обычная подача', () => {
|
it('positionId/nomenclatureId без rptype игнорируются — обычная подача', () => {
|
||||||
// Внешние параметры имеют смысл только при открытии формы (rptype); сами по
|
// Внешние параметры имеют смысл только при открытии формы (rptype); сами по
|
||||||
// себе не должны влиять на маршрут.
|
// себе не должны влиять на маршрут.
|
||||||
|
|||||||
@@ -86,6 +86,13 @@ export interface IRoute {
|
|||||||
* этому складу. Приходит по той же внешней ссылке; пусто — все склады.
|
* этому складу. Приходит по той же внешней ссылке; пусто — все склады.
|
||||||
*/
|
*/
|
||||||
storeName?: string;
|
storeName?: string;
|
||||||
|
/**
|
||||||
|
* Система, из которой открыли форму по внешней ссылке (параметр `source`,
|
||||||
|
* напр. 'kps'). Сохраняется в заявке, чтобы знать, откуда она пришла и куда
|
||||||
|
* отвечать. Не путать с sourceId (rpsource) — тот про предзаполнение из
|
||||||
|
* другой заявки портала.
|
||||||
|
*/
|
||||||
|
sourceSystem?: string;
|
||||||
/** Подвкладка «Моих заявок»; задана только при nav='own'. */
|
/** Подвкладка «Моих заявок»; задана только при nav='own'. */
|
||||||
ownTab?: OwnTab;
|
ownTab?: OwnTab;
|
||||||
/** Фильтр по статусу заявки; задан только при nav='own' и ownTab='created'. */
|
/** Фильтр по статусу заявки; задан только при nav='own' и ownTab='created'. */
|
||||||
@@ -378,6 +385,7 @@ export function parseRoute(search: string = window.location.search): IRoute {
|
|||||||
const positionId = params.get('positionId') ?? undefined;
|
const positionId = params.get('positionId') ?? undefined;
|
||||||
const nomenclatureId = params.get('nomenclatureId') ?? undefined;
|
const nomenclatureId = params.get('nomenclatureId') ?? undefined;
|
||||||
const storeName = params.get('storeName') ?? undefined;
|
const storeName = params.get('storeName') ?? undefined;
|
||||||
|
const sourceSystem = params.get('source') ?? undefined;
|
||||||
|
|
||||||
// Подвкладка и фильтр — состояние раздела «Мои заявки», поэтому вне его не
|
// Подвкладка и фильтр — состояние раздела «Мои заявки», поэтому вне его не
|
||||||
// читаются вовсе (иначе чужой rptab из ссылки «протёк» бы в другой раздел).
|
// читаются вовсе (иначе чужой rptab из ссылки «протёк» бы в другой раздел).
|
||||||
@@ -432,7 +440,7 @@ export function parseRoute(search: string = window.location.search): IRoute {
|
|||||||
// rpmode=approve без валидного rpid) должна откатываться на обычную подачу,
|
// rpmode=approve без валидного rpid) должна откатываться на обычную подачу,
|
||||||
// а не тихо превращаться в открытие пустой формы другого режима.
|
// а не тихо превращаться в открытие пустой формы другого режима.
|
||||||
if (formKey && mode !== 'approve' && mode !== 'edit') {
|
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 };
|
return { mode: 'submit', nav, ownTab, ownStatus, allTypeId, page, query };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user