Add initial config-driven triage webapp (backend + frontend)
This commit is contained in:
+179
-14
@@ -1,16 +1,116 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import LanguageSelect from './components/LanguageSelect.vue'
|
||||
import SymptomSelector from './components/SymptomSelector.vue'
|
||||
import PainSlider from './components/PainSlider.vue'
|
||||
|
||||
const language = ref('de')
|
||||
const chiefComplaint = ref<string | null>(null)
|
||||
const pain = ref(0)
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:8000'
|
||||
|
||||
function startSession() {
|
||||
console.log('Start session', { language: language.value, chiefComplaint: chiefComplaint.value, pain: pain.value })
|
||||
// TODO: POST an Backend /sessions
|
||||
interface QuestionOption {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
interface Question {
|
||||
code: string
|
||||
type: string
|
||||
title: string
|
||||
options?: QuestionOption[]
|
||||
min?: number
|
||||
max?: number
|
||||
}
|
||||
|
||||
interface MtsPreparation {
|
||||
session_id: string
|
||||
proposed_presenting_flowchart?: string | null
|
||||
red_flag_indicators: string[]
|
||||
suggested_priority_level?: string | null
|
||||
}
|
||||
|
||||
const language = ref<'de' | 'en' | 'ar'>('de')
|
||||
const questions = ref<Question[]>([])
|
||||
const isLoadingConfig = ref(false)
|
||||
const loadError = ref<string | null>(null)
|
||||
|
||||
const chiefComplaint = ref<string | null>(null)
|
||||
const pain = ref<number>(0)
|
||||
|
||||
const isSubmitting = ref(false)
|
||||
const submitError = ref<string | null>(null)
|
||||
const result = ref<MtsPreparation | null>(null)
|
||||
|
||||
const chiefComplaintQuestion = computed(() =>
|
||||
questions.value.find((q) => q.code === 'chief_complaint') ?? null,
|
||||
)
|
||||
|
||||
const painQuestion = computed(() =>
|
||||
questions.value.find((q) => q.code === 'pain_intensity') ?? null,
|
||||
)
|
||||
|
||||
async function loadQuestions() {
|
||||
isLoadingConfig.value = true
|
||||
loadError.value = null
|
||||
questions.value = []
|
||||
chiefComplaint.value = null
|
||||
pain.value = 0
|
||||
result.value = null
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/questions/${language.value}`)
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`)
|
||||
}
|
||||
const data = (await res.json()) as { language: string; questions: Question[] }
|
||||
questions.value = data.questions
|
||||
} catch (err) {
|
||||
console.error('Failed to load questions', err)
|
||||
loadError.value = 'Konfiguration konnte nicht geladen werden.'
|
||||
} finally {
|
||||
isLoadingConfig.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => language.value,
|
||||
() => {
|
||||
void loadQuestions()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
async function startSession() {
|
||||
if (!chiefComplaint.value) return
|
||||
|
||||
isSubmitting.value = true
|
||||
submitError.value = null
|
||||
result.value = null
|
||||
|
||||
const payload = {
|
||||
language: language.value,
|
||||
answers: [
|
||||
{ question_code: 'chief_complaint', value: chiefComplaint.value },
|
||||
{ question_code: 'pain_intensity', value: pain.value },
|
||||
],
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/sessions`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`)
|
||||
}
|
||||
|
||||
result.value = (await res.json()) as MtsPreparation
|
||||
} catch (err) {
|
||||
console.error('Failed to start session', err)
|
||||
submitError.value = 'Sitzung konnte nicht angelegt werden.'
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -22,17 +122,63 @@ function startSession() {
|
||||
<LanguageSelect v-model="language" />
|
||||
</section>
|
||||
|
||||
<section v-if="language">
|
||||
<SymptomSelector v-model="chiefComplaint" />
|
||||
<section v-if="isLoadingConfig" class="status">
|
||||
Konfiguration wird geladen ...
|
||||
</section>
|
||||
|
||||
<section v-if="chiefComplaint">
|
||||
<PainSlider v-model="pain" />
|
||||
<section v-if="loadError" class="status error">
|
||||
{{ loadError }}
|
||||
</section>
|
||||
|
||||
<section v-if="chiefComplaint">
|
||||
<button class="primary" @click="startSession">Weiter</button>
|
||||
</section>
|
||||
<template v-if="!isLoadingConfig && !loadError">
|
||||
<section v-if="chiefComplaintQuestion">
|
||||
<SymptomSelector
|
||||
v-model="chiefComplaint"
|
||||
:title="chiefComplaintQuestion.title"
|
||||
:options="chiefComplaintQuestion.options ?? []"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section v-if="chiefComplaint && painQuestion">
|
||||
<PainSlider
|
||||
v-model="pain"
|
||||
:title="painQuestion.title"
|
||||
:min="painQuestion.min ?? 0"
|
||||
:max="painQuestion.max ?? 10"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section v-if="chiefComplaint" class="actions">
|
||||
<button class="primary" type="button" :disabled="isSubmitting" @click="startSession">
|
||||
<span v-if="!isSubmitting">Weiter</span>
|
||||
<span v-else>Wird gesendet ...</span>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section v-if="submitError" class="status error">
|
||||
{{ submitError }}
|
||||
</section>
|
||||
|
||||
<section v-if="result" class="result">
|
||||
<h2>Vorbereitung für MTS</h2>
|
||||
<p>
|
||||
Sitzungs-ID:
|
||||
<strong>{{ result.session_id }}</strong>
|
||||
</p>
|
||||
<p v-if="result.proposed_presenting_flowchart">
|
||||
Vorgeschlagenes Flussdiagramm:
|
||||
<strong>{{ result.proposed_presenting_flowchart }}</strong>
|
||||
</p>
|
||||
<p v-if="result.red_flag_indicators.length">
|
||||
Red Flags:
|
||||
<strong>{{ result.red_flag_indicators.join(', ') }}</strong>
|
||||
</p>
|
||||
<p v-if="result.suggested_priority_level">
|
||||
Vorgeschlagene Priorität:
|
||||
<strong>{{ result.suggested_priority_level }}</strong>
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -52,9 +198,28 @@ section {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
button.primary {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.status.error {
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.result {
|
||||
margin-top: 2rem;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
background: #f9fafb;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
const modelValue = defineModel<number>({ default: 0 })
|
||||
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
min: number
|
||||
max: number
|
||||
}>()
|
||||
|
||||
function onInput(ev: Event) {
|
||||
const target = ev.target as HTMLInputElement
|
||||
modelValue.value = Number(target.value)
|
||||
@@ -9,12 +15,12 @@ function onInput(ev: Event) {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2>Wie stark sind die Schmerzen?</h2>
|
||||
<h2>{{ props.title }}</h2>
|
||||
<div class="value">{{ modelValue }}</div>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="10"
|
||||
:min="props.min"
|
||||
:max="props.max"
|
||||
:value="modelValue"
|
||||
@input="onInput"
|
||||
/>
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
const modelValue = defineModel<string | null>()
|
||||
|
||||
const options = [
|
||||
{ code: 'chest_pain', label: 'Brust' },
|
||||
{ code: 'abdominal_pain', label: 'Bauch' },
|
||||
{ code: 'headache', label: 'Kopf' },
|
||||
{ code: 'trauma', label: 'Unfall/Verletzung' },
|
||||
{ code: 'unwell', label: 'Allgemein schlecht' }
|
||||
]
|
||||
interface QuestionOption {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
function select(code: string) {
|
||||
modelValue.value = code
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
options: QuestionOption[]
|
||||
}>()
|
||||
|
||||
function select(value: string) {
|
||||
modelValue.value = value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2>Wo ist das Hauptproblem?</h2>
|
||||
<h2>{{ props.title }}</h2>
|
||||
<div class="grid">
|
||||
<button
|
||||
v-for="opt in options"
|
||||
:key="opt.code"
|
||||
v-for="opt in props.options"
|
||||
:key="opt.value"
|
||||
type="button"
|
||||
class="symptom-btn"
|
||||
:class="{ active: opt.code === modelValue }"
|
||||
@click="select(opt.code)"
|
||||
:class="{ active: opt.value === modelValue }"
|
||||
@click="select(opt.value)"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user