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.
- package/AGENTS.md +5 -0
- package/CHANGELOG.md +28 -0
- package/CLAUDE.md +1 -0
- package/README.md +36 -0
- package/app/api/backup/route.ts +39 -0
- package/app/api/entry/delete-bulk/route.ts +53 -0
- package/app/api/entry/move/route.ts +46 -0
- package/app/api/entry/move-bulk/route.ts +62 -0
- package/app/api/entry/route.ts +75 -0
- package/app/api/in/route.ts +38 -0
- package/app/api/note/route.ts +120 -0
- package/app/api/out/route.ts +31 -0
- package/app/api/sheet/route.ts +68 -0
- package/app/api/state/route.ts +16 -0
- package/app/api/tags/route.ts +75 -0
- package/app/color-palettes.css +260 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +140 -0
- package/app/layout.tsx +54 -0
- package/app/page.tsx +24 -0
- package/app/reporting/page.tsx +11 -0
- package/app/settings/data/page.tsx +9 -0
- package/app/settings/display/page.tsx +8 -0
- package/app/settings/page.tsx +12 -0
- package/app/settings/tags/page.tsx +13 -0
- package/bin/stt-ui.js +63 -0
- package/components/active-entry-panel.tsx +199 -0
- package/components/backup-restore-setting.tsx +168 -0
- package/components/check-in-form-collapsed-setting.tsx +44 -0
- package/components/check-in-form-collapsible.tsx +52 -0
- package/components/check-in-form.tsx +89 -0
- package/components/checkbox.tsx +75 -0
- package/components/checkout-button-group.tsx +73 -0
- package/components/chevron-icon.tsx +25 -0
- package/components/clear-tag-filters-on-sheet-change-setting.tsx +45 -0
- package/components/color-palette-setting.tsx +75 -0
- package/components/compact-lists-setting.tsx +42 -0
- package/components/confirm-before-checkout-setting.tsx +42 -0
- package/components/confirm-destructive-actions-setting.tsx +46 -0
- package/components/confirm-dialog-provider.tsx +71 -0
- package/components/confirm-dialog.tsx +90 -0
- package/components/data-settings-view.tsx +47 -0
- package/components/default-reporting-range-setting.tsx +56 -0
- package/components/default-reporting-sort-setting.tsx +45 -0
- package/components/default-sheet-session-setting.tsx +118 -0
- package/components/display-settings-view.tsx +75 -0
- package/components/duration-format-setting.tsx +40 -0
- package/components/entry-actions-menu.tsx +207 -0
- package/components/entry-edit-form.tsx +113 -0
- package/components/entry-list-bulk-bar.tsx +128 -0
- package/components/entry-list-sort-setting.tsx +41 -0
- package/components/entry-list.tsx +336 -0
- package/components/entry-notes-list.tsx +211 -0
- package/components/entry-tag-filter.tsx +99 -0
- package/components/format_datetime_hint.ts +8 -0
- package/components/format_time.ts +10 -0
- package/components/general-settings-view.tsx +40 -0
- package/components/hamburger-icon.tsx +21 -0
- package/components/note-edit-form.tsx +77 -0
- package/components/note-form.tsx +109 -0
- package/components/pencil-icon.tsx +21 -0
- package/components/reporting-date-range-picker.tsx +121 -0
- package/components/reporting-sort-controls.tsx +53 -0
- package/components/reporting-view.tsx +340 -0
- package/components/setting-radio-group.tsx +79 -0
- package/components/settings-nav.tsx +66 -0
- package/components/settings-page-layout.tsx +53 -0
- package/components/settings-saved-toast.tsx +57 -0
- package/components/sheet-actions-menu.tsx +108 -0
- package/components/sheet-sidebar.tsx +196 -0
- package/components/tag-autocomplete-input.tsx +183 -0
- package/components/tag-filter-mode-setting.tsx +47 -0
- package/components/tag-management-view.tsx +290 -0
- package/components/theme-mode-setting.tsx +44 -0
- package/components/theme-mode-system-listener.tsx +43 -0
- package/components/theme_switcher.tsx +38 -0
- package/components/time-format-setting.tsx +39 -0
- package/components/timer-in-title-setting.tsx +38 -0
- package/components/timer-show-seconds-setting.tsx +41 -0
- package/components/tracker-active-bar.tsx +76 -0
- package/components/tracker-app.tsx +338 -0
- package/components/tracker-breadcrumb.tsx +56 -0
- package/components/tracker-document-title.tsx +67 -0
- package/components/tracker-topbar.tsx +63 -0
- package/components/trash-icon.tsx +24 -0
- package/components/week-starts-on-setting.tsx +39 -0
- package/eslint.config.mjs +18 -0
- package/lib/add_note_to_entry.ts +65 -0
- package/lib/api_error_response.ts +10 -0
- package/lib/apply_accent_color.ts +12 -0
- package/lib/apply_color_palette.ts +12 -0
- package/lib/apply_compact_lists.ts +9 -0
- package/lib/apply_tag_autocomplete_selection.ts +26 -0
- package/lib/apply_theme.ts +8 -0
- package/lib/build_reporting_stats.ts +55 -0
- package/lib/build_resume_description.ts +15 -0
- package/lib/check_in_entry.ts +81 -0
- package/lib/check_out_entry.ts +75 -0
- package/lib/collect_known_tags.ts +22 -0
- package/lib/collect_tag_stats.ts +27 -0
- package/lib/collect_tags_from_entries.ts +35 -0
- package/lib/config.ts +9 -0
- package/lib/convert_json_db.ts +49 -0
- package/lib/delete_entries.ts +62 -0
- package/lib/delete_entry.ts +29 -0
- package/lib/delete_note_on_entry.ts +42 -0
- package/lib/delete_sheet.ts +30 -0
- package/lib/delete_tracker_action.ts +22 -0
- package/lib/edit_entry.ts +56 -0
- package/lib/edit_note_on_entry.ts +49 -0
- package/lib/ensure_dir_exists.ts +22 -0
- package/lib/entry_matches_tag_filter.ts +26 -0
- package/lib/fetch_tracker_state.ts +15 -0
- package/lib/filter_entries_by_tags.ts +20 -0
- package/lib/filter_known_tags.ts +20 -0
- package/lib/find_all_serialized_active_entries.ts +28 -0
- package/lib/find_serialized_active_entry.ts +12 -0
- package/lib/find_serialized_active_entry_for_sheet.ts +31 -0
- package/lib/find_sheet_with_active_entry.ts +16 -0
- package/lib/format_display_tag.ts +6 -0
- package/lib/format_duration.ts +45 -0
- package/lib/gen_db.ts +43 -0
- package/lib/get_active_panel_class_name.ts +20 -0
- package/lib/get_average_entry_ms.ts +13 -0
- package/lib/get_button_class_name.ts +24 -0
- package/lib/get_check_out_confirm_dialog.ts +19 -0
- package/lib/get_clipped_entry_duration_ms.ts +18 -0
- package/lib/get_compact_lists_snapshot.ts +15 -0
- package/lib/get_date_range_ms_from_inputs.ts +31 -0
- package/lib/get_delete_entries_confirm_dialog.ts +21 -0
- package/lib/get_delete_entry_confirm_dialog.ts +19 -0
- package/lib/get_delete_note_confirm_dialog.ts +21 -0
- package/lib/get_delete_sheet_confirm_dialog.ts +25 -0
- package/lib/get_entry_duration_ms.ts +14 -0
- package/lib/get_entry_row_key.ts +8 -0
- package/lib/get_initial_preferred_sheet_name.ts +34 -0
- package/lib/get_initial_reporting_range_inputs.ts +31 -0
- package/lib/get_input_class_name.ts +15 -0
- package/lib/get_merge_tags_confirm_dialog.ts +25 -0
- package/lib/get_period_range_ms.ts +43 -0
- package/lib/get_reporting_date_range_shortcut_inputs.ts +84 -0
- package/lib/get_reporting_period_totals.ts +39 -0
- package/lib/get_reporting_stats.ts +25 -0
- package/lib/get_restore_db_confirm_dialog.ts +14 -0
- package/lib/get_running_entry_key.ts +8 -0
- package/lib/get_serialized_entries_total_ms.ts +10 -0
- package/lib/get_sheet.ts +14 -0
- package/lib/get_sheet_report_stats.ts +22 -0
- package/lib/get_sheet_report_stats_for_range.ts +46 -0
- package/lib/get_sheet_tag_filter_snapshot.ts +22 -0
- package/lib/get_sheets_duration_in_range.ts +27 -0
- package/lib/get_tag_autocomplete_context.ts +32 -0
- package/lib/get_theme_snapshot.ts +16 -0
- package/lib/get_tracker_state.ts +67 -0
- package/lib/has_string_value.ts +6 -0
- package/lib/is_entry_in_day.ts +15 -0
- package/lib/is_idle_sheet_report.ts +8 -0
- package/lib/is_json_time_tracker_db.ts +14 -0
- package/lib/merge_tags_across_db.ts +79 -0
- package/lib/migrate_json_db.ts +56 -0
- package/lib/migrate_json_db_to_version_three.ts +51 -0
- package/lib/migrate_json_db_to_version_two.ts +50 -0
- package/lib/move_entries_to_sheet.ts +152 -0
- package/lib/move_entry_to_sheet.ts +82 -0
- package/lib/normalize_stored_tag.ts +16 -0
- package/lib/notify_settings_saved.ts +47 -0
- package/lib/parse_default_sheet_session_mode.ts +21 -0
- package/lib/parse_entry_from_input.ts +23 -0
- package/lib/parse_natural_language_date.ts +23 -0
- package/lib/parse_reporting_source_sheets.ts +22 -0
- package/lib/partition_sheet_report_stats.ts +30 -0
- package/lib/patch_tracker_action.ts +22 -0
- package/lib/persist_ui_preference.ts +18 -0
- package/lib/post_tracker_action.ts +22 -0
- package/lib/preferences/accent_color_preference.ts +21 -0
- package/lib/preferences/check_in_form_collapsed_preference.ts +20 -0
- package/lib/preferences/clear_tag_filters_on_sheet_change_preference.ts +20 -0
- package/lib/preferences/color_palette_preference.ts +21 -0
- package/lib/preferences/confirm_before_checkout_preference.ts +20 -0
- package/lib/preferences/confirm_destructive_actions_preference.ts +20 -0
- package/lib/preferences/default_reporting_range_preference.ts +21 -0
- package/lib/preferences/default_reporting_sort_preference.ts +24 -0
- package/lib/preferences/duration_format_preference.ts +19 -0
- package/lib/preferences/entry_list_sort_preference.ts +21 -0
- package/lib/preferences/tag_filter_mode_preference.ts +18 -0
- package/lib/preferences/theme_mode_preference.ts +18 -0
- package/lib/preferences/time_format_preference.ts +18 -0
- package/lib/preferences/timer_in_title_preference.ts +18 -0
- package/lib/preferences/timer_show_seconds_preference.ts +19 -0
- package/lib/preferences/week_starts_on_preference.ts +19 -0
- package/lib/prompt_check_out_at.ts +17 -0
- package/lib/prompt_entry_note.ts +14 -0
- package/lib/prune_sheet_tag_filter.ts +27 -0
- package/lib/read_db.ts +49 -0
- package/lib/read_db_backup_contents.ts +22 -0
- package/lib/read_document_compact_lists.ts +12 -0
- package/lib/read_document_theme.ts +14 -0
- package/lib/read_sheet_tag_filter.ts +26 -0
- package/lib/read_stored_active_sheet.ts +14 -0
- package/lib/read_stored_compact_lists.ts +24 -0
- package/lib/read_stored_default_sheet_fixed_name.ts +16 -0
- package/lib/read_stored_default_sheet_session_mode.ts +18 -0
- package/lib/read_stored_sheet_tag_filters.ts +28 -0
- package/lib/read_stored_theme.ts +18 -0
- package/lib/rename_sheet.ts +39 -0
- package/lib/rename_tag_across_db.ts +19 -0
- package/lib/resolve_active_sheet_name.ts +36 -0
- package/lib/resolve_session_preferred_sheet.ts +37 -0
- package/lib/resolve_theme.ts +18 -0
- package/lib/resolve_theme_mode_to_theme.ts +19 -0
- package/lib/restore_db_from_uploaded_json.ts +24 -0
- package/lib/serialize_entry.ts +27 -0
- package/lib/serialize_reporting_source_sheets.ts +19 -0
- package/lib/serialize_sheet_entries.ts +18 -0
- package/lib/set_accent_color.ts +12 -0
- package/lib/set_active_sheet.ts +18 -0
- package/lib/set_color_palette.ts +12 -0
- package/lib/set_compact_lists.ts +12 -0
- package/lib/set_default_sheet_fixed_name.ts +8 -0
- package/lib/set_default_sheet_session_mode.ts +11 -0
- package/lib/set_sheet_tag_filter.ts +13 -0
- package/lib/set_theme_mode.ts +19 -0
- package/lib/sheet_tag_filter_snapshots.ts +48 -0
- package/lib/sort_serialized_entries.ts +35 -0
- package/lib/sort_sheet_report_stats.ts +43 -0
- package/lib/subscribe_compact_lists.ts +25 -0
- package/lib/subscribe_sheet_tag_filters.ts +28 -0
- package/lib/subscribe_theme.ts +23 -0
- package/lib/sync_active_sheet_preference.ts +19 -0
- package/lib/tags_are_equal.ts +12 -0
- package/lib/theme_init_script.ts +11 -0
- package/lib/toggle_sheet_tag_filter.ts +28 -0
- package/lib/toggle_theme.ts +20 -0
- package/lib/types/confirm_dialog.ts +9 -0
- package/lib/types/data.ts +16 -0
- package/lib/types/generic_data.ts +25 -0
- package/lib/types/index.ts +2 -0
- package/lib/types/reporting.ts +59 -0
- package/lib/types/tag_management.ts +7 -0
- package/lib/types/theme.ts +3 -0
- package/lib/types/tracker_state.ts +39 -0
- package/lib/types/ui_preferences.ts +104 -0
- package/lib/types/ui_settings.ts +17 -0
- package/lib/ui_preference_store.ts +80 -0
- package/lib/ui_settings_init_script.ts +33 -0
- package/lib/use_check_in_form_collapsed.ts +18 -0
- package/lib/use_clear_tag_filters_on_sheet_change.ts +18 -0
- package/lib/use_confirm_before_checkout.ts +18 -0
- package/lib/use_confirm_destructive_actions.ts +18 -0
- package/lib/use_duration_format.ts +17 -0
- package/lib/use_entry_list_sort.ts +17 -0
- package/lib/use_tag_filter_mode.ts +17 -0
- package/lib/use_time_format.ts +17 -0
- package/lib/use_timer_in_title.ts +18 -0
- package/lib/use_timer_show_seconds.ts +18 -0
- package/lib/use_week_starts_on.ts +17 -0
- package/lib/validate_entry_times.ts +12 -0
- package/lib/week_starts_on_to_index.ts +8 -0
- package/lib/write_active_sheet_preference.ts +28 -0
- package/lib/write_db.ts +20 -0
- package/lib/write_sheet_tag_filter.ts +20 -0
- package/lib/write_stored_compact_lists.ts +15 -0
- package/lib/write_stored_default_sheet_fixed_name.ts +28 -0
- package/lib/write_stored_default_sheet_session_mode.ts +24 -0
- package/lib/write_stored_sheet_tag_filters.ts +18 -0
- package/lib/write_stored_theme.ts +12 -0
- package/next.config.ts +7 -0
- package/package.json +96 -0
- package/pnpm-workspace.yaml +7 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- 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,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
|
+
}
|
package/lib/get_sheet.ts
ADDED
|
@@ -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,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
|
+
}
|