Несохранённые правки жили в памяти вкладки и пропадали при её закрытии, а
«Сохранить и продолжить» на Шаге 3 не пускает без победителя и километража.
Черновик пишет шаг в SharePoint так же, без проверок, и оставляет на шаге;
рядом — время последнего сохранения.
На Шаге 3 черновик не переносит поставщиков, добавленных на шаге, в общий
список: таблица держит их в своём, перенос задвоил бы колонку и обнулил бы
суммы. Повторные черновики дублей в SP не создают — SP Id уже запомнены.
На Шаге 2 черновик доступен, когда заданы курсы валют: они пишутся в общий
список «Курс Валют».
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Выбор победителя жил только в состоянии таблицы и попадал в данные формы
лишь по «Сохранить». После F5 сессия не знала, что выбор сняли, и форма
подставляла победителя из шапки СТ. Теперь выбор и снятие сразу пишутся в
данные формы, как статус и исключение поставщика, а победитель из шапки СТ
берётся только когда сессии нет (СТ открыта в другом браузере).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Кнопку «Выбран» нельзя было снять. А если победитель не выбран, «Сохранить»
не трогала winSupplier — в шапке СТ оставался прежний (так же при исключении
выбранного поставщика, #1). Теперь «Выбрать» — переключатель, победитель
обязателен: «Сохранить и продолжить» без него не пускает, проверка — до
диалога комментария ИИ. При открытии СТ без сессии выбор восстанавливается из
winSupplier шапки, иначе в другом браузере пришлось бы выбирать заново.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ввод чисел разбирался parseFloat, который читает до первого пробела: цену
«100 000», скопированную из КП, таблица считала как 100. Все поля ввода СТ
(цена, цена в ₸, количество, километраж, курс валюты) разбирает одна функция:
обычные и неразрывные пробелы, «275 293,50», «1,234,567.89», хвост «₸».
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Раньше поиск фильтровал в браузере всё, что загружено за период: чтобы найти
старую потребность, приходилось поднимать период и ждать загрузки сотен
потребностей ради одной. Теперь номер уходит в API (?requestNumber=, часть
номера, в пределах периода) после паузы в наборе — приходят только совпадения.
Попутно: позиции, выбранные до смены списка (поиск, другой период), больше не
пропадают при «Сохранить» — карта позиций копится, а не пересобирается из
текущего списка.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Без них списки, пересозданные по манифестам, ломали бы шаг 1 СТ
(requestPositionId), сохранение офферов (offerFileId), добавление поставщика
из базы (контакты в «Поставщики») и позиции ОИД (RequestCode, RequestUrl).
Названия и типы — как в живых списках /sites/sed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Манифесты отстают от живых списков, когда колонки добавляют руками или кодом,
и пересоздание по ним ломает функции. Скрипт только читает и показывает, чего
нет в манифесте, чего нет на сайте и где отличаются тип, варианты или лукап.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Шаг 3 СТ ОМТС: крестик исключает поставщика с обязательной причиной, цены
хранятся в «Исключённые предложения», «Вернуть» возвращает колонку целиком.
Решение и варианты — docs/adr/0001. Проверено в воркбенче на СТ 372;
открытие в другом браузере и PDF-отчёт — после выкатки.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Раньше крестик на Шаге 3 удалял поставщика вместе с ценами, а «Сохранить»
стирал его офферы в SP — случайно удалённого было не вернуть.
Теперь крестик спрашивает обязательную причину и скрывает колонку: таблица,
итоги, рекомендация, ИИ и Adata считаются без него, выбор с исключённого
снимается. Цены остаются в правках и сессии, «Сохранить» пишет их в
«Исключённые предложения», при открытии СТ они подтягиваются. Под таблицей —
список исключённых с кнопкой «Вернуть»; в режиме просмотра скрыт константой.
Повторная загрузка КП на Шаге 2 исключение не снимает.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CtService: loadExcludedOffers / replaceExcludedOffers — сначала новые строки,
потом прежние в корзину, чтобы сбой записи не терял исключённых; дубли отсеивает
чтение. removeCTPositions убирает и исключённые строки снятых позиций.
Преобразования строк SP ↔ колонки и офферы формы — отдельно, с тестами.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Чистые функции без SharePoint: тело записи, разбор элемента, сборка колонок
(поставщик + файл КП) и строка-маркер для поставщика без цен. Один модуль
на СТ, ОИД и КПС, чтобы правила хранения не разошлись между формами.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Рабочий app-only сертификат для /sites/sed лежит файлом с паролем, а не в
хранилище CurrentUser\My — скрипт умел только по отпечатку.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Полный прогон на рабочем сайте перезаписывает варианты Choice живых списков
значениями из манифеста, а манифесты отстали от живых списков (#2). Новые
списки «Исключённые предложения» создаём точечно, не трогая остальные.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Исключённые поставщики хранятся отдельно от «Оценки поставщиков», чтобы
отчёт СТ, INCORE и потоки их не видели. Колонки — копия офферов своей формы
плюс СТ и причина; файл КП — числом, лукап на библиотеку не читается через REST.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Один разработчик без ревью: ветка нужна не всегда, а когда защищает main от
полуготового кода. Мелкие фиксы — прямо в main.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Требования приходят из чата и меняются — фиксируем, что просили, что решили
и что выехало: регламент в docs/PROCESS.md, ADR 0001 по исключению
поставщиков вместо удаления, шаблон задачи для Gitea, пустой CHANGELOG.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Тот же промпт вернул другую структуру: массив из одного объекта, список
поставщиков suppliers_analysis вместо suppliers_evaluation, плюсы/минусы
строкой вместо массива — и снова показывался сырой JSON. Разбор стал
терпимым: объект или массив объектов, список поставщиков — любое поле-
массив объектов с supplier, плюсы/минусы — строка или массив, плюс
recommendation.
Поток «Комментарий ИИ» стал отвечать JSON {summary, suppliers_evaluation,
conclusion} вместо текста — в шаге 3 и в SP (а значит и в PDF) лежал
сырой JSON. aiCommentToText разворачивает структуру в текст (вывод,
плюсы/минусы по поставщикам, итог) без Markdown-разметки; прежние
форматы (текст, {comment}) — как раньше. Подключено в СТ, ОИД и КПС —
при ответе потока и при чтении из SP, чтобы старые записи с JSON тоже
показывались текстом.
Когда в КП нет поля (например, сертификатов), поток отдаёт null, а
String(null) давал «null» — он показывался в «Сертификатах» шага 3 и
сохранялся в «Критерии». Разбор ответа вынесен в mapFlowResult: null —
«не распознано», как уже сделано в КПС и ОИД.
Шапку потребности старше периода собираем из позиций СТ, а вида работ
потребности там нет — подставлялся smrType позиции («ГКЛ перегородки»
вместо «СТЕНЫ И ПЕРЕГОРОДКИ»). Теперь пусто, в таблице «—».
В новой вкладке (из реестра) сессии нет, и у черновика без маршрута
степпер давал кликнуть только шаг 1 — до таблицы приходилось идти через
«Сохранить и продолжить» на каждом шаге. Теперь доступные шаги — по
данным СТ из SP: позиции → 2, КП → 3, маршрут → 4.
Шаги рендерились сразу, а СТ подгружалась позже. Шаг 1 запоминал выбор
при появлении — пустой, и при открытии из реестра показывал «Выберите
позиции», пока не обновить страницу. Теперь до загрузки — спиннер,
ошибка загрузки показывается в форме, а не только в консоли.
API /Requests/{oid}/positions отдаёт позиции без id и в разном порядке
(lineNumber — номер после сортировки по наименованию). id вида
GUID__номер указывал на разные позиции: на шаге 1 снимали одну — из СТ
удалялась другая.
Ключ — GUID потребности + хеш наименования, R66, АГСК, ед. изм.,
количества и вида СМР; одинаковые строки получают номер совпадения.
Метка h- отличает ключ от старых номерных — те ни с чем не сопоставятся.
Бэкенд extractPDFText теряет расширение у PDF с кириллицей в имени
(OCR 415 «unsupported file type ''») и отвечает 500 — КП падало при
каждой первой загрузке. Повторная загрузка «проходила» лишь потому, что
текст бэкенд успевал записать, и фронт пропускал извлечение.
Имя в библиотеке КП транслитерируется, остальные символы — в «_».
Результат детерминированный: поиск уже загруженного файла и проверка
дублей работают как раньше.
Позиции СТ из SP не были связаны с потребностями (id = SP Id), поэтому
на шаге 1 при «Выбрано: N» ни одна строка не была отмечена, «Только
выбранные» показывал пустой список, а снять выбор было негде.
- в «Позиции СТ» пишется requestPositionId (GUID потребности__номер),
шаг 1 сопоставляет выбор по нему; id позиции остаётся SP Id, к нему
привязаны предложения;
- потребности, чьи позиции уже в СТ, показываются и вне выбранного
периода (позиции догружаются сразу);
- проект восстанавливается из шапки СТ — «Сохранить и продолжить»
молча ничего не делала и снимался запрет на чужой проект.
У СТ, созданных до колонки, связи нет — их позиции в таблице не видны.
По «Сохранить и продолжить» выбранный поставщик пишется в лукап winSupplier: на шаге 4 меняется
только статус, а поставщик мог быть как обычной колонкой, так и добавленным на шаге 3.
recycle() слал POST без Content-Type с пустым odata-version — SPFx (jsonRequest) сам бросал
«requires the OData-Version header to be 3.0 or 4.0» до запроса. writeRoute общий: повторная
отправка после доработки/перезапуска ломалась во всех трёх формах; в КПС тот же recycle.
Раньше ?ctid= (ссылки потока, реестр не-черновиков) включал режим согласующего любому, кто открыл,
и режим залипал в сессии: автор СТ «На доработке» по ссылке из письма не мог править.
Теперь getCtAccess: автор правит в draft…formed и «На доработке»; согласующий и автор на
согласовании/после — просмотр с ценами на шаге 3; посторонний — просмотр без цен.
ctid/ctId равнозначны, approvalSent/isNotificationLink убраны.
Вызовы syncNoOfferSuppliers/replaceCriteria из п.3 и п.4 претензий не попали в коммиты
f5853b1 и 2b102b8 (вошли только импорты): без них согласующий и PDF не видели поставщиков
без КП и доп. поля, если шаг 2 прошли крошками.
Без активного согласующего форма отправляла потоку последнюю строку маршрута — при возврате на
доработку это мог быть следующий согласующий в статусе «Ждёт очереди», и в письме инициатору
нельзя было указать, кто вернул и с каким комментарием. Теперь (pickRowToNotify): активный
согласующий → его строка; иначе строка «На доработке» или «Отклонено»; иначе последняя.
Минимум по позиции считался, но подсветку получали только строка «Итого по позиции» у комплекта
и строки доп. расходов — обычная ячейка «Сумма, ₸» не подсвечивалась никогда, и зелёный был виден
только в общем итоге. Итог комплекта к тому же пересчитывался отдельно (без override цены в ₸ и
кол-ва позиции по умолчанию) и сравнивался строгим ===, поэтому минимум терялся на копейках.
Теперь суммы «поставщик × позиция» — один источник (sumPositionTotals), сравнение с допуском
(isMinPositionTotal), минимальная сумма зелёная в каждой позиции.
Вернуть СТ инициатору на исправление было нельзя — только «Отклонить». Теперь у согласующего
(шаг 4 и шаг 3 по ссылке из Teams) есть «На доработку» с обязательным комментарием. Строка и СТ
получают статус «На доработке», СТ сразу открыта инициатору для правки, на шаге 4 — плашка, кто
вернул и что исправить. Повторная отправка переписывает маршрут целиком — согласование с начала.
«Отклонить» с перезапуском работает как раньше.
Решение rework добавлено в общий ApproverService/DecisionPanel, но кнопка включена только в СТ ОМТС
(allowRework); в ОИД/КПС статус недостижим. В SP в Choice status «Сравнительная таблица» и
«Согласование СТ» добавлено «На доработке» (и «Отклонено» в статус СТ, которое форма уже писала).
Уведомление инициатору при доработке — в потоке Power Automate (вне репо).
КП загружались в корень библиотеки под исходным именем с перезаписью: «КП.pdf» одной СТ затирал
«КП.pdf» другой, и согласующий первой СТ открывал чужое КП. Теперь файл хранится как
«<id СТ>_<имя>» (buildKpStorageName); это имя идёт в поиск уже загруженного файла, поток, список
«Загруженные КП» и ссылку на файл. Повторная загрузка того же файла в эту же СТ по-прежнему
распознаётся.
Согласующий по ссылке из Teams попадает сразу на шаг 3, а ссылки на КП были только на шаге 2 —
открыть КП при согласовании было негде. Теперь под таблицей сравнения блок «Коммерческие
предложения» (поставщик → файл). Показывается тем же, кому видны цены: в КП тоже цены.
Участие в цепочке согласования сверялось по ФИО — при другом написании согласующему скрывались
цены (и теперь КП); сверяем по почте без учёта регистра, ФИО — запасной вариант.
Несоответствие единиц было видно только по незаметной «*» и только при включённом тумблере.
Теперь «Ед. изм.» поставщика подсвечивается жёлтым, если отличается от заявки (подсказка «В
заявке: кг, у поставщика: т»). С включённым тумблером единица, кол-во и цены подсвечиваются
голубым, если пересчитаны в ед. заявки, и красным, если пересчитать нельзя (шт ↔ кг). Статус —
чистая функция getUnitStatus; у тумблера — легенда цветов вместо «*».
Тумблер знал только массу, длину, площадь и объём в одном написании. «Штука» в заявке и «шт.»
в КП считались разными незнакомыми единицами — тумблер помечал их «пересчёт невозможен», и
казалось, что он ничего не делает. Теперь единица приводится к канонической через словарь
синонимов (шт/Штука/ед, кг/Килограмм, л/Литр, кв.м./м², пог.м/м.п. и т.д.), добавлены штучные
единицы (компл, упак, пара, рулон, лист, мешок — между собой не пересчитываются). Заодно
«кв.м.» с точкой на конце снова распознаётся: точку срезали после поиска синонимов.
Значение своей строки хранится в comment, а это в «Критерии» ОМТС многострочное поле с
форматированием: SharePoint отдаёт его как <div class="ExternalClass…">да</div>, и по ссылке
показывалась разметка. При загрузке теперь остаётся чистый текст (plainTextFromSp).
Ручные правки условий и свои строки («Добавить условие») жили только в браузере: saveCriteria
не вызывался, и согласующий, PDF и автор в новой вкладке видели лишь то, что распознал ИИ.
Теперь по «Сохранить» шага 3 «Критерии» этой СТ заменяются текущими (replaceCriteria), а при
открытии грузятся по СТ, а не по поставщику из всех СТ (loadCriteriaForCt). Свои строки
хранятся в новой колонке criteriaLabel, значение — в comment, как в ОИД.
По просьбе заказчика: «Условия оплаты» — готовые варианты (частые формулировки из SP) или свой
вариант; «Сертификаты» — Да/Нет; «Условия поставки» можно сменить и после заполнения ИИ
(раньше заполненное показывалось текстом без выбора).
Поставщиков без КП убрали из колонок (64973a2), но взамен нигде не показывали. Теперь на
шаге 3 под «Дополнительной информацией» — список «Поставщики, не предоставившие КП» с
комментарием. «Не получены КП» сохраняются с привязкой к СТ (колонка CT была в списке, но не
заполнялась, а дедуп шёл по названию на все СТ) и синхронизируются по «Сохранить» на шагах
2 и 3 — удалённые убираются; по ссылке согласующий видит список из SP. Комментарий больше
не теряется при возврате на шаг 2.
Вернулись на шаг 1 и нажали «Сохранить и продолжить» — создавалась ещё одна СТ (№10 → №11) с
теми же позициями. Предложения оставались в прежней СТ, а форма переключалась на новую,
которая по ссылке открывалась без поставщиков. Теперь, если СТ уже создана, позиции
обновляются: новые дописываются, снятые уходят в корзину вместе с их предложениями, проект
в шапке меняется при смене проекта (planStep1Save).
Заодно при возврате на шаг 1 прежний выбор виден и сохраняется: позиции нераскрытых
потребностей берутся из формы, выбранные считаются по id — раньше галочки не стояли,
«Только выбранные» был пуст, «Сохранить» неактивна.
При открытии СТ по ссылке (согласующий) или в новой вкладке предложения собирались из SP без
поля supplierPosition, и шаг 3 подставлял вместо названия из КП название позиции заявки.
Согласующий не видел, что поставщик предложил другую модель или аналог, а автор, сохранив
шаг 3 без сессии, записывал в SP название заявки поверх названия поставщика. Теперь название
читается из SP и переносится (сборка офферов вынесена в buildOffersFromSpRecords).
Окно на каждый второй КП поставщика при пакетной загрузке (6 файлов — 6 окон) было непонятно.
Теперь вопросов при загрузке нет: каждый КП — своя колонка. В «Загруженных КП» файлы одного
поставщика сгруппированы с пометкой «N КП — сравниваются отдельными колонками», а корзина
(и крестик у обработанного файла) убирает КП из сравнения — колонку, цены и записи этого
файла в SP, с подтверждением. Раньше корзина только прятала строку, колонка оставалась.
Так заменяют исправленный КП: загружают новый, старый убирают.
Заменяет окно из 744b603 (п.2).