Перейти к содержанию

Аддоны: различия между версиями

Материал из Химсофт Вики
м Sidminik переименовал страницу Аддоны WEB ЛИМС Тритея в Аддоны
 
(не показано 13 промежуточных версий 1 участника)
Строка 11: Строка 11:
   </div>
   </div>
  <div class="mw-collapsible-content">
  <div class="mw-collapsible-content">
  <syntaxhighlight lang="typescript">
    <syntaxhighlight lang="typescript">
import { JournalRecordManager } from "../../src/servivces/worker/api/interactors"
import { JournalRecordManager } from "../../src/servivces/worker/api/interactors"
import { MessageBoxResults } from "@triteia/types-integration-gui"
import { MessageBoxResults } from "@triteia/types-integration-gui"
import { getDataFromDialog } from "../../src/gui/api"
import { getDataFromDialog } from "../../src/gui/api"
import { DialogType } from "@triteia/types-integration-gui"
import { DialogType } from "@triteia/types-integration-gui"
import type { FetchApiClient } from "../../src/servivces/worker/api/ApiClient"
import { apiInstance } from "../../src/servivces/worker/api/interactors/ApiInstance"


// Конфигурация аддона:
// - Поле "Дата регистрации" — контрольная точка для расчёта срока
// - expirationLimit: 10 дней — максимальный допустимый срок хранения пробы
// - journalRecordId: 201 — запись из журнала "Лаб. воздуха / Анализы завершены"
const config = {
const config = {
     dateField: 'Дата регистрации',
     dateField: 'Дата регистрации',
     expirationLimit: 10,
     expirationLimit: 10,
     journalRecordId: 201, // Лаб. воздуха / Анализы завершены
     journalRecordId: 201,
     integrationResult: { message: 'Срок регистрации пробы не истёк !'}
     integrationResult: { message: 'Срок регистрации пробы не истёк!' }
}
}


