Files
vigilcare-clinical/vigilcare-dashboard/src/views/FeedbackSummary.vue
T

103 lines
4.3 KiB
Vue

<script setup>
import { storeToRefs } from 'pinia'
import { useFeedbackStore } from '@/stores/feedback'
import { alertTypeLabel } from '@/api/normalize'
import Badge from '@/components/ui/Badge.vue'
import Button from '@/components/ui/Button.vue'
import Card from '@/components/ui/Card.vue'
const feedbackStore = useFeedbackStore()
const { entries, stats, byAlertType } = storeToRefs(feedbackStore)
</script>
<template>
<div class="w-full min-w-0 space-y-8">
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<h1 class="text-xl font-bold dark:text-white">Feedback Summary</h1>
<div class="flex flex-wrap gap-2">
<Button variant="secondary" size="sm" @click="feedbackStore.exportAsJson()">
Export JSON
</Button>
<Button variant="secondary" size="sm" @click="feedbackStore.exportAsCsv()">
Export CSV
</Button>
</div>
</div>
<!-- Aggregate stats -->
<div class="grid w-full min-w-0 grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<Card>
<div class="text-center">
<div class="text-3xl font-bold dark:text-white">{{ stats.total }}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">Total Ratings</div>
</div>
</Card>
<Card>
<div class="text-center">
<div class="text-3xl font-bold text-green-600">{{ stats.usefulPct }}%</div>
<div class="text-sm text-gray-500 dark:text-gray-400">Useful / Would Act</div>
</div>
</Card>
<Card>
<div class="text-center">
<div class="text-3xl font-bold text-red-600">{{ stats.fpPct }}%</div>
<div class="text-sm text-gray-500 dark:text-gray-400">False Positive</div>
</div>
</Card>
<Card>
<div class="text-center">
<div class="text-3xl font-bold text-amber-600">
{{ stats.total - stats.useful - stats.falsePositive }}
</div>
<div class="text-sm text-gray-500 dark:text-gray-400">Timing Issues</div>
</div>
</Card>
</div>
<!-- Per alert type breakdown -->
<Card>
<h2 class="mb-4 text-lg font-semibold dark:text-white">By Alert Type</h2>
<div class="divide-y divide-gray-200 dark:divide-gray-700">
<div v-for="(feedbacks, alertType) in byAlertType" :key="alertType"
class="flex flex-col gap-2 py-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<span class="text-sm font-medium dark:text-white">{{ alertTypeLabel(alertType) }}</span>
<span class="ml-2 text-xs text-gray-500 dark:text-gray-400">
({{ feedbacks.length }} ratings)
</span>
</div>
<div class="flex flex-wrap gap-2">
<Badge v-if="feedbacks.filter(f => f.rating === 'useful' || f.rating === 'would-act').length"
variant="success" size="xs">
{{ feedbacks.filter(f => f.rating === 'useful' || f.rating === 'would-act').length }} useful
</Badge>
<Badge v-if="feedbacks.filter(f => f.rating === 'false-positive').length"
variant="critical" size="xs">
{{ feedbacks.filter(f => f.rating === 'false-positive').length }} FP
</Badge>
</div>
</div>
</div>
</Card>
<!-- Recent feedback entries -->
<Card>
<h2 class="mb-4 text-lg font-semibold dark:text-white">Recent Feedback</h2>
<div class="divide-y divide-gray-200 dark:divide-gray-700">
<div v-for="entry in [...entries].reverse().slice(0, 20)" :key="entry.alertId"
class="py-4">
<div class="flex flex-wrap items-center gap-2">
<Badge :variant="entry.rating === 'false-positive' ? 'critical' : entry.rating === 'useful' || entry.rating === 'would-act' ? 'success' : 'warning'" size="xs">
{{ entry.rating }}
</Badge>
<span class="text-sm dark:text-white">{{ alertTypeLabel(entry.alertType) }}</span>
<span class="text-xs text-gray-400">{{ new Date(entry.timestamp).toLocaleString() }}</span>
</div>
<p v-if="entry.notes" class="mt-2 text-xs text-gray-500 dark:text-gray-400 line-clamp-2">
{{ entry.notes }}
</p>
</div>
</div>
</Card>
</div>
</template>