feature: Doctor Feedback Mode
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import Card from '@/components/ui/Card.vue'
|
||||
import Badge from '@/components/ui/Badge.vue'
|
||||
import Button from '@/components/ui/Button.vue'
|
||||
import FeedbackButtons from '@/components/feedback/FeedbackButtons.vue'
|
||||
import { alertTypeLabel } from '@/api/normalize'
|
||||
|
||||
defineProps({
|
||||
@@ -70,5 +71,13 @@ function formatTime(iso) {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 border-t border-gray-100 pt-4 dark:border-gray-700">
|
||||
<FeedbackButtons
|
||||
:alert-id="alert.id"
|
||||
:alert-type="alert.alertType"
|
||||
:severity="alert.severity"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed } from 'vue'
|
||||
import Badge from '@/components/ui/Badge.vue'
|
||||
import Card from '@/components/ui/Card.vue'
|
||||
import FeedbackButtons from '@/components/feedback/FeedbackButtons.vue'
|
||||
|
||||
const props = defineProps({
|
||||
alert: { type: Object, required: true },
|
||||
@@ -76,6 +77,14 @@ function formatMed(med) {
|
||||
<div class="text-xs text-gray-500 dark:text-gray-500">
|
||||
Triggered at {{ new Date(alert.triggeredAt).toLocaleString() }}
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-100 pt-4 dark:border-gray-700">
|
||||
<FeedbackButtons
|
||||
:alert-id="alert.id"
|
||||
:alert-type="alert.alertType"
|
||||
:severity="alert.severity"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useFeedbackStore } from '@/stores/feedback'
|
||||
|
||||
const props = defineProps({
|
||||
alertId: { type: String, required: true },
|
||||
alertType: { type: String, required: true },
|
||||
severity: { type: String, required: true },
|
||||
})
|
||||
|
||||
const feedbackStore = useFeedbackStore()
|
||||
const showNotes = ref(false)
|
||||
const notes = ref('')
|
||||
|
||||
const existing = computed(() => feedbackStore.getFeedback(props.alertId))
|
||||
const selectedRating = computed(() => existing.value?.rating ?? null)
|
||||
|
||||
const ratings = [
|
||||
{ value: 'useful', label: 'Useful', color: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300' },
|
||||
{ value: 'too-early', label: 'Too early', color: 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300' },
|
||||
{ value: 'too-late', label: 'Too late', color: 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300' },
|
||||
{ value: 'false-positive', label: 'False positive', color: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300' },
|
||||
{ value: 'missing-context', label: 'Missing context', color: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300' },
|
||||
{ value: 'would-act', label: 'Would act', color: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300' },
|
||||
]
|
||||
|
||||
function select(rating) {
|
||||
feedbackStore.addFeedback(props.alertId, props.alertType, props.severity, rating, notes.value)
|
||||
}
|
||||
|
||||
function submitNotes() {
|
||||
if (selectedRating.value) {
|
||||
feedbackStore.addFeedback(props.alertId, props.alertType, props.severity, selectedRating.value, notes.value)
|
||||
}
|
||||
showNotes.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-wrap gap-2" role="radiogroup" aria-label="Rate this alert">
|
||||
<button
|
||||
v-for="r in ratings"
|
||||
:key="r.value"
|
||||
role="radio"
|
||||
:aria-checked="selectedRating === r.value"
|
||||
class="rounded-full px-4 py-2 text-xs font-medium transition duration-150 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
:class="[
|
||||
selectedRating === r.value ? r.color : 'bg-gray-100 text-gray-500 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700',
|
||||
selectedRating === r.value ? 'ring-2 ring-current' : '',
|
||||
]"
|
||||
@click.stop="select(r.value)"
|
||||
>
|
||||
{{ r.label }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="selectedRating && !showNotes"
|
||||
class="rounded-full px-4 py-2 text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
@click.stop="showNotes = true"
|
||||
>
|
||||
+ Note
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Transition name="slide">
|
||||
<div v-if="showNotes" class="flex flex-col gap-2 sm:flex-row">
|
||||
<input
|
||||
v-model.trim="notes"
|
||||
type="text"
|
||||
placeholder="Optional note..."
|
||||
class="min-w-0 flex-1 rounded border border-gray-300 px-4 py-2 text-xs dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
||||
@keydown.enter="submitNotes"
|
||||
/>
|
||||
<button
|
||||
class="rounded bg-blue-500 px-4 py-2 text-xs text-white hover:bg-blue-600"
|
||||
@click.stop="submitNotes"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.slide-enter-active,
|
||||
.slide-leave-active { transition: all 0.15s ease; }
|
||||
.slide-enter-from,
|
||||
.slide-leave-to { opacity: 0; transform: translateY(-8px); }
|
||||
</style>
|
||||
@@ -6,6 +6,7 @@ const route = useRoute()
|
||||
const links = [
|
||||
{ to: '/ward', label: 'Virtual Ward', icon: 'ward' },
|
||||
{ to: '/alerts', label: 'Alert Center', icon: 'alerts' },
|
||||
{ to: '/feedback', label: 'Feedback Summary', icon: 'feedback' },
|
||||
]
|
||||
|
||||
function linkClasses(path) {
|
||||
@@ -45,7 +46,7 @@ function linkClasses(path) {
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
v-else-if="link.icon === 'alerts'"
|
||||
class="h-6 w-6 shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -59,6 +60,21 @@ function linkClasses(path) {
|
||||
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
class="h-6 w-6 shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z"
|
||||
/>
|
||||
</svg>
|
||||
{{ link.label }}
|
||||
</RouterLink>
|
||||
</li>
|
||||
|
||||
Reference in New Issue
Block a user