feat: show habit history in details
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { archivePanelFlags, calendarModeLabel, changedHabitFields, clampFabPosition, countdownDayText, countdownKindLabel, dateKey, defaultView, formatArchivedAt, formatAuditAction, formatAuditEntity, formatHabitApiError, formatLocalShortDateTime, formatUserAgent, habitActionState, habitButtonNotice, habitButtonValue, habitWeek, invalidateHabitGridCache, isFabDrag, isHabitComplete, isHabitScheduledToday, isTaskView, nextHabitSwipeValue, nextTotalAfterLocalTaskAdd, nextTotalAfterLocalTaskRemoval, snapFabPosition, normalizeRequiredName, numericHabitInputValue, performHabitRestore, performTrashMutation, previousHabitSwipeValue, quickTaskFields, readCountdownCache, readHabitGridCache, readStoredBoolean, readStoredNavigation, shouldToggleRowSwipe, validateHabitForm, writeCountdownCache, writeHabitGridCache, writeStoredBoolean, writeStoredNavigation } from './mvp-utils'
|
||||
import { archivePanelFlags, calendarModeLabel, changedHabitFields, clampFabPosition, countdownDayText, countdownKindLabel, dateKey, dayBefore, defaultView, formatArchivedAt, formatAuditAction, formatAuditEntity, formatHabitApiError, formatHabitHistoryNumber, formatLocalShortDateTime, formatUserAgent, habitActionState, habitButtonNotice, habitButtonValue, habitHistoryWindow, habitWeek, invalidateHabitGridCache, isFabDrag, isHabitComplete, isHabitScheduledToday, isTaskView, mergeHabitHistory, nextHabitSwipeValue, nextTotalAfterLocalTaskAdd, nextTotalAfterLocalTaskRemoval, snapFabPosition, normalizeRequiredName, numericHabitInputValue, performHabitRestore, performTrashMutation, previousHabitSwipeValue, quickTaskFields, readCountdownCache, readHabitGridCache, readStoredBoolean, readStoredNavigation, shouldToggleRowSwipe, validateHabitForm, writeCountdownCache, writeHabitGridCache, writeStoredBoolean, writeStoredNavigation } from './mvp-utils'
|
||||
|
||||
describe('MVP view utilities', () => {
|
||||
it('formats a local date as YYYY-MM-DD', () => {
|
||||
@@ -71,6 +71,25 @@ describe('MVP view utilities', () => {
|
||||
expect(readStoredNavigation(fakeStorage, 'dodo.navigation')).toEqual({ view: 'today', listId: '' })
|
||||
})
|
||||
|
||||
it('builds non-overlapping 90-day habit history windows', () => {
|
||||
expect(habitHistoryWindow('2026-09-15')).toEqual({ from: '2026-06-18', to: '2026-09-15' })
|
||||
expect(dayBefore('2026-06-18')).toBe('2026-06-17')
|
||||
expect(habitHistoryWindow('2026-01-01', 2)).toEqual({ from: '2025-12-31', to: '2026-01-01' })
|
||||
})
|
||||
|
||||
it('merges habit history by day, coerces API values, and keeps newest first', () => {
|
||||
expect(mergeHabitHistory(
|
||||
[{ day: '2026-09-14', value: 1 }, { day: '2026-09-13', value: 2 }],
|
||||
[{ day: '2026-09-15', value: 3 }, { day: '2026-09-14', value: 4 }],
|
||||
)).toEqual([
|
||||
{ day: '2026-09-15', value: 3 },
|
||||
{ day: '2026-09-14', value: 4 },
|
||||
{ day: '2026-09-13', value: 2 },
|
||||
])
|
||||
expect(formatHabitHistoryNumber(8)).toBe('8')
|
||||
expect(formatHabitHistoryNumber(1.25)).toBe('1.25')
|
||||
})
|
||||
|
||||
it('only completes numeric habits when progress reaches the target', () => {
|
||||
expect(isHabitComplete('numeric', 3, 5)).toBe(false)
|
||||
expect(isHabitComplete('numeric', 5, 5)).toBe(true)
|
||||
|
||||
@@ -80,6 +80,33 @@ export function isHabitComplete(kind: string | undefined, value: number | boolea
|
||||
return Boolean(value)
|
||||
}
|
||||
|
||||
export type HabitHistoryLog = { day: string; value: number }
|
||||
|
||||
export function habitHistoryWindow(toDay: string, size = 90) {
|
||||
const [year, month, day] = toDay.split('-').map(Number)
|
||||
const to = new Date(year, month - 1, day)
|
||||
const from = new Date(to)
|
||||
from.setDate(from.getDate() - (size - 1))
|
||||
return { from: dateKey(from), to: dateKey(to) }
|
||||
}
|
||||
|
||||
export function dayBefore(day: string) {
|
||||
const [year, month, date] = day.split('-').map(Number)
|
||||
const value = new Date(year, month - 1, date)
|
||||
value.setDate(value.getDate() - 1)
|
||||
return dateKey(value)
|
||||
}
|
||||
|
||||
export function mergeHabitHistory(previous: HabitHistoryLog[], incoming: HabitHistoryLog[]) {
|
||||
const merged = new Map(previous.map((item) => [item.day, item]))
|
||||
for (const item of incoming) merged.set(item.day, { day: item.day, value: Number(item.value) })
|
||||
return [...merged.values()].sort((a, b) => b.day.localeCompare(a.day))
|
||||
}
|
||||
|
||||
export function formatHabitHistoryNumber(value: number) {
|
||||
return Number.isInteger(value) ? String(value) : String(Number(value.toFixed(6)))
|
||||
}
|
||||
|
||||
type HabitSchedule = {
|
||||
kind?: string
|
||||
cells?: Array<{ day: string; scheduled?: boolean; paused?: boolean; value?: number | boolean }>
|
||||
|
||||
Reference in New Issue
Block a user