From 00a0eb6ce7c25c1081ca45d9983bd04bd2f53831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=B8=D0=BC=D1=83=D1=80=20=D0=93=D0=BE=D1=80=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=D1=86=D0=BA=D0=B8=D0=B9?= Date: Mon, 31 Aug 2026 17:48:22 +0500 Subject: [PATCH] =?UTF-8?q?feat(ct):=20=D0=B8=D0=BD=D1=82=D0=B5=D0=B3?= =?UTF-8?q?=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20Adata=20=D0=B8=20=D0=B2=D1=8B?= =?UTF-8?q?=D0=B1=D0=BE=D1=80=20=D1=80=D0=B5=D0=BA=D0=BE=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B4=D0=BE=D0=B2=D0=B0=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=B0=D0=B2=D1=89=D0=B8=D0=BA=D0=B0=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D1=84=D0=BE=D1=80=D0=BC=D1=83=D0=BB=D0=B5=2080/20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- provisioning/manifests/ct.provisioning.json | 45 ++--- .../components/CtFormsPortal.module.scss | 4 + .../Step2CommercialOffers/kpTypes.ts | 1 + .../Step2CommercialOffers/useKpProcessing.ts | 9 + .../Step3ComparisonTable/AdataIndicator.tsx | 149 ++++++++++++++ .../Step3ComparisonTable/ComparisonMatrix.tsx | 91 +++++---- .../Step3ComparisonTable/CriteriaTable.tsx | 18 +- .../Step3ComparisonTable/MatrixRowView.tsx | 6 +- .../Step3ComparisonTable.module.scss | 183 ++++++++++++++++-- .../Step3ComparisonTable.tsx | 32 ++- .../Step3ComparisonTable/useAdataCheck.ts | 86 ++++++++ .../useRecommendedPick.ts | 87 +++++++++ .../Step3ComparisonTable/useStep3Matrix.ts | 22 --- .../forms/ct/components/types/domain.ts | 32 +++ .../forms/ct/services/CtService.ts | 174 +++++++++++++++++ 15 files changed, 826 insertions(+), 113 deletions(-) create mode 100644 src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/AdataIndicator.tsx create mode 100644 src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/useAdataCheck.ts create mode 100644 src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/useRecommendedPick.ts diff --git a/provisioning/manifests/ct.provisioning.json b/provisioning/manifests/ct.provisioning.json index b91ba0b..a1c3921 100644 --- a/provisioning/manifests/ct.provisioning.json +++ b/provisioning/manifests/ct.provisioning.json @@ -27,11 +27,7 @@ "internalName": "status", "title": "Статус", "type": "Choice", - "choices": [ - "Черновик", - "Согласовано", - "На согласовании" - ] + "choices": ["Черновик", "Согласовано", "На согласовании"] }, { "internalName": "winSupplier", @@ -105,10 +101,7 @@ "internalName": "positionType", "title": "Тип", "type": "Choice", - "choices": [ - "ТМЦ", - "Дополнительные затраты" - ] + "choices": ["ТМЦ", "Дополнительные затраты"] }, { "internalName": "deadline", @@ -226,12 +219,7 @@ "internalName": "deliveryType", "title": "Условия поставки", "type": "Choice", - "choices": [ - "DAP", - "FOB", - "DDP", - "EXW" - ] + "choices": ["DAP", "FOB", "DDP", "EXW"] }, { "internalName": "paymentType", @@ -247,10 +235,7 @@ "internalName": "sertificates", "title": "Сертификаты", "type": "Choice", - "choices": [ - "Да", - "Нет" - ] + "choices": ["Да", "Нет"] }, { "internalName": "comment", @@ -361,6 +346,21 @@ "internalName": "status", "title": "Статус", "type": "Text" + }, + { + "internalName": "bin", + "title": "БИН", + "type": "Text" + }, + { + "internalName": "adataData", + "title": "Данные Adata", + "type": "Note" + }, + { + "internalName": "adataCheckedAt", + "title": "Дата проверки Adata", + "type": "DateTime" } ] }, @@ -373,12 +373,7 @@ "internalName": "code", "title": "Код валюты", "type": "Choice", - "choices": [ - "USD", - "RUB", - "CNY", - "EUR" - ] + "choices": ["USD", "RUB", "CNY", "EUR"] }, { "internalName": "courseDate", diff --git a/src/webparts/ctFormsPortal/components/CtFormsPortal.module.scss b/src/webparts/ctFormsPortal/components/CtFormsPortal.module.scss index cbab444..032822b 100644 --- a/src/webparts/ctFormsPortal/components/CtFormsPortal.module.scss +++ b/src/webparts/ctFormsPortal/components/CtFormsPortal.module.scss @@ -14,6 +14,10 @@ .root { width: 100%; + // Изолирует стекинг-контекст всего портала: без этого элементы с position:sticky/z-index + // (залипающие колонки таблиц, шапки и т.п.) визуально "пробиваются" через затемнение модалок + // (Dialog из ui-kit рендерится React-порталом в document.body, вне этого дерева). + isolation: isolate; } // Панель «назад к реестру» над встроенной формой. diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/kpTypes.ts b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/kpTypes.ts index 74294ce..bc49813 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/kpTypes.ts +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/kpTypes.ts @@ -15,6 +15,7 @@ export interface KpFile { spId?: number; supplierName?: string; supplierSpId?: number; + supplierBIN?: string; flowDone: number; flowTotal: number; error?: string; diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/useKpProcessing.ts b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/useKpProcessing.ts index 759b066..97fc4ac 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/useKpProcessing.ts +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/useKpProcessing.ts @@ -109,6 +109,15 @@ export function useKpProcessing( } } + // БИН поставщика (для Adata) — берём первый результат из позиций, где он вообще пришёл + const supplierBIN = collectedResults.find(r => r.flowResult.supplierBIN)?.flowResult.supplierBIN; + if (supplierBIN && resolvedSupplierSpId) { + patchFile(localId, { supplierBIN }); + service.saveSupplierBIN(resolvedSupplierSpId, supplierBIN).catch(() => { + // сохранение БИН не критично для основного процесса — молча продолжаем + }); + } + // Auto-save criteria (one record per supplier — take first result that has any criteria field) const criteriaEntry = collectedResults.find( r => r.flowResult.conditions || r.flowResult.deliveryType || r.flowResult.deadline || r.flowResult.sertificates diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/AdataIndicator.tsx b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/AdataIndicator.tsx new file mode 100644 index 0000000..115e221 --- /dev/null +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/AdataIndicator.tsx @@ -0,0 +1,149 @@ +import * as React from 'react'; +import { Icon, Spinner, Dialog, Button, Link } from '@tnsu/ui-kit'; +import { AdataCompanyInfo, Supplier } from '../types/domain'; +import { AdataSupplierState } from './useAdataCheck'; +import styles from './Step3ComparisonTable.module.scss'; + +interface Props { + supplier: Supplier; + state?: AdataSupplierState; + onCheck: () => void; +} + +function formatDateTime(iso?: string): string { + if (!iso) return ''; + return new Date(iso).toLocaleString('ru-RU', { + day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', + }); +} + +function formatDateOnly(iso?: string | null): string { + if (!iso) return ''; + return new Date(iso).toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric' }); +} + +const COUNTERPARTY_TYPE_LABELS: Record = { + company: 'Юридическое лицо', + enterprise: 'Индивидуальный предприниматель', +}; + +const RISK_FLAGS: Array<{ key: 'company_problems' | 'financial_problems' | 'unreliable_zakup' | 'head_problems'; label: string }> = [ + { key: 'company_problems', label: 'Компания' }, + { key: 'financial_problems', label: 'Финансы' }, + { key: 'unreliable_zakup', label: 'Закупки' }, + { key: 'head_problems', label: 'Руководитель' }, +]; + +function InfoRow({ label, value }: { label: string; value?: React.ReactNode }): JSX.Element | null { + if (value === undefined || value === null || value === '') return null; + return ( +
+ {label} + {value} +
+ ); +} + +function buildRiskRows(info: AdataCompanyInfo): Array<{ label: string; value: string }> { + return [ + ...RISK_FLAGS.map(f => ({ label: f.label, value: info[f.key] ? 'Да' : 'Нет' })), + { label: 'Ликвидирована', value: info.end_date ? 'Да' : 'Нет' }, + ]; +} + +function buildInfoRows(info: AdataCompanyInfo): Array<{ label: string; value?: React.ReactNode }> { + return [ + { label: 'Полное наименование', value: info.name_ru }, + { label: 'Тип контрагента', value: COUNTERPARTY_TYPE_LABELS[info.counterparty_type] ?? info.counterparty_type }, + { label: 'Форма собственности', value: info.type_of_ownership || info.legal_form }, + { label: 'Юридический адрес', value: info.legal_address }, + { label: 'Регион', value: info.kato_name }, + { label: 'Вид деятельности', value: info.oked }, + { label: 'Дата регистрации', value: formatDateOnly(info.date_registration) }, + { label: 'Дата перерегистрации', value: formatDateOnly(info.last_date_registration) }, + { label: 'Дата ликвидации', value: info.end_date ? formatDateOnly(info.end_date) : undefined }, + { label: 'Размер компании', value: info.krp }, + { label: 'Сотрудников', value: info.employee_count || undefined }, + { label: 'Руководитель', value: info.fullname_director }, + { label: 'Плательщик НДС', value: info.is_nds_payer ? 'Да' : 'Нет' }, + { label: 'Резидентство', value: info.is_non_resident ? `Нерезидент${info.non_resident_country ? ` (${info.non_resident_country})` : ''}` : 'Резидент' }, + { label: 'Орган регистрации', value: info.registration_department_name }, + ]; +} + +// Компактный индикатор проверки Adata в шапке колонки поставщика: точка меняет цвет +// по результату (серая — не проверено, красная — есть риски, зелёная — рисков нет), +// клик по проверенному индикатору открывает модалку с полной карточкой контрагента. +export function AdataIndicator({ supplier, state, onCheck }: Props): JSX.Element | null { + const [showDialog, setShowDialog] = React.useState(false); + if (!state?.bin) return null; + + const info = state.data; + + if (state.isLoading) { + return ; + } + + if (!info) { + const isError = !!state.error; + return ( + + ); + } + + const hasRisk = info.company_problems || info.financial_problems || info.unreliable_zakup || info.head_problems || !!info.end_date; + + return ( + <> + + {showDialog && ( + setShowDialog(false)} + width="md" + footer={ + <> + + + + } + > +
+
Риски
+
+ {buildRiskRows(info).map(row => )} +
+ +
+ {buildInfoRows(info).map(row => )} +
+ +
Подробнее на adata.kz
+ {state.checkedAt &&
Проверено: {formatDateTime(state.checkedAt)}
} +
+
+ )} + + ); +} diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/ComparisonMatrix.tsx b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/ComparisonMatrix.tsx index 455f6fa..abeb6a7 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/ComparisonMatrix.tsx +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/ComparisonMatrix.tsx @@ -5,6 +5,8 @@ import { MatrixRowView } from './MatrixRowView'; import { ExtraPositionRow } from './ExtraPositionRow'; import { AddExtraRow } from './AddExtraRow'; import { SUPPLIER_STATUS_OPTIONS, getSubCols, formatKzt } from './matrixHelpers'; +import { UseAdataCheckResult } from './useAdataCheck'; +import { AdataIndicator } from './AdataIndicator'; import styles from './Step3ComparisonTable.module.scss'; const FIXED_COLS = 3; // Наименование / Ед. изм. / Кол-во @@ -13,14 +15,16 @@ interface Props { matrix: Step3Matrix; currencyRates: CurrencyRate[]; isRestrictedView: boolean; + adata: UseAdataCheckResult; + recommendedId?: string; // победитель формулы 80% цена / 20% условия оплаты (useRecommendedPick) } // Таблица сравнения целиком: шапка (фикс-колонки + заголовки поставщиков) + строки позиций // (MatrixRowView/ExtraPositionRow) + итоговые строки + строка добавления (AddExtraRow). // Перенесено 1:1 из Step3ComparisonTable (§стайл-гайд: чистая вёрстка, логика — в useStep3Matrix). -export function ComparisonMatrix({ matrix, currencyRates, isRestrictedView }: Props): JSX.Element { +export function ComparisonMatrix({ matrix, currencyRates, isRestrictedView, adata, recommendedId }: Props): JSX.Element { const { - isReadOnly, suppliersWithOffer, supplierCurrencies, selectedSupplierId, recommendedSupplierId, + isReadOnly, suppliersWithOffer, supplierCurrencies, selectedSupplierId, supplierStatuses, nameColWidth, regularPositions, extraPositionsRows, allPositions, computedOffers, setSelectedSupplierId, handleDeleteSupplier, handleStatusChange, handleNameColResizeMouseDown, } = matrix; @@ -39,57 +43,63 @@ export function ComparisonMatrix({ matrix, currencyRates, isRestrictedView }: Pr {suppliersWithOffer.map(s => { const currency = supplierCurrencies[s.id] ?? 'KZT'; const subCols = getSubCols(currency, isRestrictedView); - const isRec = s.id === recommendedSupplierId; const isSel = s.id === selectedSupplierId; + const isRec = s.id === recommendedId; return (
- {s.name} - {isRec && Рекомендуется} - {s.isNoOffer && Без КП} - {currency !== 'KZT' && {currency}} - {!isReadOnly ? ( - - ) : ( - supplierStatuses[s.id] ? {supplierStatuses[s.id]} : undefined - )} -
+
+ {s.name} + {isRec && Рекомендуется} +
+
+ void adata.checkSupplier(s.id)} /> + {s.isNoOffer && Без КП} + {currency !== 'KZT' && {currency}} +
+
{!isReadOnly ? ( - <> - - - + ) : ( - isSel && Выбран + supplierStatuses[s.id] ? {supplierStatuses[s.id]} : undefined )} +
+ {!isReadOnly ? ( + <> + + + + ) : ( + isSel && Выбран + )} +
@@ -208,13 +218,12 @@ export function ComparisonMatrix({ matrix, currencyRates, isRestrictedView }: Pr const posOffers = computedOffers.filter(o => o.supplierId === s.id && o.positionId === pos.id); return sum + posOffers.reduce((s2, o) => s2 + (o.totalWithExtraKzt ?? 0), 0); }, 0); - const isRec = s.id === recommendedSupplierId; const isSel = s.id === selectedSupplierId; return ( {isRestrictedView ? - : <>{formatKzt(total)} + : <>{formatKzt(total)} } ); diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/CriteriaTable.tsx b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/CriteriaTable.tsx index 01cc9e6..92985cb 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/CriteriaTable.tsx +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/CriteriaTable.tsx @@ -18,8 +18,8 @@ export function CriteriaTable({ matrix }: Props): JSX.Element | null { handleAddCustomCriteria, handleRemoveCustomCriteria, } = matrix; - const fixedRows: Array<{ key: keyof CriteriaEntry; label: string }> = [ - { key: 'paymentType', label: 'Условия оплаты' }, + const fixedRows: Array<{ key: keyof CriteriaEntry; label: string; required?: boolean }> = [ + { key: 'paymentType', label: 'Условия оплаты', required: true }, { key: 'deliveryType', label: 'Условия поставки' }, { key: 'deadline', label: 'Сроки изготовления и доставки' }, { key: 'sertificates', label: 'Сертификаты' }, @@ -45,8 +45,13 @@ export function CriteriaTable({ matrix }: Props): JSX.Element | null { {fixedRows.map(row => ( - {row.label} - {suppliersWithOffer.map(s => ( + + {row.label} + {row.required && *} + + {suppliersWithOffer.map(s => { + const isEmpty = !(criteriaMap[s.id]?.[row.key] ?? '').trim(); + return ( {!isReadOnly ? ( row.key === 'deliveryType' ? ( @@ -68,7 +73,7 @@ export function CriteriaTable({ matrix }: Props): JSX.Element | null { ) ) : ( -
+