diff --git a/docs/plans/2026-02-20-enhancement-round2-design.md b/docs/plans/2026-02-20-enhancement-round2-design.md new file mode 100644 index 0000000..384df49 --- /dev/null +++ b/docs/plans/2026-02-20-enhancement-round2-design.md @@ -0,0 +1,415 @@ +# ZeroClock Enhancement Round 2 - Design Document + +## Overview + +15 enhancements to existing ZeroClock functionality, balanced across polish/reliability, power-user productivity, and data/insights. All features WCAG 2.2 AAA compliant from the start. + +## Global WCAG 2.2 AAA Requirements (apply to every feature) + +- 7:1 minimum contrast ratio for all text against backgrounds +- All interactive elements have `focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-accent` +- All dialogs/popovers use `useFocusTrap`, `role="dialog"` or `role="alertdialog"`, `aria-modal="true"`, `aria-labelledby`/`aria-describedby` +- All dynamic content changes announced via `aria-live` regions +- All animations use `motion-safe:` prefix or respect the global `prefers-reduced-motion` override +- All information conveyed by color must also be conveyed by text, icon, or pattern (WCAG 1.4.1) +- All drag operations must have keyboard alternatives (WCAG 2.5.7) +- All timed interactions must be adjustable or disableable (WCAG 2.2.1) +- Design tokens used throughout: bg-bg-base, bg-bg-surface, bg-bg-elevated, text-text-primary, text-text-secondary, text-text-tertiary, border-border-subtle, accent, accent-hover + +--- + +## Phase 1: Polish and Reliability + +### 1. Smart Timer Safety Net + +**Problem:** Stopping the timer without a project selected silently discards tracked time. Timers left running for 8+ hours are likely accidental. + +**Enhancement:** +- When `timerStore.stop()` is called with no `selectedProjectId`, show a "Save Entry" dialog instead of discarding +- Dialog contains: project picker (AppSelect), description field, the tracked duration (read-only), Save and Discard buttons +- When stopping a timer with elapsed time > 8 hours, show a confirmation: "Timer has been running for X hours. Stop and save?" +- Both dialogs use focus trap, are keyboard-operable, and announce via aria-live + +**Files:** +- Modify: `src/stores/timer.ts` (stop method) +- Create: `src/components/TimerSaveDialog.vue` +- Modify: `src/App.vue` (mount the dialog) + +**Accessibility:** +- `role="alertdialog"` with focus trap +- `aria-live="assertive"` announcement when dialog opens: "Timer stopped. Select a project to save X hours of tracked time." +- Duration display uses `aria-label` with full spoken format ("2 hours and 34 minutes") + +--- + +### 2. Toast Auto-Dismiss with Undo + +**Problem:** Toasts persist indefinitely until manually closed or pushed out by newer ones. No way to undo destructive actions. + +**Enhancement:** +- Success toasts auto-dismiss after 4 seconds, error toasts after 8 seconds, info toasts after 6 seconds +- WCAG 2.2.1 compliance: toast timers pause on hover and on keyboard focus. Add a setting in General tab to disable auto-dismiss entirely ("Persistent notifications" toggle) +- Destructive operations (entry delete, expense delete, client delete, project delete) pass an `onUndo` callback to the toast store +- Toast with undo shows an "Undo" button. Clicking it calls the callback within the timeout window +- Undo button has `role="button"`, `aria-label="Undo delete"`, keyboard-accessible via Tab + +**Files:** +- Modify: `src/stores/toast.ts` (add duration, onUndo, pause/resume logic) +- Modify: `src/components/ToastNotification.vue` (auto-dismiss timer, undo button, hover/focus pause) +- Modify: `src/views/Settings.vue` (add persistent notifications toggle in General tab) + +**Accessibility:** +- Toast timer pauses on `:hover` and `:focus-within` (WCAG 2.2.1 Timing Adjustable) +- Undo button is keyboard-focusable and announced: "Undo available. Press to reverse." +- Screen reader announces remaining time: "Toast will dismiss in X seconds" +- User can disable auto-dismiss entirely in Settings + +--- + +### 3. Unified Error Handling with Retry + +**Problem:** Error handling is inconsistent across stores - some throw, some return null, some only console.error. Users rarely see actionable feedback. + +**Enhancement:** +- Create a shared `handleInvokeError(error, context)` utility that: + - Shows a toast with the error context ("Failed to save entry") + - For transient errors (connection, database busy), adds a "Retry" button to the toast + - Logs the full error to console for debugging +- Standardize all store catch blocks to use this utility +- The retry callback re-executes the failed operation + +**Files:** +- Create: `src/utils/errorHandler.ts` +- Modify: all 13 stores to use the shared handler in catch blocks + +**Accessibility:** +- Retry button in toast: `role="button"`, `aria-label="Retry failed operation"`, keyboard-accessible +- Error toasts use `role="alert"` (already the case) for immediate screen reader announcement +- After retry succeeds, announce "Operation completed successfully" via aria-live + +--- + +### 4. Onboarding Detection Resilience + +**Problem:** `detectCompletions()` in the onboarding store has all backend calls in a single try/catch - if `get_clients` fails, nothing else runs. No periodic re-check. + +**Enhancement:** +- Wrap each detection call in its own try/catch so failures are independent +- Add a 5-minute periodic re-check interval (started in App.vue alongside recurring entry checks) +- Clear the interval in onUnmounted + +**Files:** +- Modify: `src/stores/onboarding.ts` (individual try/catch per detection, no new UI) +- Modify: `src/App.vue` (add periodic re-check interval) + +**Accessibility:** +- No UI changes - this is purely backend resilience +- When an item transitions from incomplete to complete, the existing checklist UI already handles aria updates + +--- + +### 5. Invoice Items Save Reliability + +**Problem:** `saveInvoiceItems` creates items one at a time in a loop - if one fails, the rest are silently lost. No transaction rollback. + +**Enhancement:** +- Add a new Rust command `save_invoice_items_batch` that wraps all inserts in a single transaction (BEGIN/COMMIT/ROLLBACK) +- Replace the sequential loop in `invoicesStore.saveInvoiceItems` with a single call to the batch command +- On failure, show a clear error toast with retry option + +**Files:** +- Modify: `src-tauri/src/commands.rs` (add `save_invoice_items_batch` command) +- Modify: `src-tauri/src/lib.rs` (register command) +- Modify: `src/stores/invoices.ts` (replace loop with batch call) + +**Accessibility:** +- Error feedback via toast (already accessible) +- No new UI elements + +--- + +## Phase 2: Power User Productivity + +### 6. Global Quick Entry + +**Problem:** Logging a manual time entry requires navigating to Entries view and filling out a full form. No way to quickly capture time from wherever you are. + +**Enhancement:** +- Register a global keyboard shortcut (configurable, default Ctrl+Shift+N) via Tauri's global shortcut plugin +- Opens a compact floating dialog (Teleported to body, zoom-aware) with: + - Project picker (AppSelect) + - Task picker (AppSelect, filtered by project) + - Description text input + - Date picker (defaults to today) + - Duration input (supports H:MM and decimal formats) + - Tag picker (AppTagInput) + - Save and Cancel buttons +- Pre-fills with last-used project from settingsStore +- On save, creates entry via entriesStore.createEntry and shows success toast +- Dialog closes on Escape or Cancel + +**Files:** +- Create: `src/components/QuickEntryDialog.vue` +- Modify: `src/App.vue` (mount dialog, register shortcut) +- Modify: `src/views/Settings.vue` (add shortcut recorder for quick entry in Timer tab) + +**Accessibility:** +- `role="dialog"` with `aria-modal="true"`, `aria-label="Quick time entry"` +- Focus trap activated on open, first focusable element receives focus +- `aria-live="polite"` announces "Quick entry dialog opened" +- All form controls have visible labels (not just placeholders) +- Escape closes and restores focus to previously focused element +- `aria-keyshortcuts` attribute on the trigger describes the shortcut + +--- + +### 7. Entry Templates and Duplication + +**Problem:** Favorites only work for starting timers. No way to save and reuse manual entry configurations, and no quick duplicate for existing entries. + +**Enhancement:** +- Add "Save as Template" option when editing an entry (saves project + task + description + tags + duration to a new `entry_templates` table) +- Add "From Template" button in Entries view header that opens a picker dialog listing saved templates +- Selecting a template pre-fills the create entry form +- Add "Duplicate" button on each entry row in the entries table (creates a copy with today's date) +- Templates manageable (delete) from a section in Settings > Timer tab + +**Files:** +- Modify: `src-tauri/src/commands.rs` (add entry_templates CRUD commands + create table in database.rs) +- Modify: `src-tauri/src/lib.rs` (register commands) +- Create: `src/stores/entryTemplates.ts` +- Create: `src/components/EntryTemplatePicker.vue` +- Modify: `src/views/Entries.vue` (add From Template button, Duplicate button per row) +- Modify: `src/views/Settings.vue` (template management section) + +**Accessibility:** +- Template picker: `role="dialog"` with focus trap, `role="listbox"` for template list, `role="option"` per item +- Duplicate button: `aria-label="Duplicate entry for [project] on [date]"` +- "From Template" button: `aria-haspopup="dialog"` +- Template list items keyboard-navigable (ArrowUp/Down, Enter to select) +- Screen reader announces "Entry duplicated" or "Template applied" on action + +--- + +### 8. Timesheet Smart Row Persistence + +**Problem:** The timesheet view requires manually adding project/task rows each week. Starting a new week means rebuilding the same row configuration. + +**Enhancement:** +- When opening a week with no timesheet data, auto-populate rows from the previous week's row structure (projects + tasks, but with zero hours) +- Add a "Copy Last Week" button in the header that copies both structure and values +- Save row configuration per week in a `timesheet_rows` table so manually added rows persist across navigation +- "Copy Last Week" shows a confirmation dialog before overwriting any existing data + +**Files:** +- Modify: `src-tauri/src/commands.rs` (add `get_timesheet_rows`, `save_timesheet_rows`, `get_previous_week_structure` commands) +- Modify: `src-tauri/src/lib.rs` (register commands) +- Modify: `src/views/TimesheetView.vue` (auto-populate logic, copy button, persist rows) + +**Accessibility:** +- "Copy Last Week" button: `aria-label="Copy last week's timesheet structure"` +- Confirmation dialog for overwrite: `role="alertdialog"` with focus trap +- `aria-live="polite"` announces "Timesheet populated with X rows from last week" +- Auto-populated rows announced: "Loaded X project rows from previous week" + +--- + +### 9. Client Cascade Awareness + +**Problem:** Deleting a client has no cascade check or warning, unlike projects which now show dependent counts via AppCascadeDeleteDialog. + +**Enhancement:** +- Add `get_client_dependents` backend command that counts related projects, invoices, and expenses +- Add transactional cascade delete to `delete_client` (delete expenses, invoice items, invoices, tracked_apps, entry_tags, time_entries, favorites, recurring_entries, timeline_events, tasks, projects, then client) +- Wire AppCascadeDeleteDialog into Clients.vue delete flow (same pattern as Projects.vue) + +**Files:** +- Modify: `src-tauri/src/commands.rs` (add `get_client_dependents`, update `delete_client` to cascade) +- Modify: `src-tauri/src/lib.rs` (register command) +- Modify: `src/views/Clients.vue` (import AppCascadeDeleteDialog, add dependency check before delete) + +**Accessibility:** +- Reuses existing AppCascadeDeleteDialog which is already WCAG AAA compliant (focus trap, 3-second countdown, aria-live announcement, role="alertdialog") +- No new accessibility work needed - the existing component handles everything + +--- + +### 10. Expenses Receipt Management + +**Problem:** Expenses store a `receipt_path` but there's no way to view receipts inline, and attachment is manual file path entry only. + +**Enhancement:** +- Add inline receipt thumbnail on expense cards (small image preview if receipt_path exists) +- Add a lightbox component for full-size receipt viewing (focus trap, Escape to close, zoom controls) +- Add a file picker button for attaching receipts (uses Tauri's dialog plugin for native file picker - the keyboard-accessible alternative to drag-and-drop) +- Add drag-and-drop zone on the expense edit form as an additional attachment method +- Show a "No receipt" indicator (icon + text) on expenses without one +- Keyboard alternative for drag-and-drop: the file picker button serves as the primary accessible method (WCAG 2.5.7 Dragging Movements) + +**Files:** +- Create: `src/components/ReceiptLightbox.vue` +- Modify: `src/views/Entries.vue` (expense tab: add thumbnail, lightbox trigger, file picker, drop zone) + +**Accessibility:** +- Lightbox: `role="dialog"`, `aria-modal="true"`, `aria-label="Receipt image for [expense description]"`, focus trap, Escape closes +- Image: `alt="Receipt for [category] expense on [date], [amount]"` +- File picker button: `aria-label="Attach receipt file"` - this is the primary method, drag-and-drop is the enhancement (WCAG 2.5.7) +- Drop zone: `role="button"`, `aria-label="Drop receipt file here or press Enter to browse"`, keyboard-activatable (Enter/Space opens file picker) +- "No receipt" indicator uses both icon and text (not icon alone) +- Lightbox zoom controls: keyboard-accessible (+/- buttons with aria-labels) + +--- + +## Phase 3: Data and Insights + +### 11. Dashboard Weekly Comparison + +**Problem:** Dashboard shows current stats but gives no sense of trend - no way to know if this week is better or worse than last week. + +**Enhancement:** +- Add comparison indicators on each stat card: "+2.5h vs last week" or "-1.2h vs last week" with directional arrow icon +- Add a sparkline row below the weekly chart showing the last 4 weeks of daily totals (7 bars each, muted styling) +- Comparison data fetched via existing `get_reports` command with last week's date range + +**Files:** +- Modify: `src/views/Dashboard.vue` (add comparison computeds, sparkline rendering, fetch last week data) + +**Accessibility:** +- Comparison text is actual text, not just a colored arrow - e.g., "2.5 hours more than last week" or "1.2 hours less than last week" +- Color conveys reinforcement only (green for increase, red for decrease) - the text and arrow direction carry the meaning (WCAG 1.4.1) +- Arrow icon has `aria-hidden="true"` since the text already conveys direction +- Sparkline bars have a `role="img"` wrapper with `aria-label="Last 4 weeks: [week1] hours, [week2] hours, [week3] hours, [week4] hours"` +- Each individual sparkline bar does NOT need to be interactive - the summary label covers it +- Comparison values use `sr-only` prefix: "Compared to last week:" for screen readers + +--- + +### 12. Project Health Dashboard Cards + +**Problem:** Budget progress bars on project cards are basic - no at-a-glance health assessment combining pace, burn rate, and risk level. + +**Enhancement:** +- Add a health badge per project card combining budget burn rate and pace data from `get_project_budget_status` +- Three health states: "On Track" (green icon + text), "At Risk" (yellow icon + text), "Over Budget" (red icon + text) +- Health determined by: budget percentage > 100% = over budget, pace = "behind" and > 75% budget used = at risk, else on track +- Add an "Attention Needed" section at the top of Projects view listing at-risk and over-budget projects +- Each state uses BOTH a distinct icon (checkmark, warning triangle, x-circle) AND text label - never color alone + +**Files:** +- Modify: `src/views/Projects.vue` (add health badge computation, attention section) + +**Accessibility:** +- Health badge: icon + text label, never color alone (WCAG 1.4.1 Use of Color) +- Icons: CheckCircle for on-track, AlertTriangle for at-risk, XCircle for over-budget - each with `aria-hidden="true"` since text label is present +- Badge has `role="status"` so screen readers announce it +- Attention section: `role="region"` with `aria-label="Projects needing attention"`, `aria-live="polite"` so it updates when data changes +- Each attention item is a focusable link/button that navigates to the project +- Text labels: "On Track", "At Risk", "Over Budget" - clear, unambiguous, no jargon + +--- + +### 13. Reports Time-of-Day Heatmap + +**Problem:** Reports view has no visibility into when work happens - no pattern analysis of productive hours or workday distribution. + +**Enhancement:** +- Add a "Patterns" tab to Reports view +- Primary visualization: a 7x24 grid (days of week x hours of day) showing work intensity +- Each cell uses both color intensity AND a numeric label showing hours (e.g., "2.5h") - not color alone (WCAG 1.4.1) +- Cells with zero hours are empty (no false visual noise) +- Below the grid, add a "Data Table" toggle that switches to a fully accessible HTML table view of the same data (accessible alternative to the visual grid) +- Data computed from entry start_time timestamps within the selected date range + +**Files:** +- Modify: `src/views/Reports.vue` (add Patterns tab, heatmap grid, data table alternative) + +**Accessibility:** +- Grid uses `role="grid"` with `role="row"` and `role="gridcell"` for keyboard navigation +- Each cell has `aria-label="[Day] [Hour]: [X] hours"` (e.g., "Monday 9 AM: 2.5 hours") +- Arrow key navigation between cells (up/down/left/right) +- Color intensity reinforces the numeric values but is NOT the sole information channel - every cell shows its numeric value as text +- "Data Table" toggle button: `aria-pressed` toggle, switches between visual grid and `
| ` and ` | ` +- Table alternative is a standard HTML table with proper headers, fully navigable by screen readers +- Summary stats below the grid as text: "Most productive: Monday 9-10 AM (avg 1.8h)", "Quietest: Sunday (avg 0.2h)" +- `motion-safe:` on any cell hover/focus transitions + +--- + +### 14. Rounding Visibility and Preview + +**Problem:** Time rounding is configured in Settings but its effect is invisible - users can't see how much time is being rounded. + +**Enhancement:** +- On entry rows in Entries view: show a small "rounded" indicator next to duration when rounding changes the value, with tooltip showing "Actual: 1h 23m, Rounded: 1h 30m" +- On invoice line items: show both actual and rounded hours when rounding is active +- In Reports view Hours tab: add a "Rounding Impact" summary line showing total time added/subtracted by rounding across all entries in the date range +- Rounding calculations use the existing `roundDuration()` utility from `src/utils/rounding.ts` + +**Files:** +- Modify: `src/views/Entries.vue` (add rounded indicator on entry rows) +- Modify: `src/views/Invoices.vue` (show actual vs rounded on line items) +- Modify: `src/views/Reports.vue` (add rounding impact summary) + +**Accessibility:** +- Rounded indicator: not just a colored dot - shows text "Rounded" with a clock icon, `aria-label="Duration rounded from [actual] to [rounded]"` +- Tooltip: accessible via keyboard focus (not hover-only), uses `role="tooltip"` with `aria-describedby` +- Rounding impact summary: plain text, 7:1 contrast, e.g., "Rounding added 2h 15m across 47 entries" +- Actual vs. rounded on invoices: both values visible as text, difference highlighted with both color and "+Xm" / "-Xm" text label +- All diff indicators use 7:1 contrast ratios against their backgrounds + +--- + +### 15. Export Completeness and Scheduling + +**Problem:** `export_data` only exports clients, projects, entries, and invoices - missing tags, expenses, favorites, recurring entries, tracked apps, timeline events, calendar sources, and settings. No automated backup. + +**Enhancement:** +- Expand `export_data` to include ALL data: tags, entry_tags, expenses, favorites, recurring_entries, tracked_apps, timeline_events, calendar_sources, calendar_events, timesheet_locks, and settings +- Update `import_json_data` to handle the expanded format (backward compatible with old exports) +- Add a "Last exported" timestamp display in Settings > Data tab +- Add an "Auto-backup on close" toggle in Settings > Data tab that saves a backup JSON to a user-configured directory when the app closes +- Add a "Backup path" file picker (Tauri dialog) for choosing the auto-backup directory +- Backup files named with timestamp: `zeroclock-backup-YYYY-MM-DD.json` +- WCAG 2.2.1: the auto-backup happens on app close (user-initiated), not on a timer, so no timing concerns + +**Files:** +- Modify: `src-tauri/src/commands.rs` (expand `export_data`, update `import_json_data`, add `auto_backup` command) +- Modify: `src-tauri/src/lib.rs` (register commands) +- Modify: `src/views/Settings.vue` (add last-exported display, auto-backup toggle, path picker) +- Modify: `src/App.vue` (hook into Tauri window close event for auto-backup) + +**Accessibility:** +- File picker button: `aria-label="Choose backup directory"` +- Last exported display: `role="status"`, readable text "Last exported: February 20, 2026 at 3:45 PM" (not relative time like "2 hours ago") +- Auto-backup toggle: standard toggle with label, `aria-checked` +- Backup path shown as text with a "Change" button, not an editable text field +- All controls have visible labels and 7:1 contrast + +--- + +## Implementation Phases + +### Phase 1: Polish and Reliability (Features 1-5) +Mostly backend and store changes, minimal new UI. Low risk, high impact on data integrity. + +### Phase 2: Power User Productivity (Features 6-10) +Mix of new components and view modifications. Medium complexity, high impact on daily workflow. + +### Phase 3: Data and Insights (Features 11-15) +Primarily view-level changes with some backend expansion. Medium complexity, high impact on decision-making. + +## Verification Criteria + +Each feature must pass: +1. `npx vue-tsc --noEmit` - TypeScript type check +2. `npx vite build` - Production build +3. `cargo build` - Rust backend build (if backend changed) +4. Manual WCAG AAA spot check: + - All interactive elements keyboard-operable + - All dialogs trap focus and close with Escape + - All dynamic content announced via aria-live + - All form controls have visible labels + - No information conveyed by color alone + - 7:1 contrast on all text + - Drag operations have keyboard alternatives + - Timed interactions are adjustable |
|---|