feat(dashboard): add Tasks Today + ADO Priority Items panels
Tasks Today shows pending tasks for today with quick-complete. ADO Priority Items shows top 5 open items sorted by priority. Falls back to a connect prompt when no DevOps integration is configured. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
cbce273b49
commit
fe31ca0fd7
1 changed files with 127 additions and 1 deletions
|
|
@ -9,10 +9,14 @@ import CardTitle from '@/components/ui/CardTitle.vue'
|
|||
import CardContent from '@/components/ui/CardContent.vue'
|
||||
import Progress from '@/components/ui/Progress.vue'
|
||||
import Button from '@/components/ui/Button.vue'
|
||||
import { useTasksStore } from '@/stores/tasks'
|
||||
import { useDevopsStore } from '@/stores/devops'
|
||||
import { formatDuration, formatDate, isoDateStr } from '@/lib/utils'
|
||||
import type { KpiSummary, ProjectSummary, MonthlyDataPoint, DailyPoint, DowDataPoint, ToolUsage } from '@/types'
|
||||
|
||||
const router = useRouter()
|
||||
const tasksStore = useTasksStore()
|
||||
const devopsStore = useDevopsStore()
|
||||
|
||||
type Preset = 'today' | '7d' | '30d' | 'custom'
|
||||
|
||||
|
|
@ -26,6 +30,7 @@ const monthly = ref<DailyPoint[]>([])
|
|||
const dow = ref<DowDataPoint[]>([])
|
||||
const tools = ref<ToolUsage[]>([])
|
||||
const loading = ref(false)
|
||||
const completingTask = ref<string | null>(null)
|
||||
|
||||
const dateRange = computed(() => {
|
||||
const now = new Date()
|
||||
|
|
@ -73,7 +78,32 @@ watch(preset, () => {
|
|||
if (preset.value !== 'custom') loadData()
|
||||
})
|
||||
|
||||
onMounted(() => loadData())
|
||||
async function quickComplete(taskId: string) {
|
||||
if (completingTask.value) return
|
||||
completingTask.value = taskId
|
||||
try {
|
||||
await tasksStore.complete(taskId)
|
||||
await tasksStore.fetchForDate(isoDateStr(new Date()))
|
||||
} finally {
|
||||
completingTask.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const topAdoItems = computed(() =>
|
||||
devopsStore.workItems
|
||||
.filter((wi) => !['Closed', 'Done', 'Removed'].includes(wi.state))
|
||||
.sort((a, b) => (a.priority ?? 3) - (b.priority ?? 3))
|
||||
.slice(0, 5)
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
loadData()
|
||||
tasksStore.fetchForDate(isoDateStr(new Date()))
|
||||
try {
|
||||
await devopsStore.fetchIntegration()
|
||||
if (devopsStore.integration) devopsStore.fetchWorkItems()
|
||||
} catch { /* no integration */ }
|
||||
})
|
||||
|
||||
const maxMonthlyHours = computed(() => Math.max(...monthly.value.map((d) => d.hours), 1))
|
||||
const maxDowHours = computed(() => Math.max(...dow.value.map((d) => d.hours), 1))
|
||||
|
|
@ -172,6 +202,102 @@ const DOW_LABELS = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']
|
|||
/>
|
||||
</div>
|
||||
|
||||
<!-- Tasks Today + ADO row -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<!-- Tasks Today -->
|
||||
<Card class="border-border/60 bg-card panel-glow">
|
||||
<CardHeader class="pb-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<CardTitle class="text-xs font-semibold text-muted-foreground uppercase tracking-widest">Tasks Today</CardTitle>
|
||||
<RouterLink to="/planner" class="text-xs text-primary hover:underline">View all →</RouterLink>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div v-if="tasksStore.tasks.filter(t => t.status !== 'done' && t.status !== 'cancelled').length === 0" class="flex flex-col items-center justify-center py-6 gap-2">
|
||||
<svg class="h-7 w-7 text-muted-foreground/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
|
||||
</svg>
|
||||
<p class="text-xs text-muted-foreground">No pending tasks for today</p>
|
||||
</div>
|
||||
<div v-else class="space-y-1.5">
|
||||
<div
|
||||
v-for="task in tasksStore.tasks.filter(t => t.status !== 'done' && t.status !== 'cancelled').slice(0, 6)"
|
||||
:key="task.id"
|
||||
class="flex items-center gap-2 group py-1"
|
||||
>
|
||||
<button
|
||||
class="h-4 w-4 shrink-0 rounded border border-border/70 group-hover:border-primary transition-colors flex items-center justify-center"
|
||||
:class="{ 'opacity-50': completingTask === task.id }"
|
||||
:disabled="!!completingTask"
|
||||
@click="quickComplete(task.id)"
|
||||
title="Mark done"
|
||||
>
|
||||
<svg v-if="completingTask === task.id" class="h-3 w-3 animate-spin text-primary" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="flex-1 text-xs text-foreground truncate">{{ task.title }}</span>
|
||||
<span
|
||||
v-if="task.status === 'doing'"
|
||||
class="text-[10px] px-1 py-0.5 rounded bg-primary/20 text-primary shrink-0"
|
||||
>in progress</span>
|
||||
<span
|
||||
v-else-if="task.priority <= 2"
|
||||
class="text-[10px] px-1 py-0.5 rounded bg-destructive/20 text-destructive shrink-0"
|
||||
>urgent</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- ADO Priority Items -->
|
||||
<Card v-if="devopsStore.integration" class="border-border/60 bg-card panel-glow">
|
||||
<CardHeader class="pb-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<CardTitle class="text-xs font-semibold text-muted-foreground uppercase tracking-widest">ADO Priority Items</CardTitle>
|
||||
<RouterLink to="/devops" class="text-xs text-primary hover:underline">View all →</RouterLink>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div v-if="topAdoItems.length === 0" class="flex flex-col items-center justify-center py-6 gap-2">
|
||||
<p class="text-xs text-muted-foreground">No open work items</p>
|
||||
</div>
|
||||
<div v-else class="space-y-2">
|
||||
<a
|
||||
v-for="wi in topAdoItems"
|
||||
:key="wi.id"
|
||||
:href="wi.url"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="flex items-start gap-2 group py-1"
|
||||
>
|
||||
<span
|
||||
class="mt-0.5 shrink-0 h-4 w-4 rounded text-[10px] font-bold flex items-center justify-center"
|
||||
:class="(wi.priority ?? 3) <= 2 ? 'bg-destructive/20 text-destructive' : 'bg-muted text-muted-foreground'"
|
||||
>{{ wi.priority ?? 3 }}</span>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-xs text-foreground truncate group-hover:text-primary transition-colors">{{ wi.title }}</p>
|
||||
<p class="text-[10px] text-muted-foreground truncate">{{ wi.team_project ?? '' }}</p>
|
||||
</div>
|
||||
<span class="text-[10px] px-1.5 py-0.5 rounded border border-border/50 text-muted-foreground shrink-0">{{ wi.state }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Placeholder when no ADO integration -->
|
||||
<Card v-else class="border-border/60 bg-card panel-glow border-dashed">
|
||||
<CardContent class="flex flex-col items-center justify-center py-10 gap-2">
|
||||
<svg class="h-8 w-8 text-muted-foreground/20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
||||
</svg>
|
||||
<p class="text-xs text-muted-foreground">Connect Azure DevOps in</p>
|
||||
<RouterLink to="/devops" class="text-xs text-primary hover:underline">Settings → DevOps</RouterLink>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- Charts row -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<!-- Hours by Day bar chart -->
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue