This commit is contained in:
@@ -33,6 +33,10 @@
|
|||||||
{
|
{
|
||||||
"resource": "Microsoft Flow Service",
|
"resource": "Microsoft Flow Service",
|
||||||
"scope": "Flows.Read.All"
|
"scope": "Flows.Read.All"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resource": "https://org95ca17d6.crm4.dynamics.com",
|
||||||
|
"scope": "user_impersonation"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"features": [
|
"features": [
|
||||||
|
|||||||
@@ -367,17 +367,24 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
|
|||||||
</div>
|
</div>
|
||||||
<SubmitFeature
|
<SubmitFeature
|
||||||
key={
|
key={
|
||||||
route.mode === 'create' && route.formKey && route.sourceId
|
route.mode === 'create' && route.formKey
|
||||||
? `create-${route.formKey}-${route.sourceId}`
|
? `create-${route.formKey}-${route.sourceId ?? route.positionId ?? 'blank'}`
|
||||||
: 'blank'
|
: 'blank'
|
||||||
}
|
}
|
||||||
context={context}
|
context={context}
|
||||||
activeCategoryId={activeCategoryId}
|
activeCategoryId={activeCategoryId}
|
||||||
initialForm={
|
initialForm={
|
||||||
route.mode === 'create' && route.formKey && route.sourceId
|
route.mode === 'create' && route.formKey
|
||||||
? { formKey: route.formKey, sourceId: route.sourceId }
|
? {
|
||||||
|
formKey: route.formKey,
|
||||||
|
sourceId: route.sourceId,
|
||||||
|
positionId: route.positionId,
|
||||||
|
nomenclatureId: route.nomenclatureId,
|
||||||
|
storeName: route.storeName
|
||||||
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
onFormSelected={(formKey) => navigate({ mode: 'create', nav: 'new', formKey })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,15 @@ export interface ISubmitFeatureProps {
|
|||||||
/** Активная категория. Вкладки теперь рендерятся на уровне портала (RequestPortal),
|
/** Активная категория. Вкладки теперь рендерятся на уровне портала (RequestPortal),
|
||||||
* чтобы оставаться на экране и в режиме согласования — эта фича их только слушает. */
|
* чтобы оставаться на экране и в режиме согласования — эта фича их только слушает. */
|
||||||
activeCategoryId: string;
|
activeCategoryId: string;
|
||||||
/** Открыть сразу форму с предзаполнением (производная заявка), минуя плитки. */
|
/** Открыть сразу форму, минуя плитки — с предзаполнением (производная заявка,
|
||||||
initialForm?: { formKey: string; sourceId: number };
|
* sourceId задан) или без него (пустая новая форма конкретного типа).
|
||||||
|
* positionId/nomenclatureId — параметры внешней ссылки (только форма ТМЦ). */
|
||||||
|
initialForm?: { formKey: string; sourceId?: number; positionId?: string; nomenclatureId?: string; storeName?: string };
|
||||||
/** Открыть сразу форму на РЕДАКТИРОВАНИЕ существующей заявки (черновик/возвращена). */
|
/** Открыть сразу форму на РЕДАКТИРОВАНИЕ существующей заявки (черновик/возвращена). */
|
||||||
editForm?: { formKey: string; itemId: number };
|
editForm?: { formKey: string; itemId: number };
|
||||||
|
/** Пользователь выбрал плитку — сообщаем наверх, чтобы адрес страницы стал
|
||||||
|
* ссылкой на эту пустую форму (см. newRequestLinkUrl в shared/routing). */
|
||||||
|
onFormSelected?: (formKey: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Фича «Подать заявку»: плитки → беспоук-форма (категории/вкладки — в RequestPortal).
|
// Фича «Подать заявку»: плитки → беспоук-форма (категории/вкладки — в RequestPortal).
|
||||||
@@ -28,6 +33,7 @@ const SubmitFeature: React.FC<ISubmitFeatureProps> = ({
|
|||||||
activeCategoryId,
|
activeCategoryId,
|
||||||
initialForm,
|
initialForm,
|
||||||
editForm,
|
editForm,
|
||||||
|
onFormSelected,
|
||||||
}) => {
|
}) => {
|
||||||
// «Создать на основе» и «редактировать» оба сразу открывают нужную форму, минуя плитки.
|
// «Создать на основе» и «редактировать» оба сразу открывают нужную форму, минуя плитки.
|
||||||
const preOpen = initialForm ?? editForm;
|
const preOpen = initialForm ?? editForm;
|
||||||
@@ -84,6 +90,9 @@ const SubmitFeature: React.FC<ISubmitFeatureProps> = ({
|
|||||||
const sourceId =
|
const sourceId =
|
||||||
initialForm && request.id === initialForm.formKey ? initialForm.sourceId : undefined;
|
initialForm && request.id === initialForm.formKey ? initialForm.sourceId : undefined;
|
||||||
const editId = editForm && request.id === editForm.formKey ? editForm.itemId : undefined;
|
const editId = editForm && request.id === editForm.formKey ? editForm.itemId : undefined;
|
||||||
|
// Параметры внешней ссылки — только когда открыта именно та форма, для
|
||||||
|
// которой они пришли (formKey совпал).
|
||||||
|
const external = initialForm && request.id === initialForm.formKey ? initialForm : undefined;
|
||||||
|
|
||||||
// Ленивый рендер формы: её код подтягивается только сейчас, при открытии.
|
// Ленивый рендер формы: её код подтягивается только сейчас, при открытии.
|
||||||
return (
|
return (
|
||||||
@@ -95,17 +104,30 @@ const SubmitFeature: React.FC<ISubmitFeatureProps> = ({
|
|||||||
onSaved={emitRequestsChanged}
|
onSaved={emitRequestsChanged}
|
||||||
sourceId={sourceId}
|
sourceId={sourceId}
|
||||||
editId={editId}
|
editId={editId}
|
||||||
|
positionId={external?.positionId}
|
||||||
|
nomenclatureId={external?.nomenclatureId}
|
||||||
|
storeName={external?.storeName}
|
||||||
/>
|
/>
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Выбор плитки — переключаем и локальный стейт (форма рисуется сразу же), и
|
||||||
|
// сообщаем наверх formKey, чтобы адрес страницы стал ссылкой на эту форму
|
||||||
|
// (см. onFormSelected). RequestPortal в ответ вызовет navigate(), отчего
|
||||||
|
// SubmitFeature пересоздастся (другой key) — initialForm подхватит тот же
|
||||||
|
// formKey, так что видимый результат не изменится, просто через новый инстанс.
|
||||||
|
const handleSelect = (request: IRequestType): void => {
|
||||||
|
setSelectedRequest(request);
|
||||||
|
onFormSelected?.(request.id);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
{selectedRequest ? (
|
{selectedRequest ? (
|
||||||
renderSelected(selectedRequest)
|
renderSelected(selectedRequest)
|
||||||
) : (
|
) : (
|
||||||
<RequestGrid requests={requests} onSelect={setSelectedRequest} />
|
<RequestGrid requests={requests} onSelect={handleSelect} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -36,7 +36,15 @@ import {
|
|||||||
IAttachmentInfo
|
IAttachmentInfo
|
||||||
} from '../../shared/attachments/attachments.service';
|
} from '../../shared/attachments/attachments.service';
|
||||||
import { effectiveUserEmail } from '../../shared/impersonation';
|
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 { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||||
import { IRequestFormProps } from '../../models/types';
|
import { IRequestFormProps } from '../../models/types';
|
||||||
import {
|
import {
|
||||||
@@ -52,6 +60,23 @@ import { HeaderSection } from './sections/HeaderSection';
|
|||||||
import { ItemsSection } from './sections/ItemsSection';
|
import { ItemsSection } from './sections/ItemsSection';
|
||||||
import { useStableCallback } from '../../shared/useStableCallback';
|
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> = ({
|
const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
||||||
requestType,
|
requestType,
|
||||||
context,
|
context,
|
||||||
@@ -59,7 +84,10 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
|||||||
onSaved,
|
onSaved,
|
||||||
editId,
|
editId,
|
||||||
editListId,
|
editListId,
|
||||||
onCodeLoaded
|
onCodeLoaded,
|
||||||
|
positionId,
|
||||||
|
nomenclatureId,
|
||||||
|
storeName
|
||||||
}) => {
|
}) => {
|
||||||
const [data, setData] = useState<IFormData>(EMPTY_DATA);
|
const [data, setData] = useState<IFormData>(EMPTY_DATA);
|
||||||
const [files, setFiles] = useState<File[]>([]);
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
@@ -112,6 +140,38 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
|||||||
};
|
};
|
||||||
}, [editId, context]);
|
}, [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 — здесь
|
// в Vacation.handleEmployeeChange, но источник тот же Employee/ByEmail — здесь
|
||||||
// дополнительно берём ФИО, т.к. TMC хранит его отдельным полем AuthorName).
|
// дополнительно берём ФИО, т.к. TMC хранит его отдельным полем AuthorName).
|
||||||
@@ -290,6 +350,19 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
|||||||
dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID),
|
dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID),
|
||||||
approvalListName: writeListId ? approvalListTitleForListId(FORM_ID, writeListId) : approvalListTitle(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',
|
'Reason',
|
||||||
'ContactPerson',
|
'ContactPerson',
|
||||||
'ContactPhone',
|
'ContactPhone',
|
||||||
|
'SourcePositionId',
|
||||||
'ItemsJson',
|
'ItemsJson',
|
||||||
'RequestStatus',
|
'RequestStatus',
|
||||||
'RequestUniqueCode'
|
'RequestUniqueCode'
|
||||||
@@ -76,6 +77,7 @@ export async function loadTmcTransferRequest(itemId: number, listId?: string): P
|
|||||||
Reason: (item.Reason as string) || '',
|
Reason: (item.Reason as string) || '',
|
||||||
ContactPerson: (item.ContactPerson as string) || '',
|
ContactPerson: (item.ContactPerson as string) || '',
|
||||||
ContactPhone: (item.ContactPhone as string) || '',
|
ContactPhone: (item.ContactPhone as string) || '',
|
||||||
|
SourcePositionId: (item.SourcePositionId as string) || '',
|
||||||
Items: parseItems(item.ItemsJson)
|
Items: parseItems(item.ItemsJson)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,16 @@ export interface IFormData {
|
|||||||
ContactPerson: string; // Исполнитель («Исп. …»), Ф.И.О.
|
ContactPerson: string; // Исполнитель («Исп. …»), Ф.И.О.
|
||||||
ContactPhone: string; // Телефон исполнителя
|
ContactPhone: string; // Телефон исполнителя
|
||||||
|
|
||||||
// Табличная часть — позиции ТМЦ (сериализуется в ItemsJson)
|
/**
|
||||||
|
* Строка-источник во внешней системе (Dataverse «Строка заявки», GUID). Одна
|
||||||
|
* на всю заявку (не на позицию) — приходит в URL как positionId. После отправки
|
||||||
|
* на согласование по нему пишем обратно ссылку на заявку. '' — заявка заведена
|
||||||
|
* вручную в портале, без внешней ссылки.
|
||||||
|
*/
|
||||||
|
SourcePositionId: string;
|
||||||
|
|
||||||
|
// Табличная часть — позиции ТМЦ (сериализуется в ItemsJson). У строк из
|
||||||
|
// справочника есть nomenclatureId (см. ILineItem), у дописанных вручную — нет.
|
||||||
Items: ILineItem[];
|
Items: ILineItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,5 +106,6 @@ export const EMPTY_DATA: IFormData = {
|
|||||||
Reason: '',
|
Reason: '',
|
||||||
ContactPerson: '',
|
ContactPerson: '',
|
||||||
ContactPhone: '',
|
ContactPhone: '',
|
||||||
|
SourcePositionId: '',
|
||||||
Items: []
|
Items: []
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,6 +23,8 @@
|
|||||||
{ "internalName": "ContactPerson", "title": "Исполнитель (Ф.И.О.)", "type": "Text" },
|
{ "internalName": "ContactPerson", "title": "Исполнитель (Ф.И.О.)", "type": "Text" },
|
||||||
{ "internalName": "ContactPhone", "title": "Телефон исполнителя", "type": "Text" },
|
{ "internalName": "ContactPhone", "title": "Телефон исполнителя", "type": "Text" },
|
||||||
|
|
||||||
|
{ "internalName": "SourcePositionId", "title": "Id строки-источника (Dataverse)", "type": "Text" },
|
||||||
|
|
||||||
{ "internalName": "ItemsJson", "title": "Позиции ТМЦ (JSON)", "type": "Note" }
|
{ "internalName": "ItemsJson", "title": "Позиции ТМЦ (JSON)", "type": "Note" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,16 @@ export interface IRequestFormProps {
|
|||||||
* Формы, не поддерживающие производные заявки, игнорируют.
|
* Формы, не поддерживающие производные заявки, игнорируют.
|
||||||
*/
|
*/
|
||||||
sourceId?: number;
|
sourceId?: number;
|
||||||
|
/**
|
||||||
|
* Параметры внешней ссылки (Dataverse «Строка заявки» + справочник
|
||||||
|
* номенклатуры). Использует только форма «СЗ на перемещение ТМЦ»: по
|
||||||
|
* nomenclatureId предзаполняет таблицу остатками, positionId запоминает как
|
||||||
|
* строку-источник для обратной записи ссылки. Остальные формы игнорируют.
|
||||||
|
*/
|
||||||
|
positionId?: string;
|
||||||
|
nomenclatureId?: string;
|
||||||
|
/** Склад-фильтр для справочника номенклатуры (см. positionId). */
|
||||||
|
storeName?: string;
|
||||||
/**
|
/**
|
||||||
* Id СВОЕЙ заявки, открытой на редактирование (черновик / возвращённая /
|
* Id СВОЕЙ заявки, открытой на редактирование (черновик / возвращённая /
|
||||||
* отклонённая). В отличие от sourceId это не предзаполнение новой заявки, а
|
* отклонённая). В отличие от sourceId это не предзаполнение новой заявки, а
|
||||||
|
|||||||
@@ -58,3 +58,31 @@ export const DICTIONARY_API_BASE_URL = 'https://api.tnsu.kz';
|
|||||||
export const FLOW_RESOURCE = 'https://service.flow.microsoft.com/';
|
export const FLOW_RESOURCE = 'https://service.flow.microsoft.com/';
|
||||||
export const FLOW_NOTIFY_URL =
|
export const FLOW_NOTIFY_URL =
|
||||||
'https://eab3d2815f2beb42bf2e7ec1731708.92.environment.api.powerplatform.com:443/powerautomate/automations/direct/cu/07/workflows/b21516edf8a14287a65a39e4116dfe4f/triggers/manual/paths/invoke?api-version=1';
|
'https://eab3d2815f2beb42bf2e7ec1731708.92.environment.api.powerplatform.com:443/powerautomate/automations/direct/cu/07/workflows/b21516edf8a14287a65a39e4116dfe4f/triggers/manual/paths/invoke?api-version=1';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Microsoft Dataverse (Power Platform) — рабочая среда «По умолчанию»
|
||||||
|
* (org95ca17d6.crm4.dynamics.com). NB: таблица cree9_demandline существует и в
|
||||||
|
* другой среде (tansu.crm5, из проекта stroy-bot) — писать нужно ИМЕННО в эту.
|
||||||
|
* Авторизация через AadHttpClient (SSO текущего пользователя): ресурс = URL
|
||||||
|
* среды, разрешение объявлено в package-solution.json (webApiPermissionRequests,
|
||||||
|
* scope user_impersonation) и одобряется в Центре администрирования SharePoint
|
||||||
|
* (API access) при развёртывании — тот же механизм, что у approval-route API.
|
||||||
|
*
|
||||||
|
* Используется формой «СЗ на перемещение ТМЦ»: после отправки на согласование
|
||||||
|
* в строку-источник (таблица cree9_demandline) записывается ссылка на заявку
|
||||||
|
* портала — см. shared/dataverse/demandLine.service.ts.
|
||||||
|
*/
|
||||||
|
export const DATAVERSE_RESOURCE_URL = 'https://org95ca17d6.crm4.dynamics.com';
|
||||||
|
export const DATAVERSE_API_VERSION = 'v9.2';
|
||||||
|
/**
|
||||||
|
* EntitySetName таблицы «Строка заявки» (LogicalName cree9_demandline).
|
||||||
|
* NB: значение выведено по конвенции Dataverse (logical + 's') — проверить в
|
||||||
|
* проде через EntityDefinitions(LogicalName='cree9_demandline')?$select=EntitySetName.
|
||||||
|
*/
|
||||||
|
export const DATAVERSE_DEMANDLINE_ENTITY_SET = 'cree9_demandlines';
|
||||||
|
/**
|
||||||
|
* Колонка-URL, куда пишем ссылку на заявку портала (создана вручную в Dataverse).
|
||||||
|
* NB: префикс cr498_ (издатель среды org95ca17d6.crm4 по умолчанию), а НЕ cree9_
|
||||||
|
* как у остальных колонок таблицы — так сгенерировалось при создании колонки.
|
||||||
|
*/
|
||||||
|
export const DATAVERSE_DEMANDLINE_LINK_FIELD = 'cr498_portalrequesturl';
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { AadHttpClient, HttpClientResponse } from '@microsoft/sp-http';
|
||||||
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
import {
|
||||||
|
DATAVERSE_API_VERSION,
|
||||||
|
DATAVERSE_DEMANDLINE_ENTITY_SET,
|
||||||
|
DATAVERSE_DEMANDLINE_LINK_FIELD,
|
||||||
|
DATAVERSE_RESOURCE_URL
|
||||||
|
} from '../config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обратная связь во внешнюю систему: записать ссылку на заявку портала в
|
||||||
|
* строку-источник таблицы Dataverse «Строка заявки» (cree9_demandline).
|
||||||
|
*
|
||||||
|
* positionId — GUID первичного ключа строки (приходит в URL как positionId, см.
|
||||||
|
* shared/routing). Адресуем строку напрямую: <entitySet>(<guid>). PATCH пишет
|
||||||
|
* URL в колонку cree9_portalrequesturl.
|
||||||
|
*
|
||||||
|
* Заголовок If-Match: * ОБЯЗАТЕЛЕН: без него PATCH в Dataverse — это upsert (нет
|
||||||
|
* строки с таким id → СОЗДАЁТ новую). Нам нужно строго обновление существующей
|
||||||
|
* строки-источника; If-Match: * это гарантирует — если строки нет, вернётся 404,
|
||||||
|
* а не появится «пустая» запись.
|
||||||
|
*
|
||||||
|
* Авторизация — AadHttpClient с ресурсом среды Dataverse (SSO текущего
|
||||||
|
* пользователя); заголовки OData — как в stroy-bot-report/DataverseService.
|
||||||
|
*/
|
||||||
|
export async function writeDemandLinePortalLink(
|
||||||
|
context: WebPartContext,
|
||||||
|
positionId: string,
|
||||||
|
portalRequestUrl: string
|
||||||
|
): Promise<void> {
|
||||||
|
const client = await context.aadHttpClientFactory.getClient(DATAVERSE_RESOURCE_URL);
|
||||||
|
const url =
|
||||||
|
`${DATAVERSE_RESOURCE_URL}/api/data/${DATAVERSE_API_VERSION}/` +
|
||||||
|
`${DATAVERSE_DEMANDLINE_ENTITY_SET}(${positionId})`;
|
||||||
|
|
||||||
|
const response: HttpClientResponse = await client.fetch(url, AadHttpClient.configurations.v1, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json;odata.metadata=minimal',
|
||||||
|
'OData-MaxVersion': '4.0',
|
||||||
|
'OData-Version': '4.0',
|
||||||
|
// Только обновление существующей строки, без upsert-создания.
|
||||||
|
'If-Match': '*'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ [DATAVERSE_DEMANDLINE_LINK_FIELD]: portalRequestUrl })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
let errBody = '';
|
||||||
|
try {
|
||||||
|
errBody = await response.text();
|
||||||
|
} catch {
|
||||||
|
// тело ошибки прочитать не удалось — оставляем пустым
|
||||||
|
}
|
||||||
|
throw new Error(`Dataverse PATCH ${DATAVERSE_DEMANDLINE_ENTITY_SET}: ${response.status} — ${errBody}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ export { CompanyPickerMulti } from './ui/CompanyPickerMulti';
|
|||||||
export type { ICompanyPickerMultiProps } from './ui/CompanyPickerMulti';
|
export type { ICompanyPickerMultiProps } from './ui/CompanyPickerMulti';
|
||||||
export { getEmployeeOrganizations, formatEmployeeOrganizations } from './employeeOrganizations.service';
|
export { getEmployeeOrganizations, formatEmployeeOrganizations } from './employeeOrganizations.service';
|
||||||
export type { IEmployeeOrganization } from './models/IEmployeeOrganization';
|
export type { IEmployeeOrganization } from './models/IEmployeeOrganization';
|
||||||
|
export { getNomenclature } from './nomenclature.service';
|
||||||
|
export type { INomenclatureRow } from './models/INomenclatureRow';
|
||||||
export {
|
export {
|
||||||
directorPositionFor,
|
directorPositionFor,
|
||||||
getEmployeeEmailByPosition,
|
getEmployeeEmailByPosition,
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Строка ответа GET /Dictionary/Nomenclature/{id}. Эндпоинт возвращает МАССИВ:
|
||||||
|
* одну и ту же номенклатуру (общий id) в разбивке ПО СКЛАДАМ — у каждой строки
|
||||||
|
* свой storeName и свой остаток quantity. Заполняет табличную часть формы
|
||||||
|
* «СЗ на перемещение ТМЦ» (см. TmcTransferForm prefill).
|
||||||
|
*/
|
||||||
|
export interface INomenclatureRow {
|
||||||
|
/** Id номенклатуры (совпадает у всех строк ответа — это nomenclatureId из URL). */
|
||||||
|
id: string;
|
||||||
|
/** Наименование. */
|
||||||
|
name: string;
|
||||||
|
/** Полное наименование. */
|
||||||
|
fullName: string;
|
||||||
|
/** Группа номенклатуры. */
|
||||||
|
groupName: string;
|
||||||
|
/** Единица измерения (напр. «шт»). */
|
||||||
|
unit: string;
|
||||||
|
/** Остаток на складе (число). */
|
||||||
|
quantity: number;
|
||||||
|
/** Номенклатурный код (напр. «БК000003794»). */
|
||||||
|
code: string;
|
||||||
|
/** Код АГСК (напр. «247-201-1002»). */
|
||||||
|
agskCode: string;
|
||||||
|
/** Тип номенклатуры (напр. «Сырьё и материалы»). */
|
||||||
|
nomenclatureType: string;
|
||||||
|
/** Склад, на котором лежит остаток. */
|
||||||
|
storeName: string;
|
||||||
|
/** МОЛ (материально ответственное лицо). */
|
||||||
|
mol: string;
|
||||||
|
/** Комментарий. */
|
||||||
|
comment: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { AadHttpClient, HttpClientResponse } from '@microsoft/sp-http';
|
||||||
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
import { APPROVAL_API_CLIENT_ID, DICTIONARY_API_BASE_URL } from '../config';
|
||||||
|
import { INomenclatureRow } from './models/INomenclatureRow';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /Dictionary/Nomenclature/{id} — остатки номенклатуры по складам. Ответ —
|
||||||
|
* плоский массив (как /Dictionary/Departments): одна номенклатура в разбивке по
|
||||||
|
* складам. Авторизация — тот же AAD-app, что и у остальных справочников.
|
||||||
|
*
|
||||||
|
* storeName (необязательный) — фильтр по складу: API вернёт остатки только по
|
||||||
|
* нему. Приходит по внешней ссылке (см. shared/routing). Пусто — все склады.
|
||||||
|
*
|
||||||
|
* Кэш не нужен: вызывается один раз при открытии формы по внешней ссылке и
|
||||||
|
* зависит от конкретного id (не общий справочник на всю сессию).
|
||||||
|
*/
|
||||||
|
export async function getNomenclature(
|
||||||
|
context: WebPartContext,
|
||||||
|
id: string,
|
||||||
|
storeName?: string
|
||||||
|
): Promise<INomenclatureRow[]> {
|
||||||
|
const client = await context.aadHttpClientFactory.getClient(APPROVAL_API_CLIENT_ID);
|
||||||
|
const query = storeName ? `?storeName=${encodeURIComponent(storeName)}` : '';
|
||||||
|
const response: HttpClientResponse = await client.get(
|
||||||
|
`${DICTIONARY_API_BASE_URL}/Dictionary/Nomenclature/${encodeURIComponent(id)}${query}`,
|
||||||
|
AadHttpClient.configurations.v1
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Dictionary/Nomenclature/${id}: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
IRoute,
|
IRoute,
|
||||||
navigate,
|
navigate,
|
||||||
navigateToRequestLink,
|
navigateToRequestLink,
|
||||||
|
newRequestLinkUrl,
|
||||||
parseRoute,
|
parseRoute,
|
||||||
persistOwnStatus,
|
persistOwnStatus,
|
||||||
persistOwnTab,
|
persistOwnTab,
|
||||||
@@ -96,6 +97,65 @@ describe('parseRoute', () => {
|
|||||||
it('открытая заявка имеет приоритет над предзаполнением', () => {
|
it('открытая заявка имеет приоритет над предзаполнением', () => {
|
||||||
expect(parseRoute('?rpmode=approve&rptype=hire&rpid=42&rpsource=15').mode).toBe('approve');
|
expect(parseRoute('?rpmode=approve&rptype=hire&rpid=42&rpsource=15').mode).toBe('approve');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('rpmode=create + rptype без rpsource — пустая новая форма без предзаполнения', () => {
|
||||||
|
expect(parseRoute('?rpmode=create&rptype=hire')).toEqual({
|
||||||
|
mode: 'create',
|
||||||
|
nav: 'new',
|
||||||
|
formKey: 'hire',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rptype БЕЗ rpmode — тоже пустая новая форма (buildQuery для create его не пишет)', () => {
|
||||||
|
expect(parseRoute('?rptype=hire')).toEqual({
|
||||||
|
mode: 'create',
|
||||||
|
nav: 'new',
|
||||||
|
formKey: 'hire',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rpmode=create без rptype не открывает пустую форму — остаётся обычная подача', () => {
|
||||||
|
expect(parseRoute('?rpmode=create').mode).toBe('submit');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('внешняя ссылка ТМЦ: rptype + positionId + nomenclatureId + storeName прокидываются в create', () => {
|
||||||
|
expect(parseRoute('?rptype=wh-tmc-transfer&positionId=00d31059-1f52-11ec-a738-3cecef292546&nomenclatureId=abc-123&storeName=Склад%20№1')).toEqual({
|
||||||
|
mode: 'create',
|
||||||
|
nav: 'new',
|
||||||
|
formKey: 'wh-tmc-transfer',
|
||||||
|
positionId: '00d31059-1f52-11ec-a738-3cecef292546',
|
||||||
|
nomenclatureId: 'abc-123',
|
||||||
|
storeName: 'Склад №1',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('positionId/nomenclatureId без rptype игнорируются — обычная подача', () => {
|
||||||
|
// Внешние параметры имеют смысл только при открытии формы (rptype); сами по
|
||||||
|
// себе не должны влиять на маршрут.
|
||||||
|
expect(parseRoute('?positionId=x&nomenclatureId=y').mode).toBe('submit');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rpmode=create + rptype + rpsource — предзаполнение важнее пустой формы', () => {
|
||||||
|
expect(parseRoute('?rpmode=create&rptype=hire&rpsource=15')).toEqual({
|
||||||
|
mode: 'create',
|
||||||
|
nav: 'new',
|
||||||
|
formKey: 'hire',
|
||||||
|
sourceId: 15,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('newRequestLinkUrl', () => {
|
||||||
|
it('строит абсолютную ссылку на пустую форму без rpsource', () => {
|
||||||
|
const url = newRequestLinkUrl('hire');
|
||||||
|
expect(url).toBe(`${window.location.origin}${window.location.pathname}?rpmode=create&rptype=hire`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ссылка на пустую форму разбирается обратно в тот же маршрут', () => {
|
||||||
|
const url = newRequestLinkUrl('hire');
|
||||||
|
const route = parseRoute(new URL(url).search);
|
||||||
|
expect(route).toEqual({ mode: 'create', nav: 'new', formKey: 'hire' });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('подвкладки и фильтр «Моих заявок»', () => {
|
describe('подвкладки и фильтр «Моих заявок»', () => {
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ import * as React from 'react';
|
|||||||
// списке данных. Заявка показывается в выезжающей панели ПОВЕРХ текущего
|
// списке данных. Заявка показывается в выезжающей панели ПОВЕРХ текущего
|
||||||
// раздела, поэтому rpnav сохраняется вместе с rpmode.
|
// раздела, поэтому rpnav сохраняется вместе с rpmode.
|
||||||
//
|
//
|
||||||
|
// Ссылка на ПУСТУЮ новую форму конкретного типа (без предзаполнения, минуя
|
||||||
|
// плитки) — см. newRequestLinkUrl():
|
||||||
|
// …/СтраницаПортала.aspx?rpmode=create&rptype=<форма>
|
||||||
|
// Отличается от «создать на основе» (тот же rpmode=create, но с rpsource) —
|
||||||
|
// без rpsource форма просто открывается пустой.
|
||||||
|
//
|
||||||
// Префикс rp (request-portal): имена id/mode/type зарезервированы SharePoint
|
// Префикс rp (request-portal): имена id/mode/type зарезервированы SharePoint
|
||||||
// (напр. ?id= он трактует как навигацию к элементу → «Элемент не существует»),
|
// (напр. ?id= он трактует как навигацию к элементу → «Элемент не существует»),
|
||||||
// поэтому используем собственные, неконфликтные имена параметров.
|
// поэтому используем собственные, неконфликтные имена параметров.
|
||||||
@@ -63,6 +69,23 @@ export interface IRoute {
|
|||||||
listId?: string;
|
listId?: string;
|
||||||
/** Id исходной заявки для предзаполнения — для 'create' (напр. орг. места на основе прием). */
|
/** Id исходной заявки для предзаполнения — для 'create' (напр. орг. места на основе прием). */
|
||||||
sourceId?: number;
|
sourceId?: number;
|
||||||
|
/**
|
||||||
|
* Id строки-источника во внешней системе (Dataverse «Строка заявки», GUID
|
||||||
|
* первичного ключа). Приходит по ссылке из внешней системы вместе с rptype —
|
||||||
|
* форма «СЗ на перемещение ТМЦ» по нему после отправки на согласование
|
||||||
|
* записывает обратно ссылку на заявку (см. shared/dataverse/demandLine.service).
|
||||||
|
*/
|
||||||
|
positionId?: string;
|
||||||
|
/**
|
||||||
|
* Id номенклатуры для GET /Dictionary/Nomenclature/{id} — форма предзаполняет
|
||||||
|
* табличную часть остатками по складам. Приходит по той же внешней ссылке.
|
||||||
|
*/
|
||||||
|
nomenclatureId?: string;
|
||||||
|
/**
|
||||||
|
* Склад-фильтр для GET /Dictionary/Nomenclature — вернуть остатки только по
|
||||||
|
* этому складу. Приходит по той же внешней ссылке; пусто — все склады.
|
||||||
|
*/
|
||||||
|
storeName?: string;
|
||||||
/** Подвкладка «Моих заявок»; задана только при nav='own'. */
|
/** Подвкладка «Моих заявок»; задана только при nav='own'. */
|
||||||
ownTab?: OwnTab;
|
ownTab?: OwnTab;
|
||||||
/** Фильтр по статусу заявки; задан только при nav='own' и ownTab='created'. */
|
/** Фильтр по статусу заявки; задан только при nav='own' и ownTab='created'. */
|
||||||
@@ -310,6 +333,12 @@ export function parseRoute(search: string = window.location.search): IRoute {
|
|||||||
const listId = params.get('rplist') ?? undefined;
|
const listId = params.get('rplist') ?? undefined;
|
||||||
const sourceRaw = params.get('rpsource');
|
const sourceRaw = params.get('rpsource');
|
||||||
const sourceId = sourceRaw ? Number(sourceRaw) : undefined;
|
const sourceId = sourceRaw ? Number(sourceRaw) : undefined;
|
||||||
|
// Параметры внешней ссылки (без rp-префикса — так их формирует внешняя
|
||||||
|
// система, менять её мы не можем). Актуальны только для сценария «создать» по
|
||||||
|
// rptype: заполнить форму ТМЦ из справочника и запомнить строку-источник.
|
||||||
|
const positionId = params.get('positionId') ?? undefined;
|
||||||
|
const nomenclatureId = params.get('nomenclatureId') ?? undefined;
|
||||||
|
const storeName = params.get('storeName') ?? undefined;
|
||||||
|
|
||||||
// Подвкладка и фильтр — состояние раздела «Мои заявки», поэтому вне его не
|
// Подвкладка и фильтр — состояние раздела «Мои заявки», поэтому вне его не
|
||||||
// читаются вовсе (иначе чужой rptab из ссылки «протёк» бы в другой раздел).
|
// читаются вовсе (иначе чужой rptab из ссылки «протёк» бы в другой раздел).
|
||||||
@@ -343,6 +372,17 @@ export function parseRoute(search: string = window.location.search): IRoute {
|
|||||||
if (formKey && sourceId && !isNaN(sourceId)) {
|
if (formKey && sourceId && !isNaN(sourceId)) {
|
||||||
return { mode: 'create', nav: 'new', formKey, sourceId };
|
return { mode: 'create', nav: 'new', formKey, sourceId };
|
||||||
}
|
}
|
||||||
|
// Ссылка на пустую новую форму конкретного типа — без предзаполнения
|
||||||
|
// (клик по плитке внутри портала, либо явная ссылка ?rptype=<форма> без
|
||||||
|
// rpsource — см. newRequestLinkUrl). Как и ветка с rpsource выше, НЕ требует
|
||||||
|
// rpmode=create: buildQuery() его для этого случая не пишет (значения по
|
||||||
|
// умолчанию в адрес не пишем), поэтому единственный сигнал — сам rptype.
|
||||||
|
// Но approve/edit явно исключаем: битая/неполная ссылка на них (напр.
|
||||||
|
// rpmode=approve без валидного rpid) должна откатываться на обычную подачу,
|
||||||
|
// а не тихо превращаться в открытие пустой формы другого режима.
|
||||||
|
if (formKey && mode !== 'approve' && mode !== 'edit') {
|
||||||
|
return { mode: 'create', nav: 'new', formKey, positionId, nomenclatureId, storeName };
|
||||||
|
}
|
||||||
return { mode: 'submit', nav, ownTab, ownStatus, page, query };
|
return { mode: 'submit', nav, ownTab, ownStatus, page, query };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,9 +420,9 @@ function buildQuery(route: IRoute): string {
|
|||||||
// Партиция неизвестна (архивная ссылка старого формата, до этого поля) —
|
// Партиция неизвестна (архивная ссылка старого формата, до этого поля) —
|
||||||
// ссылка всё равно валидна, форма/просмотр просто откатятся на текущую.
|
// ссылка всё равно валидна, форма/просмотр просто откатятся на текущую.
|
||||||
if (route.listId) params.set('rplist', route.listId);
|
if (route.listId) params.set('rplist', route.listId);
|
||||||
} else if (route.mode === 'create' && route.formKey && route.sourceId) {
|
} else if (route.mode === 'create' && route.formKey) {
|
||||||
params.set('rptype', route.formKey);
|
params.set('rptype', route.formKey);
|
||||||
params.set('rpsource', String(route.sourceId));
|
if (route.sourceId) params.set('rpsource', String(route.sourceId));
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = params.toString();
|
const query = params.toString();
|
||||||
@@ -438,6 +478,18 @@ export function requestLinkUrl(formKey: string, itemId: number, listId?: string)
|
|||||||
return `${window.location.origin}${window.location.pathname}?${params.toString()}`;
|
return `${window.location.origin}${window.location.pathname}?${params.toString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* АБСОЛЮТНАЯ ссылка на ПУСТУЮ новую форму конкретного типа — без предзаполнения,
|
||||||
|
* в отличие от requestLinkUrl (просмотр существующей). Для той же цели: письмо,
|
||||||
|
* сообщение в чат, кнопка «поделиться ссылкой на форму».
|
||||||
|
*/
|
||||||
|
export function newRequestLinkUrl(formKey: string): string {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.set('rpmode', 'create');
|
||||||
|
params.set('rptype', formKey);
|
||||||
|
return `${window.location.origin}${window.location.pathname}?${params.toString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Открыть заявку по её АБСОЛЮТНОЙ ссылке (см. requestLinkUrl) БЕЗ перезагрузки
|
* Открыть заявку по её АБСОЛЮТНОЙ ссылке (см. requestLinkUrl) БЕЗ перезагрузки
|
||||||
* страницы — если ссылка ведёт на эту же страницу портала.
|
* страницы — если ссылка ведёт на эту же страницу портала.
|
||||||
|
|||||||
@@ -8,11 +8,21 @@ export interface ILineItem {
|
|||||||
name: string;
|
name: string;
|
||||||
/** Инвентарный номер. */
|
/** Инвентарный номер. */
|
||||||
invNumber: string;
|
invNumber: string;
|
||||||
|
/** Номенклатурный код (из справочника, поле code). Необязательное — у строк,
|
||||||
|
* добавленных вручную/из Excel, может отсутствовать. */
|
||||||
|
code?: string;
|
||||||
/** Единица измерения (по умолчанию «шт»). */
|
/** Единица измерения (по умолчанию «шт»). */
|
||||||
unit: string;
|
unit: string;
|
||||||
/** Количество. */
|
/** Количество. */
|
||||||
qty: string;
|
qty: string;
|
||||||
|
/**
|
||||||
|
* Id номенклатуры-источника (для строк, предзаполненных из справочника). У
|
||||||
|
* строк, дописанных пользователем вручную, отсутствует — по наличию поля
|
||||||
|
* отличают «пришло из номенклатуры» от «добавлено руками». В UI не
|
||||||
|
* показывается, едет вместе с позицией в ItemsJson.
|
||||||
|
*/
|
||||||
|
nomenclatureId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Пустая строка позиции (для «Добавить строку»). Ед.изм по умолчанию «шт». */
|
/** Пустая строка позиции (для «Добавить строку»). Ед.изм по умолчанию «шт». */
|
||||||
export const EMPTY_LINE_ITEM: ILineItem = { name: '', invNumber: '', unit: 'шт', qty: '' };
|
export const EMPTY_LINE_ITEM: ILineItem = { name: '', invNumber: '', code: '', unit: 'шт', qty: '' };
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export const ItemsTable: React.FC<IItemsTableProps> = ({ value, onChange, readOn
|
|||||||
<th className={styles.num}>№</th>
|
<th className={styles.num}>№</th>
|
||||||
<th>Наименование</th>
|
<th>Наименование</th>
|
||||||
<th className={styles.inv}>Инв. номер</th>
|
<th className={styles.inv}>Инв. номер</th>
|
||||||
|
<th className={styles.inv}>Код</th>
|
||||||
<th className={styles.unit}>Ед.изм</th>
|
<th className={styles.unit}>Ед.изм</th>
|
||||||
<th className={styles.qty}>Кол-во</th>
|
<th className={styles.qty}>Кол-во</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -58,7 +59,7 @@ export const ItemsTable: React.FC<IItemsTableProps> = ({ value, onChange, readOn
|
|||||||
<tbody>
|
<tbody>
|
||||||
{value.length === 0 ? (
|
{value.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={5} className={styles.empty}>Позиции не добавлены</td>
|
<td colSpan={6} className={styles.empty}>Позиции не добавлены</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
value.map((row, i) => (
|
value.map((row, i) => (
|
||||||
@@ -66,6 +67,7 @@ export const ItemsTable: React.FC<IItemsTableProps> = ({ value, onChange, readOn
|
|||||||
<td className={styles.num}>{i + 1}</td>
|
<td className={styles.num}>{i + 1}</td>
|
||||||
<td>{row.name}</td>
|
<td>{row.name}</td>
|
||||||
<td>{row.invNumber}</td>
|
<td>{row.invNumber}</td>
|
||||||
|
<td>{row.code}</td>
|
||||||
<td className={styles.unit}>{row.unit}</td>
|
<td className={styles.unit}>{row.unit}</td>
|
||||||
<td className={styles.qty}>{row.qty}</td>
|
<td className={styles.qty}>{row.qty}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -86,6 +88,7 @@ export const ItemsTable: React.FC<IItemsTableProps> = ({ value, onChange, readOn
|
|||||||
<th className={styles.num}>№</th>
|
<th className={styles.num}>№</th>
|
||||||
<th>Наименование</th>
|
<th>Наименование</th>
|
||||||
<th className={styles.inv}>Инв. номер</th>
|
<th className={styles.inv}>Инв. номер</th>
|
||||||
|
<th className={styles.inv}>Код</th>
|
||||||
<th className={styles.unit}>Ед.изм</th>
|
<th className={styles.unit}>Ед.изм</th>
|
||||||
<th className={styles.qty}>Кол-во</th>
|
<th className={styles.qty}>Кол-во</th>
|
||||||
<th className={styles.act} aria-label="Действия" />
|
<th className={styles.act} aria-label="Действия" />
|
||||||
@@ -94,7 +97,7 @@ export const ItemsTable: React.FC<IItemsTableProps> = ({ value, onChange, readOn
|
|||||||
<tbody>
|
<tbody>
|
||||||
{value.length === 0 ? (
|
{value.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={6} className={styles.empty}>
|
<td colSpan={7} className={styles.empty}>
|
||||||
Нет позиций. Добавьте строку или вставьте из Excel.
|
Нет позиций. Добавьте строку или вставьте из Excel.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -116,6 +119,13 @@ export const ItemsTable: React.FC<IItemsTableProps> = ({ value, onChange, readOn
|
|||||||
onChange={(e) => setCell(i, 'invNumber', e.target.value)}
|
onChange={(e) => setCell(i, 'invNumber', e.target.value)}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
className={styles.cell}
|
||||||
|
value={row.code ?? ''}
|
||||||
|
onChange={(e) => setCell(i, 'code', e.target.value)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input
|
||||||
className={`${styles.cell} ${styles.cellCenter}`}
|
className={`${styles.cell} ${styles.cellCenter}`}
|
||||||
|
|||||||
Reference in New Issue
Block a user