diff --git a/src/lib/components/WorkLogSection.svelte b/src/lib/components/WorkLogSection.svelte index 343f60b..0eb1377 100644 --- a/src/lib/components/WorkLogSection.svelte +++ b/src/lib/components/WorkLogSection.svelte @@ -6,12 +6,14 @@ import KeyboardKey from "$lib/components/KeyboardKey.svelte"; import { addLocalDays, - formatWorkLogDateLabel, formatWorkLogTime, startOfLocalDay, } from "$lib/dateFormat"; import { linkifyWorkLogBody } from "$lib/work-log/linkify"; - import { moveWorkLogSelection } from "$lib/work-log/ui"; + import { + buildRecentWorkLogGroups, + moveWorkLogSelection, + } from "$lib/work-log/ui"; type WorkLogCommand = | "focusPreferred" @@ -33,12 +35,6 @@ onEditWorkLog: (workLog: WorkLog) => void; }; - type WorkLogGroup = { - dateKey: string; - label: string; - logs: WorkLog[]; - }; - type LastLogTone = "neutral" | "soon" | "late" | "stale"; const RECENT_WORK_LOG_DAY_COUNT = 7; @@ -65,7 +61,13 @@ let unlistenWorkLogCreated: UnlistenFn | undefined; let unlistenWorkLogUpdated: UnlistenFn | undefined; let relativeTimeInterval: ReturnType | undefined; - const visibleWorkLogGroups = $derived(groupVisibleWorkLogs(workLogs)); + const visibleWorkLogGroups = $derived( + buildRecentWorkLogGroups( + workLogs, + relativeTimeNowMs, + RECENT_WORK_LOG_DAY_COUNT, + ), + ); const lastWorkLog = $derived(workLogs[0] ?? null); const lastLogLabel = $derived( formatLastLogLabel(lastWorkLog, relativeTimeNowMs), @@ -260,34 +262,6 @@ }); } - function groupVisibleWorkLogs(logs: WorkLog[]): WorkLogGroup[] { - const todayStartMs = startOfLocalDay(Date.now()); - const groups: WorkLogGroup[] = []; - const groupByDateKey: Record = {}; - - for (const log of logs) { - const date = new Date(log.createdAtMs); - const dateKey = localDateKey(date); - const group = groupByDateKey[dateKey]; - - if (group) { - group.logs.push(log); - continue; - } - - const nextGroup = { - dateKey, - label: formatWorkLogDateLabel(date, todayStartMs), - logs: [log], - }; - - groupByDateKey[dateKey] = nextGroup; - groups.push(nextGroup); - } - - return groups; - } - function oldestVisibleDayStartMs() { return addLocalDays( startOfLocalDay(Date.now()), @@ -295,13 +269,6 @@ ); } - function localDateKey(date: Date) { - const month = String(date.getMonth() + 1).padStart(2, "0"); - const day = String(date.getDate()).padStart(2, "0"); - - return `${date.getFullYear()}-${month}-${day}`; - } - function formatLastLogLabel(workLog: WorkLog | null, nowMs: number) { if (!workLog) { return "Last log: none"; @@ -410,49 +377,51 @@ > {#if isLoadingWorkLogs}
  • Loading logs...
  • - {:else if workLogs.length === 0 || visibleWorkLogGroups.length === 0} -
  • No recent logs.
  • {:else} {#each visibleWorkLogGroups as group (group.dateKey)}
  • {group.label}

    -
      - {#each group.logs as log (log.id)} -
    1. - - - - {#each linkifyWorkLogBody(log.body) as part, partIndex (`${part.kind}-${partIndex}`)} - {#if part.kind === "url"} - - - - openExternalUrl(event, part.value)} - > + {#if group.logs.length === 0} +

      No log

      + {:else} +
        + {#each group.logs as log (log.id)} +
      1. + + + + {#each linkifyWorkLogBody(log.body) as part, partIndex (`${part.kind}-${partIndex}`)} + {#if part.kind === "url"} + + + + openExternalUrl(event, part.value)} + > + {part.value} + + + {:else} {part.value} - - - {:else} - {part.value} - {/if} - {/each} - -
      2. - {/each} -
      + {/if} + {/each} +
      +
    2. + {/each} +
    + {/if}
  • {/each} {/if} @@ -710,6 +679,12 @@ color: #858d9a; } + .log-empty-day { + margin: 0; + color: #6f7784; + font-size: 0.86rem; + } + .log-item time { color: #a8b0be; font-variant-numeric: tabular-nums; diff --git a/src/lib/work-log/ui.test.ts b/src/lib/work-log/ui.test.ts index 118117f..cab3348 100644 --- a/src/lib/work-log/ui.test.ts +++ b/src/lib/work-log/ui.test.ts @@ -1,6 +1,9 @@ import { describe, expect, it } from "vitest"; import type { WorkLog } from "$lib/api/workLogs"; -import { moveWorkLogSelection } from "$lib/work-log/ui"; +import { + buildRecentWorkLogGroups, + moveWorkLogSelection, +} from "$lib/work-log/ui"; function workLog(overrides: Partial & Pick): WorkLog { return { @@ -10,6 +13,65 @@ function workLog(overrides: Partial & Pick): WorkLog { }; } +describe("buildRecentWorkLogGroups", () => { + it("builds one group for each of the latest seven local calendar days", () => { + const today = new Date(2026, 4, 22, 12).getTime(); + const logs = [ + workLog({ + id: 1, + body: "today", + createdAtMs: new Date(2026, 4, 22, 9).getTime(), + }), + workLog({ + id: 2, + body: "four days ago", + createdAtMs: new Date(2026, 4, 18, 18).getTime(), + }), + ]; + + const groups = buildRecentWorkLogGroups(logs, today, 7); + + expect(groups.map((group) => group.label)).toEqual([ + "Today", + "Yesterday", + "May 20", + "May 19", + "May 18", + "May 17", + "May 16", + ]); + expect(groups.map((group) => group.logs.map((log) => log.id))).toEqual([ + [1], + [], + [], + [], + [2], + [], + [], + ]); + }); + + it("does not include logs outside the recent calendar window", () => { + const today = new Date(2026, 4, 22, 12).getTime(); + const logs = [ + workLog({ + id: 1, + createdAtMs: new Date(2026, 4, 16, 9).getTime(), + }), + workLog({ + id: 2, + createdAtMs: new Date(2026, 3, 22, 9).getTime(), + }), + ]; + + const groups = buildRecentWorkLogGroups(logs, today, 7); + + expect(groups.flatMap((group) => group.logs.map((log) => log.id))).toEqual([ + 1, + ]); + }); +}); + describe("moveWorkLogSelection", () => { const logs = [workLog({ id: 1 }), workLog({ id: 2 }), workLog({ id: 3 })]; diff --git a/src/lib/work-log/ui.ts b/src/lib/work-log/ui.ts index 00e427f..a42d243 100644 --- a/src/lib/work-log/ui.ts +++ b/src/lib/work-log/ui.ts @@ -1,4 +1,44 @@ import type { WorkLog } from "$lib/api/workLogs"; +import { + addLocalDays, + formatWorkLogDateLabel, + startOfLocalDay, +} from "$lib/dateFormat"; + +export type WorkLogGroup = { + dateKey: string; + label: string; + logs: WorkLog[]; +}; + +export function buildRecentWorkLogGroups( + logs: WorkLog[], + todayTimestampMs: number, + dayCount: number, +): WorkLogGroup[] { + const todayStartMs = startOfLocalDay(todayTimestampMs); + const groups: WorkLogGroup[] = []; + const groupByDateKey = new Map(); + + for (let dayOffset = 0; dayOffset > -dayCount; dayOffset -= 1) { + const dayStartMs = addLocalDays(todayStartMs, dayOffset); + const date = new Date(dayStartMs); + const group = { + dateKey: localDateKey(date), + label: formatWorkLogDateLabel(date, todayStartMs), + logs: [], + }; + + groups.push(group); + groupByDateKey.set(group.dateKey, group); + } + + for (const log of logs) { + groupByDateKey.get(localDateKey(new Date(log.createdAtMs)))?.logs.push(log); + } + + return groups; +} export function moveWorkLogSelection( logs: WorkLog[], @@ -19,3 +59,10 @@ export function moveWorkLogSelection( return logs[nextIndex]?.id ?? null; } + +function localDateKey(date: Date) { + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + + return `${date.getFullYear()}-${month}-${day}`; +}