// Calendar: month grid of items by a date (or timeline) column; drag to reschedule. import { html, useState } from '../vendor/preact-htm.mjs'; import { api } from './api.js'; import { navigate, boardPath, mutateBoard } from './store.js'; import { Icon, ymd, parseDate, todayStr, textOn, Empty, useMobile } from './ui.js'; import { labelOf } from './cells.js'; import * as A from './actions.js'; export function CalendarView({ board, view, items, subs }) { const boardId = board.board.id; const mobile = useMobile(); const dateCols = board.columns.filter((c) => c.type === 'date' || c.type === 'timeline'); const col = dateCols.find((c) => c.id === view.settings?.column) || dateCols[0]; const status = board.columns.find((c) => c.type === 'status'); const [month, setMonth] = useState(() => { const d = new Date(); return new Date(d.getFullYear(), d.getMonth(), 1); }); const [drag, setDrag] = useState(null); if (!col) return html`<${Empty} icon="calendar" title="Calendar needs a date column">`; const all = [...items, ...items.flatMap((i) => subs[i.id] || [])]; const byDay = {}; for (const it of all) { const v = it.vals[col.id]; if (!v) continue; if (col.type === 'date') (byDay[String(v).slice(0, 10)] ||= []).push(it); else { const a = parseDate(v.from); const b = parseDate(v.to || v.from); for (let d = new Date(a); d <= b && (d - a) < 400 * 86400000; d.setDate(d.getDate() + 1)) (byDay[ymd(d)] ||= []).push(it); } } const start = new Date(month); start.setDate(1 - start.getDay()); const days = Array.from({ length: 42 }, (_, i) => { const d = new Date(start); d.setDate(start.getDate() + i); return d; }); const setCol = (id) => { mutateBoard(boardId, () => { view.settings = { ...view.settings, column: id }; }); api.patch(`/api/views/${view.id}`, { settings: { ...view.settings, column: id } }); }; const drop = (day) => { if (!drag) return; const v = drag.vals[col.id]; if (col.type === 'date') A.setValue(boardId, drag.id, col.id, String(v || '').includes('T') ? `${day}T${String(v).split('T')[1]}` : day); else { const len = v?.from ? (parseDate(v.to) - parseDate(v.from)) : 0; const to = new Date(parseDate(day).getTime() + len); A.setValue(boardId, drag.id, col.id, { from: day, to: ymd(to) }); } setDrag(null); }; const chip = (it) => { const l = status && labelOf(status, it.vals[status.id]); return html``; }; if (mobile) { // Agenda list on phones. const keys = Object.keys(byDay).filter((k) => k.slice(0, 7) === ymd(month).slice(0, 7)).sort(); return html`
<${CalHead} month=${month} setMonth=${setMonth} dateCols=${dateCols} col=${col} setCol=${setCol} /> ${keys.map((k) => html`
${parseDate(k).toLocaleDateString([], { weekday: 'short', month: 'short', day: 'numeric' })}
${byDay[k].map(chip)}
`)} ${!keys.length ? html`

Nothing scheduled this month.

` : null}
`; } return html`
<${CalHead} month=${month} setMonth=${setMonth} dateCols=${dateCols} col=${col} setCol=${setCol} />
${['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((d) => html`
${d}
`)} ${days.map((d) => { const k = ymd(d); const list = byDay[k] || []; return html`
e.preventDefault()} onDrop=${() => drop(k)} onDblClick=${async () => { const it = await A.createItem(boardId, { group_id: board.groups[0]?.id, name: 'New item', vals: { [col.id]: col.type === 'date' ? k : { from: k, to: k } } }); if (it) navigate(boardPath(boardId, { viewId: view.id, itemId: it.id })); }}> ${d.getDate()}
${list.slice(0, 4).map(chip)}${list.length > 4 ? html`+${list.length - 4} more` : null}
`; })}

Drag items to reschedule ยท double-click a day to add an item

`; } function CalHead({ month, setMonth, dateCols, col, setCol }) { return html`

${month.toLocaleString([], { month: 'long', year: 'numeric' })}

${dateCols.length > 1 ? html`` : null}
`; }