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.
This commit is contained in:
Your Name
2026-02-18 10:51:56 +02:00
parent 8c56867764
commit 55505b2b6b
2 changed files with 154 additions and 0 deletions

10
src/utils/rounding.ts Normal file
View File

@@ -0,0 +1,10 @@
export function roundDuration(seconds: number, incrementMinutes: number, method: 'nearest' | 'up' | 'down'): number {
if (incrementMinutes <= 0) return seconds
const incSeconds = incrementMinutes * 60
switch (method) {
case 'up': return Math.ceil(seconds / incSeconds) * incSeconds
case 'down': return Math.floor(seconds / incSeconds) * incSeconds
case 'nearest': return Math.round(seconds / incSeconds) * incSeconds
default: return seconds
}
}

View File

@@ -264,6 +264,44 @@
@change="saveShortcutSettings" @change="saveShortcutSettings"
/> />
</div> </div>
<!-- Divider -->
<div class="border-t border-border-subtle" />
<!-- Goals -->
<h3 class="text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] font-medium">Goals</h3>
<div class="flex items-center justify-between">
<div>
<p class="text-[0.8125rem] text-text-primary">Daily Goal</p>
<p class="text-[0.6875rem] text-text-tertiary mt-0.5">Target hours per day</p>
</div>
<AppNumberInput
v-model="dailyGoalHours"
:min="0"
:max="24"
:step="0.5"
:precision="1"
suffix="hrs"
@update:model-value="saveGoalSettings"
/>
</div>
<div class="flex items-center justify-between">
<div>
<p class="text-[0.8125rem] text-text-primary">Weekly Goal</p>
<p class="text-[0.6875rem] text-text-tertiary mt-0.5">Target hours per week</p>
</div>
<AppNumberInput
v-model="weeklyGoalHours"
:min="0"
:max="168"
:step="1"
:precision="0"
suffix="hrs"
@update:model-value="saveGoalSettings"
/>
</div>
</div> </div>
</div> </div>
@@ -285,6 +323,63 @@
@update:model-value="saveSettings" @update:model-value="saveSettings"
/> />
</div> </div>
<!-- Divider -->
<div class="border-t border-border-subtle mt-5 pt-5" />
<div class="flex items-center justify-between">
<div>
<p class="text-[0.8125rem] text-text-primary">Time Rounding</p>
<p class="text-[0.6875rem] text-text-tertiary mt-0.5">Round time entries for billing</p>
</div>
<button
@click="toggleRounding"
:class="[
'relative inline-flex h-5 w-9 items-center rounded-full transition-colors duration-150',
roundingEnabled ? 'bg-status-running' : 'bg-bg-elevated'
]"
>
<span
:class="[
'inline-block h-3.5 w-3.5 transform rounded-full bg-text-primary transition-transform duration-150',
roundingEnabled ? 'translate-x-[18px]' : 'translate-x-[3px]'
]"
/>
</button>
</div>
<div v-if="roundingEnabled" class="space-y-5 pl-4 border-l-2 border-border-subtle ml-1 mt-5">
<div class="flex items-center justify-between">
<div>
<p class="text-[0.8125rem] text-text-primary">Increment</p>
<p class="text-[0.6875rem] text-text-tertiary mt-0.5">Round to nearest increment</p>
</div>
<div class="w-36">
<AppSelect
v-model="roundingIncrement"
:options="roundingIncrements"
label-key="label"
value-key="value"
@update:model-value="saveRoundingSettings"
/>
</div>
</div>
<div class="flex items-center justify-between">
<div>
<p class="text-[0.8125rem] text-text-primary">Method</p>
<p class="text-[0.6875rem] text-text-tertiary mt-0.5">Rounding direction</p>
</div>
<div class="w-36">
<AppSelect
v-model="roundingMethod"
:options="roundingMethods"
label-key="label"
value-key="value"
@update:model-value="saveRoundingSettings"
/>
</div>
</div>
</div>
</div> </div>
<!-- Data --> <!-- Data -->
@@ -448,6 +543,31 @@ const accentColor = ref('amber')
const shortcutToggleTimer = ref('CmdOrCtrl+Shift+T') const shortcutToggleTimer = ref('CmdOrCtrl+Shift+T')
const shortcutShowApp = ref('CmdOrCtrl+Shift+Z') const shortcutShowApp = ref('CmdOrCtrl+Shift+Z')
// Goal settings
const dailyGoalHours = ref(8)
const weeklyGoalHours = ref(40)
// Rounding settings
const roundingEnabled = ref(false)
const roundingIncrement = ref(15)
const roundingMethod = ref('nearest')
const roundingIncrements = [
{ value: 1, label: '1 minute' },
{ value: 5, label: '5 minutes' },
{ value: 6, label: '6 minutes' },
{ value: 10, label: '10 minutes' },
{ value: 15, label: '15 minutes' },
{ value: 30, label: '30 minutes' },
{ value: 60, label: '1 hour' },
]
const roundingMethods = [
{ value: 'nearest', label: 'Nearest' },
{ value: 'up', label: 'Round up' },
{ value: 'down', label: 'Round down' },
]
const themeModes = [ const themeModes = [
{ value: 'dark', label: 'Dark' }, { value: 'dark', label: 'Dark' },
{ value: 'light', label: 'Light' }, { value: 'light', label: 'Light' },
@@ -560,6 +680,25 @@ async function saveShortcutSettings() {
await settingsStore.updateSetting('shortcut_show_app', shortcutShowApp.value) await settingsStore.updateSetting('shortcut_show_app', shortcutShowApp.value)
} }
// Save goal settings
async function saveGoalSettings() {
await settingsStore.updateSetting('daily_goal_hours', dailyGoalHours.value.toString())
await settingsStore.updateSetting('weekly_goal_hours', weeklyGoalHours.value.toString())
}
// Toggle rounding
function toggleRounding() {
roundingEnabled.value = !roundingEnabled.value
saveRoundingSettings()
}
// Save rounding settings
async function saveRoundingSettings() {
await settingsStore.updateSetting('rounding_enabled', roundingEnabled.value.toString())
await settingsStore.updateSetting('rounding_increment', roundingIncrement.value.toString())
await settingsStore.updateSetting('rounding_method', roundingMethod.value)
}
// Import file handling // Import file handling
async function handleImportFile() { async function handleImportFile() {
try { try {
@@ -670,5 +809,10 @@ onMounted(async () => {
accentColor.value = settingsStore.settings.accent_color || 'amber' accentColor.value = settingsStore.settings.accent_color || 'amber'
shortcutToggleTimer.value = settingsStore.settings.shortcut_toggle_timer || 'CmdOrCtrl+Shift+T' shortcutToggleTimer.value = settingsStore.settings.shortcut_toggle_timer || 'CmdOrCtrl+Shift+T'
shortcutShowApp.value = settingsStore.settings.shortcut_show_app || 'CmdOrCtrl+Shift+Z' shortcutShowApp.value = settingsStore.settings.shortcut_show_app || 'CmdOrCtrl+Shift+Z'
dailyGoalHours.value = parseFloat(settingsStore.settings.daily_goal_hours) || 8
weeklyGoalHours.value = parseFloat(settingsStore.settings.weekly_goal_hours) || 40
roundingEnabled.value = settingsStore.settings.rounding_enabled === 'true'
roundingIncrement.value = parseInt(settingsStore.settings.rounding_increment) || 15
roundingMethod.value = settingsStore.settings.rounding_method || 'nearest'
}) })
</script> </script>