diff --git a/backend/api/inputs.py b/backend/api/inputs.py index 098a715..848d4d5 100644 --- a/backend/api/inputs.py +++ b/backend/api/inputs.py @@ -271,4 +271,5 @@ class ApplyTaskGraphInput: patient_id: strawberry.ID preset_id: strawberry.ID | None = None graph: TaskGraphInput | None = None + source_preset_id: strawberry.ID | None = None assign_to_current_user: bool = False diff --git a/backend/api/resolvers/task.py b/backend/api/resolvers/task.py index ae7a461..2ccee42 100644 --- a/backend/api/resolvers/task.py +++ b/backend/api/resolvers/task.py @@ -1065,6 +1065,11 @@ async def apply_task_graph( "Provide exactly one of presetId or graph", extensions={"code": "BAD_REQUEST"}, ) + if data.preset_id and data.source_preset_id is not None: + raise GraphQLError( + "sourcePresetId is only allowed when graph is provided", + extensions={"code": "BAD_REQUEST"}, + ) graph_dict: dict[str, Any] if data.preset_id: pr = await info.context.db.execute( @@ -1091,7 +1096,29 @@ async def apply_task_graph( ) validate_task_graph_dict(graph_dict) assignee_id = user.id if data.assign_to_current_user else None - source_preset_id = str(data.preset_id) if data.preset_id else None + source_preset_id: str | None + if data.preset_id: + source_preset_id = str(data.preset_id) + elif data.source_preset_id is not None: + pr_src = await info.context.db.execute( + select(models.TaskPreset).where( + models.TaskPreset.id == data.source_preset_id, + ), + ) + preset_src = pr_src.scalars().first() + if not preset_src: + raise GraphQLError( + "Preset not found", + extensions={"code": "NOT_FOUND"}, + ) + if ( + preset_src.scope == DbTaskPresetScope.PERSONAL.value + and preset_src.owner_user_id != user.id + ): + raise_forbidden() + source_preset_id = str(data.source_preset_id) + else: + source_preset_id = None return await apply_task_graph_to_patient( info.context.db, str(data.patient_id), diff --git a/backend/schema.graphql b/backend/schema.graphql index 3ca3375..822f22b 100644 --- a/backend/schema.graphql +++ b/backend/schema.graphql @@ -2,6 +2,7 @@ input ApplyTaskGraphInput { patientId: ID! presetId: ID = null graph: TaskGraphInput = null + sourcePresetId: ID = null assignToCurrentUser: Boolean! = false } diff --git a/web/api/gql/generated.ts b/web/api/gql/generated.ts index f6d0d5a..b5c3e61 100644 --- a/web/api/gql/generated.ts +++ b/web/api/gql/generated.ts @@ -24,6 +24,7 @@ export type ApplyTaskGraphInput = { graph?: InputMaybe; patientId: Scalars['ID']['input']; presetId?: InputMaybe; + sourcePresetId?: InputMaybe; }; export type AuditLogType = { diff --git a/web/components/patients/LoadTaskPresetDialog.tsx b/web/components/patients/LoadTaskPresetDialog.tsx index 4769c73..68e33dc 100644 --- a/web/components/patients/LoadTaskPresetDialog.tsx +++ b/web/components/patients/LoadTaskPresetDialog.tsx @@ -1,14 +1,19 @@ -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Button, + Checkbox, + Chip, Dialog, FocusTrapWrapper, Select, SelectOption } from '@helpwave/hightide' +import { Check, Plus, UserPlus, X } from 'lucide-react' import { useTasksTranslation } from '@/i18n/useTasksTranslation' import { useApplyTaskGraph, useTaskPresets } from '@/data' -import type { TaskPresetsQuery } from '@/api/gql/generated' +import { GetPatientDocument, type TaskPresetsQuery } from '@/api/gql/generated' +import { useSystemSuggestionTasks } from '@/context/SystemSuggestionTasksContext' +import { presetGraphToTaskGraphInput } from '@/utils/taskGraph' type PresetRow = TaskPresetsQuery['taskPresets'][number] @@ -26,25 +31,36 @@ export function LoadTaskPresetDialog({ onSuccess, }: LoadTaskPresetDialogProps) { const translation = useTasksTranslation() - const { data, loading } = useTaskPresets() + const { showToast } = useSystemSuggestionTasks() + const { data, loading, refetch: refetchPresets } = useTaskPresets() const [applyTaskGraph, { loading: applying }] = useApplyTaskGraph() - const [selectedId, setSelectedId] = useState(undefined) + const wasDialogOpenRef = useRef(false) + const [selectedId, setSelectedId] = useState(null) const [confirmOpen, setConfirmOpen] = useState(false) + const [selectedNodeIds, setSelectedNodeIds] = useState>(() => new Set()) const presets = useMemo(() => data?.taskPresets ?? [], [data?.taskPresets]) useEffect(() => { if (!isOpen) { - setSelectedId(undefined) + setSelectedId(null) + setConfirmOpen(false) } }, [isOpen]) + useEffect(() => { + if (isOpen && !wasDialogOpenRef.current) { + void refetchPresets() + } + wasDialogOpenRef.current = isOpen + }, [isOpen, refetchPresets]) + useEffect(() => { if (!isOpen || presets.length === 0) return - setSelectedId(prev => { + setSelectedId((prev) => { const first = presets[0] if (!first) return prev - return prev && presets.some(p => p.id === prev) ? prev : first.id + return prev != null && presets.some((p) => p.id === prev) ? prev : first.id }) }, [isOpen, presets]) @@ -54,27 +70,59 @@ export function LoadTaskPresetDialog({ ) const taskCount = selected?.graph.nodes.length ?? 0 + const selectedApplyCount = selectedNodeIds.size + + useEffect(() => { + if (!confirmOpen || !selected) return + setSelectedNodeIds(new Set(selected.graph.nodes.map((n) => n.id))) + }, [confirmOpen, selected]) const handlePrimary = useCallback(() => { if (!selectedId || taskCount === 0) return setConfirmOpen(true) }, [selectedId, taskCount]) - const handleConfirmApply = useCallback(async () => { - if (!selectedId) return - await applyTaskGraph({ - variables: { - data: { - patientId, - presetId: selectedId, - assignToCurrentUser: false, - }, - }, + const toggleNode = useCallback((nodeId: string) => { + setSelectedNodeIds((prev) => { + const next = new Set(prev) + if (next.has(nodeId)) next.delete(nodeId) + else next.add(nodeId) + return next }) - setConfirmOpen(false) - onClose() - onSuccess?.() - }, [applyTaskGraph, patientId, selectedId, onClose, onSuccess]) + }, []) + + const handleConfirmApply = useCallback( + async (assignToCurrentUser: boolean) => { + if (!selected) return + const graph = presetGraphToTaskGraphInput(selected.graph, selectedNodeIds) + if (!graph) return + await applyTaskGraph({ + variables: { + data: { + patientId, + graph, + sourcePresetId: selected.id, + assignToCurrentUser, + }, + }, + refetchQueries: [{ query: GetPatientDocument, variables: { id: patientId } }], + }) + showToast(translation('tasksCreatedFromPreset')) + setConfirmOpen(false) + onClose() + onSuccess?.() + }, + [ + applyTaskGraph, + patientId, + selected, + selectedNodeIds, + showToast, + translation, + onClose, + onSuccess, + ] + ) return ( <> @@ -97,8 +145,8 @@ export function LoadTaskPresetDialog({
{translation('taskPresets')}