diff --git a/backend/src/index.js b/backend/src/index.js index 44fddab..8eb1a17 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -12,6 +12,8 @@ const swaggerUI = require('swagger-ui-express'); const swaggerJsDoc = require('swagger-jsdoc'); const authRoutes = require('./routes/auth'); +const triageRoutes = require('./routes/triage'); +const bankidRoutes = require('./routes/bankid'); const fileRoutes = require('./routes/file'); const searchRoutes = require('./routes/search'); const sqlRoutes = require('./routes/sql'); @@ -102,6 +104,8 @@ require('./auth/auth'); app.use(bodyParser.json()); app.use('/api/auth', authRoutes); +app.use('/api/bankid', bankidRoutes); +app.use('/api/triage', triageRoutes); app.use('/api/file', fileRoutes); app.use('/api/pexels', pexelsRoutes); app.enable('trust proxy'); diff --git a/backend/src/routes/bankid.js b/backend/src/routes/bankid.js new file mode 100644 index 0000000..a7258d5 --- /dev/null +++ b/backend/src/routes/bankid.js @@ -0,0 +1,87 @@ +const express = require('express'); +const router = express.Router(); +const helpers = require('../helpers'); +const UsersDBApi = require('../db/api/users'); +const PatientsDBApi = require('../db/api/patients'); +const wrapAsync = require('../helpers').wrapAsync; + +const sessions = new Map(); + +router.post('/initiate', wrapAsync(async (req, res) => { + const { personalNumber } = req.body; + if (!personalNumber) { + return res.status(400).send({ message: 'Personal number is required' }); + } + const orderRef = Math.random().toString(36).substring(7); + sessions.set(orderRef, { personalNumber, status: 'pending', createdAt: Date.now() }); + res.status(200).send({ orderRef, message: 'Auth initiated. Please sign in your BankID app.' }); +})); + +router.post('/status', wrapAsync(async (req, res) => { + const { orderRef } = req.body; + const session = sessions.get(orderRef); + if (!session) { + return res.status(404).send({ message: 'Session not found' }); + } + + // Simulate progress + const elapsed = Date.now() - session.createdAt; + if (elapsed > 1000) { // Reduced from 3000 to 1000 for faster feel + session.status = 'complete'; + } + + res.status(200).send({ status: session.status }); +})); + +router.post('/verify', wrapAsync(async (req, res) => { + const { orderRef } = req.body; + const session = sessions.get(orderRef); + if (!session || session.status !== 'complete') { + return res.status(400).send({ message: 'Auth not complete' }); + } + + const { personalNumber } = session; + + // 1. Try to find patient by personal_number + const patient = await PatientsDBApi.findBy({ personal_number: personalNumber }); + let user; + + if (patient && patient.user) { + user = await UsersDBApi.findBy({ id: patient.user.id }); + } else { + // 2. Try to find user by mock email as fallback + user = await UsersDBApi.findBy({ email: `${personalNumber}@bankid.sim` }); + } + + if (!user) { + // Create mock user if not found + user = await UsersDBApi.createFromAuth({ + firstName: 'Patient', + lastName: personalNumber, + email: `${personalNumber}@bankid.sim`, + password: 'mock_password', + phoneNumber: personalNumber, + }); + + // Also create a patient record if it didn't exist + if (!patient) { + await PatientsDBApi.create({ + display_name: `Patient ${personalNumber}`, + personal_number: personalNumber, + user: user.id + }, { currentUser: user }); + } + } + + const token = helpers.jwtSign({ + user: { + id: user.id, + email: user.email + } + }); + + sessions.delete(orderRef); + res.status(200).send({ token, user }); +})); + +module.exports = router; \ No newline at end of file diff --git a/backend/src/routes/triage.js b/backend/src/routes/triage.js new file mode 100644 index 0000000..46cb5bb --- /dev/null +++ b/backend/src/routes/triage.js @@ -0,0 +1,17 @@ +const express = require('express'); +const router = express.Router(); +const TriageService = require('../services/triage'); +const { wrapAsync } = require('../helpers'); +const passport = require('passport'); + +router.post('/process', passport.authenticate('jwt', { session: false }), wrapAsync(async (req, res) => { + const { symptomText, fullName, phoneNumber } = req.body; + if (!symptomText) { + return res.status(400).send({ message: 'Symptom text is required' }); + } + + const result = await TriageService.performTriage(symptomText, req.currentUser, { fullName, phoneNumber }); + res.status(200).send(result); +})); + +module.exports = router; \ No newline at end of file diff --git a/backend/src/routes/visits.js b/backend/src/routes/visits.js index 158a192..4d6e47f 100644 --- a/backend/src/routes/visits.js +++ b/backend/src/routes/visits.js @@ -1,4 +1,3 @@ - const express = require('express'); const VisitsService = require('../services/visits'); @@ -19,6 +18,10 @@ const { router.use(checkCrudPermissions('visits')); +router.post('/:id/call', wrapAsync(async (req, res) => { + const result = await VisitsService.callPatient(req.params.id, req.currentUser); + res.status(200).send(result); +})); /** * @swagger @@ -445,4 +448,4 @@ router.get('/:id', wrapAsync(async (req, res) => { router.use('/', require('../helpers').commonErrorHandler); -module.exports = router; +module.exports = router; \ No newline at end of file diff --git a/backend/src/services/triage.js b/backend/src/services/triage.js new file mode 100644 index 0000000..3fbcf8c --- /dev/null +++ b/backend/src/services/triage.js @@ -0,0 +1,181 @@ +const db = require('../db/models'); +const { LocalAIApi } = require('../ai/LocalAIApi'); + +class TriageService { + static async performTriage(symptomText, currentUser, data = {}) { + const { fullName, phoneNumber, extraData = {} } = data; + const transaction = await db.sequelize.transaction(); + try { + // 1. Update User info if provided + if (fullName || phoneNumber) { + const names = (fullName || "").split(" "); + const firstName = names[0] || currentUser.firstName; + const lastName = names.slice(1).join(" ") || currentUser.lastName; + + await db.users.update({ + firstName, + lastName, + phoneNumber: phoneNumber || currentUser.phoneNumber + }, { + where: { id: currentUser.id }, + transaction + }); + } + + // 2. Get or create patient record for user + let patient = await db.patients.findOne({ where: { userId: currentUser.id }, transaction }); + + const patientData = { + display_name: fullName || (currentUser.firstName + ' ' + currentUser.lastName), + userId: currentUser.id + }; + + if (!patient) { + patientData.personal_number = currentUser.email.split('@')[0]; + patient = await db.patients.create(patientData, { transaction }); + } else { + await patient.update({ display_name: patientData.display_name }, { transaction }); + } + + // 3. AI Triage with detailed info + const fullContext = { + symptoms: symptomText, + ...extraData + }; + + let triageResult = await this.callAI(fullContext); + let aiFallback = false; + + if (!triageResult) { + triageResult = this.ruleBasedFallback(fullContext); + aiFallback = true; + } + + const levelStr = triageResult.level.toString(); + + // 4. Create Visit + const visit = await db.visits.create({ + patientId: patient.id, + symptom_text: symptomText, + triage_level: levelStr, + priority_score: triageResult.score, + status: 'waiting', + ai_fallback: aiFallback, + created_ts: new Date() + }, { transaction }); + + // 5. Create AI Analysis record + await db.ai_analyses.create({ + visitId: visit.id, + raw_output: JSON.stringify(triageResult), + analysis_type: 'triage', + result_level: levelStr + }, { transaction }); + + // 6. Calculate dynamic wait time + const waitTime = await this.calculateWaitTime(triageResult.level, transaction); + + // 7. Add to Queue + await db.queue.create({ + visitId: visit.id, + triage_level: levelStr, + waiting_since: new Date(), + label: `Patient ${patient.display_name}`, + estimated_wait_minutes: waitTime + }, { transaction }); + + await transaction.commit(); + return { visit, triageResult }; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async callAI(context) { + try { + const prompt = ` + You are a highly experienced medical triage specialist in a Swedish Emergency Room. + Analyze the following patient data to determine the triage level (1-5). + + LEVEL DEFINITIONS: + 1: Red - Life-threatening. Immediate intervention required. + 2: Orange - Very urgent. High risk of deterioration. + 3: Yellow - Urgent. Stable but needs investigation. + 4: Green - Standard. Non-urgent, but requires care. + 5: Blue - Non-urgent. Could be handled by primary care. + + PATIENT DATA: + - Main Symptoms: ${context.symptoms} + - Pain Level (0-10): ${context.painLevel} + - Onset: ${context.onset} + - Selected Categories: ${context.selectedSymptoms?.join(', ')} + - Medical History: ${context.medicalHistory} + - Medications: ${context.medications} + - Allergies: ${context.allergies} + + Return ONLY a JSON object: + { + "level": number (1-5), + "reasoning": "Short Swedish explanation (max 30 words)", + "score": number (0-100, where 100 is most urgent), + "advice": "Short Swedish advice for the patient while waiting" + } + `; + + const resp = await LocalAIApi.createResponse({ + input: [ + { role: "system", content: "You are a professional medical triage AI." }, + { role: "user", content: prompt } + ] + }, { poll_interval: 2, poll_timeout: 45 }); + + if (resp.success) { + return LocalAIApi.decodeJsonFromResponse(resp); + } + } catch (err) { + console.error("AI Triage failed:", err); + } + return null; + } + + static ruleBasedFallback(context) { + const text = (context.symptoms || "").toLowerCase(); + const pain = context.painLevel || 0; + + // Critical signs + if (text.includes('medvetslös') || text.includes('andas inte') || text.includes('stopp')) { + return { level: 1, reasoning: "Livshotande tillstånd identifierat (Fallback)", score: 100 }; + } + + if (text.includes('bröst') || text.includes('andas') || text.includes('hjärta') || pain >= 9) { + return { level: 2, reasoning: "Potentiellt allvarligt (Fallback)", score: 85 }; + } + + if (text.includes('blöd') || text.includes('fraktur') || pain >= 7) { + return { level: 3, reasoning: "Urgent behov av vård (Fallback)", score: 65 }; + } + + if (pain >= 4 || text.includes('feber')) { + return { level: 4, reasoning: "Stabil men behöver bedömning (Fallback)", score: 45 }; + } + + return { level: 5, reasoning: "Mindre brådskande (Fallback)", score: 25 }; + } + + static async calculateWaitTime(level, transaction) { + const base = { 1: 0, 2: 20, 3: 90, 4: 180, 5: 300 }; + + const count = await db.queue.count({ + where: { + triage_level: { [db.Sequelize.Op.lte]: level.toString() }, + called: false + }, + transaction + }); + + return (base[level] || 120) + (count * 15); + } +} + +module.exports = TriageService; \ No newline at end of file diff --git a/backend/src/services/visits.js b/backend/src/services/visits.js index 2ec90f6..16afb69 100644 --- a/backend/src/services/visits.js +++ b/backend/src/services/visits.js @@ -7,10 +7,6 @@ const axios = require('axios'); const config = require('../config'); const stream = require('stream'); - - - - module.exports = class VisitsService { static async create(data, currentUser) { const transaction = await db.sequelize.transaction(); @@ -30,6 +26,37 @@ module.exports = class VisitsService { } }; + static async callPatient(id, currentUser) { + console.log('Calling patient with visit ID:', id); + const transaction = await db.sequelize.transaction(); + try { + if (!id || id === 'undefined') { + throw new ValidationError('visitNotFound'); + } + + const visit = await db.visits.findByPk(id, { transaction }); + if (!visit) { + console.error('Visit not found for ID:', id); + throw new ValidationError('visitNotFound'); + } + + // Update visit status + await visit.update({ status: 'in_consultation' }, { transaction }); + + // Update queue entry + await db.queue.update( + { called: true }, + { where: { visitId: id }, transaction } + ); + + await transaction.commit(); + return visit; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + static async bulkImport(req, res, sendInvitationEmails = true, host) { const transaction = await db.sequelize.transaction(); @@ -131,8 +158,4 @@ module.exports = class VisitsService { throw error; } } - - -}; - - +}; \ No newline at end of file diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index d70f2ee..f3ef662 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -7,7 +7,12 @@ const menuAside: MenuAsideItem[] = [ icon: icon.mdiViewDashboardOutline, label: 'Dashboard', }, - + { + href: '/staff/queue', + label: 'Patientkö (Personal)', + icon: icon.mdiAccountMultiple, + permissions: 'READ_QUEUE' + }, { href: '/users/users-list', label: 'Users', @@ -128,4 +133,4 @@ const menuAside: MenuAsideItem[] = [ }, ] -export default menuAside +export default menuAside; \ No newline at end of file diff --git a/frontend/src/pages/bankid-login.tsx b/frontend/src/pages/bankid-login.tsx new file mode 100644 index 0000000..4c7ad05 --- /dev/null +++ b/frontend/src/pages/bankid-login.tsx @@ -0,0 +1,212 @@ +import React, { useState, useEffect } from 'react'; +import type { ReactElement } from 'react'; +import Head from 'next/head'; +import { useRouter } from 'next/router'; +import axios from 'axios'; +import CardBox from '../components/CardBox'; +import SectionFullScreen from '../components/SectionFullScreen'; +import LayoutGuest from '../layouts/Guest'; +import FormField from '../components/FormField'; +import BaseButton from '../components/BaseButton'; +import BaseButtons from '../components/BaseButtons'; +import { getPageTitle } from '../config'; +import LoadingSpinner from '../components/LoadingSpinner'; +import { mdiFingerprint, mdiShieldCheck, mdiAlertCircle, mdiClockFast } from '@mdi/js'; +import BaseIcon from '../components/BaseIcon'; + +export default function BankIDLogin() { + const [personalNumber, setPersonalNumber] = useState(''); + const [loading, setLoading] = useState(false); + const [status, setStatus] = useState('idle'); // idle, initiated, completed, error, timeout + const [orderRef, setOrderRef] = useState(null); + const [securityCode, setSecurityCode] = useState(''); + const [countdown, setCountdown] = useState(60); + const router = useRouter(); + + const handleInitiate = async () => { + // Basic validation for Swedish personal number + const pn = personalNumber.replace(/[^0-9]/g, ''); + if (pn.length !== 10 && pn.length !== 12) { + alert('Vänligen ange ett giltigt personnummer (10 eller 12 siffror)'); + return; + } + + setLoading(true); + try { + const response = await axios.post('/bankid/initiate', { personalNumber }); + setOrderRef(response.data.orderRef); + setSecurityCode(Math.floor(1000 + Math.random() * 9000).toString()); + setStatus('initiated'); + setCountdown(60); + } catch (error) { + console.error(error); + setStatus('error'); + } finally { + setLoading(false); + } + }; + + const handleCancel = () => { + setStatus('idle'); + setOrderRef(null); + }; + + useEffect(() => { + let interval; + if (status === 'initiated' && orderRef) { + interval = setInterval(async () => { + setCountdown(prev => { + if (prev <= 1) { + setStatus('timeout'); + clearInterval(interval); + return 0; + } + return prev - 1; + }); + + try { + const response = await axios.post('/bankid/status', { orderRef }); + if (response.data.status === 'complete') { + setStatus('completed'); + clearInterval(interval); + } + } catch (error) { + console.error(error); + setStatus('error'); + clearInterval(interval); + } + }, 1000); + } + return () => clearInterval(interval); + }, [status, orderRef]); + + useEffect(() => { + if (status === 'completed' && orderRef) { + const verify = async () => { + try { + const response = await axios.post('/bankid/verify', { orderRef }); + const { token, user } = response.data; + localStorage.setItem('token', token); + localStorage.setItem('user', JSON.stringify(user)); + axios.defaults.headers.common['Authorization'] = `Bearer ${token}`; + router.push('/patient/new-visit'); + } catch (error) { + console.error(error); + setStatus('error'); + } + }; + verify(); + } + }, [status, orderRef, router]); + + return ( + <> + + {getPageTitle('Logga in - Akuten Online')} + + + +
+
+
+ +
+

Akuten Online

+

Trygg och säker identifiering

+
+ + + {status === 'idle' && ( +
+
+ +

+ Identifiera dig med BankID för att hämta din journal och påbörja din triagering. +

+
+ + + setPersonalNumber(e.target.value)} + placeholder="19900101-1234" + /> + + + +
+ )} + + {(status === 'initiated' || status === 'completed') && ( +
+
+ +
+ {countdown}s +
+
+ +
+

Öppna BankID-appen

+

+ Kontrollera att säkerhetskoden i appen stämmer överens: +

+
+ {securityCode} +
+
+ + +
+ )} + + {(status === 'error' || status === 'timeout') && ( +
+
+ +
+
+

+ {status === 'timeout' ? 'Tiden har gått ut' : 'Något gick fel'} +

+

+ Inloggningen kunde inte slutföras. Vänligen kontrollera din anslutning och försök igen. +

+
+ setStatus('idle')} + /> +
+ )} +
+ +
+ +

+ Snabb & Säker Digital Triage +

+
+
+
+ + ); +} + +BankIDLogin.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; \ No newline at end of file diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 3a72312..3e46e7f 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -1,166 +1,177 @@ - -import React, { useEffect, useState } from 'react'; +import React from 'react'; import type { ReactElement } from 'react'; import Head from 'next/head'; import Link from 'next/link'; import BaseButton from '../components/BaseButton'; -import CardBox from '../components/CardBox'; import SectionFullScreen from '../components/SectionFullScreen'; import LayoutGuest from '../layouts/Guest'; -import BaseDivider from '../components/BaseDivider'; -import BaseButtons from '../components/BaseButtons'; import { getPageTitle } from '../config'; -import { useAppSelector } from '../stores/hooks'; -import CardBoxComponentTitle from "../components/CardBoxComponentTitle"; -import { getPexelsImage, getPexelsVideo } from '../helpers/pexels'; +import { mdiHospitalMarker, mdiAccountCheck, mdiClockOutline } from '@mdi/js'; +import BaseIcon from '../components/BaseIcon'; - -export default function Starter() { - const [illustrationImage, setIllustrationImage] = useState({ - src: undefined, - photographer: undefined, - photographer_url: undefined, - }) - const [illustrationVideo, setIllustrationVideo] = useState({video_files: []}) - const [contentType, setContentType] = useState('image'); - const [contentPosition, setContentPosition] = useState('left'); - const textColor = useAppSelector((state) => state.style.linkColor); - - const title = 'App Draft' - - // Fetch Pexels image/video - useEffect(() => { - async function fetchData() { - const image = await getPexelsImage(); - const video = await getPexelsVideo(); - setIllustrationImage(image); - setIllustrationVideo(video); - } - fetchData(); - }, []); - - const imageBlock = (image) => ( -
-
- - Photo by {image?.photographer} on Pexels - -
-
- ); - - const videoBlock = (video) => { - if (video?.video_files?.length > 0) { - return ( -
- -
- - Video by {video.user.name} on Pexels - -
-
) - } - }; +export default function LandingPage() { + const primaryColor = '#004B87'; // Swedish Health Blue return ( -
+
- {getPageTitle('Starter Page')} + {getPageTitle('Akuten Online - Digital Akutmottagning')} - -
- {contentType === 'image' && contentPosition !== 'background' - ? imageBlock(illustrationImage) - : null} - {contentType === 'video' && contentPosition !== 'background' - ? videoBlock(illustrationVideo) - : null} -
- - - -
-

This is a React.js/Node.js app generated by the Flatlogic Web App Generator

-

For guides and documentation please check - your local README.md and the Flatlogic documentation

-
- - - - - -
+ {/* Navigation */} +
- -
-

© 2026 {title}. All rights reserved

- - Privacy Policy - -
+
+ Om tjänsten + Hur det fungerar + Personalinloggning +
+ + + {/* Hero Section */} +
+
+
+
+ Digital triagering dygnet runt +
+

+ Sök vård digitalt
+ minska väntetiden. +

+

+ Vår AI-baserade plattform hjälper dig att snabbt få rätt vård. Genom att triagera dig hemma minskar vi trycket på akuten och ger dig en estimerad köplats direkt. +

+
+ + +
+
+
+ {[1, 2, 3, 4].map((i) => ( +
+ user +
+ ))} +
+

Över 10,000 patienter hjälpta denna månad

+
+
+ +
+ {/* Decorative Elements */} +
+
+ +
+
+
+
+
+

Status

+

Patient Triage

+
+
+
LIVE
+
+
+
+
+
+
+ Estimerad väntetid + 12 min +
+
+

"Symtom: Tryck över bröstet..."

+

Prioritet: Akut (Nivå 2)

+
+
+
+
+
+
+ + {/* Features */} +
+
+
+

Varför använda Akuten Online?

+

Vårt system är utvecklat för att optimera patientflödet och säkerställa att de mest akuta fallen får hjälp först.

+
+ +
+ {[ + { + title: 'AI-prioritering', + desc: 'Vår smarta algoritm analyserar dina symtom och riskfaktorer för att ge dig rätt prioritet direkt.', + icon: mdiAccountCheck, + color: 'bg-blue-100 text-[#004B87]' + }, + { + title: 'Realtidskö', + desc: 'Se din exakta plats i kön och få en estimerad väntetid baserat på personalens belastning.', + icon: mdiClockOutline, + color: 'bg-green-100 text-green-600' + }, + { + title: 'Minskad smittrisk', + desc: 'Vänta i hemmiljö istället för i ett fullsatt väntrum och bli kallad precis när det är din tur.', + icon: mdiHospitalMarker, + color: 'bg-red-100 text-red-600' + } + ].map((feature, i) => ( +
+
+ +
+

{feature.title}

+

{feature.desc}

+
+ ))} +
+
+
+ + {/* Footer */} +
+
+
+
+ + +
+ Akuten Online +
+
+ © 2026 Akuten Online (Flatlogic Demo). Simulation för utbildningsbruk. +
+
+ Integritetspolicy + Villkor + Logga in (Personal) +
+
+
); } -Starter.getLayout = function getLayout(page: ReactElement) { +LandingPage.getLayout = function getLayout(page: ReactElement) { return {page}; -}; - +}; \ No newline at end of file diff --git a/frontend/src/pages/patient/new-visit.tsx b/frontend/src/pages/patient/new-visit.tsx new file mode 100644 index 0000000..b9feb28 --- /dev/null +++ b/frontend/src/pages/patient/new-visit.tsx @@ -0,0 +1,278 @@ +import React, { useState, useEffect } from 'react'; +import type { ReactElement } from 'react'; +import Head from 'next/head'; +import { useRouter } from 'next/router'; +import axios from 'axios'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import { mdiMedicalBag, mdiArrowRight, mdiAccount, mdiPhone, mdiHeartPulse, mdiHistory, mdiCheck, mdiChevronLeft, mdiAlertCircle } from '@mdi/js'; +import BaseButton from '../../components/BaseButton'; +import FormField from '../../components/FormField'; +import LoadingSpinner from '../../components/LoadingSpinner'; +import { useAppSelector } from '../../stores/hooks'; +import BaseIcon from '../../components/BaseIcon'; + +const NewVisitPage = () => { + const { currentUser } = useAppSelector((state) => state.auth); + const [step, setStep] = useState(1); + const [loading, setLoading] = useState(false); + const router = useRouter(); + + // Step 1: Identity + const [fullName, setFullName] = useState(''); + const [phoneNumber, setPhoneNumber] = useState(''); + + // Step 2: Symptoms + const [symptomText, setSymptomText] = useState(''); + const [painLevel, setPainLevel] = useState(0); + + // Step 3: Details + const [onset, setOnset] = useState(''); // When did it start? + const [selectedSymptoms, setSelectedSymptoms] = useState([]); + + // Step 4: History + const [medicalHistory, setMedicalHistory] = useState(''); + const [medications, setMedications] = useState(''); + const [allergies, setAllergies] = useState(''); + + const commonSymptoms = [ + 'Feber', 'Hosta', 'Andnöd', 'Bröstsmärta', 'Huvudvärk', 'Illamående', 'Buksmärta', 'Yrsel' + ]; + + useEffect(() => { + if (currentUser) { + setFullName(`${currentUser.firstName || ''} ${currentUser.lastName || ''}`.trim()); + setPhoneNumber(currentUser.phoneNumber || ''); + } + }, [currentUser]); + + const toggleSymptom = (s: string) => { + setSelectedSymptoms(prev => + prev.includes(s) ? prev.filter(x => x !== s) : [...prev, s] + ); + }; + + const handleSubmit = async () => { + setLoading(true); + try { + const response = await axios.post('/triage/process', { + symptomText, + fullName, + phoneNumber, + extraData: { + painLevel, + onset, + selectedSymptoms, + medicalHistory, + medications, + allergies + } + }); + const { visit } = response.data; + router.push(`/patient/waiting-room?id=${visit.id}`); + } catch (error) { + console.error(error); + alert('Ett fel uppstod vid bearbetning. Försök igen.'); + } finally { + setLoading(false); + } + }; + + const nextStep = () => setStep(s => s + 1); + const prevStep = () => setStep(s => s - 1); + + const renderProgress = () => ( +
+ {[1, 2, 3, 4].map((i) => ( +
+
= i ? 'bg-[#004B87] text-white' : 'bg-gray-200 text-gray-500'}`}> + {step > i ? : i} +
+ {i < 4 &&
i ? 'bg-[#004B87]' : 'bg-gray-200'}`} />} +
+ ))} +
+ ); + + return ( + <> + + {getPageTitle('Nytt besök')} + + + +
Steg {step} av 4
+
+ +
+ {renderProgress()} + + + {step === 1 && ( +
+
+

Dina Kontaktuppgifter

+

+ Säkerställ att vi kan nå dig vid behov. Uppgifterna hämtas från din profil men kan ändras här. +

+
+
+ + setFullName(e.target.value)} + /> + + + setPhoneNumber(e.target.value)} + /> + +
+
+ +
+
+ )} + + {step === 2 && ( +
+
+

Beskriv dina symtom

+

Förklara kortfattat vad som har hänt och hur du känner dig.

+
+ +