super-time-tracker-ui 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/AGENTS.md +5 -0
  2. package/CHANGELOG.md +28 -0
  3. package/CLAUDE.md +1 -0
  4. package/README.md +36 -0
  5. package/app/api/backup/route.ts +39 -0
  6. package/app/api/entry/delete-bulk/route.ts +53 -0
  7. package/app/api/entry/move/route.ts +46 -0
  8. package/app/api/entry/move-bulk/route.ts +62 -0
  9. package/app/api/entry/route.ts +75 -0
  10. package/app/api/in/route.ts +38 -0
  11. package/app/api/note/route.ts +120 -0
  12. package/app/api/out/route.ts +31 -0
  13. package/app/api/sheet/route.ts +68 -0
  14. package/app/api/state/route.ts +16 -0
  15. package/app/api/tags/route.ts +75 -0
  16. package/app/color-palettes.css +260 -0
  17. package/app/favicon.ico +0 -0
  18. package/app/globals.css +140 -0
  19. package/app/layout.tsx +54 -0
  20. package/app/page.tsx +24 -0
  21. package/app/reporting/page.tsx +11 -0
  22. package/app/settings/data/page.tsx +9 -0
  23. package/app/settings/display/page.tsx +8 -0
  24. package/app/settings/page.tsx +12 -0
  25. package/app/settings/tags/page.tsx +13 -0
  26. package/bin/stt-ui.js +63 -0
  27. package/components/active-entry-panel.tsx +199 -0
  28. package/components/backup-restore-setting.tsx +168 -0
  29. package/components/check-in-form-collapsed-setting.tsx +44 -0
  30. package/components/check-in-form-collapsible.tsx +52 -0
  31. package/components/check-in-form.tsx +89 -0
  32. package/components/checkbox.tsx +75 -0
  33. package/components/checkout-button-group.tsx +73 -0
  34. package/components/chevron-icon.tsx +25 -0
  35. package/components/clear-tag-filters-on-sheet-change-setting.tsx +45 -0
  36. package/components/color-palette-setting.tsx +75 -0
  37. package/components/compact-lists-setting.tsx +42 -0
  38. package/components/confirm-before-checkout-setting.tsx +42 -0
  39. package/components/confirm-destructive-actions-setting.tsx +46 -0
  40. package/components/confirm-dialog-provider.tsx +71 -0
  41. package/components/confirm-dialog.tsx +90 -0
  42. package/components/data-settings-view.tsx +47 -0
  43. package/components/default-reporting-range-setting.tsx +56 -0
  44. package/components/default-reporting-sort-setting.tsx +45 -0
  45. package/components/default-sheet-session-setting.tsx +118 -0
  46. package/components/display-settings-view.tsx +75 -0
  47. package/components/duration-format-setting.tsx +40 -0
  48. package/components/entry-actions-menu.tsx +207 -0
  49. package/components/entry-edit-form.tsx +113 -0
  50. package/components/entry-list-bulk-bar.tsx +128 -0
  51. package/components/entry-list-sort-setting.tsx +41 -0
  52. package/components/entry-list.tsx +336 -0
  53. package/components/entry-notes-list.tsx +211 -0
  54. package/components/entry-tag-filter.tsx +99 -0
  55. package/components/format_datetime_hint.ts +8 -0
  56. package/components/format_time.ts +10 -0
  57. package/components/general-settings-view.tsx +40 -0
  58. package/components/hamburger-icon.tsx +21 -0
  59. package/components/note-edit-form.tsx +77 -0
  60. package/components/note-form.tsx +109 -0
  61. package/components/pencil-icon.tsx +21 -0
  62. package/components/reporting-date-range-picker.tsx +121 -0
  63. package/components/reporting-sort-controls.tsx +53 -0
  64. package/components/reporting-view.tsx +340 -0
  65. package/components/setting-radio-group.tsx +79 -0
  66. package/components/settings-nav.tsx +66 -0
  67. package/components/settings-page-layout.tsx +53 -0
  68. package/components/settings-saved-toast.tsx +57 -0
  69. package/components/sheet-actions-menu.tsx +108 -0
  70. package/components/sheet-sidebar.tsx +196 -0
  71. package/components/tag-autocomplete-input.tsx +183 -0
  72. package/components/tag-filter-mode-setting.tsx +47 -0
  73. package/components/tag-management-view.tsx +290 -0
  74. package/components/theme-mode-setting.tsx +44 -0
  75. package/components/theme-mode-system-listener.tsx +43 -0
  76. package/components/theme_switcher.tsx +38 -0
  77. package/components/time-format-setting.tsx +39 -0
  78. package/components/timer-in-title-setting.tsx +38 -0
  79. package/components/timer-show-seconds-setting.tsx +41 -0
  80. package/components/tracker-active-bar.tsx +76 -0
  81. package/components/tracker-app.tsx +338 -0
  82. package/components/tracker-breadcrumb.tsx +56 -0
  83. package/components/tracker-document-title.tsx +67 -0
  84. package/components/tracker-topbar.tsx +63 -0
  85. package/components/trash-icon.tsx +24 -0
  86. package/components/week-starts-on-setting.tsx +39 -0
  87. package/eslint.config.mjs +18 -0
  88. package/lib/add_note_to_entry.ts +65 -0
  89. package/lib/api_error_response.ts +10 -0
  90. package/lib/apply_accent_color.ts +12 -0
  91. package/lib/apply_color_palette.ts +12 -0
  92. package/lib/apply_compact_lists.ts +9 -0
  93. package/lib/apply_tag_autocomplete_selection.ts +26 -0
  94. package/lib/apply_theme.ts +8 -0
  95. package/lib/build_reporting_stats.ts +55 -0
  96. package/lib/build_resume_description.ts +15 -0
  97. package/lib/check_in_entry.ts +81 -0
  98. package/lib/check_out_entry.ts +75 -0
  99. package/lib/collect_known_tags.ts +22 -0
  100. package/lib/collect_tag_stats.ts +27 -0
  101. package/lib/collect_tags_from_entries.ts +35 -0
  102. package/lib/config.ts +9 -0
  103. package/lib/convert_json_db.ts +49 -0
  104. package/lib/delete_entries.ts +62 -0
  105. package/lib/delete_entry.ts +29 -0
  106. package/lib/delete_note_on_entry.ts +42 -0
  107. package/lib/delete_sheet.ts +30 -0
  108. package/lib/delete_tracker_action.ts +22 -0
  109. package/lib/edit_entry.ts +56 -0
  110. package/lib/edit_note_on_entry.ts +49 -0
  111. package/lib/ensure_dir_exists.ts +22 -0
  112. package/lib/entry_matches_tag_filter.ts +26 -0
  113. package/lib/fetch_tracker_state.ts +15 -0
  114. package/lib/filter_entries_by_tags.ts +20 -0
  115. package/lib/filter_known_tags.ts +20 -0
  116. package/lib/find_all_serialized_active_entries.ts +28 -0
  117. package/lib/find_serialized_active_entry.ts +12 -0
  118. package/lib/find_serialized_active_entry_for_sheet.ts +31 -0
  119. package/lib/find_sheet_with_active_entry.ts +16 -0
  120. package/lib/format_display_tag.ts +6 -0
  121. package/lib/format_duration.ts +45 -0
  122. package/lib/gen_db.ts +43 -0
  123. package/lib/get_active_panel_class_name.ts +20 -0
  124. package/lib/get_average_entry_ms.ts +13 -0
  125. package/lib/get_button_class_name.ts +24 -0
  126. package/lib/get_check_out_confirm_dialog.ts +19 -0
  127. package/lib/get_clipped_entry_duration_ms.ts +18 -0
  128. package/lib/get_compact_lists_snapshot.ts +15 -0
  129. package/lib/get_date_range_ms_from_inputs.ts +31 -0
  130. package/lib/get_delete_entries_confirm_dialog.ts +21 -0
  131. package/lib/get_delete_entry_confirm_dialog.ts +19 -0
  132. package/lib/get_delete_note_confirm_dialog.ts +21 -0
  133. package/lib/get_delete_sheet_confirm_dialog.ts +25 -0
  134. package/lib/get_entry_duration_ms.ts +14 -0
  135. package/lib/get_entry_row_key.ts +8 -0
  136. package/lib/get_initial_preferred_sheet_name.ts +34 -0
  137. package/lib/get_initial_reporting_range_inputs.ts +31 -0
  138. package/lib/get_input_class_name.ts +15 -0
  139. package/lib/get_merge_tags_confirm_dialog.ts +25 -0
  140. package/lib/get_period_range_ms.ts +43 -0
  141. package/lib/get_reporting_date_range_shortcut_inputs.ts +84 -0
  142. package/lib/get_reporting_period_totals.ts +39 -0
  143. package/lib/get_reporting_stats.ts +25 -0
  144. package/lib/get_restore_db_confirm_dialog.ts +14 -0
  145. package/lib/get_running_entry_key.ts +8 -0
  146. package/lib/get_serialized_entries_total_ms.ts +10 -0
  147. package/lib/get_sheet.ts +14 -0
  148. package/lib/get_sheet_report_stats.ts +22 -0
  149. package/lib/get_sheet_report_stats_for_range.ts +46 -0
  150. package/lib/get_sheet_tag_filter_snapshot.ts +22 -0
  151. package/lib/get_sheets_duration_in_range.ts +27 -0
  152. package/lib/get_tag_autocomplete_context.ts +32 -0
  153. package/lib/get_theme_snapshot.ts +16 -0
  154. package/lib/get_tracker_state.ts +67 -0
  155. package/lib/has_string_value.ts +6 -0
  156. package/lib/is_entry_in_day.ts +15 -0
  157. package/lib/is_idle_sheet_report.ts +8 -0
  158. package/lib/is_json_time_tracker_db.ts +14 -0
  159. package/lib/merge_tags_across_db.ts +79 -0
  160. package/lib/migrate_json_db.ts +56 -0
  161. package/lib/migrate_json_db_to_version_three.ts +51 -0
  162. package/lib/migrate_json_db_to_version_two.ts +50 -0
  163. package/lib/move_entries_to_sheet.ts +152 -0
  164. package/lib/move_entry_to_sheet.ts +82 -0
  165. package/lib/normalize_stored_tag.ts +16 -0
  166. package/lib/notify_settings_saved.ts +47 -0
  167. package/lib/parse_default_sheet_session_mode.ts +21 -0
  168. package/lib/parse_entry_from_input.ts +23 -0
  169. package/lib/parse_natural_language_date.ts +23 -0
  170. package/lib/parse_reporting_source_sheets.ts +22 -0
  171. package/lib/partition_sheet_report_stats.ts +30 -0
  172. package/lib/patch_tracker_action.ts +22 -0
  173. package/lib/persist_ui_preference.ts +18 -0
  174. package/lib/post_tracker_action.ts +22 -0
  175. package/lib/preferences/accent_color_preference.ts +21 -0
  176. package/lib/preferences/check_in_form_collapsed_preference.ts +20 -0
  177. package/lib/preferences/clear_tag_filters_on_sheet_change_preference.ts +20 -0
  178. package/lib/preferences/color_palette_preference.ts +21 -0
  179. package/lib/preferences/confirm_before_checkout_preference.ts +20 -0
  180. package/lib/preferences/confirm_destructive_actions_preference.ts +20 -0
  181. package/lib/preferences/default_reporting_range_preference.ts +21 -0
  182. package/lib/preferences/default_reporting_sort_preference.ts +24 -0
  183. package/lib/preferences/duration_format_preference.ts +19 -0
  184. package/lib/preferences/entry_list_sort_preference.ts +21 -0
  185. package/lib/preferences/tag_filter_mode_preference.ts +18 -0
  186. package/lib/preferences/theme_mode_preference.ts +18 -0
  187. package/lib/preferences/time_format_preference.ts +18 -0
  188. package/lib/preferences/timer_in_title_preference.ts +18 -0
  189. package/lib/preferences/timer_show_seconds_preference.ts +19 -0
  190. package/lib/preferences/week_starts_on_preference.ts +19 -0
  191. package/lib/prompt_check_out_at.ts +17 -0
  192. package/lib/prompt_entry_note.ts +14 -0
  193. package/lib/prune_sheet_tag_filter.ts +27 -0
  194. package/lib/read_db.ts +49 -0
  195. package/lib/read_db_backup_contents.ts +22 -0
  196. package/lib/read_document_compact_lists.ts +12 -0
  197. package/lib/read_document_theme.ts +14 -0
  198. package/lib/read_sheet_tag_filter.ts +26 -0
  199. package/lib/read_stored_active_sheet.ts +14 -0
  200. package/lib/read_stored_compact_lists.ts +24 -0
  201. package/lib/read_stored_default_sheet_fixed_name.ts +16 -0
  202. package/lib/read_stored_default_sheet_session_mode.ts +18 -0
  203. package/lib/read_stored_sheet_tag_filters.ts +28 -0
  204. package/lib/read_stored_theme.ts +18 -0
  205. package/lib/rename_sheet.ts +39 -0
  206. package/lib/rename_tag_across_db.ts +19 -0
  207. package/lib/resolve_active_sheet_name.ts +36 -0
  208. package/lib/resolve_session_preferred_sheet.ts +37 -0
  209. package/lib/resolve_theme.ts +18 -0
  210. package/lib/resolve_theme_mode_to_theme.ts +19 -0
  211. package/lib/restore_db_from_uploaded_json.ts +24 -0
  212. package/lib/serialize_entry.ts +27 -0
  213. package/lib/serialize_reporting_source_sheets.ts +19 -0
  214. package/lib/serialize_sheet_entries.ts +18 -0
  215. package/lib/set_accent_color.ts +12 -0
  216. package/lib/set_active_sheet.ts +18 -0
  217. package/lib/set_color_palette.ts +12 -0
  218. package/lib/set_compact_lists.ts +12 -0
  219. package/lib/set_default_sheet_fixed_name.ts +8 -0
  220. package/lib/set_default_sheet_session_mode.ts +11 -0
  221. package/lib/set_sheet_tag_filter.ts +13 -0
  222. package/lib/set_theme_mode.ts +19 -0
  223. package/lib/sheet_tag_filter_snapshots.ts +48 -0
  224. package/lib/sort_serialized_entries.ts +35 -0
  225. package/lib/sort_sheet_report_stats.ts +43 -0
  226. package/lib/subscribe_compact_lists.ts +25 -0
  227. package/lib/subscribe_sheet_tag_filters.ts +28 -0
  228. package/lib/subscribe_theme.ts +23 -0
  229. package/lib/sync_active_sheet_preference.ts +19 -0
  230. package/lib/tags_are_equal.ts +12 -0
  231. package/lib/theme_init_script.ts +11 -0
  232. package/lib/toggle_sheet_tag_filter.ts +28 -0
  233. package/lib/toggle_theme.ts +20 -0
  234. package/lib/types/confirm_dialog.ts +9 -0
  235. package/lib/types/data.ts +16 -0
  236. package/lib/types/generic_data.ts +25 -0
  237. package/lib/types/index.ts +2 -0
  238. package/lib/types/reporting.ts +59 -0
  239. package/lib/types/tag_management.ts +7 -0
  240. package/lib/types/theme.ts +3 -0
  241. package/lib/types/tracker_state.ts +39 -0
  242. package/lib/types/ui_preferences.ts +104 -0
  243. package/lib/types/ui_settings.ts +17 -0
  244. package/lib/ui_preference_store.ts +80 -0
  245. package/lib/ui_settings_init_script.ts +33 -0
  246. package/lib/use_check_in_form_collapsed.ts +18 -0
  247. package/lib/use_clear_tag_filters_on_sheet_change.ts +18 -0
  248. package/lib/use_confirm_before_checkout.ts +18 -0
  249. package/lib/use_confirm_destructive_actions.ts +18 -0
  250. package/lib/use_duration_format.ts +17 -0
  251. package/lib/use_entry_list_sort.ts +17 -0
  252. package/lib/use_tag_filter_mode.ts +17 -0
  253. package/lib/use_time_format.ts +17 -0
  254. package/lib/use_timer_in_title.ts +18 -0
  255. package/lib/use_timer_show_seconds.ts +18 -0
  256. package/lib/use_week_starts_on.ts +17 -0
  257. package/lib/validate_entry_times.ts +12 -0
  258. package/lib/week_starts_on_to_index.ts +8 -0
  259. package/lib/write_active_sheet_preference.ts +28 -0
  260. package/lib/write_db.ts +20 -0
  261. package/lib/write_sheet_tag_filter.ts +20 -0
  262. package/lib/write_stored_compact_lists.ts +15 -0
  263. package/lib/write_stored_default_sheet_fixed_name.ts +28 -0
  264. package/lib/write_stored_default_sheet_session_mode.ts +24 -0
  265. package/lib/write_stored_sheet_tag_filters.ts +18 -0
  266. package/lib/write_stored_theme.ts +12 -0
  267. package/next.config.ts +7 -0
  268. package/package.json +96 -0
  269. package/pnpm-workspace.yaml +7 -0
  270. package/postcss.config.mjs +7 -0
  271. package/public/file.svg +1 -0
  272. package/public/globe.svg +1 -0
  273. package/public/next.svg +1 -0
  274. package/public/vercel.svg +1 -0
  275. package/public/window.svg +1 -0
  276. package/tsconfig.json +34 -0
