Persistent notifications
+Disable auto-dismiss for toast messages
++ + No project was selected. Choose a project to save your tracked time. + + + The timer has been running for a long time. Would you like to stop and save? + +
+ ++ {{ formattedDuration }} +
+{{ tpl.name }}
++ {{ getProjectName(tpl.project_id) }} - {{ formatDuration(tpl.duration) }} +
+No saved templates
+``` + +Add script setup code for fetching and deleting templates via `useEntryTemplatesStore`. + +**Step 3:** Verify: `npx vue-tsc --noEmit` + +**Step 4:** Commit: `git add src/views/Settings.vue && git commit -m "feat: entry template management in settings"` + +--- + +### Task 19: Timesheet smart row persistence - backend + +**Files:** +- Modify: `src-tauri/src/database.rs` (add `timesheet_rows` table) +- Modify: `src-tauri/src/commands.rs` (add 3 commands) +- Modify: `src-tauri/src/lib.rs` (register) + +**Step 1:** Add table: + +```rust +conn.execute( + "CREATE TABLE IF NOT EXISTS timesheet_rows ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + week_start TEXT NOT NULL, + project_id INTEGER NOT NULL REFERENCES projects(id), + task_id INTEGER REFERENCES tasks(id), + sort_order INTEGER NOT NULL DEFAULT 0 + )", + [], +)?; +``` + +**Step 2:** Add commands: + +```rust +#[tauri::command] +pub fn get_timesheet_rows(state: State| ` and ` | ` +- Summary stats: "Most productive: Monday 9-10 AM", "Quietest: Sunday" + +Data computation: +```ts +function computePatterns() { + const grid: number[][] = Array.from({ length: 7 }, () => Array(24).fill(0)) + for (const entry of entriesStore.entries) { + const d = new Date(entry.start_time) + const day = d.getDay() === 0 ? 6 : d.getDay() - 1 // Mon=0, Sun=6 + const hour = d.getHours() + grid[day][hour] += entry.duration / 3600 + } + heatmapData.value = grid + patternsLoaded = true +} +``` + +**Step 4:** Verify: `npx vue-tsc --noEmit && npx vite build` + +**Step 5:** Commit: `git add src/views/Reports.vue && git commit -m "feat: time-of-day heatmap in reports patterns tab"` + +--- + +### Task 29: Rounding visibility - Entries.vue + +**Files:** +- Modify: `src/views/Entries.vue` + +**Step 1:** Read the duration column in the entries table (around line 161-169). + +**Step 2:** Import `roundDuration` from `../utils/rounding` and settings store. For each entry, compute if rounding changes the value: + +```ts +import { roundDuration } from '../utils/rounding' + +function getRoundedDuration(seconds: number): number | null { + if (settingsStore.settings.rounding_enabled !== 'true') return null + const increment = parseInt(settingsStore.settings.rounding_increment) || 0 + const method = (settingsStore.settings.rounding_method || 'nearest') as 'nearest' | 'up' | 'down' + if (increment <= 0) return null + const rounded = roundDuration(seconds, increment, method) + return rounded !== seconds ? rounded : null +} +``` + +In the duration ` | `, after the duration display, add:
+```html
+
+ Rounded
+
+```
+
+Add a tooltip mechanism using `title` attribute or a custom tooltip with `role="tooltip"` and `aria-describedby` for the actual vs rounded values.
+
+**Step 3:** Verify: `npx vue-tsc --noEmit`
+
+**Step 4:** Commit: `git add src/views/Entries.vue && git commit -m "feat: rounding visibility indicators on entry rows"`
+
+---
+
+### Task 30: Rounding visibility - Invoices and Reports
+
+**Files:**
+- Modify: `src/views/Invoices.vue` (show actual vs rounded on line items)
+- Modify: `src/views/Reports.vue` (add rounding impact summary in Hours tab)
+
+**Step 1:** In Invoices.vue, where invoice line items are displayed, show both actual and rounded hours when rounding is active. Add a small "+Xm" or "-Xm" text label.
+
+**Step 2:** In Reports.vue Hours tab, after the billable split line (around line 124), add a rounding impact summary:
+
+```html
+
+ Rounding {{ roundingImpact > 0 ? 'added' : 'subtracted' }}:
+ {{ formatHours(Math.abs(roundingImpact)) }}
+ across {{ roundedEntryCount }} entries
+
+
+```
+
+**Step 3:** Verify: `npx vue-tsc --noEmit && npx vite build`
+
+**Step 4:** Commit: `git add src/views/Invoices.vue src/views/Reports.vue && git commit -m "feat: rounding visibility in invoices and reports"`
+
+---
+
+### Task 31: Export completeness - backend
+
+**Files:**
+- Modify: `src-tauri/src/commands.rs` (expand `export_data`, update `import_json_data`, add `auto_backup`)
+- Modify: `src-tauri/src/lib.rs` (register)
+
+**Step 1:** Read `export_data` (lines 586-669) and `import_json_data` (lines 1501-1609).
+
+**Step 2:** Expand `export_data` to include ALL tables:
+- Add: tasks, tags, entry_tags, tracked_apps, favorites, recurring_entries, expenses, timeline_events, calendar_sources, calendar_events, timesheet_locks, invoice_items, settings, entry_templates, timesheet_rows
+
+**Step 3:** Update `import_json_data` to handle expanded format. Import new tables if present in the JSON, skip if not (backward compatible).
+
+**Step 4:** Add `auto_backup` command:
+
+```rust
+#[tauri::command]
+pub fn auto_backup(state: State
+
+
+
+
+
+
+
+
+Last exported +{{ lastExportedFormatted }} +
+
+
+
+
+
+
+Auto-backup on close +Save a backup when the app closes +
+
+```
+
+Script additions:
+```ts
+const autoBackupEnabled = ref(false)
+const backupPath = ref('')
+const lastExported = ref('')
+
+// On mount:
+autoBackupEnabled.value = settingsStore.settings.auto_backup === 'true'
+backupPath.value = settingsStore.settings.backup_path || ''
+lastExported.value = settingsStore.settings.last_exported || ''
+
+async function toggleAutoBackup() {
+ autoBackupEnabled.value = !autoBackupEnabled.value
+ await settingsStore.updateSetting('auto_backup', autoBackupEnabled.value ? 'true' : 'false')
+}
+
+async function chooseBackupPath() {
+ const { open } = await import('@tauri-apps/plugin-dialog')
+ const selected = await open({ directory: true, title: 'Choose backup directory' })
+ if (selected && typeof selected === 'string') {
+ backupPath.value = selected
+ await settingsStore.updateSetting('backup_path', selected)
+ }
+}
+```
+
+Update `exportData` to also set `last_exported`:
+```ts
+async function exportData() {
+ // ... existing export logic ...
+ const now = new Date().toISOString()
+ await settingsStore.updateSetting('last_exported', now)
+ lastExported.value = now
+}
+```
+
+**Step 2:** In App.vue, hook into window close event for auto-backup. Add in `onMounted`:
+
+```ts
+const { getCurrentWindow } = await import('@tauri-apps/api/window')
+const win = getCurrentWindow()
+win.onCloseRequested(async () => {
+ if (settingsStore.settings.auto_backup === 'true' && settingsStore.settings.backup_path) {
+ try {
+ await invoke('auto_backup', { backupDir: settingsStore.settings.backup_path })
+ } catch (e) {
+ console.error('Auto-backup failed:', e)
+ }
+ }
+})
+```
+
+**Step 3:** Verify: `npx vue-tsc --noEmit && npx vite build`
+
+**Step 4:** Commit: `git add src/views/Settings.vue src/App.vue && git commit -m "feat: auto-backup UI and window close hook"`
+
+---
+
+### Task 33: Phase 3 verification
+
+**Step 1:** Run: `npx vue-tsc --noEmit`
+**Step 2:** Run: `npx vite build`
+**Step 3:** Run: `cd src-tauri && cargo build`
+**Step 4:** Commit if any fixes needed.
+
+---
+
+### Task 34: Final verification
+
+**Step 1:** Run all three verification commands:
+```
+npx vue-tsc --noEmit && npx vite build && cd src-tauri && cargo build
+```
+
+**Step 2:** Ensure no regressions. Fix any type errors or build failures.
+
+**Step 3:** Final commit if any cleanup needed.
+
+
+Backup directory +{{ backupPath || 'Not set' }} + |
|---|