auth ui: anon tooltip = dates only; move login button to header top-right

- timeline: RenderFlags.managerView drives the tooltip. Anonymous tier shows
  a minimal tooltip (date range + duration only) — no title, task №, status,
  manager or Planfix hint. Manager tier keeps the rich tooltip.
- header: login/logout button pinned to the top-right corner (.auth-wrap,
  absolute) instead of sitting next to the view-mode switch.

Keycloak: register bare origin in post.logout.redirect.uris so keycloak-js
logout (redirectUri without trailing path) is accepted.
This commit is contained in:
aaverbitskiy 2026-08-14 11:22:16 +00:00
parent b6b9c27cb0
commit 1eda6c1e9d
4 changed files with 43 additions and 25 deletions

View File

@ -33,7 +33,6 @@
<button type="button" class="mode-btn" id="mode-split">Карта и график</button>
</div>
<button type="button" class="theme-toggle" id="theme-toggle" title="Переключить тему" aria-label="Переключить тему"></button>
<button type="button" class="auth-btn" id="auth-btn" style="display:none">Войти</button>
</div>
<!-- Group 2: fields (4-column grid; dates + search on the 2nd row) -->
@ -123,6 +122,11 @@
</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">
<button type="button" class="decor-btn" id="decor-btn" aria-label="Оформление" data-tip="Оформление">+</button>

View File

@ -130,12 +130,13 @@ function flags(): RenderFlags {
// Anonymous tier: no labels (data is blanked anyway) and no collision
// highlighting, so every bar renders in the single neutral status colour.
if (!isManager()) {
return { withBrand: false, withCompany: false, withCollisions: false };
return { withBrand: false, withCompany: false, withCollisions: false, managerView: false };
}
return {
withBrand: el.showBrand.checked,
withCompany: el.showCompany.checked,
withCollisions: el.showCollisions.checked,
managerView: true,
};
}

View File

@ -313,6 +313,9 @@ input::placeholder { color: var(--text-muted); }
}
.theme-toggle:hover { border-color: var(--border-strong); background: var(--surface); }
/* Login / logout button — pinned to the header's top-right corner */
.auth-wrap { position: absolute; right: 18px; top: 12px; z-index: 3; }
/* Login / logout button (mirrors the toggle styling, sized to its text label) */
.auth-btn {
border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg);

View File

@ -11,6 +11,9 @@ export interface RenderFlags {
withBrand: boolean;
withCompany: boolean;
withCollisions: boolean;
// Manager tier: show the rich tooltip (title, task №, status, manager, Planfix
// hint). Anonymous tier gets a minimal tooltip with only the date range.
managerView: boolean;
}
export interface TimelineElements {
@ -81,7 +84,7 @@ export function createTimelineView(el: TimelineElements, appearance: Appearance)
const itemsDS = new DataSet<any>();
let lastBoards: Board[] = [];
let lastBookings: Booking[] = [];
let lastFlags: RenderFlags = { withBrand: true, withCompany: false, withCollisions: true };
let lastFlags: RenderFlags = { withBrand: true, withCompany: false, withCollisions: true, managerView: true };
let onBoardClick: ((boardId: string) => void) | null = null;
const options: TimelineOptions = {
@ -315,32 +318,39 @@ export function createTimelineView(el: TimelineElements, appearance: Appearance)
const statusColor = colorForStatus(appearance, bk.task_status);
const color = isCollision ? appearance.collisionColor : statusColor;
// ---- rich hover tooltip (structured HTML; values escaped) ----
// ---- hover tooltip (structured HTML; values escaped) ----
const esc = escapeHtml;
const title = brand || company || 'Без названия';
const managers = (bk.manager || []).filter(Boolean).join(', ');
const dur = durationLabel(bk.start_date, bk.end_date);
const tt: string[] = [];
tt.push('<div class="tt-head">');
tt.push(`<span class="tt-title">${esc(title)}</span>`);
if (bk.task_id) tt.push(`<span class="tt-id">№${esc(bk.task_id)}</span>`);
tt.push('</div>');
if (brand && company) tt.push(`<div class="tt-sub">${esc(company)}</div>`);
tt.push('<div class="tt-sep"></div>');
tt.push(`<div class="tt-row"><span class="tt-ico">📅</span><span>${fmtDate(bk.start_date)} ${fmtDate(bk.end_date)}${dur ? ' · ' + esc(dur) : ''}</span></div>`);
if (bk.task_status) tt.push(`<div class="tt-row"><span class="tt-dot" style="background:${statusColor}"></span><span>${esc(bk.task_status)}</span></div>`);
if (managers) tt.push(`<div class="tt-row"><span class="tt-ico">👤</span><span>${esc(managers)}</span></div>`);
if (isCollision) {
tt.push('<div class="tt-warn"><div class="tt-warn-head">⚠ Коллизия</div>');
for (const pIdx of partnerIdxs!) {
const p = bookings[pIdx]!;
const pLabel = p.brand || p.company_name || 'Без названия';
tt.push(`<div class="tt-warn-row">${esc(pLabel)} <span class="tt-warn-dates">${fmtDate(p.start_date)} ${fmtDate(p.end_date)}</span></div>`);
}
const datesRow = `<div class="tt-row"><span class="tt-ico">📅</span><span>${fmtDate(bk.start_date)} ${fmtDate(bk.end_date)}${dur ? ' · ' + esc(dur) : ''}</span></div>`;
let tooltipHtml: string;
if (!flags.managerView) {
// Anonymous tier: date range only — no title, task №, status or Planfix hint.
tooltipHtml = `<div class="tt" style="--tt-accent:${color}">${datesRow}</div>`;
} else {
const title = brand || company || 'Без названия';
const managers = (bk.manager || []).filter(Boolean).join(', ');
const tt: string[] = [];
tt.push('<div class="tt-head">');
tt.push(`<span class="tt-title">${esc(title)}</span>`);
if (bk.task_id) tt.push(`<span class="tt-id">№${esc(bk.task_id)}</span>`);
tt.push('</div>');
if (brand && company) tt.push(`<div class="tt-sub">${esc(company)}</div>`);
tt.push('<div class="tt-sep"></div>');
tt.push(datesRow);
if (bk.task_status) tt.push(`<div class="tt-row"><span class="tt-dot" style="background:${statusColor}"></span><span>${esc(bk.task_status)}</span></div>`);
if (managers) tt.push(`<div class="tt-row"><span class="tt-ico">👤</span><span>${esc(managers)}</span></div>`);
if (isCollision) {
tt.push('<div class="tt-warn"><div class="tt-warn-head">⚠ Коллизия</div>');
for (const pIdx of partnerIdxs!) {
const p = bookings[pIdx]!;
const pLabel = p.brand || p.company_name || 'Без названия';
tt.push(`<div class="tt-warn-row">${esc(pLabel)} <span class="tt-warn-dates">${fmtDate(p.start_date)} ${fmtDate(p.end_date)}</span></div>`);
}
tt.push('</div>');
}
tt.push('<div class="tt-hint">↗ Клик — открыть в ПланФикс</div>');
tooltipHtml = `<div class="tt" style="--tt-accent:${color}">${tt.join('')}</div>`;
}
tt.push('<div class="tt-hint">↗ Клик — открыть в ПланФикс</div>');
const tooltipHtml = `<div class="tt" style="--tt-accent:${color}">${tt.join('')}</div>`;
return {
id: bk.board_id + '__' + idx,