Add modular flow step for path-specific triage questions
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
<script setup lang="ts">
|
||||
const modelValue = defineModel<boolean | null>()
|
||||
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
trueLabel?: string
|
||||
falseLabel?: string
|
||||
}>()
|
||||
|
||||
function select(value: boolean) {
|
||||
modelValue.value = value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="boolean-section" aria-labelledby="boolean-title" role="radiogroup">
|
||||
<h2 id="boolean-title">{{ props.title }}</h2>
|
||||
<div class="grid">
|
||||
<button
|
||||
type="button"
|
||||
class="choice-btn"
|
||||
:class="{ active: modelValue === true }"
|
||||
role="radio"
|
||||
:aria-checked="modelValue === true"
|
||||
@click="select(true)"
|
||||
>
|
||||
{{ props.trueLabel ?? 'Ja' }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="choice-btn"
|
||||
:class="{ active: modelValue === false }"
|
||||
role="radio"
|
||||
:aria-checked="modelValue === false"
|
||||
@click="select(false)"
|
||||
>
|
||||
{{ props.falseLabel ?? 'Nein' }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h2 {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.choice-btn {
|
||||
min-height: 56px;
|
||||
padding: 0.9rem 1rem;
|
||||
border-radius: 18px;
|
||||
border: 1px solid #d1d5db;
|
||||
background: #f9fafb;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
cursor: pointer;
|
||||
transition: background 0.12s ease-out, border-color 0.12s ease-out, box-shadow 0.12s ease-out,
|
||||
transform 0.08s ease-out;
|
||||
}
|
||||
|
||||
.choice-btn:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.choice-btn.active {
|
||||
background: #e5f0ff;
|
||||
border-color: #2563eb;
|
||||
color: #0b1f4a;
|
||||
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.25);
|
||||
}
|
||||
|
||||
.choice-btn:focus-visible {
|
||||
outline: 3px solid #2563eb;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -29,23 +29,38 @@ const questions = reactive<Question[]>([])
|
||||
const isLoadingConfig = ref(false)
|
||||
const loadError = ref<string | null>(null)
|
||||
|
||||
const chiefComplaint = ref<string | null>(null)
|
||||
const pain = ref<number>(0)
|
||||
const answers = reactive<Record<string, string | number | boolean | null>>({
|
||||
chief_complaint: null,
|
||||
breathlessness: null,
|
||||
pain_intensity: 0,
|
||||
})
|
||||
|
||||
const isSubmitting = ref(false)
|
||||
const submitError = ref<string | null>(null)
|
||||
const result = ref<MtsPreparation | null>(null)
|
||||
|
||||
const chiefComplaintQuestion = computed(() =>
|
||||
questions.find((q) => q.code === 'chief_complaint') ?? null,
|
||||
)
|
||||
|
||||
const orderedQuestions = computed(() => questions)
|
||||
const chiefComplaintQuestion = computed(() => questions.find((q) => q.code === 'chief_complaint') ?? null)
|
||||
const flowQuestions = computed(() => questions.filter((q) => q.code !== 'chief_complaint' && q.code !== 'pain_intensity'))
|
||||
const painQuestion = computed(() => questions.find((q) => q.code === 'pain_intensity') ?? null)
|
||||
const chiefComplaint = computed({
|
||||
get: () => (typeof answers.chief_complaint === 'string' ? answers.chief_complaint : null),
|
||||
set: (value: string | null) => {
|
||||
answers.chief_complaint = value
|
||||
},
|
||||
})
|
||||
const pain = computed({
|
||||
get: () => (typeof answers.pain_intensity === 'number' ? answers.pain_intensity : 0),
|
||||
set: (value: number) => {
|
||||
answers.pain_intensity = value
|
||||
},
|
||||
})
|
||||
|
||||
const priorityClass = computed(() => {
|
||||
const level = result.value?.suggested_priority_level
|
||||
if (!level) return ''
|
||||
if (level === 'RED_OR_ORANGE') return 'priority-high'
|
||||
if (level === 'YELLOW_OR_ORANGE') return 'priority-medium'
|
||||
return 'priority-default'
|
||||
})
|
||||
|
||||
@@ -53,15 +68,29 @@ const priorityLabel = computed(() => {
|
||||
const level = result.value?.suggested_priority_level
|
||||
if (!level) return ''
|
||||
if (level === 'RED_OR_ORANGE') return 'Hohe Dringlichkeit'
|
||||
if (level === 'YELLOW_OR_ORANGE') return 'Erhöhte Dringlichkeit'
|
||||
return 'Priorität'
|
||||
})
|
||||
|
||||
function resetAnswers() {
|
||||
answers.chief_complaint = null
|
||||
answers.breathlessness = null
|
||||
answers.pain_intensity = 0
|
||||
}
|
||||
|
||||
function setAnswer(questionCode: string, value: string | number | boolean | null) {
|
||||
answers[questionCode] = value
|
||||
}
|
||||
|
||||
function getAnswer(questionCode: string) {
|
||||
return answers[questionCode] ?? null
|
||||
}
|
||||
|
||||
async function loadQuestions() {
|
||||
isLoadingConfig.value = true
|
||||
loadError.value = null
|
||||
questions.splice(0, questions.length)
|
||||
chiefComplaint.value = null
|
||||
pain.value = 0
|
||||
resetAnswers()
|
||||
result.value = null
|
||||
|
||||
try {
|
||||
@@ -88,10 +117,9 @@ async function startSession() {
|
||||
|
||||
const payload = {
|
||||
language: language.value,
|
||||
answers: [
|
||||
{ question_code: 'chief_complaint', value: chiefComplaint.value },
|
||||
{ question_code: 'pain_intensity', value: pain.value },
|
||||
],
|
||||
answers: orderedQuestions.value
|
||||
.filter((q) => answers[q.code] !== null && answers[q.code] !== undefined)
|
||||
.map((q) => ({ question_code: q.code, value: answers[q.code] })),
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -118,10 +146,13 @@ export function useTriageSession() {
|
||||
return {
|
||||
language,
|
||||
questions,
|
||||
orderedQuestions,
|
||||
isLoadingConfig,
|
||||
loadError,
|
||||
answers,
|
||||
chiefComplaint,
|
||||
pain,
|
||||
flowQuestions,
|
||||
isSubmitting,
|
||||
submitError,
|
||||
result,
|
||||
@@ -129,7 +160,10 @@ export function useTriageSession() {
|
||||
painQuestion,
|
||||
priorityClass,
|
||||
priorityLabel,
|
||||
setAnswer,
|
||||
getAnswer,
|
||||
loadQuestions,
|
||||
startSession,
|
||||
resetAnswers,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import LanguageView from '../views/LanguageView.vue'
|
||||
import ComplaintView from '../views/ComplaintView.vue'
|
||||
import FlowView from '../views/FlowView.vue'
|
||||
import PainView from '../views/PainView.vue'
|
||||
import SummaryView from '../views/SummaryView.vue'
|
||||
|
||||
@@ -10,6 +11,7 @@ const router = createRouter({
|
||||
{ path: '/', redirect: '/language' },
|
||||
{ path: '/language', component: LanguageView },
|
||||
{ path: '/complaint', component: ComplaintView },
|
||||
{ path: '/flow', component: FlowView },
|
||||
{ path: '/pain', component: PainView },
|
||||
{ path: '/summary', component: SummaryView },
|
||||
],
|
||||
|
||||
@@ -17,7 +17,7 @@ onMounted(() => {
|
||||
|
||||
function goNext() {
|
||||
if (chiefComplaint.value) {
|
||||
router.push('/pain')
|
||||
router.push('/flow')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import BooleanChoice from '../components/BooleanChoice.vue'
|
||||
import { useTriageSession } from '../composables/useTriageSession'
|
||||
|
||||
const router = useRouter()
|
||||
const { chiefComplaint, flowQuestions, getAnswer, setAnswer } = useTriageSession()
|
||||
|
||||
const isReady = computed(() =>
|
||||
flowQuestions.value.every((q) => getAnswer(q.code) !== null && getAnswer(q.code) !== undefined),
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
if (!chiefComplaint.value) {
|
||||
router.replace('/complaint')
|
||||
}
|
||||
})
|
||||
|
||||
function goNext() {
|
||||
if (isReady.value) {
|
||||
router.push('/pain')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<div class="stack" v-if="flowQuestions.length > 0">
|
||||
<template v-for="question in flowQuestions" :key="question.code">
|
||||
<BooleanChoice
|
||||
v-if="question.type === 'boolean'"
|
||||
:title="question.title"
|
||||
:model-value="(getAnswer(question.code) as boolean | null)"
|
||||
@update:model-value="(value) => setAnswer(question.code, value)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<p v-else class="status">Für diesen Pfad sind noch keine Zusatzfragen konfiguriert.</p>
|
||||
|
||||
<div class="actions">
|
||||
<button class="primary" type="button" :disabled="!isReady && flowQuestions.length > 0" @click="goNext">
|
||||
Weiter
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.stack {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: 1rem;
|
||||
font-size: 0.9rem;
|
||||
color: #4b5563;
|
||||
}
|
||||
</style>
|
||||
@@ -11,7 +11,7 @@ const isReady = computed(() => !!chiefComplaint.value && !!painQuestion.value)
|
||||
|
||||
onMounted(() => {
|
||||
if (!chiefComplaint.value) {
|
||||
router.replace('/complaint')
|
||||
router.replace('/flow')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user