@@ -473,8 +528,19 @@ export const PatientList = forwardRef
(({ initi
patientId={selectedPatient?.id ?? openedPatientId ?? undefined}
onClose={handleClose}
onSuccess={refetch}
+ onOpenSystemSuggestion={(suggestion, patientName) => {
+ setSuggestionModalSuggestion(suggestion)
+ setSuggestionModalPatientName(patientName)
+ setSuggestionModalOpen(true)
+ }}
/>
+
)
diff --git a/web/components/tables/TaskList.tsx b/web/components/tables/TaskList.tsx
index 72945ded..c346e058 100644
--- a/web/components/tables/TaskList.tsx
+++ b/web/components/tables/TaskList.tsx
@@ -1,6 +1,6 @@
import { useMemo, useState, forwardRef, useImperativeHandle, useEffect, useRef, useCallback } from 'react'
import { useQueryClient } from '@tanstack/react-query'
-import { Button, Checkbox, ConfirmDialog, FillerCell, HelpwaveLogo, IconButton, LoadingContainer, SearchBar, Select, SelectOption, TableColumnSwitcher, TableDisplay, TablePagination, TableProvider } from '@helpwave/hightide'
+import { Button, Checkbox, Chip, ConfirmDialog, FillerCell, HelpwaveLogo, IconButton, LoadingContainer, SearchBar, Select, SelectOption, TableColumnSwitcher, TableDisplay, TablePagination, TableProvider } from '@helpwave/hightide'
import { PlusIcon, UserCheck, Users } from 'lucide-react'
import type { TaskPriority, GetTasksQuery } from '@/api/gql/generated'
import { PropertyEntity } from '@/api/gql/generated'
@@ -44,6 +44,9 @@ export type TaskViewModel = {
assigneeTeam?: { id: string, title: string },
done: boolean,
properties?: GetTasksQuery['tasks'][0]['properties'],
+ machineGenerated?: boolean,
+ source?: 'manual' | 'systemSuggestion',
+ assignedTo?: 'me' | null,
}
export type TaskListRef = {
@@ -389,12 +392,18 @@ export const TaskList = forwardRef
(({ tasks: initial
accessorKey: 'name',
cell: ({ row }) => {
if (refreshingTaskIds.has(row.original.id)) return rowLoadingCell
+ const showSystemBadge = row.original.machineGenerated || row.original.source === 'systemSuggestion'
return (
-
+
{row.original.priority && (
)}
{row.original.name}
+ {showSystemBadge && (
+
+ System
+
+ )}
)
},
diff --git a/web/components/tasks/TaskCardView.tsx b/web/components/tasks/TaskCardView.tsx
index 1e681b60..72a722be 100644
--- a/web/components/tasks/TaskCardView.tsx
+++ b/web/components/tasks/TaskCardView.tsx
@@ -1,4 +1,4 @@
-import { Button, Checkbox } from '@helpwave/hightide'
+import { Button, Checkbox, Chip } from '@helpwave/hightide'
import { AvatarStatusComponent } from '@/components/AvatarStatusComponent'
import { Clock, User, Users, Flag } from 'lucide-react'
import clsx from 'clsx'
@@ -42,6 +42,8 @@ type FlexibleTask = {
id: string,
title: string,
} | null,
+ machineGenerated?: boolean,
+ source?: 'manual' | 'systemSuggestion',
}
type TaskCardViewProps = {
@@ -206,6 +208,11 @@ export const TaskCardView = ({ task, onToggleDone: _onToggleDone, onClick, showA
>
{taskName}
+ {((task as FlexibleTask).machineGenerated || (task as FlexibleTask).source === 'systemSuggestion') && (
+
+ System
+
+ )}
{task.assigneeTeam && (
diff --git a/web/context/SystemSuggestionTasksContext.tsx b/web/context/SystemSuggestionTasksContext.tsx
new file mode 100644
index 00000000..8d056311
--- /dev/null
+++ b/web/context/SystemSuggestionTasksContext.tsx
@@ -0,0 +1,125 @@
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+ type ReactNode,
+} from 'react'
+import type { MachineGeneratedTask } from '@/types/systemSuggestion'
+import type { SuggestedTaskItem } from '@/types/systemSuggestion'
+
+type ToastState = { message: string } | null
+
+type SystemSuggestionTasksContextValue = {
+ getCreatedTasksForPatient: (patientId: string) => MachineGeneratedTask[]
+ addCreatedTasks: (
+ patientId: string,
+ items: SuggestedTaskItem[],
+ assignToMe?: boolean
+ ) => void
+ setCreatedTaskDone: (patientId: string, taskId: string, done: boolean) => void
+ toast: ToastState
+ showToast: (message: string) => void
+ clearToast: () => void
+}
+
+const SystemSuggestionTasksContext = createContext
(null)
+
+const TOAST_DURATION_MS = 3000
+
+function generateTaskId(): string {
+ return `suggestion-created-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
+}
+
+export function SystemSuggestionTasksProvider({ children }: { children: ReactNode }) {
+ const [createdByPatientId, setCreatedByPatientId] = useState>({})
+ const [toast, setToast] = useState(null)
+
+ const getCreatedTasksForPatient = useCallback((patientId: string): MachineGeneratedTask[] => {
+ return createdByPatientId[patientId] ?? []
+ }, [createdByPatientId])
+
+ const addCreatedTasks = useCallback(
+ (patientId: string, items: SuggestedTaskItem[], assignToMe?: boolean) => {
+ const now = new Date()
+ const newTasks: MachineGeneratedTask[] = items.map((item) => ({
+ id: generateTaskId(),
+ title: item.title,
+ description: item.description ?? null,
+ done: false,
+ patientId,
+ machineGenerated: true,
+ source: 'systemSuggestion',
+ assignedTo: assignToMe ? 'me' : null,
+ updateDate: now,
+ dueDate: null,
+ priority: null,
+ estimatedTime: null,
+ }))
+ setCreatedByPatientId((prev) => {
+ const existing = prev[patientId] ?? []
+ return { ...prev, [patientId]: [...existing, ...newTasks] }
+ })
+ },
+ []
+ )
+
+ const setCreatedTaskDone = useCallback((patientId: string, taskId: string, done: boolean) => {
+ setCreatedByPatientId((prev) => {
+ const list = prev[patientId] ?? []
+ const next = list.map((t) => (t.id === taskId ? { ...t, done } : t))
+ return { ...prev, [patientId]: next }
+ })
+ }, [])
+
+ const showToast = useCallback((message: string) => {
+ setToast({ message })
+ }, [])
+
+ const clearToast = useCallback(() => {
+ setToast(null)
+ }, [])
+
+ useEffect(() => {
+ if (!toast) return
+ const t = setTimeout(() => setToast(null), TOAST_DURATION_MS)
+ return () => clearTimeout(t)
+ }, [toast])
+
+ const value = useMemo(
+ () => ({
+ getCreatedTasksForPatient,
+ addCreatedTasks,
+ setCreatedTaskDone,
+ toast,
+ showToast,
+ clearToast,
+ }),
+ [getCreatedTasksForPatient, addCreatedTasks, setCreatedTaskDone, toast, showToast, clearToast]
+ )
+
+ return (
+
+ {children}
+
+ )
+}
+
+export function useSystemSuggestionTasks(): SystemSuggestionTasksContextValue {
+ const ctx = useContext(SystemSuggestionTasksContext)
+ if (!ctx) {
+ throw new Error('useSystemSuggestionTasks must be used within SystemSuggestionTasksProvider')
+ }
+ return ctx
+}
+
+export function useSystemSuggestionTasksOptional(): SystemSuggestionTasksContextValue | null {
+ return useContext(SystemSuggestionTasksContext)
+}
+
+export function useCreatedTasksForPatient(patientId: string): MachineGeneratedTask[] {
+ const ctx = useSystemSuggestionTasksOptional()
+ return ctx ? ctx.getCreatedTasksForPatient(patientId) : []
+}
diff --git a/web/data/mockPatients.ts b/web/data/mockPatients.ts
new file mode 100644
index 00000000..4971daef
--- /dev/null
+++ b/web/data/mockPatients.ts
@@ -0,0 +1,91 @@
+import { PatientState, Sex } from '@/api/gql/generated'
+import type { PatientViewModel } from '@/components/tables/PatientList'
+
+export const MOCK_PATIENT_A_ID = 'mock-patient-a'
+export const MOCK_PATIENT_B_ID = 'mock-patient-b'
+export const MOCK_PATIENT_C_ID = 'mock-patient-c'
+export const MOCK_PATIENT_D_ID = 'mock-patient-d'
+export const MOCK_PATIENT_E_ID = 'mock-patient-e'
+
+const mockBirthdateA = new Date(1965, 2, 15)
+const mockBirthdateB = new Date(1970, 8, 1)
+const mockBirthdateC = new Date(1980, 5, 20)
+const mockBirthdateD = new Date(1975, 11, 8)
+const mockBirthdateE = new Date(1988, 1, 14)
+
+export const MOCK_PATIENT_A: PatientViewModel = {
+ id: MOCK_PATIENT_A_ID,
+ name: 'Patient A',
+ firstname: 'Patient',
+ lastname: 'A',
+ position: null,
+ openTasksCount: 0,
+ closedTasksCount: 0,
+ birthdate: mockBirthdateA,
+ sex: Sex.Male,
+ state: PatientState.Admitted,
+ tasks: [],
+ properties: [],
+}
+
+export const MOCK_PATIENT_B: PatientViewModel = {
+ id: MOCK_PATIENT_B_ID,
+ name: 'Patient B',
+ firstname: 'Patient',
+ lastname: 'B',
+ position: null,
+ openTasksCount: 0,
+ closedTasksCount: 0,
+ birthdate: mockBirthdateB,
+ sex: Sex.Female,
+ state: PatientState.Admitted,
+ tasks: [],
+ properties: [],
+}
+
+export const MOCK_PATIENT_C: PatientViewModel = {
+ id: MOCK_PATIENT_C_ID,
+ name: 'Patient C',
+ firstname: 'Patient',
+ lastname: 'C',
+ position: null,
+ openTasksCount: 0,
+ closedTasksCount: 0,
+ birthdate: mockBirthdateC,
+ sex: Sex.Female,
+ state: PatientState.Admitted,
+ tasks: [],
+ properties: [],
+}
+
+export const MOCK_PATIENT_D: PatientViewModel = {
+ id: MOCK_PATIENT_D_ID,
+ name: 'Patient D',
+ firstname: 'Patient',
+ lastname: 'D',
+ position: null,
+ openTasksCount: 0,
+ closedTasksCount: 0,
+ birthdate: mockBirthdateD,
+ sex: Sex.Male,
+ state: PatientState.Admitted,
+ tasks: [],
+ properties: [],
+}
+
+export const MOCK_PATIENT_E: PatientViewModel = {
+ id: MOCK_PATIENT_E_ID,
+ name: 'Patient E',
+ firstname: 'Patient',
+ lastname: 'E',
+ position: null,
+ openTasksCount: 0,
+ closedTasksCount: 0,
+ birthdate: mockBirthdateE,
+ sex: Sex.Male,
+ state: PatientState.Admitted,
+ tasks: [],
+ properties: [],
+}
+
+export const MOCK_PATIENTS: PatientViewModel[] = [MOCK_PATIENT_A, MOCK_PATIENT_B, MOCK_PATIENT_C, MOCK_PATIENT_D, MOCK_PATIENT_E]
diff --git a/web/data/mockSystemSuggestions.ts b/web/data/mockSystemSuggestions.ts
new file mode 100644
index 00000000..867b55a6
--- /dev/null
+++ b/web/data/mockSystemSuggestions.ts
@@ -0,0 +1,70 @@
+import type { GuidelineAdherenceStatus, SystemSuggestion } from '@/types/systemSuggestion'
+import { MOCK_PATIENT_A_ID, MOCK_PATIENT_B_ID, MOCK_PATIENT_C_ID, MOCK_PATIENT_D_ID, MOCK_PATIENT_E_ID } from '@/data/mockPatients'
+
+export const MOCK_SUGGESTION_FOR_PATIENT_A: SystemSuggestion = {
+ id: 'mock-suggestion-a',
+ patientId: MOCK_PATIENT_A_ID,
+ adherenceStatus: 'non_adherent',
+ reasonSummary: 'Current treatment plan does not align with guideline recommendations for this condition. Recommended tasks address screening and follow-up intervals that have been missed.',
+ suggestedTasks: [
+ { id: 'sug-1', title: 'Schedule guideline-recommended screening', description: 'Book lab and imaging per protocol.' },
+ { id: 'sug-2', title: 'Document treatment rationale', description: 'Record clinical reasoning for any deviation from guidelines.' },
+ { id: 'sug-3', title: 'Plan follow-up within 4 weeks', description: 'Set reminder for next review date.' },
+ ],
+ explanation: {
+ details: 'The recommendation is shown because de-facto treatment of this patient is not adherent with the de-jure models. The suggested tasks are derived from evidence-based protocols to improve adherence and outcomes.',
+ references: [
+ { title: 'Clinical guideline (PDF)', url: 'https://example.com/guideline.pdf' },
+ { title: 'Supporting literature', url: 'https://example.com/literature' },
+ ],
+ },
+ createdAt: new Date().toISOString(),
+}
+
+export const MOCK_SUGGESTION_FOR_PATIENT_E: SystemSuggestion = {
+ id: 'mock-suggestion-e',
+ patientId: MOCK_PATIENT_E_ID,
+ adherenceStatus: 'adherent',
+ reasonSummary: 'Guideline targets are met. Optional follow-up tasks may help maintain adherence and document progress.',
+ suggestedTasks: [
+ { id: 'sug-e1', title: 'Optional: Schedule next routine review', description: 'Book follow-up within 6 months.' },
+ { id: 'sug-e2', title: 'Optional: Update care plan summary', description: 'Keep documentation in sync with current status.' },
+ ],
+ explanation: {
+ details: 'The recommendation is shown because de-facto treatment is not fully aligned with de-jure models. The suggested tasks are derived as optional improvements to support ongoing adherence and documentation.',
+ references: [
+ { title: 'Follow-up protocol', url: 'https://example.com/follow-up.pdf' },
+ ],
+ },
+ createdAt: new Date().toISOString(),
+}
+
+const adherenceByPatientId: Record = {
+ [MOCK_PATIENT_A_ID]: 'non_adherent',
+ [MOCK_PATIENT_B_ID]: 'adherent',
+ [MOCK_PATIENT_C_ID]: 'adherent',
+ [MOCK_PATIENT_D_ID]: 'adherent',
+ [MOCK_PATIENT_E_ID]: 'adherent',
+}
+
+const suggestionByPatientId: Record = {
+ [MOCK_PATIENT_A_ID]: MOCK_SUGGESTION_FOR_PATIENT_A,
+ [MOCK_PATIENT_E_ID]: MOCK_SUGGESTION_FOR_PATIENT_E,
+}
+
+export function getAdherenceByPatientId(patientId: string): GuidelineAdherenceStatus {
+ return adherenceByPatientId[patientId] ?? 'unknown'
+}
+
+export function getSuggestionByPatientId(patientId: string): SystemSuggestion | null {
+ return suggestionByPatientId[patientId] ?? null
+}
+
+export const DUMMY_SUGGESTION: SystemSuggestion = {
+ id: 'dummy',
+ patientId: '',
+ adherenceStatus: 'unknown',
+ reasonSummary: '',
+ suggestedTasks: [],
+ explanation: { details: '', references: [] },
+}
diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx
index b2195b1e..e288a164 100644
--- a/web/pages/_app.tsx
+++ b/web/pages/_app.tsx
@@ -14,6 +14,8 @@ import { TasksContextProvider } from '@/hooks/useTasksContext'
import { ApolloProviderWithData } from '@/providers/ApolloProviderWithData'
import { ConflictProvider } from '@/providers/ConflictProvider'
import { SubscriptionProvider } from '@/providers/SubscriptionProvider'
+import { SystemSuggestionTasksProvider } from '@/context/SystemSuggestionTasksContext'
+import { FeedbackToast } from '@/components/FeedbackToast'
import { InstallPrompt } from '@/components/pwa/InstallPrompt'
import { registerServiceWorker, requestNotificationPermission } from '@/utils/pushNotifications'
import { useEffect } from 'react'
@@ -77,10 +79,13 @@ function MyApp({
-
-
-
-
+
+
+
+
+
+
+
diff --git a/web/types/systemSuggestion.ts b/web/types/systemSuggestion.ts
new file mode 100644
index 00000000..f9816650
--- /dev/null
+++ b/web/types/systemSuggestion.ts
@@ -0,0 +1,39 @@
+export type GuidelineAdherenceStatus = 'adherent' | 'non_adherent' | 'unknown'
+
+export type SuggestedTaskItem = {
+ id: string
+ title: string
+ description?: string
+}
+
+export type SystemSuggestionExplanation = {
+ details: string
+ references: Array<{ title: string; url: string }>
+}
+
+export type SystemSuggestion = {
+ id: string
+ patientId: string
+ adherenceStatus: GuidelineAdherenceStatus
+ reasonSummary: string
+ suggestedTasks: SuggestedTaskItem[]
+ explanation: SystemSuggestionExplanation
+ createdAt?: string
+}
+
+export type TaskSource = 'manual' | 'systemSuggestion'
+
+export type MachineGeneratedTask = {
+ id: string
+ title: string
+ description?: string | null
+ done: boolean
+ patientId: string
+ machineGenerated: true
+ source: 'systemSuggestion'
+ assignedTo?: 'me' | null
+ updateDate: Date
+ dueDate?: Date | null
+ priority?: string | null
+ estimatedTime?: number | null
+}