diff --git a/frontend/index.html b/frontend/index.html index 0d29f98..fb8b0f3 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -33,7 +33,6 @@ - @@ -123,6 +122,11 @@ + +
+ +
+
diff --git a/frontend/src/main.ts b/frontend/src/main.ts index f42c864..6a49ba8 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -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, }; } diff --git a/frontend/src/styles.css b/frontend/src/styles.css index b1f9a19..8a0295e 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -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); diff --git a/frontend/src/timeline.ts b/frontend/src/timeline.ts index dc3dbec..2eb022d 100644 --- a/frontend/src/timeline.ts +++ b/frontend/src/timeline.ts @@ -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(); 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('
'); - tt.push(`${esc(title)}`); - if (bk.task_id) tt.push(`№${esc(bk.task_id)}`); - tt.push('
'); - if (brand && company) tt.push(`
${esc(company)}
`); - tt.push('
'); - tt.push(`
📅${fmtDate(bk.start_date)} – ${fmtDate(bk.end_date)}${dur ? ' · ' + esc(dur) : ''}
`); - if (bk.task_status) tt.push(`
${esc(bk.task_status)}
`); - if (managers) tt.push(`
👤${esc(managers)}
`); - if (isCollision) { - tt.push('
⚠ Коллизия
'); - for (const pIdx of partnerIdxs!) { - const p = bookings[pIdx]!; - const pLabel = p.brand || p.company_name || 'Без названия'; - tt.push(`
${esc(pLabel)} ${fmtDate(p.start_date)} – ${fmtDate(p.end_date)}
`); - } + const datesRow = `
📅${fmtDate(bk.start_date)} – ${fmtDate(bk.end_date)}${dur ? ' · ' + esc(dur) : ''}
`; + let tooltipHtml: string; + if (!flags.managerView) { + // Anonymous tier: date range only — no title, task №, status or Planfix hint. + tooltipHtml = `
${datesRow}
`; + } else { + const title = brand || company || 'Без названия'; + const managers = (bk.manager || []).filter(Boolean).join(', '); + const tt: string[] = []; + tt.push('
'); + tt.push(`${esc(title)}`); + if (bk.task_id) tt.push(`№${esc(bk.task_id)}`); tt.push('
'); + if (brand && company) tt.push(`
${esc(company)}
`); + tt.push('
'); + tt.push(datesRow); + if (bk.task_status) tt.push(`
${esc(bk.task_status)}
`); + if (managers) tt.push(`
👤${esc(managers)}
`); + if (isCollision) { + tt.push('
⚠ Коллизия
'); + for (const pIdx of partnerIdxs!) { + const p = bookings[pIdx]!; + const pLabel = p.brand || p.company_name || 'Без названия'; + tt.push(`
${esc(pLabel)} ${fmtDate(p.start_date)} – ${fmtDate(p.end_date)}
`); + } + tt.push('
'); + } + tt.push('
↗ Клик — открыть в ПланФикс
'); + tooltipHtml = `
${tt.join('')}
`; } - tt.push('
↗ Клик — открыть в ПланФикс
'); - const tooltipHtml = `
${tt.join('')}
`; return { id: bk.board_id + '__' + idx,