diff --git a/src/views/Dashboard.vue b/src/views/Dashboard.vue
index 0ac9431..1062da6 100644
--- a/src/views/Dashboard.vue
+++ b/src/views/Dashboard.vue
@@ -38,6 +38,41 @@
+
+
+
Goals
+
+
+
+ Today
+ {{ formatGoalHours(goalProgress.today_seconds) }}
+
+
+
+
+
+ This Week
+ {{ formatGoalHours(goalProgress.week_seconds) }}
+
+
+
+
+ {{ goalProgress.streak_days }}
+ day streak
+
+
+
+
Weekly Hours
@@ -74,6 +109,27 @@
No entries yet. Start tracking your time.
+
+
+
+
Budget Alerts
+
+
+
+
+ {{ alert.pct.toFixed(0) }}%
+
+
+
+
@@ -94,6 +150,7 @@ import {
import { Clock } from 'lucide-vue-next'
import { useEntriesStore } from '../stores/entries'
import { useProjectsStore } from '../stores/projects'
+import { useSettingsStore } from '../stores/settings'
import { formatDateLong } from '../utils/locale'
import type { TimeEntry } from '../stores/entries'
@@ -102,6 +159,28 @@ ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend)
const entriesStore = useEntriesStore()
const projectsStore = useProjectsStore()
+const settingsStore = useSettingsStore()
+
+const goalProgress = ref<{ today_seconds: number; week_seconds: number; streak_days: number } | null>(null)
+
+async function loadGoalProgress() {
+ try {
+ goalProgress.value = await invoke('get_goal_progress')
+ } catch (e) {
+ // ignore
+ }
+}
+
+const dailyGoalHours = computed(() => parseFloat(settingsStore.settings.daily_goal_hours) || 8)
+const weeklyGoalHours = computed(() => parseFloat(settingsStore.settings.weekly_goal_hours) || 40)
+const dailyPct = computed(() => goalProgress.value ? (goalProgress.value.today_seconds / 3600 / dailyGoalHours.value) * 100 : 0)
+const weeklyPct = computed(() => goalProgress.value ? (goalProgress.value.week_seconds / 3600 / weeklyGoalHours.value) * 100 : 0)
+
+function formatGoalHours(seconds: number): string {
+ const h = Math.floor(seconds / 3600)
+ const m = Math.floor((seconds % 3600) / 60)
+ return `${h}h ${m}m`
+}
const todayStats = ref<{ totalSeconds: number; byProject: unknown[] }>({ totalSeconds: 0, byProject: [] })
const weekStats = ref<{ totalSeconds: number; byProject: unknown[] }>({ totalSeconds: 0, byProject: [] })
@@ -173,6 +252,34 @@ const activeProjectsCount = computed(() => {
return projectsStore.projects.filter(p => !p.archived).length
})
+// Budget status
+const budgetStatus = ref>({})
+
+async function loadBudgetStatus() {
+ for (const project of projectsStore.projects) {
+ if (project.id && project.budget_hours) {
+ try {
+ const status = await invoke<{ used_hours: number; used_amount: number }>('get_project_budget_status', { projectId: project.id })
+ budgetStatus.value[project.id] = status
+ } catch (e) {
+ // ignore
+ }
+ }
+ }
+}
+
+const budgetAlerts = computed(() => {
+ return projectsStore.projects
+ .filter(p => p.id && p.budget_hours)
+ .map(p => {
+ const used = budgetStatus.value[p.id!]?.used_hours || 0
+ const pct = (used / p.budget_hours!) * 100
+ return { id: p.id!, name: p.name, color: p.color, pct }
+ })
+ .filter(a => a.pct > 75)
+ .sort((a, b) => b.pct - a.pct)
+})
+
// Chart data for weekly hours
const chartData = computed(() => {
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
@@ -252,6 +359,9 @@ const chartOptions = {
onMounted(async () => {
await projectsStore.fetchProjects()
await entriesStore.fetchEntries()
+ await settingsStore.fetchSettings()
+ await loadGoalProgress()
+ await loadBudgetStatus()
try {
todayStats.value = await invoke('get_reports', {
diff --git a/src/views/Projects.vue b/src/views/Projects.vue
index 80d7108..101ba94 100644
--- a/src/views/Projects.vue
+++ b/src/views/Projects.vue
@@ -47,6 +47,19 @@
+
+
+ {{ getBudgetUsed(project).toFixed(0) }}h / {{ project.budget_hours }}h
+ {{ getBudgetPct(project).toFixed(0) }}%
+
+
+
@@ -69,7 +82,7 @@
@@ -117,28 +130,94 @@
-
-
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
![]()
+
+
+
{{ app.display_name || app.exe_name }}
+
{{ app.exe_name }}
+
+
+
+
+
+
No tracked apps. Timer will run without app visibility checks.
+
+
+
+
@@ -189,18 +268,36 @@
+
+