Refactor frontend into routed views with shared triage session state
This commit is contained in:
Generated
+23
-1
@@ -8,7 +8,8 @@
|
|||||||
"name": "triage-frontend",
|
"name": "triage-frontend",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vue": "^3.4.0"
|
"vue": "^3.4.0",
|
||||||
|
"vue-router": "^4.6.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.0.0",
|
"@vitejs/plugin-vue": "^5.0.0",
|
||||||
@@ -880,6 +881,12 @@
|
|||||||
"@vue/shared": "3.5.32"
|
"@vue/shared": "3.5.32"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vue/devtools-api": {
|
||||||
|
"version": "6.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
|
||||||
|
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@vue/reactivity": {
|
"node_modules/@vue/reactivity": {
|
||||||
"version": "3.5.32",
|
"version": "3.5.32",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz",
|
||||||
@@ -1217,6 +1224,21 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vue-router": {
|
||||||
|
"version": "4.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
|
||||||
|
"integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/devtools-api": "^6.6.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/posva"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.5.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vue": "^3.4.0"
|
"vue": "^3.4.0",
|
||||||
|
"vue-router": "^4.6.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.0.0",
|
"@vitejs/plugin-vue": "^5.0.0",
|
||||||
|
|||||||
+2
-314
@@ -1,131 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue'
|
import { RouterView } from 'vue-router'
|
||||||
import LanguageSelect from './components/LanguageSelect.vue'
|
|
||||||
import SymptomSelector from './components/SymptomSelector.vue'
|
|
||||||
import PainSlider from './components/PainSlider.vue'
|
|
||||||
|
|
||||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:8000'
|
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
const priorityClass = computed(() => {
|
|
||||||
const level = result.value?.suggested_priority_level
|
|
||||||
if (!level) return ''
|
|
||||||
if (level === 'RED_OR_ORANGE') return 'priority-high'
|
|
||||||
return 'priority-default'
|
|
||||||
})
|
|
||||||
|
|
||||||
const priorityLabel = computed(() => {
|
|
||||||
const level = result.value?.suggested_priority_level
|
|
||||||
if (!level) return ''
|
|
||||||
if (level === 'RED_OR_ORANGE') return 'Hohe Dringlichkeit'
|
|
||||||
return 'Priorität'
|
|
||||||
})
|
|
||||||
|
|
||||||
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. Bitte prüfen, ob der Server läuft.'
|
|
||||||
} 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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -139,74 +13,7 @@ async function startSession() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="block">
|
<RouterView />
|
||||||
<LanguageSelect v-model="language" />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section v-if="isLoadingConfig" class="status">
|
|
||||||
Konfiguration wird geladen ...
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section v-if="loadError" class="status error">
|
|
||||||
{{ loadError }}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<template v-if="!isLoadingConfig && !loadError">
|
|
||||||
<section v-if="chiefComplaintQuestion" class="block card">
|
|
||||||
<SymptomSelector
|
|
||||||
v-model="chiefComplaint"
|
|
||||||
:title="chiefComplaintQuestion.title"
|
|
||||||
:options="chiefComplaintQuestion.options ?? []"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section v-if="chiefComplaint && painQuestion" class="block card">
|
|
||||||
<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 block" :class="priorityClass">
|
|
||||||
<div class="result-header">
|
|
||||||
<div>
|
|
||||||
<h2>Vorbereitung für MTS</h2>
|
|
||||||
<p class="result-subtitle">Automatisierte Einschätzung anhand der Antworten</p>
|
|
||||||
</div>
|
|
||||||
<p v-if="priorityLabel" class="priority-pill">
|
|
||||||
{{ priorityLabel }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="result-body">
|
|
||||||
<p class="result-line">
|
|
||||||
<span class="label">Sitzungs-ID</span>
|
|
||||||
<span class="value">{{ result.session_id }}</span>
|
|
||||||
</p>
|
|
||||||
<p v-if="result.proposed_presenting_flowchart" class="result-line">
|
|
||||||
<span class="label">Flussdiagramm</span>
|
|
||||||
<span class="value">{{ result.proposed_presenting_flowchart }}</span>
|
|
||||||
</p>
|
|
||||||
<p v-if="result.red_flag_indicators.length" class="result-line">
|
|
||||||
<span class="label">Red Flags</span>
|
|
||||||
<span class="value">{{ result.red_flag_indicators.join(', ') }}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
@@ -263,123 +70,4 @@ h1 {
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
}
|
}
|
||||||
|
|
||||||
.block {
|
|
||||||
margin-top: 1.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 1.1rem 1rem 1rem;
|
|
||||||
border-radius: 20px;
|
|
||||||
background: #f9fafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
margin-top: 1.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.primary {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.9rem 1rem;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
font-weight: 600;
|
|
||||||
border-radius: 999px;
|
|
||||||
border: none;
|
|
||||||
background: linear-gradient(135deg, #007aff, #0b84ff);
|
|
||||||
color: #ffffff;
|
|
||||||
box-shadow: 0 10px 20px rgba(0, 122, 255, 0.35);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: transform 0.08s ease-out, box-shadow 0.08s ease-out, opacity 0.15s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.primary:hover:enabled {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 14px 26px rgba(0, 122, 255, 0.45);
|
|
||||||
}
|
|
||||||
|
|
||||||
button.primary:active:enabled {
|
|
||||||
transform: translateY(0);
|
|
||||||
box-shadow: 0 6px 14px rgba(0, 122, 255, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
button.primary:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: default;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
margin-top: 1rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #4b5563;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status.error {
|
|
||||||
color: #b91c1c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result {
|
|
||||||
margin-top: 1.8rem;
|
|
||||||
padding: 1.2rem 1rem 1rem;
|
|
||||||
border-radius: 20px;
|
|
||||||
background: #f9fafb;
|
|
||||||
border-left: 4px solid #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result.priority-high {
|
|
||||||
background: #fef2f2;
|
|
||||||
border-left-color: #ff3b30;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result.priority-default {
|
|
||||||
background: #eff6ff;
|
|
||||||
border-left-color: #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result h2 {
|
|
||||||
font-size: 1.05rem;
|
|
||||||
margin-bottom: 0.15rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-subtitle {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
.priority-pill {
|
|
||||||
padding: 0.3rem 0.7rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
background: #fee2e2;
|
|
||||||
color: #b91c1c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-body {
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-line {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: baseline;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-line .label {
|
|
||||||
color: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-line .value {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { computed, reactive, ref } from 'vue'
|
||||||
|
|
||||||
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:8000'
|
||||||
|
|
||||||
|
export interface QuestionOption {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Question {
|
||||||
|
code: string
|
||||||
|
type: string
|
||||||
|
title: string
|
||||||
|
options?: QuestionOption[]
|
||||||
|
min?: number
|
||||||
|
max?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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 = reactive<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.find((q) => q.code === 'chief_complaint') ?? null,
|
||||||
|
)
|
||||||
|
|
||||||
|
const painQuestion = computed(() => questions.find((q) => q.code === 'pain_intensity') ?? null)
|
||||||
|
|
||||||
|
const priorityClass = computed(() => {
|
||||||
|
const level = result.value?.suggested_priority_level
|
||||||
|
if (!level) return ''
|
||||||
|
if (level === 'RED_OR_ORANGE') return 'priority-high'
|
||||||
|
return 'priority-default'
|
||||||
|
})
|
||||||
|
|
||||||
|
const priorityLabel = computed(() => {
|
||||||
|
const level = result.value?.suggested_priority_level
|
||||||
|
if (!level) return ''
|
||||||
|
if (level === 'RED_OR_ORANGE') return 'Hohe Dringlichkeit'
|
||||||
|
return 'Priorität'
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadQuestions() {
|
||||||
|
isLoadingConfig.value = true
|
||||||
|
loadError.value = null
|
||||||
|
questions.splice(0, questions.length)
|
||||||
|
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.push(...data.questions)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load questions', err)
|
||||||
|
loadError.value = 'Konfiguration konnte nicht geladen werden. Bitte prüfen, ob der Server läuft.'
|
||||||
|
} finally {
|
||||||
|
isLoadingConfig.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTriageSession() {
|
||||||
|
return {
|
||||||
|
language,
|
||||||
|
questions,
|
||||||
|
isLoadingConfig,
|
||||||
|
loadError,
|
||||||
|
chiefComplaint,
|
||||||
|
pain,
|
||||||
|
isSubmitting,
|
||||||
|
submitError,
|
||||||
|
result,
|
||||||
|
chiefComplaintQuestion,
|
||||||
|
painQuestion,
|
||||||
|
priorityClass,
|
||||||
|
priorityLabel,
|
||||||
|
loadQuestions,
|
||||||
|
startSession,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
|
||||||
createApp(App).mount('#app')
|
const app = createApp(App)
|
||||||
|
app.use(router)
|
||||||
|
app.mount('#app')
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import LanguageView from '../views/LanguageView.vue'
|
||||||
|
import ComplaintView from '../views/ComplaintView.vue'
|
||||||
|
import PainView from '../views/PainView.vue'
|
||||||
|
import SummaryView from '../views/SummaryView.vue'
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes: [
|
||||||
|
{ path: '/', redirect: '/language' },
|
||||||
|
{ path: '/language', component: LanguageView },
|
||||||
|
{ path: '/complaint', component: ComplaintView },
|
||||||
|
{ path: '/pain', component: PainView },
|
||||||
|
{ path: '/summary', component: SummaryView },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import SymptomSelector from '../components/SymptomSelector.vue'
|
||||||
|
import { useTriageSession } from '../composables/useTriageSession'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const { chiefComplaint, chiefComplaintQuestion, questions } = useTriageSession()
|
||||||
|
|
||||||
|
const hasConfig = computed(() => questions.length > 0)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!hasConfig.value) {
|
||||||
|
router.replace('/language')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function goNext() {
|
||||||
|
if (chiefComplaint.value) {
|
||||||
|
router.push('/pain')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section v-if="chiefComplaintQuestion">
|
||||||
|
<SymptomSelector
|
||||||
|
v-model="chiefComplaint"
|
||||||
|
:title="chiefComplaintQuestion.title"
|
||||||
|
:options="chiefComplaintQuestion.options ?? []"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button class="primary" type="button" :disabled="!chiefComplaint" @click="goNext">
|
||||||
|
Weiter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<p v-else class="status">Keine Konfiguration für Beschwerden gefunden.</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.actions {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
margin-top: 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { watch } from 'vue'
|
||||||
|
import LanguageSelect from '../components/LanguageSelect.vue'
|
||||||
|
import { useTriageSession } from '../composables/useTriageSession'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const { language, isLoadingConfig, loadError, loadQuestions } = useTriageSession()
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => language.value,
|
||||||
|
() => {
|
||||||
|
void loadQuestions()
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
function goNext() {
|
||||||
|
if (!isLoadingConfig.value && !loadError.value) {
|
||||||
|
router.push('/complaint')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section>
|
||||||
|
<LanguageSelect v-model="language" />
|
||||||
|
|
||||||
|
<p v-if="isLoadingConfig" class="status">Konfiguration wird geladen ...</p>
|
||||||
|
<p v-if="loadError" class="status error">{{ loadError }}</p>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button class="primary" type="button" :disabled="!!loadError || isLoadingConfig" @click="goNext">
|
||||||
|
Weiter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.status {
|
||||||
|
margin-top: 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.error {
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import PainSlider from '../components/PainSlider.vue'
|
||||||
|
import { useTriageSession } from '../composables/useTriageSession'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const { pain, painQuestion, chiefComplaint, startSession, isSubmitting, submitError } = useTriageSession()
|
||||||
|
|
||||||
|
const isReady = computed(() => !!chiefComplaint.value && !!painQuestion.value)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!chiefComplaint.value) {
|
||||||
|
router.replace('/complaint')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function goNext() {
|
||||||
|
await startSession()
|
||||||
|
router.push('/summary')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section v-if="painQuestion">
|
||||||
|
<PainSlider
|
||||||
|
v-model="pain"
|
||||||
|
:title="painQuestion.title"
|
||||||
|
:min="painQuestion.min ?? 0"
|
||||||
|
:max="painQuestion.max ?? 10"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p v-if="submitError" class="status error">{{ submitError }}</p>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button class="primary" type="button" :disabled="!isReady || isSubmitting" @click="goNext">
|
||||||
|
<span v-if="!isSubmitting">Weiter</span>
|
||||||
|
<span v-else>Wird gesendet ...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<p v-else class="status">Keine Konfiguration für Schmerz-Frage gefunden.</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.actions {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
margin-top: 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.error {
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useTriageSession } from '../composables/useTriageSession'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const { result, priorityClass, priorityLabel } = useTriageSession()
|
||||||
|
|
||||||
|
const hasResult = computed(() => !!result.value)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!hasResult.value) {
|
||||||
|
router.replace('/language')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section v-if="hasResult" class="result" :class="priorityClass">
|
||||||
|
<div class="result-header">
|
||||||
|
<div>
|
||||||
|
<h2>Vorbereitung für MTS</h2>
|
||||||
|
<p class="result-subtitle">Automatisierte Einschätzung anhand der Antworten</p>
|
||||||
|
</div>
|
||||||
|
<p v-if="priorityLabel" class="priority-pill">
|
||||||
|
{{ priorityLabel }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-body">
|
||||||
|
<p class="result-line">
|
||||||
|
<span class="label">Sitzungs-ID</span>
|
||||||
|
<span class="value">{{ result!.session_id }}</span>
|
||||||
|
</p>
|
||||||
|
<p v-if="result!.proposed_presenting_flowchart" class="result-line">
|
||||||
|
<span class="label">Flussdiagramm</span>
|
||||||
|
<span class="value">{{ result!.proposed_presenting_flowchart }}</span>
|
||||||
|
</p>
|
||||||
|
<p v-if="result!.red_flag_indicators.length" class="result-line">
|
||||||
|
<span class="label">Red Flags</span>
|
||||||
|
<span class="value">{{ result!.red_flag_indicators.join(', ') }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button class="primary" type="button" @click="router.push('/language')">
|
||||||
|
Neue Erfassung starten
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<p v-else class="status">Kein Ergebnis vorhanden.</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.result {
|
||||||
|
padding: 1.2rem 1rem 1rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: #f9fafb;
|
||||||
|
border-left: 4px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result h2 {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
margin-bottom: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-subtitle {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.priority-pill {
|
||||||
|
padding: 0.3rem 0.7rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-body {
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-line {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-line .label {
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-line .value {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
margin-top: 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user