Compare commits

...

2 Commits

Author SHA1 Message Date
Тимур Городецкий 899b14b869 feat(oid): импорт работ из заявки на услуги на шаге 1
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-02 15:27:30 +05:00
Тимур Городецкий 9faff36ba2 feat(ct): карточка INCORE во вложениях СТ, кнопка только после согласования
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-02 15:27:21 +05:00
11 changed files with 608 additions and 14 deletions
@@ -279,3 +279,19 @@
font-size: 13px;
color: $muted;
}
.cardLink {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
font-size: 13px;
color: $ok;
text-decoration: none;
border: 1px solid $ok;
border-radius: 4px;
&:hover {
background: $ok-bg;
}
}
@@ -266,8 +266,21 @@ export const Step4Approval: React.FC<Props> = (props) => {
</Button>
)}
{a.isOnApproval && a.isInitiator && a.ct?.status !== "approved" && (
<>
{/* INCORE: согласованная СТ — финал. Есть карточка во вложении → показываем файл;
нет → кнопка формирования. Только для инициатора. */}
{a.isInitiator && a.ct?.status === "approved" && !a.isLoadingCard && (
a.cardAttachment ? (
<a
className={styles.cardLink}
href={a.cardAttachment.url}
target="_blank"
rel="noreferrer"
title="Открыть карточку электронного документа"
>
<Icon name="download" size="sm" />
{a.cardAttachment.name}
</a>
) : (
<Button variant="secondary" onClick={a.handleSendToIncore} disabled={a.isSendingToIncore}>
{a.isSendingToIncore ? (
<Spinner size="small" />
@@ -278,11 +291,15 @@ export const Step4Approval: React.FC<Props> = (props) => {
</>
)}
</Button>
<Button variant="secondary" onClick={a.handleRestart} disabled={a.isSubmitting}>
<Icon name="refresh" size="sm" />
Перезапустить согласование
</Button>
</>
)
)}
{/* Перезапуск — пока СТ на согласовании (ещё не согласована) */}
{a.isOnApproval && a.isInitiator && a.ct?.status !== "approved" && (
<Button variant="secondary" onClick={a.handleRestart} disabled={a.isSubmitting}>
<Icon name="refresh" size="sm" />
Перезапустить согласование
</Button>
)}
</div>
</div>
@@ -1,4 +1,4 @@
import { useState, useCallback, useRef } from 'react';
import { useState, useCallback, useRef, useEffect } from 'react';
import { ApprovalMember, ApprovalMemberStatus, FormStep } from '../types/domain';
import { CtService } from '../../services/CtService';
import { SigexSignService, SigexSignStep } from '../../services/SigexSignService';
@@ -53,6 +53,10 @@ export function useStep4Approval(args: UseStep4ApprovalArgs) {
const [isSendingToIncore, setIsSendingToIncore] = useState(false);
const [incoreStep, setIncoreStep] = useState<IncoreStep | null>(null);
const [incoreError, setIncoreError] = useState<string | null>(null);
// Карточка INCORE во вложениях элемента СТ: если она уже есть — показываем файл, а не кнопку.
const [cardAttachment, setCardAttachment] = useState<{ name: string; url: string } | null>(null);
const [isLoadingCard, setIsLoadingCard] = useState(false);
const incoreCardName = ct ? `Карточка_CT_${ct.id}.pdf` : '';
// ─── Add member ───
const [showAddRow, setShowAddRow] = useState(false);
@@ -347,8 +351,12 @@ export function useStep4Approval(args: UseStep4ApprovalArgs) {
setIncoreStep,
);
// 3. Пакет для INCORE: карточка с ЭЦП + файлы КП.
service.downloadBlob(cardBlob, `Карточка_CT_${ct.id}.pdf`);
// 3. Архивируем карточку во вложения элемента СТ и переключаем UI на показ файла.
const saved = await service.saveCtCardAttachment(ct.id, cardBlob, incoreCardName);
setCardAttachment(saved);
// 4. Пакет для INCORE: карточка с ЭЦП + файлы КП — скачиваем пользователю.
service.downloadBlob(cardBlob, incoreCardName);
const kpFiles = formData.uploadedKpFiles ?? [];
for (const f of kpFiles) {
@@ -403,6 +411,23 @@ export function useStep4Approval(args: UseStep4ApprovalArgs) {
chain.filter((prev) => prev.order < m.order).every((prev) => prev.status !== 'pending'),
);
// Согласованная СТ — финал: если карточка уже во вложениях, показываем файл вместо кнопки.
// Грузим только для инициатора и только в статусе «Согласована».
useEffect(() => {
if (ct?.status !== 'approved' || !isInitiator) {
setCardAttachment(null);
return;
}
let cancelled = false;
setIsLoadingCard(true);
service
.findCtCardAttachment(ct.id, `Карточка_CT_${ct.id}.pdf`)
.then((found) => { if (!cancelled) setCardAttachment(found); })
.catch(() => { if (!cancelled) setCardAttachment(null); })
.finally(() => { if (!cancelled) setIsLoadingCard(false); });
return () => { cancelled = true; };
}, [ct?.status, ct?.id, isInitiator, service]);
return {
ct,
chain,
@@ -414,6 +439,8 @@ export function useStep4Approval(args: UseStep4ApprovalArgs) {
incoreStep,
incoreError,
dismissIncoreError: () => setIncoreError(null),
cardAttachment,
isLoadingCard,
isOnApproval,
showStatuses,
isInitiator,
@@ -1536,6 +1536,54 @@ export class CtService {
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
// Ищет карточку электронного документа (INCORE) во вложениях элемента СТ по имени файла.
async findCtCardAttachment(
ctId: string,
fileName: string,
): Promise<{ name: string; url: string } | null> {
const res = await this.http.get(
`${this._liById(LISTS.CT, ctId)}/AttachmentFiles`,
SPHttpClient.configurations.v1,
);
if (!res.ok) return null;
const data = await res.json();
const files: Array<{ FileName: string; ServerRelativeUrl: string }> = data.value ?? [];
const found = files.find((f) => f.FileName === fileName);
return found ? { name: found.FileName, url: found.ServerRelativeUrl } : null;
}
// Сохраняет карточку во вложения элемента СТ. Одноимённое вложение перезаписывается.
async saveCtCardAttachment(
ctId: string,
blob: Blob,
fileName: string,
): Promise<{ name: string; url: string }> {
const itemUrl = this._liById(LISTS.CT, ctId);
const encodedName = encodeURIComponent(fileName);
const existing = await this.findCtCardAttachment(ctId, fileName);
if (existing) {
await this.http.fetch(
`${itemUrl}/AttachmentFiles/getByFileName('${encodedName}')`,
SPHttpClient.configurations.v1,
{ method: "POST", headers: { "X-HTTP-Method": "DELETE", "IF-MATCH": "*" } },
);
}
const buffer = await blob.arrayBuffer();
const res = await this.http.post(
`${itemUrl}/AttachmentFiles/add(FileName='${encodedName}')`,
SPHttpClient.configurations.v1,
{
headers: { Accept: "application/json;odata=nometadata" },
body: buffer as unknown as string,
},
);
if (!res.ok) throw new Error(`Ошибка сохранения карточки во вложения: ${res.status}`);
const data = await res.json();
return { name: data.FileName ?? fileName, url: data.ServerRelativeUrl };
}
async extractWordText(itemId: number): Promise<void> {
const token = await this._getApiToken();
const res = await fetch(`${EXTERNAL_API}/Document/extractWordText`, {
@@ -0,0 +1,112 @@
@import '../common/theme';
.state {
display: flex;
align-items: center;
gap: 8px;
padding: 16px 0;
color: $muted;
font-size: 14px;
}
.searchBox {
margin-bottom: 8px;
}
.hint {
margin-bottom: 8px;
font-size: 12px;
color: $muted;
}
.list {
list-style: none;
margin: 0;
padding: 4px;
width: 100%;
max-height: 50vh;
overflow-y: auto;
box-sizing: border-box;
border: 1px solid $border-strong;
border-radius: 4px;
background: $surface;
display: flex;
flex-direction: column;
gap: 4px;
}
.item {
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px 10px;
border-radius: 4px;
cursor: pointer;
&:hover {
background: $hover;
}
}
.itemMain {
display: flex;
align-items: baseline;
gap: 8px;
flex-wrap: wrap;
}
.itemCode {
font-weight: 600;
color: $text;
flex-shrink: 0;
}
.itemActivity {
color: $text;
font-size: 13px;
overflow-wrap: anywhere;
}
.itemMeta {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
font-size: 12px;
color: $muted;
}
// ─── Статус-пилюля ───────────────────────────────────────────────────────────
.pill {
display: inline-block;
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
font-weight: 600;
white-space: nowrap;
}
.pillOk {
color: $ok;
background: $ok-bg;
}
.pillAccent {
color: $accent-dark;
background: $accent-bg;
}
.pillWarn {
color: $warn-fg;
background: $warn-bg;
}
.pillDanger {
color: $danger;
background: $danger-bg;
}
.pillMuted {
color: $muted;
background: $head-bg;
}
@@ -0,0 +1,130 @@
import * as React from "react";
import { useState, useEffect, useMemo } from "react";
import { Dialog, TextInput, Spinner, Alert } from "@tnsu/ui-kit";
import type { ServiceRequest } from "../types/domain";
import { OidService } from "../../services/OidService";
import styles from "./RequestPickerDialog.module.scss";
interface Props {
service: OidService;
onPick: (request: ServiceRequest) => void;
onClose: () => void;
}
// Сколько последних заявок показывать без поиска (список может быть очень большим).
const LATEST_LIMIT = 5;
function positionsCount(request: ServiceRequest): number {
return request.groups.reduce((sum, g) => sum + g.positions.length, 0);
}
function statusPillClass(status: string): string {
switch (status) {
case "Согласовано":
return styles.pillOk;
case "Выполнение":
return styles.pillAccent;
case "На согласовании":
case "Возвращена на доработку":
return styles.pillWarn;
case "Отклонено":
return styles.pillDanger;
default:
return styles.pillMuted; // Черновик, Закрыта, прочее
}
}
// Диалог выбора заявки на услуги (Шаг 1 ОИД): поиск + список (по умолчанию 5 последних).
// Выбор заявки заполняет форму работами из неё.
export const RequestPickerDialog: React.FC<Props> = ({ service, onPick, onClose }) => {
const [requests, setRequests] = useState<ServiceRequest[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [query, setQuery] = useState("");
useEffect(() => {
let cancelled = false;
setIsLoading(true);
service
.fetchServiceRequests()
.then((list) => { if (!cancelled) setRequests(list); })
.catch((e) => { if (!cancelled) setError((e as Error).message || "Ошибка загрузки заявок"); })
.finally(() => { if (!cancelled) setIsLoading(false); });
return () => { cancelled = true; };
}, [service]);
const isSearching = query.trim().length > 0;
const filtered = useMemo(() => {
const q = query.trim().toLowerCase();
if (!q) return requests.slice(0, LATEST_LIMIT);
return requests.filter(
(r) =>
r.code.toLowerCase().includes(q) ||
r.activityName.toLowerCase().includes(q) ||
r.projectName.toLowerCase().includes(q),
);
}, [requests, query]);
return (
<Dialog
open
title="Выбор заявки на услуги"
subText="Выберите заявку — её проект, работы и позиции заполнят форму."
onDismiss={onClose}
width="lg"
>
{isLoading && (
<div className={styles.state}>
<Spinner size="small" /> Загрузка заявок…
</div>
)}
{!isLoading && error && <Alert variant="err">{error}</Alert>}
{!isLoading && !error && requests.length === 0 && (
<div className={styles.state}>Заявки не найдены</div>
)}
{!isLoading && !error && requests.length > 0 && (
<>
<div className={styles.searchBox}>
<TextInput
value={query}
onChange={setQuery}
placeholder="Поиск по коду, наименованию работ или проекту…"
ariaLabel="Поиск заявки"
/>
</div>
{!isSearching && requests.length > LATEST_LIMIT && (
<div className={styles.hint}>
Показаны {LATEST_LIMIT} последних — используйте поиск, чтобы найти остальные
</div>
)}
{filtered.length === 0 ? (
<div className={styles.state}>Ничего не найдено</div>
) : (
<ul className={styles.list}>
{filtered.map((r) => (
<li key={r.id} className={styles.item} onClick={() => onPick(r)}>
<div className={styles.itemMain}>
<span className={styles.itemCode}>{r.code || "б/н"}</span>
<span className={styles.itemActivity}>{r.activityName || "—"}</span>
</div>
<div className={styles.itemMeta}>
<span className={`${styles.pill} ${statusPillClass(r.status)}`}>
{r.status || "—"}
</span>
<span>{r.projectName || "—"}</span>
<span>позиций {positionsCount(r)}</span>
</div>
</li>
))}
</ul>
)}
</>
)}
</Dialog>
);
};
@@ -6,6 +6,34 @@
gap: 24px;
}
.importRow {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.importHint {
font-size: 13px;
color: $muted;
}
.sourceRow {
display: flex;
align-items: center;
gap: 8px;
}
.sourceLink {
font-size: 13px;
color: $text;
text-decoration: underline;
&:hover {
opacity: 0.8;
}
}
.workNameRow {
max-width: 600px;
}
@@ -4,6 +4,7 @@ import type { OIDFormData, FormStep } from "../types/domain";
import { OidService } from "../../services/OidService";
import { useStep1WorksInput } from "./useStep1WorksInput";
import { ComboField } from "./ComboField";
import { RequestPickerDialog } from "./RequestPickerDialog";
import styles from "./Step1WorksInput.module.scss";
interface Props {
@@ -17,11 +18,52 @@ interface Props {
// Шаг 1 «Ввод работ» (ручной ввод групп работ, не список потребностей — своя доменка ОИД).
// Логика — в useStep1WorksInput; здесь только разметка.
export const Step1WorksInput: React.FC<Props> = (props) => {
const { isReadOnly = false } = props;
const { isReadOnly = false, service } = props;
const a = useStep1WorksInput(props);
const [showPicker, setShowPicker] = React.useState(false);
return (
<div className={styles.root}>
{!isReadOnly && (
<div className={styles.importRow}>
<Button
variant="secondary"
onClick={() => setShowPicker(true)}
disabled={a.isSaving || a.isCreatingOid}
>
Выбрать из заявки
</Button>
<span className={styles.importHint}>
Заполнить работы из заявки на услуги (или введите вручную ниже)
</span>
</div>
)}
{a.sourceRequest?.url && (
<div className={styles.sourceRow}>
<span className={styles.importHint}>Источник:</span>
<a
className={styles.sourceLink}
href={a.sourceRequest.url}
target="_blank"
rel="noreferrer"
>
Заявка {a.sourceRequest.code || ""} ↗
</a>
</div>
)}
{showPicker && (
<RequestPickerDialog
service={service}
onClose={() => setShowPicker(false)}
onPick={(req) => {
a.applyRequest(req);
setShowPicker(false);
}}
/>
)}
<div className={styles.workNameRow}>
<FormField label="Проект" required>
<ComboField
@@ -1,6 +1,6 @@
import * as React from "react";
import { useState, useCallback, useRef, useEffect } from "react";
import type { OIDFormData, PositionGroup, OIDPosition, FormStep } from "../types/domain";
import { useState, useCallback, useRef, useEffect, useMemo } from "react";
import type { OIDFormData, PositionGroup, OIDPosition, ServiceRequest, FormStep } from "../types/domain";
import { OidService } from "../../services/OidService";
export interface ComboOption {
@@ -254,6 +254,36 @@ export function useStep1WorksInput(args: UseStep1WorksInputArgs) {
[formData.oid, onSave, service],
);
// Заполнить форму данными выбранной заявки (проект, наименование работ, группы/позиции).
// Ручной ввод не трогаем — это отдельное действие «Выбрать из заявки». Позиции получают
// свежие id и requestCode/requestUrl из заявки (чтобы записались в «Позиции СТ ОИД»).
const applyRequest = useCallback(
(req: ServiceRequest): void => {
dirtyRef.current = true;
setProject(req.projectName);
setProjectOptions((prev) =>
prev.some((o) => o.key === req.projectName)
? prev
: [{ key: req.projectName, text: req.projectName }, ...prev],
);
setWorkName(req.activityName);
const groupsFromReq: PositionGroup[] = req.groups.map((g) => ({
name: g.name,
positions: g.positions.map((p) => ({ ...p, id: genId(), spId: undefined })),
}));
setGroups(groupsFromReq.length > 0 ? groupsFromReq : [makeEmptyGroup()]);
const oid = formData.oid;
if (oid) {
onSave({ oid: { ...oid, project: req.projectName } });
service.updateOidProject(oid.id, req.projectName).catch(() => undefined);
}
markDirtyAndSync();
},
[formData.oid, onSave, service, markDirtyAndSync],
);
const updateGroupName = useCallback((gIdx: number, name: string): void => {
setGroups((prev) =>
prev.map((g, i) =>
@@ -328,6 +358,19 @@ export function useStep1WorksInput(args: UseStep1WorksInputArgs) {
markDirtyAndSync();
}, []);
// Заявка-источник (для ссылки на форме): берём из первой позиции, где проставлены
// requestCode/requestUrl — работает и после импорта, и после перезагрузки из SP.
const sourceRequest = useMemo(() => {
for (const g of groups) {
for (const p of g.positions) {
if (p.requestCode || p.requestUrl) {
return { code: p.requestCode ?? "", url: p.requestUrl ?? "" };
}
}
}
return null;
}, [groups]);
const validate = (): boolean => {
if (!project.trim()) {
setValidationError("Выберите проект");
@@ -434,10 +477,12 @@ export function useStep1WorksInput(args: UseStep1WorksInputArgs) {
saveError,
validationError,
unitOptions,
sourceRequest,
dirtyRef,
loadProjects,
handleWorkNameBlur,
handleProjectChange,
applyRequest,
updateGroupName,
updatePosition,
addGroup,
@@ -30,6 +30,22 @@ export interface OIDPosition {
quantity: number;
priceSmeta?: number;
summaSmeta?: number;
requestCode?: string; // номер заявки-источника (SP-поле RequestCode)
requestUrl?: string; // ссылка на заявку в SP (SP-поле RequestUrl)
}
// Заявка на услуги из SP-списка «Заявка на услуги 1 — данные» (/sites/requests).
// Источник для импорта работ на Шаге 1 ОИД.
export interface ServiceRequest {
id: number;
code: string; // RequestUniqueCode
status: string; // RequestStatus
projectName: string; // ProjectName
activityName: string; // ActivityName
requestDate: string; // RequestDate
requesterName: string; // RequesterName
url: string; // ссылка на форму просмотра элемента заявки
groups: PositionGroup[]; // распарсено из WorkVolumesJson (позиции уже с requestCode/requestUrl)
}
export interface Supplier {
@@ -7,6 +7,7 @@ import type {
OIDStatus,
OIDPosition,
PositionGroup,
ServiceRequest,
Supplier,
SupplierOffer,
SupplierCriteria,
@@ -18,6 +19,13 @@ import type {
const SITE_URL = "https://tnsukz.sharepoint.com/sites/sed";
// Список заявок на услуги (источник импорта работ на Шаге 1). Отдельный сайт, тот же тенант.
const REQUESTS_SITE_URL = "https://tnsukz.sharepoint.com/sites/requests";
const REQUESTS_LIST_TITLE = "Заявка на услуги 1 — данные";
// Ссылка на заявку: страница портала заявок, rpid = Id элемента списка.
const REQUEST_LINK_BASE =
"https://tnsukz.sharepoint.com/sites/requests/SitePages/Портал-Заявок.aspx?rpmode=approve&rptype=wh-service-request&rpid=";
// "Единицы измерения" lives on the construction site collection (same tenant).
const UNITS_SITE_URL = "https://tnsukz.sharepoint.com/sites/stroy";
const UNITS_LIST_TITLE = "Единицы измерения";
@@ -73,6 +81,63 @@ function listUrl(title: string): string {
return `${SITE_URL}/_api/web/lists/getbytitle('${encodeURIComponent(title)}')`;
}
function reqGenId(): string {
return Math.random().toString(36).slice(2) + Date.now().toString(36);
}
// qty/rate в JSON заявки — строки, бывают с пробелами и запятой ("2000 ", "5 000,5").
function reqToNum(v: unknown): number {
const n = parseFloat(String(v ?? "").trim().replace(/\s/g, "").replace(",", "."));
return isNaN(n) ? 0 : n;
}
interface WorkVolumeItem {
description?: string;
unit?: string;
qty?: string | number;
rate?: string | number;
}
interface WorkVolumeGroup {
name?: string;
items?: WorkVolumeItem[];
}
// Парсит WorkVolumesJson заявки в группы/позиции формы ОИД.
// Формат: {"groups":[{"name":"...","items":[{"description","unit","qty","rate"}]}],"total":N}
function parseWorkVolumes(raw: string, requestCode: string, requestUrl: string): PositionGroup[] {
if (!raw || !raw.trim()) return [];
let data: { groups?: WorkVolumeGroup[] };
try {
data = JSON.parse(raw);
} catch {
return [];
}
const groups = Array.isArray(data.groups) ? data.groups : [];
return groups.map((g) => {
const name = String(g.name ?? "").trim();
const items = Array.isArray(g.items) ? g.items : [];
return {
name,
positions: items.map((it): OIDPosition => {
const quantity = reqToNum(it.qty);
const price = reqToNum(it.rate);
return {
id: reqGenId(),
parentGroup: name,
position: String(it.description ?? "").trim(),
positionType: "Услуга",
unit: String(it.unit ?? "").trim(),
quantity,
priceSmeta: price || undefined,
summaSmeta: price && quantity ? price * quantity : undefined,
requestCode,
requestUrl,
};
}),
};
});
}
export class OidService {
private readonly context: WebPartContext;
private readonly baseUrl: string;
@@ -245,6 +310,48 @@ export class OidService {
}
}
// ─── Заявки на услуги (импорт работ на Шаге 1) ──────────────────────────────
// Читает заявки из списка «Заявка на услуги 1 — данные» (/sites/requests).
// Грузим все заявки (последние сверху); поиск по коду/наименованию — на клиенте.
async fetchServiceRequests(search?: string): Promise<ServiceRequest[]> {
const url =
`${REQUESTS_SITE_URL}/_api/web/lists/getbytitle('${encodeURIComponent(REQUESTS_LIST_TITLE)}')/items` +
`?$select=Id,RequestUniqueCode,RequestStatus,ProjectName,ProjectId,ActivityName,RequestDate,RequesterName,WorkVolumesJson` +
`&$orderby=RequestDate%20desc` +
`&$top=200`;
const res = await this.sp.get(url, SPHttpClient.configurations.v1);
if (!res.ok) throw new Error(`Ошибка загрузки заявок: ${res.status}`);
const data = await res.json();
const items = (data.value as Array<Record<string, unknown>>) || [];
const mapped: ServiceRequest[] = items.map((it) => {
const id = it.Id as number;
const code = (it.RequestUniqueCode as string) || "";
const requestUrl = `${REQUEST_LINK_BASE}${id}`;
return {
id,
code,
status: (it.RequestStatus as string) || "",
projectName: (it.ProjectName as string) || "",
activityName: (it.ActivityName as string) || "",
requestDate: (it.RequestDate as string) || "",
requesterName: (it.RequesterName as string) || "",
url: requestUrl,
groups: parseWorkVolumes((it.WorkVolumesJson as string) || "", code, requestUrl),
};
});
const q = (search ?? "").trim().toLowerCase();
if (!q) return mapped;
return mapped.filter(
(r) =>
r.code.toLowerCase().includes(q) ||
r.activityName.toLowerCase().includes(q),
);
}
// ─── Positions ──────────────────────────────────────────────────────────────
async createOIDPositions(
@@ -265,6 +372,8 @@ export class OidService {
};
if (pos.priceSmeta !== undefined) body.priceSmeta = pos.priceSmeta;
if (pos.summaSmeta !== undefined) body.summaSmeta = pos.summaSmeta;
if (pos.requestCode) body.RequestCode = pos.requestCode;
if (pos.requestUrl) body.RequestUrl = pos.requestUrl;
const res = await this.sp.post(
`${listUrl(LISTS.OID_POSITIONS)}/items`,
@@ -308,6 +417,8 @@ export class OidService {
quantity: pos.quantity,
priceSmeta: pos.priceSmeta ?? null,
summaSmeta: pos.summaSmeta ?? null,
RequestCode: pos.requestCode ?? null,
RequestUrl: pos.requestUrl ?? null,
};
if (pos.spId && existingIds.has(pos.spId)) {
@@ -376,7 +487,7 @@ export class OidService {
const url =
`${listUrl(LISTS.OID_POSITIONS)}/items` +
`?$filter=CTId eq ${oidId}` +
`&$select=Id,parentGroup,position,positionType,unit,quantity,priceSmeta,summaSmeta` +
`&$select=Id,parentGroup,position,positionType,unit,quantity,priceSmeta,summaSmeta,RequestCode,RequestUrl` +
`&$top=500`;
const res = await this.sp.get(url, SPHttpClient.configurations.v1);
const data = await res.json();
@@ -392,6 +503,8 @@ export class OidService {
quantity: (item.quantity as number) || 0,
priceSmeta: item.priceSmeta as number | undefined,
summaSmeta: item.summaSmeta as number | undefined,
requestCode: (item.RequestCode as string) || undefined,
requestUrl: (item.RequestUrl as string) || undefined,
}),
);
}