Compare commits

127 Commits

Author SHA1 Message Date
Your Name
ae02cba958 fix: auto-detect date format (DD/MM vs MM/DD) in CSV imports
Scans all date values in imported CSVs to determine whether the file
uses DD/MM/YYYY or MM/DD/YYYY format. When the format is ambiguous
(all day and month values are <= 12), shows an inline dropdown for the
user to choose. Bump version to 1.0.2.
2026-02-21 16:56:27 +02:00
Your Name
ad0bdc05be fix: close button and CSV import parsing for Clockify/Harvest
Close button did nothing when "close to tray" was disabled - the
onCloseRequested handler lacked an explicit destroy call for the
non-tray path.

Clockify CSV import threw RangeError because locale-dependent date
formats (MM/DD/YYYY, DD.MM.YYYY, 12h time) were passed straight
to the Date constructor. Added flexible date/time parsers that
handle all Clockify export variants without relying on Date parsing.

Added dedicated Clockify mapper that prefers Duration (decimal)
column and a new Harvest CSV importer (date + decimal hours, no
start/end times).

Bump version to 1.0.1.
2026-02-21 14:56:53 +02:00
Your Name
7b118c1a1c feat: complete export/import cycle and remove sample data
Export now includes invoice_payments and recurring_invoices tables.
Import restored to use ID-based lookups and all fields for clients,
projects, tasks, and time entries. Added missing import support for
timeline_events, calendar_sources, calendar_events, invoice_payments,
and recurring_invoices. Export uses native save dialog instead of blob
download. Removed sample data seeding (seed.rs, UI, command).
2026-02-21 01:34:26 +02:00
Your Name
03eff89947 chore: add CC0 license file and update readme badge 2026-02-21 01:20:20 +02:00
Your Name
ee82abe63e feat: tooltips, two-column timer, font selector, tray behavior, icons, readme
- Custom tooltip directive (WCAG AAA) on every button in the app
- Two-column timer layout with sticky hero and recent entries sidebar
- Timer font selector with 16 monospace Google Fonts and live preview
- UI font selector with 15+ Google Fonts
- Close-to-tray and minimize-to-tray settings
- New app icons (no-glow variants), platform icon set
- Mini timer pop-out window
- Favorites strip with drag-reorder and inline actions
- Comprehensive README with feature documentation
- Remove tracked files that belong in gitignore
2026-02-21 01:15:57 +02:00
Your Name
ef6255042d fix: boost text-tertiary contrast for WCAG AAA (7:1) 2026-02-20 18:40:19 +02:00
Your Name
b7b1789380 feat: auto-backup UI and window close hook 2026-02-20 15:41:38 +02:00
Your Name
0ae431b8ac feat: comprehensive export with all tables and auto-backup command 2026-02-20 15:40:02 +02:00
Your Name
e97bc0f640 feat: rounding visibility in invoices and reports 2026-02-20 15:37:20 +02:00
Your Name
d22d6e844f feat: rounding visibility indicators on entry rows 2026-02-20 15:36:07 +02:00
Your Name
f5223d7a0b feat: time-of-day heatmap in reports patterns tab 2026-02-20 15:32:20 +02:00
Your Name
9d0ab92afc feat: project health badges and attention section 2026-02-20 15:32:14 +02:00
Your Name
ea2d0cba7f feat: weekly comparison indicators and sparklines on dashboard 2026-02-20 15:32:07 +02:00
Your Name
a4fce3b7ab feat: receipt thumbnails, lightbox, and file picker for expenses 2026-02-20 15:25:18 +02:00
Your Name
7c8effb0a7 feat: receipt lightbox component with zoom and focus trap 2026-02-20 15:23:11 +02:00
Your Name
f337f8c28f feat: global shortcut for quick entry dialog 2026-02-20 15:20:27 +02:00
Your Name
96ef48000c feat: global quick entry dialog component 2026-02-20 15:18:34 +02:00
Your Name
c6f6b61503 feat: timesheet row persistence and copy last week 2026-02-20 15:17:01 +02:00
Your Name
ebadbbc2a6 feat: timesheet row persistence backend 2026-02-20 15:15:50 +02:00
Your Name
f504241fc9 feat: entry template management in settings 2026-02-20 15:10:48 +02:00
Your Name
1e324ef0ca feat: entry template picker and save-as-template in entries view 2026-02-20 15:09:37 +02:00
Your Name
2292e4ff9e feat: entry templates pinia store 2026-02-20 15:07:18 +02:00
Your Name
2b47f3412a feat: entry templates CRUD backend 2026-02-20 15:06:50 +02:00
Your Name
fb41f67145 feat: cascade delete dialog for clients with dependency counts 2026-02-20 15:02:39 +02:00
Your Name
0ddf8aa14e feat: client cascade delete with dependency counts 2026-02-20 15:01:33 +02:00
Your Name
eb58794555 feat: smart timer safety net - save dialog on stop without project 2026-02-20 14:58:02 +02:00
Your Name
f0d8e066cb feat: timer save dialog for no-project and long-timer scenarios 2026-02-20 14:56:17 +02:00
Your Name
349b9eb95d feat: use batch save for invoice items 2026-02-20 14:55:17 +02:00
Your Name
37751eb0c8 feat: batch invoice items save with transaction 2026-02-20 14:54:37 +02:00
Your Name
4faac61901 fix: independent try/catch per onboarding detection call 2026-02-20 14:47:26 +02:00
Your Name
83a3ca1e93 feat: standardize error handling across all stores 2026-02-20 14:46:56 +02:00
Your Name
6c961b6c9f feat: use unified error handler in entries store 2026-02-20 14:43:10 +02:00
Your Name
8bcd81b4f0 feat: unified error handler with retry for transient errors 2026-02-20 14:42:30 +02:00
Your Name
6179c8fee8 feat: persistent notifications toggle in settings 2026-02-20 14:40:50 +02:00
Your Name
6df5485813 feat: toast undo button and hover/focus pause 2026-02-20 14:38:34 +02:00
Your Name
d83a832cb1 feat: toast auto-dismiss with undo and pause support 2026-02-20 14:38:08 +02:00
Your Name
6757b7d800 docs: enhancement round 2 implementation plan - 34 tasks
Phase 1: Toast auto-dismiss/undo, unified error handling, onboarding
resilience, invoice batch save, smart timer safety net.
Phase 2: Client cascade delete, entry templates, timesheet persistence,
global quick entry, receipt management.
Phase 3: Dashboard comparison, project health, heatmap, rounding
visibility, complete export with auto-backup.
2026-02-20 14:29:25 +02:00
Your Name
03c1157683 docs: enhancement round 2 design - 15 feature proposals
Covers smart timer safety net, toast undo system, unified error
handling, onboarding resilience, invoice save reliability, global
quick entry, entry templates, timesheet persistence, client cascade,
receipt management, weekly comparison, project health cards, time
heatmap, rounding preview, and export scheduling. All features
designed for WCAG 2.2 AAA compliance.
2026-02-20 14:22:01 +02:00
Your Name
2a1ed8d875 feat: add tour store for guided walkthrough state 2026-02-20 09:36:26 +02:00
Your Name
3dcbd4a888 chore: tidy up project structure and normalize formatting 2026-02-19 22:43:14 +02:00
Your Name
47eb1af7ab fix: mini timer renders via window label instead of hash routing
The mini timer window was blank because hash-based routing
(createWebHashHistory) doesn't work with Tauri's WebviewUrl path.
Now App.vue detects the mini timer by checking getCurrentWindow().label
=== 'mini-timer' and renders the MiniTimer component directly,
bypassing the router entirely.
2026-02-18 15:26:44 +02:00
Your Name
28eb7a2639 fix: mini timer window blank due to hash routing mismatch
The app uses createWebHashHistory but the mini timer window was
opened with WebviewUrl::App("/mini-timer") which sets the URL path,
not the hash fragment. Vue Router never matched the route, so the
Dashboard rendered in a 300x64 window (appearing blank). Now loads
root URL and sets window.location.hash via eval. Also shows/focuses
the main window when closing the mini timer.
2026-02-18 15:23:20 +02:00
Your Name
8c8de6a2a7 feat: load invoice templates from JSON files via backend
Templates are now loaded dynamically from data/templates/*.json
via the get_invoice_templates Tauri command instead of being
hardcoded in TypeScript. Preview and PDF renderer switch on
template.layout instead of template.id, allowing custom templates
to reuse built-in layouts with different colors.
2026-02-18 15:17:54 +02:00
Your Name
8b8d451806 feat: load invoice templates from JSON files in data/templates directory 2026-02-18 15:12:30 +02:00
Your Name
f0571fb1bb fix: delete invoice_items before invoice to prevent FK constraint failure 2026-02-18 15:07:43 +02:00
Your Name
92749bdb63 fix: make template picker full-screen with fixed positioning so buttons are visible 2026-02-18 15:05:02 +02:00
Your Name
e3450ff92e feat: rewrite InvoicePreview with 15 unique typographic layouts 2026-02-18 14:50:49 +02:00
Your Name
efc9fce811 feat: rewrite PDF renderer with 15 unique typographic layouts 2026-02-18 14:45:38 +02:00
Your Name
c499acd17d feat: add two-step invoice flow with full-screen template picker 2026-02-18 14:43:55 +02:00
Your Name
056333c31c feat: update invoicePdf wrapper with new default template ID 2026-02-18 14:41:23 +02:00
Your Name
59e713f1ec feat: rewrite invoice template configs with design-doc IDs and colors 2026-02-18 14:39:01 +02:00
Your Name
ed8b0c0776 feat: add template_id to Invoice interface and updateInvoiceTemplate action 2026-02-18 14:38:14 +02:00
Your Name
6a252facf6 feat: add template_id column to invoices table and update_invoice_template command 2026-02-18 14:37:26 +02:00
Your Name
4a45713c77 docs: add invoice templates v2 implementation plan 2026-02-18 14:32:38 +02:00
Your Name
de5f65aed0 docs: add invoice templates v2 complete redesign design doc 2026-02-18 14:28:41 +02:00
Your Name
0ecb4d80c1 feat: integrate template picker into invoice create and preview views 2026-02-18 13:35:11 +02:00
Your Name
d739033463 feat: add business identity settings for invoice branding 2026-02-18 13:34:44 +02:00
Your Name
cf4d64eced feat: add InvoicePreview.vue with all 7 header styles and 5 table styles 2026-02-18 13:30:27 +02:00
Your Name
98152984c1 feat: add InvoiceTemplatePicker split-pane component 2026-02-18 13:28:40 +02:00
Your Name
b05bd415fb feat: add config-driven jsPDF invoice renderer with all header and table styles 2026-02-18 13:26:11 +02:00
Your Name
185b20cab2 feat: add 15 invoice template configs and registry 2026-02-18 13:16:36 +02:00
Your Name
a05e7555e8 docs: add invoice templates implementation plan
9-task plan covering template config types, jsPDF renderer,
HTML preview component, template picker UI, Invoices.vue
integration, business identity settings, and polish passes.
2026-02-18 13:12:37 +02:00
Your Name
2e4143edc0 docs: add invoice templates design document
15 visually distinct templates across 4 tiers (Professional
Essentials, Creative & Modern, Warm & Distinctive, Premium &
Specialized) with template config schema, picker UI design,
shared renderer architecture, and business identity support.
2026-02-18 13:07:39 +02:00
Your Name
291429e1b8 refactor: migrate remaining dialogs to Vue Transition, remove old keyframes
Convert Settings, Invoices, IdlePrompt, AppTrackingPrompt, and
AppDiscard dialogs from animate-modal-enter CSS class to proper
<Transition name="modal"> wrappers for enter/leave animations.
Remove unused animate-modal-enter and animate-dropdown-enter keyframes.
2026-02-18 11:36:35 +02:00
Your Name
a3a6ab2fdf feat: add transitions and micro-interactions across all views
- Page transitions with slide-up/fade on route changes (App.vue)
- NavRail sliding active indicator with spring-like easing
- List enter/leave/move animations on Entries, Projects, Clients, Timer
- Modal enter/leave transitions with scale+fade on all dialogs
- Dropdown transitions with overshoot on all select/picker components
- Button feedback (scale on hover/active), card hover lift effects
- Timer pulse on start, glow on stop, floating empty state icons
- Content fade-in on Dashboard, Reports, Calendar, Timesheet
- Tag chip enter/leave animations in AppTagInput
- Progress bar smooth width transitions
- Implementation plan document
2026-02-18 11:33:58 +02:00
Your Name
c66e71f57d feat: add animation CSS classes, keyframes, and reduced-motion support 2026-02-18 11:22:32 +02:00
Your Name
31bb66dbfd feat: install @vueuse/motion and create spring presets 2026-02-18 11:19:52 +02:00
Your Name
78a0537632 docs: add motion system design for animations and micro-interactions 2026-02-18 11:07:57 +02:00
Your Name
55505b2b6b feat: add daily/weekly goals, streaks, and time rounding
Settings Timer tab now has daily/weekly goal hour inputs. Dashboard
shows goal progress bars and streak counter. Settings Billing tab
has rounding toggle with increment and method selectors. New
rounding.ts utility for nearest/up/down time rounding.
2026-02-18 10:51:56 +02:00
Your Name
8c56867764 feat: add budget progress indicators to Projects and Dashboard
Project edit dialog includes budget hours and amount fields. Project
cards show progress bars with color-coded status. Dashboard displays
budget alerts section for projects exceeding 75% of budget.
2026-02-18 10:51:47 +02:00
Your Name
5e608a98e6 feat: integrate tags in Timer and Entries views
Timer shows tag selector below description, saves tags on stop.
Entries table displays tag chips per row with color coding.
Tags loaded from store on mount.
2026-02-18 10:51:39 +02:00
Your Name
8eb2d135c8 feat: add data import from CSV and JSON
Import utility with CSV parser, Toggl/Clockify format mapping, and
generic CSV column mapping. Settings Data tab has import UI with
file picker, format selector, preview table, and import execution.
2026-02-18 10:46:33 +02:00
Your Name
bd3e0ba5a6 feat: enhance floating mini timer with controls and pop-out button
MiniTimer shows project color dot, name, elapsed time, stop button,
and expand-to-main button. Timer.vue has pop-out button when running.
2026-02-18 10:46:25 +02:00
Your Name
5ac890aad4 feat: add global keyboard shortcuts for timer toggle and show app
Register CmdOrCtrl+Shift+T (toggle timer) and CmdOrCtrl+Shift+Z
(show app) via tauri-plugin-global-shortcut. Shortcut keys are
configurable in Settings Timer tab. Shortcuts re-register on change.
2026-02-18 10:46:18 +02:00
Your Name
8d0f6c6c7d feat: add profitability tab and favorites strip
Reports view now has Hours/Profitability tabs with per-project revenue
table. Timer view shows favorites strip for quick project selection
and a Save as Favorite button next to the description input.
2026-02-18 10:46:10 +02:00
Your Name
46ce6d119d feat: add Calendar, Timesheet, and MiniTimer views
Calendar shows weekly time-block layout with hour rows, entry positioning,
current time indicator, and week navigation. Timesheet provides a weekly
grid with project/task rows, day columns, totals, and add-row functionality.
MiniTimer is a minimal always-on-top timer display for the floating window.
2026-02-18 10:39:08 +02:00
Your Name
838cb55c8e feat: add AppTagInput multi-select tag component 2026-02-18 10:35:18 +02:00
Your Name
72a86cf2c9 feat: add markdown rendering for entry descriptions 2026-02-18 10:35:12 +02:00
Your Name
d585f449db feat: add duplicate, copy previous day/week, and repeat entry 2026-02-18 10:35:06 +02:00
Your Name
0fe491c15f feat: add theme customization with accent colors and light mode 2026-02-18 10:34:59 +02:00
Your Name
99bca0709b feat: add global-shortcut plugin and mini timer window commands 2026-02-18 02:06:07 +02:00
Your Name
c6cb26553a feat: add goals, profitability, timesheet, and import commands 2026-02-18 02:04:10 +02:00
Your Name
6892bf8b98 feat: add favorites table, CRUD commands, and Pinia store 2026-02-18 02:02:57 +02:00
Your Name
68ce724980 feat: add project budgets and rounding override columns 2026-02-18 02:02:13 +02:00
Your Name
ee30647b44 feat: add tags table, CRUD commands, and Pinia store 2026-02-18 02:01:04 +02:00
Your Name
6049536284 fix: dynamic currency symbols and integrated datetime picker
- Replace all hardcoded prefix="$" with :prefix="getCurrencySymbol()"
  in Settings, Projects, and Invoices views
- Replace hardcoded ($) labels with dynamic currency symbol
- Extend AppDatePicker with showTime prop + hour/minute v-models
  for integrated date+time selection
- Simplify Entries.vue to use single AppDatePicker with showTime
  instead of separate hour/minute inputs
2026-02-17 23:53:45 +02:00
Your Name
9dbd6992e0 fix: add viewport margin to all modal dialogs 2026-02-17 23:41:59 +02:00
Your Name
5af8661b83 feat: replace native datetime-local with custom date picker + time inputs 2026-02-17 23:41:24 +02:00
Your Name
9a894bbc40 feat: replace all hardcoded en-US and $ formatting with locale-aware helpers 2026-02-17 23:39:31 +02:00
Your Name
8dc915c8aa feat: replace native number inputs with AppNumberInput across all views 2026-02-17 23:36:02 +02:00
Your Name
d3709c170b feat: add locale and currency settings with searchable dropdowns 2026-02-17 23:35:27 +02:00
Your Name
d0961c93fd fix: apply default hourly rate from settings when creating new projects 2026-02-17 23:35:24 +02:00
Your Name
952e41ef01 feat: add AppNumberInput component with press-and-hold repeat 2026-02-17 23:33:13 +02:00
Your Name
ef5eecd711 feat: add searchable prop to AppSelect for filtering long option lists 2026-02-17 23:33:11 +02:00
Your Name
dbea5658c2 feat: add comprehensive locale utility with 140+ locales and 120+ currencies 2026-02-17 23:31:04 +02:00
Your Name
1c05b690ad docs: add UI improvements batch implementation plan 2026-02-17 23:22:40 +02:00
Your Name
4a40c22515 docs: add UI improvements batch design (locale, datetime picker, number input, etc.) 2026-02-17 23:17:00 +02:00
Your Name
d33159594d feat: add Clients view with card grid, dialogs, and billing details 2026-02-17 22:57:08 +02:00
Your Name
8ee45cdefc feat: add Client billing fields to store, /clients route, and reorder NavRail 2026-02-17 22:54:31 +02:00
Your Name
89d121bbea feat: add client billing fields to database and Rust backend 2026-02-17 22:52:51 +02:00
Your Name
c0ad93a758 docs: add Clients view and NavRail reorg implementation plan 2026-02-17 22:48:22 +02:00
Your Name
a478aba6ec docs: add Clients view and NavRail reorg design 2026-02-17 22:44:50 +02:00
Your Name
5fd1d8cb77 fix: make custom dropdowns and date pickers respect UI zoom setting
Teleported panels read zoom from #app and apply it to their own style,
with position coordinates divided by the zoom factor so they align
correctly with the zoomed trigger elements.
2026-02-17 22:35:42 +02:00
Your Name
b646dcd801 feat: replace all native selects and date inputs with custom components 2026-02-17 22:27:51 +02:00
Your Name
5fea155332 feat: add AppDatePicker custom calendar component 2026-02-17 22:24:47 +02:00
Your Name
19f0813d2a feat: add AppSelect custom dropdown component 2026-02-17 22:22:43 +02:00
Your Name
0b04e5016e docs: add custom dropdowns and date pickers implementation plan 2026-02-17 22:17:42 +02:00
Your Name
9602630f18 docs: add custom dropdowns and date pickers design 2026-02-17 22:15:10 +02:00
Your Name
64f04db2f2 feat: upgrade typography — Plus Jakarta Sans headings, JetBrains Mono data, 14px base
Heading font: Plus Jakarta Sans (500/600/700) for all h1-h3, stat values, dialog titles, timer display, and wordmark.
Body font: Inter (400/500/600/700) unchanged but base bumped from 13px to 14px.
Mono font: JetBrains Mono replaces IBM Plex Mono for code and tabular data.
2026-02-17 22:06:48 +02:00
Your Name
f9542b6b7e style: bump border-radius globally — rounded to rounded-lg, rounded-lg to rounded-xl 2026-02-17 21:56:48 +02:00
Your Name
f3d9a938ac feat: redesign Settings with left sidebar tabs per Apple HIG
Four tabs (General, Timer, Billing, Data) with icon + label sidebar,
amber active indicator, auto-save on change, progressive disclosure
for timer settings, and danger zone isolated within Data tab.
2026-02-17 21:49:48 +02:00
Your Name
228a8cd6b4 docs: add Settings sidebar tabs design 2026-02-17 21:48:28 +02:00
Your Name
83a812d5b0 refactor: reorganize Settings per Apple HIG — auto-save, progressive disclosure, danger zone 2026-02-17 21:43:04 +02:00
Your Name
9eda8aaa99 fix: window dragging — use startDragging() API instead of data attribute 2026-02-17 21:36:30 +02:00
Your Name
94a035d0bf feat: persist window position and size between runs 2026-02-17 21:33:32 +02:00
Your Name
5daa426182 feat: portable storage — data directory next to exe 2026-02-17 21:33:26 +02:00
Your Name
c218dc1db5 feat: zoom initialization and toast container in App.vue 2026-02-17 21:32:15 +02:00
Your Name
740b9f0e4b feat: redesign Settings — amber save, UI zoom, toasts 2026-02-17 21:31:54 +02:00
Your Name
ed6e10efd3 feat: redesign Invoices — amber tabs and totals, rich empty state 2026-02-17 21:31:05 +02:00
Your Name
90bb035b72 feat: redesign Reports — amber actions and stats, toast notifications 2026-02-17 21:29:53 +02:00
Your Name
a9a7b0aceb feat: redesign Entries — filter container, amber actions, rich empty state 2026-02-17 21:28:42 +02:00
Your Name
e92c445782 feat: redesign Projects — amber button, color presets, rich empty state 2026-02-17 21:27:49 +02:00
Your Name
b3f852a460 feat: redesign Timer — amber Start, colon pulse, toast 2026-02-17 21:26:33 +02:00
Your Name
db9bacf310 feat: redesign Dashboard — greeting, amber stats, rich empty state 2026-02-17 21:25:52 +02:00
Your Name
27152985f7 feat: amber wordmark and NavRail active indicator 2026-02-17 21:25:08 +02:00
Your Name
fba9fab4d6 feat: add toast notification system 2026-02-17 21:25:02 +02:00
Your Name
bb9f329fec feat: overhaul design tokens — charcoal palette + amber accent 2026-02-17 21:24:15 +02:00
6 changed files with 90 additions and 20 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "zeroclock",
"version": "1.0.1",
"version": "1.0.2",
"description": "Time tracking desktop application",
"type": "module",
"scripts": {

2
src-tauri/Cargo.lock generated
View File

@@ -5743,7 +5743,7 @@ dependencies = [
[[package]]
name = "zeroclock"
version = "1.0.0"
version = "1.0.1"
dependencies = [
"chrono",
"env_logger",

View File

@@ -1,6 +1,6 @@
[package]
name = "zeroclock"
version = "1.0.1"
version = "1.0.2"
description = "A local time tracking app with invoicing"
authors = ["you"]
edition = "2021"

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "ZeroClock",
"version": "1.0.1",
"version": "1.0.2",
"identifier": "com.localtimetracker.app",
"build": {
"beforeDevCommand": "npm run dev",

View File

@@ -9,6 +9,23 @@ export interface ImportEntry {
tags?: string[]
}
export type DateFormat = 'DMY' | 'MDY'
export type DateDetectResult = DateFormat | 'ambiguous'
export function detectDateFormat(rows: string[][], dateColIndex: number): DateDetectResult {
if (dateColIndex < 0) return 'ambiguous'
for (let i = 1; i < rows.length; i++) {
const val = (rows[i]?.[dateColIndex] || '').trim()
const match = val.match(/^(\d{1,2})[\/.](\d{1,2})[\/.](\d{2,4})$/)
if (!match) continue
const a = parseInt(match[1])
const b = parseInt(match[2])
if (a > 12) return 'DMY'
if (b > 12) return 'MDY'
}
return 'ambiguous'
}
export function parseCSV(text: string): string[][] {
const lines = text.split('\n').filter(l => l.trim())
return lines.map(line => {
@@ -44,7 +61,7 @@ export function parseDurationString(dur: string): number {
}
// Parse locale-variable date strings into YYYY-MM-DD
function parseFlexDate(dateStr: string): string {
function parseFlexDate(dateStr: string, formatHint?: DateFormat): string {
if (!dateStr) return new Date().toISOString().split('T')[0]
const s = dateStr.trim()
@@ -63,7 +80,9 @@ function parseFlexDate(dateStr: string): string {
if (parseInt(a) > 12) return `${year}-${b.padStart(2, '0')}-${a.padStart(2, '0')}`
// If second part > 12, must be month-first (MM/DD/YYYY)
if (parseInt(b) > 12) return `${year}-${a.padStart(2, '0')}-${b.padStart(2, '0')}`
// Ambiguous - assume MM/DD/YYYY (US default, most common Clockify setting)
// Use format hint if provided
if (formatHint === 'DMY') return `${year}-${b.padStart(2, '0')}-${a.padStart(2, '0')}`
// Default to MDY
return `${year}-${a.padStart(2, '0')}-${b.padStart(2, '0')}`
}
@@ -99,13 +118,13 @@ function parseFlexTime(timeStr: string): string {
return '00:00:00'
}
function safeDateTime(date: string, time?: string): string {
const d = parseFlexDate(date)
function safeDateTime(date: string, time?: string, dateFormat?: DateFormat): string {
const d = parseFlexDate(date, dateFormat)
const t = time ? parseFlexTime(time) : '00:00:00'
return `${d}T${t}`
}
function findCol(header: string[], ...terms: string[]): number {
export function findCol(header: string[], ...terms: string[]): number {
for (const term of terms) {
const idx = header.findIndex(h => h === term)
if (idx >= 0) return idx
@@ -121,7 +140,7 @@ function col(row: string[], idx: number): string {
return idx >= 0 ? (row[idx] || '') : ''
}
export function mapTogglCSV(rows: string[][]): ImportEntry[] {
export function mapTogglCSV(rows: string[][], dateFormat?: DateFormat): ImportEntry[] {
const header = rows[0].map(h => h.toLowerCase().trim())
const descIdx = findCol(header, 'description')
const projIdx = findCol(header, 'project')
@@ -137,13 +156,13 @@ export function mapTogglCSV(rows: string[][]): ImportEntry[] {
client_name: col(row, clientIdx) || undefined,
task_name: col(row, taskIdx) || undefined,
description: col(row, descIdx) || undefined,
start_time: safeDateTime(col(row, startDateIdx), col(row, startTimeIdx) || undefined),
start_time: safeDateTime(col(row, startDateIdx), col(row, startTimeIdx) || undefined, dateFormat),
duration: parseDurationString(col(row, durationIdx)),
tags: tagIdx >= 0 && row[tagIdx] ? row[tagIdx].split(',').map(t => t.trim()).filter(Boolean) : undefined,
}))
}
export function mapClockifyCSV(rows: string[][]): ImportEntry[] {
export function mapClockifyCSV(rows: string[][], dateFormat?: DateFormat): ImportEntry[] {
const header = rows[0].map(h => h.toLowerCase().trim())
const descIdx = findCol(header, 'description')
const projIdx = findCol(header, 'project')
@@ -172,14 +191,14 @@ export function mapClockifyCSV(rows: string[][]): ImportEntry[] {
client_name: col(row, clientIdx) || undefined,
task_name: col(row, taskIdx) || undefined,
description: col(row, descIdx) || undefined,
start_time: safeDateTime(col(row, startDateIdx), col(row, startTimeIdx) || undefined),
start_time: safeDateTime(col(row, startDateIdx), col(row, startTimeIdx) || undefined, dateFormat),
duration,
tags: tagIdx >= 0 && row[tagIdx] ? row[tagIdx].split(',').map(t => t.trim()).filter(Boolean) : undefined,
}
})
}
export function mapHarvestCSV(rows: string[][]): ImportEntry[] {
export function mapHarvestCSV(rows: string[][], dateFormat?: DateFormat): ImportEntry[] {
const header = rows[0].map(h => h.toLowerCase().trim())
const dateIdx = findCol(header, 'date')
const projIdx = header.findIndex(h => h === 'project')
@@ -193,7 +212,7 @@ export function mapHarvestCSV(rows: string[][]): ImportEntry[] {
client_name: col(row, clientIdx) || undefined,
task_name: col(row, taskIdx) || undefined,
description: col(row, notesIdx) || undefined,
start_time: safeDateTime(col(row, dateIdx)),
start_time: safeDateTime(col(row, dateIdx), undefined, dateFormat),
duration: hoursIdx >= 0 && row[hoursIdx] ? Math.round(parseFloat(row[hoursIdx]) * 3600) : 0,
}))
}

View File

@@ -1445,6 +1445,32 @@
</table>
</div>
<!-- Date format detection -->
<div v-if="importFormat !== 'json' && importFormat !== 'generic'" class="flex items-center gap-3 mb-3">
<template v-if="importDateDetected !== 'ambiguous'">
<span class="text-[0.6875rem] text-text-tertiary">
Date format detected: {{ importDateFormat === 'DMY' ? 'DD/MM/YYYY' : 'MM/DD/YYYY' }}
</span>
<button
@click="importDateDetected = 'ambiguous'"
class="text-[0.6875rem] text-accent hover:text-accent-hover transition-colors"
>
Change
</button>
</template>
<template v-else>
<label class="text-[0.6875rem] text-text-tertiary">Date format:</label>
<div class="w-36">
<AppSelect
v-model="importDateFormat"
:options="dateFormatOptions"
label-key="label"
value-key="value"
/>
</div>
</template>
</div>
<button
@click="executeImport"
:disabled="isImporting"
@@ -1541,7 +1567,7 @@ import AppSelect from '../components/AppSelect.vue'
import AppShortcutRecorder from '../components/AppShortcutRecorder.vue'
import AppTimePicker from '../components/AppTimePicker.vue'
import { LOCALES, getCurrencies, getCurrencySymbol } from '../utils/locale'
import { parseCSV, mapTogglCSV, mapClockifyCSV, mapHarvestCSV, mapGenericCSV, type ImportEntry } from '../utils/import'
import { parseCSV, mapTogglCSV, mapClockifyCSV, mapHarvestCSV, mapGenericCSV, detectDateFormat, findCol, type ImportEntry, type DateFormat, type DateDetectResult } from '../utils/import'
import { TIMER_FONTS, loadGoogleFont, loadAndApplyTimerFont } from '../utils/fonts'
import { UI_FONTS, loadUIFont } from '../utils/uiFonts'
import { useFocusTrap } from '../utils/focusTrap'
@@ -1978,6 +2004,13 @@ const importFileName = ref('')
const importPreview = ref<string[][]>([])
const importStatus = ref('')
const isImporting = ref(false)
const importDateDetected = ref<DateDetectResult>('ambiguous')
const importDateFormat = ref<DateFormat>('MDY')
const dateFormatOptions = [
{ label: 'MM/DD/YYYY', value: 'MDY' },
{ label: 'DD/MM/YYYY', value: 'DMY' },
]
const importFormats = [
{ label: 'Toggl CSV', value: 'toggl' },
@@ -2358,8 +2391,23 @@ async function handleImportFile() {
if (importFormat.value === 'json') {
importPreview.value = []
importDateDetected.value = 'ambiguous'
importDateFormat.value = 'MDY'
} else {
importPreview.value = parseCSV(content).slice(0, 6)
const allRows = parseCSV(content)
importPreview.value = allRows.slice(0, 6)
// Detect date format from CSV data
const header = allRows[0]?.map(h => h.toLowerCase().trim()) || []
let dateColIdx = -1
if (importFormat.value === 'harvest') {
dateColIdx = findCol(header, 'date')
} else {
dateColIdx = findCol(header, 'start date')
}
const detected = detectDateFormat(allRows, dateColIdx)
importDateDetected.value = detected
importDateFormat.value = detected === 'ambiguous' ? 'MDY' : detected
}
} catch (e) {
console.error('Failed to read file:', e)
@@ -2380,12 +2428,13 @@ async function executeImport() {
const rows = parseCSV(importFileContent.value)
let entries: ImportEntry[]
const df = importDateFormat.value
if (importFormat.value === 'toggl') {
entries = mapTogglCSV(rows)
entries = mapTogglCSV(rows, df)
} else if (importFormat.value === 'clockify') {
entries = mapClockifyCSV(rows)
entries = mapClockifyCSV(rows, df)
} else if (importFormat.value === 'harvest') {
entries = mapHarvestCSV(rows)
entries = mapHarvestCSV(rows, df)
} else {
entries = mapGenericCSV(rows, { project: 0, description: 1, start_time: 2, duration: 3, client: -1, task: -1 })
}
@@ -2397,6 +2446,8 @@ async function executeImport() {
importFileContent.value = ''
importFileName.value = ''
importPreview.value = []
importDateDetected.value = 'ambiguous'
importDateFormat.value = 'MDY'
} catch (e) {
importStatus.value = `Error: ${e}`
} finally {