dana-medical-site/internal/api/adminui/weekly-experiment.js
2026-10-14 13:33:13 +00:00

103 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* Full-viewport presentation of the real weekly-report table.
No demo periods, metrics or contact numbers: WeeklyDashboard owns all data. */
const WeeklyFullscreen = {
ensure() {
if (this.dialog) return;
const dialog = el('dialog', { id: 'weekly-fullscreen', className: 'weekly-experiment', ariaLabel: ReportClinic.brand + ' — маркетинговая аналитика по неделям' });
const layout = el('div', { className: 'wke-layout' });
const toolbar = el('div', { className: 'wke-toolbar' });
const modes = el('div', { className: 'wke-modes', role: 'group', ariaLabel: 'Периодичность отчёта' });
for (const [mode, label] of [['weeks', 'По неделям'], ['months', 'По месяцам']]) {
const button = el('button', { type: 'button', className: 'wke-mode', textContent: label });
button.dataset.mode = mode;
button.onclick = () => WeeklyDashboard.setMode(mode);
modes.append(button);
}
const status = el('span', { className: 'wke-status', role: 'status', ariaLive: 'polite' });
const refresh = el('button', { type: 'button', className: 'wke-refresh', textContent: 'Обновить', title: 'Загрузить актуальные данные отчётов' });
refresh.onclick = () => WeeklyDashboard.load();
const close = el('button', { type: 'button', className: 'wke-close', ariaLabel: 'Вернуться в админку', title: 'Вернуться в админку · Esc', textContent: '×' });
close.onclick = () => dialog.close();
toolbar.append(modes, status, refresh, close);
const scroll = el('div', { className: 'wke-scroll' });
const empty = el('div', { className: 'wke-empty', role: 'status', hidden: true });
layout.append(toolbar, empty, scroll);
dialog.append(layout);
scroll.addEventListener('scroll', () => {
dialog.classList.toggle('wke-scrolled', scroll.scrollLeft > 0);
dialog.classList.toggle('wke-scrolled-y', scroll.scrollTop > 0);
}, { passive: true });
dialog.addEventListener('close', () => {
// Return the actual nodes (including budget/filter handlers), not copies.
this.tableHome.append(this.table);
this.filterHome.append(this.filter);
document.body.style.overflow = this.savedOverflow;
document.title = this.savedTitle;
});
document.body.append(dialog);
Object.assign(this, { dialog, toolbar, modes, status, refresh, close, scroll, empty });
this.resizeObserver = new ResizeObserver(() => this.resize());
this.resizeObserver.observe(scroll);
},
async open() {
this.ensure();
if (this.dialog.open) return;
this.table = $('#wk-table');
this.filter = $('#wk-filter');
this.tableHome = this.table.parentElement;
this.filterHome = this.filter.parentElement;
this.savedOverflow = document.body.style.overflow;
this.savedTitle = document.title;
this.scroll.append(this.table);
this.toolbar.insertBefore(this.filter, this.refresh);
this.scroll.scrollTop = 0;
this.scroll.scrollLeft = 0;
this.dialog.classList.remove('wke-scrolled', 'wke-scrolled-y');
document.body.style.overflow = 'hidden';
document.title = ReportClinic.brand + ' — маркетинговая аналитика';
this.dialog.showModal();
this.close.focus({ preventScroll: true });
await WeeklyDashboard.load();
},
sync() {
if (!this.dialog?.open) return;
const { list, loading, error } = WeeklyDashboard;
const periods = WeeklyDashboard.periods();
const monthly = WeeklyDashboard.mode === 'months';
for (const button of this.modes.children) button.setAttribute('aria-pressed', String(button.dataset.mode === WeeklyDashboard.mode));
// Keep the page's top tabs in sync when the mode changes inside the report.
for (const button of document.querySelectorAll('#rp-tabs button[data-mode]')) button.setAttribute('aria-selected', String(button.dataset.mode === WeeklyDashboard.mode));
const heading = $('#tab-reports .rp-head h1');
if (heading) heading.textContent = monthly ? 'Дашборд по месяцам' : 'Дашборд по неделям';
this.dialog.setAttribute('aria-label', ReportClinic.brand + ' — маркетинговая аналитика ' + (monthly ? 'по месяцам' : 'по неделям'));
const visible = WeeklyDashboard.filtered();
this.table.style.setProperty('--wk-period-count', visible.length);
this.refresh.disabled = loading;
this.dialog.setAttribute('aria-busy', String(loading));
this.status.textContent = loading ? 'Загрузка отчётов…'
: error ? `${error}${list.length ? ' · показаны ранее загруженные данные' : ''}`
: `Периодов: ${periods.length}${visible.length !== periods.length ? ' · показано: ' + visible.length : ''}`;
this.status.classList.toggle('wke-status--error', Boolean(error));
this.scroll.hidden = false;
this.empty.hidden = Boolean(visible.length);
this.empty.textContent = loading ? 'Загрузка реальных данных…'
: error ? 'Не удалось загрузить отчёты. Нажмите «Обновить», чтобы повторить.'
: periods.length ? 'За выбранные даты отчётов нет. Измените даты или нажмите «Показать все».'
: monthly ? 'Месячных отчётов пока нет. Загрузите выгрузку 1С за полный календарный месяц во вкладке «Новый отчёт». Недельные отчёты не суммируются, чтобы не задвоить пациентов.'
: 'Недельных отчётов пока нет. Загрузите выгрузку 1С с указанным периодом во вкладке «Новый отчёт».';
this.resize();
},
resize() {
if (!this.dialog?.open) return;
const style = getComputedStyle(this.dialog);
const frozenWidth = parseFloat(style.getPropertyValue('--wk-channel-width')) + parseFloat(style.getPropertyValue('--wk-metric-width'));
const count = Math.max(WeeklyDashboard.filtered().length, 1);
// Few actual weeks must not stretch into enormous columns or get fake dates.
const width = Math.max(112, Math.min(160, (this.scroll.clientWidth - frozenWidth - 24) / count));
this.table.style.setProperty('--wk-period-width', width + 'px');
},
};