<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ru">
	<id>https://wiki.chemsoft.ru/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Arhidiman</id>
	<title>Химсофт Вики - Вклад [ru]</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.chemsoft.ru/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Arhidiman"/>
	<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php/%D0%A1%D0%BB%D1%83%D0%B6%D0%B5%D0%B1%D0%BD%D0%B0%D1%8F:%D0%92%D0%BA%D0%BB%D0%B0%D0%B4/Arhidiman"/>
	<updated>2026-04-17T21:14:20Z</updated>
	<subtitle>Вклад</subtitle>
	<generator>MediaWiki 1.45.1</generator>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2382</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2382"/>
		<updated>2026-04-03T06:48:03Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Деление данных. copy-journal-record */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { MessageBoxResults } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { FetchApiClient } from &amp;quot;../../src/servivces/worker/api/ApiClient&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
&lt;br /&gt;
// Конфигурация аддона:&lt;br /&gt;
// - Поле &amp;quot;Дата регистрации&amp;quot; — контрольная точка для расчёта срока&lt;br /&gt;
// - expirationLimit: 10 дней — максимальный допустимый срок хранения пробы&lt;br /&gt;
// - journalRecordId: 201 — запись из журнала &amp;quot;Лаб. воздуха / Анализы завершены&amp;quot;&lt;br /&gt;
const config = {&lt;br /&gt;
    dateField: &#039;Дата регистрации&#039;,&lt;br /&gt;
    expirationLimit: 10,&lt;br /&gt;
    journalRecordId: 201,&lt;br /&gt;
    integrationResult: { message: &#039;Срок регистрации пробы не истёк!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Сообщения для пользовательских уведомлений и ошибок&lt;br /&gt;
const messages = {&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    // Успех: информирует пользователя, что срок не вышел&lt;br /&gt;
    success: (recordId) =&amp;gt; `Срок регистрации пробы не истёк! Id записи ${recordId}`,&lt;br /&gt;
    // Ошибка: превышен лимит хранения&lt;br /&gt;
    expired: (limit, recordId) =&amp;gt; `Срок регистрации пробы истёк!\nПрошло более ${limit} дней с момента регистрации. Id записи: ${recordId}`,&lt;br /&gt;
    // Ошибка: отсутствует дата в нужном поле&lt;br /&gt;
    dateField: (dateField, stageName, journalRecordId) =&amp;gt; [&lt;br /&gt;
        `Не заполнено либо отсутствует поле &amp;quot;${dateField}&amp;quot;`,&lt;br /&gt;
        `в этапе &amp;quot;${stageName || &#039;&#039;}&amp;quot;.\nId записи: ${journalRecordId}`&lt;br /&gt;
    ].join(&#039; &#039;)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Централизованные обработчики ошибок — обеспечивают единообразие и чистоту основной логики&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    // Вызывается, если поле с датой не заполнено&lt;br /&gt;
    dateError: (dateField, stageName, journalRecordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.dateField(dateField, stageName, journalRecordId))&lt;br /&gt;
    },&lt;br /&gt;
    // Вызывается при превышении срока хранения&lt;br /&gt;
    expirationError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.expired(config.expirationLimit, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Извлекает socketId из queryParams — необходим для отправки диалогов в GUI&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Переводит дату в количество дней с начала эпохи (для арифметики дат)&lt;br /&gt;
const getDaysByDate = (date: Date) =&amp;gt; {&lt;br /&gt;
    const dayMs = 60 * 60 * 24 * 1000&lt;br /&gt;
    const timestamp = date.getTime()&lt;br /&gt;
    return timestamp / dayMs&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Формирует конфиг диалога: тип, заголовок и сообщение для отображения в GUI&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Рассчитывает, сколько дней прошло с момента регистрации пробы&lt;br /&gt;
const getExpirationOffset = (registrationDate: string) =&amp;gt; {&lt;br /&gt;
    const regDate = new Date(registrationDate)&lt;br /&gt;
    const today = new Date()&lt;br /&gt;
    return getDaysByDate(today) - getDaysByDate(regDate)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Показывает пользователю сообщение об ошибке&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
// Показывает пользователю уведомление об успешной проверке&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Основная функция аддона:&lt;br /&gt;
 * Проверяет, не истёк ли срок хранения пробы (с момента &amp;quot;Дата регистрации&amp;quot;).&lt;br /&gt;
 * &lt;br /&gt;
 * Логика:&lt;br /&gt;
 * 1. Получает значение поля &amp;quot;Дата регистрации&amp;quot; из записи журнала.&lt;br /&gt;
 * 2. Если дата не указана — ошибка.&lt;br /&gt;
 * 3. Если с даты регистрации прошло &amp;gt;= 10 дней — ошибка.&lt;br /&gt;
 * 4. Иначе — успех, показываем подтверждение.&lt;br /&gt;
 */&lt;br /&gt;
async function main({ queryParams, ...apiInstance }) {&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        // Получаем доступ к данным записи журнала&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        // Извлекаем значение поля &amp;quot;Дата регистрации&amp;quot;&lt;br /&gt;
        const date = journalRecordManager.getFieldValue(config.dateField)&lt;br /&gt;
&lt;br /&gt;
        // Если дата не заполнена — прерываем с пояснением&lt;br /&gt;
        if (!date) {&lt;br /&gt;
            errorHandlers.dateError(config.dateField, journalRecordManager?.data?.stage?.stage?.name, config.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Считаем, сколько дней прошло с регистрации&lt;br /&gt;
        const expirationOffset = getExpirationOffset(date as string)&lt;br /&gt;
&lt;br /&gt;
        // Если срок хранения превышен — ошибка&lt;br /&gt;
        if (expirationOffset &amp;gt;= config.expirationLimit) {&lt;br /&gt;
            errorHandlers.expirationError()&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Всё в порядке — информируем пользователя&lt;br /&gt;
        await showSuccessMessage(messages.success(config.journalRecordId), socketId)&lt;br /&gt;
&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        // При любой ошибке — показываем её в модальном окне&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordIndicatorResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
// Описание свойств показателя (индикатора), которые нужно проверить&lt;br /&gt;
// Каждое свойство имеет метку (для отображения пользователю) и геттер значения&lt;br /&gt;
type IndicatorProp = { label: string, get: () =&amp;gt; unknown }&lt;br /&gt;
type IndicatorPropsConfig = Record&amp;lt;string, IndicatorProp&amp;gt;&lt;br /&gt;
&lt;br /&gt;
// Конфигурация полей, которые проверяются у каждого показателя&lt;br /&gt;
// Например: &amp;quot;Исполнители&amp;quot;, &amp;quot;Методики&amp;quot;, &amp;quot;Средний результат&amp;quot; и т.д.&lt;br /&gt;
const indicatorPropsConfig = (indicator): IndicatorPropsConfig =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        executors: {&lt;br /&gt;
            label: &#039;Исполнители&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.executors&lt;br /&gt;
        },&lt;br /&gt;
        methodic: {&lt;br /&gt;
            label: &#039;Методики&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.methodics&lt;br /&gt;
        },&lt;br /&gt;
        averageResult: {&lt;br /&gt;
            label: &#039;Средний результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageValue&lt;br /&gt;
        },&lt;br /&gt;
        averageRoundedValue: {&lt;br /&gt;
            label: &#039;Средний округлённый результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageRoundedValue&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Конфигурация аддона:&lt;br /&gt;
// - indicatorsList: список обязательных показателей (например, &amp;quot;Аммоний-ион&amp;quot;)&lt;br /&gt;
// - journalRecordId: 144 — тестовая запись (&amp;quot;Мк / Тест_Ушакова&amp;quot;)&lt;br /&gt;
const config = {&lt;br /&gt;
    indicatorsList: [&lt;br /&gt;
        &#039;Аммоний-ион&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144,&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Сообщения для пользовательских уведомлений и ошибок&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyIndicators: (recordId) =&amp;gt; `в записи с Id ${recordId} отсутствуют показатели`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
    indicators: (indicators: string[], recordId) =&amp;gt; `Показатели ${indicators.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} отсутствуют в записи с ID ${recordId}`,&lt;br /&gt;
    // Формирует детализированное сообщение о незаполненных полях внутри показателей&lt;br /&gt;
    emptyIndicatorsProps: (props: Record&amp;lt;string, string[]&amp;gt;, recordId) =&amp;gt; {&lt;br /&gt;
        const propsString = Object.keys(props).map(prop =&amp;gt; !isEmptyArray(props[prop]) ? `${prop}: ${props[prop].map(p =&amp;gt; `&amp;quot;${p}&amp;quot;`).join(&#039;, &#039;)}` : &#039;&#039;).join(&#039;; &#039;)&lt;br /&gt;
        return `В записи с Id ${recordId} отсутствуют либо не заполнены следующие поля у показателей: ${propsString}`&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Централизованные обработчики ошибок — обеспечивают единообразие и чистоту основной логики&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    indicatorsError: (recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyIndicators(recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFoundIndicators: (indicators: string[], recordId: number) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.indicators(indicators, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFilledIndicatorProps: (props: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyIndicatorsProps(props, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Формирует конфиг диалога для отправки в GUI: тип, заголовок и сообщение&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Извлекает socketId из queryParams — необходим для связи с интерфейсом&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Показывает пользователю модальное окно с ошибкой&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
// Показывает уведомление об успешной проверке&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
// Универсальная проверка: является ли массив пустым (с учётом типа)&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(arr) &amp;amp;&amp;amp; arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Проверяет, есть ли у записи хотя бы один показатель&lt;br /&gt;
const hasIndicators = (journalRecordManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
    return Array.isArray(indicators) &amp;amp;&amp;amp; indicators.length &amp;gt; 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Определяет, какие из требуемых показателей отсутствуют в записи&lt;br /&gt;
const getNotFoundIndicators = (indicators: JournalRecordIndicatorResponse[], indicatorsList = config.indicatorsList) =&amp;gt; {&lt;br /&gt;
    return indicatorsList.filter(ind =&amp;gt; &lt;br /&gt;
        indicators.every(ljInd =&amp;gt; ljInd?.indicator?.name?.toLowerCase() !== ind.toLowerCase())&lt;br /&gt;
    )&lt;br /&gt;
} &lt;br /&gt;
&lt;br /&gt;
// Собирает список незаполненных полей для каждого показателя&lt;br /&gt;
// Возвращает объект: { &amp;quot;Аммоний-ион&amp;quot;: [&amp;quot;Исполнители&amp;quot;, &amp;quot;Средний результат&amp;quot;] }&lt;br /&gt;
const getNotFilledResultsFields = (indicators: JournalRecordIndicatorResponse[]): Record&amp;lt;string, string[]&amp;gt; =&amp;gt; {&lt;br /&gt;
    return indicators.reduce((acc, indicator) =&amp;gt; {&lt;br /&gt;
        const config = indicatorPropsConfig(indicator)&lt;br /&gt;
        return {&lt;br /&gt;
            ...acc,&lt;br /&gt;
            [indicator?.indicator?.name]: Object.keys(config).reduce((acc, prop) =&amp;gt; {&lt;br /&gt;
                if (!config[prop].get()) return [...acc, config[prop].label]&lt;br /&gt;
                return acc&lt;br /&gt;
            }, [] as string[])&lt;br /&gt;
        }&lt;br /&gt;
    }, {}) as Record&amp;lt;string, string[]&amp;gt;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Проверяет, есть ли хотя бы один показатель с незаполненными полями&lt;br /&gt;
const hasNotFilledProps = (indicatorsProps: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
    return Object.keys(indicatorsProps).some(indicator =&amp;gt; !isEmptyArray(indicatorsProps[indicator]))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Проверяет, есть ли отсутствующие показатели в списке&lt;br /&gt;
const hasNotFoundIndicators = (indicators: string[]) =&amp;gt; !isEmptyArray(indicators)&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Основная функция аддона:&lt;br /&gt;
 * Проверяет заполненность:&lt;br /&gt;
 * 1. Наличие указанных показателей (например, &amp;quot;Аммоний-ион&amp;quot;).&lt;br /&gt;
 * 2. Заполненность внутренних полей каждого показателя (исполнители, методики и т.д.).&lt;br /&gt;
 * &lt;br /&gt;
 * При обнаружении проблем — прерывает выполнение и показывает детализированную ошибку.&lt;br /&gt;
 */&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        // Проверка: есть ли вообще показатели в записи?&lt;br /&gt;
        if (!hasIndicators(journalRecordManager)) {&lt;br /&gt;
            errorHandlers.indicatorsError(journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
&lt;br /&gt;
        // Проверка: все ли требуемые показатели присутствуют?&lt;br /&gt;
        const notFoundIndicators = getNotFoundIndicators(indicators)&lt;br /&gt;
        if (hasNotFoundIndicators(notFoundIndicators)) { &lt;br /&gt;
            errorHandlers.notFoundIndicators(notFoundIndicators, journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Проверка: все ли поля у показателей заполнены?&lt;br /&gt;
        const emptyIndicatorProps = getNotFilledResultsFields(indicators)&lt;br /&gt;
        if (hasNotFilledProps(emptyIndicatorProps)) {&lt;br /&gt;
            errorHandlers.notFilledIndicatorProps(emptyIndicatorProps)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Все проверки пройдены — информируем пользователя&lt;br /&gt;
        showSuccessMessage(messages.success, socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        // При любой ошибке — показываем её в модальном окне&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-record-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Список. Целое число&#039;,&lt;br /&gt;
        &#039;Строка&#039;,&lt;br /&gt;
        &#039;Целое число&#039;,&lt;br /&gt;
        &#039;Вещественное число&#039;,&lt;br /&gt;
        &#039;Логический тип&#039;,&lt;br /&gt;
        &#039;Дата&#039;,&lt;br /&gt;
        &#039;Время&#039;,&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144,  // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFilledFields = (fields: string[], journalRecordsManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    console.log(journalRecordsManager.data.attributes, &#039;data.attributes&#039;)&lt;br /&gt;
    return fields.filter(f =&amp;gt; !journalRecordsManager.getFieldValue(f))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const emptyFields = getNotFilledFields(config.requiredFields, journalRecordManager)&lt;br /&gt;
&lt;br /&gt;
        if (isEmptyArray(emptyFields)) {&lt;br /&gt;
            showSuccessMessage(messages.success, socketId)&lt;br /&gt;
            return config.integrationResult&lt;br /&gt;
        } else errorHandlers.fieldsError(emptyFields, config.journalRecordId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-sample-moving.png|frame]]&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { JournalsRecordRouteController } from &amp;quot;../../src/servivces/worker/api/controllers/journals-record-route&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRouteDto } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
// Конфигурация аддона:&lt;br /&gt;
// - journalRecordId: 114 — запись из журнала &amp;quot;Лаб. воздуха / Анализы завершены&amp;quot;&lt;br /&gt;
// Аддон анализирует маршрут движения пробы по этапам ЛЖ&lt;br /&gt;
const config = {&lt;br /&gt;
    journalRecordId: 114&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Сообщения для отображения пользователю&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    // Формирует строку с маршрутом пробы: этапы через &amp;quot; -&amp;gt; &amp;quot;&lt;br /&gt;
    route: (routes: JournalRecordRouteDto[], recordId: number) =&amp;gt; &lt;br /&gt;
        `Маршрут пробы: ${routes.map(r =&amp;gt; `&amp;quot;${r.stageName}&amp;quot;`).join(&#039; -&amp;gt; &#039;)}. Id записи ЛЖ: ${recordId}`,&lt;br /&gt;
    // Сообщение, если проба не перемещалась между этапами&lt;br /&gt;
    notMoved: (recordId: number) =&amp;gt; &lt;br /&gt;
        `Движение пробы не происходило. Id записи ЛЖ: ${recordId}`&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Обработчики ошибок — централизованно выбрасывают исключения с понятными сообщениями&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Формирует конфиг диалога для GUI: тип (confirm), заголовок и текст&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Извлекает socketId из параметров запроса — необходим для отправки ответа в интерфейс&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Показывает пользователю модальное окно с ошибкой&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
// Показывает информационное сообщение (успех или статус)&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
// Определяет, перемещалась ли проба: если более одного этапа — значит, было движение&lt;br /&gt;
const isSampleMoved = (moves: JournalRecordRouteDto[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(moves) &amp;amp;&amp;amp; moves.length &amp;gt; 1&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Основная функция аддона:&lt;br /&gt;
 * Получает маршрут пробы по этапам лабораторного журнала и отображает его пользователю.&lt;br /&gt;
 * &lt;br /&gt;
 * Логика:&lt;br /&gt;
 * 1. Проверяет наличие параметров и подключение клиента (socketId).&lt;br /&gt;
 * 2. Запрашивает историю перемещений пробы через API.&lt;br /&gt;
 * 3. Если проба прошла более одного этапа — отображает маршрут.&lt;br /&gt;
 * 4. Если осталась на одном этапе — сообщает, что движения не было.&lt;br /&gt;
 */&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        // Контроллер для работы с маршрутом записи (история этапов)&lt;br /&gt;
        const journalsRecordRouteController = new JournalsRecordRouteController(apiInstance)&lt;br /&gt;
        // Получаем список этапов, через которые прошла проба&lt;br /&gt;
        const sampleMoves = await journalsRecordRouteController.getJournalRecordRoute(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (isSampleMoved(sampleMoves)) {&lt;br /&gt;
            // Проба перемещалась — показываем полный маршрут&lt;br /&gt;
            showSuccessMessage(messages.route(sampleMoves, config.journalRecordId), socketId)&lt;br /&gt;
        } else {&lt;br /&gt;
            // Проба не покидала исходный этап&lt;br /&gt;
            showSuccessMessage(messages.notMoved(config.journalRecordId), socketId)&lt;br /&gt;
        }&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        // При любой ошибке (сеть, доступ, данные) — показываем её пользователю&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-user-rights.png|frame]]&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import { &lt;br /&gt;
    PermissionController, &lt;br /&gt;
    PermissionResource, &lt;br /&gt;
    PermissionRwAccess&lt;br /&gt;
} from &amp;quot;../../src/servivces/worker/api/controllers/permisson&amp;quot;&lt;br /&gt;
&lt;br /&gt;
import { TableBuilder } from &amp;quot;../../src/gui/builder&amp;quot;&lt;br /&gt;
import type { DialogType, TableConfig } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRouteDto } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
// Отображение типов доступа в понятные пользователю названия&lt;br /&gt;
const accessNamesMap = {&lt;br /&gt;
    [PermissionRwAccess.EXEC]: &#039;Выполнение&#039;,&lt;br /&gt;
    [PermissionRwAccess.READ]: &#039;Чтение&#039;,&lt;br /&gt;
    [PermissionRwAccess.WRITE]: &#039;Запись&#039;,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Список ресурсов, права на которые необходимо проверить&lt;br /&gt;
const requiredResources = [&lt;br /&gt;
    PermissionResource.ANALYSIS_OBJECT, &lt;br /&gt;
    PermissionResource.METHODOLOGY,&lt;br /&gt;
    PermissionResource.JOURNAL_RECORD&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
// Отображение идентификаторов ресурсов в читаемые названия для отображения в таблице&lt;br /&gt;
const resourcesNamesMap = {&lt;br /&gt;
    [PermissionResource.ANALYSIS_OBJECT]: &#039;Объекты анализа&#039;, &lt;br /&gt;
    [PermissionResource.METHODOLOGY]: &#039;Методики&#039;, &lt;br /&gt;
    [PermissionResource.JOURNAL_RECORD]: &#039;Записи ЛЖ&#039;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Формирует конфигурацию таблицы с правами доступа:&lt;br /&gt;
 * - Для каждого ресурса проверяются все типы доступа (чтение, запись, выполнение).&lt;br /&gt;
 * - Возвращает готовую конфигурацию таблицы для отображения в GUI.&lt;br /&gt;
 */&lt;br /&gt;
const getAccessRightsTable = async (tableBuilder: TableBuilder, permissionController: PermissionController): Promise&amp;lt;TableConfig&amp;gt; =&amp;gt; {&lt;br /&gt;
    // Получаем список строк: комбинация &amp;quot;ресурс + тип доступа + наличие права&amp;quot;&lt;br /&gt;
    const rows = (await Promise.all(requiredResources.map(async (source: PermissionResource) =&amp;gt; {&lt;br /&gt;
            const sourceCombinations = await Promise.all(Object.keys(PermissionRwAccess).map(async (action) =&amp;gt; {&lt;br /&gt;
                const access = await permissionController.getSourcePermission(source, PermissionRwAccess[action])&lt;br /&gt;
                return { &lt;br /&gt;
                    source: resourcesNamesMap[source], &lt;br /&gt;
                    action: accessNamesMap[PermissionRwAccess[action]], &lt;br /&gt;
                    access: access.hasPermission &lt;br /&gt;
                }&lt;br /&gt;
            }))&lt;br /&gt;
            return sourceCombinations&lt;br /&gt;
        }   &lt;br /&gt;
    ))).flat()&lt;br /&gt;
&lt;br /&gt;
    // Строим таблицу с помощью TableBuilder&lt;br /&gt;
    const table = tableBuilder&lt;br /&gt;
        .name(&#039;simpleTable&#039;)&lt;br /&gt;
        .label(&#039;Таблица&#039;)&lt;br /&gt;
        .isAddable(false)           // Нельзя добавлять строки&lt;br /&gt;
        .actionsCol(false)          // Скрыть колонку действий&lt;br /&gt;
        .addColumn(&#039;Ресурс&#039;, &#039;source&#039;, false)&lt;br /&gt;
        .config(&#039;input&#039;, { name: &#039;source&#039;, rules: [{ required: true }] }, { placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .addColumn(&#039;Тип доступа&#039;, &#039;action&#039;, false)&lt;br /&gt;
        .config(&#039;input&#039;, { name: &#039;action&#039; }, { type: &#039;string&#039;, placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .addColumn(&#039;Доступ&#039;, &#039;access&#039;, false)&lt;br /&gt;
        .config(&#039;checkbox&#039;, { name: &#039;access&#039; }, { type: &#039;boolean&#039;, placeholder: &#039;&#039;, disabled: true }) // Только для просмотра&lt;br /&gt;
        .next()&lt;br /&gt;
        .setRows(rows)&lt;br /&gt;
        .build();&lt;br /&gt;
  &lt;br /&gt;
  return table&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Сообщения для пользователя&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Обработчики ошибок — централизованно выбрасывают исключения&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Формирует конфиг диалога для отправки в GUI&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Извлекает socketId из параметров — необходим для обратной связи с интерфейсом&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Показывает пользователю сообщение об ошибке&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
// Показывает таблицу с правами доступа в модальном окне&lt;br /&gt;
const showTable = async (builder: TableBuilder, controller: PermissionController, socketId: string) =&amp;gt; &lt;br /&gt;
    await getDataFromDialog({ type: &#039;input-data&#039;, config: [await getAccessRightsTable(builder, controller)] }, socketId)&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Основная функция аддона:&lt;br /&gt;
 * Отображает пользователю таблицу с текущими правами доступа на ключевые ресурсы системы.&lt;br /&gt;
 * &lt;br /&gt;
 * Логика:&lt;br /&gt;
 * 1. Проверяет наличие параметров и активное соединение (socketId).&lt;br /&gt;
 * 2. Запрашивает права для каждого ресурса и типа доступа.&lt;br /&gt;
 * 3. Формирует таблицу через TableBuilder.&lt;br /&gt;
 * 4. Показывает её в модальном окне.&lt;br /&gt;
 * &lt;br /&gt;
 * Результат: пользователь видит, какие действия разрешены для &amp;quot;Объектов анализа&amp;quot;, &amp;quot;Методик&amp;quot; и &amp;quot;Записей ЛЖ&amp;quot;.&lt;br /&gt;
 */&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        // Создаём экземпляры билдера и контроллера прав доступа&lt;br /&gt;
        await showTable(new TableBuilder(), new PermissionController(apiInstance), socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        // При любой ошибке — показываем её в модальном окне&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Fill-record-fields.png|frame]]&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
import { JournalRecordAttributeFieldResponse } from &amp;quot;../../src/servivces/worker/api/controllers/lj-eav-attribute-fields&amp;quot;&lt;br /&gt;
import { FieldMatch } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
// Текущая дата в формате YYYY-MM-DD — используется как значение по умолчанию для полей с датой&lt;br /&gt;
const date = new Date().toISOString().split(&#039;T&#039;)[0]&lt;br /&gt;
&lt;br /&gt;
// Конфигурация аддона:&lt;br /&gt;
// - requiredFields: список полей, которые нужно заполнить&lt;br /&gt;
//   Каждое поле содержит:&lt;br /&gt;
//     • name — отображаемое имя&lt;br /&gt;
//     • value — значение, которое будет установлено&lt;br /&gt;
//     • keywords — ключевые слова для поиска соответствующего поля в записи&lt;br /&gt;
// - journalRecordId: 118 — тестовая запись (&amp;quot;Мк / 111&amp;quot;)&lt;br /&gt;
// - Результат при успехе: сообщение о завершении&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        { name: &#039;Дата регистрации&#039;, value: date, keywords: [&#039;дат&#039;, &#039;регистр&#039;] },&lt;br /&gt;
        { name: &#039;Место отбора&#039;, value: &#039;Скважина 516563Г &#039;, keywords: [&#039;мест&#039;, &#039;отбор&#039;] },&lt;br /&gt;
        { name: &#039;Дата отбора&#039;, value: date, keywords: [&#039;дат&#039;, &#039;отбор&#039;] },&lt;br /&gt;
        { name: &#039;Шифр пробы&#039;, value: &#039;shifr 123&#039;, keywords: [&#039;дат&#039;, &#039;регистр&#039;]},&lt;br /&gt;
        { name: &#039;Список. Целое число&#039;, value: 666, keywords: [&#039;спис&#039;, &#039;цел&#039;, &#039;числ&#039;] },&lt;br /&gt;
        { name: &#039;Помещение&#039;, value: &#039;помещение&#039;, keywords: [&#039;помещ&#039;]},&lt;br /&gt;
    ] as FieldMatch[],&lt;br /&gt;
    journalRecordId: 118, // Мк / 111&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Сообщения для пользователя&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: (recordId) =&amp;gt; `Все необходимые поля заполнены! Id записи: ${recordId}`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Обработчики ошибок — централизованно выбрасывают исключения&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Формирует конфиг диалога для отправки в GUI: тип, заголовок и сообщение&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Извлекает socketId из параметров — необходим для обратной связи с интерфейсом&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Показывает пользователю сообщение об ошибке&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
// Показывает уведомление об успешном заполнении&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
// Сопоставляет поле из записи (по имени) с требуемым полем через ключевые слова&lt;br /&gt;
// Например: если имя поля содержит &amp;quot;дат&amp;quot; и &amp;quot;регистр&amp;quot; → подходит под &amp;quot;Дата регистрации&amp;quot;&lt;br /&gt;
const fieldsMatcher = (field: JournalRecordAttributeFieldResponse, searchField: FieldMatch) =&amp;gt; {&lt;br /&gt;
    return searchField.keywords.every(k =&amp;gt; &lt;br /&gt;
        String(field.name).toLocaleLowerCase().includes(k.toLocaleLowerCase())&lt;br /&gt;
    )&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Заполняет указанные поля в записи журнала с использованием кастомного механизма сопоставления&lt;br /&gt;
const fillFields = async (fields: FieldMatch[], journalRecordsManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    await journalRecordsManager.setFieldsValues(fields, journalRecordsManager, fieldsMatcher)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Основная функция аддона:&lt;br /&gt;
 * Автоматически заполняет заданные поля в записи лабораторного журнала.&lt;br /&gt;
 * &lt;br /&gt;
 * Особенности:&lt;br /&gt;
 * - Поля ищутся не по точному имени, а по ключевым словам (гибкое сопоставление).&lt;br /&gt;
 * - Поддерживает разные типы значений: строки, числа, даты.&lt;br /&gt;
 * - Используется в тестовых или демонстрационных целях.&lt;br /&gt;
 * &lt;br /&gt;
 * Логика:&lt;br /&gt;
 * 1. Проверяет наличие параметров и активное соединение.&lt;br /&gt;
 * 2. Загружает запись журнала.&lt;br /&gt;
 * 3. Для каждого поля из config.requiredFields:&lt;br /&gt;
 *    - Находит соответствующее поле в записи по ключевым словам.&lt;br /&gt;
 *    - Устанавливает указанное значение.&lt;br /&gt;
 * 4. Сообщает пользователю об успехе.&lt;br /&gt;
 */&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        // Получаем менеджер для работы с записью&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        // Заполняем поля по ключевым словам&lt;br /&gt;
        await fillFields(config.requiredFields, journalRecordManager)&lt;br /&gt;
        // Информируем пользователя о завершении&lt;br /&gt;
        showSuccessMessage(messages.success(journalRecordManager.journalRecordId), socketId)&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        // При любой ошибке — показываем её в модальном окне&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Copy-journal-record.png|frame]]&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { JournalRecordsController } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRequest, JournalRecordResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { JournalRecordAttributeResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
// Тип данных поля (DATE, TIME, STRING и т.д.) — используется для корректной обработки значений при копировании&lt;br /&gt;
type DataType = JournalRecordAttributeResponse[&#039;field&#039;][&#039;dataType&#039;]&lt;br /&gt;
&lt;br /&gt;
// Конфигурация аддона:&lt;br /&gt;
// - journalRecordId: 208 — исходная запись (&amp;quot;Лаб. воздуха / Регистрация&amp;quot;)&lt;br /&gt;
// - nextJournalRecordStage: 2 — ID этапа, на который нужно скопировать запись (&amp;quot;Анализ&amp;quot;)&lt;br /&gt;
// - nextStageName: отображаемое имя целевого этапа&lt;br /&gt;
// - integrationResult: сообщение при успешном копировании&lt;br /&gt;
const config = {&lt;br /&gt;
    journalRecordId: 208,  // Лаб. воздуха / Регистрация&lt;br /&gt;
    nextJournalRecordStage: 2, // Лаб. воздуха / Анализ&lt;br /&gt;
    nextStageName: &#039;Анализ&#039;,&lt;br /&gt;
    integrationResult: { message: &#039;Запись ЛЖ скопирована успешно!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Сообщения для пользователя&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Обработчики ошибок — централизованно выбрасывают исключения&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Формирует конфиг диалога для отправки в GUI: тип, заголовок и сообщение&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Извлекает socketId из параметров — необходим для обратной связи с интерфейсом&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Показывает пользователю сообщение об ошибке&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
// Показывает уведомление об успешном копировании&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
// Преобразует ISO-дату в строку формата YYYY-MM-DD (без времени)&lt;br /&gt;
const getDateString = (ISODate: unknown | null = null) =&amp;gt; {&lt;br /&gt;
    return new Date(String(ISODate)).toISOString().split(&#039;T&#039;)[0]&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Преобразует ISO-дату в строку формата HH:mm:ss (без миллисекунд)&lt;br /&gt;
const getTimeString = (ISODate: unknown | null = null) =&amp;gt; {&lt;br /&gt;
    return new Date(String(ISODate)).toISOString().split(&#039;T&#039;)[1].split(&#039;.&#039;)[0]&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Создаёт новую запись лабораторного журнала на основе существующей.&lt;br /&gt;
 * &lt;br /&gt;
 * Особенности:&lt;br /&gt;
 * - Копирует все атрибуты (поля) из исходной записи.&lt;br /&gt;
 * - Автоматически корректирует значения полей типа DATE и TIME.&lt;br /&gt;
 * - Устанавливает новый этап.&lt;br /&gt;
 */&lt;br /&gt;
const createRecord = async (&lt;br /&gt;
    journalRecordController: JournalRecordsController,&lt;br /&gt;
    baseRecord: JournalRecordResponse,&lt;br /&gt;
    newStageId: number&lt;br /&gt;
) =&amp;gt; {&lt;br /&gt;
    // Корректирует значение поля в зависимости от его типа&lt;br /&gt;
    const getAttrValue = (value: unknown, dataType: DataType) =&amp;gt; {&lt;br /&gt;
        if (dataType === &amp;quot;DATE&amp;quot;) return getDateString(value)&lt;br /&gt;
        if (dataType === &amp;quot;TIME&amp;quot;) return getTimeString(value)&lt;br /&gt;
        return value&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Извлекает ID привязок уровня компании (если есть)&lt;br /&gt;
    const getBindings = (baseRecord: JournalRecordResponse): number[] | null =&amp;gt; {&lt;br /&gt;
        return baseRecord?.companyLevelBindings?.map(b =&amp;gt; b?.id) || null&lt;br /&gt;
    } &lt;br /&gt;
&lt;br /&gt;
    // Формирует объект запроса для создания новой записи&lt;br /&gt;
    const getRecordRequest = (baseRecord: JournalRecordResponse, newStageId: number): JournalRecordRequest =&amp;gt; {&lt;br /&gt;
        return {&lt;br /&gt;
            routeId: baseRecord?.route?.id,&lt;br /&gt;
            stageId: newStageId,&lt;br /&gt;
            analysisObjectId: baseRecord?.analysisObject?.id,&lt;br /&gt;
            sampleTypeId: baseRecord.sampleType.id,&lt;br /&gt;
            attributes: baseRecord.attributes.map(attr =&amp;gt; ({&lt;br /&gt;
                fieldId: attr.field.id,&lt;br /&gt;
                stageId: newStageId,&lt;br /&gt;
                data: {&lt;br /&gt;
                    ...attr.data, &lt;br /&gt;
                    value: getAttrValue(attr?.data?.value, attr?.field?.dataType),&lt;br /&gt;
                    id: undefined // Убираем ID, чтобы создать новое значение&lt;br /&gt;
                },&lt;br /&gt;
                statusId: null&lt;br /&gt;
            })),&lt;br /&gt;
            companyLevelBindings: getBindings(baseRecord)&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Отправляет запрос на создание новой записи&lt;br /&gt;
    return await journalRecordController.createRecord(getRecordRequest(baseRecord, newStageId))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Основная функция аддона:&lt;br /&gt;
 * Копирует запись лабораторного журнала на следующий этап (например, с &amp;quot;Регистрации&amp;quot; на &amp;quot;Анализ&amp;quot;).&lt;br /&gt;
 * &lt;br /&gt;
 * Логика:&lt;br /&gt;
 * 1. Загружает исходную запись через JournalRecordManager.&lt;br /&gt;
 * 2. Формирует новую запись с теми же данными, но на новом этапе.&lt;br /&gt;
 * 3. Особое внимание — полям типа DATE и TIME: они преобразуются в нужный формат.&lt;br /&gt;
 * 4. Создаёт новую запись через API.&lt;br /&gt;
 * 5. Показывает пользователю ID старой и новой записи.&lt;br /&gt;
 * &lt;br /&gt;
 * Используется для автоматизации деления данных.&lt;br /&gt;
 */&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        // Получаем данные исходной записи&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const journalRecordController = new JournalRecordsController(apiInstance)&lt;br /&gt;
        &lt;br /&gt;
        // Создаём копию записи на новом этапе&lt;br /&gt;
        const newRecord = await createRecord(&lt;br /&gt;
            journalRecordController, &lt;br /&gt;
            journalRecordManager.data, &lt;br /&gt;
            config.nextJournalRecordStage&lt;br /&gt;
        )&lt;br /&gt;
&lt;br /&gt;
        // Уведомляем пользователя об успехе с деталями&lt;br /&gt;
        showSuccessMessage(&lt;br /&gt;
            `${messages.success}. Id изначальной записи: ${config.journalRecordId}. ` +&lt;br /&gt;
            `Id новой записи ЛЖ: ${newRecord.id}. Этап: ${config.nextStageName}`, &lt;br /&gt;
            socketId&lt;br /&gt;
        )&lt;br /&gt;
        &lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        // При любой ошибке — показываем её в модальном окне&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2381</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2381"/>
		<updated>2026-04-03T06:41:34Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Заполнение атрибутов записи ЛЖ. fill-record-fields */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { MessageBoxResults } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { FetchApiClient } from &amp;quot;../../src/servivces/worker/api/ApiClient&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
&lt;br /&gt;
// Конфигурация аддона:&lt;br /&gt;
// - Поле &amp;quot;Дата регистрации&amp;quot; — контрольная точка для расчёта срока&lt;br /&gt;
// - expirationLimit: 10 дней — максимальный допустимый срок хранения пробы&lt;br /&gt;
// - journalRecordId: 201 — запись из журнала &amp;quot;Лаб. воздуха / Анализы завершены&amp;quot;&lt;br /&gt;
const config = {&lt;br /&gt;
    dateField: &#039;Дата регистрации&#039;,&lt;br /&gt;
    expirationLimit: 10,&lt;br /&gt;
    journalRecordId: 201,&lt;br /&gt;
    integrationResult: { message: &#039;Срок регистрации пробы не истёк!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Сообщения для пользовательских уведомлений и ошибок&lt;br /&gt;
const messages = {&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    // Успех: информирует пользователя, что срок не вышел&lt;br /&gt;
    success: (recordId) =&amp;gt; `Срок регистрации пробы не истёк! Id записи ${recordId}`,&lt;br /&gt;
    // Ошибка: превышен лимит хранения&lt;br /&gt;
    expired: (limit, recordId) =&amp;gt; `Срок регистрации пробы истёк!\nПрошло более ${limit} дней с момента регистрации. Id записи: ${recordId}`,&lt;br /&gt;
    // Ошибка: отсутствует дата в нужном поле&lt;br /&gt;
    dateField: (dateField, stageName, journalRecordId) =&amp;gt; [&lt;br /&gt;
        `Не заполнено либо отсутствует поле &amp;quot;${dateField}&amp;quot;`,&lt;br /&gt;
        `в этапе &amp;quot;${stageName || &#039;&#039;}&amp;quot;.\nId записи: ${journalRecordId}`&lt;br /&gt;
    ].join(&#039; &#039;)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Централизованные обработчики ошибок — обеспечивают единообразие и чистоту основной логики&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    // Вызывается, если поле с датой не заполнено&lt;br /&gt;
    dateError: (dateField, stageName, journalRecordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.dateField(dateField, stageName, journalRecordId))&lt;br /&gt;
    },&lt;br /&gt;
    // Вызывается при превышении срока хранения&lt;br /&gt;
    expirationError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.expired(config.expirationLimit, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Извлекает socketId из queryParams — необходим для отправки диалогов в GUI&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Переводит дату в количество дней с начала эпохи (для арифметики дат)&lt;br /&gt;
const getDaysByDate = (date: Date) =&amp;gt; {&lt;br /&gt;
    const dayMs = 60 * 60 * 24 * 1000&lt;br /&gt;
    const timestamp = date.getTime()&lt;br /&gt;
    return timestamp / dayMs&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Формирует конфиг диалога: тип, заголовок и сообщение для отображения в GUI&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Рассчитывает, сколько дней прошло с момента регистрации пробы&lt;br /&gt;
const getExpirationOffset = (registrationDate: string) =&amp;gt; {&lt;br /&gt;
    const regDate = new Date(registrationDate)&lt;br /&gt;
    const today = new Date()&lt;br /&gt;
    return getDaysByDate(today) - getDaysByDate(regDate)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Показывает пользователю сообщение об ошибке&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
// Показывает пользователю уведомление об успешной проверке&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Основная функция аддона:&lt;br /&gt;
 * Проверяет, не истёк ли срок хранения пробы (с момента &amp;quot;Дата регистрации&amp;quot;).&lt;br /&gt;
 * &lt;br /&gt;
 * Логика:&lt;br /&gt;
 * 1. Получает значение поля &amp;quot;Дата регистрации&amp;quot; из записи журнала.&lt;br /&gt;
 * 2. Если дата не указана — ошибка.&lt;br /&gt;
 * 3. Если с даты регистрации прошло &amp;gt;= 10 дней — ошибка.&lt;br /&gt;
 * 4. Иначе — успех, показываем подтверждение.&lt;br /&gt;
 */&lt;br /&gt;
async function main({ queryParams, ...apiInstance }) {&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        // Получаем доступ к данным записи журнала&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        // Извлекаем значение поля &amp;quot;Дата регистрации&amp;quot;&lt;br /&gt;
        const date = journalRecordManager.getFieldValue(config.dateField)&lt;br /&gt;
&lt;br /&gt;
        // Если дата не заполнена — прерываем с пояснением&lt;br /&gt;
        if (!date) {&lt;br /&gt;
            errorHandlers.dateError(config.dateField, journalRecordManager?.data?.stage?.stage?.name, config.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Считаем, сколько дней прошло с регистрации&lt;br /&gt;
        const expirationOffset = getExpirationOffset(date as string)&lt;br /&gt;
&lt;br /&gt;
        // Если срок хранения превышен — ошибка&lt;br /&gt;
        if (expirationOffset &amp;gt;= config.expirationLimit) {&lt;br /&gt;
            errorHandlers.expirationError()&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Всё в порядке — информируем пользователя&lt;br /&gt;
        await showSuccessMessage(messages.success(config.journalRecordId), socketId)&lt;br /&gt;
&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        // При любой ошибке — показываем её в модальном окне&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordIndicatorResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
// Описание свойств показателя (индикатора), которые нужно проверить&lt;br /&gt;
// Каждое свойство имеет метку (для отображения пользователю) и геттер значения&lt;br /&gt;
type IndicatorProp = { label: string, get: () =&amp;gt; unknown }&lt;br /&gt;
type IndicatorPropsConfig = Record&amp;lt;string, IndicatorProp&amp;gt;&lt;br /&gt;
&lt;br /&gt;
// Конфигурация полей, которые проверяются у каждого показателя&lt;br /&gt;
// Например: &amp;quot;Исполнители&amp;quot;, &amp;quot;Методики&amp;quot;, &amp;quot;Средний результат&amp;quot; и т.д.&lt;br /&gt;
const indicatorPropsConfig = (indicator): IndicatorPropsConfig =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        executors: {&lt;br /&gt;
            label: &#039;Исполнители&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.executors&lt;br /&gt;
        },&lt;br /&gt;
        methodic: {&lt;br /&gt;
            label: &#039;Методики&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.methodics&lt;br /&gt;
        },&lt;br /&gt;
        averageResult: {&lt;br /&gt;
            label: &#039;Средний результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageValue&lt;br /&gt;
        },&lt;br /&gt;
        averageRoundedValue: {&lt;br /&gt;
            label: &#039;Средний округлённый результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageRoundedValue&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Конфигурация аддона:&lt;br /&gt;
// - indicatorsList: список обязательных показателей (например, &amp;quot;Аммоний-ион&amp;quot;)&lt;br /&gt;
// - journalRecordId: 144 — тестовая запись (&amp;quot;Мк / Тест_Ушакова&amp;quot;)&lt;br /&gt;
const config = {&lt;br /&gt;
    indicatorsList: [&lt;br /&gt;
        &#039;Аммоний-ион&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144,&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Сообщения для пользовательских уведомлений и ошибок&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyIndicators: (recordId) =&amp;gt; `в записи с Id ${recordId} отсутствуют показатели`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
    indicators: (indicators: string[], recordId) =&amp;gt; `Показатели ${indicators.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} отсутствуют в записи с ID ${recordId}`,&lt;br /&gt;
    // Формирует детализированное сообщение о незаполненных полях внутри показателей&lt;br /&gt;
    emptyIndicatorsProps: (props: Record&amp;lt;string, string[]&amp;gt;, recordId) =&amp;gt; {&lt;br /&gt;
        const propsString = Object.keys(props).map(prop =&amp;gt; !isEmptyArray(props[prop]) ? `${prop}: ${props[prop].map(p =&amp;gt; `&amp;quot;${p}&amp;quot;`).join(&#039;, &#039;)}` : &#039;&#039;).join(&#039;; &#039;)&lt;br /&gt;
        return `В записи с Id ${recordId} отсутствуют либо не заполнены следующие поля у показателей: ${propsString}`&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Централизованные обработчики ошибок — обеспечивают единообразие и чистоту основной логики&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    indicatorsError: (recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyIndicators(recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFoundIndicators: (indicators: string[], recordId: number) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.indicators(indicators, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFilledIndicatorProps: (props: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyIndicatorsProps(props, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Формирует конфиг диалога для отправки в GUI: тип, заголовок и сообщение&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Извлекает socketId из queryParams — необходим для связи с интерфейсом&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Показывает пользователю модальное окно с ошибкой&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
// Показывает уведомление об успешной проверке&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
// Универсальная проверка: является ли массив пустым (с учётом типа)&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(arr) &amp;amp;&amp;amp; arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Проверяет, есть ли у записи хотя бы один показатель&lt;br /&gt;
const hasIndicators = (journalRecordManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
    return Array.isArray(indicators) &amp;amp;&amp;amp; indicators.length &amp;gt; 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Определяет, какие из требуемых показателей отсутствуют в записи&lt;br /&gt;
const getNotFoundIndicators = (indicators: JournalRecordIndicatorResponse[], indicatorsList = config.indicatorsList) =&amp;gt; {&lt;br /&gt;
    return indicatorsList.filter(ind =&amp;gt; &lt;br /&gt;
        indicators.every(ljInd =&amp;gt; ljInd?.indicator?.name?.toLowerCase() !== ind.toLowerCase())&lt;br /&gt;
    )&lt;br /&gt;
} &lt;br /&gt;
&lt;br /&gt;
// Собирает список незаполненных полей для каждого показателя&lt;br /&gt;
// Возвращает объект: { &amp;quot;Аммоний-ион&amp;quot;: [&amp;quot;Исполнители&amp;quot;, &amp;quot;Средний результат&amp;quot;] }&lt;br /&gt;
const getNotFilledResultsFields = (indicators: JournalRecordIndicatorResponse[]): Record&amp;lt;string, string[]&amp;gt; =&amp;gt; {&lt;br /&gt;
    return indicators.reduce((acc, indicator) =&amp;gt; {&lt;br /&gt;
        const config = indicatorPropsConfig(indicator)&lt;br /&gt;
        return {&lt;br /&gt;
            ...acc,&lt;br /&gt;
            [indicator?.indicator?.name]: Object.keys(config).reduce((acc, prop) =&amp;gt; {&lt;br /&gt;
                if (!config[prop].get()) return [...acc, config[prop].label]&lt;br /&gt;
                return acc&lt;br /&gt;
            }, [] as string[])&lt;br /&gt;
        }&lt;br /&gt;
    }, {}) as Record&amp;lt;string, string[]&amp;gt;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Проверяет, есть ли хотя бы один показатель с незаполненными полями&lt;br /&gt;
const hasNotFilledProps = (indicatorsProps: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
    return Object.keys(indicatorsProps).some(indicator =&amp;gt; !isEmptyArray(indicatorsProps[indicator]))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Проверяет, есть ли отсутствующие показатели в списке&lt;br /&gt;
const hasNotFoundIndicators = (indicators: string[]) =&amp;gt; !isEmptyArray(indicators)&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Основная функция аддона:&lt;br /&gt;
 * Проверяет заполненность:&lt;br /&gt;
 * 1. Наличие указанных показателей (например, &amp;quot;Аммоний-ион&amp;quot;).&lt;br /&gt;
 * 2. Заполненность внутренних полей каждого показателя (исполнители, методики и т.д.).&lt;br /&gt;
 * &lt;br /&gt;
 * При обнаружении проблем — прерывает выполнение и показывает детализированную ошибку.&lt;br /&gt;
 */&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        // Проверка: есть ли вообще показатели в записи?&lt;br /&gt;
        if (!hasIndicators(journalRecordManager)) {&lt;br /&gt;
            errorHandlers.indicatorsError(journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
&lt;br /&gt;
        // Проверка: все ли требуемые показатели присутствуют?&lt;br /&gt;
        const notFoundIndicators = getNotFoundIndicators(indicators)&lt;br /&gt;
        if (hasNotFoundIndicators(notFoundIndicators)) { &lt;br /&gt;
            errorHandlers.notFoundIndicators(notFoundIndicators, journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Проверка: все ли поля у показателей заполнены?&lt;br /&gt;
        const emptyIndicatorProps = getNotFilledResultsFields(indicators)&lt;br /&gt;
        if (hasNotFilledProps(emptyIndicatorProps)) {&lt;br /&gt;
            errorHandlers.notFilledIndicatorProps(emptyIndicatorProps)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Все проверки пройдены — информируем пользователя&lt;br /&gt;
        showSuccessMessage(messages.success, socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        // При любой ошибке — показываем её в модальном окне&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-record-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Список. Целое число&#039;,&lt;br /&gt;
        &#039;Строка&#039;,&lt;br /&gt;
        &#039;Целое число&#039;,&lt;br /&gt;
        &#039;Вещественное число&#039;,&lt;br /&gt;
        &#039;Логический тип&#039;,&lt;br /&gt;
        &#039;Дата&#039;,&lt;br /&gt;
        &#039;Время&#039;,&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144,  // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFilledFields = (fields: string[], journalRecordsManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    console.log(journalRecordsManager.data.attributes, &#039;data.attributes&#039;)&lt;br /&gt;
    return fields.filter(f =&amp;gt; !journalRecordsManager.getFieldValue(f))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const emptyFields = getNotFilledFields(config.requiredFields, journalRecordManager)&lt;br /&gt;
&lt;br /&gt;
        if (isEmptyArray(emptyFields)) {&lt;br /&gt;
            showSuccessMessage(messages.success, socketId)&lt;br /&gt;
            return config.integrationResult&lt;br /&gt;
        } else errorHandlers.fieldsError(emptyFields, config.journalRecordId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-sample-moving.png|frame]]&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { JournalsRecordRouteController } from &amp;quot;../../src/servivces/worker/api/controllers/journals-record-route&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRouteDto } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
// Конфигурация аддона:&lt;br /&gt;
// - journalRecordId: 114 — запись из журнала &amp;quot;Лаб. воздуха / Анализы завершены&amp;quot;&lt;br /&gt;
// Аддон анализирует маршрут движения пробы по этапам ЛЖ&lt;br /&gt;
const config = {&lt;br /&gt;
    journalRecordId: 114&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Сообщения для отображения пользователю&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    // Формирует строку с маршрутом пробы: этапы через &amp;quot; -&amp;gt; &amp;quot;&lt;br /&gt;
    route: (routes: JournalRecordRouteDto[], recordId: number) =&amp;gt; &lt;br /&gt;
        `Маршрут пробы: ${routes.map(r =&amp;gt; `&amp;quot;${r.stageName}&amp;quot;`).join(&#039; -&amp;gt; &#039;)}. Id записи ЛЖ: ${recordId}`,&lt;br /&gt;
    // Сообщение, если проба не перемещалась между этапами&lt;br /&gt;
    notMoved: (recordId: number) =&amp;gt; &lt;br /&gt;
        `Движение пробы не происходило. Id записи ЛЖ: ${recordId}`&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Обработчики ошибок — централизованно выбрасывают исключения с понятными сообщениями&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Формирует конфиг диалога для GUI: тип (confirm), заголовок и текст&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Извлекает socketId из параметров запроса — необходим для отправки ответа в интерфейс&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Показывает пользователю модальное окно с ошибкой&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
// Показывает информационное сообщение (успех или статус)&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
// Определяет, перемещалась ли проба: если более одного этапа — значит, было движение&lt;br /&gt;
const isSampleMoved = (moves: JournalRecordRouteDto[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(moves) &amp;amp;&amp;amp; moves.length &amp;gt; 1&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Основная функция аддона:&lt;br /&gt;
 * Получает маршрут пробы по этапам лабораторного журнала и отображает его пользователю.&lt;br /&gt;
 * &lt;br /&gt;
 * Логика:&lt;br /&gt;
 * 1. Проверяет наличие параметров и подключение клиента (socketId).&lt;br /&gt;
 * 2. Запрашивает историю перемещений пробы через API.&lt;br /&gt;
 * 3. Если проба прошла более одного этапа — отображает маршрут.&lt;br /&gt;
 * 4. Если осталась на одном этапе — сообщает, что движения не было.&lt;br /&gt;
 */&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        // Контроллер для работы с маршрутом записи (история этапов)&lt;br /&gt;
        const journalsRecordRouteController = new JournalsRecordRouteController(apiInstance)&lt;br /&gt;
        // Получаем список этапов, через которые прошла проба&lt;br /&gt;
        const sampleMoves = await journalsRecordRouteController.getJournalRecordRoute(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (isSampleMoved(sampleMoves)) {&lt;br /&gt;
            // Проба перемещалась — показываем полный маршрут&lt;br /&gt;
            showSuccessMessage(messages.route(sampleMoves, config.journalRecordId), socketId)&lt;br /&gt;
        } else {&lt;br /&gt;
            // Проба не покидала исходный этап&lt;br /&gt;
            showSuccessMessage(messages.notMoved(config.journalRecordId), socketId)&lt;br /&gt;
        }&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        // При любой ошибке (сеть, доступ, данные) — показываем её пользователю&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-user-rights.png|frame]]&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import { &lt;br /&gt;
    PermissionController, &lt;br /&gt;
    PermissionResource, &lt;br /&gt;
    PermissionRwAccess&lt;br /&gt;
} from &amp;quot;../../src/servivces/worker/api/controllers/permisson&amp;quot;&lt;br /&gt;
&lt;br /&gt;
import { TableBuilder } from &amp;quot;../../src/gui/builder&amp;quot;&lt;br /&gt;
import type { DialogType, TableConfig } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRouteDto } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
// Отображение типов доступа в понятные пользователю названия&lt;br /&gt;
const accessNamesMap = {&lt;br /&gt;
    [PermissionRwAccess.EXEC]: &#039;Выполнение&#039;,&lt;br /&gt;
    [PermissionRwAccess.READ]: &#039;Чтение&#039;,&lt;br /&gt;
    [PermissionRwAccess.WRITE]: &#039;Запись&#039;,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Список ресурсов, права на которые необходимо проверить&lt;br /&gt;
const requiredResources = [&lt;br /&gt;
    PermissionResource.ANALYSIS_OBJECT, &lt;br /&gt;
    PermissionResource.METHODOLOGY,&lt;br /&gt;
    PermissionResource.JOURNAL_RECORD&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
// Отображение идентификаторов ресурсов в читаемые названия для отображения в таблице&lt;br /&gt;
const resourcesNamesMap = {&lt;br /&gt;
    [PermissionResource.ANALYSIS_OBJECT]: &#039;Объекты анализа&#039;, &lt;br /&gt;
    [PermissionResource.METHODOLOGY]: &#039;Методики&#039;, &lt;br /&gt;
    [PermissionResource.JOURNAL_RECORD]: &#039;Записи ЛЖ&#039;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Формирует конфигурацию таблицы с правами доступа:&lt;br /&gt;
 * - Для каждого ресурса проверяются все типы доступа (чтение, запись, выполнение).&lt;br /&gt;
 * - Возвращает готовую конфигурацию таблицы для отображения в GUI.&lt;br /&gt;
 */&lt;br /&gt;
const getAccessRightsTable = async (tableBuilder: TableBuilder, permissionController: PermissionController): Promise&amp;lt;TableConfig&amp;gt; =&amp;gt; {&lt;br /&gt;
    // Получаем список строк: комбинация &amp;quot;ресурс + тип доступа + наличие права&amp;quot;&lt;br /&gt;
    const rows = (await Promise.all(requiredResources.map(async (source: PermissionResource) =&amp;gt; {&lt;br /&gt;
            const sourceCombinations = await Promise.all(Object.keys(PermissionRwAccess).map(async (action) =&amp;gt; {&lt;br /&gt;
                const access = await permissionController.getSourcePermission(source, PermissionRwAccess[action])&lt;br /&gt;
                return { &lt;br /&gt;
                    source: resourcesNamesMap[source], &lt;br /&gt;
                    action: accessNamesMap[PermissionRwAccess[action]], &lt;br /&gt;
                    access: access.hasPermission &lt;br /&gt;
                }&lt;br /&gt;
            }))&lt;br /&gt;
            return sourceCombinations&lt;br /&gt;
        }   &lt;br /&gt;
    ))).flat()&lt;br /&gt;
&lt;br /&gt;
    // Строим таблицу с помощью TableBuilder&lt;br /&gt;
    const table = tableBuilder&lt;br /&gt;
        .name(&#039;simpleTable&#039;)&lt;br /&gt;
        .label(&#039;Таблица&#039;)&lt;br /&gt;
        .isAddable(false)           // Нельзя добавлять строки&lt;br /&gt;
        .actionsCol(false)          // Скрыть колонку действий&lt;br /&gt;
        .addColumn(&#039;Ресурс&#039;, &#039;source&#039;, false)&lt;br /&gt;
        .config(&#039;input&#039;, { name: &#039;source&#039;, rules: [{ required: true }] }, { placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .addColumn(&#039;Тип доступа&#039;, &#039;action&#039;, false)&lt;br /&gt;
        .config(&#039;input&#039;, { name: &#039;action&#039; }, { type: &#039;string&#039;, placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .addColumn(&#039;Доступ&#039;, &#039;access&#039;, false)&lt;br /&gt;
        .config(&#039;checkbox&#039;, { name: &#039;access&#039; }, { type: &#039;boolean&#039;, placeholder: &#039;&#039;, disabled: true }) // Только для просмотра&lt;br /&gt;
        .next()&lt;br /&gt;
        .setRows(rows)&lt;br /&gt;
        .build();&lt;br /&gt;
  &lt;br /&gt;
  return table&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Сообщения для пользователя&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Обработчики ошибок — централизованно выбрасывают исключения&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Формирует конфиг диалога для отправки в GUI&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Извлекает socketId из параметров — необходим для обратной связи с интерфейсом&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Показывает пользователю сообщение об ошибке&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
// Показывает таблицу с правами доступа в модальном окне&lt;br /&gt;
const showTable = async (builder: TableBuilder, controller: PermissionController, socketId: string) =&amp;gt; &lt;br /&gt;
    await getDataFromDialog({ type: &#039;input-data&#039;, config: [await getAccessRightsTable(builder, controller)] }, socketId)&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Основная функция аддона:&lt;br /&gt;
 * Отображает пользователю таблицу с текущими правами доступа на ключевые ресурсы системы.&lt;br /&gt;
 * &lt;br /&gt;
 * Логика:&lt;br /&gt;
 * 1. Проверяет наличие параметров и активное соединение (socketId).&lt;br /&gt;
 * 2. Запрашивает права для каждого ресурса и типа доступа.&lt;br /&gt;
 * 3. Формирует таблицу через TableBuilder.&lt;br /&gt;
 * 4. Показывает её в модальном окне.&lt;br /&gt;
 * &lt;br /&gt;
 * Результат: пользователь видит, какие действия разрешены для &amp;quot;Объектов анализа&amp;quot;, &amp;quot;Методик&amp;quot; и &amp;quot;Записей ЛЖ&amp;quot;.&lt;br /&gt;
 */&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        // Создаём экземпляры билдера и контроллера прав доступа&lt;br /&gt;
        await showTable(new TableBuilder(), new PermissionController(apiInstance), socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        // При любой ошибке — показываем её в модальном окне&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Fill-record-fields.png|frame]]&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
import { JournalRecordAttributeFieldResponse } from &amp;quot;../../src/servivces/worker/api/controllers/lj-eav-attribute-fields&amp;quot;&lt;br /&gt;
import { FieldMatch } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
// Текущая дата в формате YYYY-MM-DD — используется как значение по умолчанию для полей с датой&lt;br /&gt;
const date = new Date().toISOString().split(&#039;T&#039;)[0]&lt;br /&gt;
&lt;br /&gt;
// Конфигурация аддона:&lt;br /&gt;
// - requiredFields: список полей, которые нужно заполнить&lt;br /&gt;
//   Каждое поле содержит:&lt;br /&gt;
//     • name — отображаемое имя&lt;br /&gt;
//     • value — значение, которое будет установлено&lt;br /&gt;
//     • keywords — ключевые слова для поиска соответствующего поля в записи&lt;br /&gt;
// - journalRecordId: 118 — тестовая запись (&amp;quot;Мк / 111&amp;quot;)&lt;br /&gt;
// - Результат при успехе: сообщение о завершении&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        { name: &#039;Дата регистрации&#039;, value: date, keywords: [&#039;дат&#039;, &#039;регистр&#039;] },&lt;br /&gt;
        { name: &#039;Место отбора&#039;, value: &#039;Скважина 516563Г &#039;, keywords: [&#039;мест&#039;, &#039;отбор&#039;] },&lt;br /&gt;
        { name: &#039;Дата отбора&#039;, value: date, keywords: [&#039;дат&#039;, &#039;отбор&#039;] },&lt;br /&gt;
        { name: &#039;Шифр пробы&#039;, value: &#039;shifr 123&#039;, keywords: [&#039;дат&#039;, &#039;регистр&#039;]},&lt;br /&gt;
        { name: &#039;Список. Целое число&#039;, value: 666, keywords: [&#039;спис&#039;, &#039;цел&#039;, &#039;числ&#039;] },&lt;br /&gt;
        { name: &#039;Помещение&#039;, value: &#039;помещение&#039;, keywords: [&#039;помещ&#039;]},&lt;br /&gt;
    ] as FieldMatch[],&lt;br /&gt;
    journalRecordId: 118, // Мк / 111&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Сообщения для пользователя&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: (recordId) =&amp;gt; `Все необходимые поля заполнены! Id записи: ${recordId}`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Обработчики ошибок — централизованно выбрасывают исключения&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Формирует конфиг диалога для отправки в GUI: тип, заголовок и сообщение&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Извлекает socketId из параметров — необходим для обратной связи с интерфейсом&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Показывает пользователю сообщение об ошибке&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
// Показывает уведомление об успешном заполнении&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
// Сопоставляет поле из записи (по имени) с требуемым полем через ключевые слова&lt;br /&gt;
// Например: если имя поля содержит &amp;quot;дат&amp;quot; и &amp;quot;регистр&amp;quot; → подходит под &amp;quot;Дата регистрации&amp;quot;&lt;br /&gt;
const fieldsMatcher = (field: JournalRecordAttributeFieldResponse, searchField: FieldMatch) =&amp;gt; {&lt;br /&gt;
    return searchField.keywords.every(k =&amp;gt; &lt;br /&gt;
        String(field.name).toLocaleLowerCase().includes(k.toLocaleLowerCase())&lt;br /&gt;
    )&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Заполняет указанные поля в записи журнала с использованием кастомного механизма сопоставления&lt;br /&gt;
const fillFields = async (fields: FieldMatch[], journalRecordsManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    await journalRecordsManager.setFieldsValues(fields, journalRecordsManager, fieldsMatcher)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Основная функция аддона:&lt;br /&gt;
 * Автоматически заполняет заданные поля в записи лабораторного журнала.&lt;br /&gt;
 * &lt;br /&gt;
 * Особенности:&lt;br /&gt;
 * - Поля ищутся не по точному имени, а по ключевым словам (гибкое сопоставление).&lt;br /&gt;
 * - Поддерживает разные типы значений: строки, числа, даты.&lt;br /&gt;
 * - Используется в тестовых или демонстрационных целях.&lt;br /&gt;
 * &lt;br /&gt;
 * Логика:&lt;br /&gt;
 * 1. Проверяет наличие параметров и активное соединение.&lt;br /&gt;
 * 2. Загружает запись журнала.&lt;br /&gt;
 * 3. Для каждого поля из config.requiredFields:&lt;br /&gt;
 *    - Находит соответствующее поле в записи по ключевым словам.&lt;br /&gt;
 *    - Устанавливает указанное значение.&lt;br /&gt;
 * 4. Сообщает пользователю об успехе.&lt;br /&gt;
 */&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        // Получаем менеджер для работы с записью&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        // Заполняем поля по ключевым словам&lt;br /&gt;
        await fillFields(config.requiredFields, journalRecordManager)&lt;br /&gt;
        // Информируем пользователя о завершении&lt;br /&gt;
        showSuccessMessage(messages.success(journalRecordManager.journalRecordId), socketId)&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        // При любой ошибке — показываем её в модальном окне&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Copy-journal-record.png|frame]]&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { JournalRecordsController } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRequest, JournalRecordResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { JournalRecordAttributeResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
type DataType = JournalRecordAttributeResponse[&#039;field&#039;][&#039;dataType&#039;]&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    journalRecordId: 208,  // Лаб. воздуха / Регистрация&lt;br /&gt;
    nextJournalRecordStage: 2, //  Лаб. воздуха / Анализ&lt;br /&gt;
    nextStageName: &#039;Анализ&#039;,&lt;br /&gt;
    nextRouteId: 50,&lt;br /&gt;
    integrationResult: { message: &#039;Запись ЛЖ скопирована успешно!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const getDateString = (ISODate: unknown | null = null) =&amp;gt; {&lt;br /&gt;
    return new Date(String(ISODate)).toISOString().split(&#039;T&#039;)[0]&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getTimeString = (ISODate: unknown | null = null) =&amp;gt; {&lt;br /&gt;
    return new Date(String(ISODate)).toISOString().split(&#039;T&#039;)[1].split(&#039;.&#039;)[0]&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const createRecord = async (journalRecordController: JournalRecordsController, baseRecord: JournalRecordResponse, newStageId) =&amp;gt; {&lt;br /&gt;
    const getAttrValue = (value: unknown, dataType: DataType) =&amp;gt; {&lt;br /&gt;
        if (dataType === &amp;quot;DATE&amp;quot;) return getDateString(value)&lt;br /&gt;
        if (dataType === &amp;quot;TIME&amp;quot;) return getTimeString(value)&lt;br /&gt;
        return value&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    const getBindings = (baseRecord: JournalRecordResponse): number[] | null =&amp;gt; {&lt;br /&gt;
        return baseRecord?.companyLevelBindings?.map(b =&amp;gt; b?.id) || null&lt;br /&gt;
    } &lt;br /&gt;
&lt;br /&gt;
    const getRecordRequest = (baseRecord: JournalRecordResponse, newStageId: number): JournalRecordRequest =&amp;gt; {&lt;br /&gt;
        return {&lt;br /&gt;
            routeId: baseRecord?.route?.id,&lt;br /&gt;
            stageId: newStageId,&lt;br /&gt;
            analysisObjectId: baseRecord?.analysisObject?.id,&lt;br /&gt;
            sampleTypeId: baseRecord.sampleType.id,&lt;br /&gt;
            attributes: baseRecord.attributes.map(attr =&amp;gt; {&lt;br /&gt;
                return {&lt;br /&gt;
                    fieldId: attr.field.id,&lt;br /&gt;
                    stageId: newStageId,&lt;br /&gt;
                    data: {&lt;br /&gt;
                        ...attr.data, &lt;br /&gt;
                        value: getAttrValue(attr?.data?.value, attr?.field?.dataType),&lt;br /&gt;
                        id: undefined&lt;br /&gt;
                    },&lt;br /&gt;
                    statusId: null&lt;br /&gt;
                }&lt;br /&gt;
            }),&lt;br /&gt;
            companyLevelBindings: getBindings(baseRecord)&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    return await journalRecordController.createRecord(getRecordRequest(baseRecord, newStageId))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const journalRecordController = new JournalRecordsController(apiInstance)&lt;br /&gt;
        const newRecord = await createRecord(journalRecordController, journalRecordManager.data, config.nextJournalRecordStage)&lt;br /&gt;
        showSuccessMessage(`${messages.success}. Id изначальной записи: ${config.journalRecordId}. Id новой записи ЛЖ: ${newRecord.id}. Этап: ${config.nextStageName}`, socketId)&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2380</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2380"/>
		<updated>2026-04-03T06:37:23Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Проверка прав пользователя. check-user-rights */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { MessageBoxResults } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { FetchApiClient } from &amp;quot;../../src/servivces/worker/api/ApiClient&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
&lt;br /&gt;
// Конфигурация аддона:&lt;br /&gt;
// - Поле &amp;quot;Дата регистрации&amp;quot; — контрольная точка для расчёта срока&lt;br /&gt;
// - expirationLimit: 10 дней — максимальный допустимый срок хранения пробы&lt;br /&gt;
// - journalRecordId: 201 — запись из журнала &amp;quot;Лаб. воздуха / Анализы завершены&amp;quot;&lt;br /&gt;
const config = {&lt;br /&gt;
    dateField: &#039;Дата регистрации&#039;,&lt;br /&gt;
    expirationLimit: 10,&lt;br /&gt;
    journalRecordId: 201,&lt;br /&gt;
    integrationResult: { message: &#039;Срок регистрации пробы не истёк!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Сообщения для пользовательских уведомлений и ошибок&lt;br /&gt;
const messages = {&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    // Успех: информирует пользователя, что срок не вышел&lt;br /&gt;
    success: (recordId) =&amp;gt; `Срок регистрации пробы не истёк! Id записи ${recordId}`,&lt;br /&gt;
    // Ошибка: превышен лимит хранения&lt;br /&gt;
    expired: (limit, recordId) =&amp;gt; `Срок регистрации пробы истёк!\nПрошло более ${limit} дней с момента регистрации. Id записи: ${recordId}`,&lt;br /&gt;
    // Ошибка: отсутствует дата в нужном поле&lt;br /&gt;
    dateField: (dateField, stageName, journalRecordId) =&amp;gt; [&lt;br /&gt;
        `Не заполнено либо отсутствует поле &amp;quot;${dateField}&amp;quot;`,&lt;br /&gt;
        `в этапе &amp;quot;${stageName || &#039;&#039;}&amp;quot;.\nId записи: ${journalRecordId}`&lt;br /&gt;
    ].join(&#039; &#039;)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Централизованные обработчики ошибок — обеспечивают единообразие и чистоту основной логики&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    // Вызывается, если поле с датой не заполнено&lt;br /&gt;
    dateError: (dateField, stageName, journalRecordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.dateField(dateField, stageName, journalRecordId))&lt;br /&gt;
    },&lt;br /&gt;
    // Вызывается при превышении срока хранения&lt;br /&gt;
    expirationError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.expired(config.expirationLimit, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Извлекает socketId из queryParams — необходим для отправки диалогов в GUI&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Переводит дату в количество дней с начала эпохи (для арифметики дат)&lt;br /&gt;
const getDaysByDate = (date: Date) =&amp;gt; {&lt;br /&gt;
    const dayMs = 60 * 60 * 24 * 1000&lt;br /&gt;
    const timestamp = date.getTime()&lt;br /&gt;
    return timestamp / dayMs&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Формирует конфиг диалога: тип, заголовок и сообщение для отображения в GUI&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Рассчитывает, сколько дней прошло с момента регистрации пробы&lt;br /&gt;
const getExpirationOffset = (registrationDate: string) =&amp;gt; {&lt;br /&gt;
    const regDate = new Date(registrationDate)&lt;br /&gt;
    const today = new Date()&lt;br /&gt;
    return getDaysByDate(today) - getDaysByDate(regDate)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Показывает пользователю сообщение об ошибке&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
// Показывает пользователю уведомление об успешной проверке&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Основная функция аддона:&lt;br /&gt;
 * Проверяет, не истёк ли срок хранения пробы (с момента &amp;quot;Дата регистрации&amp;quot;).&lt;br /&gt;
 * &lt;br /&gt;
 * Логика:&lt;br /&gt;
 * 1. Получает значение поля &amp;quot;Дата регистрации&amp;quot; из записи журнала.&lt;br /&gt;
 * 2. Если дата не указана — ошибка.&lt;br /&gt;
 * 3. Если с даты регистрации прошло &amp;gt;= 10 дней — ошибка.&lt;br /&gt;
 * 4. Иначе — успех, показываем подтверждение.&lt;br /&gt;
 */&lt;br /&gt;
async function main({ queryParams, ...apiInstance }) {&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        // Получаем доступ к данным записи журнала&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        // Извлекаем значение поля &amp;quot;Дата регистрации&amp;quot;&lt;br /&gt;
        const date = journalRecordManager.getFieldValue(config.dateField)&lt;br /&gt;
&lt;br /&gt;
        // Если дата не заполнена — прерываем с пояснением&lt;br /&gt;
        if (!date) {&lt;br /&gt;
            errorHandlers.dateError(config.dateField, journalRecordManager?.data?.stage?.stage?.name, config.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Считаем, сколько дней прошло с регистрации&lt;br /&gt;
        const expirationOffset = getExpirationOffset(date as string)&lt;br /&gt;
&lt;br /&gt;
        // Если срок хранения превышен — ошибка&lt;br /&gt;
        if (expirationOffset &amp;gt;= config.expirationLimit) {&lt;br /&gt;
            errorHandlers.expirationError()&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Всё в порядке — информируем пользователя&lt;br /&gt;
        await showSuccessMessage(messages.success(config.journalRecordId), socketId)&lt;br /&gt;
&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        // При любой ошибке — показываем её в модальном окне&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordIndicatorResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
// Описание свойств показателя (индикатора), которые нужно проверить&lt;br /&gt;
// Каждое свойство имеет метку (для отображения пользователю) и геттер значения&lt;br /&gt;
type IndicatorProp = { label: string, get: () =&amp;gt; unknown }&lt;br /&gt;
type IndicatorPropsConfig = Record&amp;lt;string, IndicatorProp&amp;gt;&lt;br /&gt;
&lt;br /&gt;
// Конфигурация полей, которые проверяются у каждого показателя&lt;br /&gt;
// Например: &amp;quot;Исполнители&amp;quot;, &amp;quot;Методики&amp;quot;, &amp;quot;Средний результат&amp;quot; и т.д.&lt;br /&gt;
const indicatorPropsConfig = (indicator): IndicatorPropsConfig =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        executors: {&lt;br /&gt;
            label: &#039;Исполнители&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.executors&lt;br /&gt;
        },&lt;br /&gt;
        methodic: {&lt;br /&gt;
            label: &#039;Методики&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.methodics&lt;br /&gt;
        },&lt;br /&gt;
        averageResult: {&lt;br /&gt;
            label: &#039;Средний результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageValue&lt;br /&gt;
        },&lt;br /&gt;
        averageRoundedValue: {&lt;br /&gt;
            label: &#039;Средний округлённый результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageRoundedValue&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Конфигурация аддона:&lt;br /&gt;
// - indicatorsList: список обязательных показателей (например, &amp;quot;Аммоний-ион&amp;quot;)&lt;br /&gt;
// - journalRecordId: 144 — тестовая запись (&amp;quot;Мк / Тест_Ушакова&amp;quot;)&lt;br /&gt;
const config = {&lt;br /&gt;
    indicatorsList: [&lt;br /&gt;
        &#039;Аммоний-ион&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144,&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Сообщения для пользовательских уведомлений и ошибок&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyIndicators: (recordId) =&amp;gt; `в записи с Id ${recordId} отсутствуют показатели`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
    indicators: (indicators: string[], recordId) =&amp;gt; `Показатели ${indicators.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} отсутствуют в записи с ID ${recordId}`,&lt;br /&gt;
    // Формирует детализированное сообщение о незаполненных полях внутри показателей&lt;br /&gt;
    emptyIndicatorsProps: (props: Record&amp;lt;string, string[]&amp;gt;, recordId) =&amp;gt; {&lt;br /&gt;
        const propsString = Object.keys(props).map(prop =&amp;gt; !isEmptyArray(props[prop]) ? `${prop}: ${props[prop].map(p =&amp;gt; `&amp;quot;${p}&amp;quot;`).join(&#039;, &#039;)}` : &#039;&#039;).join(&#039;; &#039;)&lt;br /&gt;
        return `В записи с Id ${recordId} отсутствуют либо не заполнены следующие поля у показателей: ${propsString}`&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Централизованные обработчики ошибок — обеспечивают единообразие и чистоту основной логики&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    indicatorsError: (recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyIndicators(recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFoundIndicators: (indicators: string[], recordId: number) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.indicators(indicators, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFilledIndicatorProps: (props: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyIndicatorsProps(props, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Формирует конфиг диалога для отправки в GUI: тип, заголовок и сообщение&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Извлекает socketId из queryParams — необходим для связи с интерфейсом&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Показывает пользователю модальное окно с ошибкой&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
// Показывает уведомление об успешной проверке&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
// Универсальная проверка: является ли массив пустым (с учётом типа)&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(arr) &amp;amp;&amp;amp; arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Проверяет, есть ли у записи хотя бы один показатель&lt;br /&gt;
const hasIndicators = (journalRecordManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
    return Array.isArray(indicators) &amp;amp;&amp;amp; indicators.length &amp;gt; 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Определяет, какие из требуемых показателей отсутствуют в записи&lt;br /&gt;
const getNotFoundIndicators = (indicators: JournalRecordIndicatorResponse[], indicatorsList = config.indicatorsList) =&amp;gt; {&lt;br /&gt;
    return indicatorsList.filter(ind =&amp;gt; &lt;br /&gt;
        indicators.every(ljInd =&amp;gt; ljInd?.indicator?.name?.toLowerCase() !== ind.toLowerCase())&lt;br /&gt;
    )&lt;br /&gt;
} &lt;br /&gt;
&lt;br /&gt;
// Собирает список незаполненных полей для каждого показателя&lt;br /&gt;
// Возвращает объект: { &amp;quot;Аммоний-ион&amp;quot;: [&amp;quot;Исполнители&amp;quot;, &amp;quot;Средний результат&amp;quot;] }&lt;br /&gt;
const getNotFilledResultsFields = (indicators: JournalRecordIndicatorResponse[]): Record&amp;lt;string, string[]&amp;gt; =&amp;gt; {&lt;br /&gt;
    return indicators.reduce((acc, indicator) =&amp;gt; {&lt;br /&gt;
        const config = indicatorPropsConfig(indicator)&lt;br /&gt;
        return {&lt;br /&gt;
            ...acc,&lt;br /&gt;
            [indicator?.indicator?.name]: Object.keys(config).reduce((acc, prop) =&amp;gt; {&lt;br /&gt;
                if (!config[prop].get()) return [...acc, config[prop].label]&lt;br /&gt;
                return acc&lt;br /&gt;
            }, [] as string[])&lt;br /&gt;
        }&lt;br /&gt;
    }, {}) as Record&amp;lt;string, string[]&amp;gt;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Проверяет, есть ли хотя бы один показатель с незаполненными полями&lt;br /&gt;
const hasNotFilledProps = (indicatorsProps: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
    return Object.keys(indicatorsProps).some(indicator =&amp;gt; !isEmptyArray(indicatorsProps[indicator]))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Проверяет, есть ли отсутствующие показатели в списке&lt;br /&gt;
const hasNotFoundIndicators = (indicators: string[]) =&amp;gt; !isEmptyArray(indicators)&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Основная функция аддона:&lt;br /&gt;
 * Проверяет заполненность:&lt;br /&gt;
 * 1. Наличие указанных показателей (например, &amp;quot;Аммоний-ион&amp;quot;).&lt;br /&gt;
 * 2. Заполненность внутренних полей каждого показателя (исполнители, методики и т.д.).&lt;br /&gt;
 * &lt;br /&gt;
 * При обнаружении проблем — прерывает выполнение и показывает детализированную ошибку.&lt;br /&gt;
 */&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        // Проверка: есть ли вообще показатели в записи?&lt;br /&gt;
        if (!hasIndicators(journalRecordManager)) {&lt;br /&gt;
            errorHandlers.indicatorsError(journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
&lt;br /&gt;
        // Проверка: все ли требуемые показатели присутствуют?&lt;br /&gt;
        const notFoundIndicators = getNotFoundIndicators(indicators)&lt;br /&gt;
        if (hasNotFoundIndicators(notFoundIndicators)) { &lt;br /&gt;
            errorHandlers.notFoundIndicators(notFoundIndicators, journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Проверка: все ли поля у показателей заполнены?&lt;br /&gt;
        const emptyIndicatorProps = getNotFilledResultsFields(indicators)&lt;br /&gt;
        if (hasNotFilledProps(emptyIndicatorProps)) {&lt;br /&gt;
            errorHandlers.notFilledIndicatorProps(emptyIndicatorProps)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Все проверки пройдены — информируем пользователя&lt;br /&gt;
        showSuccessMessage(messages.success, socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        // При любой ошибке — показываем её в модальном окне&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-record-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Список. Целое число&#039;,&lt;br /&gt;
        &#039;Строка&#039;,&lt;br /&gt;
        &#039;Целое число&#039;,&lt;br /&gt;
        &#039;Вещественное число&#039;,&lt;br /&gt;
        &#039;Логический тип&#039;,&lt;br /&gt;
        &#039;Дата&#039;,&lt;br /&gt;
        &#039;Время&#039;,&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144,  // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFilledFields = (fields: string[], journalRecordsManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    console.log(journalRecordsManager.data.attributes, &#039;data.attributes&#039;)&lt;br /&gt;
    return fields.filter(f =&amp;gt; !journalRecordsManager.getFieldValue(f))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const emptyFields = getNotFilledFields(config.requiredFields, journalRecordManager)&lt;br /&gt;
&lt;br /&gt;
        if (isEmptyArray(emptyFields)) {&lt;br /&gt;
            showSuccessMessage(messages.success, socketId)&lt;br /&gt;
            return config.integrationResult&lt;br /&gt;
        } else errorHandlers.fieldsError(emptyFields, config.journalRecordId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-sample-moving.png|frame]]&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { JournalsRecordRouteController } from &amp;quot;../../src/servivces/worker/api/controllers/journals-record-route&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRouteDto } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
// Конфигурация аддона:&lt;br /&gt;
// - journalRecordId: 114 — запись из журнала &amp;quot;Лаб. воздуха / Анализы завершены&amp;quot;&lt;br /&gt;
// Аддон анализирует маршрут движения пробы по этапам ЛЖ&lt;br /&gt;
const config = {&lt;br /&gt;
    journalRecordId: 114&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Сообщения для отображения пользователю&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    // Формирует строку с маршрутом пробы: этапы через &amp;quot; -&amp;gt; &amp;quot;&lt;br /&gt;
    route: (routes: JournalRecordRouteDto[], recordId: number) =&amp;gt; &lt;br /&gt;
        `Маршрут пробы: ${routes.map(r =&amp;gt; `&amp;quot;${r.stageName}&amp;quot;`).join(&#039; -&amp;gt; &#039;)}. Id записи ЛЖ: ${recordId}`,&lt;br /&gt;
    // Сообщение, если проба не перемещалась между этапами&lt;br /&gt;
    notMoved: (recordId: number) =&amp;gt; &lt;br /&gt;
        `Движение пробы не происходило. Id записи ЛЖ: ${recordId}`&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Обработчики ошибок — централизованно выбрасывают исключения с понятными сообщениями&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Формирует конфиг диалога для GUI: тип (confirm), заголовок и текст&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Извлекает socketId из параметров запроса — необходим для отправки ответа в интерфейс&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Показывает пользователю модальное окно с ошибкой&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
// Показывает информационное сообщение (успех или статус)&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
// Определяет, перемещалась ли проба: если более одного этапа — значит, было движение&lt;br /&gt;
const isSampleMoved = (moves: JournalRecordRouteDto[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(moves) &amp;amp;&amp;amp; moves.length &amp;gt; 1&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Основная функция аддона:&lt;br /&gt;
 * Получает маршрут пробы по этапам лабораторного журнала и отображает его пользователю.&lt;br /&gt;
 * &lt;br /&gt;
 * Логика:&lt;br /&gt;
 * 1. Проверяет наличие параметров и подключение клиента (socketId).&lt;br /&gt;
 * 2. Запрашивает историю перемещений пробы через API.&lt;br /&gt;
 * 3. Если проба прошла более одного этапа — отображает маршрут.&lt;br /&gt;
 * 4. Если осталась на одном этапе — сообщает, что движения не было.&lt;br /&gt;
 */&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        // Контроллер для работы с маршрутом записи (история этапов)&lt;br /&gt;
        const journalsRecordRouteController = new JournalsRecordRouteController(apiInstance)&lt;br /&gt;
        // Получаем список этапов, через которые прошла проба&lt;br /&gt;
        const sampleMoves = await journalsRecordRouteController.getJournalRecordRoute(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (isSampleMoved(sampleMoves)) {&lt;br /&gt;
            // Проба перемещалась — показываем полный маршрут&lt;br /&gt;
            showSuccessMessage(messages.route(sampleMoves, config.journalRecordId), socketId)&lt;br /&gt;
        } else {&lt;br /&gt;
            // Проба не покидала исходный этап&lt;br /&gt;
            showSuccessMessage(messages.notMoved(config.journalRecordId), socketId)&lt;br /&gt;
        }&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        // При любой ошибке (сеть, доступ, данные) — показываем её пользователю&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-user-rights.png|frame]]&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import { &lt;br /&gt;
    PermissionController, &lt;br /&gt;
    PermissionResource, &lt;br /&gt;
    PermissionRwAccess&lt;br /&gt;
} from &amp;quot;../../src/servivces/worker/api/controllers/permisson&amp;quot;&lt;br /&gt;
&lt;br /&gt;
import { TableBuilder } from &amp;quot;../../src/gui/builder&amp;quot;&lt;br /&gt;
import type { DialogType, TableConfig } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRouteDto } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
// Отображение типов доступа в понятные пользователю названия&lt;br /&gt;
const accessNamesMap = {&lt;br /&gt;
    [PermissionRwAccess.EXEC]: &#039;Выполнение&#039;,&lt;br /&gt;
    [PermissionRwAccess.READ]: &#039;Чтение&#039;,&lt;br /&gt;
    [PermissionRwAccess.WRITE]: &#039;Запись&#039;,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Список ресурсов, права на которые необходимо проверить&lt;br /&gt;
const requiredResources = [&lt;br /&gt;
    PermissionResource.ANALYSIS_OBJECT, &lt;br /&gt;
    PermissionResource.METHODOLOGY,&lt;br /&gt;
    PermissionResource.JOURNAL_RECORD&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
// Отображение идентификаторов ресурсов в читаемые названия для отображения в таблице&lt;br /&gt;
const resourcesNamesMap = {&lt;br /&gt;
    [PermissionResource.ANALYSIS_OBJECT]: &#039;Объекты анализа&#039;, &lt;br /&gt;
    [PermissionResource.METHODOLOGY]: &#039;Методики&#039;, &lt;br /&gt;
    [PermissionResource.JOURNAL_RECORD]: &#039;Записи ЛЖ&#039;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Формирует конфигурацию таблицы с правами доступа:&lt;br /&gt;
 * - Для каждого ресурса проверяются все типы доступа (чтение, запись, выполнение).&lt;br /&gt;
 * - Возвращает готовую конфигурацию таблицы для отображения в GUI.&lt;br /&gt;
 */&lt;br /&gt;
const getAccessRightsTable = async (tableBuilder: TableBuilder, permissionController: PermissionController): Promise&amp;lt;TableConfig&amp;gt; =&amp;gt; {&lt;br /&gt;
    // Получаем список строк: комбинация &amp;quot;ресурс + тип доступа + наличие права&amp;quot;&lt;br /&gt;
    const rows = (await Promise.all(requiredResources.map(async (source: PermissionResource) =&amp;gt; {&lt;br /&gt;
            const sourceCombinations = await Promise.all(Object.keys(PermissionRwAccess).map(async (action) =&amp;gt; {&lt;br /&gt;
                const access = await permissionController.getSourcePermission(source, PermissionRwAccess[action])&lt;br /&gt;
                return { &lt;br /&gt;
                    source: resourcesNamesMap[source], &lt;br /&gt;
                    action: accessNamesMap[PermissionRwAccess[action]], &lt;br /&gt;
                    access: access.hasPermission &lt;br /&gt;
                }&lt;br /&gt;
            }))&lt;br /&gt;
            return sourceCombinations&lt;br /&gt;
        }   &lt;br /&gt;
    ))).flat()&lt;br /&gt;
&lt;br /&gt;
    // Строим таблицу с помощью TableBuilder&lt;br /&gt;
    const table = tableBuilder&lt;br /&gt;
        .name(&#039;simpleTable&#039;)&lt;br /&gt;
        .label(&#039;Таблица&#039;)&lt;br /&gt;
        .isAddable(false)           // Нельзя добавлять строки&lt;br /&gt;
        .actionsCol(false)          // Скрыть колонку действий&lt;br /&gt;
        .addColumn(&#039;Ресурс&#039;, &#039;source&#039;, false)&lt;br /&gt;
        .config(&#039;input&#039;, { name: &#039;source&#039;, rules: [{ required: true }] }, { placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .addColumn(&#039;Тип доступа&#039;, &#039;action&#039;, false)&lt;br /&gt;
        .config(&#039;input&#039;, { name: &#039;action&#039; }, { type: &#039;string&#039;, placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .addColumn(&#039;Доступ&#039;, &#039;access&#039;, false)&lt;br /&gt;
        .config(&#039;checkbox&#039;, { name: &#039;access&#039; }, { type: &#039;boolean&#039;, placeholder: &#039;&#039;, disabled: true }) // Только для просмотра&lt;br /&gt;
        .next()&lt;br /&gt;
        .setRows(rows)&lt;br /&gt;
        .build();&lt;br /&gt;
  &lt;br /&gt;
  return table&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Сообщения для пользователя&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Обработчики ошибок — централизованно выбрасывают исключения&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Формирует конфиг диалога для отправки в GUI&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Извлекает socketId из параметров — необходим для обратной связи с интерфейсом&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Показывает пользователю сообщение об ошибке&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
// Показывает таблицу с правами доступа в модальном окне&lt;br /&gt;
const showTable = async (builder: TableBuilder, controller: PermissionController, socketId: string) =&amp;gt; &lt;br /&gt;
    await getDataFromDialog({ type: &#039;input-data&#039;, config: [await getAccessRightsTable(builder, controller)] }, socketId)&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Основная функция аддона:&lt;br /&gt;
 * Отображает пользователю таблицу с текущими правами доступа на ключевые ресурсы системы.&lt;br /&gt;
 * &lt;br /&gt;
 * Логика:&lt;br /&gt;
 * 1. Проверяет наличие параметров и активное соединение (socketId).&lt;br /&gt;
 * 2. Запрашивает права для каждого ресурса и типа доступа.&lt;br /&gt;
 * 3. Формирует таблицу через TableBuilder.&lt;br /&gt;
 * 4. Показывает её в модальном окне.&lt;br /&gt;
 * &lt;br /&gt;
 * Результат: пользователь видит, какие действия разрешены для &amp;quot;Объектов анализа&amp;quot;, &amp;quot;Методик&amp;quot; и &amp;quot;Записей ЛЖ&amp;quot;.&lt;br /&gt;
 */&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        // Создаём экземпляры билдера и контроллера прав доступа&lt;br /&gt;
        await showTable(new TableBuilder(), new PermissionController(apiInstance), socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        // При любой ошибке — показываем её в модальном окне&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Fill-record-fields.png|frame]]&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
import { JournalRecordAttributeFieldResponse } from &amp;quot;../../src/servivces/worker/api/controllers/lj-eav-attribute-fields&amp;quot;&lt;br /&gt;
import { FieldMatch } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const date = new Date().toISOString().split(&#039;T&#039;)[0]&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        { name: &#039;Дата регистрации&#039;, value: date, keywords: [&#039;дат&#039;, &#039;регистр&#039;] },&lt;br /&gt;
        { name: &#039;Место отбора&#039;, value: &#039;Скважина 516563Г &#039;, keywords: [&#039;мест&#039;, &#039;отбор&#039;] },&lt;br /&gt;
        { name: &#039;Дата отбора&#039;, value: date, keywords: [&#039;дат&#039;, &#039;отбор&#039;] },&lt;br /&gt;
        { name: &#039;Шифр пробы&#039;, value: &#039;shifr 123&#039;, keywords: [&#039;дат&#039;, &#039;регистр&#039;]},&lt;br /&gt;
        { name: &#039;Список. Целое число&#039;, value: 666, keywords: [&#039;спис&#039;, &#039;цел&#039;, &#039;числ&#039;] },&lt;br /&gt;
        { name: &#039;Помещение&#039;, value: &#039;помещение&#039;, keywords: [&#039;помещ&#039;]},&lt;br /&gt;
    ] as FieldMatch[],&lt;br /&gt;
    journalRecordId: 118, // Мк / 111&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: (recordId) =&amp;gt; `Все необходимые поля заполнены! Id записи: ${recordId}`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const fieldsMatcher = (field: JournalRecordAttributeFieldResponse, searchField: FieldMatch) =&amp;gt; {&lt;br /&gt;
    return searchField.keywords.every(k =&amp;gt; String(field.name).toLocaleLowerCase().includes(k.toLocaleLowerCase()))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const fillFields = async (fields: FieldMatch[], journalRecordsManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    await journalRecordsManager.setFieldsValues(fields, journalRecordsManager, fieldsMatcher)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        await fillFields(config.requiredFields, journalRecordManager)&lt;br /&gt;
        showSuccessMessage(messages.success(journalRecordManager.journalRecordId), socketId)&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Copy-journal-record.png|frame]]&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { JournalRecordsController } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRequest, JournalRecordResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { JournalRecordAttributeResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
type DataType = JournalRecordAttributeResponse[&#039;field&#039;][&#039;dataType&#039;]&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    journalRecordId: 208,  // Лаб. воздуха / Регистрация&lt;br /&gt;
    nextJournalRecordStage: 2, //  Лаб. воздуха / Анализ&lt;br /&gt;
    nextStageName: &#039;Анализ&#039;,&lt;br /&gt;
    nextRouteId: 50,&lt;br /&gt;
    integrationResult: { message: &#039;Запись ЛЖ скопирована успешно!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const getDateString = (ISODate: unknown | null = null) =&amp;gt; {&lt;br /&gt;
    return new Date(String(ISODate)).toISOString().split(&#039;T&#039;)[0]&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getTimeString = (ISODate: unknown | null = null) =&amp;gt; {&lt;br /&gt;
    return new Date(String(ISODate)).toISOString().split(&#039;T&#039;)[1].split(&#039;.&#039;)[0]&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const createRecord = async (journalRecordController: JournalRecordsController, baseRecord: JournalRecordResponse, newStageId) =&amp;gt; {&lt;br /&gt;
    const getAttrValue = (value: unknown, dataType: DataType) =&amp;gt; {&lt;br /&gt;
        if (dataType === &amp;quot;DATE&amp;quot;) return getDateString(value)&lt;br /&gt;
        if (dataType === &amp;quot;TIME&amp;quot;) return getTimeString(value)&lt;br /&gt;
        return value&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    const getBindings = (baseRecord: JournalRecordResponse): number[] | null =&amp;gt; {&lt;br /&gt;
        return baseRecord?.companyLevelBindings?.map(b =&amp;gt; b?.id) || null&lt;br /&gt;
    } &lt;br /&gt;
&lt;br /&gt;
    const getRecordRequest = (baseRecord: JournalRecordResponse, newStageId: number): JournalRecordRequest =&amp;gt; {&lt;br /&gt;
        return {&lt;br /&gt;
            routeId: baseRecord?.route?.id,&lt;br /&gt;
            stageId: newStageId,&lt;br /&gt;
            analysisObjectId: baseRecord?.analysisObject?.id,&lt;br /&gt;
            sampleTypeId: baseRecord.sampleType.id,&lt;br /&gt;
            attributes: baseRecord.attributes.map(attr =&amp;gt; {&lt;br /&gt;
                return {&lt;br /&gt;
                    fieldId: attr.field.id,&lt;br /&gt;
                    stageId: newStageId,&lt;br /&gt;
                    data: {&lt;br /&gt;
                        ...attr.data, &lt;br /&gt;
                        value: getAttrValue(attr?.data?.value, attr?.field?.dataType),&lt;br /&gt;
                        id: undefined&lt;br /&gt;
                    },&lt;br /&gt;
                    statusId: null&lt;br /&gt;
                }&lt;br /&gt;
            }),&lt;br /&gt;
            companyLevelBindings: getBindings(baseRecord)&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    return await journalRecordController.createRecord(getRecordRequest(baseRecord, newStageId))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const journalRecordController = new JournalRecordsController(apiInstance)&lt;br /&gt;
        const newRecord = await createRecord(journalRecordController, journalRecordManager.data, config.nextJournalRecordStage)&lt;br /&gt;
        showSuccessMessage(`${messages.success}. Id изначальной записи: ${config.journalRecordId}. Id новой записи ЛЖ: ${newRecord.id}. Этап: ${config.nextStageName}`, socketId)&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2379</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2379"/>
		<updated>2026-04-03T06:29:12Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Проверка движения пробы. check-sample-moving */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { MessageBoxResults } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { FetchApiClient } from &amp;quot;../../src/servivces/worker/api/ApiClient&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
&lt;br /&gt;
// Конфигурация аддона:&lt;br /&gt;
// - Поле &amp;quot;Дата регистрации&amp;quot; — контрольная точка для расчёта срока&lt;br /&gt;
// - expirationLimit: 10 дней — максимальный допустимый срок хранения пробы&lt;br /&gt;
// - journalRecordId: 201 — запись из журнала &amp;quot;Лаб. воздуха / Анализы завершены&amp;quot;&lt;br /&gt;
const config = {&lt;br /&gt;
    dateField: &#039;Дата регистрации&#039;,&lt;br /&gt;
    expirationLimit: 10,&lt;br /&gt;
    journalRecordId: 201,&lt;br /&gt;
    integrationResult: { message: &#039;Срок регистрации пробы не истёк!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Сообщения для пользовательских уведомлений и ошибок&lt;br /&gt;
const messages = {&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    // Успех: информирует пользователя, что срок не вышел&lt;br /&gt;
    success: (recordId) =&amp;gt; `Срок регистрации пробы не истёк! Id записи ${recordId}`,&lt;br /&gt;
    // Ошибка: превышен лимит хранения&lt;br /&gt;
    expired: (limit, recordId) =&amp;gt; `Срок регистрации пробы истёк!\nПрошло более ${limit} дней с момента регистрации. Id записи: ${recordId}`,&lt;br /&gt;
    // Ошибка: отсутствует дата в нужном поле&lt;br /&gt;
    dateField: (dateField, stageName, journalRecordId) =&amp;gt; [&lt;br /&gt;
        `Не заполнено либо отсутствует поле &amp;quot;${dateField}&amp;quot;`,&lt;br /&gt;
        `в этапе &amp;quot;${stageName || &#039;&#039;}&amp;quot;.\nId записи: ${journalRecordId}`&lt;br /&gt;
    ].join(&#039; &#039;)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Централизованные обработчики ошибок — обеспечивают единообразие и чистоту основной логики&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    // Вызывается, если поле с датой не заполнено&lt;br /&gt;
    dateError: (dateField, stageName, journalRecordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.dateField(dateField, stageName, journalRecordId))&lt;br /&gt;
    },&lt;br /&gt;
    // Вызывается при превышении срока хранения&lt;br /&gt;
    expirationError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.expired(config.expirationLimit, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Извлекает socketId из queryParams — необходим для отправки диалогов в GUI&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Переводит дату в количество дней с начала эпохи (для арифметики дат)&lt;br /&gt;
const getDaysByDate = (date: Date) =&amp;gt; {&lt;br /&gt;
    const dayMs = 60 * 60 * 24 * 1000&lt;br /&gt;
    const timestamp = date.getTime()&lt;br /&gt;
    return timestamp / dayMs&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Формирует конфиг диалога: тип, заголовок и сообщение для отображения в GUI&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Рассчитывает, сколько дней прошло с момента регистрации пробы&lt;br /&gt;
const getExpirationOffset = (registrationDate: string) =&amp;gt; {&lt;br /&gt;
    const regDate = new Date(registrationDate)&lt;br /&gt;
    const today = new Date()&lt;br /&gt;
    return getDaysByDate(today) - getDaysByDate(regDate)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Показывает пользователю сообщение об ошибке&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
// Показывает пользователю уведомление об успешной проверке&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Основная функция аддона:&lt;br /&gt;
 * Проверяет, не истёк ли срок хранения пробы (с момента &amp;quot;Дата регистрации&amp;quot;).&lt;br /&gt;
 * &lt;br /&gt;
 * Логика:&lt;br /&gt;
 * 1. Получает значение поля &amp;quot;Дата регистрации&amp;quot; из записи журнала.&lt;br /&gt;
 * 2. Если дата не указана — ошибка.&lt;br /&gt;
 * 3. Если с даты регистрации прошло &amp;gt;= 10 дней — ошибка.&lt;br /&gt;
 * 4. Иначе — успех, показываем подтверждение.&lt;br /&gt;
 */&lt;br /&gt;
async function main({ queryParams, ...apiInstance }) {&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        // Получаем доступ к данным записи журнала&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        // Извлекаем значение поля &amp;quot;Дата регистрации&amp;quot;&lt;br /&gt;
        const date = journalRecordManager.getFieldValue(config.dateField)&lt;br /&gt;
&lt;br /&gt;
        // Если дата не заполнена — прерываем с пояснением&lt;br /&gt;
        if (!date) {&lt;br /&gt;
            errorHandlers.dateError(config.dateField, journalRecordManager?.data?.stage?.stage?.name, config.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Считаем, сколько дней прошло с регистрации&lt;br /&gt;
        const expirationOffset = getExpirationOffset(date as string)&lt;br /&gt;
&lt;br /&gt;
        // Если срок хранения превышен — ошибка&lt;br /&gt;
        if (expirationOffset &amp;gt;= config.expirationLimit) {&lt;br /&gt;
            errorHandlers.expirationError()&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Всё в порядке — информируем пользователя&lt;br /&gt;
        await showSuccessMessage(messages.success(config.journalRecordId), socketId)&lt;br /&gt;
&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        // При любой ошибке — показываем её в модальном окне&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordIndicatorResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
// Описание свойств показателя (индикатора), которые нужно проверить&lt;br /&gt;
// Каждое свойство имеет метку (для отображения пользователю) и геттер значения&lt;br /&gt;
type IndicatorProp = { label: string, get: () =&amp;gt; unknown }&lt;br /&gt;
type IndicatorPropsConfig = Record&amp;lt;string, IndicatorProp&amp;gt;&lt;br /&gt;
&lt;br /&gt;
// Конфигурация полей, которые проверяются у каждого показателя&lt;br /&gt;
// Например: &amp;quot;Исполнители&amp;quot;, &amp;quot;Методики&amp;quot;, &amp;quot;Средний результат&amp;quot; и т.д.&lt;br /&gt;
const indicatorPropsConfig = (indicator): IndicatorPropsConfig =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        executors: {&lt;br /&gt;
            label: &#039;Исполнители&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.executors&lt;br /&gt;
        },&lt;br /&gt;
        methodic: {&lt;br /&gt;
            label: &#039;Методики&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.methodics&lt;br /&gt;
        },&lt;br /&gt;
        averageResult: {&lt;br /&gt;
            label: &#039;Средний результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageValue&lt;br /&gt;
        },&lt;br /&gt;
        averageRoundedValue: {&lt;br /&gt;
            label: &#039;Средний округлённый результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageRoundedValue&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Конфигурация аддона:&lt;br /&gt;
// - indicatorsList: список обязательных показателей (например, &amp;quot;Аммоний-ион&amp;quot;)&lt;br /&gt;
// - journalRecordId: 144 — тестовая запись (&amp;quot;Мк / Тест_Ушакова&amp;quot;)&lt;br /&gt;
const config = {&lt;br /&gt;
    indicatorsList: [&lt;br /&gt;
        &#039;Аммоний-ион&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144,&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Сообщения для пользовательских уведомлений и ошибок&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyIndicators: (recordId) =&amp;gt; `в записи с Id ${recordId} отсутствуют показатели`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
    indicators: (indicators: string[], recordId) =&amp;gt; `Показатели ${indicators.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} отсутствуют в записи с ID ${recordId}`,&lt;br /&gt;
    // Формирует детализированное сообщение о незаполненных полях внутри показателей&lt;br /&gt;
    emptyIndicatorsProps: (props: Record&amp;lt;string, string[]&amp;gt;, recordId) =&amp;gt; {&lt;br /&gt;
        const propsString = Object.keys(props).map(prop =&amp;gt; !isEmptyArray(props[prop]) ? `${prop}: ${props[prop].map(p =&amp;gt; `&amp;quot;${p}&amp;quot;`).join(&#039;, &#039;)}` : &#039;&#039;).join(&#039;; &#039;)&lt;br /&gt;
        return `В записи с Id ${recordId} отсутствуют либо не заполнены следующие поля у показателей: ${propsString}`&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Централизованные обработчики ошибок — обеспечивают единообразие и чистоту основной логики&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    indicatorsError: (recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyIndicators(recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFoundIndicators: (indicators: string[], recordId: number) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.indicators(indicators, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFilledIndicatorProps: (props: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyIndicatorsProps(props, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Формирует конфиг диалога для отправки в GUI: тип, заголовок и сообщение&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Извлекает socketId из queryParams — необходим для связи с интерфейсом&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Показывает пользователю модальное окно с ошибкой&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
// Показывает уведомление об успешной проверке&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
// Универсальная проверка: является ли массив пустым (с учётом типа)&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(arr) &amp;amp;&amp;amp; arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Проверяет, есть ли у записи хотя бы один показатель&lt;br /&gt;
const hasIndicators = (journalRecordManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
    return Array.isArray(indicators) &amp;amp;&amp;amp; indicators.length &amp;gt; 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Определяет, какие из требуемых показателей отсутствуют в записи&lt;br /&gt;
const getNotFoundIndicators = (indicators: JournalRecordIndicatorResponse[], indicatorsList = config.indicatorsList) =&amp;gt; {&lt;br /&gt;
    return indicatorsList.filter(ind =&amp;gt; &lt;br /&gt;
        indicators.every(ljInd =&amp;gt; ljInd?.indicator?.name?.toLowerCase() !== ind.toLowerCase())&lt;br /&gt;
    )&lt;br /&gt;
} &lt;br /&gt;
&lt;br /&gt;
// Собирает список незаполненных полей для каждого показателя&lt;br /&gt;
// Возвращает объект: { &amp;quot;Аммоний-ион&amp;quot;: [&amp;quot;Исполнители&amp;quot;, &amp;quot;Средний результат&amp;quot;] }&lt;br /&gt;
const getNotFilledResultsFields = (indicators: JournalRecordIndicatorResponse[]): Record&amp;lt;string, string[]&amp;gt; =&amp;gt; {&lt;br /&gt;
    return indicators.reduce((acc, indicator) =&amp;gt; {&lt;br /&gt;
        const config = indicatorPropsConfig(indicator)&lt;br /&gt;
        return {&lt;br /&gt;
            ...acc,&lt;br /&gt;
            [indicator?.indicator?.name]: Object.keys(config).reduce((acc, prop) =&amp;gt; {&lt;br /&gt;
                if (!config[prop].get()) return [...acc, config[prop].label]&lt;br /&gt;
                return acc&lt;br /&gt;
            }, [] as string[])&lt;br /&gt;
        }&lt;br /&gt;
    }, {}) as Record&amp;lt;string, string[]&amp;gt;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Проверяет, есть ли хотя бы один показатель с незаполненными полями&lt;br /&gt;
const hasNotFilledProps = (indicatorsProps: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
    return Object.keys(indicatorsProps).some(indicator =&amp;gt; !isEmptyArray(indicatorsProps[indicator]))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Проверяет, есть ли отсутствующие показатели в списке&lt;br /&gt;
const hasNotFoundIndicators = (indicators: string[]) =&amp;gt; !isEmptyArray(indicators)&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Основная функция аддона:&lt;br /&gt;
 * Проверяет заполненность:&lt;br /&gt;
 * 1. Наличие указанных показателей (например, &amp;quot;Аммоний-ион&amp;quot;).&lt;br /&gt;
 * 2. Заполненность внутренних полей каждого показателя (исполнители, методики и т.д.).&lt;br /&gt;
 * &lt;br /&gt;
 * При обнаружении проблем — прерывает выполнение и показывает детализированную ошибку.&lt;br /&gt;
 */&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        // Проверка: есть ли вообще показатели в записи?&lt;br /&gt;
        if (!hasIndicators(journalRecordManager)) {&lt;br /&gt;
            errorHandlers.indicatorsError(journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
&lt;br /&gt;
        // Проверка: все ли требуемые показатели присутствуют?&lt;br /&gt;
        const notFoundIndicators = getNotFoundIndicators(indicators)&lt;br /&gt;
        if (hasNotFoundIndicators(notFoundIndicators)) { &lt;br /&gt;
            errorHandlers.notFoundIndicators(notFoundIndicators, journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Проверка: все ли поля у показателей заполнены?&lt;br /&gt;
        const emptyIndicatorProps = getNotFilledResultsFields(indicators)&lt;br /&gt;
        if (hasNotFilledProps(emptyIndicatorProps)) {&lt;br /&gt;
            errorHandlers.notFilledIndicatorProps(emptyIndicatorProps)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Все проверки пройдены — информируем пользователя&lt;br /&gt;
        showSuccessMessage(messages.success, socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        // При любой ошибке — показываем её в модальном окне&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-record-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Список. Целое число&#039;,&lt;br /&gt;
        &#039;Строка&#039;,&lt;br /&gt;
        &#039;Целое число&#039;,&lt;br /&gt;
        &#039;Вещественное число&#039;,&lt;br /&gt;
        &#039;Логический тип&#039;,&lt;br /&gt;
        &#039;Дата&#039;,&lt;br /&gt;
        &#039;Время&#039;,&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144,  // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFilledFields = (fields: string[], journalRecordsManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    console.log(journalRecordsManager.data.attributes, &#039;data.attributes&#039;)&lt;br /&gt;
    return fields.filter(f =&amp;gt; !journalRecordsManager.getFieldValue(f))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const emptyFields = getNotFilledFields(config.requiredFields, journalRecordManager)&lt;br /&gt;
&lt;br /&gt;
        if (isEmptyArray(emptyFields)) {&lt;br /&gt;
            showSuccessMessage(messages.success, socketId)&lt;br /&gt;
            return config.integrationResult&lt;br /&gt;
        } else errorHandlers.fieldsError(emptyFields, config.journalRecordId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-sample-moving.png|frame]]&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { JournalsRecordRouteController } from &amp;quot;../../src/servivces/worker/api/controllers/journals-record-route&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRouteDto } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
// Конфигурация аддона:&lt;br /&gt;
// - journalRecordId: 114 — запись из журнала &amp;quot;Лаб. воздуха / Анализы завершены&amp;quot;&lt;br /&gt;
// Аддон анализирует маршрут движения пробы по этапам ЛЖ&lt;br /&gt;
const config = {&lt;br /&gt;
    journalRecordId: 114&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Сообщения для отображения пользователю&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    // Формирует строку с маршрутом пробы: этапы через &amp;quot; -&amp;gt; &amp;quot;&lt;br /&gt;
    route: (routes: JournalRecordRouteDto[], recordId: number) =&amp;gt; &lt;br /&gt;
        `Маршрут пробы: ${routes.map(r =&amp;gt; `&amp;quot;${r.stageName}&amp;quot;`).join(&#039; -&amp;gt; &#039;)}. Id записи ЛЖ: ${recordId}`,&lt;br /&gt;
    // Сообщение, если проба не перемещалась между этапами&lt;br /&gt;
    notMoved: (recordId: number) =&amp;gt; &lt;br /&gt;
        `Движение пробы не происходило. Id записи ЛЖ: ${recordId}`&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Обработчики ошибок — централизованно выбрасывают исключения с понятными сообщениями&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Формирует конфиг диалога для GUI: тип (confirm), заголовок и текст&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Извлекает socketId из параметров запроса — необходим для отправки ответа в интерфейс&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Показывает пользователю модальное окно с ошибкой&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
// Показывает информационное сообщение (успех или статус)&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
// Определяет, перемещалась ли проба: если более одного этапа — значит, было движение&lt;br /&gt;
const isSampleMoved = (moves: JournalRecordRouteDto[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(moves) &amp;amp;&amp;amp; moves.length &amp;gt; 1&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Основная функция аддона:&lt;br /&gt;
 * Получает маршрут пробы по этапам лабораторного журнала и отображает его пользователю.&lt;br /&gt;
 * &lt;br /&gt;
 * Логика:&lt;br /&gt;
 * 1. Проверяет наличие параметров и подключение клиента (socketId).&lt;br /&gt;
 * 2. Запрашивает историю перемещений пробы через API.&lt;br /&gt;
 * 3. Если проба прошла более одного этапа — отображает маршрут.&lt;br /&gt;
 * 4. Если осталась на одном этапе — сообщает, что движения не было.&lt;br /&gt;
 */&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        // Контроллер для работы с маршрутом записи (история этапов)&lt;br /&gt;
        const journalsRecordRouteController = new JournalsRecordRouteController(apiInstance)&lt;br /&gt;
        // Получаем список этапов, через которые прошла проба&lt;br /&gt;
        const sampleMoves = await journalsRecordRouteController.getJournalRecordRoute(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (isSampleMoved(sampleMoves)) {&lt;br /&gt;
            // Проба перемещалась — показываем полный маршрут&lt;br /&gt;
            showSuccessMessage(messages.route(sampleMoves, config.journalRecordId), socketId)&lt;br /&gt;
        } else {&lt;br /&gt;
            // Проба не покидала исходный этап&lt;br /&gt;
            showSuccessMessage(messages.notMoved(config.journalRecordId), socketId)&lt;br /&gt;
        }&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        // При любой ошибке (сеть, доступ, данные) — показываем её пользователю&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-user-rights.png|frame]]&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import { &lt;br /&gt;
    PermissionController, &lt;br /&gt;
    PermissionResource, &lt;br /&gt;
    PermissionRwAccess&lt;br /&gt;
} from &amp;quot;../../src/servivces/worker/api/controllers/permisson&amp;quot;&lt;br /&gt;
&lt;br /&gt;
import { TableBuilder } from &amp;quot;../../src/gui/builder&amp;quot;&lt;br /&gt;
import type { DialogType, TableConfig } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRouteDto } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const accessNamesMap = {&lt;br /&gt;
    [PermissionRwAccess.EXEC]: &#039;Выполнение&#039;,&lt;br /&gt;
    [PermissionRwAccess.READ]: &#039;Чтение&#039;,&lt;br /&gt;
    [PermissionRwAccess.WRITE]: &#039;Запись&#039;,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const requiredResources = [&lt;br /&gt;
    PermissionResource.ANALYSIS_OBJECT, &lt;br /&gt;
    PermissionResource.METHODOLOGY,&lt;br /&gt;
    PermissionResource.JOURNAL_RECORD&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
const resourcesNamesMap = {&lt;br /&gt;
    [PermissionResource.ANALYSIS_OBJECT]: &#039;Объекты анализа&#039;, &lt;br /&gt;
    [PermissionResource.METHODOLOGY]: &#039;Методики&#039;, &lt;br /&gt;
    [PermissionResource.JOURNAL_RECORD]: &#039;Записи ЛЖ&#039;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getAccessRightsTable = async (tableBuilder: TableBuilder, permissionController: PermissionController): Promise&amp;lt;TableConfig&amp;gt; =&amp;gt; {&lt;br /&gt;
    const rows = (await Promise.all(requiredResources.map(async (source: PermissionResource) =&amp;gt; {&lt;br /&gt;
            const sourceCombinations = await Promise.all(Object.keys(PermissionRwAccess).map(async (action) =&amp;gt; {&lt;br /&gt;
                const access = await permissionController.getSourcePermission(source, PermissionRwAccess[action])&lt;br /&gt;
                return { source: resourcesNamesMap[source], action: accessNamesMap[PermissionRwAccess[action]], access: access.hasPermission }&lt;br /&gt;
            }))&lt;br /&gt;
            return sourceCombinations&lt;br /&gt;
        }   &lt;br /&gt;
    ))).flat()&lt;br /&gt;
    const table = tableBuilder&lt;br /&gt;
        .name(&#039;simpleTable&#039;)&lt;br /&gt;
        .label(&#039;Таблица&#039;)&lt;br /&gt;
        .isAddable(false)&lt;br /&gt;
        .actionsCol(false)&lt;br /&gt;
        .addColumn(&#039;Ресурс&#039;, &#039;source&#039;, false)&lt;br /&gt;
        .config(&#039;input&#039;, { name: &#039;source&#039;, rules: [{ required: true }] }, { placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .addColumn(&#039;Тип доступа&#039;, &#039;action&#039;, false)&lt;br /&gt;
        .config(&#039;input&#039;, { name: &#039;action&#039; }, { type: &#039;string&#039;, placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .addColumn(&#039;Доступ&#039;, &#039;access&#039;, false)&lt;br /&gt;
        .config(&#039;checkbox&#039;, { name: &#039;access&#039; }, { type: &#039;boolean&#039;, placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .setRows(rows)&lt;br /&gt;
        .build();&lt;br /&gt;
  return table&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    route: (routes: JournalRecordRouteDto[], recordId: number) =&amp;gt; `Маршрут пробы: ${routes.map(r =&amp;gt; `&amp;quot;${r.stageName}&amp;quot;`).join(&#039; -&amp;gt; &#039;)}. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
    notMoved: (recordId: number) =&amp;gt; `Движение пробы не происходило. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const showTable = async (builder: TableBuilder, controller: PermissionController, socketId: string) =&amp;gt; &lt;br /&gt;
    await getDataFromDialog({ type: &#039;input-data&#039;, config: [await getAccessRightsTable(builder, controller)] }, socketId)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
    try {&lt;br /&gt;
        await showTable(new TableBuilder, new PermissionController(apiInstance), socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Fill-record-fields.png|frame]]&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
import { JournalRecordAttributeFieldResponse } from &amp;quot;../../src/servivces/worker/api/controllers/lj-eav-attribute-fields&amp;quot;&lt;br /&gt;
import { FieldMatch } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const date = new Date().toISOString().split(&#039;T&#039;)[0]&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        { name: &#039;Дата регистрации&#039;, value: date, keywords: [&#039;дат&#039;, &#039;регистр&#039;] },&lt;br /&gt;
        { name: &#039;Место отбора&#039;, value: &#039;Скважина 516563Г &#039;, keywords: [&#039;мест&#039;, &#039;отбор&#039;] },&lt;br /&gt;
        { name: &#039;Дата отбора&#039;, value: date, keywords: [&#039;дат&#039;, &#039;отбор&#039;] },&lt;br /&gt;
        { name: &#039;Шифр пробы&#039;, value: &#039;shifr 123&#039;, keywords: [&#039;дат&#039;, &#039;регистр&#039;]},&lt;br /&gt;
        { name: &#039;Список. Целое число&#039;, value: 666, keywords: [&#039;спис&#039;, &#039;цел&#039;, &#039;числ&#039;] },&lt;br /&gt;
        { name: &#039;Помещение&#039;, value: &#039;помещение&#039;, keywords: [&#039;помещ&#039;]},&lt;br /&gt;
    ] as FieldMatch[],&lt;br /&gt;
    journalRecordId: 118, // Мк / 111&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: (recordId) =&amp;gt; `Все необходимые поля заполнены! Id записи: ${recordId}`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const fieldsMatcher = (field: JournalRecordAttributeFieldResponse, searchField: FieldMatch) =&amp;gt; {&lt;br /&gt;
    return searchField.keywords.every(k =&amp;gt; String(field.name).toLocaleLowerCase().includes(k.toLocaleLowerCase()))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const fillFields = async (fields: FieldMatch[], journalRecordsManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    await journalRecordsManager.setFieldsValues(fields, journalRecordsManager, fieldsMatcher)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        await fillFields(config.requiredFields, journalRecordManager)&lt;br /&gt;
        showSuccessMessage(messages.success(journalRecordManager.journalRecordId), socketId)&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Copy-journal-record.png|frame]]&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { JournalRecordsController } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRequest, JournalRecordResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { JournalRecordAttributeResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
type DataType = JournalRecordAttributeResponse[&#039;field&#039;][&#039;dataType&#039;]&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    journalRecordId: 208,  // Лаб. воздуха / Регистрация&lt;br /&gt;
    nextJournalRecordStage: 2, //  Лаб. воздуха / Анализ&lt;br /&gt;
    nextStageName: &#039;Анализ&#039;,&lt;br /&gt;
    nextRouteId: 50,&lt;br /&gt;
    integrationResult: { message: &#039;Запись ЛЖ скопирована успешно!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const getDateString = (ISODate: unknown | null = null) =&amp;gt; {&lt;br /&gt;
    return new Date(String(ISODate)).toISOString().split(&#039;T&#039;)[0]&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getTimeString = (ISODate: unknown | null = null) =&amp;gt; {&lt;br /&gt;
    return new Date(String(ISODate)).toISOString().split(&#039;T&#039;)[1].split(&#039;.&#039;)[0]&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const createRecord = async (journalRecordController: JournalRecordsController, baseRecord: JournalRecordResponse, newStageId) =&amp;gt; {&lt;br /&gt;
    const getAttrValue = (value: unknown, dataType: DataType) =&amp;gt; {&lt;br /&gt;
        if (dataType === &amp;quot;DATE&amp;quot;) return getDateString(value)&lt;br /&gt;
        if (dataType === &amp;quot;TIME&amp;quot;) return getTimeString(value)&lt;br /&gt;
        return value&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    const getBindings = (baseRecord: JournalRecordResponse): number[] | null =&amp;gt; {&lt;br /&gt;
        return baseRecord?.companyLevelBindings?.map(b =&amp;gt; b?.id) || null&lt;br /&gt;
    } &lt;br /&gt;
&lt;br /&gt;
    const getRecordRequest = (baseRecord: JournalRecordResponse, newStageId: number): JournalRecordRequest =&amp;gt; {&lt;br /&gt;
        return {&lt;br /&gt;
            routeId: baseRecord?.route?.id,&lt;br /&gt;
            stageId: newStageId,&lt;br /&gt;
            analysisObjectId: baseRecord?.analysisObject?.id,&lt;br /&gt;
            sampleTypeId: baseRecord.sampleType.id,&lt;br /&gt;
            attributes: baseRecord.attributes.map(attr =&amp;gt; {&lt;br /&gt;
                return {&lt;br /&gt;
                    fieldId: attr.field.id,&lt;br /&gt;
                    stageId: newStageId,&lt;br /&gt;
                    data: {&lt;br /&gt;
                        ...attr.data, &lt;br /&gt;
                        value: getAttrValue(attr?.data?.value, attr?.field?.dataType),&lt;br /&gt;
                        id: undefined&lt;br /&gt;
                    },&lt;br /&gt;
                    statusId: null&lt;br /&gt;
                }&lt;br /&gt;
            }),&lt;br /&gt;
            companyLevelBindings: getBindings(baseRecord)&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    return await journalRecordController.createRecord(getRecordRequest(baseRecord, newStageId))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const journalRecordController = new JournalRecordsController(apiInstance)&lt;br /&gt;
        const newRecord = await createRecord(journalRecordController, journalRecordManager.data, config.nextJournalRecordStage)&lt;br /&gt;
        showSuccessMessage(`${messages.success}. Id изначальной записи: ${config.journalRecordId}. Id новой записи ЛЖ: ${newRecord.id}. Этап: ${config.nextStageName}`, socketId)&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2378</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2378"/>
		<updated>2026-04-03T06:22:21Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Проверка заполненности полей показателей. check-indicator-fields-filled */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { MessageBoxResults } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { FetchApiClient } from &amp;quot;../../src/servivces/worker/api/ApiClient&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
&lt;br /&gt;
// Конфигурация аддона:&lt;br /&gt;
// - Поле &amp;quot;Дата регистрации&amp;quot; — контрольная точка для расчёта срока&lt;br /&gt;
// - expirationLimit: 10 дней — максимальный допустимый срок хранения пробы&lt;br /&gt;
// - journalRecordId: 201 — запись из журнала &amp;quot;Лаб. воздуха / Анализы завершены&amp;quot;&lt;br /&gt;
const config = {&lt;br /&gt;
    dateField: &#039;Дата регистрации&#039;,&lt;br /&gt;
    expirationLimit: 10,&lt;br /&gt;
    journalRecordId: 201,&lt;br /&gt;
    integrationResult: { message: &#039;Срок регистрации пробы не истёк!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Сообщения для пользовательских уведомлений и ошибок&lt;br /&gt;
const messages = {&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    // Успех: информирует пользователя, что срок не вышел&lt;br /&gt;
    success: (recordId) =&amp;gt; `Срок регистрации пробы не истёк! Id записи ${recordId}`,&lt;br /&gt;
    // Ошибка: превышен лимит хранения&lt;br /&gt;
    expired: (limit, recordId) =&amp;gt; `Срок регистрации пробы истёк!\nПрошло более ${limit} дней с момента регистрации. Id записи: ${recordId}`,&lt;br /&gt;
    // Ошибка: отсутствует дата в нужном поле&lt;br /&gt;
    dateField: (dateField, stageName, journalRecordId) =&amp;gt; [&lt;br /&gt;
        `Не заполнено либо отсутствует поле &amp;quot;${dateField}&amp;quot;`,&lt;br /&gt;
        `в этапе &amp;quot;${stageName || &#039;&#039;}&amp;quot;.\nId записи: ${journalRecordId}`&lt;br /&gt;
    ].join(&#039; &#039;)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Централизованные обработчики ошибок — обеспечивают единообразие и чистоту основной логики&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    // Вызывается, если поле с датой не заполнено&lt;br /&gt;
    dateError: (dateField, stageName, journalRecordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.dateField(dateField, stageName, journalRecordId))&lt;br /&gt;
    },&lt;br /&gt;
    // Вызывается при превышении срока хранения&lt;br /&gt;
    expirationError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.expired(config.expirationLimit, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Извлекает socketId из queryParams — необходим для отправки диалогов в GUI&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Переводит дату в количество дней с начала эпохи (для арифметики дат)&lt;br /&gt;
const getDaysByDate = (date: Date) =&amp;gt; {&lt;br /&gt;
    const dayMs = 60 * 60 * 24 * 1000&lt;br /&gt;
    const timestamp = date.getTime()&lt;br /&gt;
    return timestamp / dayMs&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Формирует конфиг диалога: тип, заголовок и сообщение для отображения в GUI&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Рассчитывает, сколько дней прошло с момента регистрации пробы&lt;br /&gt;
const getExpirationOffset = (registrationDate: string) =&amp;gt; {&lt;br /&gt;
    const regDate = new Date(registrationDate)&lt;br /&gt;
    const today = new Date()&lt;br /&gt;
    return getDaysByDate(today) - getDaysByDate(regDate)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Показывает пользователю сообщение об ошибке&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
// Показывает пользователю уведомление об успешной проверке&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Основная функция аддона:&lt;br /&gt;
 * Проверяет, не истёк ли срок хранения пробы (с момента &amp;quot;Дата регистрации&amp;quot;).&lt;br /&gt;
 * &lt;br /&gt;
 * Логика:&lt;br /&gt;
 * 1. Получает значение поля &amp;quot;Дата регистрации&amp;quot; из записи журнала.&lt;br /&gt;
 * 2. Если дата не указана — ошибка.&lt;br /&gt;
 * 3. Если с даты регистрации прошло &amp;gt;= 10 дней — ошибка.&lt;br /&gt;
 * 4. Иначе — успех, показываем подтверждение.&lt;br /&gt;
 */&lt;br /&gt;
async function main({ queryParams, ...apiInstance }) {&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        // Получаем доступ к данным записи журнала&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        // Извлекаем значение поля &amp;quot;Дата регистрации&amp;quot;&lt;br /&gt;
        const date = journalRecordManager.getFieldValue(config.dateField)&lt;br /&gt;
&lt;br /&gt;
        // Если дата не заполнена — прерываем с пояснением&lt;br /&gt;
        if (!date) {&lt;br /&gt;
            errorHandlers.dateError(config.dateField, journalRecordManager?.data?.stage?.stage?.name, config.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Считаем, сколько дней прошло с регистрации&lt;br /&gt;
        const expirationOffset = getExpirationOffset(date as string)&lt;br /&gt;
&lt;br /&gt;
        // Если срок хранения превышен — ошибка&lt;br /&gt;
        if (expirationOffset &amp;gt;= config.expirationLimit) {&lt;br /&gt;
            errorHandlers.expirationError()&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Всё в порядке — информируем пользователя&lt;br /&gt;
        await showSuccessMessage(messages.success(config.journalRecordId), socketId)&lt;br /&gt;
&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        // При любой ошибке — показываем её в модальном окне&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordIndicatorResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
// Описание свойств показателя (индикатора), которые нужно проверить&lt;br /&gt;
// Каждое свойство имеет метку (для отображения пользователю) и геттер значения&lt;br /&gt;
type IndicatorProp = { label: string, get: () =&amp;gt; unknown }&lt;br /&gt;
type IndicatorPropsConfig = Record&amp;lt;string, IndicatorProp&amp;gt;&lt;br /&gt;
&lt;br /&gt;
// Конфигурация полей, которые проверяются у каждого показателя&lt;br /&gt;
// Например: &amp;quot;Исполнители&amp;quot;, &amp;quot;Методики&amp;quot;, &amp;quot;Средний результат&amp;quot; и т.д.&lt;br /&gt;
const indicatorPropsConfig = (indicator): IndicatorPropsConfig =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        executors: {&lt;br /&gt;
            label: &#039;Исполнители&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.executors&lt;br /&gt;
        },&lt;br /&gt;
        methodic: {&lt;br /&gt;
            label: &#039;Методики&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.methodics&lt;br /&gt;
        },&lt;br /&gt;
        averageResult: {&lt;br /&gt;
            label: &#039;Средний результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageValue&lt;br /&gt;
        },&lt;br /&gt;
        averageRoundedValue: {&lt;br /&gt;
            label: &#039;Средний округлённый результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageRoundedValue&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Конфигурация аддона:&lt;br /&gt;
// - indicatorsList: список обязательных показателей (например, &amp;quot;Аммоний-ион&amp;quot;)&lt;br /&gt;
// - journalRecordId: 144 — тестовая запись (&amp;quot;Мк / Тест_Ушакова&amp;quot;)&lt;br /&gt;
const config = {&lt;br /&gt;
    indicatorsList: [&lt;br /&gt;
        &#039;Аммоний-ион&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144,&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Сообщения для пользовательских уведомлений и ошибок&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyIndicators: (recordId) =&amp;gt; `в записи с Id ${recordId} отсутствуют показатели`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
    indicators: (indicators: string[], recordId) =&amp;gt; `Показатели ${indicators.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} отсутствуют в записи с ID ${recordId}`,&lt;br /&gt;
    // Формирует детализированное сообщение о незаполненных полях внутри показателей&lt;br /&gt;
    emptyIndicatorsProps: (props: Record&amp;lt;string, string[]&amp;gt;, recordId) =&amp;gt; {&lt;br /&gt;
        const propsString = Object.keys(props).map(prop =&amp;gt; !isEmptyArray(props[prop]) ? `${prop}: ${props[prop].map(p =&amp;gt; `&amp;quot;${p}&amp;quot;`).join(&#039;, &#039;)}` : &#039;&#039;).join(&#039;; &#039;)&lt;br /&gt;
        return `В записи с Id ${recordId} отсутствуют либо не заполнены следующие поля у показателей: ${propsString}`&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Централизованные обработчики ошибок — обеспечивают единообразие и чистоту основной логики&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    indicatorsError: (recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyIndicators(recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFoundIndicators: (indicators: string[], recordId: number) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.indicators(indicators, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFilledIndicatorProps: (props: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyIndicatorsProps(props, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Формирует конфиг диалога для отправки в GUI: тип, заголовок и сообщение&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Извлекает socketId из queryParams — необходим для связи с интерфейсом&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Показывает пользователю модальное окно с ошибкой&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
// Показывает уведомление об успешной проверке&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
// Универсальная проверка: является ли массив пустым (с учётом типа)&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(arr) &amp;amp;&amp;amp; arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Проверяет, есть ли у записи хотя бы один показатель&lt;br /&gt;
const hasIndicators = (journalRecordManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
    return Array.isArray(indicators) &amp;amp;&amp;amp; indicators.length &amp;gt; 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Определяет, какие из требуемых показателей отсутствуют в записи&lt;br /&gt;
const getNotFoundIndicators = (indicators: JournalRecordIndicatorResponse[], indicatorsList = config.indicatorsList) =&amp;gt; {&lt;br /&gt;
    return indicatorsList.filter(ind =&amp;gt; &lt;br /&gt;
        indicators.every(ljInd =&amp;gt; ljInd?.indicator?.name?.toLowerCase() !== ind.toLowerCase())&lt;br /&gt;
    )&lt;br /&gt;
} &lt;br /&gt;
&lt;br /&gt;
// Собирает список незаполненных полей для каждого показателя&lt;br /&gt;
// Возвращает объект: { &amp;quot;Аммоний-ион&amp;quot;: [&amp;quot;Исполнители&amp;quot;, &amp;quot;Средний результат&amp;quot;] }&lt;br /&gt;
const getNotFilledResultsFields = (indicators: JournalRecordIndicatorResponse[]): Record&amp;lt;string, string[]&amp;gt; =&amp;gt; {&lt;br /&gt;
    return indicators.reduce((acc, indicator) =&amp;gt; {&lt;br /&gt;
        const config = indicatorPropsConfig(indicator)&lt;br /&gt;
        return {&lt;br /&gt;
            ...acc,&lt;br /&gt;
            [indicator?.indicator?.name]: Object.keys(config).reduce((acc, prop) =&amp;gt; {&lt;br /&gt;
                if (!config[prop].get()) return [...acc, config[prop].label]&lt;br /&gt;
                return acc&lt;br /&gt;
            }, [] as string[])&lt;br /&gt;
        }&lt;br /&gt;
    }, {}) as Record&amp;lt;string, string[]&amp;gt;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Проверяет, есть ли хотя бы один показатель с незаполненными полями&lt;br /&gt;
const hasNotFilledProps = (indicatorsProps: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
    return Object.keys(indicatorsProps).some(indicator =&amp;gt; !isEmptyArray(indicatorsProps[indicator]))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Проверяет, есть ли отсутствующие показатели в списке&lt;br /&gt;
const hasNotFoundIndicators = (indicators: string[]) =&amp;gt; !isEmptyArray(indicators)&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Основная функция аддона:&lt;br /&gt;
 * Проверяет заполненность:&lt;br /&gt;
 * 1. Наличие указанных показателей (например, &amp;quot;Аммоний-ион&amp;quot;).&lt;br /&gt;
 * 2. Заполненность внутренних полей каждого показателя (исполнители, методики и т.д.).&lt;br /&gt;
 * &lt;br /&gt;
 * При обнаружении проблем — прерывает выполнение и показывает детализированную ошибку.&lt;br /&gt;
 */&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        // Проверка: есть ли вообще показатели в записи?&lt;br /&gt;
        if (!hasIndicators(journalRecordManager)) {&lt;br /&gt;
            errorHandlers.indicatorsError(journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
&lt;br /&gt;
        // Проверка: все ли требуемые показатели присутствуют?&lt;br /&gt;
        const notFoundIndicators = getNotFoundIndicators(indicators)&lt;br /&gt;
        if (hasNotFoundIndicators(notFoundIndicators)) { &lt;br /&gt;
            errorHandlers.notFoundIndicators(notFoundIndicators, journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Проверка: все ли поля у показателей заполнены?&lt;br /&gt;
        const emptyIndicatorProps = getNotFilledResultsFields(indicators)&lt;br /&gt;
        if (hasNotFilledProps(emptyIndicatorProps)) {&lt;br /&gt;
            errorHandlers.notFilledIndicatorProps(emptyIndicatorProps)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Все проверки пройдены — информируем пользователя&lt;br /&gt;
        showSuccessMessage(messages.success, socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        // При любой ошибке — показываем её в модальном окне&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-record-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Список. Целое число&#039;,&lt;br /&gt;
        &#039;Строка&#039;,&lt;br /&gt;
        &#039;Целое число&#039;,&lt;br /&gt;
        &#039;Вещественное число&#039;,&lt;br /&gt;
        &#039;Логический тип&#039;,&lt;br /&gt;
        &#039;Дата&#039;,&lt;br /&gt;
        &#039;Время&#039;,&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144,  // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFilledFields = (fields: string[], journalRecordsManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    console.log(journalRecordsManager.data.attributes, &#039;data.attributes&#039;)&lt;br /&gt;
    return fields.filter(f =&amp;gt; !journalRecordsManager.getFieldValue(f))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const emptyFields = getNotFilledFields(config.requiredFields, journalRecordManager)&lt;br /&gt;
&lt;br /&gt;
        if (isEmptyArray(emptyFields)) {&lt;br /&gt;
            showSuccessMessage(messages.success, socketId)&lt;br /&gt;
            return config.integrationResult&lt;br /&gt;
        } else errorHandlers.fieldsError(emptyFields, config.journalRecordId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-sample-moving.png|frame]]&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { JournalsRecordRouteController } from &amp;quot;../../src/servivces/worker/api/controllers/journals-record-route&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRouteDto } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    journalRecordId: 114 // Лаб. воздуха / Анализы завершены&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    route: (routes: JournalRecordRouteDto[], recordId: number) =&amp;gt; `Машрут пробы: ${routes.map(r =&amp;gt; `&amp;quot;${r.stageName}&amp;quot;`).join(&#039; -&amp;gt; &#039;)}. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
    notMoved: (recordId: number) =&amp;gt; `Движение пробы не происходило. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isSampleMoved = (moves: JournalRecordRouteDto[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(moves) &amp;amp;&amp;amp; moves.length &amp;gt; 1&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalsRecordRouteController = new JournalsRecordRouteController(apiInstance)&lt;br /&gt;
        const sampleMoves = await journalsRecordRouteController.getJournalRecordRoute(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (isSampleMoved(sampleMoves)) {&lt;br /&gt;
            showSuccessMessage(messages.route(sampleMoves, config.journalRecordId), socketId)&lt;br /&gt;
        } else {&lt;br /&gt;
            showSuccessMessage(messages.notMoved(config.journalRecordId), socketId)&lt;br /&gt;
        }&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-user-rights.png|frame]]&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import { &lt;br /&gt;
    PermissionController, &lt;br /&gt;
    PermissionResource, &lt;br /&gt;
    PermissionRwAccess&lt;br /&gt;
} from &amp;quot;../../src/servivces/worker/api/controllers/permisson&amp;quot;&lt;br /&gt;
&lt;br /&gt;
import { TableBuilder } from &amp;quot;../../src/gui/builder&amp;quot;&lt;br /&gt;
import type { DialogType, TableConfig } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRouteDto } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const accessNamesMap = {&lt;br /&gt;
    [PermissionRwAccess.EXEC]: &#039;Выполнение&#039;,&lt;br /&gt;
    [PermissionRwAccess.READ]: &#039;Чтение&#039;,&lt;br /&gt;
    [PermissionRwAccess.WRITE]: &#039;Запись&#039;,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const requiredResources = [&lt;br /&gt;
    PermissionResource.ANALYSIS_OBJECT, &lt;br /&gt;
    PermissionResource.METHODOLOGY,&lt;br /&gt;
    PermissionResource.JOURNAL_RECORD&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
const resourcesNamesMap = {&lt;br /&gt;
    [PermissionResource.ANALYSIS_OBJECT]: &#039;Объекты анализа&#039;, &lt;br /&gt;
    [PermissionResource.METHODOLOGY]: &#039;Методики&#039;, &lt;br /&gt;
    [PermissionResource.JOURNAL_RECORD]: &#039;Записи ЛЖ&#039;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getAccessRightsTable = async (tableBuilder: TableBuilder, permissionController: PermissionController): Promise&amp;lt;TableConfig&amp;gt; =&amp;gt; {&lt;br /&gt;
    const rows = (await Promise.all(requiredResources.map(async (source: PermissionResource) =&amp;gt; {&lt;br /&gt;
            const sourceCombinations = await Promise.all(Object.keys(PermissionRwAccess).map(async (action) =&amp;gt; {&lt;br /&gt;
                const access = await permissionController.getSourcePermission(source, PermissionRwAccess[action])&lt;br /&gt;
                return { source: resourcesNamesMap[source], action: accessNamesMap[PermissionRwAccess[action]], access: access.hasPermission }&lt;br /&gt;
            }))&lt;br /&gt;
            return sourceCombinations&lt;br /&gt;
        }   &lt;br /&gt;
    ))).flat()&lt;br /&gt;
    const table = tableBuilder&lt;br /&gt;
        .name(&#039;simpleTable&#039;)&lt;br /&gt;
        .label(&#039;Таблица&#039;)&lt;br /&gt;
        .isAddable(false)&lt;br /&gt;
        .actionsCol(false)&lt;br /&gt;
        .addColumn(&#039;Ресурс&#039;, &#039;source&#039;, false)&lt;br /&gt;
        .config(&#039;input&#039;, { name: &#039;source&#039;, rules: [{ required: true }] }, { placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .addColumn(&#039;Тип доступа&#039;, &#039;action&#039;, false)&lt;br /&gt;
        .config(&#039;input&#039;, { name: &#039;action&#039; }, { type: &#039;string&#039;, placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .addColumn(&#039;Доступ&#039;, &#039;access&#039;, false)&lt;br /&gt;
        .config(&#039;checkbox&#039;, { name: &#039;access&#039; }, { type: &#039;boolean&#039;, placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .setRows(rows)&lt;br /&gt;
        .build();&lt;br /&gt;
  return table&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    route: (routes: JournalRecordRouteDto[], recordId: number) =&amp;gt; `Маршрут пробы: ${routes.map(r =&amp;gt; `&amp;quot;${r.stageName}&amp;quot;`).join(&#039; -&amp;gt; &#039;)}. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
    notMoved: (recordId: number) =&amp;gt; `Движение пробы не происходило. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const showTable = async (builder: TableBuilder, controller: PermissionController, socketId: string) =&amp;gt; &lt;br /&gt;
    await getDataFromDialog({ type: &#039;input-data&#039;, config: [await getAccessRightsTable(builder, controller)] }, socketId)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
    try {&lt;br /&gt;
        await showTable(new TableBuilder, new PermissionController(apiInstance), socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Fill-record-fields.png|frame]]&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
import { JournalRecordAttributeFieldResponse } from &amp;quot;../../src/servivces/worker/api/controllers/lj-eav-attribute-fields&amp;quot;&lt;br /&gt;
import { FieldMatch } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const date = new Date().toISOString().split(&#039;T&#039;)[0]&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        { name: &#039;Дата регистрации&#039;, value: date, keywords: [&#039;дат&#039;, &#039;регистр&#039;] },&lt;br /&gt;
        { name: &#039;Место отбора&#039;, value: &#039;Скважина 516563Г &#039;, keywords: [&#039;мест&#039;, &#039;отбор&#039;] },&lt;br /&gt;
        { name: &#039;Дата отбора&#039;, value: date, keywords: [&#039;дат&#039;, &#039;отбор&#039;] },&lt;br /&gt;
        { name: &#039;Шифр пробы&#039;, value: &#039;shifr 123&#039;, keywords: [&#039;дат&#039;, &#039;регистр&#039;]},&lt;br /&gt;
        { name: &#039;Список. Целое число&#039;, value: 666, keywords: [&#039;спис&#039;, &#039;цел&#039;, &#039;числ&#039;] },&lt;br /&gt;
        { name: &#039;Помещение&#039;, value: &#039;помещение&#039;, keywords: [&#039;помещ&#039;]},&lt;br /&gt;
    ] as FieldMatch[],&lt;br /&gt;
    journalRecordId: 118, // Мк / 111&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: (recordId) =&amp;gt; `Все необходимые поля заполнены! Id записи: ${recordId}`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const fieldsMatcher = (field: JournalRecordAttributeFieldResponse, searchField: FieldMatch) =&amp;gt; {&lt;br /&gt;
    return searchField.keywords.every(k =&amp;gt; String(field.name).toLocaleLowerCase().includes(k.toLocaleLowerCase()))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const fillFields = async (fields: FieldMatch[], journalRecordsManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    await journalRecordsManager.setFieldsValues(fields, journalRecordsManager, fieldsMatcher)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        await fillFields(config.requiredFields, journalRecordManager)&lt;br /&gt;
        showSuccessMessage(messages.success(journalRecordManager.journalRecordId), socketId)&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Copy-journal-record.png|frame]]&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { JournalRecordsController } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRequest, JournalRecordResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { JournalRecordAttributeResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
type DataType = JournalRecordAttributeResponse[&#039;field&#039;][&#039;dataType&#039;]&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    journalRecordId: 208,  // Лаб. воздуха / Регистрация&lt;br /&gt;
    nextJournalRecordStage: 2, //  Лаб. воздуха / Анализ&lt;br /&gt;
    nextStageName: &#039;Анализ&#039;,&lt;br /&gt;
    nextRouteId: 50,&lt;br /&gt;
    integrationResult: { message: &#039;Запись ЛЖ скопирована успешно!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const getDateString = (ISODate: unknown | null = null) =&amp;gt; {&lt;br /&gt;
    return new Date(String(ISODate)).toISOString().split(&#039;T&#039;)[0]&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getTimeString = (ISODate: unknown | null = null) =&amp;gt; {&lt;br /&gt;
    return new Date(String(ISODate)).toISOString().split(&#039;T&#039;)[1].split(&#039;.&#039;)[0]&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const createRecord = async (journalRecordController: JournalRecordsController, baseRecord: JournalRecordResponse, newStageId) =&amp;gt; {&lt;br /&gt;
    const getAttrValue = (value: unknown, dataType: DataType) =&amp;gt; {&lt;br /&gt;
        if (dataType === &amp;quot;DATE&amp;quot;) return getDateString(value)&lt;br /&gt;
        if (dataType === &amp;quot;TIME&amp;quot;) return getTimeString(value)&lt;br /&gt;
        return value&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    const getBindings = (baseRecord: JournalRecordResponse): number[] | null =&amp;gt; {&lt;br /&gt;
        return baseRecord?.companyLevelBindings?.map(b =&amp;gt; b?.id) || null&lt;br /&gt;
    } &lt;br /&gt;
&lt;br /&gt;
    const getRecordRequest = (baseRecord: JournalRecordResponse, newStageId: number): JournalRecordRequest =&amp;gt; {&lt;br /&gt;
        return {&lt;br /&gt;
            routeId: baseRecord?.route?.id,&lt;br /&gt;
            stageId: newStageId,&lt;br /&gt;
            analysisObjectId: baseRecord?.analysisObject?.id,&lt;br /&gt;
            sampleTypeId: baseRecord.sampleType.id,&lt;br /&gt;
            attributes: baseRecord.attributes.map(attr =&amp;gt; {&lt;br /&gt;
                return {&lt;br /&gt;
                    fieldId: attr.field.id,&lt;br /&gt;
                    stageId: newStageId,&lt;br /&gt;
                    data: {&lt;br /&gt;
                        ...attr.data, &lt;br /&gt;
                        value: getAttrValue(attr?.data?.value, attr?.field?.dataType),&lt;br /&gt;
                        id: undefined&lt;br /&gt;
                    },&lt;br /&gt;
                    statusId: null&lt;br /&gt;
                }&lt;br /&gt;
            }),&lt;br /&gt;
            companyLevelBindings: getBindings(baseRecord)&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    return await journalRecordController.createRecord(getRecordRequest(baseRecord, newStageId))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const journalRecordController = new JournalRecordsController(apiInstance)&lt;br /&gt;
        const newRecord = await createRecord(journalRecordController, journalRecordManager.data, config.nextJournalRecordStage)&lt;br /&gt;
        showSuccessMessage(`${messages.success}. Id изначальной записи: ${config.journalRecordId}. Id новой записи ЛЖ: ${newRecord.id}. Этап: ${config.nextStageName}`, socketId)&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2377</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2377"/>
		<updated>2026-04-03T06:12:05Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Проверка заполненности полей показателей. check-indicator-fields-filled */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { MessageBoxResults } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { FetchApiClient } from &amp;quot;../../src/servivces/worker/api/ApiClient&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
&lt;br /&gt;
// Конфигурация аддона:&lt;br /&gt;
// - Поле &amp;quot;Дата регистрации&amp;quot; — контрольная точка для расчёта срока&lt;br /&gt;
// - expirationLimit: 10 дней — максимальный допустимый срок хранения пробы&lt;br /&gt;
// - journalRecordId: 201 — запись из журнала &amp;quot;Лаб. воздуха / Анализы завершены&amp;quot;&lt;br /&gt;
const config = {&lt;br /&gt;
    dateField: &#039;Дата регистрации&#039;,&lt;br /&gt;
    expirationLimit: 10,&lt;br /&gt;
    journalRecordId: 201,&lt;br /&gt;
    integrationResult: { message: &#039;Срок регистрации пробы не истёк!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Сообщения для пользовательских уведомлений и ошибок&lt;br /&gt;
const messages = {&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    // Успех: информирует пользователя, что срок не вышел&lt;br /&gt;
    success: (recordId) =&amp;gt; `Срок регистрации пробы не истёк! Id записи ${recordId}`,&lt;br /&gt;
    // Ошибка: превышен лимит хранения&lt;br /&gt;
    expired: (limit, recordId) =&amp;gt; `Срок регистрации пробы истёк!\nПрошло более ${limit} дней с момента регистрации. Id записи: ${recordId}`,&lt;br /&gt;
    // Ошибка: отсутствует дата в нужном поле&lt;br /&gt;
    dateField: (dateField, stageName, journalRecordId) =&amp;gt; [&lt;br /&gt;
        `Не заполнено либо отсутствует поле &amp;quot;${dateField}&amp;quot;`,&lt;br /&gt;
        `в этапе &amp;quot;${stageName || &#039;&#039;}&amp;quot;.\nId записи: ${journalRecordId}`&lt;br /&gt;
    ].join(&#039; &#039;)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Централизованные обработчики ошибок — обеспечивают единообразие и чистоту основной логики&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    // Вызывается, если поле с датой не заполнено&lt;br /&gt;
    dateError: (dateField, stageName, journalRecordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.dateField(dateField, stageName, journalRecordId))&lt;br /&gt;
    },&lt;br /&gt;
    // Вызывается при превышении срока хранения&lt;br /&gt;
    expirationError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.expired(config.expirationLimit, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Извлекает socketId из queryParams — необходим для отправки диалогов в GUI&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Переводит дату в количество дней с начала эпохи (для арифметики дат)&lt;br /&gt;
const getDaysByDate = (date: Date) =&amp;gt; {&lt;br /&gt;
    const dayMs = 60 * 60 * 24 * 1000&lt;br /&gt;
    const timestamp = date.getTime()&lt;br /&gt;
    return timestamp / dayMs&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Формирует конфиг диалога: тип, заголовок и сообщение для отображения в GUI&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Рассчитывает, сколько дней прошло с момента регистрации пробы&lt;br /&gt;
const getExpirationOffset = (registrationDate: string) =&amp;gt; {&lt;br /&gt;
    const regDate = new Date(registrationDate)&lt;br /&gt;
    const today = new Date()&lt;br /&gt;
    return getDaysByDate(today) - getDaysByDate(regDate)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Показывает пользователю сообщение об ошибке&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
// Показывает пользователю уведомление об успешной проверке&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Основная функция аддона:&lt;br /&gt;
 * Проверяет, не истёк ли срок хранения пробы (с момента &amp;quot;Дата регистрации&amp;quot;).&lt;br /&gt;
 * &lt;br /&gt;
 * Логика:&lt;br /&gt;
 * 1. Получает значение поля &amp;quot;Дата регистрации&amp;quot; из записи журнала.&lt;br /&gt;
 * 2. Если дата не указана — ошибка.&lt;br /&gt;
 * 3. Если с даты регистрации прошло &amp;gt;= 10 дней — ошибка.&lt;br /&gt;
 * 4. Иначе — успех, показываем подтверждение.&lt;br /&gt;
 */&lt;br /&gt;
async function main({ queryParams, ...apiInstance }) {&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        // Получаем доступ к данным записи журнала&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        // Извлекаем значение поля &amp;quot;Дата регистрации&amp;quot;&lt;br /&gt;
        const date = journalRecordManager.getFieldValue(config.dateField)&lt;br /&gt;
&lt;br /&gt;
        // Если дата не заполнена — прерываем с пояснением&lt;br /&gt;
        if (!date) {&lt;br /&gt;
            errorHandlers.dateError(config.dateField, journalRecordManager?.data?.stage?.stage?.name, config.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Считаем, сколько дней прошло с регистрации&lt;br /&gt;
        const expirationOffset = getExpirationOffset(date as string)&lt;br /&gt;
&lt;br /&gt;
        // Если срок хранения превышен — ошибка&lt;br /&gt;
        if (expirationOffset &amp;gt;= config.expirationLimit) {&lt;br /&gt;
            errorHandlers.expirationError()&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Всё в порядке — информируем пользователя&lt;br /&gt;
        await showSuccessMessage(messages.success(config.journalRecordId), socketId)&lt;br /&gt;
&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        // При любой ошибке — показываем её в модальном окне&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
// Конфигурация аддона:&lt;br /&gt;
// - Список полей, обязательных для проверки заполненности&lt;br /&gt;
// - journalRecordId: 144 — тестовая запись (&amp;quot;Мк / Тест_Ушакова&amp;quot;)&lt;br /&gt;
// - Результат при успехе: сообщение о полной заполненности&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Список. Целое число&#039;,&lt;br /&gt;
        &#039;Строка&#039;,&lt;br /&gt;
        &#039;Целое число&#039;,&lt;br /&gt;
        &#039;Вещественное число&#039;,&lt;br /&gt;
        &#039;Логический тип&#039;,&lt;br /&gt;
        &#039;Дата&#039;,&lt;br /&gt;
        &#039;Время&#039;,&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144,  // Используется для доступа к данным записи&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Сообщения, отображаемые пользователю или используемые в ошибках&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    // Динамическое сообщение: перечисляет незаполненные поля и ID записи&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Централизованные обработчики ошибок — обеспечивают единообразие и чистоту основной логики&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Формирует конфиг диалога для GUI: тип, заголовок и сообщение&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Извлекает socketId из queryParams — он нужен для отправки диалогов обратно в интерфейс&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Показывает пользователю модальное окно с ошибкой&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
// Показывает уведомление об успешной проверке&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
// Удобная проверка на пустой массив — повышает читаемость кода&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Определяет, какие из обязательных полей не заполнены в записи журнала&lt;br /&gt;
const getNotFilledFields = (fields: string[], journalRecordsManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    console.log(journalRecordsManager.data.attributes, &#039;data.attributes&#039;) // Отладка: структура данных записи&lt;br /&gt;
    return fields.filter(f =&amp;gt; !journalRecordsManager.getFieldValue(f))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Основная функция аддона:&lt;br /&gt;
 * Проверяет, что все указанные поля в записи журнала заполнены.&lt;br /&gt;
 * &lt;br /&gt;
 * Логика:&lt;br /&gt;
 * 1. Получает socketId для связи с GUI.&lt;br /&gt;
 * 2. Загружает данные записи через JournalRecordManager.&lt;br /&gt;
 * 3. Проверяет каждое поле из requiredFields.&lt;br /&gt;
 * 4. Если все поля заполнены — показывает успех.&lt;br /&gt;
 * 5. Если есть пустые поля — прерывает выполнение с ошибкой.&lt;br /&gt;
 */&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        // Получаем менеджер для работы с конкретной записью журнала&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        // Определяем, какие обязательные поля не заполнены&lt;br /&gt;
        const emptyFields = getNotFilledFields(config.requiredFields, journalRecordManager)&lt;br /&gt;
&lt;br /&gt;
        if (isEmptyArray(emptyFields)) {&lt;br /&gt;
            // Все поля заполнены — информируем пользователя&lt;br /&gt;
            showSuccessMessage(messages.success, socketId)&lt;br /&gt;
            return config.integrationResult&lt;br /&gt;
        } else {&lt;br /&gt;
            // Есть незаполненные поля — прерываем с детализацией&lt;br /&gt;
            errorHandlers.fieldsError(emptyFields, config.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        // При любой ошибке — показываем её в модальном окне&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-record-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Список. Целое число&#039;,&lt;br /&gt;
        &#039;Строка&#039;,&lt;br /&gt;
        &#039;Целое число&#039;,&lt;br /&gt;
        &#039;Вещественное число&#039;,&lt;br /&gt;
        &#039;Логический тип&#039;,&lt;br /&gt;
        &#039;Дата&#039;,&lt;br /&gt;
        &#039;Время&#039;,&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144,  // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFilledFields = (fields: string[], journalRecordsManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    console.log(journalRecordsManager.data.attributes, &#039;data.attributes&#039;)&lt;br /&gt;
    return fields.filter(f =&amp;gt; !journalRecordsManager.getFieldValue(f))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const emptyFields = getNotFilledFields(config.requiredFields, journalRecordManager)&lt;br /&gt;
&lt;br /&gt;
        if (isEmptyArray(emptyFields)) {&lt;br /&gt;
            showSuccessMessage(messages.success, socketId)&lt;br /&gt;
            return config.integrationResult&lt;br /&gt;
        } else errorHandlers.fieldsError(emptyFields, config.journalRecordId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-sample-moving.png|frame]]&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { JournalsRecordRouteController } from &amp;quot;../../src/servivces/worker/api/controllers/journals-record-route&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRouteDto } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    journalRecordId: 114 // Лаб. воздуха / Анализы завершены&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    route: (routes: JournalRecordRouteDto[], recordId: number) =&amp;gt; `Машрут пробы: ${routes.map(r =&amp;gt; `&amp;quot;${r.stageName}&amp;quot;`).join(&#039; -&amp;gt; &#039;)}. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
    notMoved: (recordId: number) =&amp;gt; `Движение пробы не происходило. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isSampleMoved = (moves: JournalRecordRouteDto[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(moves) &amp;amp;&amp;amp; moves.length &amp;gt; 1&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalsRecordRouteController = new JournalsRecordRouteController(apiInstance)&lt;br /&gt;
        const sampleMoves = await journalsRecordRouteController.getJournalRecordRoute(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (isSampleMoved(sampleMoves)) {&lt;br /&gt;
            showSuccessMessage(messages.route(sampleMoves, config.journalRecordId), socketId)&lt;br /&gt;
        } else {&lt;br /&gt;
            showSuccessMessage(messages.notMoved(config.journalRecordId), socketId)&lt;br /&gt;
        }&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-user-rights.png|frame]]&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import { &lt;br /&gt;
    PermissionController, &lt;br /&gt;
    PermissionResource, &lt;br /&gt;
    PermissionRwAccess&lt;br /&gt;
} from &amp;quot;../../src/servivces/worker/api/controllers/permisson&amp;quot;&lt;br /&gt;
&lt;br /&gt;
import { TableBuilder } from &amp;quot;../../src/gui/builder&amp;quot;&lt;br /&gt;
import type { DialogType, TableConfig } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRouteDto } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const accessNamesMap = {&lt;br /&gt;
    [PermissionRwAccess.EXEC]: &#039;Выполнение&#039;,&lt;br /&gt;
    [PermissionRwAccess.READ]: &#039;Чтение&#039;,&lt;br /&gt;
    [PermissionRwAccess.WRITE]: &#039;Запись&#039;,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const requiredResources = [&lt;br /&gt;
    PermissionResource.ANALYSIS_OBJECT, &lt;br /&gt;
    PermissionResource.METHODOLOGY,&lt;br /&gt;
    PermissionResource.JOURNAL_RECORD&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
const resourcesNamesMap = {&lt;br /&gt;
    [PermissionResource.ANALYSIS_OBJECT]: &#039;Объекты анализа&#039;, &lt;br /&gt;
    [PermissionResource.METHODOLOGY]: &#039;Методики&#039;, &lt;br /&gt;
    [PermissionResource.JOURNAL_RECORD]: &#039;Записи ЛЖ&#039;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getAccessRightsTable = async (tableBuilder: TableBuilder, permissionController: PermissionController): Promise&amp;lt;TableConfig&amp;gt; =&amp;gt; {&lt;br /&gt;
    const rows = (await Promise.all(requiredResources.map(async (source: PermissionResource) =&amp;gt; {&lt;br /&gt;
            const sourceCombinations = await Promise.all(Object.keys(PermissionRwAccess).map(async (action) =&amp;gt; {&lt;br /&gt;
                const access = await permissionController.getSourcePermission(source, PermissionRwAccess[action])&lt;br /&gt;
                return { source: resourcesNamesMap[source], action: accessNamesMap[PermissionRwAccess[action]], access: access.hasPermission }&lt;br /&gt;
            }))&lt;br /&gt;
            return sourceCombinations&lt;br /&gt;
        }   &lt;br /&gt;
    ))).flat()&lt;br /&gt;
    const table = tableBuilder&lt;br /&gt;
        .name(&#039;simpleTable&#039;)&lt;br /&gt;
        .label(&#039;Таблица&#039;)&lt;br /&gt;
        .isAddable(false)&lt;br /&gt;
        .actionsCol(false)&lt;br /&gt;
        .addColumn(&#039;Ресурс&#039;, &#039;source&#039;, false)&lt;br /&gt;
        .config(&#039;input&#039;, { name: &#039;source&#039;, rules: [{ required: true }] }, { placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .addColumn(&#039;Тип доступа&#039;, &#039;action&#039;, false)&lt;br /&gt;
        .config(&#039;input&#039;, { name: &#039;action&#039; }, { type: &#039;string&#039;, placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .addColumn(&#039;Доступ&#039;, &#039;access&#039;, false)&lt;br /&gt;
        .config(&#039;checkbox&#039;, { name: &#039;access&#039; }, { type: &#039;boolean&#039;, placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .setRows(rows)&lt;br /&gt;
        .build();&lt;br /&gt;
  return table&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    route: (routes: JournalRecordRouteDto[], recordId: number) =&amp;gt; `Маршрут пробы: ${routes.map(r =&amp;gt; `&amp;quot;${r.stageName}&amp;quot;`).join(&#039; -&amp;gt; &#039;)}. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
    notMoved: (recordId: number) =&amp;gt; `Движение пробы не происходило. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const showTable = async (builder: TableBuilder, controller: PermissionController, socketId: string) =&amp;gt; &lt;br /&gt;
    await getDataFromDialog({ type: &#039;input-data&#039;, config: [await getAccessRightsTable(builder, controller)] }, socketId)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
    try {&lt;br /&gt;
        await showTable(new TableBuilder, new PermissionController(apiInstance), socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Fill-record-fields.png|frame]]&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
import { JournalRecordAttributeFieldResponse } from &amp;quot;../../src/servivces/worker/api/controllers/lj-eav-attribute-fields&amp;quot;&lt;br /&gt;
import { FieldMatch } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const date = new Date().toISOString().split(&#039;T&#039;)[0]&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        { name: &#039;Дата регистрации&#039;, value: date, keywords: [&#039;дат&#039;, &#039;регистр&#039;] },&lt;br /&gt;
        { name: &#039;Место отбора&#039;, value: &#039;Скважина 516563Г &#039;, keywords: [&#039;мест&#039;, &#039;отбор&#039;] },&lt;br /&gt;
        { name: &#039;Дата отбора&#039;, value: date, keywords: [&#039;дат&#039;, &#039;отбор&#039;] },&lt;br /&gt;
        { name: &#039;Шифр пробы&#039;, value: &#039;shifr 123&#039;, keywords: [&#039;дат&#039;, &#039;регистр&#039;]},&lt;br /&gt;
        { name: &#039;Список. Целое число&#039;, value: 666, keywords: [&#039;спис&#039;, &#039;цел&#039;, &#039;числ&#039;] },&lt;br /&gt;
        { name: &#039;Помещение&#039;, value: &#039;помещение&#039;, keywords: [&#039;помещ&#039;]},&lt;br /&gt;
    ] as FieldMatch[],&lt;br /&gt;
    journalRecordId: 118, // Мк / 111&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: (recordId) =&amp;gt; `Все необходимые поля заполнены! Id записи: ${recordId}`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const fieldsMatcher = (field: JournalRecordAttributeFieldResponse, searchField: FieldMatch) =&amp;gt; {&lt;br /&gt;
    return searchField.keywords.every(k =&amp;gt; String(field.name).toLocaleLowerCase().includes(k.toLocaleLowerCase()))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const fillFields = async (fields: FieldMatch[], journalRecordsManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    await journalRecordsManager.setFieldsValues(fields, journalRecordsManager, fieldsMatcher)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        await fillFields(config.requiredFields, journalRecordManager)&lt;br /&gt;
        showSuccessMessage(messages.success(journalRecordManager.journalRecordId), socketId)&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Copy-journal-record.png|frame]]&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { JournalRecordsController } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRequest, JournalRecordResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { JournalRecordAttributeResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
type DataType = JournalRecordAttributeResponse[&#039;field&#039;][&#039;dataType&#039;]&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    journalRecordId: 208,  // Лаб. воздуха / Регистрация&lt;br /&gt;
    nextJournalRecordStage: 2, //  Лаб. воздуха / Анализ&lt;br /&gt;
    nextStageName: &#039;Анализ&#039;,&lt;br /&gt;
    nextRouteId: 50,&lt;br /&gt;
    integrationResult: { message: &#039;Запись ЛЖ скопирована успешно!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const getDateString = (ISODate: unknown | null = null) =&amp;gt; {&lt;br /&gt;
    return new Date(String(ISODate)).toISOString().split(&#039;T&#039;)[0]&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getTimeString = (ISODate: unknown | null = null) =&amp;gt; {&lt;br /&gt;
    return new Date(String(ISODate)).toISOString().split(&#039;T&#039;)[1].split(&#039;.&#039;)[0]&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const createRecord = async (journalRecordController: JournalRecordsController, baseRecord: JournalRecordResponse, newStageId) =&amp;gt; {&lt;br /&gt;
    const getAttrValue = (value: unknown, dataType: DataType) =&amp;gt; {&lt;br /&gt;
        if (dataType === &amp;quot;DATE&amp;quot;) return getDateString(value)&lt;br /&gt;
        if (dataType === &amp;quot;TIME&amp;quot;) return getTimeString(value)&lt;br /&gt;
        return value&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    const getBindings = (baseRecord: JournalRecordResponse): number[] | null =&amp;gt; {&lt;br /&gt;
        return baseRecord?.companyLevelBindings?.map(b =&amp;gt; b?.id) || null&lt;br /&gt;
    } &lt;br /&gt;
&lt;br /&gt;
    const getRecordRequest = (baseRecord: JournalRecordResponse, newStageId: number): JournalRecordRequest =&amp;gt; {&lt;br /&gt;
        return {&lt;br /&gt;
            routeId: baseRecord?.route?.id,&lt;br /&gt;
            stageId: newStageId,&lt;br /&gt;
            analysisObjectId: baseRecord?.analysisObject?.id,&lt;br /&gt;
            sampleTypeId: baseRecord.sampleType.id,&lt;br /&gt;
            attributes: baseRecord.attributes.map(attr =&amp;gt; {&lt;br /&gt;
                return {&lt;br /&gt;
                    fieldId: attr.field.id,&lt;br /&gt;
                    stageId: newStageId,&lt;br /&gt;
                    data: {&lt;br /&gt;
                        ...attr.data, &lt;br /&gt;
                        value: getAttrValue(attr?.data?.value, attr?.field?.dataType),&lt;br /&gt;
                        id: undefined&lt;br /&gt;
                    },&lt;br /&gt;
                    statusId: null&lt;br /&gt;
                }&lt;br /&gt;
            }),&lt;br /&gt;
            companyLevelBindings: getBindings(baseRecord)&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    return await journalRecordController.createRecord(getRecordRequest(baseRecord, newStageId))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const journalRecordController = new JournalRecordsController(apiInstance)&lt;br /&gt;
        const newRecord = await createRecord(journalRecordController, journalRecordManager.data, config.nextJournalRecordStage)&lt;br /&gt;
        showSuccessMessage(`${messages.success}. Id изначальной записи: ${config.journalRecordId}. Id новой записи ЛЖ: ${newRecord.id}. Этап: ${config.nextStageName}`, socketId)&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2376</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2376"/>
		<updated>2026-04-03T06:05:20Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Проверка даты регистрации. check-registration-date */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { MessageBoxResults } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { FetchApiClient } from &amp;quot;../../src/servivces/worker/api/ApiClient&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
&lt;br /&gt;
// Конфигурация аддона:&lt;br /&gt;
// - Поле &amp;quot;Дата регистрации&amp;quot; — контрольная точка для расчёта срока&lt;br /&gt;
// - expirationLimit: 10 дней — максимальный допустимый срок хранения пробы&lt;br /&gt;
// - journalRecordId: 201 — запись из журнала &amp;quot;Лаб. воздуха / Анализы завершены&amp;quot;&lt;br /&gt;
const config = {&lt;br /&gt;
    dateField: &#039;Дата регистрации&#039;,&lt;br /&gt;
    expirationLimit: 10,&lt;br /&gt;
    journalRecordId: 201,&lt;br /&gt;
    integrationResult: { message: &#039;Срок регистрации пробы не истёк!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Сообщения для пользовательских уведомлений и ошибок&lt;br /&gt;
const messages = {&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    // Успех: информирует пользователя, что срок не вышел&lt;br /&gt;
    success: (recordId) =&amp;gt; `Срок регистрации пробы не истёк! Id записи ${recordId}`,&lt;br /&gt;
    // Ошибка: превышен лимит хранения&lt;br /&gt;
    expired: (limit, recordId) =&amp;gt; `Срок регистрации пробы истёк!\nПрошло более ${limit} дней с момента регистрации. Id записи: ${recordId}`,&lt;br /&gt;
    // Ошибка: отсутствует дата в нужном поле&lt;br /&gt;
    dateField: (dateField, stageName, journalRecordId) =&amp;gt; [&lt;br /&gt;
        `Не заполнено либо отсутствует поле &amp;quot;${dateField}&amp;quot;`,&lt;br /&gt;
        `в этапе &amp;quot;${stageName || &#039;&#039;}&amp;quot;.\nId записи: ${journalRecordId}`&lt;br /&gt;
    ].join(&#039; &#039;)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Централизованные обработчики ошибок — обеспечивают единообразие и чистоту основной логики&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    // Вызывается, если поле с датой не заполнено&lt;br /&gt;
    dateError: (dateField, stageName, journalRecordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.dateField(dateField, stageName, journalRecordId))&lt;br /&gt;
    },&lt;br /&gt;
    // Вызывается при превышении срока хранения&lt;br /&gt;
    expirationError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.expired(config.expirationLimit, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Извлекает socketId из queryParams — необходим для отправки диалогов в GUI&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Переводит дату в количество дней с начала эпохи (для арифметики дат)&lt;br /&gt;
const getDaysByDate = (date: Date) =&amp;gt; {&lt;br /&gt;
    const dayMs = 60 * 60 * 24 * 1000&lt;br /&gt;
    const timestamp = date.getTime()&lt;br /&gt;
    return timestamp / dayMs&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Формирует конфиг диалога: тип, заголовок и сообщение для отображения в GUI&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Рассчитывает, сколько дней прошло с момента регистрации пробы&lt;br /&gt;
const getExpirationOffset = (registrationDate: string) =&amp;gt; {&lt;br /&gt;
    const regDate = new Date(registrationDate)&lt;br /&gt;
    const today = new Date()&lt;br /&gt;
    return getDaysByDate(today) - getDaysByDate(regDate)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Показывает пользователю сообщение об ошибке&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
// Показывает пользователю уведомление об успешной проверке&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Основная функция аддона:&lt;br /&gt;
 * Проверяет, не истёк ли срок хранения пробы (с момента &amp;quot;Дата регистрации&amp;quot;).&lt;br /&gt;
 * &lt;br /&gt;
 * Логика:&lt;br /&gt;
 * 1. Получает значение поля &amp;quot;Дата регистрации&amp;quot; из записи журнала.&lt;br /&gt;
 * 2. Если дата не указана — ошибка.&lt;br /&gt;
 * 3. Если с даты регистрации прошло &amp;gt;= 10 дней — ошибка.&lt;br /&gt;
 * 4. Иначе — успех, показываем подтверждение.&lt;br /&gt;
 */&lt;br /&gt;
async function main({ queryParams, ...apiInstance }) {&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        // Получаем доступ к данным записи журнала&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        // Извлекаем значение поля &amp;quot;Дата регистрации&amp;quot;&lt;br /&gt;
        const date = journalRecordManager.getFieldValue(config.dateField)&lt;br /&gt;
&lt;br /&gt;
        // Если дата не заполнена — прерываем с пояснением&lt;br /&gt;
        if (!date) {&lt;br /&gt;
            errorHandlers.dateError(config.dateField, journalRecordManager?.data?.stage?.stage?.name, config.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Считаем, сколько дней прошло с регистрации&lt;br /&gt;
        const expirationOffset = getExpirationOffset(date as string)&lt;br /&gt;
&lt;br /&gt;
        // Если срок хранения превышен — ошибка&lt;br /&gt;
        if (expirationOffset &amp;gt;= config.expirationLimit) {&lt;br /&gt;
            errorHandlers.expirationError()&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Всё в порядке — информируем пользователя&lt;br /&gt;
        await showSuccessMessage(messages.success(config.journalRecordId), socketId)&lt;br /&gt;
&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        // При любой ошибке — показываем её в модальном окне&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordIndicatorResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
type IndicatorProp = { label: string, get: () =&amp;gt; unknown }&lt;br /&gt;
type IndicatorPropsConfig = Record&amp;lt;string, IndicatorProp&amp;gt;&lt;br /&gt;
&lt;br /&gt;
const indicatorPropsConfig = (indicator): IndicatorPropsConfig =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        executors: {&lt;br /&gt;
            label: &#039;Исполнители&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.executors&lt;br /&gt;
        },&lt;br /&gt;
        methodic: {&lt;br /&gt;
            label: &#039;Методики&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.methodics&lt;br /&gt;
        },&lt;br /&gt;
        averageResult: {&lt;br /&gt;
            label: &#039;Средний результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageValue&lt;br /&gt;
        },&lt;br /&gt;
        averageRoundedValue: {&lt;br /&gt;
            label: &#039;Средний округлённый результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageRoundedValue&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Место отбора&#039;,&lt;br /&gt;
        &#039;Дата регистрации&#039;,&lt;br /&gt;
        &#039;Шифр пробы&#039;,&lt;br /&gt;
        &#039;Время регистрации&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    indicatorsList: [&lt;br /&gt;
        &#039;Аммоний-ион&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144, // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyIndicators: (recordId) =&amp;gt; `в записи с Id ${recordId} отсутствуют показатели`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
    indicators: (indicators: string[], recordId) =&amp;gt; `Показатели ${indicators.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} отсутствуют в записи с ID ${recordId}`,&lt;br /&gt;
    emptyIndicatorsProps: (props: Record&amp;lt;string, string[]&amp;gt;, recordId) =&amp;gt; {&lt;br /&gt;
        const propsString = Object.keys(props).map(prop =&amp;gt; !isEmptyArray(props[prop]) ? `${prop}: ${props[prop].map(p =&amp;gt; `&amp;quot;${p}&amp;quot;`).join(&#039;, &#039;)}` : &#039;&#039;).join(&#039;; &#039;)&lt;br /&gt;
        return `В записи с Id ${recordId} отсутствуют либо не заполнены следующие поля у показателей: ${propsString}`&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    indicatorsError: (recordId) =&amp;gt;{&lt;br /&gt;
        throw new Error(messages.emptyIndicators(recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFoundIndicators: (indicators: string[], recordId: number) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.indicators(indicators, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFilledIndicatorProps: (props: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyIndicatorsProps(props, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(arr) &amp;amp;&amp;amp; arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasIndicators = (journalRecordManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
    return Array.isArray(indicators) &amp;amp;&amp;amp; indicators.length &amp;gt; 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFoundIndicators = (indicators: JournalRecordIndicatorResponse[], indicatorsList = config.indicatorsList) =&amp;gt; {&lt;br /&gt;
    return indicatorsList.filter(ind =&amp;gt; indicators.every(ljInd =&amp;gt; ljInd?.indicator?.name?.toLowerCase() !== ind.toLocaleLowerCase()))&lt;br /&gt;
} &lt;br /&gt;
&lt;br /&gt;
const getNotFilledResultsFields = (indicators: JournalRecordIndicatorResponse[]): Record&amp;lt;string, string[]&amp;gt; =&amp;gt; {&lt;br /&gt;
    return indicators.reduce((acc, indicator) =&amp;gt; {&lt;br /&gt;
        const config = indicatorPropsConfig(indicator)&lt;br /&gt;
        return {&lt;br /&gt;
            ...acc,&lt;br /&gt;
            [indicator?.indicator?.name]: Object.keys(config).reduce((acc, prop) =&amp;gt; {&lt;br /&gt;
                if (!config[prop].get()) return [...acc, config[prop].label]&lt;br /&gt;
                return acc&lt;br /&gt;
            }, [] as string[])&lt;br /&gt;
        }&lt;br /&gt;
    }, {}) as Record&amp;lt;string, string[]&amp;gt;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFilledProps = (indicatorsProps: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
    return Object.keys(indicatorsProps).some(indicator =&amp;gt; !isEmptyArray(indicatorsProps[indicator]))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFoundIndicators = (indicators: string[]) =&amp;gt; !isEmptyArray(indicators)&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (!hasIndicators(journalRecordManager)) {&lt;br /&gt;
            errorHandlers.indicatorsError(journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
&lt;br /&gt;
        const notFoundIndicators = getNotFoundIndicators(indicators)&lt;br /&gt;
        if (hasNotFoundIndicators(notFoundIndicators)) { &lt;br /&gt;
            errorHandlers.notFoundIndicators(notFoundIndicators, journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const emptyIndicatorProps = getNotFilledResultsFields(indicators)&lt;br /&gt;
        if (hasNotFilledProps(emptyIndicatorProps)) {&lt;br /&gt;
            errorHandlers.notFilledIndicatorProps(emptyIndicatorProps)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        showSuccessMessage(messages.success, socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-record-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Список. Целое число&#039;,&lt;br /&gt;
        &#039;Строка&#039;,&lt;br /&gt;
        &#039;Целое число&#039;,&lt;br /&gt;
        &#039;Вещественное число&#039;,&lt;br /&gt;
        &#039;Логический тип&#039;,&lt;br /&gt;
        &#039;Дата&#039;,&lt;br /&gt;
        &#039;Время&#039;,&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144,  // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFilledFields = (fields: string[], journalRecordsManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    console.log(journalRecordsManager.data.attributes, &#039;data.attributes&#039;)&lt;br /&gt;
    return fields.filter(f =&amp;gt; !journalRecordsManager.getFieldValue(f))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const emptyFields = getNotFilledFields(config.requiredFields, journalRecordManager)&lt;br /&gt;
&lt;br /&gt;
        if (isEmptyArray(emptyFields)) {&lt;br /&gt;
            showSuccessMessage(messages.success, socketId)&lt;br /&gt;
            return config.integrationResult&lt;br /&gt;
        } else errorHandlers.fieldsError(emptyFields, config.journalRecordId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-sample-moving.png|frame]]&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { JournalsRecordRouteController } from &amp;quot;../../src/servivces/worker/api/controllers/journals-record-route&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRouteDto } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    journalRecordId: 114 // Лаб. воздуха / Анализы завершены&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    route: (routes: JournalRecordRouteDto[], recordId: number) =&amp;gt; `Машрут пробы: ${routes.map(r =&amp;gt; `&amp;quot;${r.stageName}&amp;quot;`).join(&#039; -&amp;gt; &#039;)}. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
    notMoved: (recordId: number) =&amp;gt; `Движение пробы не происходило. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isSampleMoved = (moves: JournalRecordRouteDto[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(moves) &amp;amp;&amp;amp; moves.length &amp;gt; 1&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalsRecordRouteController = new JournalsRecordRouteController(apiInstance)&lt;br /&gt;
        const sampleMoves = await journalsRecordRouteController.getJournalRecordRoute(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (isSampleMoved(sampleMoves)) {&lt;br /&gt;
            showSuccessMessage(messages.route(sampleMoves, config.journalRecordId), socketId)&lt;br /&gt;
        } else {&lt;br /&gt;
            showSuccessMessage(messages.notMoved(config.journalRecordId), socketId)&lt;br /&gt;
        }&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-user-rights.png|frame]]&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import { &lt;br /&gt;
    PermissionController, &lt;br /&gt;
    PermissionResource, &lt;br /&gt;
    PermissionRwAccess&lt;br /&gt;
} from &amp;quot;../../src/servivces/worker/api/controllers/permisson&amp;quot;&lt;br /&gt;
&lt;br /&gt;
import { TableBuilder } from &amp;quot;../../src/gui/builder&amp;quot;&lt;br /&gt;
import type { DialogType, TableConfig } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRouteDto } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const accessNamesMap = {&lt;br /&gt;
    [PermissionRwAccess.EXEC]: &#039;Выполнение&#039;,&lt;br /&gt;
    [PermissionRwAccess.READ]: &#039;Чтение&#039;,&lt;br /&gt;
    [PermissionRwAccess.WRITE]: &#039;Запись&#039;,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const requiredResources = [&lt;br /&gt;
    PermissionResource.ANALYSIS_OBJECT, &lt;br /&gt;
    PermissionResource.METHODOLOGY,&lt;br /&gt;
    PermissionResource.JOURNAL_RECORD&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
const resourcesNamesMap = {&lt;br /&gt;
    [PermissionResource.ANALYSIS_OBJECT]: &#039;Объекты анализа&#039;, &lt;br /&gt;
    [PermissionResource.METHODOLOGY]: &#039;Методики&#039;, &lt;br /&gt;
    [PermissionResource.JOURNAL_RECORD]: &#039;Записи ЛЖ&#039;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getAccessRightsTable = async (tableBuilder: TableBuilder, permissionController: PermissionController): Promise&amp;lt;TableConfig&amp;gt; =&amp;gt; {&lt;br /&gt;
    const rows = (await Promise.all(requiredResources.map(async (source: PermissionResource) =&amp;gt; {&lt;br /&gt;
            const sourceCombinations = await Promise.all(Object.keys(PermissionRwAccess).map(async (action) =&amp;gt; {&lt;br /&gt;
                const access = await permissionController.getSourcePermission(source, PermissionRwAccess[action])&lt;br /&gt;
                return { source: resourcesNamesMap[source], action: accessNamesMap[PermissionRwAccess[action]], access: access.hasPermission }&lt;br /&gt;
            }))&lt;br /&gt;
            return sourceCombinations&lt;br /&gt;
        }   &lt;br /&gt;
    ))).flat()&lt;br /&gt;
    const table = tableBuilder&lt;br /&gt;
        .name(&#039;simpleTable&#039;)&lt;br /&gt;
        .label(&#039;Таблица&#039;)&lt;br /&gt;
        .isAddable(false)&lt;br /&gt;
        .actionsCol(false)&lt;br /&gt;
        .addColumn(&#039;Ресурс&#039;, &#039;source&#039;, false)&lt;br /&gt;
        .config(&#039;input&#039;, { name: &#039;source&#039;, rules: [{ required: true }] }, { placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .addColumn(&#039;Тип доступа&#039;, &#039;action&#039;, false)&lt;br /&gt;
        .config(&#039;input&#039;, { name: &#039;action&#039; }, { type: &#039;string&#039;, placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .addColumn(&#039;Доступ&#039;, &#039;access&#039;, false)&lt;br /&gt;
        .config(&#039;checkbox&#039;, { name: &#039;access&#039; }, { type: &#039;boolean&#039;, placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .setRows(rows)&lt;br /&gt;
        .build();&lt;br /&gt;
  return table&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    route: (routes: JournalRecordRouteDto[], recordId: number) =&amp;gt; `Маршрут пробы: ${routes.map(r =&amp;gt; `&amp;quot;${r.stageName}&amp;quot;`).join(&#039; -&amp;gt; &#039;)}. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
    notMoved: (recordId: number) =&amp;gt; `Движение пробы не происходило. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const showTable = async (builder: TableBuilder, controller: PermissionController, socketId: string) =&amp;gt; &lt;br /&gt;
    await getDataFromDialog({ type: &#039;input-data&#039;, config: [await getAccessRightsTable(builder, controller)] }, socketId)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
    try {&lt;br /&gt;
        await showTable(new TableBuilder, new PermissionController(apiInstance), socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Fill-record-fields.png|frame]]&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
import { JournalRecordAttributeFieldResponse } from &amp;quot;../../src/servivces/worker/api/controllers/lj-eav-attribute-fields&amp;quot;&lt;br /&gt;
import { FieldMatch } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const date = new Date().toISOString().split(&#039;T&#039;)[0]&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        { name: &#039;Дата регистрации&#039;, value: date, keywords: [&#039;дат&#039;, &#039;регистр&#039;] },&lt;br /&gt;
        { name: &#039;Место отбора&#039;, value: &#039;Скважина 516563Г &#039;, keywords: [&#039;мест&#039;, &#039;отбор&#039;] },&lt;br /&gt;
        { name: &#039;Дата отбора&#039;, value: date, keywords: [&#039;дат&#039;, &#039;отбор&#039;] },&lt;br /&gt;
        { name: &#039;Шифр пробы&#039;, value: &#039;shifr 123&#039;, keywords: [&#039;дат&#039;, &#039;регистр&#039;]},&lt;br /&gt;
        { name: &#039;Список. Целое число&#039;, value: 666, keywords: [&#039;спис&#039;, &#039;цел&#039;, &#039;числ&#039;] },&lt;br /&gt;
        { name: &#039;Помещение&#039;, value: &#039;помещение&#039;, keywords: [&#039;помещ&#039;]},&lt;br /&gt;
    ] as FieldMatch[],&lt;br /&gt;
    journalRecordId: 118, // Мк / 111&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: (recordId) =&amp;gt; `Все необходимые поля заполнены! Id записи: ${recordId}`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const fieldsMatcher = (field: JournalRecordAttributeFieldResponse, searchField: FieldMatch) =&amp;gt; {&lt;br /&gt;
    return searchField.keywords.every(k =&amp;gt; String(field.name).toLocaleLowerCase().includes(k.toLocaleLowerCase()))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const fillFields = async (fields: FieldMatch[], journalRecordsManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    await journalRecordsManager.setFieldsValues(fields, journalRecordsManager, fieldsMatcher)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        await fillFields(config.requiredFields, journalRecordManager)&lt;br /&gt;
        showSuccessMessage(messages.success(journalRecordManager.journalRecordId), socketId)&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Copy-journal-record.png|frame]]&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { JournalRecordsController } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRequest, JournalRecordResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { JournalRecordAttributeResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
type DataType = JournalRecordAttributeResponse[&#039;field&#039;][&#039;dataType&#039;]&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    journalRecordId: 208,  // Лаб. воздуха / Регистрация&lt;br /&gt;
    nextJournalRecordStage: 2, //  Лаб. воздуха / Анализ&lt;br /&gt;
    nextStageName: &#039;Анализ&#039;,&lt;br /&gt;
    nextRouteId: 50,&lt;br /&gt;
    integrationResult: { message: &#039;Запись ЛЖ скопирована успешно!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const getDateString = (ISODate: unknown | null = null) =&amp;gt; {&lt;br /&gt;
    return new Date(String(ISODate)).toISOString().split(&#039;T&#039;)[0]&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getTimeString = (ISODate: unknown | null = null) =&amp;gt; {&lt;br /&gt;
    return new Date(String(ISODate)).toISOString().split(&#039;T&#039;)[1].split(&#039;.&#039;)[0]&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const createRecord = async (journalRecordController: JournalRecordsController, baseRecord: JournalRecordResponse, newStageId) =&amp;gt; {&lt;br /&gt;
    const getAttrValue = (value: unknown, dataType: DataType) =&amp;gt; {&lt;br /&gt;
        if (dataType === &amp;quot;DATE&amp;quot;) return getDateString(value)&lt;br /&gt;
        if (dataType === &amp;quot;TIME&amp;quot;) return getTimeString(value)&lt;br /&gt;
        return value&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    const getBindings = (baseRecord: JournalRecordResponse): number[] | null =&amp;gt; {&lt;br /&gt;
        return baseRecord?.companyLevelBindings?.map(b =&amp;gt; b?.id) || null&lt;br /&gt;
    } &lt;br /&gt;
&lt;br /&gt;
    const getRecordRequest = (baseRecord: JournalRecordResponse, newStageId: number): JournalRecordRequest =&amp;gt; {&lt;br /&gt;
        return {&lt;br /&gt;
            routeId: baseRecord?.route?.id,&lt;br /&gt;
            stageId: newStageId,&lt;br /&gt;
            analysisObjectId: baseRecord?.analysisObject?.id,&lt;br /&gt;
            sampleTypeId: baseRecord.sampleType.id,&lt;br /&gt;
            attributes: baseRecord.attributes.map(attr =&amp;gt; {&lt;br /&gt;
                return {&lt;br /&gt;
                    fieldId: attr.field.id,&lt;br /&gt;
                    stageId: newStageId,&lt;br /&gt;
                    data: {&lt;br /&gt;
                        ...attr.data, &lt;br /&gt;
                        value: getAttrValue(attr?.data?.value, attr?.field?.dataType),&lt;br /&gt;
                        id: undefined&lt;br /&gt;
                    },&lt;br /&gt;
                    statusId: null&lt;br /&gt;
                }&lt;br /&gt;
            }),&lt;br /&gt;
            companyLevelBindings: getBindings(baseRecord)&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    return await journalRecordController.createRecord(getRecordRequest(baseRecord, newStageId))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const journalRecordController = new JournalRecordsController(apiInstance)&lt;br /&gt;
        const newRecord = await createRecord(journalRecordController, journalRecordManager.data, config.nextJournalRecordStage)&lt;br /&gt;
        showSuccessMessage(`${messages.success}. Id изначальной записи: ${config.journalRecordId}. Id новой записи ЛЖ: ${newRecord.id}. Этап: ${config.nextStageName}`, socketId)&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2375</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2375"/>
		<updated>2026-04-03T04:59:05Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Проверка даты регистрации. check-registration-date */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { MessageBoxResults } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    dateField: &#039;Дата регистрации&#039;,&lt;br /&gt;
    expirationLimit: 10,&lt;br /&gt;
    journalRecordId: 201, // Лаб. воздуха / Анализы завершены&lt;br /&gt;
    integrationResult: { message: &#039;Срок регистрации пробы не истёк !&#039;}&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    success: (recordId) =&amp;gt; `Срок регистрации пробы не истёк ! Id записи ${recordId}`,&lt;br /&gt;
    expired: (limit, recordId) =&amp;gt; `Срок регистрации пробы истёк!\nПрошло более ${ limit } дней с момента регистрации. Id записи: ${recordId}`,&lt;br /&gt;
    dateField: (dateField, stageName, journalRecordId) =&amp;gt; [&lt;br /&gt;
        `Не заполнено либо отсутствует поле &amp;quot;${dateField}&amp;quot;`,&lt;br /&gt;
        `в этапе &amp;quot;${stageName || &#039;&#039;}&amp;quot;.\nId записи: ${journalRecordId}`&lt;br /&gt;
    ].join(&#039; &#039;)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    dateError: (dateField, stageName, journalRecordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.dateField(dateField, stageName, journalRecordId))&lt;br /&gt;
    },&lt;br /&gt;
    expirationError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.expired(config.expirationLimit, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getDaysByDate = (date: Date) =&amp;gt; {&lt;br /&gt;
    const dayMs = 60 * 60 * 24 * 1000&lt;br /&gt;
    const timestamp = date.getTime()&lt;br /&gt;
    return timestamp/dayMs&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getExpirationOffset = (registrationDate: string) =&amp;gt; {&lt;br /&gt;
    const regDate = new Date(registrationDate)&lt;br /&gt;
    const today = new Date()&lt;br /&gt;
    return getDaysByDate(today) - getDaysByDate(regDate)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const date = journalRecordManager.getFieldValue(config.dateField)&lt;br /&gt;
&lt;br /&gt;
        if (!date) errorHandlers.dateError(config.dateField, journalRecordManager?.data?.stage?.stage?.name, config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        const expirationOffset = getExpirationOffset(date as string)&lt;br /&gt;
&lt;br /&gt;
        if (expirationOffset &amp;gt;= config.expirationLimit) errorHandlers.expirationError()&lt;br /&gt;
&lt;br /&gt;
        await showSuccessMessage(messages.success(config.journalRecordId), socketId)&lt;br /&gt;
&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordIndicatorResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
type IndicatorProp = { label: string, get: () =&amp;gt; unknown }&lt;br /&gt;
type IndicatorPropsConfig = Record&amp;lt;string, IndicatorProp&amp;gt;&lt;br /&gt;
&lt;br /&gt;
const indicatorPropsConfig = (indicator): IndicatorPropsConfig =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        executors: {&lt;br /&gt;
            label: &#039;Исполнители&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.executors&lt;br /&gt;
        },&lt;br /&gt;
        methodic: {&lt;br /&gt;
            label: &#039;Методики&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.methodics&lt;br /&gt;
        },&lt;br /&gt;
        averageResult: {&lt;br /&gt;
            label: &#039;Средний результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageValue&lt;br /&gt;
        },&lt;br /&gt;
        averageRoundedValue: {&lt;br /&gt;
            label: &#039;Средний округлённый результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageRoundedValue&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Место отбора&#039;,&lt;br /&gt;
        &#039;Дата регистрации&#039;,&lt;br /&gt;
        &#039;Шифр пробы&#039;,&lt;br /&gt;
        &#039;Время регистрации&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    indicatorsList: [&lt;br /&gt;
        &#039;Аммоний-ион&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144, // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyIndicators: (recordId) =&amp;gt; `в записи с Id ${recordId} отсутствуют показатели`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
    indicators: (indicators: string[], recordId) =&amp;gt; `Показатели ${indicators.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} отсутствуют в записи с ID ${recordId}`,&lt;br /&gt;
    emptyIndicatorsProps: (props: Record&amp;lt;string, string[]&amp;gt;, recordId) =&amp;gt; {&lt;br /&gt;
        const propsString = Object.keys(props).map(prop =&amp;gt; !isEmptyArray(props[prop]) ? `${prop}: ${props[prop].map(p =&amp;gt; `&amp;quot;${p}&amp;quot;`).join(&#039;, &#039;)}` : &#039;&#039;).join(&#039;; &#039;)&lt;br /&gt;
        return `В записи с Id ${recordId} отсутствуют либо не заполнены следующие поля у показателей: ${propsString}`&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    indicatorsError: (recordId) =&amp;gt;{&lt;br /&gt;
        throw new Error(messages.emptyIndicators(recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFoundIndicators: (indicators: string[], recordId: number) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.indicators(indicators, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFilledIndicatorProps: (props: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyIndicatorsProps(props, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(arr) &amp;amp;&amp;amp; arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasIndicators = (journalRecordManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
    return Array.isArray(indicators) &amp;amp;&amp;amp; indicators.length &amp;gt; 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFoundIndicators = (indicators: JournalRecordIndicatorResponse[], indicatorsList = config.indicatorsList) =&amp;gt; {&lt;br /&gt;
    return indicatorsList.filter(ind =&amp;gt; indicators.every(ljInd =&amp;gt; ljInd?.indicator?.name?.toLowerCase() !== ind.toLocaleLowerCase()))&lt;br /&gt;
} &lt;br /&gt;
&lt;br /&gt;
const getNotFilledResultsFields = (indicators: JournalRecordIndicatorResponse[]): Record&amp;lt;string, string[]&amp;gt; =&amp;gt; {&lt;br /&gt;
    return indicators.reduce((acc, indicator) =&amp;gt; {&lt;br /&gt;
        const config = indicatorPropsConfig(indicator)&lt;br /&gt;
        return {&lt;br /&gt;
            ...acc,&lt;br /&gt;
            [indicator?.indicator?.name]: Object.keys(config).reduce((acc, prop) =&amp;gt; {&lt;br /&gt;
                if (!config[prop].get()) return [...acc, config[prop].label]&lt;br /&gt;
                return acc&lt;br /&gt;
            }, [] as string[])&lt;br /&gt;
        }&lt;br /&gt;
    }, {}) as Record&amp;lt;string, string[]&amp;gt;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFilledProps = (indicatorsProps: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
    return Object.keys(indicatorsProps).some(indicator =&amp;gt; !isEmptyArray(indicatorsProps[indicator]))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFoundIndicators = (indicators: string[]) =&amp;gt; !isEmptyArray(indicators)&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (!hasIndicators(journalRecordManager)) {&lt;br /&gt;
            errorHandlers.indicatorsError(journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
&lt;br /&gt;
        const notFoundIndicators = getNotFoundIndicators(indicators)&lt;br /&gt;
        if (hasNotFoundIndicators(notFoundIndicators)) { &lt;br /&gt;
            errorHandlers.notFoundIndicators(notFoundIndicators, journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const emptyIndicatorProps = getNotFilledResultsFields(indicators)&lt;br /&gt;
        if (hasNotFilledProps(emptyIndicatorProps)) {&lt;br /&gt;
            errorHandlers.notFilledIndicatorProps(emptyIndicatorProps)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        showSuccessMessage(messages.success, socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-record-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Список. Целое число&#039;,&lt;br /&gt;
        &#039;Строка&#039;,&lt;br /&gt;
        &#039;Целое число&#039;,&lt;br /&gt;
        &#039;Вещественное число&#039;,&lt;br /&gt;
        &#039;Логический тип&#039;,&lt;br /&gt;
        &#039;Дата&#039;,&lt;br /&gt;
        &#039;Время&#039;,&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144,  // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFilledFields = (fields: string[], journalRecordsManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    console.log(journalRecordsManager.data.attributes, &#039;data.attributes&#039;)&lt;br /&gt;
    return fields.filter(f =&amp;gt; !journalRecordsManager.getFieldValue(f))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const emptyFields = getNotFilledFields(config.requiredFields, journalRecordManager)&lt;br /&gt;
&lt;br /&gt;
        if (isEmptyArray(emptyFields)) {&lt;br /&gt;
            showSuccessMessage(messages.success, socketId)&lt;br /&gt;
            return config.integrationResult&lt;br /&gt;
        } else errorHandlers.fieldsError(emptyFields, config.journalRecordId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-sample-moving.png|frame]]&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { JournalsRecordRouteController } from &amp;quot;../../src/servivces/worker/api/controllers/journals-record-route&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRouteDto } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    journalRecordId: 114 // Лаб. воздуха / Анализы завершены&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    route: (routes: JournalRecordRouteDto[], recordId: number) =&amp;gt; `Машрут пробы: ${routes.map(r =&amp;gt; `&amp;quot;${r.stageName}&amp;quot;`).join(&#039; -&amp;gt; &#039;)}. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
    notMoved: (recordId: number) =&amp;gt; `Движение пробы не происходило. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isSampleMoved = (moves: JournalRecordRouteDto[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(moves) &amp;amp;&amp;amp; moves.length &amp;gt; 1&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalsRecordRouteController = new JournalsRecordRouteController(apiInstance)&lt;br /&gt;
        const sampleMoves = await journalsRecordRouteController.getJournalRecordRoute(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (isSampleMoved(sampleMoves)) {&lt;br /&gt;
            showSuccessMessage(messages.route(sampleMoves, config.journalRecordId), socketId)&lt;br /&gt;
        } else {&lt;br /&gt;
            showSuccessMessage(messages.notMoved(config.journalRecordId), socketId)&lt;br /&gt;
        }&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-user-rights.png|frame]]&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import { &lt;br /&gt;
    PermissionController, &lt;br /&gt;
    PermissionResource, &lt;br /&gt;
    PermissionRwAccess&lt;br /&gt;
} from &amp;quot;../../src/servivces/worker/api/controllers/permisson&amp;quot;&lt;br /&gt;
&lt;br /&gt;
import { TableBuilder } from &amp;quot;../../src/gui/builder&amp;quot;&lt;br /&gt;
import type { DialogType, TableConfig } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRouteDto } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const accessNamesMap = {&lt;br /&gt;
    [PermissionRwAccess.EXEC]: &#039;Выполнение&#039;,&lt;br /&gt;
    [PermissionRwAccess.READ]: &#039;Чтение&#039;,&lt;br /&gt;
    [PermissionRwAccess.WRITE]: &#039;Запись&#039;,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const requiredResources = [&lt;br /&gt;
    PermissionResource.ANALYSIS_OBJECT, &lt;br /&gt;
    PermissionResource.METHODOLOGY,&lt;br /&gt;
    PermissionResource.JOURNAL_RECORD&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
const resourcesNamesMap = {&lt;br /&gt;
    [PermissionResource.ANALYSIS_OBJECT]: &#039;Объекты анализа&#039;, &lt;br /&gt;
    [PermissionResource.METHODOLOGY]: &#039;Методики&#039;, &lt;br /&gt;
    [PermissionResource.JOURNAL_RECORD]: &#039;Записи ЛЖ&#039;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getAccessRightsTable = async (tableBuilder: TableBuilder, permissionController: PermissionController): Promise&amp;lt;TableConfig&amp;gt; =&amp;gt; {&lt;br /&gt;
    const rows = (await Promise.all(requiredResources.map(async (source: PermissionResource) =&amp;gt; {&lt;br /&gt;
            const sourceCombinations = await Promise.all(Object.keys(PermissionRwAccess).map(async (action) =&amp;gt; {&lt;br /&gt;
                const access = await permissionController.getSourcePermission(source, PermissionRwAccess[action])&lt;br /&gt;
                return { source: resourcesNamesMap[source], action: accessNamesMap[PermissionRwAccess[action]], access: access.hasPermission }&lt;br /&gt;
            }))&lt;br /&gt;
            return sourceCombinations&lt;br /&gt;
        }   &lt;br /&gt;
    ))).flat()&lt;br /&gt;
    const table = tableBuilder&lt;br /&gt;
        .name(&#039;simpleTable&#039;)&lt;br /&gt;
        .label(&#039;Таблица&#039;)&lt;br /&gt;
        .isAddable(false)&lt;br /&gt;
        .actionsCol(false)&lt;br /&gt;
        .addColumn(&#039;Ресурс&#039;, &#039;source&#039;, false)&lt;br /&gt;
        .config(&#039;input&#039;, { name: &#039;source&#039;, rules: [{ required: true }] }, { placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .addColumn(&#039;Тип доступа&#039;, &#039;action&#039;, false)&lt;br /&gt;
        .config(&#039;input&#039;, { name: &#039;action&#039; }, { type: &#039;string&#039;, placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .addColumn(&#039;Доступ&#039;, &#039;access&#039;, false)&lt;br /&gt;
        .config(&#039;checkbox&#039;, { name: &#039;access&#039; }, { type: &#039;boolean&#039;, placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .setRows(rows)&lt;br /&gt;
        .build();&lt;br /&gt;
  return table&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    route: (routes: JournalRecordRouteDto[], recordId: number) =&amp;gt; `Маршрут пробы: ${routes.map(r =&amp;gt; `&amp;quot;${r.stageName}&amp;quot;`).join(&#039; -&amp;gt; &#039;)}. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
    notMoved: (recordId: number) =&amp;gt; `Движение пробы не происходило. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const showTable = async (builder: TableBuilder, controller: PermissionController, socketId: string) =&amp;gt; &lt;br /&gt;
    await getDataFromDialog({ type: &#039;input-data&#039;, config: [await getAccessRightsTable(builder, controller)] }, socketId)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
    try {&lt;br /&gt;
        await showTable(new TableBuilder, new PermissionController(apiInstance), socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Fill-record-fields.png|frame]]&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
import { JournalRecordAttributeFieldResponse } from &amp;quot;../../src/servivces/worker/api/controllers/lj-eav-attribute-fields&amp;quot;&lt;br /&gt;
import { FieldMatch } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const date = new Date().toISOString().split(&#039;T&#039;)[0]&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        { name: &#039;Дата регистрации&#039;, value: date, keywords: [&#039;дат&#039;, &#039;регистр&#039;] },&lt;br /&gt;
        { name: &#039;Место отбора&#039;, value: &#039;Скважина 516563Г &#039;, keywords: [&#039;мест&#039;, &#039;отбор&#039;] },&lt;br /&gt;
        { name: &#039;Дата отбора&#039;, value: date, keywords: [&#039;дат&#039;, &#039;отбор&#039;] },&lt;br /&gt;
        { name: &#039;Шифр пробы&#039;, value: &#039;shifr 123&#039;, keywords: [&#039;дат&#039;, &#039;регистр&#039;]},&lt;br /&gt;
        { name: &#039;Список. Целое число&#039;, value: 666, keywords: [&#039;спис&#039;, &#039;цел&#039;, &#039;числ&#039;] },&lt;br /&gt;
        { name: &#039;Помещение&#039;, value: &#039;помещение&#039;, keywords: [&#039;помещ&#039;]},&lt;br /&gt;
    ] as FieldMatch[],&lt;br /&gt;
    journalRecordId: 118, // Мк / 111&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: (recordId) =&amp;gt; `Все необходимые поля заполнены! Id записи: ${recordId}`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const fieldsMatcher = (field: JournalRecordAttributeFieldResponse, searchField: FieldMatch) =&amp;gt; {&lt;br /&gt;
    return searchField.keywords.every(k =&amp;gt; String(field.name).toLocaleLowerCase().includes(k.toLocaleLowerCase()))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const fillFields = async (fields: FieldMatch[], journalRecordsManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    await journalRecordsManager.setFieldsValues(fields, journalRecordsManager, fieldsMatcher)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        await fillFields(config.requiredFields, journalRecordManager)&lt;br /&gt;
        showSuccessMessage(messages.success(journalRecordManager.journalRecordId), socketId)&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Copy-journal-record.png|frame]]&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { JournalRecordsController } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRequest, JournalRecordResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { JournalRecordAttributeResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
type DataType = JournalRecordAttributeResponse[&#039;field&#039;][&#039;dataType&#039;]&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    journalRecordId: 208,  // Лаб. воздуха / Регистрация&lt;br /&gt;
    nextJournalRecordStage: 2, //  Лаб. воздуха / Анализ&lt;br /&gt;
    nextStageName: &#039;Анализ&#039;,&lt;br /&gt;
    nextRouteId: 50,&lt;br /&gt;
    integrationResult: { message: &#039;Запись ЛЖ скопирована успешно!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const getDateString = (ISODate: unknown | null = null) =&amp;gt; {&lt;br /&gt;
    return new Date(String(ISODate)).toISOString().split(&#039;T&#039;)[0]&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getTimeString = (ISODate: unknown | null = null) =&amp;gt; {&lt;br /&gt;
    return new Date(String(ISODate)).toISOString().split(&#039;T&#039;)[1].split(&#039;.&#039;)[0]&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const createRecord = async (journalRecordController: JournalRecordsController, baseRecord: JournalRecordResponse, newStageId) =&amp;gt; {&lt;br /&gt;
    const getAttrValue = (value: unknown, dataType: DataType) =&amp;gt; {&lt;br /&gt;
        if (dataType === &amp;quot;DATE&amp;quot;) return getDateString(value)&lt;br /&gt;
        if (dataType === &amp;quot;TIME&amp;quot;) return getTimeString(value)&lt;br /&gt;
        return value&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    const getBindings = (baseRecord: JournalRecordResponse): number[] | null =&amp;gt; {&lt;br /&gt;
        return baseRecord?.companyLevelBindings?.map(b =&amp;gt; b?.id) || null&lt;br /&gt;
    } &lt;br /&gt;
&lt;br /&gt;
    const getRecordRequest = (baseRecord: JournalRecordResponse, newStageId: number): JournalRecordRequest =&amp;gt; {&lt;br /&gt;
        return {&lt;br /&gt;
            routeId: baseRecord?.route?.id,&lt;br /&gt;
            stageId: newStageId,&lt;br /&gt;
            analysisObjectId: baseRecord?.analysisObject?.id,&lt;br /&gt;
            sampleTypeId: baseRecord.sampleType.id,&lt;br /&gt;
            attributes: baseRecord.attributes.map(attr =&amp;gt; {&lt;br /&gt;
                return {&lt;br /&gt;
                    fieldId: attr.field.id,&lt;br /&gt;
                    stageId: newStageId,&lt;br /&gt;
                    data: {&lt;br /&gt;
                        ...attr.data, &lt;br /&gt;
                        value: getAttrValue(attr?.data?.value, attr?.field?.dataType),&lt;br /&gt;
                        id: undefined&lt;br /&gt;
                    },&lt;br /&gt;
                    statusId: null&lt;br /&gt;
                }&lt;br /&gt;
            }),&lt;br /&gt;
            companyLevelBindings: getBindings(baseRecord)&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    return await journalRecordController.createRecord(getRecordRequest(baseRecord, newStageId))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const journalRecordController = new JournalRecordsController(apiInstance)&lt;br /&gt;
        const newRecord = await createRecord(journalRecordController, journalRecordManager.data, config.nextJournalRecordStage)&lt;br /&gt;
        showSuccessMessage(`${messages.success}. Id изначальной записи: ${config.journalRecordId}. Id новой записи ЛЖ: ${newRecord.id}. Этап: ${config.nextStageName}`, socketId)&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2374</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2374"/>
		<updated>2026-04-03T04:36:45Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Проверка даты регистрации. check-registration-date */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible&amp;quot; style=&amp;quot;overflow:auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight:bold;line-height:1.6;&amp;quot;&amp;gt;Код&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { MessageBoxResults } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    dateField: &#039;Дата регистрации&#039;,&lt;br /&gt;
    expirationLimit: 10,&lt;br /&gt;
    journalRecordId: 201, // Лаб. воздуха / Анализы завершены&lt;br /&gt;
    integrationResult: { message: &#039;Срок регистрации пробы не истёк !&#039;}&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    success: (recordId) =&amp;gt; `Срок регистрации пробы не истёк ! Id записи ${recordId}`,&lt;br /&gt;
    expired: (limit, recordId) =&amp;gt; `Срок регистрации пробы истёк!\nПрошло более ${ limit } дней с момента регистрации. Id записи: ${recordId}`,&lt;br /&gt;
    dateField: (dateField, stageName, journalRecordId) =&amp;gt; [&lt;br /&gt;
        `Не заполнено либо отсутствует поле &amp;quot;${dateField}&amp;quot;`,&lt;br /&gt;
        `в этапе &amp;quot;${stageName || &#039;&#039;}&amp;quot;.\nId записи: ${journalRecordId}`&lt;br /&gt;
    ].join(&#039; &#039;)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    dateError: (dateField, stageName, journalRecordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.dateField(dateField, stageName, journalRecordId))&lt;br /&gt;
    },&lt;br /&gt;
    expirationError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.expired(config.expirationLimit, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getDaysByDate = (date: Date) =&amp;gt; {&lt;br /&gt;
    const dayMs = 60 * 60 * 24 * 1000&lt;br /&gt;
    const timestamp = date.getTime()&lt;br /&gt;
    return timestamp/dayMs&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getExpirationOffset = (registrationDate: string) =&amp;gt; {&lt;br /&gt;
    const regDate = new Date(registrationDate)&lt;br /&gt;
    const today = new Date()&lt;br /&gt;
    return getDaysByDate(today) - getDaysByDate(regDate)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const date = journalRecordManager.getFieldValue(config.dateField)&lt;br /&gt;
&lt;br /&gt;
        if (!date) errorHandlers.dateError(config.dateField, journalRecordManager?.data?.stage?.stage?.name, config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        const expirationOffset = getExpirationOffset(date as string)&lt;br /&gt;
&lt;br /&gt;
        if (expirationOffset &amp;gt;= config.expirationLimit) errorHandlers.expirationError()&lt;br /&gt;
&lt;br /&gt;
        await showSuccessMessage(messages.success(config.journalRecordId), socketId)&lt;br /&gt;
&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordIndicatorResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
type IndicatorProp = { label: string, get: () =&amp;gt; unknown }&lt;br /&gt;
type IndicatorPropsConfig = Record&amp;lt;string, IndicatorProp&amp;gt;&lt;br /&gt;
&lt;br /&gt;
const indicatorPropsConfig = (indicator): IndicatorPropsConfig =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        executors: {&lt;br /&gt;
            label: &#039;Исполнители&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.executors&lt;br /&gt;
        },&lt;br /&gt;
        methodic: {&lt;br /&gt;
            label: &#039;Методики&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.methodics&lt;br /&gt;
        },&lt;br /&gt;
        averageResult: {&lt;br /&gt;
            label: &#039;Средний результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageValue&lt;br /&gt;
        },&lt;br /&gt;
        averageRoundedValue: {&lt;br /&gt;
            label: &#039;Средний округлённый результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageRoundedValue&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Место отбора&#039;,&lt;br /&gt;
        &#039;Дата регистрации&#039;,&lt;br /&gt;
        &#039;Шифр пробы&#039;,&lt;br /&gt;
        &#039;Время регистрации&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    indicatorsList: [&lt;br /&gt;
        &#039;Аммоний-ион&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144, // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyIndicators: (recordId) =&amp;gt; `в записи с Id ${recordId} отсутствуют показатели`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
    indicators: (indicators: string[], recordId) =&amp;gt; `Показатели ${indicators.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} отсутствуют в записи с ID ${recordId}`,&lt;br /&gt;
    emptyIndicatorsProps: (props: Record&amp;lt;string, string[]&amp;gt;, recordId) =&amp;gt; {&lt;br /&gt;
        const propsString = Object.keys(props).map(prop =&amp;gt; !isEmptyArray(props[prop]) ? `${prop}: ${props[prop].map(p =&amp;gt; `&amp;quot;${p}&amp;quot;`).join(&#039;, &#039;)}` : &#039;&#039;).join(&#039;; &#039;)&lt;br /&gt;
        return `В записи с Id ${recordId} отсутствуют либо не заполнены следующие поля у показателей: ${propsString}`&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    indicatorsError: (recordId) =&amp;gt;{&lt;br /&gt;
        throw new Error(messages.emptyIndicators(recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFoundIndicators: (indicators: string[], recordId: number) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.indicators(indicators, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFilledIndicatorProps: (props: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyIndicatorsProps(props, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(arr) &amp;amp;&amp;amp; arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasIndicators = (journalRecordManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
    return Array.isArray(indicators) &amp;amp;&amp;amp; indicators.length &amp;gt; 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFoundIndicators = (indicators: JournalRecordIndicatorResponse[], indicatorsList = config.indicatorsList) =&amp;gt; {&lt;br /&gt;
    return indicatorsList.filter(ind =&amp;gt; indicators.every(ljInd =&amp;gt; ljInd?.indicator?.name?.toLowerCase() !== ind.toLocaleLowerCase()))&lt;br /&gt;
} &lt;br /&gt;
&lt;br /&gt;
const getNotFilledResultsFields = (indicators: JournalRecordIndicatorResponse[]): Record&amp;lt;string, string[]&amp;gt; =&amp;gt; {&lt;br /&gt;
    return indicators.reduce((acc, indicator) =&amp;gt; {&lt;br /&gt;
        const config = indicatorPropsConfig(indicator)&lt;br /&gt;
        return {&lt;br /&gt;
            ...acc,&lt;br /&gt;
            [indicator?.indicator?.name]: Object.keys(config).reduce((acc, prop) =&amp;gt; {&lt;br /&gt;
                if (!config[prop].get()) return [...acc, config[prop].label]&lt;br /&gt;
                return acc&lt;br /&gt;
            }, [] as string[])&lt;br /&gt;
        }&lt;br /&gt;
    }, {}) as Record&amp;lt;string, string[]&amp;gt;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFilledProps = (indicatorsProps: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
    return Object.keys(indicatorsProps).some(indicator =&amp;gt; !isEmptyArray(indicatorsProps[indicator]))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFoundIndicators = (indicators: string[]) =&amp;gt; !isEmptyArray(indicators)&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (!hasIndicators(journalRecordManager)) {&lt;br /&gt;
            errorHandlers.indicatorsError(journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
&lt;br /&gt;
        const notFoundIndicators = getNotFoundIndicators(indicators)&lt;br /&gt;
        if (hasNotFoundIndicators(notFoundIndicators)) { &lt;br /&gt;
            errorHandlers.notFoundIndicators(notFoundIndicators, journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const emptyIndicatorProps = getNotFilledResultsFields(indicators)&lt;br /&gt;
        if (hasNotFilledProps(emptyIndicatorProps)) {&lt;br /&gt;
            errorHandlers.notFilledIndicatorProps(emptyIndicatorProps)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        showSuccessMessage(messages.success, socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-record-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Список. Целое число&#039;,&lt;br /&gt;
        &#039;Строка&#039;,&lt;br /&gt;
        &#039;Целое число&#039;,&lt;br /&gt;
        &#039;Вещественное число&#039;,&lt;br /&gt;
        &#039;Логический тип&#039;,&lt;br /&gt;
        &#039;Дата&#039;,&lt;br /&gt;
        &#039;Время&#039;,&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144,  // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFilledFields = (fields: string[], journalRecordsManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    console.log(journalRecordsManager.data.attributes, &#039;data.attributes&#039;)&lt;br /&gt;
    return fields.filter(f =&amp;gt; !journalRecordsManager.getFieldValue(f))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const emptyFields = getNotFilledFields(config.requiredFields, journalRecordManager)&lt;br /&gt;
&lt;br /&gt;
        if (isEmptyArray(emptyFields)) {&lt;br /&gt;
            showSuccessMessage(messages.success, socketId)&lt;br /&gt;
            return config.integrationResult&lt;br /&gt;
        } else errorHandlers.fieldsError(emptyFields, config.journalRecordId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-sample-moving.png|frame]]&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { JournalsRecordRouteController } from &amp;quot;../../src/servivces/worker/api/controllers/journals-record-route&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRouteDto } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    journalRecordId: 114 // Лаб. воздуха / Анализы завершены&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    route: (routes: JournalRecordRouteDto[], recordId: number) =&amp;gt; `Машрут пробы: ${routes.map(r =&amp;gt; `&amp;quot;${r.stageName}&amp;quot;`).join(&#039; -&amp;gt; &#039;)}. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
    notMoved: (recordId: number) =&amp;gt; `Движение пробы не происходило. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isSampleMoved = (moves: JournalRecordRouteDto[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(moves) &amp;amp;&amp;amp; moves.length &amp;gt; 1&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalsRecordRouteController = new JournalsRecordRouteController(apiInstance)&lt;br /&gt;
        const sampleMoves = await journalsRecordRouteController.getJournalRecordRoute(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (isSampleMoved(sampleMoves)) {&lt;br /&gt;
            showSuccessMessage(messages.route(sampleMoves, config.journalRecordId), socketId)&lt;br /&gt;
        } else {&lt;br /&gt;
            showSuccessMessage(messages.notMoved(config.journalRecordId), socketId)&lt;br /&gt;
        }&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-user-rights.png|frame]]&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import { &lt;br /&gt;
    PermissionController, &lt;br /&gt;
    PermissionResource, &lt;br /&gt;
    PermissionRwAccess&lt;br /&gt;
} from &amp;quot;../../src/servivces/worker/api/controllers/permisson&amp;quot;&lt;br /&gt;
&lt;br /&gt;
import { TableBuilder } from &amp;quot;../../src/gui/builder&amp;quot;&lt;br /&gt;
import type { DialogType, TableConfig } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRouteDto } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const accessNamesMap = {&lt;br /&gt;
    [PermissionRwAccess.EXEC]: &#039;Выполнение&#039;,&lt;br /&gt;
    [PermissionRwAccess.READ]: &#039;Чтение&#039;,&lt;br /&gt;
    [PermissionRwAccess.WRITE]: &#039;Запись&#039;,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const requiredResources = [&lt;br /&gt;
    PermissionResource.ANALYSIS_OBJECT, &lt;br /&gt;
    PermissionResource.METHODOLOGY,&lt;br /&gt;
    PermissionResource.JOURNAL_RECORD&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
const resourcesNamesMap = {&lt;br /&gt;
    [PermissionResource.ANALYSIS_OBJECT]: &#039;Объекты анализа&#039;, &lt;br /&gt;
    [PermissionResource.METHODOLOGY]: &#039;Методики&#039;, &lt;br /&gt;
    [PermissionResource.JOURNAL_RECORD]: &#039;Записи ЛЖ&#039;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getAccessRightsTable = async (tableBuilder: TableBuilder, permissionController: PermissionController): Promise&amp;lt;TableConfig&amp;gt; =&amp;gt; {&lt;br /&gt;
    const rows = (await Promise.all(requiredResources.map(async (source: PermissionResource) =&amp;gt; {&lt;br /&gt;
            const sourceCombinations = await Promise.all(Object.keys(PermissionRwAccess).map(async (action) =&amp;gt; {&lt;br /&gt;
                const access = await permissionController.getSourcePermission(source, PermissionRwAccess[action])&lt;br /&gt;
                return { source: resourcesNamesMap[source], action: accessNamesMap[PermissionRwAccess[action]], access: access.hasPermission }&lt;br /&gt;
            }))&lt;br /&gt;
            return sourceCombinations&lt;br /&gt;
        }   &lt;br /&gt;
    ))).flat()&lt;br /&gt;
    const table = tableBuilder&lt;br /&gt;
        .name(&#039;simpleTable&#039;)&lt;br /&gt;
        .label(&#039;Таблица&#039;)&lt;br /&gt;
        .isAddable(false)&lt;br /&gt;
        .actionsCol(false)&lt;br /&gt;
        .addColumn(&#039;Ресурс&#039;, &#039;source&#039;, false)&lt;br /&gt;
        .config(&#039;input&#039;, { name: &#039;source&#039;, rules: [{ required: true }] }, { placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .addColumn(&#039;Тип доступа&#039;, &#039;action&#039;, false)&lt;br /&gt;
        .config(&#039;input&#039;, { name: &#039;action&#039; }, { type: &#039;string&#039;, placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .addColumn(&#039;Доступ&#039;, &#039;access&#039;, false)&lt;br /&gt;
        .config(&#039;checkbox&#039;, { name: &#039;access&#039; }, { type: &#039;boolean&#039;, placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .setRows(rows)&lt;br /&gt;
        .build();&lt;br /&gt;
  return table&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    route: (routes: JournalRecordRouteDto[], recordId: number) =&amp;gt; `Маршрут пробы: ${routes.map(r =&amp;gt; `&amp;quot;${r.stageName}&amp;quot;`).join(&#039; -&amp;gt; &#039;)}. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
    notMoved: (recordId: number) =&amp;gt; `Движение пробы не происходило. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const showTable = async (builder: TableBuilder, controller: PermissionController, socketId: string) =&amp;gt; &lt;br /&gt;
    await getDataFromDialog({ type: &#039;input-data&#039;, config: [await getAccessRightsTable(builder, controller)] }, socketId)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
    try {&lt;br /&gt;
        await showTable(new TableBuilder, new PermissionController(apiInstance), socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Fill-record-fields.png|frame]]&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
import { JournalRecordAttributeFieldResponse } from &amp;quot;../../src/servivces/worker/api/controllers/lj-eav-attribute-fields&amp;quot;&lt;br /&gt;
import { FieldMatch } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const date = new Date().toISOString().split(&#039;T&#039;)[0]&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        { name: &#039;Дата регистрации&#039;, value: date, keywords: [&#039;дат&#039;, &#039;регистр&#039;] },&lt;br /&gt;
        { name: &#039;Место отбора&#039;, value: &#039;Скважина 516563Г &#039;, keywords: [&#039;мест&#039;, &#039;отбор&#039;] },&lt;br /&gt;
        { name: &#039;Дата отбора&#039;, value: date, keywords: [&#039;дат&#039;, &#039;отбор&#039;] },&lt;br /&gt;
        { name: &#039;Шифр пробы&#039;, value: &#039;shifr 123&#039;, keywords: [&#039;дат&#039;, &#039;регистр&#039;]},&lt;br /&gt;
        { name: &#039;Список. Целое число&#039;, value: 666, keywords: [&#039;спис&#039;, &#039;цел&#039;, &#039;числ&#039;] },&lt;br /&gt;
        { name: &#039;Помещение&#039;, value: &#039;помещение&#039;, keywords: [&#039;помещ&#039;]},&lt;br /&gt;
    ] as FieldMatch[],&lt;br /&gt;
    journalRecordId: 118, // Мк / 111&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: (recordId) =&amp;gt; `Все необходимые поля заполнены! Id записи: ${recordId}`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const fieldsMatcher = (field: JournalRecordAttributeFieldResponse, searchField: FieldMatch) =&amp;gt; {&lt;br /&gt;
    return searchField.keywords.every(k =&amp;gt; String(field.name).toLocaleLowerCase().includes(k.toLocaleLowerCase()))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const fillFields = async (fields: FieldMatch[], journalRecordsManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    await journalRecordsManager.setFieldsValues(fields, journalRecordsManager, fieldsMatcher)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        await fillFields(config.requiredFields, journalRecordManager)&lt;br /&gt;
        showSuccessMessage(messages.success(journalRecordManager.journalRecordId), socketId)&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Copy-journal-record.png|frame]]&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { JournalRecordsController } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRequest, JournalRecordResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { JournalRecordAttributeResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
type DataType = JournalRecordAttributeResponse[&#039;field&#039;][&#039;dataType&#039;]&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    journalRecordId: 208,  // Лаб. воздуха / Регистрация&lt;br /&gt;
    nextJournalRecordStage: 2, //  Лаб. воздуха / Анализ&lt;br /&gt;
    nextStageName: &#039;Анализ&#039;,&lt;br /&gt;
    nextRouteId: 50,&lt;br /&gt;
    integrationResult: { message: &#039;Запись ЛЖ скопирована успешно!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const getDateString = (ISODate: unknown | null = null) =&amp;gt; {&lt;br /&gt;
    return new Date(String(ISODate)).toISOString().split(&#039;T&#039;)[0]&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getTimeString = (ISODate: unknown | null = null) =&amp;gt; {&lt;br /&gt;
    return new Date(String(ISODate)).toISOString().split(&#039;T&#039;)[1].split(&#039;.&#039;)[0]&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const createRecord = async (journalRecordController: JournalRecordsController, baseRecord: JournalRecordResponse, newStageId) =&amp;gt; {&lt;br /&gt;
    const getAttrValue = (value: unknown, dataType: DataType) =&amp;gt; {&lt;br /&gt;
        if (dataType === &amp;quot;DATE&amp;quot;) return getDateString(value)&lt;br /&gt;
        if (dataType === &amp;quot;TIME&amp;quot;) return getTimeString(value)&lt;br /&gt;
        return value&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    const getBindings = (baseRecord: JournalRecordResponse): number[] | null =&amp;gt; {&lt;br /&gt;
        return baseRecord?.companyLevelBindings?.map(b =&amp;gt; b?.id) || null&lt;br /&gt;
    } &lt;br /&gt;
&lt;br /&gt;
    const getRecordRequest = (baseRecord: JournalRecordResponse, newStageId: number): JournalRecordRequest =&amp;gt; {&lt;br /&gt;
        return {&lt;br /&gt;
            routeId: baseRecord?.route?.id,&lt;br /&gt;
            stageId: newStageId,&lt;br /&gt;
            analysisObjectId: baseRecord?.analysisObject?.id,&lt;br /&gt;
            sampleTypeId: baseRecord.sampleType.id,&lt;br /&gt;
            attributes: baseRecord.attributes.map(attr =&amp;gt; {&lt;br /&gt;
                return {&lt;br /&gt;
                    fieldId: attr.field.id,&lt;br /&gt;
                    stageId: newStageId,&lt;br /&gt;
                    data: {&lt;br /&gt;
                        ...attr.data, &lt;br /&gt;
                        value: getAttrValue(attr?.data?.value, attr?.field?.dataType),&lt;br /&gt;
                        id: undefined&lt;br /&gt;
                    },&lt;br /&gt;
                    statusId: null&lt;br /&gt;
                }&lt;br /&gt;
            }),&lt;br /&gt;
            companyLevelBindings: getBindings(baseRecord)&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    return await journalRecordController.createRecord(getRecordRequest(baseRecord, newStageId))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const journalRecordController = new JournalRecordsController(apiInstance)&lt;br /&gt;
        const newRecord = await createRecord(journalRecordController, journalRecordManager.data, config.nextJournalRecordStage)&lt;br /&gt;
        showSuccessMessage(`${messages.success}. Id изначальной записи: ${config.journalRecordId}. Id новой записи ЛЖ: ${newRecord.id}. Этап: ${config.nextStageName}`, socketId)&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2372</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2372"/>
		<updated>2026-04-02T01:39:08Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Деление данных. copy-journal-record */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
   &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { MessageBoxResults } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    dateField: &#039;Дата регистрации&#039;,&lt;br /&gt;
    expirationLimit: 10,&lt;br /&gt;
    journalRecordId: 201, // Лаб. воздуха / Анализы завершены&lt;br /&gt;
    integrationResult: { message: &#039;Срок регистрации пробы не истёк !&#039;}&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    success: (recordId) =&amp;gt; `Срок регистрации пробы не истёк ! Id записи ${recordId}`,&lt;br /&gt;
    expired: (limit, recordId) =&amp;gt; `Срок регистрации пробы истёк!\nПрошло более ${ limit } дней с момента регистрации. Id записи: ${recordId}`,&lt;br /&gt;
    dateField: (dateField, stageName, journalRecordId) =&amp;gt; [&lt;br /&gt;
        `Не заполнено либо отсутствует поле &amp;quot;${dateField}&amp;quot;`,&lt;br /&gt;
        `в этапе &amp;quot;${stageName || &#039;&#039;}&amp;quot;.\nId записи: ${journalRecordId}`&lt;br /&gt;
    ].join(&#039; &#039;)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    dateError: (dateField, stageName, journalRecordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.dateField(dateField, stageName, journalRecordId))&lt;br /&gt;
    },&lt;br /&gt;
    expirationError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.expired(config.expirationLimit, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getDaysByDate = (date: Date) =&amp;gt; {&lt;br /&gt;
    const dayMs = 60 * 60 * 24 * 1000&lt;br /&gt;
    const timestamp = date.getTime()&lt;br /&gt;
    return timestamp/dayMs&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getExpirationOffset = (registrationDate: string) =&amp;gt; {&lt;br /&gt;
    const regDate = new Date(registrationDate)&lt;br /&gt;
    const today = new Date()&lt;br /&gt;
    return getDaysByDate(today) - getDaysByDate(regDate)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const date = journalRecordManager.getFieldValue(config.dateField)&lt;br /&gt;
&lt;br /&gt;
        if (!date) errorHandlers.dateError(config.dateField, journalRecordManager?.data?.stage?.stage?.name, config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        const expirationOffset = getExpirationOffset(date as string)&lt;br /&gt;
&lt;br /&gt;
        if (expirationOffset &amp;gt;= config.expirationLimit) errorHandlers.expirationError()&lt;br /&gt;
&lt;br /&gt;
        await showSuccessMessage(messages.success(config.journalRecordId), socketId)&lt;br /&gt;
&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordIndicatorResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
type IndicatorProp = { label: string, get: () =&amp;gt; unknown }&lt;br /&gt;
type IndicatorPropsConfig = Record&amp;lt;string, IndicatorProp&amp;gt;&lt;br /&gt;
&lt;br /&gt;
const indicatorPropsConfig = (indicator): IndicatorPropsConfig =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        executors: {&lt;br /&gt;
            label: &#039;Исполнители&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.executors&lt;br /&gt;
        },&lt;br /&gt;
        methodic: {&lt;br /&gt;
            label: &#039;Методики&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.methodics&lt;br /&gt;
        },&lt;br /&gt;
        averageResult: {&lt;br /&gt;
            label: &#039;Средний результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageValue&lt;br /&gt;
        },&lt;br /&gt;
        averageRoundedValue: {&lt;br /&gt;
            label: &#039;Средний округлённый результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageRoundedValue&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Место отбора&#039;,&lt;br /&gt;
        &#039;Дата регистрации&#039;,&lt;br /&gt;
        &#039;Шифр пробы&#039;,&lt;br /&gt;
        &#039;Время регистрации&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    indicatorsList: [&lt;br /&gt;
        &#039;Аммоний-ион&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144, // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyIndicators: (recordId) =&amp;gt; `в записи с Id ${recordId} отсутствуют показатели`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
    indicators: (indicators: string[], recordId) =&amp;gt; `Показатели ${indicators.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} отсутствуют в записи с ID ${recordId}`,&lt;br /&gt;
    emptyIndicatorsProps: (props: Record&amp;lt;string, string[]&amp;gt;, recordId) =&amp;gt; {&lt;br /&gt;
        const propsString = Object.keys(props).map(prop =&amp;gt; !isEmptyArray(props[prop]) ? `${prop}: ${props[prop].map(p =&amp;gt; `&amp;quot;${p}&amp;quot;`).join(&#039;, &#039;)}` : &#039;&#039;).join(&#039;; &#039;)&lt;br /&gt;
        return `В записи с Id ${recordId} отсутствуют либо не заполнены следующие поля у показателей: ${propsString}`&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    indicatorsError: (recordId) =&amp;gt;{&lt;br /&gt;
        throw new Error(messages.emptyIndicators(recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFoundIndicators: (indicators: string[], recordId: number) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.indicators(indicators, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFilledIndicatorProps: (props: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyIndicatorsProps(props, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(arr) &amp;amp;&amp;amp; arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasIndicators = (journalRecordManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
    return Array.isArray(indicators) &amp;amp;&amp;amp; indicators.length &amp;gt; 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFoundIndicators = (indicators: JournalRecordIndicatorResponse[], indicatorsList = config.indicatorsList) =&amp;gt; {&lt;br /&gt;
    return indicatorsList.filter(ind =&amp;gt; indicators.every(ljInd =&amp;gt; ljInd?.indicator?.name?.toLowerCase() !== ind.toLocaleLowerCase()))&lt;br /&gt;
} &lt;br /&gt;
&lt;br /&gt;
const getNotFilledResultsFields = (indicators: JournalRecordIndicatorResponse[]): Record&amp;lt;string, string[]&amp;gt; =&amp;gt; {&lt;br /&gt;
    return indicators.reduce((acc, indicator) =&amp;gt; {&lt;br /&gt;
        const config = indicatorPropsConfig(indicator)&lt;br /&gt;
        return {&lt;br /&gt;
            ...acc,&lt;br /&gt;
            [indicator?.indicator?.name]: Object.keys(config).reduce((acc, prop) =&amp;gt; {&lt;br /&gt;
                if (!config[prop].get()) return [...acc, config[prop].label]&lt;br /&gt;
                return acc&lt;br /&gt;
            }, [] as string[])&lt;br /&gt;
        }&lt;br /&gt;
    }, {}) as Record&amp;lt;string, string[]&amp;gt;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFilledProps = (indicatorsProps: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
    return Object.keys(indicatorsProps).some(indicator =&amp;gt; !isEmptyArray(indicatorsProps[indicator]))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFoundIndicators = (indicators: string[]) =&amp;gt; !isEmptyArray(indicators)&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (!hasIndicators(journalRecordManager)) {&lt;br /&gt;
            errorHandlers.indicatorsError(journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
&lt;br /&gt;
        const notFoundIndicators = getNotFoundIndicators(indicators)&lt;br /&gt;
        if (hasNotFoundIndicators(notFoundIndicators)) { &lt;br /&gt;
            errorHandlers.notFoundIndicators(notFoundIndicators, journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const emptyIndicatorProps = getNotFilledResultsFields(indicators)&lt;br /&gt;
        if (hasNotFilledProps(emptyIndicatorProps)) {&lt;br /&gt;
            errorHandlers.notFilledIndicatorProps(emptyIndicatorProps)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        showSuccessMessage(messages.success, socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-record-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Список. Целое число&#039;,&lt;br /&gt;
        &#039;Строка&#039;,&lt;br /&gt;
        &#039;Целое число&#039;,&lt;br /&gt;
        &#039;Вещественное число&#039;,&lt;br /&gt;
        &#039;Логический тип&#039;,&lt;br /&gt;
        &#039;Дата&#039;,&lt;br /&gt;
        &#039;Время&#039;,&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144,  // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFilledFields = (fields: string[], journalRecordsManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    console.log(journalRecordsManager.data.attributes, &#039;data.attributes&#039;)&lt;br /&gt;
    return fields.filter(f =&amp;gt; !journalRecordsManager.getFieldValue(f))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const emptyFields = getNotFilledFields(config.requiredFields, journalRecordManager)&lt;br /&gt;
&lt;br /&gt;
        if (isEmptyArray(emptyFields)) {&lt;br /&gt;
            showSuccessMessage(messages.success, socketId)&lt;br /&gt;
            return config.integrationResult&lt;br /&gt;
        } else errorHandlers.fieldsError(emptyFields, config.journalRecordId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-sample-moving.png|frame]]&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { JournalsRecordRouteController } from &amp;quot;../../src/servivces/worker/api/controllers/journals-record-route&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRouteDto } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    journalRecordId: 114 // Лаб. воздуха / Анализы завершены&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    route: (routes: JournalRecordRouteDto[], recordId: number) =&amp;gt; `Машрут пробы: ${routes.map(r =&amp;gt; `&amp;quot;${r.stageName}&amp;quot;`).join(&#039; -&amp;gt; &#039;)}. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
    notMoved: (recordId: number) =&amp;gt; `Движение пробы не происходило. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isSampleMoved = (moves: JournalRecordRouteDto[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(moves) &amp;amp;&amp;amp; moves.length &amp;gt; 1&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalsRecordRouteController = new JournalsRecordRouteController(apiInstance)&lt;br /&gt;
        const sampleMoves = await journalsRecordRouteController.getJournalRecordRoute(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (isSampleMoved(sampleMoves)) {&lt;br /&gt;
            showSuccessMessage(messages.route(sampleMoves, config.journalRecordId), socketId)&lt;br /&gt;
        } else {&lt;br /&gt;
            showSuccessMessage(messages.notMoved(config.journalRecordId), socketId)&lt;br /&gt;
        }&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-user-rights.png|frame]]&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import { &lt;br /&gt;
    PermissionController, &lt;br /&gt;
    PermissionResource, &lt;br /&gt;
    PermissionRwAccess&lt;br /&gt;
} from &amp;quot;../../src/servivces/worker/api/controllers/permisson&amp;quot;&lt;br /&gt;
&lt;br /&gt;
import { TableBuilder } from &amp;quot;../../src/gui/builder&amp;quot;&lt;br /&gt;
import type { DialogType, TableConfig } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRouteDto } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const accessNamesMap = {&lt;br /&gt;
    [PermissionRwAccess.EXEC]: &#039;Выполнение&#039;,&lt;br /&gt;
    [PermissionRwAccess.READ]: &#039;Чтение&#039;,&lt;br /&gt;
    [PermissionRwAccess.WRITE]: &#039;Запись&#039;,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const requiredResources = [&lt;br /&gt;
    PermissionResource.ANALYSIS_OBJECT, &lt;br /&gt;
    PermissionResource.METHODOLOGY,&lt;br /&gt;
    PermissionResource.JOURNAL_RECORD&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
const resourcesNamesMap = {&lt;br /&gt;
    [PermissionResource.ANALYSIS_OBJECT]: &#039;Объекты анализа&#039;, &lt;br /&gt;
    [PermissionResource.METHODOLOGY]: &#039;Методики&#039;, &lt;br /&gt;
    [PermissionResource.JOURNAL_RECORD]: &#039;Записи ЛЖ&#039;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getAccessRightsTable = async (tableBuilder: TableBuilder, permissionController: PermissionController): Promise&amp;lt;TableConfig&amp;gt; =&amp;gt; {&lt;br /&gt;
    const rows = (await Promise.all(requiredResources.map(async (source: PermissionResource) =&amp;gt; {&lt;br /&gt;
            const sourceCombinations = await Promise.all(Object.keys(PermissionRwAccess).map(async (action) =&amp;gt; {&lt;br /&gt;
                const access = await permissionController.getSourcePermission(source, PermissionRwAccess[action])&lt;br /&gt;
                return { source: resourcesNamesMap[source], action: accessNamesMap[PermissionRwAccess[action]], access: access.hasPermission }&lt;br /&gt;
            }))&lt;br /&gt;
            return sourceCombinations&lt;br /&gt;
        }   &lt;br /&gt;
    ))).flat()&lt;br /&gt;
    const table = tableBuilder&lt;br /&gt;
        .name(&#039;simpleTable&#039;)&lt;br /&gt;
        .label(&#039;Таблица&#039;)&lt;br /&gt;
        .isAddable(false)&lt;br /&gt;
        .actionsCol(false)&lt;br /&gt;
        .addColumn(&#039;Ресурс&#039;, &#039;source&#039;, false)&lt;br /&gt;
        .config(&#039;input&#039;, { name: &#039;source&#039;, rules: [{ required: true }] }, { placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .addColumn(&#039;Тип доступа&#039;, &#039;action&#039;, false)&lt;br /&gt;
        .config(&#039;input&#039;, { name: &#039;action&#039; }, { type: &#039;string&#039;, placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .addColumn(&#039;Доступ&#039;, &#039;access&#039;, false)&lt;br /&gt;
        .config(&#039;checkbox&#039;, { name: &#039;access&#039; }, { type: &#039;boolean&#039;, placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .setRows(rows)&lt;br /&gt;
        .build();&lt;br /&gt;
  return table&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    route: (routes: JournalRecordRouteDto[], recordId: number) =&amp;gt; `Маршрут пробы: ${routes.map(r =&amp;gt; `&amp;quot;${r.stageName}&amp;quot;`).join(&#039; -&amp;gt; &#039;)}. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
    notMoved: (recordId: number) =&amp;gt; `Движение пробы не происходило. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const showTable = async (builder: TableBuilder, controller: PermissionController, socketId: string) =&amp;gt; &lt;br /&gt;
    await getDataFromDialog({ type: &#039;input-data&#039;, config: [await getAccessRightsTable(builder, controller)] }, socketId)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
    try {&lt;br /&gt;
        await showTable(new TableBuilder, new PermissionController(apiInstance), socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Fill-record-fields.png|frame]]&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
import { JournalRecordAttributeFieldResponse } from &amp;quot;../../src/servivces/worker/api/controllers/lj-eav-attribute-fields&amp;quot;&lt;br /&gt;
import { FieldMatch } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const date = new Date().toISOString().split(&#039;T&#039;)[0]&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        { name: &#039;Дата регистрации&#039;, value: date, keywords: [&#039;дат&#039;, &#039;регистр&#039;] },&lt;br /&gt;
        { name: &#039;Место отбора&#039;, value: &#039;Скважина 516563Г &#039;, keywords: [&#039;мест&#039;, &#039;отбор&#039;] },&lt;br /&gt;
        { name: &#039;Дата отбора&#039;, value: date, keywords: [&#039;дат&#039;, &#039;отбор&#039;] },&lt;br /&gt;
        { name: &#039;Шифр пробы&#039;, value: &#039;shifr 123&#039;, keywords: [&#039;дат&#039;, &#039;регистр&#039;]},&lt;br /&gt;
        { name: &#039;Список. Целое число&#039;, value: 666, keywords: [&#039;спис&#039;, &#039;цел&#039;, &#039;числ&#039;] },&lt;br /&gt;
        { name: &#039;Помещение&#039;, value: &#039;помещение&#039;, keywords: [&#039;помещ&#039;]},&lt;br /&gt;
    ] as FieldMatch[],&lt;br /&gt;
    journalRecordId: 118, // Мк / 111&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: (recordId) =&amp;gt; `Все необходимые поля заполнены! Id записи: ${recordId}`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const fieldsMatcher = (field: JournalRecordAttributeFieldResponse, searchField: FieldMatch) =&amp;gt; {&lt;br /&gt;
    return searchField.keywords.every(k =&amp;gt; String(field.name).toLocaleLowerCase().includes(k.toLocaleLowerCase()))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const fillFields = async (fields: FieldMatch[], journalRecordsManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    await journalRecordsManager.setFieldsValues(fields, journalRecordsManager, fieldsMatcher)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        await fillFields(config.requiredFields, journalRecordManager)&lt;br /&gt;
        showSuccessMessage(messages.success(journalRecordManager.journalRecordId), socketId)&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Copy-journal-record.png|frame]]&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { JournalRecordsController } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRequest, JournalRecordResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { JournalRecordAttributeResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
type DataType = JournalRecordAttributeResponse[&#039;field&#039;][&#039;dataType&#039;]&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    journalRecordId: 208,  // Лаб. воздуха / Регистрация&lt;br /&gt;
    nextJournalRecordStage: 2, //  Лаб. воздуха / Анализ&lt;br /&gt;
    nextStageName: &#039;Анализ&#039;,&lt;br /&gt;
    nextRouteId: 50,&lt;br /&gt;
    integrationResult: { message: &#039;Запись ЛЖ скопирована успешно!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const getDateString = (ISODate: unknown | null = null) =&amp;gt; {&lt;br /&gt;
    return new Date(String(ISODate)).toISOString().split(&#039;T&#039;)[0]&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getTimeString = (ISODate: unknown | null = null) =&amp;gt; {&lt;br /&gt;
    return new Date(String(ISODate)).toISOString().split(&#039;T&#039;)[1].split(&#039;.&#039;)[0]&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const createRecord = async (journalRecordController: JournalRecordsController, baseRecord: JournalRecordResponse, newStageId) =&amp;gt; {&lt;br /&gt;
    const getAttrValue = (value: unknown, dataType: DataType) =&amp;gt; {&lt;br /&gt;
        if (dataType === &amp;quot;DATE&amp;quot;) return getDateString(value)&lt;br /&gt;
        if (dataType === &amp;quot;TIME&amp;quot;) return getTimeString(value)&lt;br /&gt;
        return value&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    const getBindings = (baseRecord: JournalRecordResponse): number[] | null =&amp;gt; {&lt;br /&gt;
        return baseRecord?.companyLevelBindings?.map(b =&amp;gt; b?.id) || null&lt;br /&gt;
    } &lt;br /&gt;
&lt;br /&gt;
    const getRecordRequest = (baseRecord: JournalRecordResponse, newStageId: number): JournalRecordRequest =&amp;gt; {&lt;br /&gt;
        return {&lt;br /&gt;
            routeId: baseRecord?.route?.id,&lt;br /&gt;
            stageId: newStageId,&lt;br /&gt;
            analysisObjectId: baseRecord?.analysisObject?.id,&lt;br /&gt;
            sampleTypeId: baseRecord.sampleType.id,&lt;br /&gt;
            attributes: baseRecord.attributes.map(attr =&amp;gt; {&lt;br /&gt;
                return {&lt;br /&gt;
                    fieldId: attr.field.id,&lt;br /&gt;
                    stageId: newStageId,&lt;br /&gt;
                    data: {&lt;br /&gt;
                        ...attr.data, &lt;br /&gt;
                        value: getAttrValue(attr?.data?.value, attr?.field?.dataType),&lt;br /&gt;
                        id: undefined&lt;br /&gt;
                    },&lt;br /&gt;
                    statusId: null&lt;br /&gt;
                }&lt;br /&gt;
            }),&lt;br /&gt;
            companyLevelBindings: getBindings(baseRecord)&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    return await journalRecordController.createRecord(getRecordRequest(baseRecord, newStageId))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const journalRecordController = new JournalRecordsController(apiInstance)&lt;br /&gt;
        const newRecord = await createRecord(journalRecordController, journalRecordManager.data, config.nextJournalRecordStage)&lt;br /&gt;
        showSuccessMessage(`${messages.success}. Id изначальной записи: ${config.journalRecordId}. Id новой записи ЛЖ: ${newRecord.id}. Этап: ${config.nextStageName}`, socketId)&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2371</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2371"/>
		<updated>2026-04-02T01:38:40Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Заполнение атрибутов записи ЛЖ. fill-record-fields */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
   &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { MessageBoxResults } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    dateField: &#039;Дата регистрации&#039;,&lt;br /&gt;
    expirationLimit: 10,&lt;br /&gt;
    journalRecordId: 201, // Лаб. воздуха / Анализы завершены&lt;br /&gt;
    integrationResult: { message: &#039;Срок регистрации пробы не истёк !&#039;}&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    success: (recordId) =&amp;gt; `Срок регистрации пробы не истёк ! Id записи ${recordId}`,&lt;br /&gt;
    expired: (limit, recordId) =&amp;gt; `Срок регистрации пробы истёк!\nПрошло более ${ limit } дней с момента регистрации. Id записи: ${recordId}`,&lt;br /&gt;
    dateField: (dateField, stageName, journalRecordId) =&amp;gt; [&lt;br /&gt;
        `Не заполнено либо отсутствует поле &amp;quot;${dateField}&amp;quot;`,&lt;br /&gt;
        `в этапе &amp;quot;${stageName || &#039;&#039;}&amp;quot;.\nId записи: ${journalRecordId}`&lt;br /&gt;
    ].join(&#039; &#039;)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    dateError: (dateField, stageName, journalRecordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.dateField(dateField, stageName, journalRecordId))&lt;br /&gt;
    },&lt;br /&gt;
    expirationError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.expired(config.expirationLimit, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getDaysByDate = (date: Date) =&amp;gt; {&lt;br /&gt;
    const dayMs = 60 * 60 * 24 * 1000&lt;br /&gt;
    const timestamp = date.getTime()&lt;br /&gt;
    return timestamp/dayMs&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getExpirationOffset = (registrationDate: string) =&amp;gt; {&lt;br /&gt;
    const regDate = new Date(registrationDate)&lt;br /&gt;
    const today = new Date()&lt;br /&gt;
    return getDaysByDate(today) - getDaysByDate(regDate)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const date = journalRecordManager.getFieldValue(config.dateField)&lt;br /&gt;
&lt;br /&gt;
        if (!date) errorHandlers.dateError(config.dateField, journalRecordManager?.data?.stage?.stage?.name, config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        const expirationOffset = getExpirationOffset(date as string)&lt;br /&gt;
&lt;br /&gt;
        if (expirationOffset &amp;gt;= config.expirationLimit) errorHandlers.expirationError()&lt;br /&gt;
&lt;br /&gt;
        await showSuccessMessage(messages.success(config.journalRecordId), socketId)&lt;br /&gt;
&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordIndicatorResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
type IndicatorProp = { label: string, get: () =&amp;gt; unknown }&lt;br /&gt;
type IndicatorPropsConfig = Record&amp;lt;string, IndicatorProp&amp;gt;&lt;br /&gt;
&lt;br /&gt;
const indicatorPropsConfig = (indicator): IndicatorPropsConfig =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        executors: {&lt;br /&gt;
            label: &#039;Исполнители&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.executors&lt;br /&gt;
        },&lt;br /&gt;
        methodic: {&lt;br /&gt;
            label: &#039;Методики&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.methodics&lt;br /&gt;
        },&lt;br /&gt;
        averageResult: {&lt;br /&gt;
            label: &#039;Средний результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageValue&lt;br /&gt;
        },&lt;br /&gt;
        averageRoundedValue: {&lt;br /&gt;
            label: &#039;Средний округлённый результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageRoundedValue&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Место отбора&#039;,&lt;br /&gt;
        &#039;Дата регистрации&#039;,&lt;br /&gt;
        &#039;Шифр пробы&#039;,&lt;br /&gt;
        &#039;Время регистрации&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    indicatorsList: [&lt;br /&gt;
        &#039;Аммоний-ион&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144, // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyIndicators: (recordId) =&amp;gt; `в записи с Id ${recordId} отсутствуют показатели`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
    indicators: (indicators: string[], recordId) =&amp;gt; `Показатели ${indicators.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} отсутствуют в записи с ID ${recordId}`,&lt;br /&gt;
    emptyIndicatorsProps: (props: Record&amp;lt;string, string[]&amp;gt;, recordId) =&amp;gt; {&lt;br /&gt;
        const propsString = Object.keys(props).map(prop =&amp;gt; !isEmptyArray(props[prop]) ? `${prop}: ${props[prop].map(p =&amp;gt; `&amp;quot;${p}&amp;quot;`).join(&#039;, &#039;)}` : &#039;&#039;).join(&#039;; &#039;)&lt;br /&gt;
        return `В записи с Id ${recordId} отсутствуют либо не заполнены следующие поля у показателей: ${propsString}`&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    indicatorsError: (recordId) =&amp;gt;{&lt;br /&gt;
        throw new Error(messages.emptyIndicators(recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFoundIndicators: (indicators: string[], recordId: number) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.indicators(indicators, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFilledIndicatorProps: (props: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyIndicatorsProps(props, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(arr) &amp;amp;&amp;amp; arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasIndicators = (journalRecordManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
    return Array.isArray(indicators) &amp;amp;&amp;amp; indicators.length &amp;gt; 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFoundIndicators = (indicators: JournalRecordIndicatorResponse[], indicatorsList = config.indicatorsList) =&amp;gt; {&lt;br /&gt;
    return indicatorsList.filter(ind =&amp;gt; indicators.every(ljInd =&amp;gt; ljInd?.indicator?.name?.toLowerCase() !== ind.toLocaleLowerCase()))&lt;br /&gt;
} &lt;br /&gt;
&lt;br /&gt;
const getNotFilledResultsFields = (indicators: JournalRecordIndicatorResponse[]): Record&amp;lt;string, string[]&amp;gt; =&amp;gt; {&lt;br /&gt;
    return indicators.reduce((acc, indicator) =&amp;gt; {&lt;br /&gt;
        const config = indicatorPropsConfig(indicator)&lt;br /&gt;
        return {&lt;br /&gt;
            ...acc,&lt;br /&gt;
            [indicator?.indicator?.name]: Object.keys(config).reduce((acc, prop) =&amp;gt; {&lt;br /&gt;
                if (!config[prop].get()) return [...acc, config[prop].label]&lt;br /&gt;
                return acc&lt;br /&gt;
            }, [] as string[])&lt;br /&gt;
        }&lt;br /&gt;
    }, {}) as Record&amp;lt;string, string[]&amp;gt;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFilledProps = (indicatorsProps: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
    return Object.keys(indicatorsProps).some(indicator =&amp;gt; !isEmptyArray(indicatorsProps[indicator]))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFoundIndicators = (indicators: string[]) =&amp;gt; !isEmptyArray(indicators)&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (!hasIndicators(journalRecordManager)) {&lt;br /&gt;
            errorHandlers.indicatorsError(journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
&lt;br /&gt;
        const notFoundIndicators = getNotFoundIndicators(indicators)&lt;br /&gt;
        if (hasNotFoundIndicators(notFoundIndicators)) { &lt;br /&gt;
            errorHandlers.notFoundIndicators(notFoundIndicators, journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const emptyIndicatorProps = getNotFilledResultsFields(indicators)&lt;br /&gt;
        if (hasNotFilledProps(emptyIndicatorProps)) {&lt;br /&gt;
            errorHandlers.notFilledIndicatorProps(emptyIndicatorProps)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        showSuccessMessage(messages.success, socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-record-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Список. Целое число&#039;,&lt;br /&gt;
        &#039;Строка&#039;,&lt;br /&gt;
        &#039;Целое число&#039;,&lt;br /&gt;
        &#039;Вещественное число&#039;,&lt;br /&gt;
        &#039;Логический тип&#039;,&lt;br /&gt;
        &#039;Дата&#039;,&lt;br /&gt;
        &#039;Время&#039;,&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144,  // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFilledFields = (fields: string[], journalRecordsManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    console.log(journalRecordsManager.data.attributes, &#039;data.attributes&#039;)&lt;br /&gt;
    return fields.filter(f =&amp;gt; !journalRecordsManager.getFieldValue(f))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const emptyFields = getNotFilledFields(config.requiredFields, journalRecordManager)&lt;br /&gt;
&lt;br /&gt;
        if (isEmptyArray(emptyFields)) {&lt;br /&gt;
            showSuccessMessage(messages.success, socketId)&lt;br /&gt;
            return config.integrationResult&lt;br /&gt;
        } else errorHandlers.fieldsError(emptyFields, config.journalRecordId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-sample-moving.png|frame]]&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { JournalsRecordRouteController } from &amp;quot;../../src/servivces/worker/api/controllers/journals-record-route&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRouteDto } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    journalRecordId: 114 // Лаб. воздуха / Анализы завершены&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    route: (routes: JournalRecordRouteDto[], recordId: number) =&amp;gt; `Машрут пробы: ${routes.map(r =&amp;gt; `&amp;quot;${r.stageName}&amp;quot;`).join(&#039; -&amp;gt; &#039;)}. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
    notMoved: (recordId: number) =&amp;gt; `Движение пробы не происходило. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isSampleMoved = (moves: JournalRecordRouteDto[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(moves) &amp;amp;&amp;amp; moves.length &amp;gt; 1&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalsRecordRouteController = new JournalsRecordRouteController(apiInstance)&lt;br /&gt;
        const sampleMoves = await journalsRecordRouteController.getJournalRecordRoute(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (isSampleMoved(sampleMoves)) {&lt;br /&gt;
            showSuccessMessage(messages.route(sampleMoves, config.journalRecordId), socketId)&lt;br /&gt;
        } else {&lt;br /&gt;
            showSuccessMessage(messages.notMoved(config.journalRecordId), socketId)&lt;br /&gt;
        }&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-user-rights.png|frame]]&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import { &lt;br /&gt;
    PermissionController, &lt;br /&gt;
    PermissionResource, &lt;br /&gt;
    PermissionRwAccess&lt;br /&gt;
} from &amp;quot;../../src/servivces/worker/api/controllers/permisson&amp;quot;&lt;br /&gt;
&lt;br /&gt;
import { TableBuilder } from &amp;quot;../../src/gui/builder&amp;quot;&lt;br /&gt;
import type { DialogType, TableConfig } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRouteDto } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const accessNamesMap = {&lt;br /&gt;
    [PermissionRwAccess.EXEC]: &#039;Выполнение&#039;,&lt;br /&gt;
    [PermissionRwAccess.READ]: &#039;Чтение&#039;,&lt;br /&gt;
    [PermissionRwAccess.WRITE]: &#039;Запись&#039;,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const requiredResources = [&lt;br /&gt;
    PermissionResource.ANALYSIS_OBJECT, &lt;br /&gt;
    PermissionResource.METHODOLOGY,&lt;br /&gt;
    PermissionResource.JOURNAL_RECORD&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
const resourcesNamesMap = {&lt;br /&gt;
    [PermissionResource.ANALYSIS_OBJECT]: &#039;Объекты анализа&#039;, &lt;br /&gt;
    [PermissionResource.METHODOLOGY]: &#039;Методики&#039;, &lt;br /&gt;
    [PermissionResource.JOURNAL_RECORD]: &#039;Записи ЛЖ&#039;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getAccessRightsTable = async (tableBuilder: TableBuilder, permissionController: PermissionController): Promise&amp;lt;TableConfig&amp;gt; =&amp;gt; {&lt;br /&gt;
    const rows = (await Promise.all(requiredResources.map(async (source: PermissionResource) =&amp;gt; {&lt;br /&gt;
            const sourceCombinations = await Promise.all(Object.keys(PermissionRwAccess).map(async (action) =&amp;gt; {&lt;br /&gt;
                const access = await permissionController.getSourcePermission(source, PermissionRwAccess[action])&lt;br /&gt;
                return { source: resourcesNamesMap[source], action: accessNamesMap[PermissionRwAccess[action]], access: access.hasPermission }&lt;br /&gt;
            }))&lt;br /&gt;
            return sourceCombinations&lt;br /&gt;
        }   &lt;br /&gt;
    ))).flat()&lt;br /&gt;
    const table = tableBuilder&lt;br /&gt;
        .name(&#039;simpleTable&#039;)&lt;br /&gt;
        .label(&#039;Таблица&#039;)&lt;br /&gt;
        .isAddable(false)&lt;br /&gt;
        .actionsCol(false)&lt;br /&gt;
        .addColumn(&#039;Ресурс&#039;, &#039;source&#039;, false)&lt;br /&gt;
        .config(&#039;input&#039;, { name: &#039;source&#039;, rules: [{ required: true }] }, { placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .addColumn(&#039;Тип доступа&#039;, &#039;action&#039;, false)&lt;br /&gt;
        .config(&#039;input&#039;, { name: &#039;action&#039; }, { type: &#039;string&#039;, placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .addColumn(&#039;Доступ&#039;, &#039;access&#039;, false)&lt;br /&gt;
        .config(&#039;checkbox&#039;, { name: &#039;access&#039; }, { type: &#039;boolean&#039;, placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .setRows(rows)&lt;br /&gt;
        .build();&lt;br /&gt;
  return table&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    route: (routes: JournalRecordRouteDto[], recordId: number) =&amp;gt; `Маршрут пробы: ${routes.map(r =&amp;gt; `&amp;quot;${r.stageName}&amp;quot;`).join(&#039; -&amp;gt; &#039;)}. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
    notMoved: (recordId: number) =&amp;gt; `Движение пробы не происходило. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const showTable = async (builder: TableBuilder, controller: PermissionController, socketId: string) =&amp;gt; &lt;br /&gt;
    await getDataFromDialog({ type: &#039;input-data&#039;, config: [await getAccessRightsTable(builder, controller)] }, socketId)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
    try {&lt;br /&gt;
        await showTable(new TableBuilder, new PermissionController(apiInstance), socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Fill-record-fields.png|frame]]&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
import { JournalRecordAttributeFieldResponse } from &amp;quot;../../src/servivces/worker/api/controllers/lj-eav-attribute-fields&amp;quot;&lt;br /&gt;
import { FieldMatch } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const date = new Date().toISOString().split(&#039;T&#039;)[0]&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        { name: &#039;Дата регистрации&#039;, value: date, keywords: [&#039;дат&#039;, &#039;регистр&#039;] },&lt;br /&gt;
        { name: &#039;Место отбора&#039;, value: &#039;Скважина 516563Г &#039;, keywords: [&#039;мест&#039;, &#039;отбор&#039;] },&lt;br /&gt;
        { name: &#039;Дата отбора&#039;, value: date, keywords: [&#039;дат&#039;, &#039;отбор&#039;] },&lt;br /&gt;
        { name: &#039;Шифр пробы&#039;, value: &#039;shifr 123&#039;, keywords: [&#039;дат&#039;, &#039;регистр&#039;]},&lt;br /&gt;
        { name: &#039;Список. Целое число&#039;, value: 666, keywords: [&#039;спис&#039;, &#039;цел&#039;, &#039;числ&#039;] },&lt;br /&gt;
        { name: &#039;Помещение&#039;, value: &#039;помещение&#039;, keywords: [&#039;помещ&#039;]},&lt;br /&gt;
    ] as FieldMatch[],&lt;br /&gt;
    journalRecordId: 118, // Мк / 111&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: (recordId) =&amp;gt; `Все необходимые поля заполнены! Id записи: ${recordId}`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const fieldsMatcher = (field: JournalRecordAttributeFieldResponse, searchField: FieldMatch) =&amp;gt; {&lt;br /&gt;
    return searchField.keywords.every(k =&amp;gt; String(field.name).toLocaleLowerCase().includes(k.toLocaleLowerCase()))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const fillFields = async (fields: FieldMatch[], journalRecordsManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    await journalRecordsManager.setFieldsValues(fields, journalRecordsManager, fieldsMatcher)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        await fillFields(config.requiredFields, journalRecordManager)&lt;br /&gt;
        showSuccessMessage(messages.success(journalRecordManager.journalRecordId), socketId)&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Copy-journal-record.png|frame]]&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2370</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2370"/>
		<updated>2026-04-02T01:38:03Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Проверка прав пользователя. check-user-rights */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
   &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { MessageBoxResults } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    dateField: &#039;Дата регистрации&#039;,&lt;br /&gt;
    expirationLimit: 10,&lt;br /&gt;
    journalRecordId: 201, // Лаб. воздуха / Анализы завершены&lt;br /&gt;
    integrationResult: { message: &#039;Срок регистрации пробы не истёк !&#039;}&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    success: (recordId) =&amp;gt; `Срок регистрации пробы не истёк ! Id записи ${recordId}`,&lt;br /&gt;
    expired: (limit, recordId) =&amp;gt; `Срок регистрации пробы истёк!\nПрошло более ${ limit } дней с момента регистрации. Id записи: ${recordId}`,&lt;br /&gt;
    dateField: (dateField, stageName, journalRecordId) =&amp;gt; [&lt;br /&gt;
        `Не заполнено либо отсутствует поле &amp;quot;${dateField}&amp;quot;`,&lt;br /&gt;
        `в этапе &amp;quot;${stageName || &#039;&#039;}&amp;quot;.\nId записи: ${journalRecordId}`&lt;br /&gt;
    ].join(&#039; &#039;)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    dateError: (dateField, stageName, journalRecordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.dateField(dateField, stageName, journalRecordId))&lt;br /&gt;
    },&lt;br /&gt;
    expirationError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.expired(config.expirationLimit, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getDaysByDate = (date: Date) =&amp;gt; {&lt;br /&gt;
    const dayMs = 60 * 60 * 24 * 1000&lt;br /&gt;
    const timestamp = date.getTime()&lt;br /&gt;
    return timestamp/dayMs&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getExpirationOffset = (registrationDate: string) =&amp;gt; {&lt;br /&gt;
    const regDate = new Date(registrationDate)&lt;br /&gt;
    const today = new Date()&lt;br /&gt;
    return getDaysByDate(today) - getDaysByDate(regDate)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const date = journalRecordManager.getFieldValue(config.dateField)&lt;br /&gt;
&lt;br /&gt;
        if (!date) errorHandlers.dateError(config.dateField, journalRecordManager?.data?.stage?.stage?.name, config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        const expirationOffset = getExpirationOffset(date as string)&lt;br /&gt;
&lt;br /&gt;
        if (expirationOffset &amp;gt;= config.expirationLimit) errorHandlers.expirationError()&lt;br /&gt;
&lt;br /&gt;
        await showSuccessMessage(messages.success(config.journalRecordId), socketId)&lt;br /&gt;
&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordIndicatorResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
type IndicatorProp = { label: string, get: () =&amp;gt; unknown }&lt;br /&gt;
type IndicatorPropsConfig = Record&amp;lt;string, IndicatorProp&amp;gt;&lt;br /&gt;
&lt;br /&gt;
const indicatorPropsConfig = (indicator): IndicatorPropsConfig =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        executors: {&lt;br /&gt;
            label: &#039;Исполнители&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.executors&lt;br /&gt;
        },&lt;br /&gt;
        methodic: {&lt;br /&gt;
            label: &#039;Методики&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.methodics&lt;br /&gt;
        },&lt;br /&gt;
        averageResult: {&lt;br /&gt;
            label: &#039;Средний результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageValue&lt;br /&gt;
        },&lt;br /&gt;
        averageRoundedValue: {&lt;br /&gt;
            label: &#039;Средний округлённый результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageRoundedValue&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Место отбора&#039;,&lt;br /&gt;
        &#039;Дата регистрации&#039;,&lt;br /&gt;
        &#039;Шифр пробы&#039;,&lt;br /&gt;
        &#039;Время регистрации&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    indicatorsList: [&lt;br /&gt;
        &#039;Аммоний-ион&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144, // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyIndicators: (recordId) =&amp;gt; `в записи с Id ${recordId} отсутствуют показатели`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
    indicators: (indicators: string[], recordId) =&amp;gt; `Показатели ${indicators.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} отсутствуют в записи с ID ${recordId}`,&lt;br /&gt;
    emptyIndicatorsProps: (props: Record&amp;lt;string, string[]&amp;gt;, recordId) =&amp;gt; {&lt;br /&gt;
        const propsString = Object.keys(props).map(prop =&amp;gt; !isEmptyArray(props[prop]) ? `${prop}: ${props[prop].map(p =&amp;gt; `&amp;quot;${p}&amp;quot;`).join(&#039;, &#039;)}` : &#039;&#039;).join(&#039;; &#039;)&lt;br /&gt;
        return `В записи с Id ${recordId} отсутствуют либо не заполнены следующие поля у показателей: ${propsString}`&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    indicatorsError: (recordId) =&amp;gt;{&lt;br /&gt;
        throw new Error(messages.emptyIndicators(recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFoundIndicators: (indicators: string[], recordId: number) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.indicators(indicators, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFilledIndicatorProps: (props: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyIndicatorsProps(props, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(arr) &amp;amp;&amp;amp; arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasIndicators = (journalRecordManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
    return Array.isArray(indicators) &amp;amp;&amp;amp; indicators.length &amp;gt; 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFoundIndicators = (indicators: JournalRecordIndicatorResponse[], indicatorsList = config.indicatorsList) =&amp;gt; {&lt;br /&gt;
    return indicatorsList.filter(ind =&amp;gt; indicators.every(ljInd =&amp;gt; ljInd?.indicator?.name?.toLowerCase() !== ind.toLocaleLowerCase()))&lt;br /&gt;
} &lt;br /&gt;
&lt;br /&gt;
const getNotFilledResultsFields = (indicators: JournalRecordIndicatorResponse[]): Record&amp;lt;string, string[]&amp;gt; =&amp;gt; {&lt;br /&gt;
    return indicators.reduce((acc, indicator) =&amp;gt; {&lt;br /&gt;
        const config = indicatorPropsConfig(indicator)&lt;br /&gt;
        return {&lt;br /&gt;
            ...acc,&lt;br /&gt;
            [indicator?.indicator?.name]: Object.keys(config).reduce((acc, prop) =&amp;gt; {&lt;br /&gt;
                if (!config[prop].get()) return [...acc, config[prop].label]&lt;br /&gt;
                return acc&lt;br /&gt;
            }, [] as string[])&lt;br /&gt;
        }&lt;br /&gt;
    }, {}) as Record&amp;lt;string, string[]&amp;gt;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFilledProps = (indicatorsProps: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
    return Object.keys(indicatorsProps).some(indicator =&amp;gt; !isEmptyArray(indicatorsProps[indicator]))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFoundIndicators = (indicators: string[]) =&amp;gt; !isEmptyArray(indicators)&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (!hasIndicators(journalRecordManager)) {&lt;br /&gt;
            errorHandlers.indicatorsError(journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
&lt;br /&gt;
        const notFoundIndicators = getNotFoundIndicators(indicators)&lt;br /&gt;
        if (hasNotFoundIndicators(notFoundIndicators)) { &lt;br /&gt;
            errorHandlers.notFoundIndicators(notFoundIndicators, journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const emptyIndicatorProps = getNotFilledResultsFields(indicators)&lt;br /&gt;
        if (hasNotFilledProps(emptyIndicatorProps)) {&lt;br /&gt;
            errorHandlers.notFilledIndicatorProps(emptyIndicatorProps)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        showSuccessMessage(messages.success, socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-record-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Список. Целое число&#039;,&lt;br /&gt;
        &#039;Строка&#039;,&lt;br /&gt;
        &#039;Целое число&#039;,&lt;br /&gt;
        &#039;Вещественное число&#039;,&lt;br /&gt;
        &#039;Логический тип&#039;,&lt;br /&gt;
        &#039;Дата&#039;,&lt;br /&gt;
        &#039;Время&#039;,&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144,  // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFilledFields = (fields: string[], journalRecordsManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    console.log(journalRecordsManager.data.attributes, &#039;data.attributes&#039;)&lt;br /&gt;
    return fields.filter(f =&amp;gt; !journalRecordsManager.getFieldValue(f))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const emptyFields = getNotFilledFields(config.requiredFields, journalRecordManager)&lt;br /&gt;
&lt;br /&gt;
        if (isEmptyArray(emptyFields)) {&lt;br /&gt;
            showSuccessMessage(messages.success, socketId)&lt;br /&gt;
            return config.integrationResult&lt;br /&gt;
        } else errorHandlers.fieldsError(emptyFields, config.journalRecordId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-sample-moving.png|frame]]&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { JournalsRecordRouteController } from &amp;quot;../../src/servivces/worker/api/controllers/journals-record-route&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRouteDto } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    journalRecordId: 114 // Лаб. воздуха / Анализы завершены&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    route: (routes: JournalRecordRouteDto[], recordId: number) =&amp;gt; `Машрут пробы: ${routes.map(r =&amp;gt; `&amp;quot;${r.stageName}&amp;quot;`).join(&#039; -&amp;gt; &#039;)}. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
    notMoved: (recordId: number) =&amp;gt; `Движение пробы не происходило. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isSampleMoved = (moves: JournalRecordRouteDto[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(moves) &amp;amp;&amp;amp; moves.length &amp;gt; 1&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalsRecordRouteController = new JournalsRecordRouteController(apiInstance)&lt;br /&gt;
        const sampleMoves = await journalsRecordRouteController.getJournalRecordRoute(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (isSampleMoved(sampleMoves)) {&lt;br /&gt;
            showSuccessMessage(messages.route(sampleMoves, config.journalRecordId), socketId)&lt;br /&gt;
        } else {&lt;br /&gt;
            showSuccessMessage(messages.notMoved(config.journalRecordId), socketId)&lt;br /&gt;
        }&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-user-rights.png|frame]]&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import { &lt;br /&gt;
    PermissionController, &lt;br /&gt;
    PermissionResource, &lt;br /&gt;
    PermissionRwAccess&lt;br /&gt;
} from &amp;quot;../../src/servivces/worker/api/controllers/permisson&amp;quot;&lt;br /&gt;
&lt;br /&gt;
import { TableBuilder } from &amp;quot;../../src/gui/builder&amp;quot;&lt;br /&gt;
import type { DialogType, TableConfig } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRouteDto } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const accessNamesMap = {&lt;br /&gt;
    [PermissionRwAccess.EXEC]: &#039;Выполнение&#039;,&lt;br /&gt;
    [PermissionRwAccess.READ]: &#039;Чтение&#039;,&lt;br /&gt;
    [PermissionRwAccess.WRITE]: &#039;Запись&#039;,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const requiredResources = [&lt;br /&gt;
    PermissionResource.ANALYSIS_OBJECT, &lt;br /&gt;
    PermissionResource.METHODOLOGY,&lt;br /&gt;
    PermissionResource.JOURNAL_RECORD&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
const resourcesNamesMap = {&lt;br /&gt;
    [PermissionResource.ANALYSIS_OBJECT]: &#039;Объекты анализа&#039;, &lt;br /&gt;
    [PermissionResource.METHODOLOGY]: &#039;Методики&#039;, &lt;br /&gt;
    [PermissionResource.JOURNAL_RECORD]: &#039;Записи ЛЖ&#039;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getAccessRightsTable = async (tableBuilder: TableBuilder, permissionController: PermissionController): Promise&amp;lt;TableConfig&amp;gt; =&amp;gt; {&lt;br /&gt;
    const rows = (await Promise.all(requiredResources.map(async (source: PermissionResource) =&amp;gt; {&lt;br /&gt;
            const sourceCombinations = await Promise.all(Object.keys(PermissionRwAccess).map(async (action) =&amp;gt; {&lt;br /&gt;
                const access = await permissionController.getSourcePermission(source, PermissionRwAccess[action])&lt;br /&gt;
                return { source: resourcesNamesMap[source], action: accessNamesMap[PermissionRwAccess[action]], access: access.hasPermission }&lt;br /&gt;
            }))&lt;br /&gt;
            return sourceCombinations&lt;br /&gt;
        }   &lt;br /&gt;
    ))).flat()&lt;br /&gt;
    const table = tableBuilder&lt;br /&gt;
        .name(&#039;simpleTable&#039;)&lt;br /&gt;
        .label(&#039;Таблица&#039;)&lt;br /&gt;
        .isAddable(false)&lt;br /&gt;
        .actionsCol(false)&lt;br /&gt;
        .addColumn(&#039;Ресурс&#039;, &#039;source&#039;, false)&lt;br /&gt;
        .config(&#039;input&#039;, { name: &#039;source&#039;, rules: [{ required: true }] }, { placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .addColumn(&#039;Тип доступа&#039;, &#039;action&#039;, false)&lt;br /&gt;
        .config(&#039;input&#039;, { name: &#039;action&#039; }, { type: &#039;string&#039;, placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .addColumn(&#039;Доступ&#039;, &#039;access&#039;, false)&lt;br /&gt;
        .config(&#039;checkbox&#039;, { name: &#039;access&#039; }, { type: &#039;boolean&#039;, placeholder: &#039;&#039;, disabled: false })&lt;br /&gt;
        .next()&lt;br /&gt;
        .setRows(rows)&lt;br /&gt;
        .build();&lt;br /&gt;
  return table&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    route: (routes: JournalRecordRouteDto[], recordId: number) =&amp;gt; `Маршрут пробы: ${routes.map(r =&amp;gt; `&amp;quot;${r.stageName}&amp;quot;`).join(&#039; -&amp;gt; &#039;)}. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
    notMoved: (recordId: number) =&amp;gt; `Движение пробы не происходило. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const showTable = async (builder: TableBuilder, controller: PermissionController, socketId: string) =&amp;gt; &lt;br /&gt;
    await getDataFromDialog({ type: &#039;input-data&#039;, config: [await getAccessRightsTable(builder, controller)] }, socketId)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
    try {&lt;br /&gt;
        await showTable(new TableBuilder, new PermissionController(apiInstance), socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Fill-record-fields.png|frame]]&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Copy-journal-record.png|frame]]&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2369</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2369"/>
		<updated>2026-04-02T01:36:44Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Проверка движения пробы. check-sample-moving */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
   &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { MessageBoxResults } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    dateField: &#039;Дата регистрации&#039;,&lt;br /&gt;
    expirationLimit: 10,&lt;br /&gt;
    journalRecordId: 201, // Лаб. воздуха / Анализы завершены&lt;br /&gt;
    integrationResult: { message: &#039;Срок регистрации пробы не истёк !&#039;}&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    success: (recordId) =&amp;gt; `Срок регистрации пробы не истёк ! Id записи ${recordId}`,&lt;br /&gt;
    expired: (limit, recordId) =&amp;gt; `Срок регистрации пробы истёк!\nПрошло более ${ limit } дней с момента регистрации. Id записи: ${recordId}`,&lt;br /&gt;
    dateField: (dateField, stageName, journalRecordId) =&amp;gt; [&lt;br /&gt;
        `Не заполнено либо отсутствует поле &amp;quot;${dateField}&amp;quot;`,&lt;br /&gt;
        `в этапе &amp;quot;${stageName || &#039;&#039;}&amp;quot;.\nId записи: ${journalRecordId}`&lt;br /&gt;
    ].join(&#039; &#039;)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    dateError: (dateField, stageName, journalRecordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.dateField(dateField, stageName, journalRecordId))&lt;br /&gt;
    },&lt;br /&gt;
    expirationError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.expired(config.expirationLimit, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getDaysByDate = (date: Date) =&amp;gt; {&lt;br /&gt;
    const dayMs = 60 * 60 * 24 * 1000&lt;br /&gt;
    const timestamp = date.getTime()&lt;br /&gt;
    return timestamp/dayMs&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getExpirationOffset = (registrationDate: string) =&amp;gt; {&lt;br /&gt;
    const regDate = new Date(registrationDate)&lt;br /&gt;
    const today = new Date()&lt;br /&gt;
    return getDaysByDate(today) - getDaysByDate(regDate)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const date = journalRecordManager.getFieldValue(config.dateField)&lt;br /&gt;
&lt;br /&gt;
        if (!date) errorHandlers.dateError(config.dateField, journalRecordManager?.data?.stage?.stage?.name, config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        const expirationOffset = getExpirationOffset(date as string)&lt;br /&gt;
&lt;br /&gt;
        if (expirationOffset &amp;gt;= config.expirationLimit) errorHandlers.expirationError()&lt;br /&gt;
&lt;br /&gt;
        await showSuccessMessage(messages.success(config.journalRecordId), socketId)&lt;br /&gt;
&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordIndicatorResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
type IndicatorProp = { label: string, get: () =&amp;gt; unknown }&lt;br /&gt;
type IndicatorPropsConfig = Record&amp;lt;string, IndicatorProp&amp;gt;&lt;br /&gt;
&lt;br /&gt;
const indicatorPropsConfig = (indicator): IndicatorPropsConfig =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        executors: {&lt;br /&gt;
            label: &#039;Исполнители&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.executors&lt;br /&gt;
        },&lt;br /&gt;
        methodic: {&lt;br /&gt;
            label: &#039;Методики&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.methodics&lt;br /&gt;
        },&lt;br /&gt;
        averageResult: {&lt;br /&gt;
            label: &#039;Средний результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageValue&lt;br /&gt;
        },&lt;br /&gt;
        averageRoundedValue: {&lt;br /&gt;
            label: &#039;Средний округлённый результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageRoundedValue&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Место отбора&#039;,&lt;br /&gt;
        &#039;Дата регистрации&#039;,&lt;br /&gt;
        &#039;Шифр пробы&#039;,&lt;br /&gt;
        &#039;Время регистрации&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    indicatorsList: [&lt;br /&gt;
        &#039;Аммоний-ион&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144, // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyIndicators: (recordId) =&amp;gt; `в записи с Id ${recordId} отсутствуют показатели`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
    indicators: (indicators: string[], recordId) =&amp;gt; `Показатели ${indicators.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} отсутствуют в записи с ID ${recordId}`,&lt;br /&gt;
    emptyIndicatorsProps: (props: Record&amp;lt;string, string[]&amp;gt;, recordId) =&amp;gt; {&lt;br /&gt;
        const propsString = Object.keys(props).map(prop =&amp;gt; !isEmptyArray(props[prop]) ? `${prop}: ${props[prop].map(p =&amp;gt; `&amp;quot;${p}&amp;quot;`).join(&#039;, &#039;)}` : &#039;&#039;).join(&#039;; &#039;)&lt;br /&gt;
        return `В записи с Id ${recordId} отсутствуют либо не заполнены следующие поля у показателей: ${propsString}`&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    indicatorsError: (recordId) =&amp;gt;{&lt;br /&gt;
        throw new Error(messages.emptyIndicators(recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFoundIndicators: (indicators: string[], recordId: number) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.indicators(indicators, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFilledIndicatorProps: (props: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyIndicatorsProps(props, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(arr) &amp;amp;&amp;amp; arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasIndicators = (journalRecordManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
    return Array.isArray(indicators) &amp;amp;&amp;amp; indicators.length &amp;gt; 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFoundIndicators = (indicators: JournalRecordIndicatorResponse[], indicatorsList = config.indicatorsList) =&amp;gt; {&lt;br /&gt;
    return indicatorsList.filter(ind =&amp;gt; indicators.every(ljInd =&amp;gt; ljInd?.indicator?.name?.toLowerCase() !== ind.toLocaleLowerCase()))&lt;br /&gt;
} &lt;br /&gt;
&lt;br /&gt;
const getNotFilledResultsFields = (indicators: JournalRecordIndicatorResponse[]): Record&amp;lt;string, string[]&amp;gt; =&amp;gt; {&lt;br /&gt;
    return indicators.reduce((acc, indicator) =&amp;gt; {&lt;br /&gt;
        const config = indicatorPropsConfig(indicator)&lt;br /&gt;
        return {&lt;br /&gt;
            ...acc,&lt;br /&gt;
            [indicator?.indicator?.name]: Object.keys(config).reduce((acc, prop) =&amp;gt; {&lt;br /&gt;
                if (!config[prop].get()) return [...acc, config[prop].label]&lt;br /&gt;
                return acc&lt;br /&gt;
            }, [] as string[])&lt;br /&gt;
        }&lt;br /&gt;
    }, {}) as Record&amp;lt;string, string[]&amp;gt;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFilledProps = (indicatorsProps: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
    return Object.keys(indicatorsProps).some(indicator =&amp;gt; !isEmptyArray(indicatorsProps[indicator]))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFoundIndicators = (indicators: string[]) =&amp;gt; !isEmptyArray(indicators)&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (!hasIndicators(journalRecordManager)) {&lt;br /&gt;
            errorHandlers.indicatorsError(journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
&lt;br /&gt;
        const notFoundIndicators = getNotFoundIndicators(indicators)&lt;br /&gt;
        if (hasNotFoundIndicators(notFoundIndicators)) { &lt;br /&gt;
            errorHandlers.notFoundIndicators(notFoundIndicators, journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const emptyIndicatorProps = getNotFilledResultsFields(indicators)&lt;br /&gt;
        if (hasNotFilledProps(emptyIndicatorProps)) {&lt;br /&gt;
            errorHandlers.notFilledIndicatorProps(emptyIndicatorProps)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        showSuccessMessage(messages.success, socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-record-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Список. Целое число&#039;,&lt;br /&gt;
        &#039;Строка&#039;,&lt;br /&gt;
        &#039;Целое число&#039;,&lt;br /&gt;
        &#039;Вещественное число&#039;,&lt;br /&gt;
        &#039;Логический тип&#039;,&lt;br /&gt;
        &#039;Дата&#039;,&lt;br /&gt;
        &#039;Время&#039;,&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144,  // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFilledFields = (fields: string[], journalRecordsManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    console.log(journalRecordsManager.data.attributes, &#039;data.attributes&#039;)&lt;br /&gt;
    return fields.filter(f =&amp;gt; !journalRecordsManager.getFieldValue(f))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const emptyFields = getNotFilledFields(config.requiredFields, journalRecordManager)&lt;br /&gt;
&lt;br /&gt;
        if (isEmptyArray(emptyFields)) {&lt;br /&gt;
            showSuccessMessage(messages.success, socketId)&lt;br /&gt;
            return config.integrationResult&lt;br /&gt;
        } else errorHandlers.fieldsError(emptyFields, config.journalRecordId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-sample-moving.png|frame]]&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { JournalsRecordRouteController } from &amp;quot;../../src/servivces/worker/api/controllers/journals-record-route&amp;quot;&lt;br /&gt;
import { apiInstance } from &amp;quot;../../src/servivces/worker/api/interactors/ApiInstance&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordRouteDto } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    journalRecordId: 114 // Лаб. воздуха / Анализы завершены&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    route: (routes: JournalRecordRouteDto[], recordId: number) =&amp;gt; `Машрут пробы: ${routes.map(r =&amp;gt; `&amp;quot;${r.stageName}&amp;quot;`).join(&#039; -&amp;gt; &#039;)}. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
    notMoved: (recordId: number) =&amp;gt; `Движение пробы не происходило. Id записи ЛЖ: ${recordId}`, &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isSampleMoved = (moves: JournalRecordRouteDto[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(moves) &amp;amp;&amp;amp; moves.length &amp;gt; 1&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalsRecordRouteController = new JournalsRecordRouteController(apiInstance)&lt;br /&gt;
        const sampleMoves = await journalsRecordRouteController.getJournalRecordRoute(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (isSampleMoved(sampleMoves)) {&lt;br /&gt;
            showSuccessMessage(messages.route(sampleMoves, config.journalRecordId), socketId)&lt;br /&gt;
        } else {&lt;br /&gt;
            showSuccessMessage(messages.notMoved(config.journalRecordId), socketId)&lt;br /&gt;
        }&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-user-rights.png|frame]]&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Fill-record-fields.png|frame]]&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Copy-journal-record.png|frame]]&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2368</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2368"/>
		<updated>2026-04-02T01:35:51Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Проверка заполненности атрибутов записи ЛЖ. check-record-fields-filled */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
   &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { MessageBoxResults } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    dateField: &#039;Дата регистрации&#039;,&lt;br /&gt;
    expirationLimit: 10,&lt;br /&gt;
    journalRecordId: 201, // Лаб. воздуха / Анализы завершены&lt;br /&gt;
    integrationResult: { message: &#039;Срок регистрации пробы не истёк !&#039;}&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    success: (recordId) =&amp;gt; `Срок регистрации пробы не истёк ! Id записи ${recordId}`,&lt;br /&gt;
    expired: (limit, recordId) =&amp;gt; `Срок регистрации пробы истёк!\nПрошло более ${ limit } дней с момента регистрации. Id записи: ${recordId}`,&lt;br /&gt;
    dateField: (dateField, stageName, journalRecordId) =&amp;gt; [&lt;br /&gt;
        `Не заполнено либо отсутствует поле &amp;quot;${dateField}&amp;quot;`,&lt;br /&gt;
        `в этапе &amp;quot;${stageName || &#039;&#039;}&amp;quot;.\nId записи: ${journalRecordId}`&lt;br /&gt;
    ].join(&#039; &#039;)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    dateError: (dateField, stageName, journalRecordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.dateField(dateField, stageName, journalRecordId))&lt;br /&gt;
    },&lt;br /&gt;
    expirationError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.expired(config.expirationLimit, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getDaysByDate = (date: Date) =&amp;gt; {&lt;br /&gt;
    const dayMs = 60 * 60 * 24 * 1000&lt;br /&gt;
    const timestamp = date.getTime()&lt;br /&gt;
    return timestamp/dayMs&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getExpirationOffset = (registrationDate: string) =&amp;gt; {&lt;br /&gt;
    const regDate = new Date(registrationDate)&lt;br /&gt;
    const today = new Date()&lt;br /&gt;
    return getDaysByDate(today) - getDaysByDate(regDate)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const date = journalRecordManager.getFieldValue(config.dateField)&lt;br /&gt;
&lt;br /&gt;
        if (!date) errorHandlers.dateError(config.dateField, journalRecordManager?.data?.stage?.stage?.name, config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        const expirationOffset = getExpirationOffset(date as string)&lt;br /&gt;
&lt;br /&gt;
        if (expirationOffset &amp;gt;= config.expirationLimit) errorHandlers.expirationError()&lt;br /&gt;
&lt;br /&gt;
        await showSuccessMessage(messages.success(config.journalRecordId), socketId)&lt;br /&gt;
&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordIndicatorResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
type IndicatorProp = { label: string, get: () =&amp;gt; unknown }&lt;br /&gt;
type IndicatorPropsConfig = Record&amp;lt;string, IndicatorProp&amp;gt;&lt;br /&gt;
&lt;br /&gt;
const indicatorPropsConfig = (indicator): IndicatorPropsConfig =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        executors: {&lt;br /&gt;
            label: &#039;Исполнители&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.executors&lt;br /&gt;
        },&lt;br /&gt;
        methodic: {&lt;br /&gt;
            label: &#039;Методики&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.methodics&lt;br /&gt;
        },&lt;br /&gt;
        averageResult: {&lt;br /&gt;
            label: &#039;Средний результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageValue&lt;br /&gt;
        },&lt;br /&gt;
        averageRoundedValue: {&lt;br /&gt;
            label: &#039;Средний округлённый результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageRoundedValue&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Место отбора&#039;,&lt;br /&gt;
        &#039;Дата регистрации&#039;,&lt;br /&gt;
        &#039;Шифр пробы&#039;,&lt;br /&gt;
        &#039;Время регистрации&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    indicatorsList: [&lt;br /&gt;
        &#039;Аммоний-ион&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144, // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyIndicators: (recordId) =&amp;gt; `в записи с Id ${recordId} отсутствуют показатели`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
    indicators: (indicators: string[], recordId) =&amp;gt; `Показатели ${indicators.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} отсутствуют в записи с ID ${recordId}`,&lt;br /&gt;
    emptyIndicatorsProps: (props: Record&amp;lt;string, string[]&amp;gt;, recordId) =&amp;gt; {&lt;br /&gt;
        const propsString = Object.keys(props).map(prop =&amp;gt; !isEmptyArray(props[prop]) ? `${prop}: ${props[prop].map(p =&amp;gt; `&amp;quot;${p}&amp;quot;`).join(&#039;, &#039;)}` : &#039;&#039;).join(&#039;; &#039;)&lt;br /&gt;
        return `В записи с Id ${recordId} отсутствуют либо не заполнены следующие поля у показателей: ${propsString}`&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    indicatorsError: (recordId) =&amp;gt;{&lt;br /&gt;
        throw new Error(messages.emptyIndicators(recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFoundIndicators: (indicators: string[], recordId: number) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.indicators(indicators, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFilledIndicatorProps: (props: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyIndicatorsProps(props, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(arr) &amp;amp;&amp;amp; arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasIndicators = (journalRecordManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
    return Array.isArray(indicators) &amp;amp;&amp;amp; indicators.length &amp;gt; 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFoundIndicators = (indicators: JournalRecordIndicatorResponse[], indicatorsList = config.indicatorsList) =&amp;gt; {&lt;br /&gt;
    return indicatorsList.filter(ind =&amp;gt; indicators.every(ljInd =&amp;gt; ljInd?.indicator?.name?.toLowerCase() !== ind.toLocaleLowerCase()))&lt;br /&gt;
} &lt;br /&gt;
&lt;br /&gt;
const getNotFilledResultsFields = (indicators: JournalRecordIndicatorResponse[]): Record&amp;lt;string, string[]&amp;gt; =&amp;gt; {&lt;br /&gt;
    return indicators.reduce((acc, indicator) =&amp;gt; {&lt;br /&gt;
        const config = indicatorPropsConfig(indicator)&lt;br /&gt;
        return {&lt;br /&gt;
            ...acc,&lt;br /&gt;
            [indicator?.indicator?.name]: Object.keys(config).reduce((acc, prop) =&amp;gt; {&lt;br /&gt;
                if (!config[prop].get()) return [...acc, config[prop].label]&lt;br /&gt;
                return acc&lt;br /&gt;
            }, [] as string[])&lt;br /&gt;
        }&lt;br /&gt;
    }, {}) as Record&amp;lt;string, string[]&amp;gt;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFilledProps = (indicatorsProps: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
    return Object.keys(indicatorsProps).some(indicator =&amp;gt; !isEmptyArray(indicatorsProps[indicator]))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFoundIndicators = (indicators: string[]) =&amp;gt; !isEmptyArray(indicators)&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (!hasIndicators(journalRecordManager)) {&lt;br /&gt;
            errorHandlers.indicatorsError(journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
&lt;br /&gt;
        const notFoundIndicators = getNotFoundIndicators(indicators)&lt;br /&gt;
        if (hasNotFoundIndicators(notFoundIndicators)) { &lt;br /&gt;
            errorHandlers.notFoundIndicators(notFoundIndicators, journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const emptyIndicatorProps = getNotFilledResultsFields(indicators)&lt;br /&gt;
        if (hasNotFilledProps(emptyIndicatorProps)) {&lt;br /&gt;
            errorHandlers.notFilledIndicatorProps(emptyIndicatorProps)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        showSuccessMessage(messages.success, socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-record-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Список. Целое число&#039;,&lt;br /&gt;
        &#039;Строка&#039;,&lt;br /&gt;
        &#039;Целое число&#039;,&lt;br /&gt;
        &#039;Вещественное число&#039;,&lt;br /&gt;
        &#039;Логический тип&#039;,&lt;br /&gt;
        &#039;Дата&#039;,&lt;br /&gt;
        &#039;Время&#039;,&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144,  // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFilledFields = (fields: string[], journalRecordsManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    console.log(journalRecordsManager.data.attributes, &#039;data.attributes&#039;)&lt;br /&gt;
    return fields.filter(f =&amp;gt; !journalRecordsManager.getFieldValue(f))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const emptyFields = getNotFilledFields(config.requiredFields, journalRecordManager)&lt;br /&gt;
&lt;br /&gt;
        if (isEmptyArray(emptyFields)) {&lt;br /&gt;
            showSuccessMessage(messages.success, socketId)&lt;br /&gt;
            return config.integrationResult&lt;br /&gt;
        } else errorHandlers.fieldsError(emptyFields, config.journalRecordId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-sample-moving.png|frame]]&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-user-rights.png|frame]]&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Fill-record-fields.png|frame]]&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Copy-journal-record.png|frame]]&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2367</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2367"/>
		<updated>2026-04-02T01:33:57Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Проверка заполненности полей показателей. check-indicator-fields-filled */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
   &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { MessageBoxResults } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    dateField: &#039;Дата регистрации&#039;,&lt;br /&gt;
    expirationLimit: 10,&lt;br /&gt;
    journalRecordId: 201, // Лаб. воздуха / Анализы завершены&lt;br /&gt;
    integrationResult: { message: &#039;Срок регистрации пробы не истёк !&#039;}&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    success: (recordId) =&amp;gt; `Срок регистрации пробы не истёк ! Id записи ${recordId}`,&lt;br /&gt;
    expired: (limit, recordId) =&amp;gt; `Срок регистрации пробы истёк!\nПрошло более ${ limit } дней с момента регистрации. Id записи: ${recordId}`,&lt;br /&gt;
    dateField: (dateField, stageName, journalRecordId) =&amp;gt; [&lt;br /&gt;
        `Не заполнено либо отсутствует поле &amp;quot;${dateField}&amp;quot;`,&lt;br /&gt;
        `в этапе &amp;quot;${stageName || &#039;&#039;}&amp;quot;.\nId записи: ${journalRecordId}`&lt;br /&gt;
    ].join(&#039; &#039;)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    dateError: (dateField, stageName, journalRecordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.dateField(dateField, stageName, journalRecordId))&lt;br /&gt;
    },&lt;br /&gt;
    expirationError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.expired(config.expirationLimit, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getDaysByDate = (date: Date) =&amp;gt; {&lt;br /&gt;
    const dayMs = 60 * 60 * 24 * 1000&lt;br /&gt;
    const timestamp = date.getTime()&lt;br /&gt;
    return timestamp/dayMs&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getExpirationOffset = (registrationDate: string) =&amp;gt; {&lt;br /&gt;
    const regDate = new Date(registrationDate)&lt;br /&gt;
    const today = new Date()&lt;br /&gt;
    return getDaysByDate(today) - getDaysByDate(regDate)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const date = journalRecordManager.getFieldValue(config.dateField)&lt;br /&gt;
&lt;br /&gt;
        if (!date) errorHandlers.dateError(config.dateField, journalRecordManager?.data?.stage?.stage?.name, config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        const expirationOffset = getExpirationOffset(date as string)&lt;br /&gt;
&lt;br /&gt;
        if (expirationOffset &amp;gt;= config.expirationLimit) errorHandlers.expirationError()&lt;br /&gt;
&lt;br /&gt;
        await showSuccessMessage(messages.success(config.journalRecordId), socketId)&lt;br /&gt;
&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordIndicatorResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
type IndicatorProp = { label: string, get: () =&amp;gt; unknown }&lt;br /&gt;
type IndicatorPropsConfig = Record&amp;lt;string, IndicatorProp&amp;gt;&lt;br /&gt;
&lt;br /&gt;
const indicatorPropsConfig = (indicator): IndicatorPropsConfig =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        executors: {&lt;br /&gt;
            label: &#039;Исполнители&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.executors&lt;br /&gt;
        },&lt;br /&gt;
        methodic: {&lt;br /&gt;
            label: &#039;Методики&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.methodics&lt;br /&gt;
        },&lt;br /&gt;
        averageResult: {&lt;br /&gt;
            label: &#039;Средний результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageValue&lt;br /&gt;
        },&lt;br /&gt;
        averageRoundedValue: {&lt;br /&gt;
            label: &#039;Средний округлённый результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageRoundedValue&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Место отбора&#039;,&lt;br /&gt;
        &#039;Дата регистрации&#039;,&lt;br /&gt;
        &#039;Шифр пробы&#039;,&lt;br /&gt;
        &#039;Время регистрации&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    indicatorsList: [&lt;br /&gt;
        &#039;Аммоний-ион&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144, // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyIndicators: (recordId) =&amp;gt; `в записи с Id ${recordId} отсутствуют показатели`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
    indicators: (indicators: string[], recordId) =&amp;gt; `Показатели ${indicators.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} отсутствуют в записи с ID ${recordId}`,&lt;br /&gt;
    emptyIndicatorsProps: (props: Record&amp;lt;string, string[]&amp;gt;, recordId) =&amp;gt; {&lt;br /&gt;
        const propsString = Object.keys(props).map(prop =&amp;gt; !isEmptyArray(props[prop]) ? `${prop}: ${props[prop].map(p =&amp;gt; `&amp;quot;${p}&amp;quot;`).join(&#039;, &#039;)}` : &#039;&#039;).join(&#039;; &#039;)&lt;br /&gt;
        return `В записи с Id ${recordId} отсутствуют либо не заполнены следующие поля у показателей: ${propsString}`&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    indicatorsError: (recordId) =&amp;gt;{&lt;br /&gt;
        throw new Error(messages.emptyIndicators(recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFoundIndicators: (indicators: string[], recordId: number) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.indicators(indicators, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFilledIndicatorProps: (props: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyIndicatorsProps(props, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(arr) &amp;amp;&amp;amp; arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasIndicators = (journalRecordManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
    return Array.isArray(indicators) &amp;amp;&amp;amp; indicators.length &amp;gt; 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFoundIndicators = (indicators: JournalRecordIndicatorResponse[], indicatorsList = config.indicatorsList) =&amp;gt; {&lt;br /&gt;
    return indicatorsList.filter(ind =&amp;gt; indicators.every(ljInd =&amp;gt; ljInd?.indicator?.name?.toLowerCase() !== ind.toLocaleLowerCase()))&lt;br /&gt;
} &lt;br /&gt;
&lt;br /&gt;
const getNotFilledResultsFields = (indicators: JournalRecordIndicatorResponse[]): Record&amp;lt;string, string[]&amp;gt; =&amp;gt; {&lt;br /&gt;
    return indicators.reduce((acc, indicator) =&amp;gt; {&lt;br /&gt;
        const config = indicatorPropsConfig(indicator)&lt;br /&gt;
        return {&lt;br /&gt;
            ...acc,&lt;br /&gt;
            [indicator?.indicator?.name]: Object.keys(config).reduce((acc, prop) =&amp;gt; {&lt;br /&gt;
                if (!config[prop].get()) return [...acc, config[prop].label]&lt;br /&gt;
                return acc&lt;br /&gt;
            }, [] as string[])&lt;br /&gt;
        }&lt;br /&gt;
    }, {}) as Record&amp;lt;string, string[]&amp;gt;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFilledProps = (indicatorsProps: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
    return Object.keys(indicatorsProps).some(indicator =&amp;gt; !isEmptyArray(indicatorsProps[indicator]))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFoundIndicators = (indicators: string[]) =&amp;gt; !isEmptyArray(indicators)&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (!hasIndicators(journalRecordManager)) {&lt;br /&gt;
            errorHandlers.indicatorsError(journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
&lt;br /&gt;
        const notFoundIndicators = getNotFoundIndicators(indicators)&lt;br /&gt;
        if (hasNotFoundIndicators(notFoundIndicators)) { &lt;br /&gt;
            errorHandlers.notFoundIndicators(notFoundIndicators, journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const emptyIndicatorProps = getNotFilledResultsFields(indicators)&lt;br /&gt;
        if (hasNotFilledProps(emptyIndicatorProps)) {&lt;br /&gt;
            errorHandlers.notFilledIndicatorProps(emptyIndicatorProps)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        showSuccessMessage(messages.success, socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-record-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-sample-moving.png|frame]]&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-user-rights.png|frame]]&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Fill-record-fields.png|frame]]&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Copy-journal-record.png|frame]]&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2366</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2366"/>
		<updated>2026-04-02T01:31:54Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Проверка даты регистрации. check-registration-date */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;strong&amp;gt;Просмотр кода&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
 &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
   &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { MessageBoxResults } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    dateField: &#039;Дата регистрации&#039;,&lt;br /&gt;
    expirationLimit: 10,&lt;br /&gt;
    journalRecordId: 201, // Лаб. воздуха / Анализы завершены&lt;br /&gt;
    integrationResult: { message: &#039;Срок регистрации пробы не истёк !&#039;}&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    success: (recordId) =&amp;gt; `Срок регистрации пробы не истёк ! Id записи ${recordId}`,&lt;br /&gt;
    expired: (limit, recordId) =&amp;gt; `Срок регистрации пробы истёк!\nПрошло более ${ limit } дней с момента регистрации. Id записи: ${recordId}`,&lt;br /&gt;
    dateField: (dateField, stageName, journalRecordId) =&amp;gt; [&lt;br /&gt;
        `Не заполнено либо отсутствует поле &amp;quot;${dateField}&amp;quot;`,&lt;br /&gt;
        `в этапе &amp;quot;${stageName || &#039;&#039;}&amp;quot;.\nId записи: ${journalRecordId}`&lt;br /&gt;
    ].join(&#039; &#039;)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    dateError: (dateField, stageName, journalRecordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.dateField(dateField, stageName, journalRecordId))&lt;br /&gt;
    },&lt;br /&gt;
    expirationError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.expired(config.expirationLimit, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getDaysByDate = (date: Date) =&amp;gt; {&lt;br /&gt;
    const dayMs = 60 * 60 * 24 * 1000&lt;br /&gt;
    const timestamp = date.getTime()&lt;br /&gt;
    return timestamp/dayMs&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getExpirationOffset = (registrationDate: string) =&amp;gt; {&lt;br /&gt;
    const regDate = new Date(registrationDate)&lt;br /&gt;
    const today = new Date()&lt;br /&gt;
    return getDaysByDate(today) - getDaysByDate(regDate)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const date = journalRecordManager.getFieldValue(config.dateField)&lt;br /&gt;
&lt;br /&gt;
        if (!date) errorHandlers.dateError(config.dateField, journalRecordManager?.data?.stage?.stage?.name, config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        const expirationOffset = getExpirationOffset(date as string)&lt;br /&gt;
&lt;br /&gt;
        if (expirationOffset &amp;gt;= config.expirationLimit) errorHandlers.expirationError()&lt;br /&gt;
&lt;br /&gt;
        await showSuccessMessage(messages.success(config.journalRecordId), socketId)&lt;br /&gt;
&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordIndicatorResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
type IndicatorProp = { label: string, get: () =&amp;gt; unknown }&lt;br /&gt;
type IndicatorPropsConfig = Record&amp;lt;string, IndicatorProp&amp;gt;&lt;br /&gt;
&lt;br /&gt;
const indicatorPropsConfig = (indicator): IndicatorPropsConfig =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        executors: {&lt;br /&gt;
            label: &#039;Исполнители&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.executors&lt;br /&gt;
        },&lt;br /&gt;
        methodic: {&lt;br /&gt;
            label: &#039;Методики&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.methodics&lt;br /&gt;
        },&lt;br /&gt;
        averageResult: {&lt;br /&gt;
            label: &#039;Средний результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageValue&lt;br /&gt;
        },&lt;br /&gt;
        averageRoundedValue: {&lt;br /&gt;
            label: &#039;Средний округлённый результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageRoundedValue&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Место отбора&#039;,&lt;br /&gt;
        &#039;Дата регистрации&#039;,&lt;br /&gt;
        &#039;Шифр пробы&#039;,&lt;br /&gt;
        &#039;Время регистрации&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    indicatorsList: [&lt;br /&gt;
        &#039;Аммоний-ион&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144, // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyIndicators: (recordId) =&amp;gt; `в записи с Id ${recordId} отсутствуют показатели`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
    indicators: (indicators: string[], recordId) =&amp;gt; `Показатели ${indicators.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} отсутствуют в записи с ID ${recordId}`,&lt;br /&gt;
    emptyIndicatorsProps: (props: Record&amp;lt;string, string[]&amp;gt;, recordId) =&amp;gt; {&lt;br /&gt;
        const propsString = Object.keys(props).map(prop =&amp;gt; !isEmptyArray(props[prop]) ? `${prop}: ${props[prop].map(p =&amp;gt; `&amp;quot;${p}&amp;quot;`).join(&#039;, &#039;)}` : &#039;&#039;).join(&#039;; &#039;)&lt;br /&gt;
        return `В записи с Id ${recordId} отсутствуют либо не заполнены следующие поля у показателей: ${propsString}`&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    indicatorsError: (recordId) =&amp;gt;{&lt;br /&gt;
        throw new Error(messages.emptyIndicators(recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFoundIndicators: (indicators: string[], recordId: number) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.indicators(indicators, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFilledIndicatorProps: (props: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyIndicatorsProps(props, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(arr) &amp;amp;&amp;amp; arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasIndicators = (journalRecordManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
    return Array.isArray(indicators) &amp;amp;&amp;amp; indicators.length &amp;gt; 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFoundIndicators = (indicators: JournalRecordIndicatorResponse[], indicatorsList = config.indicatorsList) =&amp;gt; {&lt;br /&gt;
    return indicatorsList.filter(ind =&amp;gt; indicators.every(ljInd =&amp;gt; ljInd?.indicator?.name?.toLowerCase() !== ind.toLocaleLowerCase()))&lt;br /&gt;
} &lt;br /&gt;
&lt;br /&gt;
const getNotFilledResultsFields = (indicators: JournalRecordIndicatorResponse[]): Record&amp;lt;string, string[]&amp;gt; =&amp;gt; {&lt;br /&gt;
    return indicators.reduce((acc, indicator) =&amp;gt; {&lt;br /&gt;
        const config = indicatorPropsConfig(indicator)&lt;br /&gt;
        return {&lt;br /&gt;
            ...acc,&lt;br /&gt;
            [indicator?.indicator?.name]: Object.keys(config).reduce((acc, prop) =&amp;gt; {&lt;br /&gt;
                if (!config[prop].get()) return [...acc, config[prop].label]&lt;br /&gt;
                return acc&lt;br /&gt;
            }, [] as string[])&lt;br /&gt;
        }&lt;br /&gt;
    }, {}) as Record&amp;lt;string, string[]&amp;gt;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFilledProps = (indicatorsProps: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
    return Object.keys(indicatorsProps).some(indicator =&amp;gt; !isEmptyArray(indicatorsProps[indicator]))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFoundIndicators = (indicators: string[]) =&amp;gt; !isEmptyArray(indicators)&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (!hasIndicators(journalRecordManager)) {&lt;br /&gt;
            errorHandlers.indicatorsError(journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
&lt;br /&gt;
        const notFoundIndicators = getNotFoundIndicators(indicators)&lt;br /&gt;
        if (hasNotFoundIndicators(notFoundIndicators)) { &lt;br /&gt;
            errorHandlers.notFoundIndicators(notFoundIndicators, journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const emptyIndicatorProps = getNotFilledResultsFields(indicators)&lt;br /&gt;
        if (hasNotFilledProps(emptyIndicatorProps)) {&lt;br /&gt;
            errorHandlers.notFilledIndicatorProps(emptyIndicatorProps)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        showSuccessMessage(messages.success, socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-record-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-sample-moving.png|frame]]&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-user-rights.png|frame]]&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Fill-record-fields.png|frame]]&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Copy-journal-record.png|frame]]&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2365</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2365"/>
		<updated>2026-04-02T01:15:01Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Проверка даты регистрации. check-registration-date */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{{Collapse&lt;br /&gt;
| title=Посмотреть код&lt;br /&gt;
| content=&amp;lt;syntaxhighlight lang=&amp;quot;cpp&amp;quot;&amp;gt;&lt;br /&gt;
#include &amp;lt;iostream&amp;gt;&lt;br /&gt;
int main() {&lt;br /&gt;
    std::cout &amp;lt;&amp;lt; &amp;quot;Hello, World!&amp;quot; &amp;lt;&amp;lt; std::endl;&lt;br /&gt;
    return 0;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;strong&amp;gt;Код&amp;lt;/strong&amp;gt;&lt;br /&gt;
        &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { MessageBoxResults } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    dateField: &#039;Дата регистрации&#039;,&lt;br /&gt;
    expirationLimit: 10,&lt;br /&gt;
    journalRecordId: 201, // Лаб. воздуха / Анализы завершены&lt;br /&gt;
    integrationResult: { message: &#039;Срок регистрации пробы не истёк !&#039;}&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    success: (recordId) =&amp;gt; `Срок регистрации пробы не истёк ! Id записи ${recordId}`,&lt;br /&gt;
    expired: (limit, recordId) =&amp;gt; `Срок регистрации пробы истёк!\nПрошло более ${ limit } дней с момента регистрации. Id записи: ${recordId}`,&lt;br /&gt;
    dateField: (dateField, stageName, journalRecordId) =&amp;gt; [&lt;br /&gt;
        `Не заполнено либо отсутствует поле &amp;quot;${dateField}&amp;quot;`,&lt;br /&gt;
        `в этапе &amp;quot;${stageName || &#039;&#039;}&amp;quot;.\nId записи: ${journalRecordId}`&lt;br /&gt;
    ].join(&#039; &#039;)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    dateError: (dateField, stageName, journalRecordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.dateField(dateField, stageName, journalRecordId))&lt;br /&gt;
    },&lt;br /&gt;
    expirationError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.expired(config.expirationLimit, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getDaysByDate = (date: Date) =&amp;gt; {&lt;br /&gt;
    const dayMs = 60 * 60 * 24 * 1000&lt;br /&gt;
    const timestamp = date.getTime()&lt;br /&gt;
    return timestamp/dayMs&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getExpirationOffset = (registrationDate: string) =&amp;gt; {&lt;br /&gt;
    const regDate = new Date(registrationDate)&lt;br /&gt;
    const today = new Date()&lt;br /&gt;
    return getDaysByDate(today) - getDaysByDate(regDate)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const date = journalRecordManager.getFieldValue(config.dateField)&lt;br /&gt;
&lt;br /&gt;
        if (!date) errorHandlers.dateError(config.dateField, journalRecordManager?.data?.stage?.stage?.name, config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        const expirationOffset = getExpirationOffset(date as string)&lt;br /&gt;
&lt;br /&gt;
        if (expirationOffset &amp;gt;= config.expirationLimit) errorHandlers.expirationError()&lt;br /&gt;
&lt;br /&gt;
        await showSuccessMessage(messages.success(config.journalRecordId), socketId)&lt;br /&gt;
&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
        &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordIndicatorResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
type IndicatorProp = { label: string, get: () =&amp;gt; unknown }&lt;br /&gt;
type IndicatorPropsConfig = Record&amp;lt;string, IndicatorProp&amp;gt;&lt;br /&gt;
&lt;br /&gt;
const indicatorPropsConfig = (indicator): IndicatorPropsConfig =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        executors: {&lt;br /&gt;
            label: &#039;Исполнители&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.executors&lt;br /&gt;
        },&lt;br /&gt;
        methodic: {&lt;br /&gt;
            label: &#039;Методики&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.methodics&lt;br /&gt;
        },&lt;br /&gt;
        averageResult: {&lt;br /&gt;
            label: &#039;Средний результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageValue&lt;br /&gt;
        },&lt;br /&gt;
        averageRoundedValue: {&lt;br /&gt;
            label: &#039;Средний округлённый результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageRoundedValue&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Место отбора&#039;,&lt;br /&gt;
        &#039;Дата регистрации&#039;,&lt;br /&gt;
        &#039;Шифр пробы&#039;,&lt;br /&gt;
        &#039;Время регистрации&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    indicatorsList: [&lt;br /&gt;
        &#039;Аммоний-ион&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144, // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyIndicators: (recordId) =&amp;gt; `в записи с Id ${recordId} отсутствуют показатели`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
    indicators: (indicators: string[], recordId) =&amp;gt; `Показатели ${indicators.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} отсутствуют в записи с ID ${recordId}`,&lt;br /&gt;
    emptyIndicatorsProps: (props: Record&amp;lt;string, string[]&amp;gt;, recordId) =&amp;gt; {&lt;br /&gt;
        const propsString = Object.keys(props).map(prop =&amp;gt; !isEmptyArray(props[prop]) ? `${prop}: ${props[prop].map(p =&amp;gt; `&amp;quot;${p}&amp;quot;`).join(&#039;, &#039;)}` : &#039;&#039;).join(&#039;; &#039;)&lt;br /&gt;
        return `В записи с Id ${recordId} отсутствуют либо не заполнены следующие поля у показателей: ${propsString}`&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    indicatorsError: (recordId) =&amp;gt;{&lt;br /&gt;
        throw new Error(messages.emptyIndicators(recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFoundIndicators: (indicators: string[], recordId: number) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.indicators(indicators, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFilledIndicatorProps: (props: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyIndicatorsProps(props, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(arr) &amp;amp;&amp;amp; arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasIndicators = (journalRecordManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
    return Array.isArray(indicators) &amp;amp;&amp;amp; indicators.length &amp;gt; 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFoundIndicators = (indicators: JournalRecordIndicatorResponse[], indicatorsList = config.indicatorsList) =&amp;gt; {&lt;br /&gt;
    return indicatorsList.filter(ind =&amp;gt; indicators.every(ljInd =&amp;gt; ljInd?.indicator?.name?.toLowerCase() !== ind.toLocaleLowerCase()))&lt;br /&gt;
} &lt;br /&gt;
&lt;br /&gt;
const getNotFilledResultsFields = (indicators: JournalRecordIndicatorResponse[]): Record&amp;lt;string, string[]&amp;gt; =&amp;gt; {&lt;br /&gt;
    return indicators.reduce((acc, indicator) =&amp;gt; {&lt;br /&gt;
        const config = indicatorPropsConfig(indicator)&lt;br /&gt;
        return {&lt;br /&gt;
            ...acc,&lt;br /&gt;
            [indicator?.indicator?.name]: Object.keys(config).reduce((acc, prop) =&amp;gt; {&lt;br /&gt;
                if (!config[prop].get()) return [...acc, config[prop].label]&lt;br /&gt;
                return acc&lt;br /&gt;
            }, [] as string[])&lt;br /&gt;
        }&lt;br /&gt;
    }, {}) as Record&amp;lt;string, string[]&amp;gt;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFilledProps = (indicatorsProps: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
    return Object.keys(indicatorsProps).some(indicator =&amp;gt; !isEmptyArray(indicatorsProps[indicator]))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFoundIndicators = (indicators: string[]) =&amp;gt; !isEmptyArray(indicators)&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (!hasIndicators(journalRecordManager)) {&lt;br /&gt;
            errorHandlers.indicatorsError(journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
&lt;br /&gt;
        const notFoundIndicators = getNotFoundIndicators(indicators)&lt;br /&gt;
        if (hasNotFoundIndicators(notFoundIndicators)) { &lt;br /&gt;
            errorHandlers.notFoundIndicators(notFoundIndicators, journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const emptyIndicatorProps = getNotFilledResultsFields(indicators)&lt;br /&gt;
        if (hasNotFilledProps(emptyIndicatorProps)) {&lt;br /&gt;
            errorHandlers.notFilledIndicatorProps(emptyIndicatorProps)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        showSuccessMessage(messages.success, socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-record-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-sample-moving.png|frame]]&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-user-rights.png|frame]]&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Fill-record-fields.png|frame]]&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Copy-journal-record.png|frame]]&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2364</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2364"/>
		<updated>2026-04-02T01:13:12Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Проверка даты регистрации. check-registration-date */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 2px;&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;strong&amp;gt;Код&amp;lt;/strong&amp;gt;&lt;br /&gt;
        &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { MessageBoxResults } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    dateField: &#039;Дата регистрации&#039;,&lt;br /&gt;
    expirationLimit: 10,&lt;br /&gt;
    journalRecordId: 201, // Лаб. воздуха / Анализы завершены&lt;br /&gt;
    integrationResult: { message: &#039;Срок регистрации пробы не истёк !&#039;}&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    success: (recordId) =&amp;gt; `Срок регистрации пробы не истёк ! Id записи ${recordId}`,&lt;br /&gt;
    expired: (limit, recordId) =&amp;gt; `Срок регистрации пробы истёк!\nПрошло более ${ limit } дней с момента регистрации. Id записи: ${recordId}`,&lt;br /&gt;
    dateField: (dateField, stageName, journalRecordId) =&amp;gt; [&lt;br /&gt;
        `Не заполнено либо отсутствует поле &amp;quot;${dateField}&amp;quot;`,&lt;br /&gt;
        `в этапе &amp;quot;${stageName || &#039;&#039;}&amp;quot;.\nId записи: ${journalRecordId}`&lt;br /&gt;
    ].join(&#039; &#039;)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    dateError: (dateField, stageName, journalRecordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.dateField(dateField, stageName, journalRecordId))&lt;br /&gt;
    },&lt;br /&gt;
    expirationError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.expired(config.expirationLimit, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getDaysByDate = (date: Date) =&amp;gt; {&lt;br /&gt;
    const dayMs = 60 * 60 * 24 * 1000&lt;br /&gt;
    const timestamp = date.getTime()&lt;br /&gt;
    return timestamp/dayMs&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getExpirationOffset = (registrationDate: string) =&amp;gt; {&lt;br /&gt;
    const regDate = new Date(registrationDate)&lt;br /&gt;
    const today = new Date()&lt;br /&gt;
    return getDaysByDate(today) - getDaysByDate(regDate)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const date = journalRecordManager.getFieldValue(config.dateField)&lt;br /&gt;
&lt;br /&gt;
        if (!date) errorHandlers.dateError(config.dateField, journalRecordManager?.data?.stage?.stage?.name, config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        const expirationOffset = getExpirationOffset(date as string)&lt;br /&gt;
&lt;br /&gt;
        if (expirationOffset &amp;gt;= config.expirationLimit) errorHandlers.expirationError()&lt;br /&gt;
&lt;br /&gt;
        await showSuccessMessage(messages.success(config.journalRecordId), socketId)&lt;br /&gt;
&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
        &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordIndicatorResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
type IndicatorProp = { label: string, get: () =&amp;gt; unknown }&lt;br /&gt;
type IndicatorPropsConfig = Record&amp;lt;string, IndicatorProp&amp;gt;&lt;br /&gt;
&lt;br /&gt;
const indicatorPropsConfig = (indicator): IndicatorPropsConfig =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        executors: {&lt;br /&gt;
            label: &#039;Исполнители&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.executors&lt;br /&gt;
        },&lt;br /&gt;
        methodic: {&lt;br /&gt;
            label: &#039;Методики&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.methodics&lt;br /&gt;
        },&lt;br /&gt;
        averageResult: {&lt;br /&gt;
            label: &#039;Средний результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageValue&lt;br /&gt;
        },&lt;br /&gt;
        averageRoundedValue: {&lt;br /&gt;
            label: &#039;Средний округлённый результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageRoundedValue&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Место отбора&#039;,&lt;br /&gt;
        &#039;Дата регистрации&#039;,&lt;br /&gt;
        &#039;Шифр пробы&#039;,&lt;br /&gt;
        &#039;Время регистрации&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    indicatorsList: [&lt;br /&gt;
        &#039;Аммоний-ион&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144, // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyIndicators: (recordId) =&amp;gt; `в записи с Id ${recordId} отсутствуют показатели`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
    indicators: (indicators: string[], recordId) =&amp;gt; `Показатели ${indicators.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} отсутствуют в записи с ID ${recordId}`,&lt;br /&gt;
    emptyIndicatorsProps: (props: Record&amp;lt;string, string[]&amp;gt;, recordId) =&amp;gt; {&lt;br /&gt;
        const propsString = Object.keys(props).map(prop =&amp;gt; !isEmptyArray(props[prop]) ? `${prop}: ${props[prop].map(p =&amp;gt; `&amp;quot;${p}&amp;quot;`).join(&#039;, &#039;)}` : &#039;&#039;).join(&#039;; &#039;)&lt;br /&gt;
        return `В записи с Id ${recordId} отсутствуют либо не заполнены следующие поля у показателей: ${propsString}`&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    indicatorsError: (recordId) =&amp;gt;{&lt;br /&gt;
        throw new Error(messages.emptyIndicators(recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFoundIndicators: (indicators: string[], recordId: number) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.indicators(indicators, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFilledIndicatorProps: (props: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyIndicatorsProps(props, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(arr) &amp;amp;&amp;amp; arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasIndicators = (journalRecordManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
    return Array.isArray(indicators) &amp;amp;&amp;amp; indicators.length &amp;gt; 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFoundIndicators = (indicators: JournalRecordIndicatorResponse[], indicatorsList = config.indicatorsList) =&amp;gt; {&lt;br /&gt;
    return indicatorsList.filter(ind =&amp;gt; indicators.every(ljInd =&amp;gt; ljInd?.indicator?.name?.toLowerCase() !== ind.toLocaleLowerCase()))&lt;br /&gt;
} &lt;br /&gt;
&lt;br /&gt;
const getNotFilledResultsFields = (indicators: JournalRecordIndicatorResponse[]): Record&amp;lt;string, string[]&amp;gt; =&amp;gt; {&lt;br /&gt;
    return indicators.reduce((acc, indicator) =&amp;gt; {&lt;br /&gt;
        const config = indicatorPropsConfig(indicator)&lt;br /&gt;
        return {&lt;br /&gt;
            ...acc,&lt;br /&gt;
            [indicator?.indicator?.name]: Object.keys(config).reduce((acc, prop) =&amp;gt; {&lt;br /&gt;
                if (!config[prop].get()) return [...acc, config[prop].label]&lt;br /&gt;
                return acc&lt;br /&gt;
            }, [] as string[])&lt;br /&gt;
        }&lt;br /&gt;
    }, {}) as Record&amp;lt;string, string[]&amp;gt;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFilledProps = (indicatorsProps: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
    return Object.keys(indicatorsProps).some(indicator =&amp;gt; !isEmptyArray(indicatorsProps[indicator]))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFoundIndicators = (indicators: string[]) =&amp;gt; !isEmptyArray(indicators)&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (!hasIndicators(journalRecordManager)) {&lt;br /&gt;
            errorHandlers.indicatorsError(journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
&lt;br /&gt;
        const notFoundIndicators = getNotFoundIndicators(indicators)&lt;br /&gt;
        if (hasNotFoundIndicators(notFoundIndicators)) { &lt;br /&gt;
            errorHandlers.notFoundIndicators(notFoundIndicators, journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const emptyIndicatorProps = getNotFilledResultsFields(indicators)&lt;br /&gt;
        if (hasNotFilledProps(emptyIndicatorProps)) {&lt;br /&gt;
            errorHandlers.notFilledIndicatorProps(emptyIndicatorProps)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        showSuccessMessage(messages.success, socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-record-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-sample-moving.png|frame]]&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-user-rights.png|frame]]&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Fill-record-fields.png|frame]]&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Copy-journal-record.png|frame]]&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2362</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2362"/>
		<updated>2026-04-02T01:11:21Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Проверка даты регистрации. check-registration-date */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;strong&amp;gt;Код&amp;lt;/strong&amp;gt;&lt;br /&gt;
        &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { MessageBoxResults } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    dateField: &#039;Дата регистрации&#039;,&lt;br /&gt;
    expirationLimit: 10,&lt;br /&gt;
    journalRecordId: 201, // Лаб. воздуха / Анализы завершены&lt;br /&gt;
    integrationResult: { message: &#039;Срок регистрации пробы не истёк !&#039;}&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    success: (recordId) =&amp;gt; `Срок регистрации пробы не истёк ! Id записи ${recordId}`,&lt;br /&gt;
    expired: (limit, recordId) =&amp;gt; `Срок регистрации пробы истёк!\nПрошло более ${ limit } дней с момента регистрации. Id записи: ${recordId}`,&lt;br /&gt;
    dateField: (dateField, stageName, journalRecordId) =&amp;gt; [&lt;br /&gt;
        `Не заполнено либо отсутствует поле &amp;quot;${dateField}&amp;quot;`,&lt;br /&gt;
        `в этапе &amp;quot;${stageName || &#039;&#039;}&amp;quot;.\nId записи: ${journalRecordId}`&lt;br /&gt;
    ].join(&#039; &#039;)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    dateError: (dateField, stageName, journalRecordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.dateField(dateField, stageName, journalRecordId))&lt;br /&gt;
    },&lt;br /&gt;
    expirationError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.expired(config.expirationLimit, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getDaysByDate = (date: Date) =&amp;gt; {&lt;br /&gt;
    const dayMs = 60 * 60 * 24 * 1000&lt;br /&gt;
    const timestamp = date.getTime()&lt;br /&gt;
    return timestamp/dayMs&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getExpirationOffset = (registrationDate: string) =&amp;gt; {&lt;br /&gt;
    const regDate = new Date(registrationDate)&lt;br /&gt;
    const today = new Date()&lt;br /&gt;
    return getDaysByDate(today) - getDaysByDate(regDate)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const date = journalRecordManager.getFieldValue(config.dateField)&lt;br /&gt;
&lt;br /&gt;
        if (!date) errorHandlers.dateError(config.dateField, journalRecordManager?.data?.stage?.stage?.name, config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        const expirationOffset = getExpirationOffset(date as string)&lt;br /&gt;
&lt;br /&gt;
        if (expirationOffset &amp;gt;= config.expirationLimit) errorHandlers.expirationError()&lt;br /&gt;
&lt;br /&gt;
        await showSuccessMessage(messages.success(config.journalRecordId), socketId)&lt;br /&gt;
&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
        &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordIndicatorResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
type IndicatorProp = { label: string, get: () =&amp;gt; unknown }&lt;br /&gt;
type IndicatorPropsConfig = Record&amp;lt;string, IndicatorProp&amp;gt;&lt;br /&gt;
&lt;br /&gt;
const indicatorPropsConfig = (indicator): IndicatorPropsConfig =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        executors: {&lt;br /&gt;
            label: &#039;Исполнители&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.executors&lt;br /&gt;
        },&lt;br /&gt;
        methodic: {&lt;br /&gt;
            label: &#039;Методики&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.methodics&lt;br /&gt;
        },&lt;br /&gt;
        averageResult: {&lt;br /&gt;
            label: &#039;Средний результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageValue&lt;br /&gt;
        },&lt;br /&gt;
        averageRoundedValue: {&lt;br /&gt;
            label: &#039;Средний округлённый результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageRoundedValue&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Место отбора&#039;,&lt;br /&gt;
        &#039;Дата регистрации&#039;,&lt;br /&gt;
        &#039;Шифр пробы&#039;,&lt;br /&gt;
        &#039;Время регистрации&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    indicatorsList: [&lt;br /&gt;
        &#039;Аммоний-ион&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144, // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyIndicators: (recordId) =&amp;gt; `в записи с Id ${recordId} отсутствуют показатели`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
    indicators: (indicators: string[], recordId) =&amp;gt; `Показатели ${indicators.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} отсутствуют в записи с ID ${recordId}`,&lt;br /&gt;
    emptyIndicatorsProps: (props: Record&amp;lt;string, string[]&amp;gt;, recordId) =&amp;gt; {&lt;br /&gt;
        const propsString = Object.keys(props).map(prop =&amp;gt; !isEmptyArray(props[prop]) ? `${prop}: ${props[prop].map(p =&amp;gt; `&amp;quot;${p}&amp;quot;`).join(&#039;, &#039;)}` : &#039;&#039;).join(&#039;; &#039;)&lt;br /&gt;
        return `В записи с Id ${recordId} отсутствуют либо не заполнены следующие поля у показателей: ${propsString}`&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    indicatorsError: (recordId) =&amp;gt;{&lt;br /&gt;
        throw new Error(messages.emptyIndicators(recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFoundIndicators: (indicators: string[], recordId: number) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.indicators(indicators, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFilledIndicatorProps: (props: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyIndicatorsProps(props, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(arr) &amp;amp;&amp;amp; arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasIndicators = (journalRecordManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
    return Array.isArray(indicators) &amp;amp;&amp;amp; indicators.length &amp;gt; 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFoundIndicators = (indicators: JournalRecordIndicatorResponse[], indicatorsList = config.indicatorsList) =&amp;gt; {&lt;br /&gt;
    return indicatorsList.filter(ind =&amp;gt; indicators.every(ljInd =&amp;gt; ljInd?.indicator?.name?.toLowerCase() !== ind.toLocaleLowerCase()))&lt;br /&gt;
} &lt;br /&gt;
&lt;br /&gt;
const getNotFilledResultsFields = (indicators: JournalRecordIndicatorResponse[]): Record&amp;lt;string, string[]&amp;gt; =&amp;gt; {&lt;br /&gt;
    return indicators.reduce((acc, indicator) =&amp;gt; {&lt;br /&gt;
        const config = indicatorPropsConfig(indicator)&lt;br /&gt;
        return {&lt;br /&gt;
            ...acc,&lt;br /&gt;
            [indicator?.indicator?.name]: Object.keys(config).reduce((acc, prop) =&amp;gt; {&lt;br /&gt;
                if (!config[prop].get()) return [...acc, config[prop].label]&lt;br /&gt;
                return acc&lt;br /&gt;
            }, [] as string[])&lt;br /&gt;
        }&lt;br /&gt;
    }, {}) as Record&amp;lt;string, string[]&amp;gt;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFilledProps = (indicatorsProps: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
    return Object.keys(indicatorsProps).some(indicator =&amp;gt; !isEmptyArray(indicatorsProps[indicator]))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFoundIndicators = (indicators: string[]) =&amp;gt; !isEmptyArray(indicators)&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (!hasIndicators(journalRecordManager)) {&lt;br /&gt;
            errorHandlers.indicatorsError(journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
&lt;br /&gt;
        const notFoundIndicators = getNotFoundIndicators(indicators)&lt;br /&gt;
        if (hasNotFoundIndicators(notFoundIndicators)) { &lt;br /&gt;
            errorHandlers.notFoundIndicators(notFoundIndicators, journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const emptyIndicatorProps = getNotFilledResultsFields(indicators)&lt;br /&gt;
        if (hasNotFilledProps(emptyIndicatorProps)) {&lt;br /&gt;
            errorHandlers.notFilledIndicatorProps(emptyIndicatorProps)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        showSuccessMessage(messages.success, socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-record-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-sample-moving.png|frame]]&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-user-rights.png|frame]]&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Fill-record-fields.png|frame]]&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Copy-journal-record.png|frame]]&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2361</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2361"/>
		<updated>2026-04-02T01:10:21Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Проверка даты регистрации. check-registration-date */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;div style=&amp;quot;display: flex; align-items: center; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;span class=&amp;quot;mw-collapsible-toggle-placeholder&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
        &amp;lt;strong&amp;gt;Код [свернуть]&amp;lt;/strong&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { MessageBoxResults } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    dateField: &#039;Дата регистрации&#039;,&lt;br /&gt;
    expirationLimit: 10,&lt;br /&gt;
    journalRecordId: 201, // Лаб. воздуха / Анализы завершены&lt;br /&gt;
    integrationResult: { message: &#039;Срок регистрации пробы не истёк !&#039;}&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    success: (recordId) =&amp;gt; `Срок регистрации пробы не истёк ! Id записи ${recordId}`,&lt;br /&gt;
    expired: (limit, recordId) =&amp;gt; `Срок регистрации пробы истёк!\nПрошло более ${ limit } дней с момента регистрации. Id записи: ${recordId}`,&lt;br /&gt;
    dateField: (dateField, stageName, journalRecordId) =&amp;gt; [&lt;br /&gt;
        `Не заполнено либо отсутствует поле &amp;quot;${dateField}&amp;quot;`,&lt;br /&gt;
        `в этапе &amp;quot;${stageName || &#039;&#039;}&amp;quot;.\nId записи: ${journalRecordId}`&lt;br /&gt;
    ].join(&#039; &#039;)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    dateError: (dateField, stageName, journalRecordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.dateField(dateField, stageName, journalRecordId))&lt;br /&gt;
    },&lt;br /&gt;
    expirationError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.expired(config.expirationLimit, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getDaysByDate = (date: Date) =&amp;gt; {&lt;br /&gt;
    const dayMs = 60 * 60 * 24 * 1000&lt;br /&gt;
    const timestamp = date.getTime()&lt;br /&gt;
    return timestamp/dayMs&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getExpirationOffset = (registrationDate: string) =&amp;gt; {&lt;br /&gt;
    const regDate = new Date(registrationDate)&lt;br /&gt;
    const today = new Date()&lt;br /&gt;
    return getDaysByDate(today) - getDaysByDate(regDate)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const date = journalRecordManager.getFieldValue(config.dateField)&lt;br /&gt;
&lt;br /&gt;
        if (!date) errorHandlers.dateError(config.dateField, journalRecordManager?.data?.stage?.stage?.name, config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        const expirationOffset = getExpirationOffset(date as string)&lt;br /&gt;
&lt;br /&gt;
        if (expirationOffset &amp;gt;= config.expirationLimit) errorHandlers.expirationError()&lt;br /&gt;
&lt;br /&gt;
        await showSuccessMessage(messages.success(config.journalRecordId), socketId)&lt;br /&gt;
&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
        &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordIndicatorResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
type IndicatorProp = { label: string, get: () =&amp;gt; unknown }&lt;br /&gt;
type IndicatorPropsConfig = Record&amp;lt;string, IndicatorProp&amp;gt;&lt;br /&gt;
&lt;br /&gt;
const indicatorPropsConfig = (indicator): IndicatorPropsConfig =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        executors: {&lt;br /&gt;
            label: &#039;Исполнители&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.executors&lt;br /&gt;
        },&lt;br /&gt;
        methodic: {&lt;br /&gt;
            label: &#039;Методики&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.methodics&lt;br /&gt;
        },&lt;br /&gt;
        averageResult: {&lt;br /&gt;
            label: &#039;Средний результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageValue&lt;br /&gt;
        },&lt;br /&gt;
        averageRoundedValue: {&lt;br /&gt;
            label: &#039;Средний округлённый результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageRoundedValue&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Место отбора&#039;,&lt;br /&gt;
        &#039;Дата регистрации&#039;,&lt;br /&gt;
        &#039;Шифр пробы&#039;,&lt;br /&gt;
        &#039;Время регистрации&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    indicatorsList: [&lt;br /&gt;
        &#039;Аммоний-ион&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144, // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyIndicators: (recordId) =&amp;gt; `в записи с Id ${recordId} отсутствуют показатели`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
    indicators: (indicators: string[], recordId) =&amp;gt; `Показатели ${indicators.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} отсутствуют в записи с ID ${recordId}`,&lt;br /&gt;
    emptyIndicatorsProps: (props: Record&amp;lt;string, string[]&amp;gt;, recordId) =&amp;gt; {&lt;br /&gt;
        const propsString = Object.keys(props).map(prop =&amp;gt; !isEmptyArray(props[prop]) ? `${prop}: ${props[prop].map(p =&amp;gt; `&amp;quot;${p}&amp;quot;`).join(&#039;, &#039;)}` : &#039;&#039;).join(&#039;; &#039;)&lt;br /&gt;
        return `В записи с Id ${recordId} отсутствуют либо не заполнены следующие поля у показателей: ${propsString}`&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    indicatorsError: (recordId) =&amp;gt;{&lt;br /&gt;
        throw new Error(messages.emptyIndicators(recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFoundIndicators: (indicators: string[], recordId: number) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.indicators(indicators, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFilledIndicatorProps: (props: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyIndicatorsProps(props, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(arr) &amp;amp;&amp;amp; arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasIndicators = (journalRecordManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
    return Array.isArray(indicators) &amp;amp;&amp;amp; indicators.length &amp;gt; 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFoundIndicators = (indicators: JournalRecordIndicatorResponse[], indicatorsList = config.indicatorsList) =&amp;gt; {&lt;br /&gt;
    return indicatorsList.filter(ind =&amp;gt; indicators.every(ljInd =&amp;gt; ljInd?.indicator?.name?.toLowerCase() !== ind.toLocaleLowerCase()))&lt;br /&gt;
} &lt;br /&gt;
&lt;br /&gt;
const getNotFilledResultsFields = (indicators: JournalRecordIndicatorResponse[]): Record&amp;lt;string, string[]&amp;gt; =&amp;gt; {&lt;br /&gt;
    return indicators.reduce((acc, indicator) =&amp;gt; {&lt;br /&gt;
        const config = indicatorPropsConfig(indicator)&lt;br /&gt;
        return {&lt;br /&gt;
            ...acc,&lt;br /&gt;
            [indicator?.indicator?.name]: Object.keys(config).reduce((acc, prop) =&amp;gt; {&lt;br /&gt;
                if (!config[prop].get()) return [...acc, config[prop].label]&lt;br /&gt;
                return acc&lt;br /&gt;
            }, [] as string[])&lt;br /&gt;
        }&lt;br /&gt;
    }, {}) as Record&amp;lt;string, string[]&amp;gt;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFilledProps = (indicatorsProps: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
    return Object.keys(indicatorsProps).some(indicator =&amp;gt; !isEmptyArray(indicatorsProps[indicator]))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFoundIndicators = (indicators: string[]) =&amp;gt; !isEmptyArray(indicators)&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (!hasIndicators(journalRecordManager)) {&lt;br /&gt;
            errorHandlers.indicatorsError(journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
&lt;br /&gt;
        const notFoundIndicators = getNotFoundIndicators(indicators)&lt;br /&gt;
        if (hasNotFoundIndicators(notFoundIndicators)) { &lt;br /&gt;
            errorHandlers.notFoundIndicators(notFoundIndicators, journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const emptyIndicatorProps = getNotFilledResultsFields(indicators)&lt;br /&gt;
        if (hasNotFilledProps(emptyIndicatorProps)) {&lt;br /&gt;
            errorHandlers.notFilledIndicatorProps(emptyIndicatorProps)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        showSuccessMessage(messages.success, socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-record-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-sample-moving.png|frame]]&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-user-rights.png|frame]]&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Fill-record-fields.png|frame]]&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Copy-journal-record.png|frame]]&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2359</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2359"/>
		<updated>2026-04-02T01:06:43Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Проверка даты регистрации. check-registration-date */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { MessageBoxResults } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    dateField: &#039;Дата регистрации&#039;,&lt;br /&gt;
    expirationLimit: 10,&lt;br /&gt;
    journalRecordId: 201, // Лаб. воздуха / Анализы завершены&lt;br /&gt;
    integrationResult: { message: &#039;Срок регистрации пробы не истёк !&#039;}&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    success: (recordId) =&amp;gt; `Срок регистрации пробы не истёк ! Id записи ${recordId}`,&lt;br /&gt;
    expired: (limit, recordId) =&amp;gt; `Срок регистрации пробы истёк!\nПрошло более ${ limit } дней с момента регистрации. Id записи: ${recordId}`,&lt;br /&gt;
    dateField: (dateField, stageName, journalRecordId) =&amp;gt; [&lt;br /&gt;
        `Не заполнено либо отсутствует поле &amp;quot;${dateField}&amp;quot;`,&lt;br /&gt;
        `в этапе &amp;quot;${stageName || &#039;&#039;}&amp;quot;.\nId записи: ${journalRecordId}`&lt;br /&gt;
    ].join(&#039; &#039;)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    dateError: (dateField, stageName, journalRecordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.dateField(dateField, stageName, journalRecordId))&lt;br /&gt;
    },&lt;br /&gt;
    expirationError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.expired(config.expirationLimit, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getDaysByDate = (date: Date) =&amp;gt; {&lt;br /&gt;
    const dayMs = 60 * 60 * 24 * 1000&lt;br /&gt;
    const timestamp = date.getTime()&lt;br /&gt;
    return timestamp/dayMs&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getExpirationOffset = (registrationDate: string) =&amp;gt; {&lt;br /&gt;
    const regDate = new Date(registrationDate)&lt;br /&gt;
    const today = new Date()&lt;br /&gt;
    return getDaysByDate(today) - getDaysByDate(regDate)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const date = journalRecordManager.getFieldValue(config.dateField)&lt;br /&gt;
&lt;br /&gt;
        if (!date) errorHandlers.dateError(config.dateField, journalRecordManager?.data?.stage?.stage?.name, config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        const expirationOffset = getExpirationOffset(date as string)&lt;br /&gt;
&lt;br /&gt;
        if (expirationOffset &amp;gt;= config.expirationLimit) errorHandlers.expirationError()&lt;br /&gt;
&lt;br /&gt;
        await showSuccessMessage(messages.success(config.journalRecordId), socketId)&lt;br /&gt;
&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordIndicatorResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
type IndicatorProp = { label: string, get: () =&amp;gt; unknown }&lt;br /&gt;
type IndicatorPropsConfig = Record&amp;lt;string, IndicatorProp&amp;gt;&lt;br /&gt;
&lt;br /&gt;
const indicatorPropsConfig = (indicator): IndicatorPropsConfig =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        executors: {&lt;br /&gt;
            label: &#039;Исполнители&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.executors&lt;br /&gt;
        },&lt;br /&gt;
        methodic: {&lt;br /&gt;
            label: &#039;Методики&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.methodics&lt;br /&gt;
        },&lt;br /&gt;
        averageResult: {&lt;br /&gt;
            label: &#039;Средний результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageValue&lt;br /&gt;
        },&lt;br /&gt;
        averageRoundedValue: {&lt;br /&gt;
            label: &#039;Средний округлённый результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageRoundedValue&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Место отбора&#039;,&lt;br /&gt;
        &#039;Дата регистрации&#039;,&lt;br /&gt;
        &#039;Шифр пробы&#039;,&lt;br /&gt;
        &#039;Время регистрации&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    indicatorsList: [&lt;br /&gt;
        &#039;Аммоний-ион&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144, // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyIndicators: (recordId) =&amp;gt; `в записи с Id ${recordId} отсутствуют показатели`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
    indicators: (indicators: string[], recordId) =&amp;gt; `Показатели ${indicators.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} отсутствуют в записи с ID ${recordId}`,&lt;br /&gt;
    emptyIndicatorsProps: (props: Record&amp;lt;string, string[]&amp;gt;, recordId) =&amp;gt; {&lt;br /&gt;
        const propsString = Object.keys(props).map(prop =&amp;gt; !isEmptyArray(props[prop]) ? `${prop}: ${props[prop].map(p =&amp;gt; `&amp;quot;${p}&amp;quot;`).join(&#039;, &#039;)}` : &#039;&#039;).join(&#039;; &#039;)&lt;br /&gt;
        return `В записи с Id ${recordId} отсутствуют либо не заполнены следующие поля у показателей: ${propsString}`&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    indicatorsError: (recordId) =&amp;gt;{&lt;br /&gt;
        throw new Error(messages.emptyIndicators(recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFoundIndicators: (indicators: string[], recordId: number) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.indicators(indicators, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFilledIndicatorProps: (props: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyIndicatorsProps(props, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(arr) &amp;amp;&amp;amp; arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasIndicators = (journalRecordManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
    return Array.isArray(indicators) &amp;amp;&amp;amp; indicators.length &amp;gt; 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFoundIndicators = (indicators: JournalRecordIndicatorResponse[], indicatorsList = config.indicatorsList) =&amp;gt; {&lt;br /&gt;
    return indicatorsList.filter(ind =&amp;gt; indicators.every(ljInd =&amp;gt; ljInd?.indicator?.name?.toLowerCase() !== ind.toLocaleLowerCase()))&lt;br /&gt;
} &lt;br /&gt;
&lt;br /&gt;
const getNotFilledResultsFields = (indicators: JournalRecordIndicatorResponse[]): Record&amp;lt;string, string[]&amp;gt; =&amp;gt; {&lt;br /&gt;
    return indicators.reduce((acc, indicator) =&amp;gt; {&lt;br /&gt;
        const config = indicatorPropsConfig(indicator)&lt;br /&gt;
        return {&lt;br /&gt;
            ...acc,&lt;br /&gt;
            [indicator?.indicator?.name]: Object.keys(config).reduce((acc, prop) =&amp;gt; {&lt;br /&gt;
                if (!config[prop].get()) return [...acc, config[prop].label]&lt;br /&gt;
                return acc&lt;br /&gt;
            }, [] as string[])&lt;br /&gt;
        }&lt;br /&gt;
    }, {}) as Record&amp;lt;string, string[]&amp;gt;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFilledProps = (indicatorsProps: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
    return Object.keys(indicatorsProps).some(indicator =&amp;gt; !isEmptyArray(indicatorsProps[indicator]))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFoundIndicators = (indicators: string[]) =&amp;gt; !isEmptyArray(indicators)&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (!hasIndicators(journalRecordManager)) {&lt;br /&gt;
            errorHandlers.indicatorsError(journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
&lt;br /&gt;
        const notFoundIndicators = getNotFoundIndicators(indicators)&lt;br /&gt;
        if (hasNotFoundIndicators(notFoundIndicators)) { &lt;br /&gt;
            errorHandlers.notFoundIndicators(notFoundIndicators, journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const emptyIndicatorProps = getNotFilledResultsFields(indicators)&lt;br /&gt;
        if (hasNotFilledProps(emptyIndicatorProps)) {&lt;br /&gt;
            errorHandlers.notFilledIndicatorProps(emptyIndicatorProps)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        showSuccessMessage(messages.success, socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-record-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-sample-moving.png|frame]]&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-user-rights.png|frame]]&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Fill-record-fields.png|frame]]&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Copy-journal-record.png|frame]]&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2358</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2358"/>
		<updated>2026-04-02T01:05:54Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Проверка даты регистрации. check-registration-date */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { MessageBoxResults } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    dateField: &#039;Дата регистрации&#039;,&lt;br /&gt;
    expirationLimit: 10,&lt;br /&gt;
    journalRecordId: 201, // Лаб. воздуха / Анализы завершены&lt;br /&gt;
    integrationResult: { message: &#039;Срок регистрации пробы не истёк !&#039;}&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    success: (recordId) =&amp;gt; `Срок регистрации пробы не истёк ! Id записи ${recordId}`,&lt;br /&gt;
    expired: (limit, recordId) =&amp;gt; `Срок регистрации пробы истёк!\nПрошло более ${ limit } дней с момента регистрации. Id записи: ${recordId}`,&lt;br /&gt;
    dateField: (dateField, stageName, journalRecordId) =&amp;gt; [&lt;br /&gt;
        `Не заполнено либо отсутствует поле &amp;quot;${dateField}&amp;quot;`,&lt;br /&gt;
        `в этапе &amp;quot;${stageName || &#039;&#039;}&amp;quot;.\nId записи: ${journalRecordId}`&lt;br /&gt;
    ].join(&#039; &#039;)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    dateError: (dateField, stageName, journalRecordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.dateField(dateField, stageName, journalRecordId))&lt;br /&gt;
    },&lt;br /&gt;
    expirationError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.expired(config.expirationLimit, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getDaysByDate = (date: Date) =&amp;gt; {&lt;br /&gt;
    const dayMs = 60 * 60 * 24 * 1000&lt;br /&gt;
    const timestamp = date.getTime()&lt;br /&gt;
    return timestamp/dayMs&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getExpirationOffset = (registrationDate: string) =&amp;gt; {&lt;br /&gt;
    const regDate = new Date(registrationDate)&lt;br /&gt;
    const today = new Date()&lt;br /&gt;
    return getDaysByDate(today) - getDaysByDate(regDate)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const date = journalRecordManager.getFieldValue(config.dateField)&lt;br /&gt;
&lt;br /&gt;
        if (!date) errorHandlers.dateError(config.dateField, journalRecordManager?.data?.stage?.stage?.name, config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        const expirationOffset = getExpirationOffset(date as string)&lt;br /&gt;
&lt;br /&gt;
        if (expirationOffset &amp;gt;= config.expirationLimit) errorHandlers.expirationError()&lt;br /&gt;
&lt;br /&gt;
        await showSuccessMessage(messages.success(config.journalRecordId), socketId)&lt;br /&gt;
&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordIndicatorResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
type IndicatorProp = { label: string, get: () =&amp;gt; unknown }&lt;br /&gt;
type IndicatorPropsConfig = Record&amp;lt;string, IndicatorProp&amp;gt;&lt;br /&gt;
&lt;br /&gt;
const indicatorPropsConfig = (indicator): IndicatorPropsConfig =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        executors: {&lt;br /&gt;
            label: &#039;Исполнители&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.executors&lt;br /&gt;
        },&lt;br /&gt;
        methodic: {&lt;br /&gt;
            label: &#039;Методики&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.methodics&lt;br /&gt;
        },&lt;br /&gt;
        averageResult: {&lt;br /&gt;
            label: &#039;Средний результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageValue&lt;br /&gt;
        },&lt;br /&gt;
        averageRoundedValue: {&lt;br /&gt;
            label: &#039;Средний округлённый результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageRoundedValue&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Место отбора&#039;,&lt;br /&gt;
        &#039;Дата регистрации&#039;,&lt;br /&gt;
        &#039;Шифр пробы&#039;,&lt;br /&gt;
        &#039;Время регистрации&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    indicatorsList: [&lt;br /&gt;
        &#039;Аммоний-ион&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144, // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyIndicators: (recordId) =&amp;gt; `в записи с Id ${recordId} отсутствуют показатели`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
    indicators: (indicators: string[], recordId) =&amp;gt; `Показатели ${indicators.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} отсутствуют в записи с ID ${recordId}`,&lt;br /&gt;
    emptyIndicatorsProps: (props: Record&amp;lt;string, string[]&amp;gt;, recordId) =&amp;gt; {&lt;br /&gt;
        const propsString = Object.keys(props).map(prop =&amp;gt; !isEmptyArray(props[prop]) ? `${prop}: ${props[prop].map(p =&amp;gt; `&amp;quot;${p}&amp;quot;`).join(&#039;, &#039;)}` : &#039;&#039;).join(&#039;; &#039;)&lt;br /&gt;
        return `В записи с Id ${recordId} отсутствуют либо не заполнены следующие поля у показателей: ${propsString}`&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    indicatorsError: (recordId) =&amp;gt;{&lt;br /&gt;
        throw new Error(messages.emptyIndicators(recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFoundIndicators: (indicators: string[], recordId: number) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.indicators(indicators, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFilledIndicatorProps: (props: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyIndicatorsProps(props, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(arr) &amp;amp;&amp;amp; arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasIndicators = (journalRecordManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
    return Array.isArray(indicators) &amp;amp;&amp;amp; indicators.length &amp;gt; 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFoundIndicators = (indicators: JournalRecordIndicatorResponse[], indicatorsList = config.indicatorsList) =&amp;gt; {&lt;br /&gt;
    return indicatorsList.filter(ind =&amp;gt; indicators.every(ljInd =&amp;gt; ljInd?.indicator?.name?.toLowerCase() !== ind.toLocaleLowerCase()))&lt;br /&gt;
} &lt;br /&gt;
&lt;br /&gt;
const getNotFilledResultsFields = (indicators: JournalRecordIndicatorResponse[]): Record&amp;lt;string, string[]&amp;gt; =&amp;gt; {&lt;br /&gt;
    return indicators.reduce((acc, indicator) =&amp;gt; {&lt;br /&gt;
        const config = indicatorPropsConfig(indicator)&lt;br /&gt;
        return {&lt;br /&gt;
            ...acc,&lt;br /&gt;
            [indicator?.indicator?.name]: Object.keys(config).reduce((acc, prop) =&amp;gt; {&lt;br /&gt;
                if (!config[prop].get()) return [...acc, config[prop].label]&lt;br /&gt;
                return acc&lt;br /&gt;
            }, [] as string[])&lt;br /&gt;
        }&lt;br /&gt;
    }, {}) as Record&amp;lt;string, string[]&amp;gt;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFilledProps = (indicatorsProps: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
    return Object.keys(indicatorsProps).some(indicator =&amp;gt; !isEmptyArray(indicatorsProps[indicator]))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFoundIndicators = (indicators: string[]) =&amp;gt; !isEmptyArray(indicators)&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (!hasIndicators(journalRecordManager)) {&lt;br /&gt;
            errorHandlers.indicatorsError(journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
&lt;br /&gt;
        const notFoundIndicators = getNotFoundIndicators(indicators)&lt;br /&gt;
        if (hasNotFoundIndicators(notFoundIndicators)) { &lt;br /&gt;
            errorHandlers.notFoundIndicators(notFoundIndicators, journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const emptyIndicatorProps = getNotFilledResultsFields(indicators)&lt;br /&gt;
        if (hasNotFilledProps(emptyIndicatorProps)) {&lt;br /&gt;
            errorHandlers.notFilledIndicatorProps(emptyIndicatorProps)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        showSuccessMessage(messages.success, socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-record-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-sample-moving.png|frame]]&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-user-rights.png|frame]]&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Fill-record-fields.png|frame]]&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Copy-journal-record.png|frame]]&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2348</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2348"/>
		<updated>2026-04-01T09:53:24Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Проверка заполненности полей показателей. check-indicator-fields-filled */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { MessageBoxResults } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    dateField: &#039;Дата регистрации&#039;,&lt;br /&gt;
    expirationLimit: 10,&lt;br /&gt;
    journalRecordId: 201, // Лаб. воздуха / Анализы завершены&lt;br /&gt;
    integrationResult: { message: &#039;Срок регистрации пробы не истёк !&#039;}&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    success: (recordId) =&amp;gt; `Срок регистрации пробы не истёк ! Id записи ${recordId}`,&lt;br /&gt;
    expired: (limit, recordId) =&amp;gt; `Срок регистрации пробы истёк!\nПрошло более ${ limit } дней с момента регистрации. Id записи: ${recordId}`,&lt;br /&gt;
    dateField: (dateField, stageName, journalRecordId) =&amp;gt; [&lt;br /&gt;
        `Не заполнено либо отсутствует поле &amp;quot;${dateField}&amp;quot;`,&lt;br /&gt;
        `в этапе &amp;quot;${stageName || &#039;&#039;}&amp;quot;.\nId записи: ${journalRecordId}`&lt;br /&gt;
    ].join(&#039; &#039;)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    dateError: (dateField, stageName, journalRecordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.dateField(dateField, stageName, journalRecordId))&lt;br /&gt;
    },&lt;br /&gt;
    expirationError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.expired(config.expirationLimit, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getDaysByDate = (date: Date) =&amp;gt; {&lt;br /&gt;
    const dayMs = 60 * 60 * 24 * 1000&lt;br /&gt;
    const timestamp = date.getTime()&lt;br /&gt;
    return timestamp/dayMs&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getExpirationOffset = (registrationDate: string) =&amp;gt; {&lt;br /&gt;
    const regDate = new Date(registrationDate)&lt;br /&gt;
    const today = new Date()&lt;br /&gt;
    return getDaysByDate(today) - getDaysByDate(regDate)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const date = journalRecordManager.getFieldValue(config.dateField)&lt;br /&gt;
&lt;br /&gt;
        if (!date) errorHandlers.dateError(config.dateField, journalRecordManager?.data?.stage?.stage?.name, config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        const expirationOffset = getExpirationOffset(date as string)&lt;br /&gt;
&lt;br /&gt;
        if (expirationOffset &amp;gt;= config.expirationLimit) errorHandlers.expirationError()&lt;br /&gt;
&lt;br /&gt;
        await showSuccessMessage(messages.success(config.journalRecordId), socketId)&lt;br /&gt;
&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import type { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import type { JournalRecordIndicatorResponse } from &amp;quot;../../src/servivces/worker/api/controllers/journal-records&amp;quot;&lt;br /&gt;
import type { IJournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors/JournalRecordManager/interface&amp;quot;&lt;br /&gt;
&lt;br /&gt;
type IndicatorProp = { label: string, get: () =&amp;gt; unknown }&lt;br /&gt;
type IndicatorPropsConfig = Record&amp;lt;string, IndicatorProp&amp;gt;&lt;br /&gt;
&lt;br /&gt;
const indicatorPropsConfig = (indicator): IndicatorPropsConfig =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        executors: {&lt;br /&gt;
            label: &#039;Исполнители&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.executors&lt;br /&gt;
        },&lt;br /&gt;
        methodic: {&lt;br /&gt;
            label: &#039;Методики&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.methodics&lt;br /&gt;
        },&lt;br /&gt;
        averageResult: {&lt;br /&gt;
            label: &#039;Средний результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageValue&lt;br /&gt;
        },&lt;br /&gt;
        averageRoundedValue: {&lt;br /&gt;
            label: &#039;Средний округлённый результат&#039;, &lt;br /&gt;
            get: () =&amp;gt; indicator?.result?.averageRoundedValue&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    requiredFields: [&lt;br /&gt;
        &#039;Источник&#039;,&lt;br /&gt;
        &#039;Место отбора&#039;,&lt;br /&gt;
        &#039;Дата регистрации&#039;,&lt;br /&gt;
        &#039;Шифр пробы&#039;,&lt;br /&gt;
        &#039;Время регистрации&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    indicatorsList: [&lt;br /&gt;
        &#039;Аммоний-ион&#039;&lt;br /&gt;
    ],&lt;br /&gt;
    journalRecordId: 144, // Мк / Тест_Ушакова&lt;br /&gt;
    integrationResult: { message: &#039;Все поля заполнены!&#039; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    success: &#039;Все необходимые поля заполнены!&#039;,&lt;br /&gt;
    emptyIndicators: (recordId) =&amp;gt; `в записи с Id ${recordId} отсутствуют показатели`,&lt;br /&gt;
    emptyFieldsError: (fields, recordId) =&amp;gt; `Поля ${fields.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} не заполнены в записи с ID ${recordId}`,&lt;br /&gt;
    indicators: (indicators: string[], recordId) =&amp;gt; `Показатели ${indicators.map(f =&amp;gt; `&amp;quot;${f}&amp;quot;`).join(&#039;, &#039;)} отсутствуют в записи с ID ${recordId}`,&lt;br /&gt;
    emptyIndicatorsProps: (props: Record&amp;lt;string, string[]&amp;gt;, recordId) =&amp;gt; {&lt;br /&gt;
        const propsString = Object.keys(props).map(prop =&amp;gt; !isEmptyArray(props[prop]) ? `${prop}: ${props[prop].map(p =&amp;gt; `&amp;quot;${p}&amp;quot;`).join(&#039;, &#039;)}` : &#039;&#039;).join(&#039;; &#039;)&lt;br /&gt;
        return `В записи с Id ${recordId} отсутствуют либо не заполнены следующие поля у показателей: ${propsString}`&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    fieldsError: (emptyFields, recordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    indicatorsError: (recordId) =&amp;gt;{&lt;br /&gt;
        throw new Error(messages.emptyIndicators(recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFoundIndicators: (indicators: string[], recordId: number) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.indicators(indicators, recordId))&lt;br /&gt;
    },&lt;br /&gt;
    notFilledIndicatorProps: (props: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.emptyIndicatorsProps(props, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
const isEmptyArray = (arr: unknown[]) =&amp;gt; {&lt;br /&gt;
    return Array.isArray(arr) &amp;amp;&amp;amp; arr.length === 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasIndicators = (journalRecordManager: IJournalRecordManager) =&amp;gt; {&lt;br /&gt;
    const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
    return Array.isArray(indicators) &amp;amp;&amp;amp; indicators.length &amp;gt; 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getNotFoundIndicators = (indicators: JournalRecordIndicatorResponse[], indicatorsList = config.indicatorsList) =&amp;gt; {&lt;br /&gt;
    return indicatorsList.filter(ind =&amp;gt; indicators.every(ljInd =&amp;gt; ljInd?.indicator?.name?.toLowerCase() !== ind.toLocaleLowerCase()))&lt;br /&gt;
} &lt;br /&gt;
&lt;br /&gt;
const getNotFilledResultsFields = (indicators: JournalRecordIndicatorResponse[]): Record&amp;lt;string, string[]&amp;gt; =&amp;gt; {&lt;br /&gt;
    return indicators.reduce((acc, indicator) =&amp;gt; {&lt;br /&gt;
        const config = indicatorPropsConfig(indicator)&lt;br /&gt;
        return {&lt;br /&gt;
            ...acc,&lt;br /&gt;
            [indicator?.indicator?.name]: Object.keys(config).reduce((acc, prop) =&amp;gt; {&lt;br /&gt;
                if (!config[prop].get()) return [...acc, config[prop].label]&lt;br /&gt;
                return acc&lt;br /&gt;
            }, [] as string[])&lt;br /&gt;
        }&lt;br /&gt;
    }, {}) as Record&amp;lt;string, string[]&amp;gt;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFilledProps = (indicatorsProps: Record&amp;lt;string, string[]&amp;gt;) =&amp;gt; {&lt;br /&gt;
    return Object.keys(indicatorsProps).some(indicator =&amp;gt; !isEmptyArray(indicatorsProps[indicator]))&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const hasNotFoundIndicators = (indicators: string[]) =&amp;gt; !isEmptyArray(indicators)&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        if (!hasIndicators(journalRecordManager)) {&lt;br /&gt;
            errorHandlers.indicatorsError(journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const indicators = journalRecordManager?.data?.indicators&lt;br /&gt;
&lt;br /&gt;
        const notFoundIndicators = getNotFoundIndicators(indicators)&lt;br /&gt;
        if (hasNotFoundIndicators(notFoundIndicators)) { &lt;br /&gt;
            errorHandlers.notFoundIndicators(notFoundIndicators, journalRecordManager.journalRecordId)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const emptyIndicatorProps = getNotFilledResultsFields(indicators)&lt;br /&gt;
        if (hasNotFilledProps(emptyIndicatorProps)) {&lt;br /&gt;
            errorHandlers.notFilledIndicatorProps(emptyIndicatorProps)&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        showSuccessMessage(messages.success, socketId)&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-record-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-sample-moving.png|frame]]&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-user-rights.png|frame]]&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Fill-record-fields.png|frame]]&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Copy-journal-record.png|frame]]&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2347</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2347"/>
		<updated>2026-04-01T09:52:11Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Проверка даты регистрации. check-registration-date */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
import { JournalRecordManager } from &amp;quot;../../src/servivces/worker/api/interactors&amp;quot;&lt;br /&gt;
import { MessageBoxResults } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
import { getDataFromDialog } from &amp;quot;../../src/gui/api&amp;quot;&lt;br /&gt;
import { DialogType } from &amp;quot;@triteia/types-integration-gui&amp;quot;&lt;br /&gt;
&lt;br /&gt;
const config = {&lt;br /&gt;
    dateField: &#039;Дата регистрации&#039;,&lt;br /&gt;
    expirationLimit: 10,&lt;br /&gt;
    journalRecordId: 201, // Лаб. воздуха / Анализы завершены&lt;br /&gt;
    integrationResult: { message: &#039;Срок регистрации пробы не истёк !&#039;}&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const messages = {&lt;br /&gt;
    queryParamsNotFound: &#039;Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно&#039;,&lt;br /&gt;
    socketNotFound: &#039;Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно&#039;,&lt;br /&gt;
    success: (recordId) =&amp;gt; `Срок регистрации пробы не истёк ! Id записи ${recordId}`,&lt;br /&gt;
    expired: (limit, recordId) =&amp;gt; `Срок регистрации пробы истёк!\nПрошло более ${ limit } дней с момента регистрации. Id записи: ${recordId}`,&lt;br /&gt;
    dateField: (dateField, stageName, journalRecordId) =&amp;gt; [&lt;br /&gt;
        `Не заполнено либо отсутствует поле &amp;quot;${dateField}&amp;quot;`,&lt;br /&gt;
        `в этапе &amp;quot;${stageName || &#039;&#039;}&amp;quot;.\nId записи: ${journalRecordId}`&lt;br /&gt;
    ].join(&#039; &#039;)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const errorHandlers = {&lt;br /&gt;
    queryParamsError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.queryParamsNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    socketError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.socketNotFound)&lt;br /&gt;
    },&lt;br /&gt;
    dateError: (dateField, stageName, journalRecordId) =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.dateField(dateField, stageName, journalRecordId))&lt;br /&gt;
    },&lt;br /&gt;
    expirationError: () =&amp;gt; {&lt;br /&gt;
        throw new Error(messages.expired(config.expirationLimit, config.journalRecordId))&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getSocketId = (queryParams: any) =&amp;gt; {&lt;br /&gt;
    const socketId = Array.isArray(queryParams?.socketId) &amp;amp;&amp;amp; queryParams?.socketId?.length ? queryParams?.socketId[0] : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!queryParams) errorHandlers.queryParamsError()&lt;br /&gt;
    if (!socketId) errorHandlers.socketError()&lt;br /&gt;
  &lt;br /&gt;
    return socketId&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getDaysByDate = (date: Date) =&amp;gt; {&lt;br /&gt;
    const dayMs = 60 * 60 * 24 * 1000&lt;br /&gt;
    const timestamp = date.getTime()&lt;br /&gt;
    return timestamp/dayMs&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getConfig = (type: DialogType, title: string, message) =&amp;gt; {&lt;br /&gt;
    return {&lt;br /&gt;
        type,&lt;br /&gt;
        config: {&lt;br /&gt;
            title,&lt;br /&gt;
            message&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const getExpirationOffset = (registrationDate: string) =&amp;gt; {&lt;br /&gt;
    const regDate = new Date(registrationDate)&lt;br /&gt;
    const today = new Date()&lt;br /&gt;
    return getDaysByDate(today) - getDaysByDate(regDate)&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const showErrorMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Ошибка&#039;, message), socketId);&lt;br /&gt;
const showSuccessMessage = async (message: string, socketId) =&amp;gt; getDataFromDialog(getConfig(&#039;confirm&#039;, &#039;Внимание&#039;, message), socketId);&lt;br /&gt;
&lt;br /&gt;
async function main({ queryParams }) {&lt;br /&gt;
&lt;br /&gt;
    const socketId = getSocketId(queryParams)&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)&lt;br /&gt;
        const date = journalRecordManager.getFieldValue(config.dateField)&lt;br /&gt;
&lt;br /&gt;
        if (!date) errorHandlers.dateError(config.dateField, journalRecordManager?.data?.stage?.stage?.name, config.journalRecordId)&lt;br /&gt;
&lt;br /&gt;
        const expirationOffset = getExpirationOffset(date as string)&lt;br /&gt;
&lt;br /&gt;
        if (expirationOffset &amp;gt;= config.expirationLimit) errorHandlers.expirationError()&lt;br /&gt;
&lt;br /&gt;
        await showSuccessMessage(messages.success(config.journalRecordId), socketId)&lt;br /&gt;
&lt;br /&gt;
        return config.integrationResult&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
        await showErrorMessage(err.message, socketId)&lt;br /&gt;
        return { error: err.message }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
module.exports = { main }&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-record-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-sample-moving.png|frame]]&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-user-rights.png|frame]]&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Fill-record-fields.png|frame]]&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Copy-journal-record.png|frame]]&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2346</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2346"/>
		<updated>2026-04-01T09:28:28Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Проверка заполненности атрибутов записи ЛЖ. check-record-fields-filled */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-record-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-sample-moving.png|frame]]&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-user-rights.png|frame]]&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Fill-record-fields.png|frame]]&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Copy-journal-record.png|frame]]&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2345</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2345"/>
		<updated>2026-04-01T09:25:51Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Деление данных. copy-journal-record */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-sample-moving.png|frame]]&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-user-rights.png|frame]]&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Fill-record-fields.png|frame]]&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Copy-journal-record.png|frame]]&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2344</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2344"/>
		<updated>2026-04-01T09:24:54Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Заполнение атрибутов записи ЛЖ. fill-record-fields */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-sample-moving.png|frame]]&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-user-rights.png|frame]]&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Fill-record-fields.png|frame]]&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2343</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2343"/>
		<updated>2026-04-01T09:23:26Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Проверка прав пользователя. check-user-rights */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-sample-moving.png|frame]]&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-user-rights.png|frame]]&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Check-user-rights.png&amp;diff=2342</id>
		<title>Файл:Check-user-rights.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Check-user-rights.png&amp;diff=2342"/>
		<updated>2026-04-01T09:23:08Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2341</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2341"/>
		<updated>2026-04-01T09:22:36Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Проверка движения пробы. check-sample-moving */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-sample-moving.png|frame]]&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2340</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2340"/>
		<updated>2026-04-01T09:21:59Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Проверка заполненности полей показателей. check-indicator-fields-filled */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:Check-indicator-fields-filled.png|frame]]&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Fill-record-fields.png&amp;diff=2339</id>
		<title>Файл:Fill-record-fields.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Fill-record-fields.png&amp;diff=2339"/>
		<updated>2026-04-01T09:19:55Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Copy-journal-record.png&amp;diff=2338</id>
		<title>Файл:Copy-journal-record.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Copy-journal-record.png&amp;diff=2338"/>
		<updated>2026-04-01T09:19:23Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Check-sample-moving.png&amp;diff=2337</id>
		<title>Файл:Check-sample-moving.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Check-sample-moving.png&amp;diff=2337"/>
		<updated>2026-04-01T09:19:07Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Check-record-fields-filled.png&amp;diff=2336</id>
		<title>Файл:Check-record-fields-filled.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Check-record-fields-filled.png&amp;diff=2336"/>
		<updated>2026-04-01T09:18:54Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Check-indicator-fields-filled.png&amp;diff=2335</id>
		<title>Файл:Check-indicator-fields-filled.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Check-indicator-fields-filled.png&amp;diff=2335"/>
		<updated>2026-04-01T09:18:27Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2334</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2334"/>
		<updated>2026-04-01T09:14:40Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Проверка даты регистрации. check-registration-date */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2333</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2333"/>
		<updated>2026-04-01T09:14:25Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2332</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2332"/>
		<updated>2026-04-01T09:14:06Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Проверка даты регистрации. check-registration-date */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2331</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2331"/>
		<updated>2026-04-01T09:13:46Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Проверка даты регистрации. check-registration-date */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2330</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2330"/>
		<updated>2026-04-01T09:13:01Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: /* Проверка даты регистрации. check-registration-date */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
[[Файл:check-registration-date.png|frame]]&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Check-registration-date.png&amp;diff=2327</id>
		<title>Файл:Check-registration-date.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Check-registration-date.png&amp;diff=2327"/>
		<updated>2026-04-01T09:10:20Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2275</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2275"/>
		<updated>2026-03-31T08:32:41Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2274</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2274"/>
		<updated>2026-03-31T08:11:43Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет заполненность полей показателей (индикаторов) в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.&lt;br /&gt;
&lt;br /&gt;
== Деление данных. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%98%D0%BD%D1%82%D0%B5%D0%B3%D1%80%D0%B0%D1%86%D0%B8%D0%B8_WEB_%D0%9B%D0%98%D0%9C%D0%A1_%D0%A2%D1%80%D0%B8%D1%82%D0%B5%D1%8F&amp;diff=2273</id>
		<title>Интеграции WEB ЛИМС Тритея</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%98%D0%BD%D1%82%D0%B5%D0%B3%D1%80%D0%B0%D1%86%D0%B8%D0%B8_WEB_%D0%9B%D0%98%D0%9C%D0%A1_%D0%A2%D1%80%D0%B8%D1%82%D0%B5%D1%8F&amp;diff=2273"/>
		<updated>2026-03-31T08:03:57Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: Arhidiman переименовал страницу Интеграции WEB ЛИМС Тритея в Аддоны WEB ЛИМС Тритея&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;#перенаправление [[Аддоны WEB ЛИМС Тритея]]&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2272</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2272"/>
		<updated>2026-03-31T08:03:57Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: Arhidiman переименовал страницу Интеграции WEB ЛИМС Тритея в Аддоны WEB ЛИМС Тритея&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет заполненность полей показателей (индикаторов) в записи журнала. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (более одного этапа). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Использует ключевые слова для сопоставления полей, если точные имена не совпадают.&lt;br /&gt;
&lt;br /&gt;
== Копирование записи журнала. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Создаёт копию записи журнала на следующем этапе маршрута. Копирует все атрибуты и данные, корректируя даты и время при необходимости. Возвращает ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2271</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2271"/>
		<updated>2026-03-31T08:03:39Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет заполненность полей показателей (индикаторов) в записи журнала. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (более одного этапа). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Использует ключевые слова для сопоставления полей, если точные имена не совпадают.&lt;br /&gt;
&lt;br /&gt;
== Копирование записи журнала. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Создаёт копию записи журнала на следующем этапе маршрута. Копирует все атрибуты и данные, корректируя даты и время при необходимости. Возвращает ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2270</id>
		<title>Аддоны</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%90%D0%B4%D0%B4%D0%BE%D0%BD%D1%8B&amp;diff=2270"/>
		<updated>2026-03-31T08:02:22Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: Новая страница: «== Описание пронумерованных скриптов в папке test_scripts ==  == Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; == Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количес...»&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Описание пронумерованных скриптов в папке test_scripts ==&lt;br /&gt;
&lt;br /&gt;
== Проверка даты регистрации. &amp;lt;code&amp;gt;check-registration-date&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности полей показателей. &amp;lt;code&amp;gt;check-indicator-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет заполненность полей показателей (индикаторов) в записи журнала. Проверяет наличие определённых показателей (например, &amp;quot;Аммоний-ион&amp;quot;) и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.&lt;br /&gt;
&lt;br /&gt;
== Проверка заполненности атрибутов записи ЛЖ. &amp;lt;code&amp;gt;check-record-fields-filled&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет заполненность заданных полей в записи журнала (например, &amp;quot;Источник&amp;quot;, &amp;quot;Место отбора&amp;quot;, &amp;quot;Дата регистрации&amp;quot; и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.&lt;br /&gt;
&lt;br /&gt;
== Проверка движения пробы. &amp;lt;code&amp;gt;check-sample-moving&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (более одного этапа). Отображает маршрут или сообщает, что движение не происходило.&lt;br /&gt;
&lt;br /&gt;
== Проверка прав пользователя. &amp;lt;code&amp;gt;check-user-rights&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.&lt;br /&gt;
&lt;br /&gt;
== Заполнение атрибутов записи ЛЖ. &amp;lt;code&amp;gt;fill-record-fields&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Использует ключевые слова для сопоставления полей, если точные имена не совпадают.&lt;br /&gt;
&lt;br /&gt;
== Копирование записи журнала. &amp;lt;code&amp;gt;copy-journal-record&amp;lt;/code&amp;gt; ==&lt;br /&gt;
Создаёт копию записи журнала на следующем этапе маршрута. Копирует все атрибуты и данные, корректируя даты и время при необходимости. Возвращает ID новой записи.&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%97%D0%B0%D0%B3%D0%BB%D0%B0%D0%B2%D0%BD%D0%B0%D1%8F_%D1%81%D1%82%D1%80%D0%B0%D0%BD%D0%B8%D1%86%D0%B0&amp;diff=2269</id>
		<title>Заглавная страница</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%97%D0%B0%D0%B3%D0%BB%D0%B0%D0%B2%D0%BD%D0%B0%D1%8F_%D1%81%D1%82%D1%80%D0%B0%D0%BD%D0%B8%D1%86%D0%B0&amp;diff=2269"/>
		<updated>2026-03-31T07:47:13Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==[[Справочник разработчика ЛИС]]==&lt;br /&gt;
&lt;br /&gt;
==[[Руководство по настройке шаблонов WEB ЛИМС Тритея]]==&lt;br /&gt;
&lt;br /&gt;
==[[Основные управляющие функции в дизайнере документов ЛИС 1.5]]==&lt;br /&gt;
&lt;br /&gt;
==[[Интеграции WEB ЛИМС Тритея]]==&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
	<entry>
		<id>https://wiki.chemsoft.ru/index.php?title=%D0%98%D0%BD%D1%82%D0%B5%D0%B3%D1%80%D0%B0%D1%86%D0%B8%D0%B8&amp;diff=2267</id>
		<title>Интеграции</title>
		<link rel="alternate" type="text/html" href="https://wiki.chemsoft.ru/index.php?title=%D0%98%D0%BD%D1%82%D0%B5%D0%B3%D1%80%D0%B0%D1%86%D0%B8%D0%B8&amp;diff=2267"/>
		<updated>2026-03-31T07:36:06Z</updated>

		<summary type="html">&lt;p&gt;Arhidiman: Новая страница: «Описание интеграционных скриптов  WEB ЛИМС»&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Описание интеграционных скриптов  WEB ЛИМС&lt;/div&gt;</summary>
		<author><name>Arhidiman</name></author>
	</entry>
</feed>