// Chart view (board dashboard) and Files gallery view. import { html } from '../vendor/preact-htm.mjs'; import { api } from './api.js'; import { state, navigate, boardPath, mutateBoard } from './store.js'; import { Icon, fileUrl, isImage, fmtSize, timeAgo, Empty, todayStr, Avatar } from './ui.js'; import { labelOf, itemDone } from './cells.js'; export function ChartView({ board, view, items, subs }) { const boardId = board.board.id; const cols = ['status', 'people', 'dropdown'].flatMap((t) => board.columns.filter((c) => c.type === t)); const by = view.settings?.by || cols[0]?.id || '__group'; const kind = view.settings?.kind || 'bar'; const setting = (patch) => { mutateBoard(boardId, () => { view.settings = { ...view.settings, ...patch }; }); api.patch(`/api/views/${view.id}`, { settings: { ...view.settings, ...patch } }); }; const buckets = new Map(); const add = (key, label, color, n = 1) => { const b = buckets.get(key) || { label, color, n: 0 }; b.n += n; buckets.set(key, b); }; const col = board.columns.find((c) => c.id === by); for (const it of items) { if (by === '__group') { const g = board.groups.find((x) => x.id === it.group_id); add(g?.id, g?.title || '—', g?.color); continue; } const v = it.vals[by]; if (col.type === 'status') { const l = labelOf(col, v); add(l?.id || '_', l?.name || 'Empty', l?.color || '#c4c4c4'); } else if (col.type === 'dropdown') { if (!v?.length) add('_', 'Empty', '#c4c4c4'); for (const x of v || []) { const l = labelOf(col, x); if (l) add(l.id, l.name, l.color); } } else if (col.type === 'people') { if (!v?.length) add('_', 'Unassigned', '#c4c4c4'); for (const u of v || []) add(u, state.usersById[u]?.name || '?', null); } } const data = [...buckets.values()].sort((a, b) => b.n - a.n); const maxN = Math.max(1, ...data.map((d) => d.n)); const total = items.length; const done = items.filter((i) => itemDone(board, i)).length; const dateCol = board.columns.find((c) => c.type === 'date'); const overdue = dateCol ? items.filter((i) => i.vals[dateCol.id] && String(i.vals[dateCol.id]).slice(0, 10) < todayStr() && !itemDone(board, i)).length : 0; const palette = ['#579bfc', '#a25ddc', '#00c875', '#fdab3d', '#e2445c', '#66ccff', '#ff642e', '#037f4c', '#ff158a', '#784bd1']; let acc = 0; const arcs = data.map((d, i) => { const a0 = (acc / total) * Math.PI * 2; acc += d.n; const a1 = (acc / total) * Math.PI * 2; const large = a1 - a0 > Math.PI ? 1 : 0; const p = (a) => `${100 + 80 * Math.sin(a)} ${100 - 80 * Math.cos(a)}`; const path = data.length === 1 ? 'M100 20 A80 80 0 1 1 99.99 20 Z' : `M100 100 L${p(a0)} A80 80 0 ${large} 1 ${p(a1)} Z`; return html`${d.label}: ${d.n}`; }); return html`
${total}${board.board.item_label}s
${total ? Math.round((done / total) * 100) : 0}%Done (${done})
${overdue}Overdue
${Object.keys(subs).reduce((a, k) => a + subs[k].length, 0)}Subitems
Group by
${!total ? html`

No items to chart.

` : kind === 'bar' ? html`
${data.map((d, i) => html`
${d.label} ${d.n} ${Math.round((d.n / total) * 100)}%
`)}
` : html`
${arcs}
${data.map((d, i) => html`
${d.label} ${d.n}
`)}
`}
`; } export function FilesView({ board, view }) { const files = board.files || []; if (!files.length) return html`<${Empty} icon="paperclip" title="No files on this board yet">

Add a Files column or attach files to items.

`; const itemName = Object.fromEntries(board.items.map((i) => [i.id, i.name])); return html``; }