mapdash/frontend/index.html
aaverbitskiy 5cabad9c33 feat: all-surfaces timeline + UI polish (v0.2.0)
Timeline can now list the entire inventory (occupied + free + never
booked), not just booked surfaces. New manager-only "Все поверхности"
checkbox (on by default; forced on and hidden for anonymous visitors so
they can spot what is currently free with no future bookings).

/api/boards gains all_surfaces: sources rows from board_info and unions
pf_board so a just-booked surface not yet in the inventory snapshot still
gets a row. Surface facets (city/dimension/search) narrow rows; booking
facets (brand/manager/status) narrow to booked surfaces; dates are a
window that narrows bars, not the surface set.

UI polish:
- timeline tooltip flips up/left near viewport edges
- anonymous "Войти" button filled accent, inverts to white-on-accent hover
- appearance button uses a gear icon instead of "+"
- header counter shows only "Поверхности: N"
- map "всего" counter includes surfaces without coordinates

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-14 14:48:25 +00:00

194 lines
9.4 KiB
HTML
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.

<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Адресная программа</title>
<script>
// Theme is set before first paint (no flash). Dark theme is temporarily
// hidden — force light regardless of stored/OS preference. To re-enable,
// restore the localStorage/prefers-color-scheme logic and unhide the toggle.
document.documentElement.setAttribute('data-theme', 'light');
// Role is unknown until Keycloak check-sso resolves (async). Start in the
// "pending" state that hides manager-only controls, so anonymous visitors
// never see a flash of them before applyRoleUi() runs.
document.documentElement.classList.add('role-pending');
</script>
</head>
<body>
<div id="load-bar" aria-hidden="true"></div>
<header>
<!-- Group 1: brand + counters -->
<div class="hgroup hgroup-brand">
<div class="brand">
<div class="brand-name">Green Media</div>
<div class="brand-sub">Адресная программа</div>
<div id="status"></div>
</div>
</div>
<!-- Group 1b: view mode switch -->
<div class="hgroup hgroup-modes">
<div class="mode-switch">
<button type="button" class="mode-btn active" id="mode-timeline">График</button>
<button type="button" class="mode-btn" id="mode-map">Карта</button>
<button type="button" class="mode-btn" id="mode-split">Карта и график</button>
</div>
<button type="button" class="theme-toggle" id="theme-toggle" title="Переключить тему" aria-label="Переключить тему"></button>
</div>
<!-- Group 2: fields (4-column grid; dates + search on the 2nd row) -->
<div class="hgroup hgroup-fields">
<div class="field">
<label>Город</label>
<div class="dropdown" id="city-dropdown">
<button type="button" class="dropdown-btn" id="city-btn">
<span id="city-btn-text">Все города</span>
<span class="caret"></span>
</button>
<div class="dropdown-panel" id="city-panel"></div>
</div>
</div>
<div class="field manager-only">
<label>Бренд</label>
<div class="dropdown" id="brand-dropdown">
<button type="button" class="dropdown-btn" id="brand-btn">
<span id="brand-btn-text">Все бренды</span>
<span class="caret"></span>
</button>
<div class="dropdown-panel" id="brand-panel"></div>
</div>
</div>
<div class="field">
<label>Размер</label>
<div class="dropdown" id="dimension-dropdown">
<button type="button" class="dropdown-btn" id="dimension-btn">
<span id="dimension-btn-text">Все размеры</span>
<span class="caret"></span>
</button>
<div class="dropdown-panel" id="dimension-panel"></div>
</div>
</div>
<div class="field manager-only">
<label>Менеджер</label>
<div class="dropdown" id="manager-dropdown">
<button type="button" class="dropdown-btn" id="manager-btn">
<span id="manager-btn-text">Все менеджеры</span>
<span class="caret"></span>
</button>
<div class="dropdown-panel" id="manager-panel"></div>
</div>
</div>
<div class="field">
<label for="date-start">Дата начала (от)</label>
<input type="date" id="date-start" />
</div>
<div class="field">
<label for="date-end">Дата окончания (до)</label>
<input type="date" id="date-end" />
</div>
<div class="field manager-only">
<label>Статус</label>
<div class="dropdown" id="status-dropdown">
<button type="button" class="dropdown-btn" id="status-btn">
<span id="status-btn-text">Все статусы</span>
<span class="caret"></span>
</button>
<div class="dropdown-panel" id="status-panel"></div>
</div>
</div>
<div class="field">
<label for="search">Поиск (адрес / код)</label>
<input type="text" id="search" placeholder="например, Азина" />
</div>
</div>
<!-- Group 3: scale + appearance + checkboxes (compact stack) -->
<div class="hgroup hgroup-controls">
<div class="field zoom-field">
<label for="zoom-slider">Масштаб: <span id="zoom-label">12 месяцев</span></label>
<div class="zoom-row">
<input type="range" id="zoom-slider" min="0" max="9" step="1" value="3" />
</div>
</div>
<div class="checks manager-only">
<div class="checks-row">
<div class="field checkbox-field">
<label for="show-brand"><input type="checkbox" id="show-brand" checked /> Бренд</label>
</div>
<div class="field checkbox-field">
<label for="show-company"><input type="checkbox" id="show-company" /> Компания</label>
</div>
</div>
<div class="field checkbox-field">
<label for="show-collisions"><input type="checkbox" id="show-collisions" checked /> Коллизии размещения</label>
</div>
<div class="field checkbox-field">
<label for="show-all-surfaces"><input type="checkbox" id="show-all-surfaces" checked /> Все поверхности</label>
</div>
</div>
</div>
<!-- Login / logout button, pinned to the header's top-right corner -->
<div class="auth-wrap">
<button type="button" class="auth-btn" id="auth-btn" style="display:none">Войти</button>
</div>
<!-- Appearance ("+") button, pinned to the header's bottom-right corner -->
<div class="decor-wrap manager-only">
<button type="button" class="decor-btn" id="decor-btn" aria-label="Оформление" data-tip="Оформление"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></button>
<div class="decor-menu" id="decor-menu"></div>
</div>
</header>
<div id="filter-chips" class="chips" style="display:none"></div>
<div id="view" class="mode-timeline">
<div id="pane-timeline">
<div id="chart-wrap">
<div id="timeline"></div>
<div id="col-resizer" title="Потяните, чтобы изменить ширину колонки"></div>
<div id="empty" class="empty-state" style="display:none">
<svg class="empty-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
<div class="empty-title">Ничего не найдено</div>
<div class="empty-sub">Под текущие фильтры нет броней</div>
<button type="button" class="empty-reset" id="empty-reset-timeline">Сбросить фильтры</button>
</div>
</div>
</div>
<div id="split-resizer" title="Потяните, чтобы изменить пропорции"></div>
<div id="pane-map">
<div id="map"></div>
<div id="map-legend">
<span class="lg-item"><span class="lg-dot lg-free"></span><span id="lg-free">свободна</span></span>
<span class="lg-item"><span class="lg-dot lg-busy"></span><span id="lg-busy">занята</span></span>
<span class="lg-item" id="map-counter"></span>
<button type="button" class="lg-nocoords" id="map-nocoords-btn" style="display:none"></button>
</div>
<div id="map-nocoords-panel" style="display:none"></div>
<div id="map-empty" class="empty-state" style="display:none">
<svg class="empty-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 21s-7-6.3-7-11a7 7 0 0 1 14 0c0 4.7-7 11-7 11z"/><circle cx="12" cy="10" r="2.5"/></svg>
<div class="empty-title">Нет поверхностей на карте</div>
<div class="empty-sub">У выбранных поверхностей нет координат</div>
<button type="button" class="empty-reset" id="empty-reset-map">Сбросить фильтры</button>
</div>
</div>
</div>
<div id="tooltip"></div>
<div id="board-backdrop" class="bp-backdrop" hidden></div>
<aside id="board-panel" class="board-panel" hidden aria-label="Карточка поверхности">
<div class="bp-head">
<div id="board-panel-title" class="bp-title"></div>
<button type="button" class="bp-close" id="board-panel-close" aria-label="Закрыть"></button>
</div>
<div id="board-panel-body" class="bp-body"></div>
</aside>
<script type="module" src="/src/main.ts"></script>
</body>
</html>