feat: add Dashboard view
This commit is contained in:
238
src/views/Dashboard.vue
Normal file
238
src/views/Dashboard.vue
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-6">
|
||||||
|
<h1 class="text-2xl font-bold mb-6">Dashboard</h1>
|
||||||
|
|
||||||
|
<!-- Stats Cards -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||||
|
<!-- This Week -->
|
||||||
|
<div class="bg-surface border border-border rounded-lg p-6">
|
||||||
|
<p class="text-text-secondary text-sm mb-1">This Week</p>
|
||||||
|
<p class="text-3xl font-bold text-text-primary">{{ formatDuration(weekStats.totalSeconds) }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- This Month -->
|
||||||
|
<div class="bg-surface border border-border rounded-lg p-6">
|
||||||
|
<p class="text-text-secondary text-sm mb-1">This Month</p>
|
||||||
|
<p class="text-3xl font-bold text-text-primary">{{ formatDuration(monthStats.totalSeconds) }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Active Projects -->
|
||||||
|
<div class="bg-surface border border-border rounded-lg p-6">
|
||||||
|
<p class="text-text-secondary text-sm mb-1">Active Projects</p>
|
||||||
|
<p class="text-3xl font-bold text-text-primary">{{ activeProjectsCount }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Weekly Chart -->
|
||||||
|
<div class="bg-surface border border-border rounded-lg p-6 mb-6">
|
||||||
|
<h2 class="text-lg font-semibold mb-4">This Week's Hours</h2>
|
||||||
|
<div class="h-64">
|
||||||
|
<Bar v-if="chartData" :data="chartData" :options="chartOptions" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Time Entries -->
|
||||||
|
<div class="bg-surface border border-border rounded-lg p-6">
|
||||||
|
<h2 class="text-lg font-semibold mb-4">Recent Entries</h2>
|
||||||
|
|
||||||
|
<div v-if="recentEntries.length > 0" class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="entry in recentEntries"
|
||||||
|
:key="entry.id"
|
||||||
|
class="flex items-center justify-between py-2 border-b border-border last:border-0"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
class="w-3 h-3 rounded-full"
|
||||||
|
:style="{ backgroundColor: getProjectColor(entry.project_id) }"
|
||||||
|
></div>
|
||||||
|
<span class="text-text-primary">{{ getProjectName(entry.project_id) }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-text-secondary font-mono">{{ formatDuration(entry.duration) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="text-center text-text-secondary py-8">
|
||||||
|
No time entries yet. Start tracking your time!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import { Bar } from 'vue-chartjs'
|
||||||
|
import {
|
||||||
|
Chart as ChartJS,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
BarElement,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Legend
|
||||||
|
} from 'chart.js'
|
||||||
|
import { useEntriesStore } from '../stores/entries'
|
||||||
|
import { useProjectsStore } from '../stores/projects'
|
||||||
|
import type { TimeEntry } from '../stores/entries'
|
||||||
|
|
||||||
|
// Register Chart.js components
|
||||||
|
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend)
|
||||||
|
|
||||||
|
const entriesStore = useEntriesStore()
|
||||||
|
const projectsStore = useProjectsStore()
|
||||||
|
|
||||||
|
const weekStats = ref<{ totalSeconds: number; byProject: unknown[] }>({ totalSeconds: 0, byProject: [] })
|
||||||
|
const monthStats = ref<{ totalSeconds: number; byProject: unknown[] }>({ totalSeconds: 0, byProject: [] })
|
||||||
|
const recentEntries = ref<TimeEntry[]>([])
|
||||||
|
|
||||||
|
// Get start of current week (Monday)
|
||||||
|
function getWeekStart(): string {
|
||||||
|
const now = new Date()
|
||||||
|
const day = now.getDay()
|
||||||
|
const diff = now.getDate() - day + (day === 0 ? -6 : 1)
|
||||||
|
const monday = new Date(now.setDate(diff))
|
||||||
|
return monday.toISOString().split('T')[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get start of current month
|
||||||
|
function getMonthStart(): string {
|
||||||
|
const now = new Date()
|
||||||
|
return new Date(now.getFullYear(), now.getMonth(), 1).toISOString().split('T')[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get today's date
|
||||||
|
function getToday(): string {
|
||||||
|
return new Date().toISOString().split('T')[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format duration from seconds to readable format
|
||||||
|
function formatDuration(seconds: number): string {
|
||||||
|
const hours = Math.floor(seconds / 3600)
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60)
|
||||||
|
if (hours > 0) {
|
||||||
|
return `${hours}h ${minutes}m`
|
||||||
|
}
|
||||||
|
return `${minutes}m`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get project name by ID
|
||||||
|
function getProjectName(projectId: number): string {
|
||||||
|
const project = projectsStore.projects.find(p => p.id === projectId)
|
||||||
|
return project?.name || 'Unknown Project'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get project color by ID
|
||||||
|
function getProjectColor(projectId: number): string {
|
||||||
|
const project = projectsStore.projects.find(p => p.id === projectId)
|
||||||
|
return project?.color || '#6B7280'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active projects count (non-archived)
|
||||||
|
const activeProjectsCount = computed(() => {
|
||||||
|
return projectsStore.projects.filter(p => !p.archived).length
|
||||||
|
})
|
||||||
|
|
||||||
|
// Chart data for weekly hours
|
||||||
|
const chartData = computed(() => {
|
||||||
|
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
||||||
|
const weekStart = getWeekStart()
|
||||||
|
const today = getToday()
|
||||||
|
|
||||||
|
// Initialize hours for each day
|
||||||
|
const hoursPerDay = [0, 0, 0, 0, 0, 0, 0]
|
||||||
|
|
||||||
|
// Sum up hours from entries for this week
|
||||||
|
recentEntries.value.forEach(entry => {
|
||||||
|
const entryDate = new Date(entry.start_time).toISOString().split('T')[0]
|
||||||
|
if (entryDate >= weekStart && entryDate <= today) {
|
||||||
|
const dayIndex = new Date(entry.start_time).getDay()
|
||||||
|
// Convert Monday=0, Sunday=6 to array index
|
||||||
|
const index = dayIndex === 0 ? 6 : dayIndex - 1
|
||||||
|
hoursPerDay[index] += entry.duration / 3600
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels: days,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Hours',
|
||||||
|
data: hoursPerDay,
|
||||||
|
backgroundColor: '#F59E0B',
|
||||||
|
borderRadius: 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Chart options
|
||||||
|
const chartOptions = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: (context: { raw: unknown }) => {
|
||||||
|
const hours = context.raw as number
|
||||||
|
return `${hours.toFixed(1)} hours`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
grid: {
|
||||||
|
color: '#2E2E2E'
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
color: '#A0A0A0'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
grid: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
color: '#A0A0A0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load data on mount
|
||||||
|
onMounted(async () => {
|
||||||
|
// Fetch projects first
|
||||||
|
await projectsStore.fetchProjects()
|
||||||
|
|
||||||
|
// Fetch all entries
|
||||||
|
await entriesStore.fetchEntries()
|
||||||
|
|
||||||
|
// Get this week's stats
|
||||||
|
try {
|
||||||
|
weekStats.value = await invoke('get_reports', {
|
||||||
|
startDate: getWeekStart(),
|
||||||
|
endDate: getToday()
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch week stats:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get this month's stats
|
||||||
|
try {
|
||||||
|
monthStats.value = await invoke('get_reports', {
|
||||||
|
startDate: getMonthStart(),
|
||||||
|
endDate: getToday()
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch month stats:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get recent entries (last 5)
|
||||||
|
recentEntries.value = entriesStore.entries.slice(0, 5)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user