features: shareable filter links, board detail panel, spacing rhythm on tokens
- Shareable filter links: current filters sync to the URL query
(?city=&brand=&dimension=&manager=&status=&q=&from=&to=, repeated params for
multi-selects) and are restored on load; a "Скопировать ссылку" action in the
chips row copies the current URL.
- Board detail panel: clicking a timeline row label opens a right-side panel with
the surface's code, city · size, address and the full list of its bookings
(brand/company, dates, status dot, manager, Planfix link). Close via ✕ /
backdrop / Esc. Bars still open the Planfix task. Dropdown gains a public
setOnBoardClick on the timeline view.
- Fix: the panel showed empty on load because .board-panel{display:flex}
overrode the UA [hidden] rule — added an explicit [hidden]{display:none}.
- Spacing: tokenised the secondary chrome (header groups, fields, dropdown panel,
appearance menu, legend, map tooltip) onto the Open Props --size-* scale;
structural density of the header/fields/inputs deliberately kept.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
52a1a7cb2d
commit
4425bd394c
@ -165,6 +165,15 @@
|
|||||||
|
|
||||||
<div id="tooltip"></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>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -15,9 +15,10 @@ import { api } from './api';
|
|||||||
import type { Board, Booking, Filters } from './types';
|
import type { Board, Booking, Filters } from './types';
|
||||||
import { createDropdown } from './dropdown';
|
import { createDropdown } from './dropdown';
|
||||||
import { createTimelineView, zoomSteps, type RenderFlags } from './timeline';
|
import { createTimelineView, zoomSteps, type RenderFlags } from './timeline';
|
||||||
import { loadAppearance, applyCssVars, buildAppearanceMenu } from './appearance';
|
import { loadAppearance, applyCssVars, buildAppearanceMenu, colorForStatus } from './appearance';
|
||||||
import { createMapView } from './map';
|
import { createMapView } from './map';
|
||||||
import { createViewModes, type ViewMode } from './viewmode';
|
import { createViewModes, type ViewMode } from './viewmode';
|
||||||
|
import { escapeHtml } from './util';
|
||||||
|
|
||||||
const el = {
|
const el = {
|
||||||
search: document.getElementById('search') as HTMLInputElement,
|
search: document.getElementById('search') as HTMLInputElement,
|
||||||
@ -162,6 +163,34 @@ const fmtD = (iso: string): string => {
|
|||||||
const p = iso.split('-');
|
const p = iso.split('-');
|
||||||
return p.length === 3 ? `${p[2]}.${p[1]}.${p[0]}` : iso;
|
return p.length === 3 ? `${p[2]}.${p[1]}.${p[0]}` : iso;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ---- shareable filter links: current filters <-> URL query ----
|
||||||
|
function filtersToUrl(): void {
|
||||||
|
const f = currentFilters();
|
||||||
|
const p = new URLSearchParams();
|
||||||
|
f.cities.forEach((v) => p.append('city', v));
|
||||||
|
f.brands.forEach((v) => p.append('brand', v));
|
||||||
|
f.dimensions.forEach((v) => p.append('dimension', v));
|
||||||
|
f.managers.forEach((v) => p.append('manager', v));
|
||||||
|
f.statuses.forEach((v) => p.append('status', v));
|
||||||
|
if (f.search) p.set('q', f.search);
|
||||||
|
if (f.dateStart) p.set('from', f.dateStart);
|
||||||
|
if (f.dateEnd) p.set('to', f.dateEnd);
|
||||||
|
const qs = p.toString();
|
||||||
|
history.replaceState(null, '', qs ? `${location.pathname}?${qs}` : location.pathname);
|
||||||
|
}
|
||||||
|
function applyUrlToFilters(): void {
|
||||||
|
const p = new URLSearchParams(location.search);
|
||||||
|
cityDropdown.setSelected(p.getAll('city'));
|
||||||
|
brandDropdown.setSelected(p.getAll('brand'));
|
||||||
|
dimensionDropdown.setSelected(p.getAll('dimension'));
|
||||||
|
managerDropdown.setSelected(p.getAll('manager'));
|
||||||
|
statusDropdown.setSelected(p.getAll('status'));
|
||||||
|
el.search.value = p.get('q') || '';
|
||||||
|
el.dateStart.value = p.get('from') || '';
|
||||||
|
el.dateEnd.value = p.get('to') || '';
|
||||||
|
}
|
||||||
|
|
||||||
function resetFilters(): void {
|
function resetFilters(): void {
|
||||||
for (const d of [cityDropdown, brandDropdown, dimensionDropdown, managerDropdown, statusDropdown]) d.clear();
|
for (const d of [cityDropdown, brandDropdown, dimensionDropdown, managerDropdown, statusDropdown]) d.clear();
|
||||||
el.search.value = '';
|
el.search.value = '';
|
||||||
@ -202,6 +231,21 @@ function renderChips(): void {
|
|||||||
clear.textContent = 'Сбросить всё';
|
clear.textContent = 'Сбросить всё';
|
||||||
clear.addEventListener('click', resetFilters);
|
clear.addEventListener('click', resetFilters);
|
||||||
el.filterChips.appendChild(clear);
|
el.filterChips.appendChild(clear);
|
||||||
|
|
||||||
|
const share = document.createElement('button');
|
||||||
|
share.type = 'button';
|
||||||
|
share.className = 'chips-share';
|
||||||
|
share.textContent = 'Скопировать ссылку';
|
||||||
|
share.addEventListener('click', () => {
|
||||||
|
navigator.clipboard
|
||||||
|
?.writeText(location.href)
|
||||||
|
.then(() => {
|
||||||
|
share.textContent = 'Ссылка скопирована';
|
||||||
|
window.setTimeout(() => { share.textContent = 'Скопировать ссылку'; }, 1600);
|
||||||
|
})
|
||||||
|
.catch(() => { /* clipboard unavailable */ });
|
||||||
|
});
|
||||||
|
el.filterChips.appendChild(share);
|
||||||
}
|
}
|
||||||
el.emptyResetTimeline.addEventListener('click', resetFilters);
|
el.emptyResetTimeline.addEventListener('click', resetFilters);
|
||||||
el.emptyResetMap.addEventListener('click', resetFilters);
|
el.emptyResetMap.addEventListener('click', resetFilters);
|
||||||
@ -224,6 +268,7 @@ async function loadData(fit: boolean): Promise<void> {
|
|||||||
el.status.innerHTML = `Поверхности: ${boards.length}<br>Брони: ${bookings.length}`;
|
el.status.innerHTML = `Поверхности: ${boards.length}<br>Брони: ${bookings.length}`;
|
||||||
rebuildDecorMenu(); // status list may have changed
|
rebuildDecorMenu(); // status list may have changed
|
||||||
renderChips();
|
renderChips();
|
||||||
|
filtersToUrl();
|
||||||
// Keep the map in sync when it is visible (facets filter it; dates do not).
|
// Keep the map in sync when it is visible (facets filter it; dates do not).
|
||||||
if (mapVisible()) void refreshMap();
|
if (mapVisible()) void refreshMap();
|
||||||
} finally {
|
} finally {
|
||||||
@ -307,12 +352,65 @@ el.zoomSlider.addEventListener('input', () => {
|
|||||||
view.setScale(step.days);
|
view.setScale(step.days);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- board detail panel (opened from a timeline row label) ----
|
||||||
|
const PLANFIX_TASK_URL = 'https://green-media.planfix.ru/task/';
|
||||||
|
const boardPanel = document.getElementById('board-panel') as HTMLElement;
|
||||||
|
const boardBackdrop = document.getElementById('board-backdrop') as HTMLElement;
|
||||||
|
const boardPanelTitle = document.getElementById('board-panel-title') as HTMLElement;
|
||||||
|
const boardPanelBody = document.getElementById('board-panel-body') as HTMLElement;
|
||||||
|
function closeBoardPanel(): void {
|
||||||
|
boardPanel.hidden = true;
|
||||||
|
boardBackdrop.hidden = true;
|
||||||
|
}
|
||||||
|
function openBoardPanel(boardId: string): void {
|
||||||
|
const board = lastBoards.find((b) => b.board_id === boardId);
|
||||||
|
const bookings = lastBookings
|
||||||
|
.filter((b) => b.board_id === boardId)
|
||||||
|
.sort((a, b) => a.start_date.localeCompare(b.start_date));
|
||||||
|
boardPanelTitle.textContent = boardId;
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (board) {
|
||||||
|
const line1 = [board.board_city, board.board_dimension].filter(Boolean).map(escapeHtml).join(' · ');
|
||||||
|
if (line1) parts.push(`<div class="bp-meta">${line1}</div>`);
|
||||||
|
if (board.board_address) parts.push(`<div class="bp-meta">${escapeHtml(board.board_address)}</div>`);
|
||||||
|
}
|
||||||
|
parts.push(`<div class="bp-label">Брони (${bookings.length})</div>`);
|
||||||
|
if (!bookings.length) {
|
||||||
|
parts.push('<div class="bp-empty">Броней нет</div>');
|
||||||
|
} else {
|
||||||
|
for (const b of bookings) {
|
||||||
|
const title = escapeHtml(b.brand || b.company_name || 'Без названия');
|
||||||
|
const sub = b.brand && b.company_name ? `<div class="bp-b-sub">${escapeHtml(b.company_name)}</div>` : '';
|
||||||
|
const color = colorForStatus(appearance, b.task_status);
|
||||||
|
const mgr = (b.manager || []).filter(Boolean).join(', ');
|
||||||
|
parts.push(
|
||||||
|
'<div class="bp-booking">' +
|
||||||
|
`<div class="bp-b-head"><span class="bp-b-title">${title}</span>` +
|
||||||
|
`<a class="bp-b-link" href="${PLANFIX_TASK_URL}${encodeURIComponent(b.task_id)}" target="_blank" rel="noopener">№${escapeHtml(b.task_id)} ↗</a></div>` +
|
||||||
|
sub +
|
||||||
|
`<div class="bp-b-dates">${fmtD(b.start_date)} – ${fmtD(b.end_date)}</div>` +
|
||||||
|
`<div class="bp-b-row"><span class="bp-dot" style="background:${color}"></span>${escapeHtml(b.task_status || '')}</div>` +
|
||||||
|
(mgr ? `<div class="bp-b-row">👤 ${escapeHtml(mgr)}</div>` : '') +
|
||||||
|
'</div>',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boardPanelBody.innerHTML = parts.join('');
|
||||||
|
boardPanel.hidden = false;
|
||||||
|
boardBackdrop.hidden = false;
|
||||||
|
}
|
||||||
|
(document.getElementById('board-panel-close') as HTMLElement).addEventListener('click', closeBoardPanel);
|
||||||
|
boardBackdrop.addEventListener('click', closeBoardPanel);
|
||||||
|
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeBoardPanel(); });
|
||||||
|
view.setOnBoardClick(openBoardPanel);
|
||||||
|
|
||||||
async function init(): Promise<void> {
|
async function init(): Promise<void> {
|
||||||
cityDropdown.setValues([], 'Все города');
|
cityDropdown.setValues([], 'Все города');
|
||||||
brandDropdown.setValues([], 'Все бренды');
|
brandDropdown.setValues([], 'Все бренды');
|
||||||
dimensionDropdown.setValues([], 'Все размеры');
|
dimensionDropdown.setValues([], 'Все размеры');
|
||||||
managerDropdown.setValues([], 'Все менеджеры');
|
managerDropdown.setValues([], 'Все менеджеры');
|
||||||
statusDropdown.setValues([], 'Все статусы');
|
statusDropdown.setValues([], 'Все статусы');
|
||||||
|
applyUrlToFilters(); // restore filters from a shared link
|
||||||
rebuildDecorMenu();
|
rebuildDecorMenu();
|
||||||
view.applyLayout();
|
view.applyLayout();
|
||||||
await loadData(true);
|
await loadData(true);
|
||||||
|
|||||||
@ -98,8 +98,8 @@ header {
|
|||||||
.hgroup {
|
.hgroup {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: var(--size-3);
|
||||||
padding: 4px 0;
|
padding: var(--size-1) 0;
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
@ -171,7 +171,7 @@ input::placeholder { color: var(--text-muted); }
|
|||||||
.brand-name { font-size: 18px; font-weight: 700; white-space: nowrap; }
|
.brand-name { font-size: 18px; font-weight: 700; white-space: nowrap; }
|
||||||
.brand-sub { font-size: 15px; font-weight: 400; color: var(--text-muted); white-space: nowrap; }
|
.brand-sub { font-size: 15px; font-weight: 400; color: var(--text-muted); white-space: nowrap; }
|
||||||
|
|
||||||
.field { display: flex; flex-direction: column; gap: 5px; }
|
.field { display: flex; flex-direction: column; gap: var(--size-1); }
|
||||||
.field label {
|
.field label {
|
||||||
font-size: 11px; color: var(--text-muted);
|
font-size: 11px; color: var(--text-muted);
|
||||||
text-transform: uppercase; letter-spacing: .05em; font-weight: 500;
|
text-transform: uppercase; letter-spacing: .05em; font-weight: 500;
|
||||||
@ -218,7 +218,7 @@ input::placeholder { color: var(--text-muted); }
|
|||||||
}
|
}
|
||||||
.dropdown.open .dropdown-panel { display: block; }
|
.dropdown.open .dropdown-panel { display: block; }
|
||||||
.dropdown-panel label {
|
.dropdown-panel label {
|
||||||
display: flex; align-items: center; gap: 8px; padding: 6px 12px;
|
display: flex; align-items: center; gap: var(--size-2); padding: var(--size-2) var(--size-3);
|
||||||
font-size: 13px; cursor: pointer; white-space: nowrap; color: var(--text);
|
font-size: 13px; cursor: pointer; white-space: nowrap; color: var(--text);
|
||||||
/* Option labels are <label>s inside .field, so they'd inherit the uppercase /
|
/* Option labels are <label>s inside .field, so they'd inherit the uppercase /
|
||||||
weight / letter-spacing of `.field label`. Reset to clean body text. */
|
weight / letter-spacing of `.field label`. Reset to clean body text. */
|
||||||
@ -257,7 +257,7 @@ input::placeholder { color: var(--text-muted); }
|
|||||||
.decor-menu {
|
.decor-menu {
|
||||||
display: none; position: absolute; top: calc(100% + 6px); right: 0;
|
display: none; position: absolute; top: calc(100% + 6px); right: 0;
|
||||||
background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius);
|
background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius);
|
||||||
box-shadow: var(--shadow-md); padding: 12px 14px; z-index: 50;
|
box-shadow: var(--shadow-md); padding: var(--size-3); z-index: 50;
|
||||||
width: max-content; max-width: min(420px, 92vw); max-height: calc(100vh - 96px); overflow-y: auto;
|
width: max-content; max-width: min(420px, 92vw); max-height: calc(100vh - 96px); overflow-y: auto;
|
||||||
}
|
}
|
||||||
.decor-menu.open { display: block; }
|
.decor-menu.open { display: block; }
|
||||||
@ -349,8 +349,8 @@ input::placeholder { color: var(--text-muted); }
|
|||||||
#map-legend {
|
#map-legend {
|
||||||
position: absolute; left: 10px; bottom: 44px; z-index: 5;
|
position: absolute; left: 10px; bottom: 44px; z-index: 5;
|
||||||
background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius);
|
background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius);
|
||||||
padding: 8px 12px; font-size: 12px; color: var(--text);
|
padding: var(--size-2) var(--size-3); font-size: 12px; color: var(--text);
|
||||||
display: flex; flex-direction: column; align-items: flex-start; gap: 5px;
|
display: flex; flex-direction: column; align-items: flex-start; gap: var(--size-1);
|
||||||
box-shadow: var(--shadow-md); font-variant-numeric: tabular-nums;
|
box-shadow: var(--shadow-md); font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
#map-legend .lg-item { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; font-size: 12px; }
|
#map-legend .lg-item { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; font-size: 12px; }
|
||||||
@ -402,7 +402,7 @@ input::placeholder { color: var(--text-muted); }
|
|||||||
.map-tip {
|
.map-tip {
|
||||||
position: fixed; z-index: 1000; pointer-events: none;
|
position: fixed; z-index: 1000; pointer-events: none;
|
||||||
background: var(--tip-bg); color: var(--tip-text); font-size: 12px; line-height: 1.35;
|
background: var(--tip-bg); color: var(--tip-text); font-size: 12px; line-height: 1.35;
|
||||||
padding: 8px 10px; border-radius: var(--radius-sm); max-width: 340px;
|
padding: var(--size-2); border-radius: var(--radius-sm); max-width: 340px;
|
||||||
max-height: calc(100vh - 24px); overflow: hidden;
|
max-height: calc(100vh - 24px); overflow: hidden;
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
@ -561,6 +561,12 @@ input::placeholder { color: var(--text-muted); }
|
|||||||
transition: color var(--transition), background var(--transition);
|
transition: color var(--transition), background var(--transition);
|
||||||
}
|
}
|
||||||
.chips-clear:hover { color: var(--text); background: var(--surface-2); }
|
.chips-clear:hover { color: var(--text); background: var(--surface-2); }
|
||||||
|
.chips-share {
|
||||||
|
border: none; background: transparent; color: var(--accent); cursor: pointer;
|
||||||
|
font-size: 12px; font-weight: 500; padding: 3px 8px; border-radius: var(--radius-sm);
|
||||||
|
margin-left: auto; transition: background var(--transition);
|
||||||
|
}
|
||||||
|
.chips-share:hover { background: var(--accent-weak); }
|
||||||
|
|
||||||
/* ---- polish: focus rings, animations, scrollbars, tabular figures ---- */
|
/* ---- polish: focus rings, animations, scrollbars, tabular figures ---- */
|
||||||
button:focus-visible, input[type=text]:focus-visible, input[type=date]:focus-visible,
|
button:focus-visible, input[type=text]:focus-visible, input[type=date]:focus-visible,
|
||||||
@ -577,3 +583,37 @@ input[type=checkbox]:focus-visible { outline: 2px solid var(--accent); outline-o
|
|||||||
background: var(--border-strong); border-radius: 999px; border: 2px solid var(--bg);
|
background: var(--border-strong); border-radius: 999px; border: 2px solid var(--bg);
|
||||||
}
|
}
|
||||||
.vis-time-axis .vis-text, #tooltip, .map-tip, .bl, input[type=date] { font-variant-numeric: tabular-nums; }
|
.vis-time-axis .vis-text, #tooltip, .map-tip, .bl, input[type=date] { font-variant-numeric: tabular-nums; }
|
||||||
|
|
||||||
|
/* ---- board detail panel (slide-in from the right) ---- */
|
||||||
|
.bp-backdrop { position: fixed; inset: 0; background: rgba(15, 20, 32, .35); z-index: 300; }
|
||||||
|
.board-panel {
|
||||||
|
position: fixed; top: 0; right: 0; bottom: 0; width: 380px; max-width: 92vw; z-index: 301;
|
||||||
|
background: var(--bg); border-left: 1px solid var(--border); box-shadow: var(--shadow-md);
|
||||||
|
display: flex; flex-direction: column; animation: slide-in .18s var(--ease-3);
|
||||||
|
}
|
||||||
|
.board-panel[hidden], .bp-backdrop[hidden] { display: none; }
|
||||||
|
@keyframes slide-in { from { transform: translateX(24px); opacity: .5; } to { transform: none; opacity: 1; } }
|
||||||
|
.bp-head {
|
||||||
|
display: flex; align-items: center; justify-content: space-between; gap: var(--size-2);
|
||||||
|
padding: var(--size-3); border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.bp-title { font-size: 16px; font-weight: 600; word-break: break-word; }
|
||||||
|
.bp-close {
|
||||||
|
border: none; background: transparent; color: var(--text-muted); font-size: 17px; cursor: pointer;
|
||||||
|
width: 30px; height: 30px; border-radius: var(--radius-sm); flex: 0 0 auto;
|
||||||
|
display: inline-flex; align-items: center; justify-content: center; transition: background var(--transition), color var(--transition);
|
||||||
|
}
|
||||||
|
.bp-close:hover { background: var(--surface-2); color: var(--text); }
|
||||||
|
.bp-body { padding: var(--size-3); overflow-y: auto; flex: 1 1 auto; }
|
||||||
|
.bp-meta { font-size: 13px; color: var(--text-muted); margin-bottom: 2px; }
|
||||||
|
.bp-label { font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: var(--text-muted); font-weight: 600; margin: var(--size-3) 0 var(--size-2); }
|
||||||
|
.bp-empty { color: var(--text-muted); font-size: 13px; }
|
||||||
|
.bp-booking { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: var(--size-2) var(--size-3); margin-bottom: var(--size-2); }
|
||||||
|
.bp-b-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--size-2); }
|
||||||
|
.bp-b-title { font-size: 14px; font-weight: 500; }
|
||||||
|
.bp-b-link { font-size: 12px; color: var(--accent); text-decoration: none; font-weight: 500; white-space: nowrap; }
|
||||||
|
.bp-b-link:hover { text-decoration: underline; }
|
||||||
|
.bp-b-sub { font-size: 12px; color: var(--text-muted); margin-top: 1px; }
|
||||||
|
.bp-b-dates { font-size: 13px; margin-top: 4px; font-variant-numeric: tabular-nums; }
|
||||||
|
.bp-b-row { display: flex; align-items: center; gap: 7px; font-size: 12.5px; color: var(--text-muted); margin-top: 4px; }
|
||||||
|
.bp-dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; }
|
||||||
|
|||||||
@ -72,6 +72,8 @@ export interface TimelineView {
|
|||||||
/** Re-apply row height / centring from appearance and redraw. */
|
/** Re-apply row height / centring from appearance and redraw. */
|
||||||
applyLayout(): void;
|
applyLayout(): void;
|
||||||
setScale(days: number): void;
|
setScale(days: number): void;
|
||||||
|
/** Called with a board_id when its row label is clicked. */
|
||||||
|
setOnBoardClick(fn: (boardId: string) => void): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createTimelineView(el: TimelineElements, appearance: Appearance): TimelineView {
|
export function createTimelineView(el: TimelineElements, appearance: Appearance): TimelineView {
|
||||||
@ -80,6 +82,7 @@ export function createTimelineView(el: TimelineElements, appearance: Appearance)
|
|||||||
let lastBoards: Board[] = [];
|
let lastBoards: Board[] = [];
|
||||||
let lastBookings: Booking[] = [];
|
let lastBookings: Booking[] = [];
|
||||||
let lastFlags: RenderFlags = { withBrand: true, withCompany: false, withCollisions: true };
|
let lastFlags: RenderFlags = { withBrand: true, withCompany: false, withCollisions: true };
|
||||||
|
let onBoardClick: ((boardId: string) => void) | null = null;
|
||||||
|
|
||||||
const options: TimelineOptions = {
|
const options: TimelineOptions = {
|
||||||
editable: false,
|
editable: false,
|
||||||
@ -132,12 +135,17 @@ export function createTimelineView(el: TimelineElements, appearance: Appearance)
|
|||||||
window.addEventListener('resize', scheduleHeightFit);
|
window.addEventListener('resize', scheduleHeightFit);
|
||||||
scheduleHeightFit();
|
scheduleHeightFit();
|
||||||
|
|
||||||
// ---- clickable bars -> Planfix task ----
|
// ---- clicks: bar -> Planfix task; row label -> board detail panel ----
|
||||||
timeline.on('click', (props: any) => {
|
timeline.on('click', (props: any) => {
|
||||||
if (!props.item) return;
|
if (props.item) {
|
||||||
const item = itemsDS.get(props.item) as any;
|
const item = itemsDS.get(props.item) as any;
|
||||||
if (item && item.taskId) {
|
if (item && item.taskId) {
|
||||||
window.open(PLANFIX_TASK_URL + encodeURIComponent(item.taskId), '_blank', 'noopener');
|
window.open(PLANFIX_TASK_URL + encodeURIComponent(item.taskId), '_blank', 'noopener');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (props.what === 'group-label' && props.group != null && onBoardClick) {
|
||||||
|
onBoardClick(String(props.group));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -373,5 +381,11 @@ export function createTimelineView(el: TimelineElements, appearance: Appearance)
|
|||||||
timeline.setWindow(new Date(center - half), new Date(center + half));
|
timeline.setWindow(new Date(center - half), new Date(center + half));
|
||||||
}
|
}
|
||||||
|
|
||||||
return { render, refresh, applyLayout, setScale };
|
return {
|
||||||
|
render,
|
||||||
|
refresh,
|
||||||
|
applyLayout,
|
||||||
|
setScale,
|
||||||
|
setOnBoardClick(fn: (boardId: string) => void): void { onBoardClick = fn; },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user