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:
10
src/utils/rounding.ts
Normal file
10
src/utils/rounding.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -264,6 +264,44 @@
|
||||
@change="saveShortcutSettings"
|
||||
/>
|
||||
</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>
|
||||
|
||||
@@ -285,6 +323,63 @@
|
||||
@update:model-value="saveSettings"
|
||||
/>
|
||||
</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>
|
||||
|
||||
<!-- Data -->
|
||||
@@ -448,6 +543,31 @@ const accentColor = ref('amber')
|
||||
const shortcutToggleTimer = ref('CmdOrCtrl+Shift+T')
|
||||
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 = [
|
||||
{ value: 'dark', label: 'Dark' },
|
||||
{ value: 'light', label: 'Light' },
|
||||
@@ -560,6 +680,25 @@ async function saveShortcutSettings() {
|
||||
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
|
||||
async function handleImportFile() {
|
||||
try {
|
||||
@@ -670,5 +809,10 @@ onMounted(async () => {
|
||||
accentColor.value = settingsStore.settings.accent_color || 'amber'
|
||||
shortcutToggleTimer.value = settingsStore.settings.shortcut_toggle_timer || 'CmdOrCtrl+Shift+T'
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user