317 lines
20 KiB
JavaScript
317 lines
20 KiB
JavaScript
/* Оболочка админки: Sidebar (органическая лента навигации), Topbar (поиск, календарь,
|
||
уведомления, профиль) и маршрутизация разделов через #адрес. */
|
||
|
||
const ICONS = {
|
||
home: '<path d="M3.5 10.5 12 3.8l8.5 6.7V19a1.5 1.5 0 0 1-1.5 1.5h-4.2v-5.8H9.2v5.8H5A1.5 1.5 0 0 1 3.5 19z"/>',
|
||
leads: '<path d="M5.2 3.8h3.3l1.7 4.3-2.1 1.3a10.5 10.5 0 0 0 5.3 5.3l1.3-2.1 4.3 1.7v3.3a1.9 1.9 0 0 1-2 1.9A15.8 15.8 0 0 1 3.3 5.8a1.9 1.9 0 0 1 1.9-2z"/>',
|
||
doctors: '<path d="M6 3.5v5.2a4 4 0 0 0 8 0V3.5"/><path d="M10 12.7v2.1a4.8 4.8 0 0 0 9.6 0v-2"/><circle cx="19.6" cy="10.8" r="1.9"/>',
|
||
catalog: '<path d="M20.4 5.9a4.9 4.9 0 0 0-6.9 0L12 7.4l-1.5-1.5a4.9 4.9 0 1 0-6.9 6.9l8.4 8.4 8.4-8.4a4.9 4.9 0 0 0 0-6.9z"/><path d="M6.5 12.2h3l1.4-2.2 2.2 4.3 1.4-2.1h3"/>',
|
||
content: '<path d="M7 3.5h7l5 5v12H7a1.5 1.5 0 0 1-1.5-1.5V5A1.5 1.5 0 0 1 7 3.5z"/><path d="M14 3.5v5h5M9.5 13h6M9.5 16.5h6"/>',
|
||
reviews: '<path d="M20.5 11.6a8 8 0 0 1-11.7 7.1L4 19.8l1.1-4.4a8 8 0 1 1 15.4-3.8z"/><path d="m12 8.4 1 2 2.2.3-1.6 1.6.4 2.2-2-1-2 1 .4-2.2-1.6-1.6 2.2-.3z"/>',
|
||
settings: '<circle cx="12" cy="12" r="3.1"/><path d="M19.2 14.7a1.6 1.6 0 0 0 .3 1.8l.1.1a1.9 1.9 0 1 1-2.7 2.7l-.1-.1a1.6 1.6 0 0 0-1.8-.3 1.6 1.6 0 0 0-1 1.5v.2a1.9 1.9 0 1 1-3.9 0v-.1a1.6 1.6 0 0 0-1-1.5 1.6 1.6 0 0 0-1.8.3l-.1.1a1.9 1.9 0 1 1-2.7-2.7l.1-.1a1.6 1.6 0 0 0 .3-1.8 1.6 1.6 0 0 0-1.5-1H3.3a1.9 1.9 0 1 1 0-3.9h.1a1.6 1.6 0 0 0 1.5-1 1.6 1.6 0 0 0-.3-1.8l-.1-.1a1.9 1.9 0 1 1 2.7-2.7l.1.1a1.6 1.6 0 0 0 1.8.3h.1a1.6 1.6 0 0 0 1-1.5v-.2a1.9 1.9 0 1 1 3.9 0v.1a1.6 1.6 0 0 0 1 1.5 1.6 1.6 0 0 0 1.8-.3l.1-.1a1.9 1.9 0 1 1 2.7 2.7l-.1.1a1.6 1.6 0 0 0-.3 1.8v.1a1.6 1.6 0 0 0 1.5 1h.2a1.9 1.9 0 1 1 0 3.9h-.1a1.6 1.6 0 0 0-1.5 1z"/>',
|
||
stats: '<path d="M4 20.5h16"/><rect x="5.5" y="12" width="3.2" height="6.5" rx="1"/><rect x="10.4" y="7" width="3.2" height="11.5" rx="1"/><rect x="15.3" y="3.5" width="3.2" height="15" rx="1"/>',
|
||
reports: '<path d="M7 3.5h7l5 5v12H7a1.5 1.5 0 0 1-1.5-1.5V5A1.5 1.5 0 0 1 7 3.5z"/><path d="M14 3.5v5h5M9 17v-3M12 17v-5M15 17v-2"/>',
|
||
logout: '<path d="M14.5 4.2A8.3 8.3 0 1 0 20 12"/><path d="M12 12h9M18 9l3 3-3 3"/>',
|
||
search: '<circle cx="11" cy="11" r="6.3"/><path d="m20 20-4.3-4.3"/>',
|
||
calendar: '<rect x="4" y="5.5" width="16" height="14.5" rx="2.5"/><path d="M8 3.5v4M16 3.5v4M4 10h16M9.5 14.5h5"/>',
|
||
bell: '<path d="M6.3 16.6V11a5.7 5.7 0 0 1 11.4 0v5.6l1.6 1.9H4.7z"/><path d="M10 20.5a2.2 2.2 0 0 0 4 0"/>',
|
||
};
|
||
const svg = (name) => `<svg viewBox="0 0 24 24" aria-hidden="true">${ICONS[name]}</svg>`;
|
||
|
||
// Разделы ленты. Подразделы — прежние вкладки: их разметка и код не менялись.
|
||
const NAV = [
|
||
{ key: 'home', label: 'Главная', icon: 'home', tabs: [['home', 'Главная']] },
|
||
{ key: 'leads', label: 'Заявки', icon: 'leads', tabs: [['leads', 'Заявки']] },
|
||
{ key: 'doctors', label: 'Врачи', icon: 'doctors', tabs: [['doctors-dash', 'Дашборд'], ['doctors', 'Список врачей'], ['onec', 'Запись через 1С']] },
|
||
{ key: 'catalog', label: 'Услуги и цены', icon: 'catalog',
|
||
tabs: [['services', 'Услуги'], ['prices', 'Прайс'], ['checkups', 'Чек-апы'], ['promos', 'Акции']] },
|
||
{ key: 'content', label: 'Контент сайта', icon: 'content',
|
||
tabs: [['posts', 'Новости и статьи'], ['banners', 'Слайды на главной'], ['pages', 'Страницы'], ['faq', 'Вопросы'],
|
||
['documents', 'Документы'], ['vacancies', 'Вакансии'], ['partners', 'Партнёры']] },
|
||
{ key: 'reviews', label: 'Отзывы', icon: 'reviews', tabs: [['reviews', 'Отзывы']] },
|
||
{ key: 'stats', label: 'Статистика', icon: 'stats', tabs: [['stats', 'Статистика']] },
|
||
{ key: 'reports', label: 'Отчёты', icon: 'reports', tabs: [['reports', 'Отчёты']] },
|
||
{ key: 'settings', label: 'Настройки', icon: 'settings', tabs: [['settings', 'Реквизиты и филиалы'], ['redirects', 'Редиректы']] },
|
||
];
|
||
const TAB_GROUP = Object.fromEntries(NAV.flatMap((g) => g.tabs.map(([t]) => [t, g])));
|
||
// Подзаголовок под названием раздела
|
||
const TAB_SUB = {
|
||
doctors: 'Управление врачами, их услугами и филиалами',
|
||
leads: 'Заявки с форм сайта: запись, обратный звонок, отклик на вакансию',
|
||
};
|
||
const TAB_TITLE = Object.fromEntries(NAV.flatMap((g) => g.tabs.map(([t, title]) => [t, g.tabs.length > 1 ? g.label : title])));
|
||
|
||
/* ---------- Sidebar ---------- */
|
||
const Sidebar = {
|
||
render() {
|
||
const nav = $('#side-nav');
|
||
nav.textContent = '';
|
||
for (const g of NAV) {
|
||
const b = el('button', { type: 'button', className: 'side__btn' });
|
||
b.dataset.group = g.key;
|
||
b.setAttribute('aria-label', g.label);
|
||
b.innerHTML = svg(g.icon) + `<span class="tip" aria-hidden="true">${g.label}</span>`;
|
||
if (g.key === 'reviews') b.append($('#rv-badge'));
|
||
if (g.key === 'leads') b.append(el('span', { id: 'ld-badge', className: 'side__badge', hidden: true }));
|
||
b.onclick = () => go(g.tabs[0][0]);
|
||
nav.append(b);
|
||
}
|
||
$('#logout').setAttribute('aria-label', 'Выйти');
|
||
$('#logout').innerHTML = svg('logout') + '<span class="tip" aria-hidden="true">Выйти</span>';
|
||
Sidebar.shape();
|
||
window.addEventListener('resize', debounce(Sidebar.shape, 100));
|
||
},
|
||
|
||
// Контур ленты: три «капсулы» (логотип, меню, низ), соединённые плавными перетяжками.
|
||
shape() {
|
||
const side = $('.side');
|
||
const h = side.clientHeight, w = side.clientWidth;
|
||
if (!h || getComputedStyle($('.side__shape')).display === 'none') return;
|
||
const logo = $('.side__logo'), nav = $('#side-nav'), foot = $('.side__foot');
|
||
const gap = (a, b) => ({ a, b, m: (a + b) / 2 });
|
||
const n1 = gap(logo.offsetTop + logo.offsetHeight + 14, nav.offsetTop - 14);
|
||
const n2 = gap(nav.offsetTop + nav.offsetHeight + 14, foot.offsetTop + 46);
|
||
const neck = 17; // насколько сужается лента в перетяжке
|
||
const r = w / 2, k = 0.55;
|
||
const down = (x0, x1, n) => `L ${x0} ${n.a} C ${x0} ${n.a + (n.m - n.a) * k}, ${x1} ${n.m - (n.m - n.a) * k}, ${x1} ${n.m} ` +
|
||
`C ${x1} ${n.m + (n.b - n.m) * k}, ${x0} ${n.b - (n.b - n.m) * k}, ${x0} ${n.b} `;
|
||
const up = (x0, x1, n) => `L ${x0} ${n.b} C ${x0} ${n.b - (n.b - n.m) * k}, ${x1} ${n.m + (n.b - n.m) * k}, ${x1} ${n.m} ` +
|
||
`C ${x1} ${n.m - (n.m - n.a) * k}, ${x0} ${n.a + (n.m - n.a) * k}, ${x0} ${n.a} `;
|
||
const d = `M ${r} 0 A ${r} ${r} 0 0 0 0 ${r} ` + down(0, neck, n1) + down(0, neck, n2) +
|
||
`L 0 ${h - r} A ${r} ${r} 0 0 0 ${w} ${h - r} ` + up(w, w - neck, n2) + up(w, w - neck, n1) +
|
||
`L ${w} ${r} A ${r} ${r} 0 0 0 ${r} 0 Z`;
|
||
$('.side__shape').setAttribute('viewBox', `0 0 ${w} ${h}`);
|
||
$('.side__shape path').setAttribute('d', d);
|
||
},
|
||
|
||
highlight(tab) {
|
||
const g = TAB_GROUP[tab];
|
||
document.querySelectorAll('.side__btn[data-group]').forEach((b) => {
|
||
if (b.dataset.group === g.key) b.setAttribute('aria-current', 'page'); else b.removeAttribute('aria-current');
|
||
});
|
||
},
|
||
};
|
||
|
||
// Шапки с фото у разделов с вкладками: кнопка «Добавить» зависит от вкладки и нажимает
|
||
// скрытую кнопку самой вкладки. «Услуги и цены» — catalogHero в services.js.
|
||
const GROUP_HERO = {
|
||
posts: ['Добавить публикацию', '#po-add'],
|
||
banners: ['Добавить слайд', '#bn-add'],
|
||
faq: ['Добавить вопрос', '#fq-add'],
|
||
documents: ['Добавить документ', '#dc-add'],
|
||
vacancies: ['Добавить вакансию', '#va-add'],
|
||
partners: ['Добавить партнёра', '#pt-add'],
|
||
reviews: ['Добавить отзыв', '#rv-add'],
|
||
};
|
||
function groupHero(tab) {
|
||
const h = document.querySelector('[data-hero-for]:not([hidden])');
|
||
const add = h?.querySelector('[data-hero-add]');
|
||
if (!add) return;
|
||
const c = GROUP_HERO[tab];
|
||
add.hidden = !c;
|
||
if (!c) return;
|
||
add.innerHTML = svIcon(BN_ICONS.plus) + c[0];
|
||
add.onclick = () => $(c[1]).click();
|
||
}
|
||
|
||
/* ---------- маршруты ---------- */
|
||
function go(tab) {
|
||
if (location.hash.slice(1) !== tab) location.hash = tab; else route();
|
||
}
|
||
function route() {
|
||
let tab = location.hash.slice(1) || 'home';
|
||
if (!TAB_GROUP[tab]) tab = 'home';
|
||
document.body.classList.toggle('leads-view', tab === 'leads');
|
||
document.body.classList.toggle('reports-history-view', tab === 'reports' && $('#tab-reports').classList.contains('rp-history-view'));
|
||
const g = TAB_GROUP[tab];
|
||
document.querySelectorAll('main > section').forEach((s) => { s.hidden = s.id !== 'tab-' + tab; });
|
||
Sidebar.highlight(tab);
|
||
const sub = $('#subtabs');
|
||
sub.textContent = '';
|
||
if (g.tabs.length > 1) {
|
||
for (const [t, title] of g.tabs) {
|
||
const b = el('button', { type: 'button', textContent: title });
|
||
if (t === tab) b.setAttribute('aria-current', 'page');
|
||
b.onclick = () => go(t);
|
||
sub.append(b);
|
||
}
|
||
}
|
||
$('#page-title').textContent = tab === 'home' ? '' : TAB_TITLE[tab];
|
||
$('#page-title').hidden = tab === 'home';
|
||
$('#page-sub').textContent = TAB_SUB[tab] || '';
|
||
$('#page-sub').hidden = !TAB_SUB[tab];
|
||
// у раздела со своей шапкой (data-hero) общий заголовок не нужен
|
||
if ($('#tab-' + tab)?.hasAttribute('data-hero')) { $('#page-title').hidden = true; $('#page-sub').hidden = true; }
|
||
// подразделы — внутри шапки раздела, если в ней есть место для них
|
||
// общие шапки групп разделов (data-hero-for="services prices …")
|
||
document.querySelectorAll('[data-hero-for]').forEach((h) => { h.hidden = !h.dataset.heroFor.split(' ').includes(tab); });
|
||
if (typeof catalogHero === 'function') catalogHero(tab);
|
||
groupHero(tab);
|
||
const slot = document.querySelector('[data-hero-for]:not([hidden]) [data-subtabs-slot]') || $('#tab-' + tab)?.querySelector('[data-subtabs-slot]');
|
||
if (!Sidebar.subtabsHome) Sidebar.subtabsHome = sub.previousElementSibling;
|
||
if (slot) slot.append(sub); else if (sub.parentElement !== Sidebar.subtabsHome.parentElement) Sidebar.subtabsHome.after(sub);
|
||
document.title = (tab === 'home' ? 'Панель клиники' : TAB_TITLE[tab]) + ' — DANA';
|
||
if (tab === 'home') Dashboard.load();
|
||
if (tab === 'leads') Leads.load();
|
||
if (tab === 'stats') Stats.open();
|
||
if (tab === 'reports') Reports.open();
|
||
if (tab === 'doctors-dash') DoctorDash.open();
|
||
if (tab === 'onec') Onec.open();
|
||
window.scrollTo({ top: 0 });
|
||
}
|
||
window.addEventListener('hashchange', route);
|
||
|
||
/* ---------- Topbar ---------- */
|
||
const Topbar = {
|
||
searchIndex: null,
|
||
|
||
init(user) {
|
||
const name = user.name || 'Администратор';
|
||
$('#who-name').textContent = name;
|
||
$('#who-mail').textContent = user.email;
|
||
$('#who-avatar').textContent = (name.match(/\S+/g) || ['A']).slice(0, 2).map((w) => w[0]).join('').toUpperCase();
|
||
$('#search-btn').innerHTML = '<span>' + svg('search') + '</span>';
|
||
$('#cal-btn').insertAdjacentHTML('afterbegin', svg('calendar'));
|
||
// сегодняшняя дата рядом с календарём, как в шапке референса
|
||
const now = new Date();
|
||
$('#cal-date').innerHTML = `<b>${new Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' }).format(now).replace(' г.', '')}</b>` +
|
||
`<small>${new Intl.DateTimeFormat('ru-RU', { weekday: 'long' }).format(now).replace(/^./, (c) => c.toUpperCase())}</small>`;
|
||
$('#bell-btn').insertAdjacentHTML('afterbegin', svg('bell'));
|
||
|
||
const input = $('#search-q');
|
||
input.addEventListener('input', debounce(() => Topbar.search(input.value), 150));
|
||
input.addEventListener('focus', () => { if (input.value.trim()) Topbar.search(input.value); });
|
||
input.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape') { input.value = ''; Topbar.closeAll(); }
|
||
if (e.key === 'ArrowDown') { $('#search-results button')?.focus(); e.preventDefault(); }
|
||
});
|
||
$('#search-btn').onclick = () => { input.focus(); Topbar.search(input.value); };
|
||
$('#cal-btn').onclick = (e) => { e.stopPropagation(); Topbar.toggle('#cal-pop'); };
|
||
$('#bell-btn').onclick = (e) => { e.stopPropagation(); Topbar.toggle('#bell-pop'); };
|
||
document.addEventListener('click', (e) => { if (!e.target.closest('.search, .popover, .round')) Topbar.closeAll(); });
|
||
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') Topbar.closeAll(); });
|
||
},
|
||
|
||
closeAll() {
|
||
$('#search-results').hidden = true;
|
||
['#cal-pop', '#bell-pop'].forEach((id) => { $(id).hidden = true; });
|
||
['#cal-btn', '#bell-btn'].forEach((id) => $(id).setAttribute('aria-expanded', 'false'));
|
||
},
|
||
toggle(id) {
|
||
const open = $(id).hidden;
|
||
Topbar.closeAll();
|
||
$(id).hidden = !open;
|
||
$(id === '#cal-pop' ? '#cal-btn' : '#bell-btn').setAttribute('aria-expanded', String(open));
|
||
},
|
||
|
||
// Поиск по уже загруженным спискам админки: врачи, услуги, чек-апы, публикации, страницы.
|
||
async buildIndex() {
|
||
const [news, articles, pages] = await Promise.all(['/admin/news', '/admin/articles', '/admin/pages']
|
||
.map((p) => api(p).then((r) => r.data || []).catch(() => [])));
|
||
const idx = [];
|
||
for (const d of adminDoctors) idx.push({ text: fullName(d) + ' ' + (d.position || ''), title: fullName(d), kind: 'Врач', open: () => { go('doctors'); editDoctor(d); } });
|
||
for (const s of servicesCache) idx.push({ text: s.name, title: s.name, kind: s.parent_id ? 'Услуга' : 'Направление', open: () => { go('services'); editService(s); } });
|
||
for (const c of checkupsCache) idx.push({ text: c.name, title: c.name, kind: 'Чек-ап', open: () => { go('checkups'); editCheckup(c); } });
|
||
for (const [list, kind, key] of [[news, 'Новость', 'news'], [articles, 'Статья', 'articles']]) {
|
||
for (const p of list) idx.push({ text: p.title, title: p.title, kind, open: () => { go('posts'); $('#po-kind').value = key; loadPosts().then(() => editPost(p)); } });
|
||
}
|
||
for (const p of pages) idx.push({ text: p.title, title: p.title, kind: 'Страница', open: () => { go('pages'); editPage(p); } });
|
||
Topbar.searchIndex = idx;
|
||
},
|
||
|
||
async search(raw) {
|
||
const q = raw.trim().toLowerCase();
|
||
const box = $('#search-results');
|
||
if (!q) { box.hidden = true; return; }
|
||
if (!Topbar.searchIndex) await Topbar.buildIndex();
|
||
const hits = Topbar.searchIndex.filter((x) => x.text.toLowerCase().includes(q)).slice(0, 12);
|
||
box.textContent = '';
|
||
if (!hits.length) box.append(el('div', { className: 'search__empty', textContent: 'Ничего не нашлось. Ищем по врачам, услугам, чек-апам, публикациям и страницам.' }));
|
||
for (const h of hits) {
|
||
const b = el('button', { type: 'button' });
|
||
b.append(el('span', { textContent: h.title }), el('small', { textContent: h.kind }));
|
||
b.onclick = () => { Topbar.closeAll(); $('#search-q').value = ''; h.open(); };
|
||
b.onkeydown = (e) => {
|
||
if (e.key === 'ArrowDown') { b.nextElementSibling?.focus(); e.preventDefault(); }
|
||
if (e.key === 'ArrowUp') { (b.previousElementSibling || $('#search-q')).focus(); e.preventDefault(); }
|
||
};
|
||
box.append(b);
|
||
}
|
||
box.hidden = false;
|
||
},
|
||
|
||
// Календарь: реальные сроки акций и слайдов на главной на ближайшие 60 дней.
|
||
calendar(items) {
|
||
const box = $('#cal-list');
|
||
box.textContent = '';
|
||
if (!items.length) { box.append(el('div', { className: 'popover__empty', textContent: 'В ближайшие 60 дней акций и слайдов со сроками нет.' })); return; }
|
||
const fmt = new Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'short' });
|
||
for (const it of items) {
|
||
const b = el('button', { type: 'button', className: 'popover__item' });
|
||
const kind = it.kind === 'promo' ? 'Акция' : 'Слайд';
|
||
b.append(el('b', { textContent: fmt.format(new Date(it.at)).replace('.', '') }));
|
||
const t = el('span', { textContent: it.title });
|
||
t.append(el('small', { textContent: `${kind} · ${it.event === 'ends' ? 'последний день показа' : 'начало показа'}` }));
|
||
b.append(t);
|
||
b.onclick = () => { Topbar.closeAll(); go(it.kind === 'promo' ? 'promos' : 'banners'); };
|
||
box.append(b);
|
||
}
|
||
},
|
||
|
||
// Уведомления: незакрытые дела по данным базы, без выдуманных событий.
|
||
todo(items) {
|
||
const TEXT = {
|
||
new_leads: ['новых заявок', 'Ответьте пациентам', 'leads'],
|
||
pending_reviews: ['отзывов ждут проверки', 'Опубликуйте или отклоните', 'reviews'],
|
||
doctors_without_services: ['врачей без услуг', 'Отметьте, какие услуги ведут', 'doctors'],
|
||
doctors_without_branches: ['врачей без филиала', 'Укажите, где принимают', 'doctors'],
|
||
doctors_without_photo: ['врачей без фото', 'Загрузите фото', 'doctors'],
|
||
patients_page_hidden: ['страница «Пациенту» скрыта', 'Проверьте телефон в тексте и включите', 'pages'],
|
||
};
|
||
const box = $('#bell-list');
|
||
box.textContent = '';
|
||
$('#bell-dot').hidden = !items.length;
|
||
if (!items.length) { box.append(el('div', { className: 'popover__empty', textContent: 'Всё заполнено — дел нет.' })); return; }
|
||
for (const it of items) {
|
||
const [label, hint, tab] = TEXT[it.key] || [it.key, '', 'home'];
|
||
const b = el('button', { type: 'button', className: 'popover__item' });
|
||
b.append(el('b', { textContent: it.key === 'patients_page_hidden' ? '!' : it.count }));
|
||
const t = el('span', { textContent: label });
|
||
t.append(el('small', { textContent: hint }));
|
||
b.append(t);
|
||
b.onclick = () => {
|
||
Topbar.closeAll(); go(tab);
|
||
if (tab === 'doctors') Doctors.setFilter({ doctors_without_branches: 'no-branches', doctors_without_services: 'no-services', doctors_without_photo: 'no-photo' }[it.key] || '');
|
||
};
|
||
box.append(b);
|
||
}
|
||
},
|
||
};
|
||
|
||
/* ---------- анимации: переключатель внизу ленты ---------- */
|
||
function initMotionSwitch() {
|
||
const sw = $('#motion');
|
||
let off = false;
|
||
try { off = localStorage.getItem('dana-admin-motion') === 'off'; } catch { /* приватный режим */ }
|
||
const apply = () => {
|
||
document.documentElement.dataset.motion = off ? 'off' : 'on';
|
||
sw.setAttribute('aria-checked', String(!off));
|
||
sw.querySelector('.tip').textContent = off ? 'Анимации выключены' : 'Анимации включены';
|
||
};
|
||
sw.onclick = () => { off = !off; try { localStorage.setItem('dana-admin-motion', off ? 'off' : 'on'); } catch { /* без сохранения */ } apply(); };
|
||
apply();
|
||
}
|
||
|
||
/* ---------- запуск ---------- */
|
||
const baseShowApp = showApp;
|
||
showApp = function (user) {
|
||
baseShowApp(user);
|
||
Sidebar.render();
|
||
Topbar.init(user);
|
||
initMotionSwitch();
|
||
route();
|
||
// на главной сводку загрузит сам экран; на остальных страницах — только для шапки
|
||
if (location.hash && location.hash !== '#home') Dashboard.summary().catch(() => {});
|
||
};
|
||
boot();
|