Пример 1. Поиск пересечения отпусков.
Задача: нужно, чтобы сотрудник при указании периода мог сразу увидеть пересечения с коллегами и сдвинуть даты своего отпуска.
Чтобы найти отпуска, которые попадают в указанный период, нам потребуется воспользоваться поиском.
Создадим новый виджет и назовём его vacation_overlap. На вход в нём будет приходить выбранный период, а в качестве результата мы будем возвращать список дат отпусков и фамилии сотрудников, которые попали в пересечение.
Также добавим в контекст виджета переменную типа Приложение со ссылкой на созданное нами приложение Отпуск:
Создадим метод, который будет выполняться при запуске виджета. Пока весь код пишем в клиентских сценариях для ускорения разработки и отладки:
Код:
async function onInit(): Promise<void> {
if (!Context.data.first_date || !Context.data.second_date) {
return;
}
// Создаём поиск и заполняем условия
const vacation_searcher = Context.fields.vacation.app.search();
vacation_searcher.where((vacation, gf) =>
// Задаем условие через ИЛИ
gf.or(
// Здесь условие уже через И
gf.and(
vacation.date_start.gte(Context.data.first_date!),
vacation.date_start.lt(Context.data.second_date!),
),
gf.and(
vacation.date_end.gt(Context.data.first_date!),
vacation.date_end.lte(Context.data.second_date!),
),
gf.and(
vacation.date_start.lt(Context.data.first_date!),
vacation.date_end.gt(Context.data.second_date!),
)
)
);
// Ограничиваем размер получаемой выборки
vacation_searcher.size(100);
// Сортируем по дате создания
vacation_searcher.sort("date_start", false);
// Выполняем запрос и сохраняем результат в переменную
const vacations = await vacation_searcher.all();
Context.data.vacations = vacations.map(vac => vac.data.__name);
}
Context.data.vacations в сценарии — это переменная произвольного типа, которая нужна для передачи массива строк в шаблон.
На форму шаблона добавим виджет Код со следующим содержимым:
Код:
<% if (Context.data.vacations) { %>
<% for(let item of Context.data.vacations) { %>
<div><%= item%></div>
<% } %>
<% } %>
Запускаем отладку и проверяем все возможные случаи пересечения. Обязательно проверяем случаи, когда отпуска не пересекаются.
Когда тестирование успешно окончено, публикуем виджет с комментарием:
Теперь можно добавить виджет на форму создания отпуска. Допишем немного кода в виджет Код, добавленный на неё:
Код:
<% if (Context.data.date_start || Context.data.date_end) { %>
<%= UI.widget.groupbox({title: 'Количество рабочих дней', collapsible: true }, panelDaysCount) %>
<%= UI.widget.groupbox({title: 'Пересечения', collapsible: true }, panelOverlap) %>
<% } %>
<% $template panelDaysCount %>
<%= UI.widget.render('leave@days_count', {
readonly: true,
first_date: Context.data.date_start,
second_date: Context.data.date_end,
days_count: Context.data.days,
working_hours: ViewContext.data.workminutes
} ) %>
<% $endtemplate %>
<% $template panelOverlap %>
<%= UI.widget.render('leave@vacation_overlap', {
readonly: true,
first_date: Context.data.date_start,
second_date: Context.data.date_end,
} ) %>
<% $endtemplate %>
Получаем следующий результат:
На данный момент виджет находит пересечения только с нашими собственными отпусками. Перенесём сценарий поиска в серверную часть, чтобы можно было получить все отпуска.
Полностью скопируем код из клиентского сценария, изменив название функции с onInit() на getVacationOverlap().
Функцию onInit перепишем следующим образом:
Код:
async function onInit(): Promise<void> {
if (!Context.data.first_date || !Context.data.second_date) {
return;
}
await Server.rpc.getVacationOverlap();
}
Обратите внимание, что проверку на то, заполнены ли поля с датами, мы оставили и на клиентской стороне. Это позволит нам не ждать сервер, если данных ещё нет.
В серверном коде проверку также оставили: она не влияет на быстродействие, но поможет избежать ошибок.
Теперь на форме отображаются также отпуска, созданные другими пользователями:
В заключение добавим фильтрацию по статусу Согласован и типу отпуска Основной оплачиваемый:
Код:
// Только со статусом Согласован
vacation_searcher.where(vacation => vacation.__status.eq(Context.fields.vacation.app.fields.__status.variants.agreed.id));
const leave_type_main = await Context.fields.vacation_type.app.search()
.where(vt => vt.__name.eq('Основной оплачиваемый'))
.first();
// Если есть такой вид отпуска, как Основной оплачиваемый
if (leave_type_main) {
// то ищем только по таким элементам
// условие добавится через И
vacation_searcher.where(vacation => vacation.leave_type.link(leave_type_main));
}
// Выполняем запрос и сохраняем результат в переменную
const vacations = await vacation_searcher.all();
В итоге с учётом условий осталось только одно пересечение:
Пример 2. Получение списка телефонов контактов из определённых компаний.
Необходимо получить список телефонных номеров контактов для рассылки рекламных сообщений. Контакты должны быть связаны с компаниями, которые будут фильтроваться по полю Отрасль. Рассылка сообщений будет происходить в отдельном процессе, который реализовать не нужно.
Делаем решение «в лоб».
Для начала получим список компаний с фильтрацией по отрасли. Нужно учесть, что отрасль могут не задать. Тогда нужно найти вообще все компании.
Код:
/**
* Получить список компаний, отфильтрованный по полю Отрасль
* @param limit Размер выборки, по умолчанию - 1000
* @returns Список компаний
*/
async function getCompanies(limit: number = 1000): Promise<ApplicationItem<Application$_clients$_companies$Data, any>[]> {
const companySearch = Context.fields.company.app.search();
if (Context.data.industry) {
companySearch.where(it => it._industries.has(Context.data.industry!.id));
}
return await companySearch.size(limit).all();
}
После получения списка компаний мы будем перебирать их, получать список контактов и доставать их телефонные номера.
Код:
const EMPTY_UID = '00000000-0000-0000-0000-000000000000';
/**
* Получить список контактов
* @returns Список телефонных номеров
*/
async function getContactPhones() {
const companies = await getCompanies();
const phones: TPhone<PhoneType>[] = [];
for (let company of companies) {
const contactRef = company.data._contacts?.find(item => !!item.id);
if (contactRef && contactRef.id != EMPTY_UID) {
const contact = await contactRef.fetch();
if (contact && contact.data._phone && contact.data._phone.length > 0) {
phones.push(...contact.data._phone);
}
}
}
Context.data.phones = phones;
}
Этот способ позволяет нам получить список телефонных номеров, однако операция выполняется очень долго. Причина в том, что в цикле мы отправляем много запросов. Каждый отдельный запрос выполняется быстро, но когда происходит серия таких запросов, и все они выполняются последовательно, система выдаёт результат только через несколько десятков секунд.
Чтобы ускорить получение контактов, попробуем выполнить все запросы параллельно:
Код:
async function getContactPhones() {
const companies = await getCompanies();
const phones: TPhone<PhoneType>[] = [];
await Promise.all(companies.map(async (company) => {
const contactRef = company.data._contacts?.find(item => !!item.id);
if (contactRef && contactRef.id != EMPTY_UID) {
const contact = await contactRef.fetch();
if (contact && contact.data._phone && contact.data._phone.length > 0) {
phones!.push(...contact.data._phone);
}
}
}));
Context.data.fetchalldebug += `count=${phones.length}`;
Context.data.phones = phones;
}
В этом примере запросы отправляются одновременно, и в целом мы получаем результат быстрее. Но при большом количестве параллельных запросов могут происходить непредсказуемые зависания. Поэтому данный метод подойдет только для поиска по небольшим объёмам данных. К тому же, на сервер по-прежнему отправляется много запросов, что может негативно сказаться на его производительности.
Попробуем еще раз оптимизировать код — перенесём функцию в серверный сценарий и вызовем её из клиентского:
Код:
async function getContactPhones() {
await Server.rpc.getContactPhones();
}
Этот способ также позволяет получить список компаний. По времени это занимает чуть дольше, чем предыдущий вариант, зато мы отправляем на сервер всего один запрос. Конечно, скорость запроса будет зависеть от загрузки сервера, но в целом мы получили способ с неплохой производительностью. К тому же, такой подход позволит искать компании и контакты с повышенными привилегиями.
Если для задачи важна скорость и нет проблем с правами, можно ещё сильнее оптимизировать запрос. Вместо перебора компаний и получения контактов в цикле сразу получим все контакты через поиск:
Код:
async function getContactPhones() {
const companies = await getCompanies();
const contacts = await Context.fields.contact.app.search()
.where(x => x.__deletedAt.eq(null))
.where(x => x._companies.link(companies)).size(10000)
.all();
const phones: TPhone<PhoneType>[] = [];
for (let company of companies) {
const contactRef = company.data._contacts?.find(item => !!item.id);
if (contactRef && contactRef.id != EMPTY_UID) {
const contact = contacts.find(item => item.id === contactRef.id);
if (contact && contact.data._phone && contact.data._phone.length > 0) {
phones!.push(...contact.data._phone);
}
}
}
Context.data.phones = phones;
}
В этом варианте выполняется всего два запроса — получение списка компаний и получение списка контактов. Это происходит довольно быстро, и мы не нагружаем сервер большой очередью запросов. С более детальным сравнением методов можно ознакомиться в статье «Как достать много данных?» в ELMA365 Community.