// Сообщения для пользовательских уведомлений и ошибок
const messages = {
const messages = {
     queryParamsNotFound: 'Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно',
     queryParamsNotFound: 'Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно',
     socketNotFound: 'Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно',
     socketNotFound: 'Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно',
     success: (recordId) => `Срок регистрации пробы не истёк ! Id записи ${recordId}`,
    // Успех: информирует пользователя, что срок не вышел
     expired: (limit, recordId) => `Срок регистрации пробы истёк!\nПрошло более ${ limit } дней с момента регистрации. Id записи: ${recordId}`,
     success: (recordId) => `Срок регистрации пробы не истёк! Id записи ${recordId}`,
    // Ошибка: превышен лимит хранения
     expired: (limit, recordId) => `Срок регистрации пробы истёк!\nПрошло более ${limit} дней с момента регистрации. Id записи: ${recordId}`,
    // Ошибка: отсутствует дата в нужном поле
     dateField: (dateField, stageName, journalRecordId) => [
     dateField: (dateField, stageName, journalRecordId) => [
         `Не заполнено либо отсутствует поле "${dateField}"`,
         `Не заполнено либо отсутствует поле "${dateField}"`,
Строка 35: Строка 45:
}
}


// Централизованные обработчики ошибок — обеспечивают единообразие и чистоту основной логики
const errorHandlers = {
const errorHandlers = {
     queryParamsError: () => {
     queryParamsError: () => {
Строка 42: Строка 53:
         throw new Error(messages.socketNotFound)
         throw new Error(messages.socketNotFound)
     },
     },
    // Вызывается, если поле с датой не заполнено
     dateError: (dateField, stageName, journalRecordId) => {
     dateError: (dateField, stageName, journalRecordId) => {
         throw new Error(messages.dateField(dateField, stageName, journalRecordId))
         throw new Error(messages.dateField(dateField, stageName, journalRecordId))
     },
     },
    // Вызывается при превышении срока хранения
     expirationError: () => {
     expirationError: () => {
         throw new Error(messages.expired(config.expirationLimit, config.journalRecordId))
         throw new Error(messages.expired(config.expirationLimit, config.journalRecordId))
Строка 50: Строка 63:
}
}


// Извлекает socketId из queryParams — необходим для отправки диалогов в GUI
const getSocketId = (queryParams: any) => {
const getSocketId = (queryParams: any) => {
     const socketId = Array.isArray(queryParams?.socketId) && queryParams?.socketId?.length ? queryParams?.socketId[0] : '';
     const socketId = Array.isArray(queryParams?.socketId) && queryParams?.socketId?.length ? queryParams?.socketId[0] : '';
Строка 59: Строка 73:
}
}


// Переводит дату в количество дней с начала эпохи (для арифметики дат)
const getDaysByDate = (date: Date) => {
const getDaysByDate = (date: Date) => {
     const dayMs = 60 * 60 * 24 * 1000
     const dayMs = 60 * 60 * 24 * 1000
     const timestamp = date.getTime()
     const timestamp = date.getTime()
     return timestamp/dayMs
     return timestamp / dayMs
}
}


// Формирует конфиг диалога: тип, заголовок и сообщение для отображения в GUI
const getConfig = (type: DialogType, title: string, message) => {
const getConfig = (type: DialogType, title: string, message) => {
     return {
     return {
Строка 75: Строка 91:
}
}


// Рассчитывает, сколько дней прошло с момента регистрации пробы
const getExpirationOffset = (registrationDate: string) => {
const getExpirationOffset = (registrationDate: string) => {
     const regDate = new Date(registrationDate)
     const regDate = new Date(registrationDate)
Строка 81: Строка 98:
}
}


// Показывает пользователю сообщение об ошибке
const showErrorMessage = async (message: string, socketId) => getDataFromDialog(getConfig('confirm', 'Ошибка', message), socketId);
const showErrorMessage = async (message: string, socketId) => getDataFromDialog(getConfig('confirm', 'Ошибка', message), socketId);
// Показывает пользователю уведомление об успешной проверке
const showSuccessMessage = async (message: string, socketId) => getDataFromDialog(getConfig('confirm', 'Внимание', message), socketId);
const showSuccessMessage = async (message: string, socketId) => getDataFromDialog(getConfig('confirm', 'Внимание', message), socketId);


async function main({ queryParams }) {
/**
 
* Основная функция аддона:
* Проверяет, не истёк ли срок хранения пробы (с момента "Дата регистрации").
*
* Логика:
* 1. Получает значение поля "Дата регистрации" из записи журнала.
* 2. Если дата не указана — ошибка.
* 3. Если с даты регистрации прошло >= 10 дней — ошибка.
* 4. Иначе — успех, показываем подтверждение.
*/
async function main({ queryParams, ...apiInstance }) {
     const socketId = getSocketId(queryParams)
     const socketId = getSocketId(queryParams)


     try {
     try {
        // Получаем доступ к данным записи журнала
         const journalRecordManager = await JournalRecordManager(config.journalRecordId)
         const journalRecordManager = await JournalRecordManager(config.journalRecordId)
        // Извлекаем значение поля "Дата регистрации"
         const date = journalRecordManager.getFieldValue(config.dateField)
         const date = journalRecordManager.getFieldValue(config.dateField)


         if (!date) errorHandlers.dateError(config.dateField, journalRecordManager?.data?.stage?.stage?.name, config.journalRecordId)
        // Если дата не заполнена — прерываем с пояснением
         if (!date) {
            errorHandlers.dateError(config.dateField, journalRecordManager?.data?.stage?.stage?.name, config.journalRecordId)
        }


        // Считаем, сколько дней прошло с регистрации
         const expirationOffset = getExpirationOffset(date as string)
         const expirationOffset = getExpirationOffset(date as string)


         if (expirationOffset >= config.expirationLimit) errorHandlers.expirationError()
        // Если срок хранения превышен — ошибка
         if (expirationOffset >= config.expirationLimit) {
            errorHandlers.expirationError()
        }


        // Всё в порядке — информируем пользователя
         await showSuccessMessage(messages.success(config.journalRecordId), socketId)
         await showSuccessMessage(messages.success(config.journalRecordId), socketId)


         return config.integrationResult
         return config.integrationResult
     } catch (err) {
     } catch (err) {
        // При любой ошибке — показываем её в модальном окне
         await showErrorMessage(err.message, socketId)
         await showErrorMessage(err.message, socketId)
         return { error: err.message }
         return { error: err.message }
Строка 109: Строка 148:
module.exports = { main }
module.exports = { main }
     </syntaxhighlight>
     </syntaxhighlight>
  </div>
    </div>
</div>
</div>


Строка 129: Строка 168:
import type { IJournalRecordManager } from "../../src/servivces/worker/api/interactors/JournalRecordManager/interface"
import type { IJournalRecordManager } from "../../src/servivces/worker/api/interactors/JournalRecordManager/interface"


// Описание свойств показателя (индикатора), которые нужно проверить
// Каждое свойство имеет метку (для отображения пользователю) и геттер значения
type IndicatorProp = { label: string, get: () => unknown }
type IndicatorProp = { label: string, get: () => unknown }
type IndicatorPropsConfig = Record<string, IndicatorProp>
type IndicatorPropsConfig = Record<string, IndicatorProp>


// Конфигурация полей, которые проверяются у каждого показателя
// Например: "Исполнители", "Методики", "Средний результат" и т.д.
const indicatorPropsConfig = (indicator): IndicatorPropsConfig => {
const indicatorPropsConfig = (indicator): IndicatorPropsConfig => {
     return {
     return {
Строка 153: Строка 196:
}
}


// Конфигурация аддона:
// - indicatorsList: список обязательных показателей (например, "Аммоний-ион")
// - journalRecordId: 144 — тестовая запись ("Мк / Тест_Ушакова")
const config = {
const config = {
    requiredFields: [
        'Источник',
        'Место отбора',
        'Дата регистрации',
        'Шифр пробы',
        'Время регистрации'
    ],
     indicatorsList: [
     indicatorsList: [
         'Аммоний-ион'
         'Аммоний-ион'
     ],
     ],
     journalRecordId: 144, // Мк / Тест_Ушакова
     journalRecordId: 144,
     integrationResult: { message: 'Все поля заполнены!' }
     integrationResult: { message: 'Все поля заполнены!' }
}
}


// Сообщения для пользовательских уведомлений и ошибок
const messages = {
const messages = {
     socketNotFound: 'Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно',
     socketNotFound: 'Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно',
Строка 175: Строка 215:
     emptyFieldsError: (fields, recordId) => `Поля ${fields.map(f => `"${f}"`).join(', ')} не заполнены в записи с ID ${recordId}`,
     emptyFieldsError: (fields, recordId) => `Поля ${fields.map(f => `"${f}"`).join(', ')} не заполнены в записи с ID ${recordId}`,
     indicators: (indicators: string[], recordId) => `Показатели ${indicators.map(f => `"${f}"`).join(', ')} отсутствуют в записи с ID ${recordId}`,
     indicators: (indicators: string[], recordId) => `Показатели ${indicators.map(f => `"${f}"`).join(', ')} отсутствуют в записи с ID ${recordId}`,
    // Формирует детализированное сообщение о незаполненных полях внутри показателей
     emptyIndicatorsProps: (props: Record<string, string[]>, recordId) => {
     emptyIndicatorsProps: (props: Record<string, string[]>, recordId) => {
         const propsString = Object.keys(props).map(prop => !isEmptyArray(props[prop]) ? `${prop}: ${props[prop].map(p => `"${p}"`).join(', ')}` : '').join('; ')
         const propsString = Object.keys(props).map(prop => !isEmptyArray(props[prop]) ? `${prop}: ${props[prop].map(p => `"${p}"`).join(', ')}` : '').join('; ')
Строка 181: Строка 222:
}
}


// Централизованные обработчики ошибок — обеспечивают единообразие и чистоту основной логики
const errorHandlers = {
const errorHandlers = {
     queryParamsError: () => {
     queryParamsError: () => {
Строка 191: Строка 233:
         throw new Error(messages.socketNotFound)
         throw new Error(messages.socketNotFound)
     },
     },
     indicatorsError: (recordId) =>{
     indicatorsError: (recordId) => {
         throw new Error(messages.emptyIndicators(recordId))
         throw new Error(messages.emptyIndicators(recordId))
     },
     },
Строка 202: Строка 244:
}
}


// Формирует конфиг диалога для отправки в GUI: тип, заголовок и сообщение
const getConfig = (type: DialogType, title: string, message) => {
const getConfig = (type: DialogType, title: string, message) => {
     return {
     return {
Строка 212: Строка 255:
}
}


// Извлекает socketId из queryParams — необходим для связи с интерфейсом
const getSocketId = (queryParams: any) => {
const getSocketId = (queryParams: any) => {
     const socketId = Array.isArray(queryParams?.socketId) && queryParams?.socketId?.length ? queryParams?.socketId[0] : '';
     const socketId = Array.isArray(queryParams?.socketId) && queryParams?.socketId?.length ? queryParams?.socketId[0] : '';
Строка 221: Строка 265:
}
}


// Показывает пользователю модальное окно с ошибкой
const showErrorMessage = async (message: string, socketId) => getDataFromDialog(getConfig('confirm', 'Ошибка', message), socketId);
const showErrorMessage = async (message: string, socketId) => getDataFromDialog(getConfig('confirm', 'Ошибка', message), socketId);
// Показывает уведомление об успешной проверке
const showSuccessMessage = async (message: string, socketId) => getDataFromDialog(getConfig('confirm', 'Внимание', message), socketId);
const showSuccessMessage = async (message: string, socketId) => getDataFromDialog(getConfig('confirm', 'Внимание', message), socketId);


// Универсальная проверка: является ли массив пустым (с учётом типа)
const isEmptyArray = (arr: unknown[]) => {
const isEmptyArray = (arr: unknown[]) => {
     return Array.isArray(arr) && arr.length === 0
     return Array.isArray(arr) && arr.length === 0
}
}


// Проверяет, есть ли у записи хотя бы один показатель
const hasIndicators = (journalRecordManager: IJournalRecordManager) => {
const hasIndicators = (journalRecordManager: IJournalRecordManager) => {
     const indicators = journalRecordManager?.data?.indicators
     const indicators = journalRecordManager?.data?.indicators
Строка 233: Строка 281:
}
}


// Определяет, какие из требуемых показателей отсутствуют в записи
const getNotFoundIndicators = (indicators: JournalRecordIndicatorResponse[], indicatorsList = config.indicatorsList) => {
const getNotFoundIndicators = (indicators: JournalRecordIndicatorResponse[], indicatorsList = config.indicatorsList) => {
     return indicatorsList.filter(ind => indicators.every(ljInd => ljInd?.indicator?.name?.toLowerCase() !== ind.toLocaleLowerCase()))
     return indicatorsList.filter(ind =>  
        indicators.every(ljInd => ljInd?.indicator?.name?.toLowerCase() !== ind.toLowerCase())
    )
}  
}  


// Собирает список незаполненных полей для каждого показателя
// Возвращает объект: { "Аммоний-ион": ["Исполнители", "Средний результат"] }
const getNotFilledResultsFields = (indicators: JournalRecordIndicatorResponse[]): Record<string, string[]> => {
const getNotFilledResultsFields = (indicators: JournalRecordIndicatorResponse[]): Record<string, string[]> => {
     return indicators.reduce((acc, indicator) => {
     return indicators.reduce((acc, indicator) => {
Строка 250: Строка 303:
}
}


// Проверяет, есть ли хотя бы один показатель с незаполненными полями
const hasNotFilledProps = (indicatorsProps: Record<string, string[]>) => {
const hasNotFilledProps = (indicatorsProps: Record<string, string[]>) => {
     return Object.keys(indicatorsProps).some(indicator => !isEmptyArray(indicatorsProps[indicator]))
     return Object.keys(indicatorsProps).some(indicator => !isEmptyArray(indicatorsProps[indicator]))
}
}


// Проверяет, есть ли отсутствующие показатели в списке
const hasNotFoundIndicators = (indicators: string[]) => !isEmptyArray(indicators)
const hasNotFoundIndicators = (indicators: string[]) => !isEmptyArray(indicators)


/**
* Основная функция аддона:
* Проверяет заполненность:
* 1. Наличие указанных показателей (например, "Аммоний-ион").
* 2. Заполненность внутренних полей каждого показателя (исполнители, методики и т.д.).
*
* При обнаружении проблем — прерывает выполнение и показывает детализированную ошибку.
*/
async function main({ queryParams }) {
async function main({ queryParams }) {
     if (!queryParams) errorHandlers.queryParamsError()
     if (!queryParams) errorHandlers.queryParamsError()
Строка 263: Строка 326:
         const journalRecordManager = await JournalRecordManager(config.journalRecordId)
         const journalRecordManager = await JournalRecordManager(config.journalRecordId)


        // Проверка: есть ли вообще показатели в записи?
         if (!hasIndicators(journalRecordManager)) {
         if (!hasIndicators(journalRecordManager)) {
             errorHandlers.indicatorsError(journalRecordManager.journalRecordId)
             errorHandlers.indicatorsError(journalRecordManager.journalRecordId)
Строка 269: Строка 333:
         const indicators = journalRecordManager?.data?.indicators
         const indicators = journalRecordManager?.data?.indicators


        // Проверка: все ли требуемые показатели присутствуют?
         const notFoundIndicators = getNotFoundIndicators(indicators)
         const notFoundIndicators = getNotFoundIndicators(indicators)
         if (hasNotFoundIndicators(notFoundIndicators)) {  
         if (hasNotFoundIndicators(notFoundIndicators)) {  
Строка 274: Строка 339:
         }
         }


        // Проверка: все ли поля у показателей заполнены?
         const emptyIndicatorProps = getNotFilledResultsFields(indicators)
         const emptyIndicatorProps = getNotFilledResultsFields(indicators)
         if (hasNotFilledProps(emptyIndicatorProps)) {
         if (hasNotFilledProps(emptyIndicatorProps)) {
Строка 279: Строка 345:
         }
         }


        // Все проверки пройдены — информируем пользователя
         showSuccessMessage(messages.success, socketId)
         showSuccessMessage(messages.success, socketId)
     } catch (err) {
     } catch (err) {
        // При любой ошибке — показываем её в модальном окне
         await showErrorMessage(err.message, socketId)
         await showErrorMessage(err.message, socketId)
         return { error: err.message }
         return { error: err.message }
Строка 400: Строка 468:
[[Файл:Check-sample-moving.png|frame]]
[[Файл:Check-sample-moving.png|frame]]
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.
Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.
<div class="mw-collapsible mw-collapsed">
  <div style="display: flex; align-items: center; gap: 2px;">
    <strong>Просмотр кода</strong>
    <span class="mw-collapsible-toggle-placeholder"></span>
  </div>
<div class="mw-collapsible-content">
    <syntaxhighlight lang="typescript">
import { getDataFromDialog } from "../../src/gui/api"
import { JournalsRecordRouteController } from "../../src/servivces/worker/api/controllers/journals-record-route"
import { apiInstance } from "../../src/servivces/worker/api/interactors/ApiInstance"
import type { DialogType } from "@triteia/types-integration-gui"
import type { JournalRecordRouteDto } from "../../src/servivces/worker/api/controllers/journal-records"
// Конфигурация аддона:
// - journalRecordId: 114 — запись из журнала "Лаб. воздуха / Анализы завершены"
// Аддон анализирует маршрут движения пробы по этапам ЛЖ
const config = {
    journalRecordId: 114
}
// Сообщения для отображения пользователю
const messages = {
    socketNotFound: 'Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно',
    queryParamsNotFound: 'Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно',
    // Формирует строку с маршрутом пробы: этапы через " -> "
    route: (routes: JournalRecordRouteDto[], recordId: number) =>
        `Маршрут пробы: ${routes.map(r => `"${r.stageName}"`).join(' -> ')}. Id записи ЛЖ: ${recordId}`,
    // Сообщение, если проба не перемещалась между этапами
    notMoved: (recordId: number) =>
        `Движение пробы не происходило. Id записи ЛЖ: ${recordId}`
}
// Обработчики ошибок — централизованно выбрасывают исключения с понятными сообщениями
const errorHandlers = {
    queryParamsError: () => {
        throw new Error(messages.queryParamsNotFound)
    },
    socketError: () => {
        throw new Error(messages.socketNotFound)
    }
}
// Формирует конфиг диалога для GUI: тип (confirm), заголовок и текст
const getConfig = (type: DialogType, title: string, message) => {
    return {
        type,
        config: {
            title,
            message
        }
    }
}
// Извлекает socketId из параметров запроса — необходим для отправки ответа в интерфейс
const getSocketId = (queryParams: any) => {
    const socketId = Array.isArray(queryParams?.socketId) && queryParams?.socketId?.length ? queryParams?.socketId[0] : '';
    if (!queryParams) errorHandlers.queryParamsError()
    if (!socketId) errorHandlers.socketError()
 
    return socketId
}
// Показывает пользователю модальное окно с ошибкой
const showErrorMessage = async (message: string, socketId) => getDataFromDialog(getConfig('confirm', 'Ошибка', message), socketId);
// Показывает информационное сообщение (успех или статус)
const showSuccessMessage = async (message: string, socketId) => getDataFromDialog(getConfig('confirm', 'Внимание', message), socketId);
// Определяет, перемещалась ли проба: если более одного этапа — значит, было движение
const isSampleMoved = (moves: JournalRecordRouteDto[]) => {
    return Array.isArray(moves) && moves.length > 1
}
/**
* Основная функция аддона:
* Получает маршрут пробы по этапам лабораторного журнала и отображает его пользователю.
*
* Логика:
* 1. Проверяет наличие параметров и подключение клиента (socketId).
* 2. Запрашивает историю перемещений пробы через API.
* 3. Если проба прошла более одного этапа — отображает маршрут.
* 4. Если осталась на одном этапе — сообщает, что движения не было.
*/
async function main({ queryParams }) {
    if (!queryParams) errorHandlers.queryParamsError()
    const socketId = getSocketId(queryParams)
    try {
        // Контроллер для работы с маршрутом записи (история этапов)
        const journalsRecordRouteController = new JournalsRecordRouteController(apiInstance)
        // Получаем список этапов, через которые прошла проба
        const sampleMoves = await journalsRecordRouteController.getJournalRecordRoute(config.journalRecordId)
        if (isSampleMoved(sampleMoves)) {
            // Проба перемещалась — показываем полный маршрут
            showSuccessMessage(messages.route(sampleMoves, config.journalRecordId), socketId)
        } else {
            // Проба не покидала исходный этап
            showSuccessMessage(messages.notMoved(config.journalRecordId), socketId)
        }
    } catch (err) {
        // При любой ошибке (сеть, доступ, данные) — показываем её пользователю
        await showErrorMessage(err.message, socketId)
        return { error: err.message }
    }
}
module.exports = { main }
    </syntaxhighlight>
  </div>
</div>


== Проверка прав пользователя. <code>check-user-rights</code> ==
== Проверка прав пользователя. <code>check-user-rights</code> ==
[[Файл:Check-user-rights.png|frame]]
[[Файл:Check-user-rights.png|frame]]
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.
Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.
<div class="mw-collapsible mw-collapsed">
  <div style="display: flex; align-items: center; gap: 2px;">
    <strong>Просмотр кода</strong>
    <span class="mw-collapsible-toggle-placeholder"></span>
  </div>
<div class="mw-collapsible-content">
    <syntaxhighlight lang="typescript">
import { getDataFromDialog } from "../../src/gui/api"
import { apiInstance } from "../../src/servivces/worker/api/interactors/ApiInstance"
import {
    PermissionController,
    PermissionResource,
    PermissionRwAccess
} from "../../src/servivces/worker/api/controllers/permisson"
import { TableBuilder } from "../../src/gui/builder"
import type { DialogType, TableConfig } from "@triteia/types-integration-gui"
import type { JournalRecordRouteDto } from "../../src/servivces/worker/api/controllers/journal-records"
// Отображение типов доступа в понятные пользователю названия
const accessNamesMap = {
    [PermissionRwAccess.EXEC]: 'Выполнение',
    [PermissionRwAccess.READ]: 'Чтение',
    [PermissionRwAccess.WRITE]: 'Запись',
}
// Список ресурсов, права на которые необходимо проверить
const requiredResources = [
    PermissionResource.ANALYSIS_OBJECT,
    PermissionResource.METHODOLOGY,
    PermissionResource.JOURNAL_RECORD
]
// Отображение идентификаторов ресурсов в читаемые названия для отображения в таблице
const resourcesNamesMap = {
    [PermissionResource.ANALYSIS_OBJECT]: 'Объекты анализа',
    [PermissionResource.METHODOLOGY]: 'Методики',
    [PermissionResource.JOURNAL_RECORD]: 'Записи ЛЖ'
}
/**
* Формирует конфигурацию таблицы с правами доступа:
* - Для каждого ресурса проверяются все типы доступа (чтение, запись, выполнение).
* - Возвращает готовую конфигурацию таблицы для отображения в GUI.
*/
const getAccessRightsTable = async (tableBuilder: TableBuilder, permissionController: PermissionController): Promise<TableConfig> => {
    // Получаем список строк: комбинация "ресурс + тип доступа + наличие права"
    const rows = (await Promise.all(requiredResources.map(async (source: PermissionResource) => {
            const sourceCombinations = await Promise.all(Object.keys(PermissionRwAccess).map(async (action) => {
                const access = await permissionController.getSourcePermission(source, PermissionRwAccess[action])
                return {
                    source: resourcesNamesMap[source],
                    action: accessNamesMap[PermissionRwAccess[action]],
                    access: access.hasPermission
                }
            }))
            return sourceCombinations
        } 
    ))).flat()
    // Строим таблицу с помощью TableBuilder
    const table = tableBuilder
        .name('simpleTable')
        .label('Таблица')
        .isAddable(false)          // Нельзя добавлять строки
        .actionsCol(false)          // Скрыть колонку действий
        .addColumn('Ресурс', 'source', false)
        .config('input', { name: 'source', rules: [{ required: true }] }, { placeholder: '', disabled: false })
        .next()
        .addColumn('Тип доступа', 'action', false)
        .config('input', { name: 'action' }, { type: 'string', placeholder: '', disabled: false })
        .next()
        .addColumn('Доступ', 'access', false)
        .config('checkbox', { name: 'access' }, { type: 'boolean', placeholder: '', disabled: true }) // Только для просмотра
        .next()
        .setRows(rows)
        .build();
 
  return table
}
// Сообщения для пользователя
const messages = {
    socketNotFound: 'Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно',
    queryParamsNotFound: 'Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно',
}
// Обработчики ошибок — централизованно выбрасывают исключения
const errorHandlers = {
    queryParamsError: () => {
        throw new Error(messages.queryParamsNotFound)
    },
    socketError: () => {
        throw new Error(messages.socketNotFound)
    }
}
// Формирует конфиг диалога для отправки в GUI
const getConfig = (type: DialogType, title: string, message) => {
    return {
        type,
        config: {
            title,
            message
        }
    }
}
// Извлекает socketId из параметров — необходим для обратной связи с интерфейсом
const getSocketId = (queryParams: any) => {
    const socketId = Array.isArray(queryParams?.socketId) && queryParams?.socketId?.length ? queryParams?.socketId[0] : '';
    if (!queryParams) errorHandlers.queryParamsError()
    if (!socketId) errorHandlers.socketError()
    return socketId
}
// Показывает пользователю сообщение об ошибке
const showErrorMessage = async (message: string, socketId) => getDataFromDialog(getConfig('confirm', 'Ошибка', message), socketId);
// Показывает таблицу с правами доступа в модальном окне
const showTable = async (builder: TableBuilder, controller: PermissionController, socketId: string) =>
    await getDataFromDialog({ type: 'input-data', config: [await getAccessRightsTable(builder, controller)] }, socketId)
/**
* Основная функция аддона:
* Отображает пользователю таблицу с текущими правами доступа на ключевые ресурсы системы.
*
* Логика:
* 1. Проверяет наличие параметров и активное соединение (socketId).
* 2. Запрашивает права для каждого ресурса и типа доступа.
* 3. Формирует таблицу через TableBuilder.
* 4. Показывает её в модальном окне.
*
* Результат: пользователь видит, какие действия разрешены для "Объектов анализа", "Методик" и "Записей ЛЖ".
*/
async function main({ queryParams }) {
    if (!queryParams) errorHandlers.queryParamsError()
    const socketId = getSocketId(queryParams)
    try {
        // Создаём экземпляры билдера и контроллера прав доступа
        await showTable(new TableBuilder(), new PermissionController(apiInstance), socketId)
    } catch (err) {
        // При любой ошибке — показываем её в модальном окне
        await showErrorMessage(err.message, socketId)
        return { error: err.message }
    }
}
module.exports = { main }
    </syntaxhighlight>
  </div>
</div>


== Заполнение атрибутов записи ЛЖ. <code>fill-record-fields</code> ==
== Заполнение атрибутов записи ЛЖ. <code>fill-record-fields</code> ==
[[Файл:Fill-record-fields.png|frame]]
[[Файл:Fill-record-fields.png|frame]]
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.
Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.
<div class="mw-collapsible mw-collapsed">
  <div style="display: flex; align-items: center; gap: 2px;">
    <strong>Просмотр кода</strong>
    <span class="mw-collapsible-toggle-placeholder"></span>
  </div>
<div class="mw-collapsible-content">
    <syntaxhighlight lang="typescript">
import { JournalRecordManager } from "../../src/servivces/worker/api/interactors"
import { getDataFromDialog } from "../../src/gui/api"
import { DialogType } from "@triteia/types-integration-gui"
import { IJournalRecordManager } from "../../src/servivces/worker/api/interactors/JournalRecordManager/interface"
import { JournalRecordAttributeFieldResponse } from "../../src/servivces/worker/api/controllers/lj-eav-attribute-fields"
import { FieldMatch } from "../../src/servivces/worker/api/interactors/JournalRecordManager/interface"
// Текущая дата в формате YYYY-MM-DD — используется как значение по умолчанию для полей с датой
const date = new Date().toISOString().split('T')[0]
// Конфигурация аддона:
// - requiredFields: список полей, которые нужно заполнить
//  Каждое поле содержит:
//    • name — отображаемое имя
//    • value — значение, которое будет установлено
//    • keywords — ключевые слова для поиска соответствующего поля в записи
// - journalRecordId: 118 — тестовая запись ("Мк / 111")
// - Результат при успехе: сообщение о завершении
const config = {
    requiredFields: [
        { name: 'Дата регистрации', value: date, keywords: ['дат', 'регистр'] },
        { name: 'Место отбора', value: 'Скважина 516563Г ', keywords: ['мест', 'отбор'] },
        { name: 'Дата отбора', value: date, keywords: ['дат', 'отбор'] },
        { name: 'Шифр пробы', value: 'shifr 123', keywords: ['дат', 'регистр']},
        { name: 'Список. Целое число', value: 666, keywords: ['спис', 'цел', 'числ'] },
        { name: 'Помещение', value: 'помещение', keywords: ['помещ']},
    ] as FieldMatch[],
    journalRecordId: 118, // Мк / 111
    integrationResult: { message: 'Все поля заполнены!' }
}
// Сообщения для пользователя
const messages = {
    socketNotFound: 'Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно',
    queryParamsNotFound: 'Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно',
    success: (recordId) => `Все необходимые поля заполнены! Id записи: ${recordId}`,
    emptyFieldsError: (fields, recordId) => `Поля ${fields.map(f => `"${f}"`).join(', ')} не заполнены в записи с ID ${recordId}`,
}
// Обработчики ошибок — централизованно выбрасывают исключения
const errorHandlers = {
    queryParamsError: () => {
        throw new Error(messages.queryParamsNotFound)
    },
    socketError: () => {
        throw new Error(messages.socketNotFound)
    }
}
// Формирует конфиг диалога для отправки в GUI: тип, заголовок и сообщение
const getConfig = (type: DialogType, title: string, message) => {
    return {
        type,
        config: {
            title,
            message
        }
    }
}
// Извлекает socketId из параметров — необходим для обратной связи с интерфейсом
const getSocketId = (queryParams: any) => {
    const socketId = Array.isArray(queryParams?.socketId) && queryParams?.socketId?.length ? queryParams?.socketId[0] : '';
    if (!queryParams) errorHandlers.queryParamsError()
    if (!socketId) errorHandlers.socketError()
    return socketId
}
// Показывает пользователю сообщение об ошибке
const showErrorMessage = async (message: string, socketId) => getDataFromDialog(getConfig('confirm', 'Ошибка', message), socketId);
// Показывает уведомление об успешном заполнении
const showSuccessMessage = async (message: string, socketId) => getDataFromDialog(getConfig('confirm', 'Внимание', message), socketId);
// Сопоставляет поле из записи (по имени) с требуемым полем через ключевые слова
// Например: если имя поля содержит "дат" и "регистр" → подходит под "Дата регистрации"
const fieldsMatcher = (field: JournalRecordAttributeFieldResponse, searchField: FieldMatch) => {
    return searchField.keywords.every(k =>
        String(field.name).toLocaleLowerCase().includes(k.toLocaleLowerCase())
    )
}
// Заполняет указанные поля в записи журнала с использованием кастомного механизма сопоставления
const fillFields = async (fields: FieldMatch[], journalRecordsManager: IJournalRecordManager) => {
    await journalRecordsManager.setFieldsValues(fields, journalRecordsManager, fieldsMatcher)
}
/**
* Основная функция аддона:
* Автоматически заполняет заданные поля в записи лабораторного журнала.
*
* Особенности:
* - Поля ищутся не по точному имени, а по ключевым словам (гибкое сопоставление).
* - Поддерживает разные типы значений: строки, числа, даты.
* - Используется в тестовых или демонстрационных целях.
*
* Логика:
* 1. Проверяет наличие параметров и активное соединение.
* 2. Загружает запись журнала.
* 3. Для каждого поля из config.requiredFields:
*    - Находит соответствующее поле в записи по ключевым словам.
*    - Устанавливает указанное значение.
* 4. Сообщает пользователю об успехе.
*/
async function main({ queryParams }) {
    if (!queryParams) errorHandlers.queryParamsError()
    const socketId = getSocketId(queryParams)
    try {
        // Получаем менеджер для работы с записью
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)
        // Заполняем поля по ключевым словам
        await fillFields(config.requiredFields, journalRecordManager)
        // Информируем пользователя о завершении
        showSuccessMessage(messages.success(journalRecordManager.journalRecordId), socketId)
        return config.integrationResult
    } catch (err) {
        // При любой ошибке — показываем её в модальном окне
        await showErrorMessage(err.message, socketId)
        return { error: err.message }
    }
}
module.exports = { main }
    </syntaxhighlight>
  </div>
</div>


== Деление данных. <code>copy-journal-record</code> ==
== Деление данных. <code>copy-journal-record</code> ==
[[Файл:Copy-journal-record.png|frame]]
[[Файл:Copy-journal-record.png|frame]]
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.
Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.
<div class="mw-collapsible mw-collapsed">
  <div style="display: flex; align-items: center; gap: 2px;">
    <strong>Просмотр кода</strong>
    <span class="mw-collapsible-toggle-placeholder"></span>
  </div>
<div class="mw-collapsible-content">
    <syntaxhighlight lang="typescript">
import { JournalRecordManager } from "../../src/servivces/worker/api/interactors"
import { JournalRecordsController } from "../../src/servivces/worker/api/controllers/journal-records"
import { getDataFromDialog } from "../../src/gui/api"
import { apiInstance } from "../../src/servivces/worker/api/interactors/ApiInstance"
import type { DialogType } from "@triteia/types-integration-gui"
import type { JournalRecordRequest, JournalRecordResponse } from "../../src/servivces/worker/api/controllers/journal-records"
import type { JournalRecordAttributeResponse } from "../../src/servivces/worker/api/controllers/journal-records"
// Тип данных поля (DATE, TIME, STRING и т.д.) — используется для корректной обработки значений при копировании
type DataType = JournalRecordAttributeResponse['field']['dataType']
// Конфигурация аддона:
// - journalRecordId: 208 — исходная запись ("Лаб. воздуха / Регистрация")
// - nextJournalRecordStage: 2 — ID этапа, на который нужно скопировать запись ("Анализ")
// - nextStageName: отображаемое имя целевого этапа
// - integrationResult: сообщение при успешном копировании
const config = {
    journalRecordId: 208,  // Лаб. воздуха / Регистрация
    nextJournalRecordStage: 2, // Лаб. воздуха / Анализ
    nextStageName: 'Анализ',
    integrationResult: { message: 'Запись ЛЖ скопирована успешно!' }
}
// Сообщения для пользователя
const messages = {
    socketNotFound: 'Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно',
    queryParamsNotFound: 'Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно',
    success: 'Все необходимые поля заполнены!',
}
// Обработчики ошибок — централизованно выбрасывают исключения
const errorHandlers = {
    queryParamsError: () => {
        throw new Error(messages.queryParamsNotFound)
    },
    socketError: () => {
        throw new Error(messages.socketNotFound)
    }
}
// Формирует конфиг диалога для отправки в GUI: тип, заголовок и сообщение
const getConfig = (type: DialogType, title: string, message) => {
    return {
        type,
        config: {
            title,
            message
        }
    }
}
// Извлекает socketId из параметров — необходим для обратной связи с интерфейсом
const getSocketId = (queryParams: any) => {
    const socketId = Array.isArray(queryParams?.socketId) && queryParams?.socketId?.length ? queryParams?.socketId[0] : '';
    if (!queryParams) errorHandlers.queryParamsError()
    if (!socketId) errorHandlers.socketError()
    return socketId
}
// Показывает пользователю сообщение об ошибке
const showErrorMessage = async (message: string, socketId) => getDataFromDialog(getConfig('confirm', 'Ошибка', message), socketId);
// Показывает уведомление об успешном копировании
const showSuccessMessage = async (message: string, socketId) => getDataFromDialog(getConfig('confirm', 'Внимание', message), socketId);
// Преобразует ISO-дату в строку формата YYYY-MM-DD (без времени)
const getDateString = (ISODate: unknown | null = null) => {
    return new Date(String(ISODate)).toISOString().split('T')[0]
}
// Преобразует ISO-дату в строку формата HH:mm:ss (без миллисекунд)
const getTimeString = (ISODate: unknown | null = null) => {
    return new Date(String(ISODate)).toISOString().split('T')[1].split('.')[0]
}
/**
* Создаёт новую запись лабораторного журнала на основе существующей.
*
* Особенности:
* - Копирует все атрибуты (поля) из исходной записи.
* - Автоматически корректирует значения полей типа DATE и TIME.
* - Устанавливает новый этап.
*/
const createRecord = async (
    journalRecordController: JournalRecordsController,
    baseRecord: JournalRecordResponse,
    newStageId: number
) => {
    // Корректирует значение поля в зависимости от его типа
    const getAttrValue = (value: unknown, dataType: DataType) => {
        if (dataType === "DATE") return getDateString(value)
        if (dataType === "TIME") return getTimeString(value)
        return value
    }
    // Извлекает ID привязок уровня компании (если есть)
    const getBindings = (baseRecord: JournalRecordResponse): number[] | null => {
        return baseRecord?.companyLevelBindings?.map(b => b?.id) || null
    }
    // Формирует объект запроса для создания новой записи
    const getRecordRequest = (baseRecord: JournalRecordResponse, newStageId: number): JournalRecordRequest => {
        return {
            routeId: baseRecord?.route?.id,
            stageId: newStageId,
            analysisObjectId: baseRecord?.analysisObject?.id,
            sampleTypeId: baseRecord.sampleType.id,
            attributes: baseRecord.attributes.map(attr => ({
                fieldId: attr.field.id,
                stageId: newStageId,
                data: {
                    ...attr.data,
                    value: getAttrValue(attr?.data?.value, attr?.field?.dataType),
                    id: undefined // Убираем ID, чтобы создать новое значение
                },
                statusId: null
            })),
            companyLevelBindings: getBindings(baseRecord)
        }
    }
    // Отправляет запрос на создание новой записи
    return await journalRecordController.createRecord(getRecordRequest(baseRecord, newStageId))
}
/**
* Основная функция аддона:
* Копирует запись лабораторного журнала на следующий этап (например, с "Регистрации" на "Анализ").
*
* Логика:
* 1. Загружает исходную запись через JournalRecordManager.
* 2. Формирует новую запись с теми же данными, но на новом этапе.
* 3. Особое внимание — полям типа DATE и TIME: они преобразуются в нужный формат.
* 4. Создаёт новую запись через API.
* 5. Показывает пользователю ID старой и новой записи.
*
* Используется для автоматизации деления данных.
*/
async function main({ queryParams }) {
    if (!queryParams) errorHandlers.queryParamsError()
    const socketId = getSocketId(queryParams)
    try {
        // Получаем данные исходной записи
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)
        const journalRecordController = new JournalRecordsController(apiInstance)
       
        // Создаём копию записи на новом этапе
        const newRecord = await createRecord(
            journalRecordController,
            journalRecordManager.data,
            config.nextJournalRecordStage
        )
        // Уведомляем пользователя об успехе с деталями
        showSuccessMessage(
            `${messages.success}. Id изначальной записи: ${config.journalRecordId}. ` +
            `Id новой записи ЛЖ: ${newRecord.id}. Этап: ${config.nextStageName}`,
            socketId
        )
       
        return config.integrationResult
    } catch (err) {
        // При любой ошибке — показываем её в модальном окне
        await showErrorMessage(err.message, socketId)
        return { error: err.message }
    }
}
module.exports = { main }
    </syntaxhighlight>
  </div>
</div>

Текущая версия от 04:26, 6 апреля 2026

Список демонстрационных аддонов, показывающих возможности интеграционного сервиса.

Проверка даты регистрации. check-registration-date

Проверяет, не истёк ли срок регистрации пробы. Сравнивает дату регистрации из указанного поля с текущей датой. Если прошло более заданного количества дней (по умолчанию — 10), выдаёт ошибку. В противном случае показывает сообщение об успешной проверке.

   Просмотр кода
   
import { JournalRecordManager } from "../../src/servivces/worker/api/interactors"
import { MessageBoxResults } from "@triteia/types-integration-gui"
import { getDataFromDialog } from "../../src/gui/api"
import { DialogType } from "@triteia/types-integration-gui"
import type { FetchApiClient } from "../../src/servivces/worker/api/ApiClient"
import { apiInstance } from "../../src/servivces/worker/api/interactors/ApiInstance"

// Конфигурация аддона:
// - Поле "Дата регистрации" — контрольная точка для расчёта срока
// - expirationLimit: 10 дней — максимальный допустимый срок хранения пробы
// - journalRecordId: 201 — запись из журнала "Лаб. воздуха / Анализы завершены"
const config = {
    dateField: 'Дата регистрации',
    expirationLimit: 10,
    journalRecordId: 201,
    integrationResult: { message: 'Срок регистрации пробы не истёк!' }
}

// Сообщения для пользовательских уведомлений и ошибок
const messages = {
    queryParamsNotFound: 'Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно',
    socketNotFound: 'Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно',
    // Успех: информирует пользователя, что срок не вышел
    success: (recordId) => `Срок регистрации пробы не истёк! Id записи ${recordId}`,
    // Ошибка: превышен лимит хранения
    expired: (limit, recordId) => `Срок регистрации пробы истёк!\nПрошло более ${limit} дней с момента регистрации. Id записи: ${recordId}`,
    // Ошибка: отсутствует дата в нужном поле
    dateField: (dateField, stageName, journalRecordId) => [
        `Не заполнено либо отсутствует поле "${dateField}"`,
        `в этапе "${stageName || ''}".\nId записи: ${journalRecordId}`
    ].join(' ')
}

// Централизованные обработчики ошибок — обеспечивают единообразие и чистоту основной логики
const errorHandlers = {
    queryParamsError: () => {
        throw new Error(messages.queryParamsNotFound)
    },
    socketError: () => {
        throw new Error(messages.socketNotFound)
    },
    // Вызывается, если поле с датой не заполнено
    dateError: (dateField, stageName, journalRecordId) => {
        throw new Error(messages.dateField(dateField, stageName, journalRecordId))
    },
    // Вызывается при превышении срока хранения
    expirationError: () => {
        throw new Error(messages.expired(config.expirationLimit, config.journalRecordId))
    }
}

// Извлекает socketId из queryParams — необходим для отправки диалогов в GUI
const getSocketId = (queryParams: any) => {
    const socketId = Array.isArray(queryParams?.socketId) && queryParams?.socketId?.length ? queryParams?.socketId[0] : '';

    if (!queryParams) errorHandlers.queryParamsError()
    if (!socketId) errorHandlers.socketError()
  
    return socketId
}

// Переводит дату в количество дней с начала эпохи (для арифметики дат)
const getDaysByDate = (date: Date) => {
    const dayMs = 60 * 60 * 24 * 1000
    const timestamp = date.getTime()
    return timestamp / dayMs
}

// Формирует конфиг диалога: тип, заголовок и сообщение для отображения в GUI
const getConfig = (type: DialogType, title: string, message) => {
    return {
        type,
        config: {
            title,
            message
        }
    }
}

// Рассчитывает, сколько дней прошло с момента регистрации пробы
const getExpirationOffset = (registrationDate: string) => {
    const regDate = new Date(registrationDate)
    const today = new Date()
    return getDaysByDate(today) - getDaysByDate(regDate)
}

// Показывает пользователю сообщение об ошибке
const showErrorMessage = async (message: string, socketId) => getDataFromDialog(getConfig('confirm', 'Ошибка', message), socketId);
// Показывает пользователю уведомление об успешной проверке
const showSuccessMessage = async (message: string, socketId) => getDataFromDialog(getConfig('confirm', 'Внимание', message), socketId);

/**
 * Основная функция аддона:
 * Проверяет, не истёк ли срок хранения пробы (с момента "Дата регистрации").
 * 
 * Логика:
 * 1. Получает значение поля "Дата регистрации" из записи журнала.
 * 2. Если дата не указана — ошибка.
 * 3. Если с даты регистрации прошло >= 10 дней — ошибка.
 * 4. Иначе — успех, показываем подтверждение.
 */
async function main({ queryParams, ...apiInstance }) {
    const socketId = getSocketId(queryParams)

    try {
        // Получаем доступ к данным записи журнала
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)
        // Извлекаем значение поля "Дата регистрации"
        const date = journalRecordManager.getFieldValue(config.dateField)

        // Если дата не заполнена — прерываем с пояснением
        if (!date) {
            errorHandlers.dateError(config.dateField, journalRecordManager?.data?.stage?.stage?.name, config.journalRecordId)
        }

        // Считаем, сколько дней прошло с регистрации
        const expirationOffset = getExpirationOffset(date as string)

        // Если срок хранения превышен — ошибка
        if (expirationOffset >= config.expirationLimit) {
            errorHandlers.expirationError()
        }

        // Всё в порядке — информируем пользователя
        await showSuccessMessage(messages.success(config.journalRecordId), socketId)

        return config.integrationResult
    } catch (err) {
        // При любой ошибке — показываем её в модальном окне
        await showErrorMessage(err.message, socketId)
        return { error: err.message }
    }
}

module.exports = { main }

Проверка заполненности полей показателей. check-indicator-fields-filled

Проверяет заполненность полей показателей в записи журнала по настраиваемому списку. Проверяет наличие определённых показателей (например, "Аммоний-ион") и заполненность у них таких полей, как исполнители, методики, средний результат и др. Выдаёт ошибку, если показатели отсутствуют или не все поля заполнены.

   Просмотр кода
   
import { JournalRecordManager } from "../../src/servivces/worker/api/interactors"
import { getDataFromDialog } from "../../src/gui/api"
import type { DialogType } from "@triteia/types-integration-gui"
import type { JournalRecordIndicatorResponse } from "../../src/servivces/worker/api/controllers/journal-records"
import type { IJournalRecordManager } from "../../src/servivces/worker/api/interactors/JournalRecordManager/interface"

// Описание свойств показателя (индикатора), которые нужно проверить
// Каждое свойство имеет метку (для отображения пользователю) и геттер значения
type IndicatorProp = { label: string, get: () => unknown }
type IndicatorPropsConfig = Record<string, IndicatorProp>

// Конфигурация полей, которые проверяются у каждого показателя
// Например: "Исполнители", "Методики", "Средний результат" и т.д.
const indicatorPropsConfig = (indicator): IndicatorPropsConfig => {
    return {
        executors: {
            label: 'Исполнители', 
            get: () => indicator?.result?.executors
        },
        methodic: {
            label: 'Методики', 
            get: () => indicator?.methodics
        },
        averageResult: {
            label: 'Средний результат', 
            get: () => indicator?.result?.averageValue
        },
        averageRoundedValue: {
            label: 'Средний округлённый результат', 
            get: () => indicator?.result?.averageRoundedValue
        }
    }
}

// Конфигурация аддона:
// - indicatorsList: список обязательных показателей (например, "Аммоний-ион")
// - journalRecordId: 144 — тестовая запись ("Мк / Тест_Ушакова")
const config = {
    indicatorsList: [
        'Аммоний-ион'
    ],
    journalRecordId: 144,
    integrationResult: { message: 'Все поля заполнены!' }
}

// Сообщения для пользовательских уведомлений и ошибок
const messages = {
    socketNotFound: 'Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно',
    queryParamsNotFound: 'Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно',
    success: 'Все необходимые поля заполнены!',
    emptyIndicators: (recordId) => `в записи с Id ${recordId} отсутствуют показатели`,
    emptyFieldsError: (fields, recordId) => `Поля ${fields.map(f => `"${f}"`).join(', ')} не заполнены в записи с ID ${recordId}`,
    indicators: (indicators: string[], recordId) => `Показатели ${indicators.map(f => `"${f}"`).join(', ')} отсутствуют в записи с ID ${recordId}`,
    // Формирует детализированное сообщение о незаполненных полях внутри показателей
    emptyIndicatorsProps: (props: Record<string, string[]>, recordId) => {
        const propsString = Object.keys(props).map(prop => !isEmptyArray(props[prop]) ? `${prop}: ${props[prop].map(p => `"${p}"`).join(', ')}` : '').join('; ')
        return `В записи с Id ${recordId} отсутствуют либо не заполнены следующие поля у показателей: ${propsString}`
    }
}

// Централизованные обработчики ошибок — обеспечивают единообразие и чистоту основной логики
const errorHandlers = {
    queryParamsError: () => {
        throw new Error(messages.queryParamsNotFound)
    },
    fieldsError: (emptyFields, recordId) => {
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))
    },
    socketError: () => {
        throw new Error(messages.socketNotFound)
    },
    indicatorsError: (recordId) => {
        throw new Error(messages.emptyIndicators(recordId))
    },
    notFoundIndicators: (indicators: string[], recordId: number) => {
        throw new Error(messages.indicators(indicators, recordId))
    },
    notFilledIndicatorProps: (props: Record<string, string[]>) => {
        throw new Error(messages.emptyIndicatorsProps(props, config.journalRecordId))
    }
}

// Формирует конфиг диалога для отправки в GUI: тип, заголовок и сообщение
const getConfig = (type: DialogType, title: string, message) => {
    return {
        type,
        config: {
            title,
            message
        }
    }
}

// Извлекает socketId из queryParams — необходим для связи с интерфейсом
const getSocketId = (queryParams: any) => {
    const socketId = Array.isArray(queryParams?.socketId) && queryParams?.socketId?.length ? queryParams?.socketId[0] : '';

    if (!queryParams) errorHandlers.queryParamsError()
    if (!socketId) errorHandlers.socketError()
  
    return socketId
}

// Показывает пользователю модальное окно с ошибкой
const showErrorMessage = async (message: string, socketId) => getDataFromDialog(getConfig('confirm', 'Ошибка', message), socketId);
// Показывает уведомление об успешной проверке
const showSuccessMessage = async (message: string, socketId) => getDataFromDialog(getConfig('confirm', 'Внимание', message), socketId);

// Универсальная проверка: является ли массив пустым (с учётом типа)
const isEmptyArray = (arr: unknown[]) => {
    return Array.isArray(arr) && arr.length === 0
}

// Проверяет, есть ли у записи хотя бы один показатель
const hasIndicators = (journalRecordManager: IJournalRecordManager) => {
    const indicators = journalRecordManager?.data?.indicators
    return Array.isArray(indicators) && indicators.length > 0
}

// Определяет, какие из требуемых показателей отсутствуют в записи
const getNotFoundIndicators = (indicators: JournalRecordIndicatorResponse[], indicatorsList = config.indicatorsList) => {
    return indicatorsList.filter(ind => 
        indicators.every(ljInd => ljInd?.indicator?.name?.toLowerCase() !== ind.toLowerCase())
    )
} 

// Собирает список незаполненных полей для каждого показателя
// Возвращает объект: { "Аммоний-ион": ["Исполнители", "Средний результат"] }
const getNotFilledResultsFields = (indicators: JournalRecordIndicatorResponse[]): Record<string, string[]> => {
    return indicators.reduce((acc, indicator) => {
        const config = indicatorPropsConfig(indicator)
        return {
            ...acc,
            [indicator?.indicator?.name]: Object.keys(config).reduce((acc, prop) => {
                if (!config[prop].get()) return [...acc, config[prop].label]
                return acc
            }, [] as string[])
        }
    }, {}) as Record<string, string[]>
}

// Проверяет, есть ли хотя бы один показатель с незаполненными полями
const hasNotFilledProps = (indicatorsProps: Record<string, string[]>) => {
    return Object.keys(indicatorsProps).some(indicator => !isEmptyArray(indicatorsProps[indicator]))
}

// Проверяет, есть ли отсутствующие показатели в списке
const hasNotFoundIndicators = (indicators: string[]) => !isEmptyArray(indicators)

/**
 * Основная функция аддона:
 * Проверяет заполненность:
 * 1. Наличие указанных показателей (например, "Аммоний-ион").
 * 2. Заполненность внутренних полей каждого показателя (исполнители, методики и т.д.).
 * 
 * При обнаружении проблем — прерывает выполнение и показывает детализированную ошибку.
 */
async function main({ queryParams }) {
    if (!queryParams) errorHandlers.queryParamsError()
    const socketId = getSocketId(queryParams)

    try {
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)

        // Проверка: есть ли вообще показатели в записи?
        if (!hasIndicators(journalRecordManager)) {
            errorHandlers.indicatorsError(journalRecordManager.journalRecordId)
        }

        const indicators = journalRecordManager?.data?.indicators

        // Проверка: все ли требуемые показатели присутствуют?
        const notFoundIndicators = getNotFoundIndicators(indicators)
        if (hasNotFoundIndicators(notFoundIndicators)) { 
            errorHandlers.notFoundIndicators(notFoundIndicators, journalRecordManager.journalRecordId)
        }

        // Проверка: все ли поля у показателей заполнены?
        const emptyIndicatorProps = getNotFilledResultsFields(indicators)
        if (hasNotFilledProps(emptyIndicatorProps)) {
            errorHandlers.notFilledIndicatorProps(emptyIndicatorProps)
        }

        // Все проверки пройдены — информируем пользователя
        showSuccessMessage(messages.success, socketId)
    } catch (err) {
        // При любой ошибке — показываем её в модальном окне
        await showErrorMessage(err.message, socketId)
        return { error: err.message }
    }
}

module.exports = { main }

Проверка заполненности атрибутов записи ЛЖ. check-record-fields-filled

Проверяет заполненность заданных полей в записи журнала (например, "Источник", "Место отбора", "Дата регистрации" и др.). Если какое-либо из обязательных полей не заполнено, выдаётся соответствующая ошибка.

   Просмотр кода
   
import { JournalRecordManager } from "../../src/servivces/worker/api/interactors"
import { getDataFromDialog } from "../../src/gui/api"
import { DialogType } from "@triteia/types-integration-gui"
import { IJournalRecordManager } from "../../src/servivces/worker/api/interactors/JournalRecordManager/interface"

const config = {
    requiredFields: [
        'Источник',
        'Список. Целое число',
        'Строка',
        'Целое число',
        'Вещественное число',
        'Логический тип',
        'Дата',
        'Время',
    ],
    journalRecordId: 144,  // Мк / Тест_Ушакова
    integrationResult: { message: 'Все поля заполнены!' }
}

const messages = {
    socketNotFound: 'Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно',
    queryParamsNotFound: 'Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно',
    success: 'Все необходимые поля заполнены!',
    emptyFieldsError: (fields, recordId) => `Поля ${fields.map(f => `"${f}"`).join(', ')} не заполнены в записи с ID ${recordId}`,
}

const errorHandlers = {
    queryParamsError: () => {
        throw new Error(messages.queryParamsNotFound)
    },
    fieldsError: (emptyFields, recordId) => {
        throw new Error(messages.emptyFieldsError(emptyFields, recordId))
    },
    socketError: () => {
        throw new Error(messages.socketNotFound)
    }
}

const getConfig = (type: DialogType, title: string, message) => {
    return {
        type,
        config: {
            title,
            message
        }
    }
}

const getSocketId = (queryParams: any) => {
    const socketId = Array.isArray(queryParams?.socketId) && queryParams?.socketId?.length ? queryParams?.socketId[0] : '';

    if (!queryParams) errorHandlers.queryParamsError()
    if (!socketId) errorHandlers.socketError()
  
    return socketId
}

const showErrorMessage = async (message: string, socketId) => getDataFromDialog(getConfig('confirm', 'Ошибка', message), socketId);
const showSuccessMessage = async (message: string, socketId) => getDataFromDialog(getConfig('confirm', 'Внимание', message), socketId);

const isEmptyArray = (arr: unknown[]) => {
    return arr.length === 0
}

const getNotFilledFields = (fields: string[], journalRecordsManager: IJournalRecordManager) => {
    console.log(journalRecordsManager.data.attributes, 'data.attributes')
    return fields.filter(f => !journalRecordsManager.getFieldValue(f))
}

async function main({ queryParams }) {
    if (!queryParams) errorHandlers.queryParamsError()
    const socketId = getSocketId(queryParams)

    try {
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)
        const emptyFields = getNotFilledFields(config.requiredFields, journalRecordManager)

        if (isEmptyArray(emptyFields)) {
            showSuccessMessage(messages.success, socketId)
            return config.integrationResult
        } else errorHandlers.fieldsError(emptyFields, config.journalRecordId)
    } catch (err) {
        await showErrorMessage(err.message, socketId)
        return { error: err.message }
    }
}

module.exports = { main }

Проверка движения пробы. check-sample-moving

Проверяет маршрут движения пробы по этапам. Получает историю перемещений записи и определяет, происходило ли движение (нахождение более чем на одном этапе). Отображает маршрут или сообщает, что движение не происходило.

   Просмотр кода
   
import { getDataFromDialog } from "../../src/gui/api"
import { JournalsRecordRouteController } from "../../src/servivces/worker/api/controllers/journals-record-route"
import { apiInstance } from "../../src/servivces/worker/api/interactors/ApiInstance"
import type { DialogType } from "@triteia/types-integration-gui"
import type { JournalRecordRouteDto } from "../../src/servivces/worker/api/controllers/journal-records"

// Конфигурация аддона:
// - journalRecordId: 114 — запись из журнала "Лаб. воздуха / Анализы завершены"
// Аддон анализирует маршрут движения пробы по этапам ЛЖ
const config = {
    journalRecordId: 114
}

// Сообщения для отображения пользователю
const messages = {
    socketNotFound: 'Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно',
    queryParamsNotFound: 'Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно',
    // Формирует строку с маршрутом пробы: этапы через " -> "
    route: (routes: JournalRecordRouteDto[], recordId: number) => 
        `Маршрут пробы: ${routes.map(r => `"${r.stageName}"`).join(' -> ')}. Id записи ЛЖ: ${recordId}`,
    // Сообщение, если проба не перемещалась между этапами
    notMoved: (recordId: number) => 
        `Движение пробы не происходило. Id записи ЛЖ: ${recordId}`
}

// Обработчики ошибок — централизованно выбрасывают исключения с понятными сообщениями
const errorHandlers = {
    queryParamsError: () => {
        throw new Error(messages.queryParamsNotFound)
    },
    socketError: () => {
        throw new Error(messages.socketNotFound)
    }
}

// Формирует конфиг диалога для GUI: тип (confirm), заголовок и текст
const getConfig = (type: DialogType, title: string, message) => {
    return {
        type,
        config: {
            title,
            message
        }
    }
}

// Извлекает socketId из параметров запроса — необходим для отправки ответа в интерфейс
const getSocketId = (queryParams: any) => {
    const socketId = Array.isArray(queryParams?.socketId) && queryParams?.socketId?.length ? queryParams?.socketId[0] : '';

    if (!queryParams) errorHandlers.queryParamsError()
    if (!socketId) errorHandlers.socketError()
  
    return socketId
}

// Показывает пользователю модальное окно с ошибкой
const showErrorMessage = async (message: string, socketId) => getDataFromDialog(getConfig('confirm', 'Ошибка', message), socketId);
// Показывает информационное сообщение (успех или статус)
const showSuccessMessage = async (message: string, socketId) => getDataFromDialog(getConfig('confirm', 'Внимание', message), socketId);

// Определяет, перемещалась ли проба: если более одного этапа — значит, было движение
const isSampleMoved = (moves: JournalRecordRouteDto[]) => {
    return Array.isArray(moves) && moves.length > 1
}

/**
 * Основная функция аддона:
 * Получает маршрут пробы по этапам лабораторного журнала и отображает его пользователю.
 * 
 * Логика:
 * 1. Проверяет наличие параметров и подключение клиента (socketId).
 * 2. Запрашивает историю перемещений пробы через API.
 * 3. Если проба прошла более одного этапа — отображает маршрут.
 * 4. Если осталась на одном этапе — сообщает, что движения не было.
 */
async function main({ queryParams }) {
    if (!queryParams) errorHandlers.queryParamsError()
    const socketId = getSocketId(queryParams)

    try {
        // Контроллер для работы с маршрутом записи (история этапов)
        const journalsRecordRouteController = new JournalsRecordRouteController(apiInstance)
        // Получаем список этапов, через которые прошла проба
        const sampleMoves = await journalsRecordRouteController.getJournalRecordRoute(config.journalRecordId)

        if (isSampleMoved(sampleMoves)) {
            // Проба перемещалась — показываем полный маршрут
            showSuccessMessage(messages.route(sampleMoves, config.journalRecordId), socketId)
        } else {
            // Проба не покидала исходный этап
            showSuccessMessage(messages.notMoved(config.journalRecordId), socketId)
        }
    } catch (err) {
        // При любой ошибке (сеть, доступ, данные) — показываем её пользователю
        await showErrorMessage(err.message, socketId)
        return { error: err.message }
    }
}

module.exports = { main }

Проверка прав пользователя. check-user-rights

Проверяет права доступа пользователя к определённым ресурсам (объекты анализа, методики, записи ЛЖ). Отображает таблицу с типами доступа (чтение, запись и т.д.) и наличием прав по каждому ресурсу.

   Просмотр кода
   
import { getDataFromDialog } from "../../src/gui/api"
import { apiInstance } from "../../src/servivces/worker/api/interactors/ApiInstance"
import { 
    PermissionController, 
    PermissionResource, 
    PermissionRwAccess
} from "../../src/servivces/worker/api/controllers/permisson"

import { TableBuilder } from "../../src/gui/builder"
import type { DialogType, TableConfig } from "@triteia/types-integration-gui"
import type { JournalRecordRouteDto } from "../../src/servivces/worker/api/controllers/journal-records"

// Отображение типов доступа в понятные пользователю названия
const accessNamesMap = {
    [PermissionRwAccess.EXEC]: 'Выполнение',
    [PermissionRwAccess.READ]: 'Чтение',
    [PermissionRwAccess.WRITE]: 'Запись',
}

// Список ресурсов, права на которые необходимо проверить
const requiredResources = [
    PermissionResource.ANALYSIS_OBJECT, 
    PermissionResource.METHODOLOGY,
    PermissionResource.JOURNAL_RECORD
]

// Отображение идентификаторов ресурсов в читаемые названия для отображения в таблице
const resourcesNamesMap = {
    [PermissionResource.ANALYSIS_OBJECT]: 'Объекты анализа', 
    [PermissionResource.METHODOLOGY]: 'Методики', 
    [PermissionResource.JOURNAL_RECORD]: 'Записи ЛЖ'
}

/**
 * Формирует конфигурацию таблицы с правами доступа:
 * - Для каждого ресурса проверяются все типы доступа (чтение, запись, выполнение).
 * - Возвращает готовую конфигурацию таблицы для отображения в GUI.
 */
const getAccessRightsTable = async (tableBuilder: TableBuilder, permissionController: PermissionController): Promise<TableConfig> => {
    // Получаем список строк: комбинация "ресурс + тип доступа + наличие права"
    const rows = (await Promise.all(requiredResources.map(async (source: PermissionResource) => {
            const sourceCombinations = await Promise.all(Object.keys(PermissionRwAccess).map(async (action) => {
                const access = await permissionController.getSourcePermission(source, PermissionRwAccess[action])
                return { 
                    source: resourcesNamesMap[source], 
                    action: accessNamesMap[PermissionRwAccess[action]], 
                    access: access.hasPermission 
                }
            }))
            return sourceCombinations
        }   
    ))).flat()

    // Строим таблицу с помощью TableBuilder
    const table = tableBuilder
        .name('simpleTable')
        .label('Таблица')
        .isAddable(false)           // Нельзя добавлять строки
        .actionsCol(false)          // Скрыть колонку действий
        .addColumn('Ресурс', 'source', false)
        .config('input', { name: 'source', rules: [{ required: true }] }, { placeholder: '', disabled: false })
        .next()
        .addColumn('Тип доступа', 'action', false)
        .config('input', { name: 'action' }, { type: 'string', placeholder: '', disabled: false })
        .next()
        .addColumn('Доступ', 'access', false)
        .config('checkbox', { name: 'access' }, { type: 'boolean', placeholder: '', disabled: true }) // Только для просмотра
        .next()
        .setRows(rows)
        .build();
  
  return table
}

// Сообщения для пользователя
const messages = {
    socketNotFound: 'Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно',
    queryParamsNotFound: 'Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно',
}

// Обработчики ошибок — централизованно выбрасывают исключения
const errorHandlers = {
    queryParamsError: () => {
        throw new Error(messages.queryParamsNotFound)
    },
    socketError: () => {
        throw new Error(messages.socketNotFound)
    }
}

// Формирует конфиг диалога для отправки в GUI
const getConfig = (type: DialogType, title: string, message) => {
    return {
        type,
        config: {
            title,
            message
        }
    }
}

// Извлекает socketId из параметров — необходим для обратной связи с интерфейсом
const getSocketId = (queryParams: any) => {
    const socketId = Array.isArray(queryParams?.socketId) && queryParams?.socketId?.length ? queryParams?.socketId[0] : '';
    if (!queryParams) errorHandlers.queryParamsError()
    if (!socketId) errorHandlers.socketError()
    return socketId
}

// Показывает пользователю сообщение об ошибке
const showErrorMessage = async (message: string, socketId) => getDataFromDialog(getConfig('confirm', 'Ошибка', message), socketId);

// Показывает таблицу с правами доступа в модальном окне
const showTable = async (builder: TableBuilder, controller: PermissionController, socketId: string) => 
    await getDataFromDialog({ type: 'input-data', config: [await getAccessRightsTable(builder, controller)] }, socketId)

/**
 * Основная функция аддона:
 * Отображает пользователю таблицу с текущими правами доступа на ключевые ресурсы системы.
 * 
 * Логика:
 * 1. Проверяет наличие параметров и активное соединение (socketId).
 * 2. Запрашивает права для каждого ресурса и типа доступа.
 * 3. Формирует таблицу через TableBuilder.
 * 4. Показывает её в модальном окне.
 * 
 * Результат: пользователь видит, какие действия разрешены для "Объектов анализа", "Методик" и "Записей ЛЖ".
 */
async function main({ queryParams }) {
    if (!queryParams) errorHandlers.queryParamsError()
    const socketId = getSocketId(queryParams)

    try {
        // Создаём экземпляры билдера и контроллера прав доступа
        await showTable(new TableBuilder(), new PermissionController(apiInstance), socketId)
    } catch (err) {
        // При любой ошибке — показываем её в модальном окне
        await showErrorMessage(err.message, socketId)
        return { error: err.message }
    }
}

module.exports = { main }

Заполнение атрибутов записи ЛЖ. fill-record-fields

Автоматически заполняет заданные поля в записи журнала (например, даты, место отбора, шифр пробы и др.). Есть возможность использования ключевых слов для сопоставления полей, если точные названия не совпадают.

   Просмотр кода
   
import { JournalRecordManager } from "../../src/servivces/worker/api/interactors"
import { getDataFromDialog } from "../../src/gui/api"
import { DialogType } from "@triteia/types-integration-gui"
import { IJournalRecordManager } from "../../src/servivces/worker/api/interactors/JournalRecordManager/interface"
import { JournalRecordAttributeFieldResponse } from "../../src/servivces/worker/api/controllers/lj-eav-attribute-fields"
import { FieldMatch } from "../../src/servivces/worker/api/interactors/JournalRecordManager/interface"

// Текущая дата в формате YYYY-MM-DD — используется как значение по умолчанию для полей с датой
const date = new Date().toISOString().split('T')[0]

// Конфигурация аддона:
// - requiredFields: список полей, которые нужно заполнить
//   Каждое поле содержит:
//     • name — отображаемое имя
//     • value — значение, которое будет установлено
//     • keywords — ключевые слова для поиска соответствующего поля в записи
// - journalRecordId: 118 — тестовая запись ("Мк / 111")
// - Результат при успехе: сообщение о завершении
const config = {
    requiredFields: [
        { name: 'Дата регистрации', value: date, keywords: ['дат', 'регистр'] },
        { name: 'Место отбора', value: 'Скважина 516563Г ', keywords: ['мест', 'отбор'] },
        { name: 'Дата отбора', value: date, keywords: ['дат', 'отбор'] },
        { name: 'Шифр пробы', value: 'shifr 123', keywords: ['дат', 'регистр']},
        { name: 'Список. Целое число', value: 666, keywords: ['спис', 'цел', 'числ'] },
        { name: 'Помещение', value: 'помещение', keywords: ['помещ']},
    ] as FieldMatch[],
    journalRecordId: 118, // Мк / 111
    integrationResult: { message: 'Все поля заполнены!' }
}

// Сообщения для пользователя
const messages = {
    socketNotFound: 'Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно',
    queryParamsNotFound: 'Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно',
    success: (recordId) => `Все необходимые поля заполнены! Id записи: ${recordId}`,
    emptyFieldsError: (fields, recordId) => `Поля ${fields.map(f => `"${f}"`).join(', ')} не заполнены в записи с ID ${recordId}`,
}

// Обработчики ошибок — централизованно выбрасывают исключения
const errorHandlers = {
    queryParamsError: () => {
        throw new Error(messages.queryParamsNotFound)
    },
    socketError: () => {
        throw new Error(messages.socketNotFound)
    }
}

// Формирует конфиг диалога для отправки в GUI: тип, заголовок и сообщение
const getConfig = (type: DialogType, title: string, message) => {
    return {
        type,
        config: {
            title,
            message
        }
    }
}

// Извлекает socketId из параметров — необходим для обратной связи с интерфейсом
const getSocketId = (queryParams: any) => {
    const socketId = Array.isArray(queryParams?.socketId) && queryParams?.socketId?.length ? queryParams?.socketId[0] : '';
    if (!queryParams) errorHandlers.queryParamsError()
    if (!socketId) errorHandlers.socketError()
    return socketId
}

// Показывает пользователю сообщение об ошибке
const showErrorMessage = async (message: string, socketId) => getDataFromDialog(getConfig('confirm', 'Ошибка', message), socketId);
// Показывает уведомление об успешном заполнении
const showSuccessMessage = async (message: string, socketId) => getDataFromDialog(getConfig('confirm', 'Внимание', message), socketId);

// Сопоставляет поле из записи (по имени) с требуемым полем через ключевые слова
// Например: если имя поля содержит "дат" и "регистр" → подходит под "Дата регистрации"
const fieldsMatcher = (field: JournalRecordAttributeFieldResponse, searchField: FieldMatch) => {
    return searchField.keywords.every(k => 
        String(field.name).toLocaleLowerCase().includes(k.toLocaleLowerCase())
    )
}

// Заполняет указанные поля в записи журнала с использованием кастомного механизма сопоставления
const fillFields = async (fields: FieldMatch[], journalRecordsManager: IJournalRecordManager) => {
    await journalRecordsManager.setFieldsValues(fields, journalRecordsManager, fieldsMatcher)
}

/**
 * Основная функция аддона:
 * Автоматически заполняет заданные поля в записи лабораторного журнала.
 * 
 * Особенности:
 * - Поля ищутся не по точному имени, а по ключевым словам (гибкое сопоставление).
 * - Поддерживает разные типы значений: строки, числа, даты.
 * - Используется в тестовых или демонстрационных целях.
 * 
 * Логика:
 * 1. Проверяет наличие параметров и активное соединение.
 * 2. Загружает запись журнала.
 * 3. Для каждого поля из config.requiredFields:
 *    - Находит соответствующее поле в записи по ключевым словам.
 *    - Устанавливает указанное значение.
 * 4. Сообщает пользователю об успехе.
 */
async function main({ queryParams }) {
    if (!queryParams) errorHandlers.queryParamsError()
    const socketId = getSocketId(queryParams)

    try {
        // Получаем менеджер для работы с записью
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)
        // Заполняем поля по ключевым словам
        await fillFields(config.requiredFields, journalRecordManager)
        // Информируем пользователя о завершении
        showSuccessMessage(messages.success(journalRecordManager.journalRecordId), socketId)
        return config.integrationResult
    } catch (err) {
        // При любой ошибке — показываем её в модальном окне
        await showErrorMessage(err.message, socketId)
        return { error: err.message }
    }
}

module.exports = { main }

Деление данных. copy-journal-record

Создаёт копию записи журнала на заданном этапе маршрута. Выводит сообщение с ID новой записи.

   Просмотр кода
   
import { JournalRecordManager } from "../../src/servivces/worker/api/interactors"
import { JournalRecordsController } from "../../src/servivces/worker/api/controllers/journal-records"
import { getDataFromDialog } from "../../src/gui/api"
import { apiInstance } from "../../src/servivces/worker/api/interactors/ApiInstance"
import type { DialogType } from "@triteia/types-integration-gui"
import type { JournalRecordRequest, JournalRecordResponse } from "../../src/servivces/worker/api/controllers/journal-records"
import type { JournalRecordAttributeResponse } from "../../src/servivces/worker/api/controllers/journal-records"

// Тип данных поля (DATE, TIME, STRING и т.д.) — используется для корректной обработки значений при копировании
type DataType = JournalRecordAttributeResponse['field']['dataType']

// Конфигурация аддона:
// - journalRecordId: 208 — исходная запись ("Лаб. воздуха / Регистрация")
// - nextJournalRecordStage: 2 — ID этапа, на который нужно скопировать запись ("Анализ")
// - nextStageName: отображаемое имя целевого этапа
// - integrationResult: сообщение при успешном копировании
const config = {
    journalRecordId: 208,  // Лаб. воздуха / Регистрация
    nextJournalRecordStage: 2, // Лаб. воздуха / Анализ
    nextStageName: 'Анализ',
    integrationResult: { message: 'Запись ЛЖ скопирована успешно!' }
}

// Сообщения для пользователя
const messages = {
    socketNotFound: 'Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно',
    queryParamsNotFound: 'Отсутствуют необходимые параметры для выполнения аддона. Дальнейшее выполнение невозможно',
    success: 'Все необходимые поля заполнены!',
}

// Обработчики ошибок — централизованно выбрасывают исключения
const errorHandlers = {
    queryParamsError: () => {
        throw new Error(messages.queryParamsNotFound)
    },
    socketError: () => {
        throw new Error(messages.socketNotFound)
    }
}

// Формирует конфиг диалога для отправки в GUI: тип, заголовок и сообщение
const getConfig = (type: DialogType, title: string, message) => {
    return {
        type,
        config: {
            title,
            message
        }
    }
}

// Извлекает socketId из параметров — необходим для обратной связи с интерфейсом
const getSocketId = (queryParams: any) => {
    const socketId = Array.isArray(queryParams?.socketId) && queryParams?.socketId?.length ? queryParams?.socketId[0] : '';
    if (!queryParams) errorHandlers.queryParamsError()
    if (!socketId) errorHandlers.socketError()
    return socketId
}

// Показывает пользователю сообщение об ошибке
const showErrorMessage = async (message: string, socketId) => getDataFromDialog(getConfig('confirm', 'Ошибка', message), socketId);
// Показывает уведомление об успешном копировании
const showSuccessMessage = async (message: string, socketId) => getDataFromDialog(getConfig('confirm', 'Внимание', message), socketId);

// Преобразует ISO-дату в строку формата YYYY-MM-DD (без времени)
const getDateString = (ISODate: unknown | null = null) => {
    return new Date(String(ISODate)).toISOString().split('T')[0]
}

// Преобразует ISO-дату в строку формата HH:mm:ss (без миллисекунд)
const getTimeString = (ISODate: unknown | null = null) => {
    return new Date(String(ISODate)).toISOString().split('T')[1].split('.')[0]
}

/**
 * Создаёт новую запись лабораторного журнала на основе существующей.
 * 
 * Особенности:
 * - Копирует все атрибуты (поля) из исходной записи.
 * - Автоматически корректирует значения полей типа DATE и TIME.
 * - Устанавливает новый этап.
 */
const createRecord = async (
    journalRecordController: JournalRecordsController,
    baseRecord: JournalRecordResponse,
    newStageId: number
) => {
    // Корректирует значение поля в зависимости от его типа
    const getAttrValue = (value: unknown, dataType: DataType) => {
        if (dataType === "DATE") return getDateString(value)
        if (dataType === "TIME") return getTimeString(value)
        return value
    }

    // Извлекает ID привязок уровня компании (если есть)
    const getBindings = (baseRecord: JournalRecordResponse): number[] | null => {
        return baseRecord?.companyLevelBindings?.map(b => b?.id) || null
    } 

    // Формирует объект запроса для создания новой записи
    const getRecordRequest = (baseRecord: JournalRecordResponse, newStageId: number): JournalRecordRequest => {
        return {
            routeId: baseRecord?.route?.id,
            stageId: newStageId,
            analysisObjectId: baseRecord?.analysisObject?.id,
            sampleTypeId: baseRecord.sampleType.id,
            attributes: baseRecord.attributes.map(attr => ({
                fieldId: attr.field.id,
                stageId: newStageId,
                data: {
                    ...attr.data, 
                    value: getAttrValue(attr?.data?.value, attr?.field?.dataType),
                    id: undefined // Убираем ID, чтобы создать новое значение
                },
                statusId: null
            })),
            companyLevelBindings: getBindings(baseRecord)
        }
    }

    // Отправляет запрос на создание новой записи
    return await journalRecordController.createRecord(getRecordRequest(baseRecord, newStageId))
}

/**
 * Основная функция аддона:
 * Копирует запись лабораторного журнала на следующий этап (например, с "Регистрации" на "Анализ").
 * 
 * Логика:
 * 1. Загружает исходную запись через JournalRecordManager.
 * 2. Формирует новую запись с теми же данными, но на новом этапе.
 * 3. Особое внимание — полям типа DATE и TIME: они преобразуются в нужный формат.
 * 4. Создаёт новую запись через API.
 * 5. Показывает пользователю ID старой и новой записи.
 * 
 * Используется для автоматизации деления данных.
 */
async function main({ queryParams }) {
    if (!queryParams) errorHandlers.queryParamsError()
    const socketId = getSocketId(queryParams)

    try {
        // Получаем данные исходной записи
        const journalRecordManager = await JournalRecordManager(config.journalRecordId)
        const journalRecordController = new JournalRecordsController(apiInstance)
        
        // Создаём копию записи на новом этапе
        const newRecord = await createRecord(
            journalRecordController, 
            journalRecordManager.data, 
            config.nextJournalRecordStage
        )

        // Уведомляем пользователя об успехе с деталями
        showSuccessMessage(
            `${messages.success}. Id изначальной записи: ${config.journalRecordId}. ` +
            `Id новой записи ЛЖ: ${newRecord.id}. Этап: ${config.nextStageName}`, 
            socketId
        )
        
        return config.integrationResult
    } catch (err) {
        // При любой ошибке — показываем её в модальном окне
        await showErrorMessage(err.message, socketId)
        return { error: err.message }
    }
}

module.exports = { main }