feat: weekly comparison indicators and sparklines on dashboard
This commit is contained in:
@@ -1,8 +1,30 @@
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<GettingStartedChecklist />
|
||||
|
||||
<!-- Loading skeleton -->
|
||||
<div v-if="loading" class="space-y-6" aria-busy="true" aria-label="Loading dashboard data">
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<div v-for="i in 4" :key="i" class="bg-bg-surface rounded-lg p-4">
|
||||
<div class="h-3 w-16 bg-bg-elevated rounded animate-pulse mb-2" />
|
||||
<div class="h-6 w-24 bg-bg-elevated rounded animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-bg-surface rounded-lg p-4">
|
||||
<div class="h-48 bg-bg-elevated rounded animate-pulse" />
|
||||
</div>
|
||||
<div class="bg-bg-surface rounded-lg p-4 space-y-3">
|
||||
<div v-for="i in 3" :key="i" class="h-10 bg-bg-elevated rounded animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sr-only" aria-live="polite">
|
||||
{{ loading ? '' : 'Dashboard loaded' }}
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div v-if="isEmpty" class="flex flex-col items-center justify-center py-16">
|
||||
<Clock class="w-12 h-12 text-text-tertiary animate-float" :stroke-width="1.5" />
|
||||
<div v-if="!loading && isEmpty" class="flex flex-col items-center justify-center py-16">
|
||||
<Clock class="w-12 h-12 text-text-tertiary animate-float" :stroke-width="1.5" aria-hidden="true" />
|
||||
<p class="text-sm text-text-secondary mt-4">Start tracking your time</p>
|
||||
<p class="text-xs text-text-tertiary mt-2 max-w-xs text-center">Your dashboard will come alive with stats, charts, and recent activity once you start logging hours.</p>
|
||||
<router-link to="/timer" class="mt-4 px-4 py-2 bg-accent text-bg-base text-xs font-medium rounded-lg hover:bg-accent-hover transition-colors">
|
||||
@@ -12,32 +34,42 @@
|
||||
|
||||
<!-- Main content -->
|
||||
<Transition name="fade" appear>
|
||||
<div v-if="!isEmpty">
|
||||
<div v-if="!loading && !isEmpty">
|
||||
<!-- Greeting header -->
|
||||
<div class="mb-8">
|
||||
<p class="text-xl font-[family-name:var(--font-heading)] text-text-secondary">{{ greeting }}</p>
|
||||
<h1 class="text-xl font-[family-name:var(--font-heading)] text-text-secondary">{{ greeting }}</h1>
|
||||
<p class="text-xs text-text-tertiary mt-1">{{ formattedDate }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Stats row - 4 columns -->
|
||||
<div class="grid grid-cols-4 gap-6 mb-8">
|
||||
<dl class="grid grid-cols-4 gap-6 mb-8">
|
||||
<div>
|
||||
<p class="text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1">Today</p>
|
||||
<p class="text-[1.25rem] font-[family-name:var(--font-heading)] text-accent-text font-medium">{{ formatDuration(todayStats.totalSeconds) }}</p>
|
||||
<dt class="text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1">Today</dt>
|
||||
<dd class="text-[1.25rem] font-[family-name:var(--font-heading)] text-accent-text font-medium">{{ formatDuration(todayStats.totalSeconds) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1">This Week</p>
|
||||
<p class="text-[1.25rem] font-[family-name:var(--font-heading)] text-accent-text font-medium">{{ formatDuration(weekStats.totalSeconds) }}</p>
|
||||
<dt class="text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1">This Week</dt>
|
||||
<dd class="text-[1.25rem] font-[family-name:var(--font-heading)] text-accent-text font-medium">{{ formatDuration(weekStats.totalSeconds) }}</dd>
|
||||
<div v-if="lastWeekSeconds > 0" class="flex items-center gap-1 mt-1">
|
||||
<span class="sr-only">Compared to last week:</span>
|
||||
<ChevronUp v-if="weekDiff > 0" class="w-3 h-3 text-status-running" aria-hidden="true" />
|
||||
<ChevronDown v-else-if="weekDiff < 0" class="w-3 h-3 text-status-error" aria-hidden="true" />
|
||||
<Minus v-else class="w-3 h-3 text-text-tertiary" aria-hidden="true" />
|
||||
<span class="text-[0.625rem]"
|
||||
:class="weekDiff >= 0 ? 'text-status-running' : 'text-status-error'">
|
||||
{{ formatDuration(Math.abs(weekDiff)) }} {{ weekDiff >= 0 ? 'more' : 'less' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1">This Month</p>
|
||||
<p class="text-[1.25rem] font-[family-name:var(--font-heading)] text-accent-text font-medium">{{ formatDuration(monthStats.totalSeconds) }}</p>
|
||||
<dt class="text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1">This Month</dt>
|
||||
<dd class="text-[1.25rem] font-[family-name:var(--font-heading)] text-accent-text font-medium">{{ formatDuration(monthStats.totalSeconds) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1">Active Projects</p>
|
||||
<p class="text-[1.25rem] font-[family-name:var(--font-heading)] text-accent-text font-medium">{{ activeProjectsCount }}</p>
|
||||
</div>
|
||||
<dt class="text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1">Active Projects</dt>
|
||||
<dd class="text-[1.25rem] font-[family-name:var(--font-heading)] text-accent-text font-medium">{{ activeProjectsCount }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<!-- Goal Progress -->
|
||||
<div v-if="goalProgress" class="mt-6">
|
||||
@@ -48,7 +80,7 @@
|
||||
<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="w-full bg-bg-elevated rounded-full h-1.5" role="progressbar" :aria-valuenow="Math.round(dailyPct)" aria-valuemin="0" aria-valuemax="100" :aria-label="'Daily goal progress: ' + Math.round(dailyPct) + '%'">
|
||||
<div
|
||||
class="h-1.5 rounded-full bg-accent progress-bar"
|
||||
:style="{ width: Math.min(dailyPct, 100) + '%' }"
|
||||
@@ -60,7 +92,7 @@
|
||||
<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="w-full bg-bg-elevated rounded-full h-1.5" role="progressbar" :aria-valuenow="Math.round(weeklyPct)" aria-valuemin="0" aria-valuemax="100" :aria-label="'Weekly goal progress: ' + Math.round(weeklyPct) + '%'">
|
||||
<div
|
||||
class="h-1.5 rounded-full bg-accent progress-bar"
|
||||
:style="{ width: Math.min(weeklyPct, 100) + '%' }"
|
||||
@@ -77,16 +109,35 @@
|
||||
<!-- 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>
|
||||
<div class="h-48">
|
||||
<div class="h-48" aria-label="Weekly hours bar chart">
|
||||
<Bar v-if="chartData" :data="chartData" :options="chartOptions" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4-week sparkline -->
|
||||
<div v-if="weeklySparkline.length > 0" class="mb-8 bg-bg-surface rounded-lg p-4 border border-border-subtle">
|
||||
<h2 class="text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-3">Last 4 Weeks</h2>
|
||||
<div class="flex items-end gap-3 h-16" role="img" :aria-label="sparklineLabel">
|
||||
<div
|
||||
v-for="(week, i) in weeklySparkline"
|
||||
:key="i"
|
||||
class="flex-1 flex flex-col items-center gap-1"
|
||||
>
|
||||
<div
|
||||
class="w-full rounded-sm transition-all duration-300"
|
||||
:class="i === weeklySparkline.length - 1 ? 'bg-accent' : 'bg-accent-muted'"
|
||||
:style="{ height: `${week.percent}%`, minHeight: week.hours > 0 ? '4px' : '0px' }"
|
||||
/>
|
||||
<span class="text-[0.5625rem] text-text-tertiary">{{ week.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent entries -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-[1.25rem] font-semibold font-[family-name:var(--font-heading)] text-text-primary">Recent Entries</h2>
|
||||
<router-link to="/entries" class="text-xs text-text-tertiary hover:text-text-secondary transition-colors">View all</router-link>
|
||||
<router-link to="/entries" class="text-xs text-text-tertiary hover:text-text-secondary transition-colors" aria-label="View all time entries">View all</router-link>
|
||||
</div>
|
||||
|
||||
<div v-if="recentEntries.length > 0">
|
||||
@@ -99,6 +150,7 @@
|
||||
<div
|
||||
class="w-2 h-2 rounded-full shrink-0"
|
||||
:style="{ backgroundColor: getProjectColor(entry.project_id) }"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="text-[0.75rem] text-text-primary">{{ getProjectName(entry.project_id) }}</span>
|
||||
</div>
|
||||
@@ -122,12 +174,46 @@
|
||||
: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 }" />
|
||||
<div class="w-2 h-2 rounded-full" :style="{ backgroundColor: alert.color }" aria-hidden="true" />
|
||||
<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>
|
||||
<span class="sr-only">{{ alert.pct > 90 ? 'Over budget' : 'Approaching budget limit' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Project Forecasts -->
|
||||
<div v-if="projectForecasts.length > 0" class="mt-6">
|
||||
<h2 class="text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-3">Project Forecasts</h2>
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="forecast in projectForecasts"
|
||||
:key="forecast.id"
|
||||
class="px-3 py-2.5 bg-bg-surface border border-border-subtle rounded-lg"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-1.5">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2 h-2 rounded-full" :style="{ backgroundColor: forecast.color }" aria-hidden="true" />
|
||||
<span class="text-[0.8125rem] text-text-primary">{{ forecast.name }}</span>
|
||||
</div>
|
||||
<span class="text-[0.6875rem] font-mono" :class="paceColor(forecast.pace)">
|
||||
{{ forecast.paceLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-[0.625rem] text-text-tertiary">{{ forecast.hoursUsed.toFixed(0) }}h / {{ forecast.budgetHours }}h</span>
|
||||
<span class="text-[0.625rem] text-text-tertiary">{{ forecast.dailyAvg.toFixed(1) }}h/day</span>
|
||||
</div>
|
||||
<div class="w-full bg-bg-elevated rounded-full h-1" role="progressbar" :aria-valuenow="Math.round(forecast.pct)" aria-valuemin="0" aria-valuemax="100">
|
||||
<div
|
||||
class="h-1 rounded-full progress-bar"
|
||||
:class="forecast.pct > 90 ? 'bg-status-error' : forecast.pct > 75 ? 'bg-status-warning' : 'bg-accent'"
|
||||
:style="{ width: Math.min(forecast.pct, 100) + '%' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -137,7 +223,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { Bar } from 'vue-chartjs'
|
||||
import {
|
||||
@@ -149,11 +235,14 @@ import {
|
||||
Tooltip,
|
||||
Legend
|
||||
} from 'chart.js'
|
||||
import { Clock } from 'lucide-vue-next'
|
||||
import { Clock, ChevronUp, ChevronDown, Minus } from 'lucide-vue-next'
|
||||
import GettingStartedChecklist from '../components/GettingStartedChecklist.vue'
|
||||
import { useOnboardingStore } from '../stores/onboarding'
|
||||
import { useEntriesStore } from '../stores/entries'
|
||||
import { useProjectsStore } from '../stores/projects'
|
||||
import { useSettingsStore } from '../stores/settings'
|
||||
import { formatDateLong } from '../utils/locale'
|
||||
import { getChartTheme, buildBarChartOptions } from '../utils/chartTheme'
|
||||
import type { TimeEntry } from '../stores/entries'
|
||||
|
||||
// Register Chart.js components
|
||||
@@ -162,6 +251,9 @@ ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend)
|
||||
const entriesStore = useEntriesStore()
|
||||
const projectsStore = useProjectsStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
const onboardingStore = useOnboardingStore()
|
||||
|
||||
const loading = ref(true)
|
||||
|
||||
const goalProgress = ref<{ today_seconds: number; week_seconds: number; streak_days: number } | null>(null)
|
||||
|
||||
@@ -189,6 +281,16 @@ const weekStats = ref<{ totalSeconds: number; byProject: unknown[] }>({ totalSec
|
||||
const monthStats = ref<{ totalSeconds: number; byProject: unknown[] }>({ totalSeconds: 0, byProject: [] })
|
||||
const recentEntries = ref<TimeEntry[]>([])
|
||||
|
||||
// Weekly comparison data
|
||||
const lastWeekSeconds = ref(0)
|
||||
const weekDiff = ref(0)
|
||||
|
||||
// 4-week sparkline data
|
||||
const weeklySparkline = ref<Array<{ hours: number; percent: number; label: string }>>([])
|
||||
const sparklineLabel = computed(() =>
|
||||
weeklySparkline.value.map(w => `${w.label}: ${w.hours.toFixed(1)} hours`).join(', ')
|
||||
)
|
||||
|
||||
// Greeting based on time of day
|
||||
const greeting = computed(() => {
|
||||
const hour = new Date().getHours()
|
||||
@@ -227,6 +329,11 @@ function getToday(): string {
|
||||
return new Date().toISOString().split('T')[0]
|
||||
}
|
||||
|
||||
// Format a Date to YYYY-MM-DD
|
||||
function formatDate(d: Date): string {
|
||||
return d.toISOString().split('T')[0]
|
||||
}
|
||||
|
||||
// Format duration from seconds to readable format
|
||||
function formatDuration(seconds: number): string {
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
@@ -255,13 +362,23 @@ const activeProjectsCount = computed(() => {
|
||||
})
|
||||
|
||||
// Budget status
|
||||
const budgetStatus = ref<Record<number, { used_hours: number; used_amount: number }>>({})
|
||||
interface DashboardBudgetStatus {
|
||||
hours_used: number
|
||||
amount_used: number
|
||||
daily_average_hours: number
|
||||
estimated_completion_days: number | null
|
||||
hours_remaining: number | null
|
||||
pace: string | null
|
||||
}
|
||||
|
||||
const budgetStatus = ref<Record<number, DashboardBudgetStatus>>({})
|
||||
|
||||
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 })
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const status = await invoke<DashboardBudgetStatus>('get_project_budget_status', { projectId: project.id, today })
|
||||
budgetStatus.value[project.id] = status
|
||||
} catch (e) {
|
||||
// ignore
|
||||
@@ -274,7 +391,7 @@ 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 used = budgetStatus.value[p.id!]?.hours_used || 0
|
||||
const pct = (used / p.budget_hours!) * 100
|
||||
return { id: p.id!, name: p.name, color: p.color, pct }
|
||||
})
|
||||
@@ -282,6 +399,60 @@ const budgetAlerts = computed(() => {
|
||||
.sort((a, b) => b.pct - a.pct)
|
||||
})
|
||||
|
||||
// Project forecasts
|
||||
interface ForecastData {
|
||||
id: number
|
||||
name: string
|
||||
color: string
|
||||
hoursUsed: number
|
||||
budgetHours: number
|
||||
pct: number
|
||||
dailyAvg: number
|
||||
pace: string | null
|
||||
paceLabel: string
|
||||
}
|
||||
|
||||
const projectForecasts = computed<ForecastData[]>(() => {
|
||||
return projectsStore.projects
|
||||
.filter(p => p.id && p.budget_hours && budgetStatus.value[p.id!])
|
||||
.map(p => {
|
||||
const status = budgetStatus.value[p.id!]
|
||||
const pct = p.budget_hours! > 0 ? (status.hours_used / p.budget_hours!) * 100 : 0
|
||||
let paceLabel = 'No data'
|
||||
if (status.pace === 'ahead') paceLabel = 'Ahead'
|
||||
else if (status.pace === 'on_track') paceLabel = 'On track'
|
||||
else if (status.pace === 'behind') paceLabel = 'Behind'
|
||||
else if (status.pace === 'complete') paceLabel = 'Complete'
|
||||
return {
|
||||
id: p.id!,
|
||||
name: p.name,
|
||||
color: p.color,
|
||||
hoursUsed: status.hours_used,
|
||||
budgetHours: p.budget_hours!,
|
||||
pct,
|
||||
dailyAvg: status.daily_average_hours || 0,
|
||||
pace: status.pace,
|
||||
paceLabel,
|
||||
}
|
||||
})
|
||||
.slice(0, 5)
|
||||
})
|
||||
|
||||
function paceColor(pace: string | null): string {
|
||||
if (pace === 'ahead') return 'text-status-running'
|
||||
if (pace === 'on_track') return 'text-accent-text'
|
||||
if (pace === 'behind') return 'text-status-error'
|
||||
if (pace === 'complete') return 'text-status-running'
|
||||
return 'text-text-tertiary'
|
||||
}
|
||||
|
||||
// Chart theme state
|
||||
const chartTheme = ref(getChartTheme())
|
||||
|
||||
function refreshChartTheme() {
|
||||
chartTheme.value = getChartTheme()
|
||||
}
|
||||
|
||||
// Chart data for weekly hours
|
||||
const chartData = computed(() => {
|
||||
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
||||
@@ -303,8 +474,8 @@ const chartData = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// Color today's bar lighter amber
|
||||
const colors = hoursPerDay.map((_, i) => i === todayArrayIndex ? '#FBBF24' : '#D97706')
|
||||
const theme = chartTheme.value
|
||||
const colors = hoursPerDay.map((_, i) => i === todayArrayIndex ? (theme.accentMuted || theme.accent) : theme.accent)
|
||||
|
||||
return {
|
||||
labels: days,
|
||||
@@ -319,15 +490,23 @@ const chartData = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// Chart options
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
// Chart options (theme-aware)
|
||||
const chartOptions = computed(() => {
|
||||
const theme = chartTheme.value
|
||||
const base = buildBarChartOptions(theme)
|
||||
return {
|
||||
...base,
|
||||
scales: {
|
||||
...base.scales,
|
||||
y: {
|
||||
...base.scales.y,
|
||||
beginAtZero: true,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
...base.plugins,
|
||||
tooltip: {
|
||||
...base.plugins.tooltip,
|
||||
callbacks: {
|
||||
label: (context: { raw: unknown }) => {
|
||||
const hours = context.raw as number
|
||||
@@ -335,63 +514,105 @@ const chartOptions = {
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
color: '#2E2E2A'
|
||||
},
|
||||
ticks: {
|
||||
color: '#5A5A54'
|
||||
}
|
||||
},
|
||||
x: {
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
ticks: {
|
||||
color: '#5A5A54'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Recreate chart on theme or accent change
|
||||
watch(() => settingsStore.settings.theme_mode, () => {
|
||||
nextTick(() => { refreshChartTheme() })
|
||||
})
|
||||
watch(() => settingsStore.settings.accent_color, () => {
|
||||
nextTick(() => { refreshChartTheme() })
|
||||
})
|
||||
|
||||
// Load data on mount
|
||||
onMounted(async () => {
|
||||
await projectsStore.fetchProjects()
|
||||
await entriesStore.fetchEntries()
|
||||
await settingsStore.fetchSettings()
|
||||
await loadGoalProgress()
|
||||
loading.value = true
|
||||
|
||||
await Promise.all([
|
||||
projectsStore.fetchProjects(),
|
||||
entriesStore.fetchEntries(),
|
||||
settingsStore.fetchSettings(),
|
||||
loadGoalProgress(),
|
||||
])
|
||||
|
||||
// Budget status depends on projects being loaded
|
||||
await loadBudgetStatus()
|
||||
|
||||
try {
|
||||
todayStats.value = await invoke('get_reports', {
|
||||
// Fetch stats in parallel
|
||||
const [todayResult, weekResult, monthResult] = await Promise.allSettled([
|
||||
invoke<{ totalSeconds: number; byProject: unknown[] }>('get_reports', {
|
||||
startDate: getToday(),
|
||||
endDate: getToday()
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch today stats:', error)
|
||||
}
|
||||
|
||||
try {
|
||||
weekStats.value = await invoke('get_reports', {
|
||||
}),
|
||||
invoke<{ totalSeconds: number; byProject: unknown[] }>('get_reports', {
|
||||
startDate: getWeekStart(),
|
||||
endDate: getToday()
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch week stats:', error)
|
||||
}
|
||||
|
||||
try {
|
||||
monthStats.value = await invoke('get_reports', {
|
||||
}),
|
||||
invoke<{ totalSeconds: number; byProject: unknown[] }>('get_reports', {
|
||||
startDate: getMonthStart(),
|
||||
endDate: getToday()
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch month stats:', error)
|
||||
}
|
||||
}),
|
||||
])
|
||||
|
||||
if (todayResult.status === 'fulfilled') todayStats.value = todayResult.value
|
||||
if (weekResult.status === 'fulfilled') weekStats.value = weekResult.value
|
||||
if (monthResult.status === 'fulfilled') monthStats.value = monthResult.value
|
||||
|
||||
recentEntries.value = entriesStore.entries.slice(0, 5)
|
||||
|
||||
// Fetch last week's data for comparison
|
||||
const lastWeekEnd = new Date()
|
||||
lastWeekEnd.setDate(lastWeekEnd.getDate() - lastWeekEnd.getDay() + (lastWeekEnd.getDay() === 0 ? -7 : 0))
|
||||
const lastWeekStart = new Date(lastWeekEnd)
|
||||
lastWeekStart.setDate(lastWeekStart.getDate() - 6)
|
||||
try {
|
||||
const lastWeek = await invoke<{ totalSeconds: number; byProject: unknown[] }>('get_reports', {
|
||||
startDate: formatDate(lastWeekStart),
|
||||
endDate: formatDate(lastWeekEnd)
|
||||
})
|
||||
lastWeekSeconds.value = lastWeek.totalSeconds || 0
|
||||
weekDiff.value = weekStats.value.totalSeconds - lastWeekSeconds.value
|
||||
} catch {
|
||||
// Comparison data is optional
|
||||
}
|
||||
|
||||
// Fetch sparkline data for last 4 weeks (including current)
|
||||
const sparklineData: Array<{ hours: number; label: string }> = []
|
||||
for (let w = 3; w >= 0; w--) {
|
||||
const now = new Date()
|
||||
const dayOfWeek = now.getDay()
|
||||
const monday = new Date(now)
|
||||
monday.setDate(now.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1) - (w * 7))
|
||||
const sunday = new Date(monday)
|
||||
sunday.setDate(sunday.getDate() + 6)
|
||||
const end = w === 0 ? now : sunday
|
||||
try {
|
||||
const report = await invoke<{ totalSeconds: number; byProject: unknown[] }>('get_reports', {
|
||||
startDate: formatDate(monday),
|
||||
endDate: formatDate(end)
|
||||
})
|
||||
sparklineData.push({
|
||||
hours: (report.totalSeconds || 0) / 3600,
|
||||
label: `${monday.getDate()}/${monday.getMonth() + 1}`
|
||||
})
|
||||
} catch {
|
||||
sparklineData.push({ hours: 0, label: `${monday.getDate()}/${monday.getMonth() + 1}` })
|
||||
}
|
||||
}
|
||||
const maxHours = Math.max(...sparklineData.map(d => d.hours), 1)
|
||||
weeklySparkline.value = sparklineData.map(d => ({
|
||||
hours: d.hours,
|
||||
percent: (d.hours / maxHours) * 100,
|
||||
label: d.label
|
||||
}))
|
||||
|
||||
await onboardingStore.load()
|
||||
|
||||
// Refresh chart theme after settings are loaded
|
||||
refreshChartTheme()
|
||||
|
||||
loading.value = false
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user