Аддоны: различия между версиями
Arhidiman (обсуждение | вклад) |
Sidminik (обсуждение | вклад) м Sidminik переименовал страницу Аддоны WEB ЛИМС Тритея в Аддоны |
||
| (не показано 15 промежуточных версий 1 участника) | |||
| Строка 11: | Строка 11: | ||
</div> | </div> | ||
<div class="mw-collapsible-content"> | <div class="mw-collapsible-content"> | ||
<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> | ||
| Строка 115: | Строка 154: | ||
[[Файл:Check-indicator-fields-filled.png|frame]] | [[Файл:Check-indicator-fields-filled.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"> | <syntaxhighlight lang="typescript"> | ||
import { JournalRecordManager } from "../../src/servivces/worker/api/interactors" | import { JournalRecordManager } from "../../src/servivces/worker/api/interactors" | ||
| Строка 122: | Строка 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 { | ||
| Строка 146: | Строка 196: | ||
} | } | ||
// Конфигурация аддона: | |||
// - indicatorsList: список обязательных показателей (например, "Аммоний-ион") | |||
// - journalRecordId: 144 — тестовая запись ("Мк / Тест_Ушакова") | |||
const config = { | const config = { | ||
indicatorsList: [ | indicatorsList: [ | ||
'Аммоний-ион' | 'Аммоний-ион' | ||
], | ], | ||
journalRecordId: 144, | journalRecordId: 144, | ||
integrationResult: { message: 'Все поля заполнены!' } | integrationResult: { message: 'Все поля заполнены!' } | ||
} | } | ||
// Сообщения для пользовательских уведомлений и ошибок | |||
const messages = { | const messages = { | ||
socketNotFound: 'Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно', | socketNotFound: 'Отсутствует соединение с клиентом. Дальнейшее выполнение аддона невозможно', | ||
| Строка 168: | Строка 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('; ') | ||
| Строка 174: | Строка 222: | ||
} | } | ||
// Централизованные обработчики ошибок — обеспечивают единообразие и чистоту основной логики | |||
const errorHandlers = { | const errorHandlers = { | ||
queryParamsError: () => { | queryParamsError: () => { | ||
| Строка 184: | Строка 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)) | ||
}, | }, | ||
| Строка 195: | Строка 244: | ||
} | } | ||
// Формирует конфиг диалога для отправки в GUI: тип, заголовок и сообщение | |||
const getConfig = (type: DialogType, title: string, message) => { | const getConfig = (type: DialogType, title: string, message) => { | ||
return { | return { | ||
| Строка 205: | Строка 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] : ''; | ||
| Строка 214: | Строка 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 | ||
| Строка 226: | Строка 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. | 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) => { | ||
| Строка 243: | Строка 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() | ||
| Строка 256: | Строка 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) | ||
| Строка 262: | Строка 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)) { | ||
| Строка 267: | Строка 339: | ||
} | } | ||
// Проверка: все ли поля у показателей заполнены? | |||
const emptyIndicatorProps = getNotFilledResultsFields(indicators) | const emptyIndicatorProps = getNotFilledResultsFields(indicators) | ||
if (hasNotFilledProps(emptyIndicatorProps)) { | if (hasNotFilledProps(emptyIndicatorProps)) { | ||
| Строка 272: | Строка 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 } | ||
| Строка 280: | Строка 355: | ||
module.exports = { main } | module.exports = { main } | ||
</syntaxhighlight> | |||
</div> | |||
</div> | |||
== Проверка заполненности атрибутов записи ЛЖ. <code>check-record-fields-filled</code> == | |||
[[Файл:Check-record-fields-filled.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" | |||
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 } | |||
</syntaxhighlight> | |||
</div> | |||
</div> | |||
== Проверка движения пробы. <code>check-sample-moving</code> == | == Проверка движения пробы. <code>check-sample-moving</code> == | ||
[[Файл: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 }