Бизнес кейс
Реализовать возможность выбора Контактов по первой букве фамилии, кликнув на определенную букву в отображаемом алфавите.
Вкладка с контактами.
Постановка
Форма просмотра Компании:
- На вкладке Контакты фильтрация контактов компании по ФИО, Телефон, Статус и Почта.
- Для отфильтрованных записей постраничная навигация и алфавитный указатель по первой букве фамилии.
- Контакты в статусе Архив отображать внизу списка.
Решение
Решения с пагинацией уже были https://community.elma365.com/ru/threads/3116/.
В данном случае к пагинации добавлена логика с алфавитным указателем.
Реализация фильтра при помощи Панели с заголовком, в которой размещены поля для фильтрации. На кнопке Поиск привязан сценарий.
Для отображения полученных контактов используется Динамический список с виджетом для отображения информации по Контакту
На кнопках нумератора страниц вызов метода changeContactsPage с формированием таблицы контактов для текущей буквы алфавитного указателя.
На кнопках алфавитного указателя вызов метода changeContactsAlphabetLetter с формированием таблицы контактов для первой страницы по выбранной букве.
Листинг нумератора и указателя
Код:
<style>
.pagination-container {
display: flex;
width: 100%;
flex-direction: row;
flex-wrap: nowrap;
gap: 0.3em;
justify-content: center;
padding: 1em;
}
</style>
<% if (ViewContext.data.contact_pages_count > 1 && ViewContext.data.contact_current_page_number) { %>
<div class="pagination-container">
<% if (ViewContext.data.contact_pages_count > 5) { %>
<button type="button" class="btn btn-default" onclick="<%=Scripts%>.changeContactsPage(1)">««</button>
<button type="button" class="btn btn-default" onclick="<%=Scripts%>.changeContactsPage(<%=Math.max(...[1, ViewContext.data.contact_current_page_number - 1])%>)">«</button>
<% } %>
<% for (let i = Math.max(...[1, ViewContext.data.contact_current_page_number - 2]); i <= Math.min(...[ViewContext.data.contact_pages_count, ViewContext.data.contact_current_page_number + 2]); i++) { %>
<% if (i == ViewContext.data.contact_current_page_number) { %>
<button type="button" class="btn btn-primary"><%=i.toString()%></button>
<% } else { %>
<button type="button" class="btn btn-default" onclick="<%=Scripts%>.changeContactsPage(<%=i%>)"><%=i.toString()%></button>
<% } %>
<% } %>
<% if (ViewContext.data.contact_pages_count > 5) { %>
<button type="button" class="btn btn-default" onclick="<%=Scripts%>.changeContactsPage(<%=Math.min(...[ViewContext.data.contact_current_page_number + 1, ViewContext.data.contact_pages_count])%>)">»</button>
<button type="button" class="btn btn-default" onclick="<%=Scripts%>.changeContactsPage(<%=ViewContext.data.contact_pages_count%>)">»»</button>
<% } %>
</div>
<% } %>
Код:
<style>
.alphabetic-container {
width: 100%;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-evenly;
align-items: baseline;
padding: 1em;
}
.alphabetic-element {
color: var(--color-theme-main);
padding: 0.5rem;
cursor: pointer;
user-select: none;
}
.alphabetic-element:hover {
color: var(--color-theme-main-darken-10);
}
.alphabetic-element.active {
color: var(--color-theme-main-darken-10);
font-size: 1.5em;
cursor: default;
}
</style>
<% if (ViewContext.data.contact_alphabetical_index?.length > 0 && ViewContext.data.contact_current_alphabet_letter) { %>
<div class="alphabetic-container">
<% for (let el of ViewContext.data.contact_alphabetical_index) { %>
<% if (el == ViewContext.data.contact_current_alphabet_letter) { %>
<span class="alphabetic-element active"><%=el%></span>
<% } else {%>
<span class="alphabetic-element" onclick="<%=Scripts%>.changeContactsAlphabetLetter('<%=el%>')"><%=el%></span>
<% } %>
<% } %>
</div>
<% } %>
Получение данных по компании выполняется асинхронно при инициализации формы просмотра в методе onInit, в том числе и данные по Контактам.
При получении контактов по компании используется сортировка по статусу контакта, а затем группировка полученного массива по первой букве фамилии.
После получения данных выполняется заполнение вкладки Контакты (чтобы не перетирать ViewContext при серверных вызовах).
При заполнении выполняется сортировка ключей сгруппированных контактов для формирования данных алфавитного указателя и вызов метода changeContactsAlphabetLetter для первой буквы.
При нажатии на кнопку Поиск выполняется фильтрация контактов компании по заданным фильтрам, а затем группировка отфильтрованных контактов по первой букве фамилии с формированием данных алфавитного указателя и вызовом метода changeContactsAlphabetLetter для заданной буквы.
Листинг клиента
Код:
/**
* Контакты по первой букве фамилии для фильтрации на форме
*/
type ContactsByLetters = {
[key: string]: ReturnType<typeof Application.fields.contacts.app.create>[],
};
let contacts_by_company: ReturnType<typeof Application.fields.contacts.app.create>[] = [];
let contacts_by_letters: ContactsByLetters = {};
let rows_max_count = 8;
/**
* Получение Контактов
*/
async function getContacts(): Promise<void> {
contacts_by_company = await Application.fields.contacts.app.search()
.where((f, g) => g.and(
f.__deletedAt.eq(null),
f.companies.has(Context.id)
))
.sort('__status', true)
.size(10000)
.all();
contacts_by_letters = groupByLetter(contacts_by_company, '_fullname');
}
/**
* Заполнение вкладки Контакты
*
* @remarks вызов после получения контактов
*/
async function setContactsTab(): Promise<void> {
ViewContext.data.contact_alphabetical_index = Object.keys(contacts_by_letters).sort();
changeContactsAlphabetLetter(ViewContext.data.contact_alphabetical_index[0]);
}
function groupByLetter(items: any[], key: string): { [key: string]: ReturnType<typeof Application.fields.contacts.app.create>[] } {
return items.reduce(function (result, item) {
const result_key = item.data[key]?.lastname[0].toUpperCase() ?? '~';
if (!result[result_key]) {
result[result_key] = [];
}
result[result_key].push(item);
return result;
}, {});
}
function changeContactsAlphabetLetter(alphabet_letter: string) {
const contacts_by_letter = contacts_by_letters[alphabet_letter] ?? [];
ViewContext.data.contact_pages_count = Math.ceil(contacts_by_letter.length / rows_max_count);
ViewContext.data.contact_current_alphabet_letter = alphabet_letter;
createContactsTable(1, contacts_by_letter);
ViewContext.data.contact_current_page_number = 1;
}
function changeContactsPage(page_number: number) {
createContactsTable(page_number, contacts_by_letters[ViewContext.data.contact_current_alphabet_letter ?? '~']);
ViewContext.data.contact_current_page_number = page_number;
}
function createContactsTable(page_number: number, contacts: ReturnType<typeof Application.fields.contacts.app.create>[]) {
const contacts_table = ViewContext.fields.contacts_table.create();
for (let contact of contacts.slice((page_number - 1) * rows_max_count, page_number * rows_max_count)) {
const row = contacts_table.insert();
row.contact = contact;
row.phones = contact.data._phone ?? [];
row.emails = contact.data.emails ?? [];
row.status_name = contact.data.__status?.name ?? '';
}
ViewContext.data.contacts_table = contacts_table;
}
/**
* При нажатии на кнопку Поиск на вкладке Контакты
*/
async function onButtonSearchContacts(): Promise<void> {
const contacts = contacts_by_company.filter(function (item) {
const fullname_search = ViewContext.data.contact_fullname_searchstring?.split(/\s+/gm)
.filter(Boolean)
.map(x => x.toLowerCase());
if (fullname_search && (!item.data._fullname ||
!fullname_search.every(x => item.data._fullname?.firstname.toLowerCase().includes(x) || item.data._fullname?.lastname.toLowerCase().includes(x) || item.data._fullname?.middlename.toLowerCase().includes(x)))) {
return false;
}
if (ViewContext.data.contact_phone_searchstring && !item.data._phone?.some(x => x.tel.includes(ViewContext.data.contact_phone_searchstring!))) {
return false;
}
if (ViewContext.data.contact_email_searchstring && !item.data.emails?.some(x => x.email.includes(ViewContext.data.contact_email_searchstring!))) {
return false;
}
if (ViewContext.data.contact_status && item.data.__status?.code !== ViewContext.data.contact_status.code) {
return false;
}
return true;
});
contacts_by_letters = groupByLetter(contacts, '_fullname');
ViewContext.data.contact_alphabetical_index = Object.keys(contacts_by_letters).sort();
changeContactsAlphabetLetter(ViewContext.data.contact_alphabetical_index.find((x: string) => x === ViewContext.data.contact_current_alphabet_letter) ?? ViewContext.data.contact_alphabetical_index[0]);
}
Дополнительно возможно добавить сброс контактов при повторном клике на выбранную букву. Для этого достаточно перенести объявление отфильтрованных контактов в глобал, сделать опциональным параметр alphabet_letter в методе changeContactsAlphabetLetter и добавить вызов метода в onclick активной буквы.
Код:
let contacts_by_filter: ReturnType<typeof Application.fields.contacts.app.create>[] = [];
/**
* Получение Контактов
*/
async function getContacts(): Promise<void> {
contacts_by_company = await Application.fields.contacts.app.search()
.where((f, g) => g.and(
f.__deletedAt.eq(null),
f.companies.has(Context.id)
))
.sort('__status', true)
.size(10000)
.all();
contacts_by_filter = contacts_by_company;
...
}
function changeContactsAlphabetLetter(alphabet_letter?: string) {
const contacts_by_letter = contacts_by_letters[alphabet_letter] ?? contacts_by_filter;
...
}
/**
* При нажатии на кнопку Поиск на вкладке Контакты
*/
async function onButtonSearchContacts(): Promise<void> {
contacts_by_filter = contacts_by_company.filter(function (item) {
...
});
contacts_by_letters = groupByLetter(contacts_by_filter, '_fullname');
...
}