diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ae094a2..b4f4f11 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,7 +8,8 @@ "name": "triage-frontend", "version": "0.1.0", "dependencies": { - "vue": "^3.4.0" + "vue": "^3.4.0", + "vue-router": "^4.6.4" }, "devDependencies": { "@vitejs/plugin-vue": "^5.0.0", @@ -880,6 +881,12 @@ "@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": { "version": "3.5.32", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz", @@ -1217,6 +1224,21 @@ "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" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 1a1faa2..f9f1e6b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,7 +8,8 @@ "preview": "vite preview" }, "dependencies": { - "vue": "^3.4.0" + "vue": "^3.4.0", + "vue-router": "^4.6.4" }, "devDependencies": { "@vitejs/plugin-vue": "^5.0.0", diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 5517033..a4e526a 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,131 +1,5 @@ @@ -263,123 +70,4 @@ h1 { font-size: 0.85rem; 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; -} diff --git a/frontend/src/composables/useTriageSession.ts b/frontend/src/composables/useTriageSession.ts new file mode 100644 index 0000000..cc8db47 --- /dev/null +++ b/frontend/src/composables/useTriageSession.ts @@ -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([]) +const isLoadingConfig = ref(false) +const loadError = ref(null) + +const chiefComplaint = ref(null) +const pain = ref(0) + +const isSubmitting = ref(false) +const submitError = ref(null) +const result = ref(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, + } +} diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 01433bc..efe493a 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -1,4 +1,7 @@ import { createApp } from 'vue' import App from './App.vue' +import router from './router' -createApp(App).mount('#app') +const app = createApp(App) +app.use(router) +app.mount('#app') diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts new file mode 100644 index 0000000..8e2711b --- /dev/null +++ b/frontend/src/router/index.ts @@ -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 diff --git a/frontend/src/views/ComplaintView.vue b/frontend/src/views/ComplaintView.vue new file mode 100644 index 0000000..e8df709 --- /dev/null +++ b/frontend/src/views/ComplaintView.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/frontend/src/views/LanguageView.vue b/frontend/src/views/LanguageView.vue new file mode 100644 index 0000000..2fca9b5 --- /dev/null +++ b/frontend/src/views/LanguageView.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/frontend/src/views/PainView.vue b/frontend/src/views/PainView.vue new file mode 100644 index 0000000..d301858 --- /dev/null +++ b/frontend/src/views/PainView.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/frontend/src/views/SummaryView.vue b/frontend/src/views/SummaryView.vue new file mode 100644 index 0000000..5fb92d6 --- /dev/null +++ b/frontend/src/views/SummaryView.vue @@ -0,0 +1,119 @@ + + + + +