diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.module.scss b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.module.scss index 9add949..b55c7ad 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.module.scss +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.module.scss @@ -781,13 +781,13 @@ display: flex; align-items: center; gap: 8px; - flex-wrap: nowrap; + flex-wrap: wrap; padding: 4px 0 8px; max-width: 30%; > :first-child { flex: 1 1 0; - min-width: 0; + min-width: 160px; } } diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.tsx b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.tsx index 2d635ca..8f70504 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.tsx +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.tsx @@ -1,7 +1,9 @@ import * as React from 'react'; +import { useMemo, useState } from 'react'; import { Button, Alert, TextInput, Toggle, Spinner, Dialog, Icon } from '@tnsu/ui-kit'; import { FormStep } from '../types/domain'; import { CtService } from '../../services/CtService'; +import { VendorDatabaseService, IVendorRecord } from '../../services/VendorDatabaseService'; import { CTFormData } from '../hooks/useCtForm'; import styles from './Step3ComparisonTable.module.scss'; import { InlineApprovalChain } from './InlineApprovalChain'; @@ -12,6 +14,7 @@ import { ComparisonMatrix } from './ComparisonMatrix'; import { CriteriaTable } from './CriteriaTable'; import { useAdataCheck } from './useAdataCheck'; import { useRecommendedPick } from './useRecommendedPick'; +import { VendorPickerDialog } from './VendorPickerDialog'; interface Props { service: CtService; @@ -42,13 +45,22 @@ export const Step3ComparisonTable: React.FC = ({ const matrix = useStep3Matrix({ service, formData, onSave, onNext, onStepComplete }); const { isReadOnly, newSupplierName, newSupplierCurrency, setNewSupplierName, setNewSupplierCurrency, - handleAddSupplier, translateUnits, setTranslateUnits, isSaving, saveError, setSaveError, handleSave, + handleAddSupplier, handleAddSupplierFromDatabase, translateUnits, setTranslateUnits, isSaving, saveError, setSaveError, handleSave, extraPositions, suppliersWithOffer, allPositions, computedOffers, selectedSupplierId, } = matrix; // Проверка контрагентов в Adata вынесена в хук. const adata = useAdataCheck(service, suppliersWithOffer); + // Модалка выбора поставщика из «База данных поставщиков» (см. VendorPickerDialog). + const [showVendorPicker, setShowVendorPicker] = useState(false); + const vendorDbService = useMemo(() => new VendorDatabaseService(service.webPartContext), [service]); + const handleAddVendorFromDatabase = (vendor: IVendorRecord): void => { + const id = handleAddSupplierFromDatabase(vendor); + if (vendor.bin) adata.seedBin(id, vendor.bin); + setShowVendorPicker(false); + }; + // Выбор рекомендованного поставщика по формуле 80% цена / 20% условия оплаты вынесен в хук. const recPick = useRecommendedPick(service, matrix); @@ -112,6 +124,10 @@ export const Step3ComparisonTable: React.FC = ({ Добавить потенциального поставщика + )} @@ -210,6 +226,14 @@ export const Step3ComparisonTable: React.FC = ({ + {showVendorPicker && ( + setShowVendorPicker(false)} + /> + )} + {ai.showAiPromptDialog && ( под обёртку. + :global(select) { + width: 100%; + } +} + +.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; +} + +.itemName { + font-weight: 600; + color: $text; +} + +.itemBin { + color: $muted; + font-size: 12px; + white-space: nowrap; +} + +.itemMeta { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; + font-size: 12px; + color: $muted; +} + +// ─── Карточка поставщика (read-only значения полей) ───────────────────────── +.viewValue { + min-height: 32px; + display: flex; + align-items: center; + padding: 6px 0; + color: $text; + font-size: 14px; + overflow-wrap: anywhere; +} diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/VendorPickerDialog.tsx b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/VendorPickerDialog.tsx new file mode 100644 index 0000000..832a26e --- /dev/null +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/VendorPickerDialog.tsx @@ -0,0 +1,176 @@ +import * as React from 'react'; +import { useState, useEffect, useMemo } from 'react'; +import { Dialog, Button, TextInput, Select, Spinner, Alert, FormGrid, FormField, Icon } from '@tnsu/ui-kit'; +import { VendorDatabaseService, IVendorRecord } from '../../services/VendorDatabaseService'; +import styles from './VendorPickerDialog.module.scss'; + +interface Props { + vendorDbService: VendorDatabaseService; + onAdd: (vendor: IVendorRecord) => void; + onClose: () => void; +} + +// Список большой (сотни поставщиков) — без поиска и без выбранного раздела показываем только +// первые N по алфавиту (сортировка уже приходит из VendorDatabaseService.listVendors), чтобы окно +// не было пустым при открытии, но и не рендерило все 500+ строк разом. +const INITIAL_LIMIT = 20; + +function ViewField({ label, value, full }: { label: string; value: string; full?: boolean }): JSX.Element { + return ( + +
{value || '—'}
+
+ ); +} + +// Диалог выбора поставщика из «База данных поставщиков» (Шаг 3 СТ): поиск + список → карточка +// поставщика с кнопкой добавления в таблицу сравнения (см. handleAddSupplierFromDatabase). +export const VendorPickerDialog: React.FC = ({ vendorDbService, onAdd, onClose }) => { + const [vendors, setVendors] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [query, setQuery] = useState(''); + const [category, setCategory] = useState(''); + const [selected, setSelected] = useState(null); + + useEffect(() => { + let cancelled = false; + setIsLoading(true); + vendorDbService + .listVendors() + .then((list) => { if (!cancelled) setVendors(list); }) + .catch((e) => { if (!cancelled) setError((e as Error).message || 'Ошибка загрузки базы поставщиков'); }) + .finally(() => { if (!cancelled) setIsLoading(false); }); + return () => { cancelled = true; }; + }, [vendorDbService]); + + const categoryOptions = useMemo(() => { + const unique = Array.from(new Set(vendors.map((v) => v.category).filter(Boolean))); + unique.sort((a, b) => a.localeCompare(b, 'ru')); + return unique.map((c) => ({ value: c, label: c })); + }, [vendors]); + + const isBrowsing = !query.trim() && !category; + + const filtered = useMemo(() => { + const q = query.trim().toLowerCase(); + const byCategory = category ? vendors.filter((v) => v.category === category) : vendors; + if (!q) return byCategory; + return byCategory.filter( + (v) => + v.name.toLowerCase().includes(q) || + v.bin.includes(q) || + v.category.toLowerCase().includes(q), + ); + }, [vendors, query, category]); + + // Без поиска и без раздела — режем список, чтобы не рендерить всё сразу; при любом сужении + // (текст и/или раздел) пользователь уже сам ограничил выборку — показываем всё, что нашлось. + const displayed = isBrowsing ? filtered.slice(0, INITIAL_LIMIT) : filtered; + + if (selected) { + return ( + + + + + } + > + + + + + + + + + + + {selected.productName && } + {selected.catalogLink && } + + + ); + } + + return ( + + {isLoading && ( +
+ Загрузка базы поставщиков… +
+ )} + + {!isLoading && error && {error}} + + {!isLoading && !error && ( + <> +
+
+ +
+
+