@@ -0,0 +1,34 @@
1
+ import { parse_default_sheet_session_mode } from '@/lib/parse_default_sheet_session_mode'
2
+ import { resolve_session_preferred_sheet } from '@/lib/resolve_session_preferred_sheet'
3
+ import { type TimeTrackerDB } from '@/lib/types'
4
+
5
+ export interface InitialPreferredSheetCookies {
6
+ session_mode?: string
7
+ fixed_sheet_name?: string
8
+ last_viewed_sheet?: string
9
+ }
10
+
11
+ /**
12
+ * Chooses the preferred sheet for a new session from cookies and the database.
13
+ */
14
+ export function get_initial_preferred_sheet_name(
15
+ db: TimeTrackerDB,
16
+ cookies: InitialPreferredSheetCookies,
17
+ ): string | null {
18
+ const mode = parse_default_sheet_session_mode(cookies.session_mode)
19
+ const last_viewed_sheet =
20
+ cookies.last_viewed_sheet !== undefined
21
+ ? decodeURIComponent(cookies.last_viewed_sheet).trim() || null
22
+ : null
23
+ const fixed_sheet_name =
24
+ cookies.fixed_sheet_name !== undefined
25
+ ? decodeURIComponent(cookies.fixed_sheet_name).trim() || null
26
+ : null
27
+
28
+ return resolve_session_preferred_sheet(
29
+ db,
30
+ mode,
31
+ last_viewed_sheet,
32
+ fixed_sheet_name,
33
+ )
34
+ }
@@ -0,0 +1,31 @@
1
+ import { default_reporting_range_preference } from '@/lib/preferences/default_reporting_range_preference'
2
+ import { get_reporting_date_range_shortcut_inputs } from '@/lib/get_reporting_date_range_shortcut_inputs'
3
+ import {
4
+ type DefaultReportingRange,
5
+ type WeekStartsOn,
6
+ } from '@/lib/types/ui_preferences'
7
+ import { type ReportingDateRangeInputs } from '@/lib/types/reporting'
8
+ import { week_starts_on_to_index } from '@/lib/week_starts_on_to_index'
9
+
10
+ const empty_range: ReportingDateRangeInputs = {
11
+ from_date: '',
12
+ to_date: '',
13
+ }
14
+
15
+ /**
16
+ * Returns the initial reporting date range from the user's preference.
17
+ */
18
+ export function get_initial_reporting_range_inputs(
19
+ range: DefaultReportingRange = default_reporting_range_preference.read(),
20
+ week_starts_on: WeekStartsOn = 'monday',
21
+ ): ReportingDateRangeInputs {
22
+ if (range === 'none') {
23
+ return empty_range
24
+ }
25
+
26
+ return get_reporting_date_range_shortcut_inputs(
27
+ range,
28
+ new Date(),
29
+ week_starts_on_to_index(week_starts_on),
30
+ )
31
+ }
@@ -0,0 +1,15 @@
1
+ export type InputSize = 'default' | 'compact'
2
+
3
+ /**
4
+ * Returns Tailwind classes for themed text inputs and selects.
5
+ */
6
+ export function get_input_class_name(size: InputSize = 'default'): string {
7
+ const base =
8
+ 'box-border max-w-full min-w-0 w-full rounded-[0.65rem] border border-panel-border bg-input-bg px-3 py-2.5 font-inherit text-inherit transition-[background-color,border-color] duration-200 focus:border-input-focus-border focus:outline-none'
9
+
10
+ if (size === 'compact') {
11
+ return `${base} px-2.5 py-2`
12
+ }
13
+
14
+ return base
15
+ }
@@ -0,0 +1,25 @@
1
+ import { type ConfirmDialogOptions } from '@/lib/types/confirm_dialog'
2
+ import { format_display_tag } from '@/lib/format_display_tag'
3
+
4
+ /**
5
+ * Builds confirm dialog options for merging tags.
6
+ */
7
+ export function get_merge_tags_confirm_dialog(
8
+ source_tags: string[],
9
+ target_tag: string,
10
+ entry_count: number,
11
+ ): ConfirmDialogOptions {
12
+ const source_label = source_tags.map((tag) => format_display_tag(tag)).join(', ')
13
+ const target_label = format_display_tag(target_tag)
14
+ const entry_note =
15
+ entry_count === 1
16
+ ? '1 entry will be updated.'
17
+ : `${entry_count} entries will be updated.`
18
+
19
+ return {
20
+ title: 'Merge tags?',
21
+ message: `Merge ${source_label} into ${target_label}? ${entry_note}`,
22
+ confirmLabel: 'Merge tags',
23
+ variant: 'danger',
24
+ }
25
+ }
@@ -0,0 +1,43 @@
1
+ import {
2
+ endOfDay,
3
+ endOfMonth,
4
+ endOfWeek,
5
+ startOfDay,
6
+ startOfMonth,
7
+ startOfWeek,
8
+ } from 'date-fns'
9
+
10
+ export type ReportingPeriod = 'today' | 'week' | 'month'
11
+
12
+ export interface PeriodRangeMs {
13
+ startMs: number
14
+ endMs: number
15
+ }
16
+
17
+ /**
18
+ * Returns inclusive millisecond bounds for a reporting calendar period.
19
+ */
20
+ export function get_period_range_ms(
21
+ period: ReportingPeriod,
22
+ reference: Date = new Date(),
23
+ week_starts_on: 0 | 1 = 1,
24
+ ): PeriodRangeMs {
25
+ switch (period) {
26
+ case 'week':
27
+ return {
28
+ startMs: +startOfWeek(reference, { weekStartsOn: week_starts_on }),
29
+ endMs: +endOfWeek(reference, { weekStartsOn: week_starts_on }),
30
+ }
31
+ case 'month':
32
+ return {
33
+ startMs: +startOfMonth(reference),
34
+ endMs: +endOfMonth(reference),
35
+ }
36
+ case 'today':
37
+ default:
38
+ return {
39
+ startMs: +startOfDay(reference),
40
+ endMs: +endOfDay(reference),
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,84 @@
1
+ import {
2
+ endOfMonth,
3
+ endOfWeek,
4
+ endOfYear,
5
+ format,
6
+ startOfDay,
7
+ startOfMonth,
8
+ startOfWeek,
9
+ startOfYear,
10
+ subDays,
11
+ subMonths,
12
+ subYears,
13
+ } from 'date-fns'
14
+
15
+ import {
16
+ type ReportingDateRangeInputs,
17
+ type ReportingDateRangeShortcut,
18
+ } from '@/lib/types/reporting'
19
+
20
+ /**
21
+ * Returns date input values for a reporting range shortcut.
22
+ */
23
+ export function get_reporting_date_range_shortcut_inputs(
24
+ shortcut: ReportingDateRangeShortcut,
25
+ reference: Date = new Date(),
26
+ week_starts_on: 0 | 1 = 1,
27
+ ): ReportingDateRangeInputs {
28
+ const format_input_date = (date: Date): string => format(date, 'yyyy-MM-dd')
29
+
30
+ switch (shortcut) {
31
+ case 'yesterday': {
32
+ const day = subDays(reference, 1)
33
+
34
+ return {
35
+ from_date: format_input_date(startOfDay(day)),
36
+ to_date: format_input_date(startOfDay(day)),
37
+ }
38
+ }
39
+ case 'week':
40
+ return {
41
+ from_date: format_input_date(
42
+ startOfWeek(reference, { weekStartsOn: week_starts_on }),
43
+ ),
44
+ to_date: format_input_date(
45
+ endOfWeek(reference, { weekStartsOn: week_starts_on }),
46
+ ),
47
+ }
48
+ case 'month':
49
+ return {
50
+ from_date: format_input_date(startOfMonth(reference)),
51
+ to_date: format_input_date(endOfMonth(reference)),
52
+ }
53
+ case 'last_month': {
54
+ const last_month = subMonths(reference, 1)
55
+
56
+ return {
57
+ from_date: format_input_date(startOfMonth(last_month)),
58
+ to_date: format_input_date(endOfMonth(last_month)),
59
+ }
60
+ }
61
+ case 'year':
62
+ return {
63
+ from_date: format_input_date(startOfYear(reference)),
64
+ to_date: format_input_date(endOfYear(reference)),
65
+ }
66
+ case 'last_year': {
67
+ const last_year = subYears(reference, 1)
68
+
69
+ return {
70
+ from_date: format_input_date(startOfYear(last_year)),
71
+ to_date: format_input_date(endOfYear(last_year)),
72
+ }
73
+ }
74
+ case 'today':
75
+ default: {
76
+ const day = startOfDay(reference)
77
+
78
+ return {
79
+ from_date: format_input_date(day),
80
+ to_date: format_input_date(day),
81
+ }
82
+ }
83
+ }
84
+ }
@@ -0,0 +1,39 @@
1
+ import { get_period_range_ms } from '@/lib/get_period_range_ms'
2
+ import { get_sheets_duration_in_range } from '@/lib/get_sheets_duration_in_range'
3
+ import { type ReportingPeriodTotals } from '@/lib/types/reporting'
4
+ import { type TimeSheet } from '@/lib/types'
5
+
6
+ /**
7
+ * Builds today, week, and month totals with duration clipped to each period.
8
+ */
9
+ export function get_reporting_period_totals(
10
+ sheets: TimeSheet[],
11
+ reference: Date = new Date(),
12
+ now: number = Date.now(),
13
+ week_starts_on: 0 | 1 = 1,
14
+ ): ReportingPeriodTotals {
15
+ const today_range = get_period_range_ms('today', reference, week_starts_on)
16
+ const week_range = get_period_range_ms('week', reference, week_starts_on)
17
+ const month_range = get_period_range_ms('month', reference, week_starts_on)
18
+
19
+ return {
20
+ todayMs: get_sheets_duration_in_range(
21
+ sheets,
22
+ today_range.startMs,
23
+ today_range.endMs,
24
+ now,
25
+ ),
26
+ weekMs: get_sheets_duration_in_range(
27
+ sheets,
28
+ week_range.startMs,
29
+ week_range.endMs,
30
+ now,
31
+ ),
32
+ monthMs: get_sheets_duration_in_range(
33
+ sheets,
34
+ month_range.startMs,
35
+ month_range.endMs,
36
+ now,
37
+ ),
38
+ }
39
+ }
@@ -0,0 +1,25 @@
1
+ import { build_reporting_stats } from '@/lib/build_reporting_stats'
2
+ import { read_db } from '@/lib/read_db'
3
+ import { serialize_reporting_source_sheets } from '@/lib/serialize_reporting_source_sheets'
4
+ import {
5
+ type ReportingSourceSheet,
6
+ type ReportingStats,
7
+ } from '@/lib/types/reporting'
8
+
9
+ export interface ReportingPageData {
10
+ sourceSheets: ReportingSourceSheet[]
11
+ stats: ReportingStats
12
+ }
13
+
14
+ /**
15
+ * Loads reporting source data and the default all-time snapshot.
16
+ */
17
+ export async function get_reporting_stats(): Promise<ReportingPageData> {
18
+ const db = await read_db()
19
+ const source_sheets = serialize_reporting_source_sheets(db.sheets)
20
+
21
+ return {
22
+ sourceSheets: source_sheets,
23
+ stats: build_reporting_stats(db.sheets),
24
+ }
25
+ }
@@ -0,0 +1,14 @@
1
+ import { type ConfirmDialogOptions } from '@/lib/types/confirm_dialog'
2
+
3
+ /**
4
+ * Builds confirm dialog options for restoring a database backup.
5
+ */
6
+ export function get_restore_db_confirm_dialog(): ConfirmDialogOptions {
7
+ return {
8
+ title: 'Restore backup?',
9
+ message:
10
+ 'This will replace your current time tracker data with the uploaded backup file. This cannot be undone.',
11
+ confirmLabel: 'Restore',
12
+ variant: 'danger',
13
+ }
14
+ }
@@ -0,0 +1,8 @@
1
+ import { type SerializedEntry } from '@/lib/types/tracker_state'
2
+
3
+ /**
4
+ * Stable key for a running entry across tracker state updates.
5
+ */
6
+ export function get_running_entry_key(entry: SerializedEntry): string {
7
+ return `${entry.sheetName}:${entry.id}`
8
+ }
@@ -0,0 +1,10 @@
1
+ import { type SerializedEntry } from '@/lib/types/tracker_state'
2
+
3
+ /**
4
+ * Sums duration milliseconds across serialized entries.
5
+ */
6
+ export function get_serialized_entries_total_ms(
7
+ entries: SerializedEntry[],
8
+ ): number {
9
+ return entries.reduce((total, entry) => total + entry.durationMs, 0)
10
+ }
@@ -0,0 +1,14 @@
1
+ import { type TimeSheet, type TimeTrackerDB } from "@/lib/types";
2
+
3
+ /**
4
+ * Returns a sheet by name or throws when it does not exist.
5
+ */
6
+ export function get_sheet(db: TimeTrackerDB, sheet_name: string): TimeSheet {
7
+ const sheet = db.sheets.find(({ name }) => name === sheet_name);
8
+
9
+ if (sheet === undefined) {
10
+ throw new Error(`Sheet ${sheet_name} not found`);
11
+ }
12
+
13
+ return sheet;
14
+ }
@@ -0,0 +1,22 @@
1
+ import { get_average_entry_ms } from '@/lib/get_average_entry_ms'
2
+ import { get_serialized_entries_total_ms } from '@/lib/get_serialized_entries_total_ms'
3
+ import { serialize_sheet_entries } from '@/lib/serialize_sheet_entries'
4
+ import { type SheetReportStats } from '@/lib/types/reporting'
5
+ import { type TimeSheet } from '@/lib/types'
6
+
7
+ /**
8
+ * Builds per-sheet time-tracking aggregates from a time sheet.
9
+ */
10
+ export function get_sheet_report_stats(sheet: TimeSheet): SheetReportStats {
11
+ const entries = serialize_sheet_entries(sheet)
12
+ const entry_count = sheet.entries.length
13
+ const total_ms = get_serialized_entries_total_ms(entries)
14
+
15
+ return {
16
+ sheetName: sheet.name,
17
+ totalMs: total_ms,
18
+ entryCount: entry_count,
19
+ averageEntryMs: get_average_entry_ms(total_ms, entry_count),
20
+ hasActiveEntry: sheet.activeEntryID !== null,
21
+ }
22
+ }
@@ -0,0 +1,46 @@
1
+ import { get_average_entry_ms } from '@/lib/get_average_entry_ms'
2
+ import { get_clipped_entry_duration_ms } from '@/lib/get_clipped_entry_duration_ms'
3
+ import { type SheetReportStats } from '@/lib/types/reporting'
4
+ import { type TimeSheet } from '@/lib/types'
5
+
6
+ /**
7
+ * Builds per-sheet aggregates with durations clipped to a date range.
8
+ */
9
+ export function get_sheet_report_stats_for_range(
10
+ sheet: TimeSheet,
11
+ range_start_ms: number,
12
+ range_end_ms: number,
13
+ now: number = Date.now(),
14
+ ): SheetReportStats {
15
+ let total_ms = 0
16
+ let entry_count = 0
17
+ let has_active_entry = false
18
+
19
+ for (const entry of sheet.entries) {
20
+ const clipped_ms = get_clipped_entry_duration_ms(
21
+ entry,
22
+ range_start_ms,
23
+ range_end_ms,
24
+ now,
25
+ )
26
+
27
+ if (clipped_ms <= 0) {
28
+ continue
29
+ }
30
+
31
+ total_ms += clipped_ms
32
+ entry_count += 1
33
+
34
+ if (sheet.activeEntryID === entry.id && entry.end === null) {
35
+ has_active_entry = true
36
+ }
37
+ }
38
+
39
+ return {
40
+ sheetName: sheet.name,
41
+ totalMs: total_ms,
42
+ entryCount: entry_count,
43
+ averageEntryMs: get_average_entry_ms(total_ms, entry_count),
44
+ hasActiveEntry: has_active_entry,
45
+ }
46
+ }
@@ -0,0 +1,22 @@
1
+ import { read_sheet_tag_filter } from '@/lib/read_sheet_tag_filter'
2
+ import {
3
+ EMPTY_SHEET_TAG_FILTER,
4
+ get_stable_sheet_tag_filter_snapshot,
5
+ } from '@/lib/sheet_tag_filter_snapshots'
6
+
7
+ /**
8
+ * Returns the sheet tag filter snapshot from localStorage (client-only).
9
+ */
10
+ export function get_sheet_tag_filter_snapshot(sheet_name: string): readonly string[] {
11
+ return get_stable_sheet_tag_filter_snapshot(
12
+ sheet_name,
13
+ read_sheet_tag_filter(sheet_name),
14
+ )
15
+ }
16
+
17
+ /**
18
+ * Returns the sheet tag filter snapshot used during server rendering.
19
+ */
20
+ export function get_sheet_tag_filter_server_snapshot(): readonly string[] {
21
+ return EMPTY_SHEET_TAG_FILTER
22
+ }
@@ -0,0 +1,27 @@
1
+ import { get_clipped_entry_duration_ms } from '@/lib/get_clipped_entry_duration_ms'
2
+ import { type TimeSheet } from '@/lib/types'
3
+
4
+ /**
5
+ * Sums clipped entry durations across all sheets within a time range.
6
+ */
7
+ export function get_sheets_duration_in_range(
8
+ sheets: TimeSheet[],
9
+ range_start_ms: number,
10
+ range_end_ms: number,
11
+ now: number = Date.now(),
12
+ ): number {
13
+ let total_ms = 0
14
+
15
+ for (const sheet of sheets) {
16
+ for (const entry of sheet.entries) {
17
+ total_ms += get_clipped_entry_duration_ms(
18
+ entry,
19
+ range_start_ms,
20
+ range_end_ms,
21
+ now,
22
+ )
23
+ }
24
+ }
25
+
26
+ return total_ms
27
+ }
@@ -0,0 +1,32 @@
1
+ export interface TagAutocompleteContext {
2
+ query: string
3
+ start_index: number
4
+ end_index: number
5
+ }
6
+
7
+ /**
8
+ * Returns tag autocomplete context when the cursor is after an open @ token.
9
+ */
10
+ export function get_tag_autocomplete_context(
11
+ text: string,
12
+ cursor_index: number,
13
+ ): TagAutocompleteContext | null {
14
+ const before_cursor = text.slice(0, cursor_index)
15
+ const at_index = before_cursor.lastIndexOf('@')
16
+
17
+ if (at_index === -1) {
18
+ return null
19
+ }
20
+
21
+ const query = before_cursor.slice(at_index + 1)
22
+
23
+ if (/\s/.test(query)) {
24
+ return null
25
+ }
26
+
27
+ return {
28
+ query,
29
+ start_index: at_index,
30
+ end_index: cursor_index,
31
+ }
32
+ }
@@ -0,0 +1,16 @@
1
+ import { read_document_theme } from "@/lib/read_document_theme";
2
+ import { type Theme } from "@/lib/types/theme";
3
+
4
+ /**
5
+ * Returns the theme snapshot read from the document (client-only).
6
+ */
7
+ export function get_theme_snapshot(): Theme {
8
+ return read_document_theme();
9
+ }
10
+
11
+ /**
12
+ * Returns the theme snapshot used during server rendering.
13
+ */
14
+ export function get_theme_server_snapshot(): Theme {
15
+ return "dark";
16
+ }
@@ -0,0 +1,67 @@
1
+ import { collect_known_tags } from '@/lib/collect_known_tags'
2
+ import { DB_PATH } from '@/lib/config'
3
+ import { get_sheet } from '@/lib/get_sheet'
4
+ import { get_serialized_entries_total_ms } from '@/lib/get_serialized_entries_total_ms'
5
+ import { read_db } from '@/lib/read_db'
6
+ import { resolve_active_sheet_name } from '@/lib/resolve_active_sheet_name'
7
+ import { find_all_serialized_active_entries } from '@/lib/find_all_serialized_active_entries'
8
+ import { find_serialized_active_entry_for_sheet } from '@/lib/find_serialized_active_entry_for_sheet'
9
+ import { serialize_sheet_entries } from '@/lib/serialize_sheet_entries'
10
+ import { set_active_sheet } from '@/lib/set_active_sheet'
11
+ import { sort_serialized_entries } from '@/lib/sort_serialized_entries'
12
+ import {
13
+ type SerializedEntry,
14
+ type TrackerState,
15
+ } from '@/lib/types/tracker_state'
16
+
17
+ /**
18
+ * Builds the tracker snapshot consumed by the web UI.
19
+ */
20
+ export async function get_tracker_state(
21
+ preferred_sheet_name?: string | null,
22
+ ): Promise<TrackerState> {
23
+ const db = await read_db()
24
+ const resolved_sheet_name = resolve_active_sheet_name(db, preferred_sheet_name)
25
+
26
+ if (db.activeSheetName !== resolved_sheet_name) {
27
+ await set_active_sheet(resolved_sheet_name)
28
+ db.activeSheetName = resolved_sheet_name
29
+ }
30
+
31
+ const { activeSheetName, sheets } = db
32
+
33
+ let active_sheet_entries: SerializedEntry[] = []
34
+
35
+ if (activeSheetName !== null) {
36
+ const sheet = get_sheet(db, activeSheetName)
37
+
38
+ active_sheet_entries = sort_serialized_entries(
39
+ serialize_sheet_entries(sheet),
40
+ )
41
+ }
42
+
43
+ const active_sheet_entry =
44
+ activeSheetName !== null
45
+ ? find_serialized_active_entry_for_sheet(db, activeSheetName)
46
+ : null
47
+ const running_entries = find_all_serialized_active_entries(db)
48
+ const running_entry = active_sheet_entry ?? running_entries[0] ?? null
49
+
50
+ return {
51
+ dbPath: DB_PATH,
52
+ activeSheetName,
53
+ knownTags: collect_known_tags(db),
54
+ sheets: sheets.map((sheet) => ({
55
+ name: sheet.name,
56
+ activeEntryID: sheet.activeEntryID,
57
+ entryCount: sheet.entries.length,
58
+ isActive: sheet.name === activeSheetName,
59
+ hasActiveEntry: sheet.activeEntryID !== null,
60
+ })),
61
+ activeEntry: active_sheet_entry,
62
+ runningEntry: running_entry,
63
+ runningEntries: running_entries,
64
+ activeSheetEntries: active_sheet_entries,
65
+ activeSheetTotalMs: get_serialized_entries_total_ms(active_sheet_entries),
66
+ }
67
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Returns true when the value is a defined, non-empty string.
3
+ */
4
+ export function has_string_value(value: string | undefined): value is string {
5
+ return value !== undefined && value.trim().length > 0;
6
+ }
@@ -0,0 +1,15 @@
1
+ import { endOfDay, startOfDay } from "date-fns";
2
+
3
+ import { type TimeSheetEntry } from "@/lib/types";
4
+
5
+ /**
6
+ * Returns whether an entry overlaps any time on the given calendar day.
7
+ */
8
+ export function is_entry_in_day(date: Date, entry: TimeSheetEntry): boolean {
9
+ const { end, start } = entry;
10
+ const start_of_day = startOfDay(date);
11
+ const end_of_day = endOfDay(date);
12
+ const effective_end = end ?? new Date();
13
+
14
+ return +start <= +end_of_day && +effective_end >= +start_of_day;
15
+ }
@@ -0,0 +1,8 @@
1
+ import { type SheetReportStats } from '@/lib/types/reporting'
2
+
3
+ /**
4
+ * Returns whether a sheet has no entries or no tracked time.
5
+ */
6
+ export function is_idle_sheet_report(sheet: SheetReportStats): boolean {
7
+ return sheet.entryCount === 0 || sheet.totalMs === 0
8
+ }
@@ -0,0 +1,14 @@
1
+ import { type JSONTimeTrackerDB } from '@/lib/types'
2
+
3
+ /**
4
+ * Returns whether a value looks like a serialized time tracker database.
5
+ */
6
+ export function is_json_time_tracker_db(value: unknown): value is JSONTimeTrackerDB {
7
+ if (typeof value !== 'object' || value === null) {
8
+ return false
9
+ }
10
+
11
+ const candidate = value as JSONTimeTrackerDB
12
+
13
+ return Array.isArray(candidate.sheets)
14
+ }