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.
This commit is contained in:
@@ -38,6 +38,41 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Goal Progress -->
|
||||
<div v-if="goalProgress" class="mt-6">
|
||||
<h2 class="text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-3">Goals</h2>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-[0.75rem] text-text-secondary">Today</span>
|
||||
<span class="text-[0.75rem] font-mono text-accent-text">{{ formatGoalHours(goalProgress.today_seconds) }}</span>
|
||||
</div>
|
||||
<div class="w-full bg-bg-elevated rounded-full h-1.5">
|
||||
<div
|
||||
class="h-1.5 rounded-full bg-accent transition-all"
|
||||
:style="{ width: Math.min(dailyPct, 100) + '%' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-[0.75rem] text-text-secondary">This Week</span>
|
||||
<span class="text-[0.75rem] font-mono text-accent-text">{{ formatGoalHours(goalProgress.week_seconds) }}</span>
|
||||
</div>
|
||||
<div class="w-full bg-bg-elevated rounded-full h-1.5">
|
||||
<div
|
||||
class="h-1.5 rounded-full bg-accent transition-all"
|
||||
:style="{ width: Math.min(weeklyPct, 100) + '%' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<span class="text-[1.25rem] font-[family-name:var(--font-heading)] text-accent-text font-medium">{{ goalProgress.streak_days }}</span>
|
||||
<span class="text-[0.6875rem] text-text-tertiary">day streak</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Weekly chart -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-[1.25rem] font-semibold font-[family-name:var(--font-heading)] text-text-primary mb-4">Weekly Hours</h2>
|
||||
@@ -74,6 +109,27 @@
|
||||
No entries yet. Start tracking your time.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Budget Alerts -->
|
||||
<div v-if="budgetAlerts.length > 0" class="mt-6">
|
||||
<h2 class="text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-3">Budget Alerts</h2>
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="alert in budgetAlerts"
|
||||
:key="alert.id"
|
||||
class="flex items-center justify-between py-2 px-3 rounded-lg"
|
||||
:class="alert.pct > 90 ? 'bg-status-error/10' : 'bg-status-warning/10'"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2 h-2 rounded-full" :style="{ backgroundColor: alert.color }" />
|
||||
<span class="text-[0.75rem] text-text-primary">{{ alert.name }}</span>
|
||||
</div>
|
||||
<span class="text-[0.75rem] font-mono" :class="alert.pct > 90 ? 'text-status-error' : 'text-status-warning'">
|
||||
{{ alert.pct.toFixed(0) }}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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<Record<number, { used_hours: number; used_amount: number }>>({})
|
||||
|
||||
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', {
|
||||
|
||||
Reference in New Issue
Block a user